A customizable, easy-to-use calendar with range selection
This library is available on Jitpack.
Add it in your root build.gradle:
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
Add the dependency:
implementation 'com.github.pelmenstar1:RangeCalendar:0.9.4'
Define RangeCalendarView
in your XML layout:
<com.github.pelmenstar1.rangecalendar.RangeCalendarView
android:id="@+id/picker"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
Get notified when the date or range are selected:
onSelectionListener = object : RangeCalendarView.OnSelectionListener {
override fun onSelectionCleared() {
}
override fun onSelection(
startYear: Int,
startMonth: Int,
startDay: Int,
endYear: Int,
endMonth: Int,
endDay: Int
) {
}
}
It's a view that will be shown when user selects date or range.
Visual example:
To keep information on that view relevant, use getOnSelectionListener()/setOnSelectionListener()
getSelectionView()/setSelectionView()
to assign or get the view.getSelectionViewTransitionDuration()/setSelectionViewTransitionDuration()
to assign or get duration (in milliseconds) of selection view show/hide animationgetSelectionViewTransitionInterpolator()/setSelectionViewTransitionInterpolator()
to assign or get time interpolator of selection show/hide animationgetSelectionViewLayoutParams()/getSelectionViewLayoutParams()
to assign or get layout params for the viewhasSelectionViewClearButton()/setHasSelectionViewClearButton()
to get or set whether on selection (if selection view is not null) 'next month' button will become 'clear selection' button.
getMinDate()/getMaxDate()
to get minimum and maximum dates respectively.setMinDate()/setMaxDate()
to set minimum and maximum dates respectively.
Use getOnSelectionListener()/setOnSelectionListener()
to get or set the listener. In all methods, month and day of
month are 1-based.
onSelectionCleared()
fires when selection is cleared.onSelection(startYear, startMonth, startDay, endYear, endMonth, endDay)
fires on selection. Start and end dates are inclusive.
They are enabled by default (currently they cannot be disabled).
getCommonAnimationDuration()/setCommonAnimationDuration()
to get or set duration (in milliseconds) of common animationsgetCommonAnimationInterpolator()/setCommonAnimationInterpolator()
to get or set time interpolator of common animations. By default, it's linear.getHoverAnimationDuration()/setHoverAnimationDuration()
to get or set duration (in milliseconds) of hover animationgetHoverAnimationInterpolator()/setHoverAnimationInterpolator()
to get or set time interpolator of hover animation. By default, it's linear.getCellAnimationType()/setCellAnimationType()
to get or set type of animation for cells. SeeCellAnimationType
.
moveToPreviousMonth(withAnimation=true)
to slide to previous month (if it's possible) with animation or not (it's controlled bywithAnimation
flag)moveToNextMonth(withAnimation=true)
to slide to next month (if it's possible) with animation or not (it's controlled bywithAnimation
flag)setYearAndMonth(year, month, withAnimation=true)
to slide to given year & month with animation or not (it's controlled bywithAnimation
flag). If given page is out of enabled range, selected page will remain the same
If you want to draw the selection in other way than the library does, you can implement SelectionManager
on your own.
There are two main abstractions in selection management:
- 'selection state' - saves type of selection, its range (rangeStart and rangeEnd) and other data required to draw it on canvas.
- 'selection manager' - responsible for creating selection state and transitions between them. The manager is expected to be stateless, except caching instances of
renderer
andtransitionController
- 'selection renderer' - responsible for rendering the selection state on
Canvas
. The implementation is not expected to be stateful, but it's acceptable to cache some information in order to make the rendering faster. - 'selection transition controller' - responsible for mutating
SelectionState.Transition
internal data to make a transition.
There's a description of methods of SelectionManager
and what they are expected to do:
createState(rangeStart, rangeEnd, measureManager)
- creates a new selection state. Note that, rangeEnd is inclusive. measureManager can be used to determine bounds of a cell and other info.updateConfiguration(state, measureManager)
- updates internal measurements and computation based on measureManager results of both previousState and currentState. Change of measureManager result means that cells might be moved or resized.createTransition(previousState, currentState, measureManager, options)
- creates a transitive state between previousState and currentState
Selection renderer is responsible for drawing simple selection state or transitive state, that is created to save information about transition between two selection states. When selection is to be drawn, the canvas' matrix is translated in such way that coordinates will be relative to the grid's leftmost point on top.
To use the custom implementation of SelectionManager
is the calendar view,
use RangeCalendarView.setSelectionManager()
To use default implementation, pass null
to setSelectionManager().
There's also a CellMeasureManager
class which returns position and size of specified cell. It's passed as an argument
to some methods of SelectionManager
. Although it's a public interface and it can be implemented on your own, you
cannot use your implementation in a calendar view. It's implemented inside the library.
The library allows to customize detecting gestures based on the MotionEvent
s.
You can do this by specifying custom implementation of RangeCalendarGestureDetectorFactory
that creates your implementation of RangeCalendarGestureDetector
class DetectorImpl : RangeCalendarGestureDetector() {
override fun processEvent(event: MotionEvent): Boolean {
// ... your code
}
}
object DetectorImplFactory : RangeCalendarGestureDetectorFactory<DetectorImpl> {
val detectorClass: Class<DetectorImpl>
get() = DetectorImpl::class.java
override fun create(): DetectorImpl {
return DetectorImpl()
}
}
rangeCalendar.gestureDetectorFactory = DetectorImplFactory
The gesture detection is based on detecting specific gesture types that can be defined for your implementation:
class MyTypeConfiguration(val configValue: Float)
object DetectorImplGestureTypes {
// Ordinal number is the thing that defines the gesture type. It's used for equality, hashing and comparing.
val myType = RangeCalendarGestureType<MyTypeConfiguration>(ordinal = 0, displayName = "myType")
}
Each gesture type is associated with some type of options that may be needed in your implementation.
To create the configuration you can use builder method:
rangeCalendar.gestureConfiguration = RangeCalendarGestureConfiguration {
enabledGestureTypes = setOf(DetectorImplGestureTypes.myType)
gestureTypeOptions {
put(DetectorImplGestureTypes.myType, MyTypeConfiguration(configValue = 1f))
}
}
The library provides default gesture detector that detects these gestures:
- single tap to select a cell (
RangeCalendarDefaultGestureTypes.singleTapCell
) - double tap to select a week (
RangeCalendarDefaultGestureTypes.doubleTapWeek
) - long press to start selecting custom range and then move one pointer to specify the range (
RangeCalendarDefaultGestureTypes.longPressRange
) - long press to start selecting custom range and then move two pointers to specify the range (
RangeCalendarDefaultGestureTypes.longPressTwoPointersRange
) - horizontal pinch to select week (
RangeCalendarDefaultGestureTypes.horizontalPinchWeek
) - diagonal pinch to select month (
RangeCalendarDefaultGestureTypes.diagonalPinchMonth
)
The configuration of gesture detector can be changed when the detector is default:
rangeCalendar.gestureConfiguration = RangeCalendarGestureConfiguration {
enabledGestureTypes {
doubleTapWeek()
horizontalPinchWeek()
// other types are disabled
}
gestureTypeOptions {
// horizontalPinchWeek is associated with PinchConfiguration
put(
RangeCalendarDefaultGestureTypes.horizontalPinchWeek,
PinchConfiguration(
// 10 degrees
angleDeviation = 10f * (180f / PI.toFloat()),
minDistance = Distance.Relative(fraction = 0.5f, anchor = Distance.RelativeAnchor.WIDTH)
)
)
}
}
getTimeZone()/setTimeZone()
to get or set calendar's time zone. By default, it's default system time zone (TimeZone.getDefault()
). Calendar's time zone affects to "today" cell recognition. When new time zone is set, " today" cell is updated.
This can be done manually by registering an BroadcastReceiver
and updating time zone via setTimeZone()
or notifying about today's date change via notifyTodayChanged()
.
Or RangeCalendarConfigObserver
can be used that, basically, does the same thing. The class is also lifecycle-aware.
Example:
val observer = RangeCalendarConfigObserver(rangeCalendar).apply {
observeDateChanges = false // if we don't want rangeCalendar to be notified about these changes.
observeTimeZoneChanges = true // by default, it's true. But can also be disabled.
}
observer.setLifecycle(lifecycle) // If we want to unregister the observer on destroy.
observer.register() // The observer should be registered manually.
By default, the calendar will use localized weekdays in 'short' format. The format of localized weekdays can be changed via weekdayType
. Currently there's only two options:
WeekdayType.SHORT
. If user locale is English, weekdays will look like: Mo, Tu, We, Th, Fr, Sa, SuWeekdayType.NARROW
. If user locale is English, weekdays will look like: M, T, W, T, F, S, S.
If you want to change weekdays, it can be done via weekdays
:
rangeCalendarView.weekdays = arrayOf("0", "1", "2", "3", "4", "5", "6")
If you want back to using localized weekdays, pass null to weekdays
.
By default, information about the first day of the week is extracted from current locale's data. If the locale is changed, the first day of the week is re-computed.
If you want to use custom the first day of the week, use can change it via firstDayOfWeek
or intFirstDayOfWeek
:
rangeCalendarView.firstDayOfWeek = java.time.DayOfWeek.SATURDAY
// or
rangeCalendarView.intFirstDayOfWeek = java.util.Calendar.SATURDAY
Changing the first day of the week via these properties makes the first day of the week fixed, i.e it will be no longer updated on configuration updates. It also clears the selection if present. NOTE: if values of the first day of the week on the time of saving view's state and restoring it are different, the saved selection is not restored.
Info textview is in red rectangle:
Information about year and month of selected page is on that textview. The text can be changed by specifying custom infoFormatter
:
rangeCalendar.infoFormatter = object : RangeCalendarView.InfoFormatter {
override fun format(year: Int, month: Int): CharSequence {
return "Year: $year Month: $month"
}
}
If your implementation depends on current configuration's locale, use RangeCalendarView.LocalizedInfoFormatter
.
If you want to use default localized implementation with custom datetime pattern, use infoPattern
. Note: by default, specified format will be additionally processed by android.text.format.DateFormat.getBestDateTimePattern
to find most suitable pattern for current locale. If it's undesirable, set useBestPatternForInfoPattern
to false
.
useBestPatternForInfoPattern
property depends on android.text.format.DateFormat.getBestDateTimePattern
that is available from API level 18. On older versions, this property changes nothing.
You can directly access info textview via infoTextView
property.
getVibrateOnSelectingCustomRange()/setVibrateOnSelectingCustomRange()
to get or set whether the device should vibrate on start of selecting custom range.getClickOnCellSelectionBehavior()/setClickOnCellSelectionBehavior()
to get or set behaviour when user (note, not from code) clicks on already selected cell. Use constants fromClickOnCellSelectionBehavior
:NONE
- nothing happensCLEAR
- selection clears
Attribute | Description |
---|---|
rangeCalendar_selectionColor | Color of background of selection shape |
rangeCalendar_dayNumberTextSize | Text size of text in cells (day number) |
rangeCalendar_inMonthDayNumberColor | Color of day number which is in selected month range |
rangeCalendar_outMonthDayNumberColor | Color of day number which is out of selected month range |
rangeCalendar_disabledMonthDayNumberColor | Color of day number which is out of enabled range |
rangeCalendar_todayColor | Color of day number which represents today |
rangeCalendar_weekdayColor | Color of text in weekday row (Mon, Tue, Wed...) |
rangeCalendar_weekdayTextSize | Size of text in weekday row (Mon, Tue, Wed...) |
rangeCalendar_hoverAlpha | Specifies alpha channel value of a black color that is drawn under the cell when the cell is in hovered state |
rangeCalendar_cellSize | Size of cell |
rangeCalendar_cellWidth | Width of cell. This value takes precedence over rangeCalendar_cellSize |
rangeCalendar_cellHeight | Height of cell. This value takes precedence over rangeCalendar_cellSize |
rangeCalendar_weekdayType | Type of weekday. |
rangeCalendar_weekdays | Custom weekdays. The value should be a string array, whose length is 7. |
rangeCalendar_clickOnCellSelectionBehavior | Specifies behaviour when user clicks on already selected cell. It can be one of these values:
|
rangeCalendar_cellRoundRadius | Round radius of the cell. By default it's positive infinity which means the shape is circle regardless the size of it. |
rangeCalendar_selectionFillGradientBoundsType | Specifies the way of determining bounds of selection. It only matters when selection fill is gradient-like. It can be one of these values:
|
rangeCalendar_cellAnimationType | Specifies type of animation for cell. It can be one of these values:
|
rangeCalendar_showAdjacentMonths | Specifies whether to show adjacent months on the calendar page. By default, it's true |
rangeCalendar_vibrateOnSelectingCustomRange | Specifies whether the device should vibrate on start of selecting custom range |
rangeCalendar_isSelectionAnimatedByDefault | Specifies whether selection animations is enabled by default. There's some cases when it can't really be controlled to animate selection or not, for example, selection by user. This property specifies whether to animate selection in such situations. |
rangeCalendar_isHoverAnimationEnabled | Specifies whether hover animations is enabled. |
rangeCalendar_infoPattern | Specifies date-time pattern for info text view (where current year and month is shown). The pattern should be suitable with java.text.SimpleDateFormat . |
rangeCalendar_useBestPatternForInfoPattern | Specifies whether android.text.format.DateFormat.getBestDateTimePattern should be called on patterns set via infoPattern using the code or specified rangeCalendar_infoPattern in XML. By default, it's true . |
rangeCalendar_infoTextSize | Text size of info text view |
rangeCalendar_firstDayOfWeek | Specifies custom first day of the week |
The library is not production-ready and its public API shape can change from version to version. If you notice any kind of bug or something unexpected, please file an issue.
MIT License
Copyright (c) 2022 Khmaruk Oleg
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.