diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 1a26534..0516aee 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -15,6 +15,7 @@
+
diff --git a/library/src/main/java/com/github/pelmenstar1/rangecalendar/CalendarToolbarManager.kt b/library/src/main/java/com/github/pelmenstar1/rangecalendar/CalendarToolbarManager.kt
new file mode 100644
index 0000000..3d3ed2b
--- /dev/null
+++ b/library/src/main/java/com/github/pelmenstar1/rangecalendar/CalendarToolbarManager.kt
@@ -0,0 +1,272 @@
+package com.github.pelmenstar1.rangecalendar
+
+import android.animation.TimeInterpolator
+import android.animation.ValueAnimator
+import android.content.Context
+import android.content.res.ColorStateList
+import android.view.View
+import android.widget.ImageButton
+import android.widget.TextView
+import androidx.appcompat.widget.AppCompatImageButton
+import kotlin.math.abs
+
+internal class CalendarToolbarManager(
+ private val context: Context,
+ iconColor: ColorStateList,
+ private val prevButton: AppCompatImageButton,
+ private val nextButton: AppCompatImageButton,
+ private val infoView: TextView
+) {
+ private var svAnimator: ValueAnimator? = null
+
+ private var isSvTransitionForward = false
+
+ private var isSvOnScreen = false
+
+ private val prevIcon: MoveButtonDrawable
+ private val nextIcon: MoveButtonDrawable
+
+ var prevMonthDescription: CharSequence = ""
+ set(value) {
+ field = value
+ prevButton.contentDescription = value
+ }
+
+ var nextMonthDescription: CharSequence = ""
+ set(value) {
+ field = value
+
+ if (!isNextButtonActClear) {
+ nextButton.contentDescription = value
+ }
+ }
+
+ var clearSelectionDescription: CharSequence = ""
+ set(value) {
+ field = value
+
+ if (isNextButtonActClear) {
+ nextButton.contentDescription = value
+ }
+ }
+
+ var selectionView: View? = null
+ var selectionViewTransitionDuration = DEFAULT_SV_TRANSITION_DURATION
+
+ var selectionViewTransitionInterpolator: TimeInterpolator = LINEAR_INTERPOLATOR
+ var selectionViewLayoutParams = CalendarSelectionViewLayoutParams.DEFAULT
+ var hasSelectionViewClearButton = true
+ set(value) {
+ field = value
+
+ // If the selection view is already on screen, then update values to match
+ // the contract of hasSelectionViewClearButton. There's no animation because
+ // it's very unlikely scenario.
+ if (isSvOnScreen) {
+ if (value) {
+ prevIcon.setAnimationFraction(0f)
+ nextIcon.setAnimationFraction(1f)
+
+ prevButton.visibility = View.GONE
+ } else {
+ prevIcon.setAnimationFraction(1f)
+ nextIcon.setAnimationFraction(0f)
+
+ prevButton.visibility = View.VISIBLE
+ }
+ }
+ }
+
+ val isNextButtonActClear: Boolean
+ get() = isSvOnScreen && hasSelectionViewClearButton
+
+ init {
+ prevIcon = MoveButtonDrawable(
+ context,
+ iconColor,
+ MoveButtonDrawable.DIRECTION_LEFT,
+ MoveButtonDrawable.ANIM_TYPE_VOID_TO_ARROW
+ ).apply {
+ // The end state of MoveButtonDrawable when animation type is ANIM_TYPE_VOID_TO_ARROW is the arrow.
+ // That's what we need.
+ setAnimationFraction(1f)
+ setStateChangeDuration(STATE_CHANGE_DURATION)
+ }
+
+ nextIcon = MoveButtonDrawable(
+ context,
+ iconColor,
+ MoveButtonDrawable.DIRECTION_RIGHT,
+ MoveButtonDrawable.ANIM_TYPE_ARROW_TO_CROSS
+ ).apply {
+ setStateChangeDuration(STATE_CHANGE_DURATION)
+ }
+
+ prevButton.setImageDrawable(prevIcon)
+ nextButton.setImageDrawable(nextIcon)
+
+ // To update content descriptions of buttons
+ onLocaleChanged()
+ }
+
+ fun onPageScrolled(fraction: Float) {
+ // 2 * |0.5 - x| is a function that increases from 0 to 1 on [0; 0.5) and
+ // decreases from 1 to 0 on (0.5; 1].
+ // Alpha of drawable is an integer from 0 to 255, so the value should be also multiplied by 255
+ // and converted to int.
+ val alpha = (255f * 2f * abs(0.5f - fraction)).toInt()
+ setButtonAlphaIfEnabled(prevButton, alpha)
+
+ if (!isNextButtonActClear) {
+ setButtonAlphaIfEnabled(nextButton, alpha)
+ }
+ }
+
+ fun restoreButtonsAlpha() {
+ setButtonAlphaIfEnabled(prevButton, alpha = 255)
+ setButtonAlphaIfEnabled(nextButton, alpha = 255)
+ }
+
+ fun onLocaleChanged() {
+ context.resources.apply {
+ prevMonthDescription = getText(R.string.previousMonthDescription)
+ nextMonthDescription = getText(R.string.nextMonthDescription)
+ clearSelectionDescription = getText(R.string.clearSelectionDescription)
+ }
+ }
+
+ fun onSelection() {
+ startSelectionViewTransition(forward = true)
+ }
+
+ fun onSelectionCleared() {
+ startSelectionViewTransition(forward = false)
+ }
+
+ /**
+ * Hides selection view from the RangeCalendarView. The method expects that [selectionView] is not null.
+ */
+ fun hideSelectionView() {
+ if (hasSelectionViewClearButton) {
+ prevIcon.setAnimationFraction(1f)
+ nextIcon.setAnimationFraction(0f)
+ }
+
+ infoView.translationY = 0f
+ setSelectionViewOnScreen(state = false, duringAnimation = false)
+ }
+
+ private fun startSelectionViewTransition(forward: Boolean) {
+ if (selectionView == null) {
+ return
+ }
+
+ var animator = svAnimator
+
+ // Don't continue if we want to show selection view and it's already shown and vise versa,
+ // but continue if animation is currently running and direction of current animation is not equals to new one.
+ if (animator != null &&
+ (!animator.isRunning || isSvTransitionForward == forward) &&
+ forward == isSvOnScreen
+ ) {
+ return
+ }
+
+ if (animator == null) {
+ animator = AnimationHelper.createFractionAnimator(::onSvTransitionTick)
+ }
+
+ isSvTransitionForward = forward
+
+ var startPlaytime = 0L
+ if (animator.isRunning) {
+ startPlaytime = animator.currentPlayTime
+ animator.end()
+ }
+
+ animator.interpolator = selectionViewTransitionInterpolator
+ animator.duration = selectionViewTransitionDuration
+
+ // ValueAnimator.setCurrentFraction() could be used, but it's available only from API >= 22,
+ animator.currentPlayTime = startPlaytime
+
+ if (forward) {
+ animator.start()
+ } else {
+ animator.reverse()
+ }
+ }
+
+ private fun onSvTransitionTick(fraction: Float) {
+ // The buttons are only animated when next button acts like 'clear selection' button on selection.
+ if (hasSelectionViewClearButton) {
+ prevIcon.setAnimationFraction(1f - fraction)
+ nextIcon.setAnimationFraction(fraction)
+ }
+
+ if (fraction < 0.5f) {
+ // When fraction < 0.5, the info textview should be moved from the initial position
+ // to the top until it's completely invisible. So the fraction is scaled from [0; 0.5] to [0; 1]
+ // and than negated in order to make translationY negative as well.
+ val f = fraction * -2f
+ infoView.translationY = infoView.bottom * f
+
+ setSelectionViewOnScreen(state = false, duringAnimation = true)
+ } else {
+ val sv = selectionView!!
+
+ // Now that the info textview is invisible, the selection view should be moved from the top where
+ // it's completely invisible to the final position.
+ //
+ // 2 - 2x is a function that decreases from 1 to 0 on [0.5; 1]
+ // It should also be negated in order to achieve the animation from the top to the final position.
+ // In result: 2x - 2 that multiplied by sv.bottom
+ val f = 2f * fraction - 2f
+ sv.translationY = f * sv.bottom
+
+ setSelectionViewOnScreen(state = true, duringAnimation = true)
+ }
+ }
+
+ private fun setSelectionViewOnScreen(state: Boolean, duringAnimation: Boolean) {
+ if (isSvOnScreen == state) {
+ return
+ }
+
+ isSvOnScreen = state
+ val sv = selectionView!!
+ val hasClearButton = hasSelectionViewClearButton
+
+ if (state) {
+ sv.visibility = View.VISIBLE
+ infoView.visibility = View.INVISIBLE
+
+ if (hasClearButton) {
+ if (!duringAnimation) {
+ prevButton.visibility = View.GONE
+ }
+
+ nextButton.contentDescription = clearSelectionDescription
+ }
+ } else {
+ sv.visibility = View.INVISIBLE
+ infoView.visibility = View.VISIBLE
+
+ if (hasClearButton) {
+ prevButton.visibility = View.VISIBLE
+ nextButton.contentDescription = nextMonthDescription
+ }
+ }
+ }
+
+ companion object {
+ private const val DEFAULT_SV_TRANSITION_DURATION = 300L
+ private const val STATE_CHANGE_DURATION = 300L
+
+ private fun setButtonAlphaIfEnabled(button: ImageButton, alpha: Int) {
+ if (button.isEnabled) {
+ button.drawable?.alpha = alpha
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/library/src/main/java/com/github/pelmenstar1/rangecalendar/RangeCalendarView.kt b/library/src/main/java/com/github/pelmenstar1/rangecalendar/RangeCalendarView.kt
index a8a69c6..23de57b 100644
--- a/library/src/main/java/com/github/pelmenstar1/rangecalendar/RangeCalendarView.kt
+++ b/library/src/main/java/com/github/pelmenstar1/rangecalendar/RangeCalendarView.kt
@@ -1,9 +1,6 @@
package com.github.pelmenstar1.rangecalendar
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
import android.animation.TimeInterpolator
-import android.animation.ValueAnimator
import android.content.Context
import android.content.res.Configuration
import android.content.res.TypedArray
@@ -13,6 +10,7 @@ import android.os.Parcelable
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
+import android.view.View.OnClickListener
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageButton
@@ -38,7 +36,6 @@ import java.time.LocalDate
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
-import kotlin.math.abs
import kotlin.math.max
/**
@@ -358,29 +355,12 @@ class RangeCalendarView @JvmOverloads constructor(
private val hPadding: Int
private val topContainerMarginBottom: Int
- private var _selectionView: View? = null
- private var svAnimator: ValueAnimator? = null
-
- // valid while svAnimator is running
- private var isSvTransitionForward = false
-
- var selectionViewTransitionInterpolator: TimeInterpolator = LINEAR_INTERPOLATOR
-
- private var svTransitionDuration = SV_TRANSITION_DURATION
- private var _hasSvClearButton = true
- private var isSelectionViewOnScreen = false
- private var svLayoutParams = CalendarSelectionViewLayoutParams.DEFAULT
-
- private val prevIcon: MoveButtonDrawable
- private val nextIcon: MoveButtonDrawable
+ private val toolbarManager: CalendarToolbarManager
private val dateFormatter: CompatDateFormatter
private var isFirstDaySunday = false
private var currentLocale: Locale? = null
- private var nextMonthDescription: CharSequence
- private var clearSelectionDescription: CharSequence
-
private val layoutRect = Rect()
private val layoutOutRect = Rect()
@@ -392,8 +372,6 @@ class RangeCalendarView @JvmOverloads constructor(
res.getDimensionPixelOffset(R.dimen.rangeCalendar_topContainerMarginBottom)
dateFormatter = CompatDateFormatter(context, DATE_FORMAT)
- initLocaleDependentValues()
-
val selectableBg = context.getSelectableItemBackground()
val cr = CalendarResources(context)
@@ -404,150 +382,23 @@ class RangeCalendarView @JvmOverloads constructor(
val today = PackedDate.today(currentTimeZone)
- adapter = RangeCalendarPagerAdapter(cr, isFirstDaySunday)
- adapter.setToday(today)
- adapter.setStyleObject(
- { STYLE_CELL_ACCESSIBILITY_INFO_PROVIDER },
- DefaultRangeCalendarCellAccessibilityInfoProvider(context),
- notify = false
- )
- adapter.setSelectionGate(object : SelectionGate {
- override fun cell(year: Int, month: Int, dayOfMonth: Int) =
- internalGate(SelectionType.CELL) {
- it.cell(year, month, dayOfMonth)
- }
-
- override fun week(
- weekIndex: Int,
- startYear: Int,
- startMonth: Int,
- startDay: Int,
- endYear: Int,
- endMonth: Int,
- endDay: Int
- ) = internalGate(SelectionType.WEEK) {
- it.week(weekIndex, startYear, startMonth, startDay, endYear, endMonth, endDay)
- }
-
- override fun month(year: Int, month: Int) = internalGate(SelectionType.MONTH) {
- it.month(year, month)
- }
-
- override fun customRange(
- startYear: Int,
- startMonth: Int,
- startDay: Int,
- endYear: Int,
- endMonth: Int,
- endDay: Int
- ) = internalGate(SelectionType.CUSTOM) {
- it.customRange(startYear, startMonth, startDay, endYear, endMonth, endDay)
- }
-
- private inline fun internalGate(
- type: SelectionType,
- method: (SelectionGate) -> Boolean
- ): Boolean {
- return if (isSelectionTypeAllowed(type)) {
- selectionGate?.let(method) ?: true
- } else {
- false
- }
- }
- })
- adapter.setOnSelectionListener(object : OnSelectionListener {
- override fun onSelectionCleared() {
- if (_selectionView != null) {
- startSelectionViewTransition(false)
- }
-
- onSelectionListener?.onSelectionCleared()
- }
-
- override fun onDaySelected(year: Int, month: Int, day: Int) {
- onSelectedHandler {
- onDaySelected(year, month, day)
- }
- }
-
- override fun onWeekSelected(
- weekIndex: Int,
- startYear: Int,
- startMonth: Int,
- startDay: Int,
- endYear: Int,
- endMonth: Int,
- endDay: Int
- ) {
- onSelectedHandler {
- onWeekSelected(
- weekIndex,
- startYear, startMonth, startDay,
- endYear, endMonth, endDay
- )
- }
- }
-
- override fun onMonthSelected(year: Int, month: Int) {
- onSelectedHandler {
- onMonthSelected(year, month)
- }
- }
-
- override fun onCustomRangeSelected(
- startYear: Int, startMonth: Int, startDay: Int,
- endYear: Int, endMonth: Int, endDay: Int
- ) {
- onSelectedHandler {
- onCustomRangeSelected(
- startYear, startMonth, startDay,
- endYear, endMonth, endDay
- )
- }
- }
-
- private inline fun onSelectedHandler(method: OnSelectionListener.() -> Unit) {
- startSelectionViewTransition(true)
- onSelectionListener?.method()
- }
- })
-
- val stateChangeDuration = SV_TRANSITION_DURATION / 2
-
- prevIcon = MoveButtonDrawable(
- context, cr.colorControlNormal,
- MoveButtonDrawable.DIRECTION_LEFT, MoveButtonDrawable.ANIM_TYPE_VOID_TO_ARROW
- ).apply {
- setAnimationFraction(1f)
- setStateChangeDuration(stateChangeDuration)
- }
-
- nextIcon = MoveButtonDrawable(
- context, cr.colorControlNormal,
- MoveButtonDrawable.DIRECTION_RIGHT, MoveButtonDrawable.ANIM_TYPE_ARROW_TO_CROSS
- ).apply {
- setStateChangeDuration(stateChangeDuration)
+ adapter = RangeCalendarPagerAdapter(cr, isFirstDaySunday).apply {
+ setToday(today)
+ setStyleObject(
+ { STYLE_CELL_ACCESSIBILITY_INFO_PROVIDER },
+ DefaultRangeCalendarCellAccessibilityInfoProvider(context),
+ notify = false
+ )
+ setSelectionGate(createSelectionGate())
+ setOnSelectionListener(createOnSelectionListener())
}
- nextMonthDescription = res.getString(R.string.nextMonthDescription)
- clearSelectionDescription = res.getString(R.string.clearSelectionDescription)
-
prevButton = AppCompatImageButton(context).apply {
- setImageDrawable(prevIcon)
setOnClickListener { moveToPreviousMonth() }
- contentDescription = res.getString(R.string.previousMonthDescription)
}
nextOrClearButton = AppCompatImageButton(context).apply {
- setImageDrawable(nextIcon)
- contentDescription = nextMonthDescription
- setOnClickListener {
- if (isSelectionViewOnScreen && _hasSvClearButton) {
- clearSelection()
- } else {
- moveToNextMonth()
- }
- }
+ setOnClickListener(createNextButtonClickListener())
}
if (selectableBg != null) {
@@ -566,6 +417,12 @@ class RangeCalendarView @JvmOverloads constructor(
}
}
+ toolbarManager = CalendarToolbarManager(
+ context,
+ cr.colorControlNormal,
+ prevButton, nextOrClearButton, infoView
+ )
+
pager = ViewPager2(context).apply {
adapter = this@RangeCalendarView.adapter
offscreenPageLimit = 1
@@ -581,12 +438,7 @@ class RangeCalendarView @JvmOverloads constructor(
positionOffset: Float,
positionOffsetPixels: Int
) {
- val alpha = (510f * abs(0.5f - positionOffset)).toInt()
- setButtonAlphaIfEnabled(prevButton, alpha)
-
- if (!_hasSvClearButton || !isSelectionViewOnScreen) {
- setButtonAlphaIfEnabled(nextOrClearButton, alpha)
- }
+ toolbarManager.onPageScrolled(positionOffset)
}
override fun onPageSelected(position: Int) {
@@ -595,9 +447,7 @@ class RangeCalendarView @JvmOverloads constructor(
currentCalendarYm = ym
setInfoViewYearMonth(ym)
- setButtonAlphaIfEnabled(prevButton, 255)
- setButtonAlphaIfEnabled(nextOrClearButton, 255)
-
+ toolbarManager.restoreButtonsAlpha()
updateMoveButtons()
onPageChangeListener?.onPageChanged(ym.year, ym.month)
@@ -620,9 +470,120 @@ class RangeCalendarView @JvmOverloads constructor(
setYearAndMonthInternal(YearMonth.forDate(today), false)
+ // It should be called after the toolbarManager is initialized.
+ initLocaleDependentValues()
+
attrs?.let { initFromAttributes(context, it, defStyleAttr) }
}
+ fun createNextButtonClickListener() = OnClickListener {
+ if (toolbarManager.isNextButtonActClear) {
+ clearSelection()
+ } else {
+ moveToNextMonth()
+ }
+ }
+
+ fun createOnSelectionListener() = object : OnSelectionListener {
+ override fun onSelectionCleared() {
+ toolbarManager.onSelectionCleared()
+
+ onSelectionListener?.onSelectionCleared()
+ }
+
+ override fun onDaySelected(year: Int, month: Int, day: Int) {
+ onSelectedHandler {
+ onDaySelected(year, month, day)
+ }
+ }
+
+ override fun onWeekSelected(
+ weekIndex: Int,
+ startYear: Int,
+ startMonth: Int,
+ startDay: Int,
+ endYear: Int,
+ endMonth: Int,
+ endDay: Int
+ ) {
+ onSelectedHandler {
+ onWeekSelected(
+ weekIndex,
+ startYear, startMonth, startDay,
+ endYear, endMonth, endDay
+ )
+ }
+ }
+
+ override fun onMonthSelected(year: Int, month: Int) {
+ onSelectedHandler {
+ onMonthSelected(year, month)
+ }
+ }
+
+ override fun onCustomRangeSelected(
+ startYear: Int, startMonth: Int, startDay: Int,
+ endYear: Int, endMonth: Int, endDay: Int
+ ) {
+ onSelectedHandler {
+ onCustomRangeSelected(
+ startYear, startMonth, startDay,
+ endYear, endMonth, endDay
+ )
+ }
+ }
+
+ private inline fun onSelectedHandler(method: OnSelectionListener.() -> Unit) {
+ toolbarManager.onSelection()
+ onSelectionListener?.method()
+ }
+ }
+
+ private fun createSelectionGate() = object : SelectionGate {
+ override fun cell(year: Int, month: Int, dayOfMonth: Int) =
+ internalGate(SelectionType.CELL) {
+ it.cell(year, month, dayOfMonth)
+ }
+
+ override fun week(
+ weekIndex: Int,
+ startYear: Int,
+ startMonth: Int,
+ startDay: Int,
+ endYear: Int,
+ endMonth: Int,
+ endDay: Int
+ ) = internalGate(SelectionType.WEEK) {
+ it.week(weekIndex, startYear, startMonth, startDay, endYear, endMonth, endDay)
+ }
+
+ override fun month(year: Int, month: Int) = internalGate(SelectionType.MONTH) {
+ it.month(year, month)
+ }
+
+ override fun customRange(
+ startYear: Int,
+ startMonth: Int,
+ startDay: Int,
+ endYear: Int,
+ endMonth: Int,
+ endDay: Int
+ ) = internalGate(SelectionType.CUSTOM) {
+ it.customRange(startYear, startMonth, startDay, endYear, endMonth, endDay)
+ }
+
+ private inline fun internalGate(
+ type: SelectionType,
+ method: (SelectionGate) -> Boolean
+ ): Boolean {
+ return if (isSelectionTypeAllowed(type)) {
+ selectionGate?.let(method) ?: true
+ } else {
+ false
+ }
+ }
+ }
+
private fun initFromAttributes(
context: Context,
attrs: AttributeSet,
@@ -661,10 +622,15 @@ class RangeCalendarView @JvmOverloads constructor(
// cellSize, cellWidth, cellHeight require special logic.
// If cellSize exists, it's written to both cellWidth and cellHeight, but
// if either of cellWidth or cellHeight exist, they take precedence over cellSize.
- var cellWidth = a.getDimension(R.styleable.RangeCalendarView_rangeCalendar_cellWidth, Float.NaN)
- var cellHeight = a.getDimension(R.styleable.RangeCalendarView_rangeCalendar_cellHeight, Float.NaN)
+ var cellWidth =
+ a.getDimension(R.styleable.RangeCalendarView_rangeCalendar_cellWidth, Float.NaN)
+ var cellHeight = a.getDimension(
+ R.styleable.RangeCalendarView_rangeCalendar_cellHeight,
+ Float.NaN
+ )
- val cellSize = a.getDimension(R.styleable.RangeCalendarView_rangeCalendar_cellSize, Float.NaN)
+ val cellSize =
+ a.getDimension(R.styleable.RangeCalendarView_rangeCalendar_cellSize, Float.NaN)
if (!cellSize.isNaN()) {
if (cellWidth.isNaN()) cellWidth = cellSize
if (cellHeight.isNaN()) cellHeight = cellSize
@@ -698,17 +664,22 @@ class RangeCalendarView @JvmOverloads constructor(
}
private fun initLocaleDependentValues() {
- val locale = context.getLocaleCompat()
- currentLocale = locale
-
- refreshIsFirstDaySunday(locale)
+ // dateFormatter is initialized on creation. No need in double creating the underlying models.
+ refreshLocaleDependentValues(
+ newLocale = context.getLocaleCompat(),
+ updateDateFormatter = false
+ )
}
- private fun refreshLocaleDependentValues(newLocale: Locale) {
+ private fun refreshLocaleDependentValues(newLocale: Locale, updateDateFormatter: Boolean) {
currentLocale = newLocale
- dateFormatter.onLocaleChanged(newLocale)
refreshIsFirstDaySunday(newLocale)
+ if (updateDateFormatter) {
+ dateFormatter.onLocaleChanged(newLocale)
+ }
+
+ toolbarManager.onLocaleChanged()
}
private fun refreshIsFirstDaySunday(locale: Locale) {
@@ -729,7 +700,8 @@ class RangeCalendarView @JvmOverloads constructor(
val newLocale = newConfig.getLocaleCompat()
if (currentLocale != newLocale) {
- refreshLocaleDependentValues(newLocale)
+ // Do a full update.
+ refreshLocaleDependentValues(newLocale, updateDateFormatter = true)
}
}
@@ -769,32 +741,37 @@ class RangeCalendarView @JvmOverloads constructor(
pager.measure(widthMeasureSpec, heightMeasureSpec)
val pagerWidth = pager.measuredWidth
+ val buttonSize = buttonSize
val buttonSpec = MeasureSpec.makeMeasureSpec(buttonSize, MeasureSpec.EXACTLY)
- val infoWidthSpec = MeasureSpec.makeMeasureSpec(pager.measuredWidth, MeasureSpec.AT_MOST)
- val infoHeightSpec = MeasureSpec.makeMeasureSpec(buttonSize, MeasureSpec.AT_MOST)
- if (_selectionView != null) {
+ val maxInfoWidth = pagerWidth - 2 * (hPadding + buttonSize)
+ val infoWidthSpec = MeasureSpec.makeMeasureSpec(maxInfoWidth, MeasureSpec.AT_MOST)
+ val infoHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
+
+ infoView.measure(infoWidthSpec, infoHeightSpec)
+ prevButton.measure(buttonSpec, buttonSpec)
+ nextOrClearButton.measure(buttonSpec, buttonSpec)
+
+ val toolbarHeight = max(infoView.measuredHeight, buttonSize)
+
+ toolbarManager.selectionView?.also { sv ->
var maxWidth = pagerWidth - 2 * hPadding - buttonSize
- if (!_hasSvClearButton) {
+ if (!toolbarManager.hasSelectionViewClearButton) {
maxWidth -= buttonSize
}
measureChild(
- _selectionView,
+ sv,
MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST),
- MeasureSpec.makeMeasureSpec(buttonSize, MeasureSpec.AT_MOST)
+ MeasureSpec.makeMeasureSpec(toolbarHeight, MeasureSpec.AT_MOST)
)
}
- infoView.measure(infoWidthSpec, infoHeightSpec)
- prevButton.measure(buttonSpec, buttonSpec)
- nextOrClearButton.measure(buttonSpec, buttonSpec)
-
setMeasuredDimension(
pager.measuredWidthAndState,
resolveSize(
- pager.measuredHeight + buttonSize + topContainerMarginBottom,
+ pager.measuredHeight + toolbarHeight + topContainerMarginBottom,
heightMeasureSpec
)
)
@@ -803,162 +780,72 @@ class RangeCalendarView @JvmOverloads constructor(
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val width = r - l
+ val hPadding = hPadding
+ val buttonSize = buttonSize
+
+ val toolbarManager = toolbarManager
val prevRight = hPadding + buttonSize
val nextLeft = width - prevRight
- val sv = _selectionView
-
- if (sv != null) {
- val gravity = svLayoutParams.gravity
- val svWidth = sv.measuredWidth
- val svHeight = sv.measuredHeight
-
- // If selection view is wanted to be in center on x-axis,
- // let it be actual center of the whole view.
- if (gravity == Gravity.CENTER || gravity == Gravity.CENTER_HORIZONTAL) {
- layoutRect.set(hPadding, 0, width - hPadding, buttonSize)
- } else {
- layoutRect.set(
- if (_hasSvClearButton) hPadding else prevRight,
- 0,
- nextLeft,
- buttonSize
- )
- }
-
- if (Build.VERSION.SDK_INT >= 17) {
- Gravity.apply(
- svLayoutParams.gravity,
- svWidth, svHeight,
- layoutRect, layoutOutRect,
- layoutDirection
- )
- } else {
- Gravity.apply(
- svLayoutParams.gravity,
- svWidth, svHeight,
- layoutRect, layoutOutRect
- )
- }
-
- sv.layout(
- layoutOutRect.left, layoutOutRect.top,
- layoutOutRect.right, layoutOutRect.bottom
- )
- }
val infoWidth = infoView.measuredWidth
val infoHeight = infoView.measuredHeight
+
+ val toolbarHeight = max(buttonSize, infoHeight)
+
val infoLeft = (width - infoWidth) / 2
- val infoTop = (buttonSize - infoHeight) / 2
+ val infoTop = (toolbarHeight - infoHeight) / 2
+ val buttonTop = (toolbarHeight - buttonSize) / 2
+ val buttonBottom = buttonTop + buttonSize
+ val pagerTop = toolbarHeight + topContainerMarginBottom
- prevButton.layout(hPadding, 0, prevRight, buttonSize)
- nextOrClearButton.layout(nextLeft, 0, nextLeft + buttonSize, buttonSize)
+ prevButton.layout(hPadding, buttonTop, prevRight, buttonBottom)
+ nextOrClearButton.layout(nextLeft, buttonTop, nextLeft + buttonSize, buttonBottom)
infoView.layout(infoLeft, infoTop, infoLeft + infoWidth, infoTop + infoHeight)
- val pagerTop = buttonSize + topContainerMarginBottom
pager.layout(0, pagerTop, pager.measuredWidth, pagerTop + pager.measuredHeight)
- }
- private fun setSelectionViewOnScreen(state: Boolean) {
- isSelectionViewOnScreen = state
- val sv = _selectionView!!
+ toolbarManager.selectionView?.also { sv ->
+ val lr = layoutRect
+ val lrOut = layoutOutRect
- if (state) {
- sv.visibility = VISIBLE
- infoView.visibility = INVISIBLE
- if (_hasSvClearButton) {
- nextOrClearButton.contentDescription = clearSelectionDescription
- }
- } else {
- sv.visibility = INVISIBLE
- infoView.visibility = VISIBLE
- if (_hasSvClearButton) {
- nextOrClearButton.contentDescription = nextMonthDescription
- }
- }
- updateMoveButtons()
- }
+ val svLayoutParams = toolbarManager.selectionViewLayoutParams
+ val gravity = svLayoutParams.gravity
- private fun startSelectionViewTransition(forward: Boolean) {
- if (_selectionView == null) {
- return
- }
+ // lr's top is always 0
+ // lr.top = 0
+ lr.bottom = toolbarHeight
- var animator = svAnimator
+ // Detection of whether the gravity is center_horizontal is a little bit complicated.
+ // Basically we need to check whether bits AXIS_PULL_BEFORE and AXIS_PULL_AFTER bits are 0.
+ val isCenterHorizontal =
+ gravity and ((Gravity.AXIS_PULL_BEFORE or Gravity.AXIS_PULL_AFTER) shl Gravity.AXIS_X_SHIFT) == 0
- // Don't continue if we want to show selection view and it's already shown and vise versa,
- // but continue if animation is currently running and direction of current animation is not equals to new one.
- if (animator != null &&
- (!animator.isRunning || isSvTransitionForward == forward) &&
- forward == isSelectionViewOnScreen
- ) {
- return
- }
-
- if (animator == null) {
- animator = AnimationHelper.createFractionAnimator { fraction ->
- onSVTransitionTick(fraction)
+ // If the gravity on x-axis is center, let the view be centered along the whole
+ // calendar view (except padding).
+ if (isCenterHorizontal) {
+ lr.left = hPadding
+ lr.right = width - hPadding
+ } else {
+ lr.left = if (toolbarManager.hasSelectionViewClearButton) hPadding else prevRight
+ lr.right = nextLeft
}
- animator.addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationStart(animation: Animator) {
- if (!isSvTransitionForward) {
- prevButton.visibility = VISIBLE
- }
- }
-
- override fun onAnimationEnd(animation: Animator) {
- if (isSvTransitionForward) {
- prevButton.visibility = GONE
- }
- }
- })
- }
- isSvTransitionForward = forward
-
- var startPlaytime: Long = 0
- if (animator.isRunning) {
- startPlaytime = animator.currentPlayTime
- animator.end()
- }
-
- animator.interpolator = selectionViewTransitionInterpolator
- animator.duration = svTransitionDuration
+ val absGravity = if (Build.VERSION.SDK_INT >= 17) {
+ Gravity.getAbsoluteGravity(gravity, layoutDirection)
+ } else {
+ // Strip off relative bits to get left/right in case we have no layoutDirection data.
+ gravity and Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK.inv()
+ }
- // ValueAnimator.setCurrentFraction() could be used, but it's available only from API >= 22,
- animator.currentPlayTime = startPlaytime
+ Gravity.apply(absGravity, sv.measuredWidth, sv.measuredHeight, lr, lrOut)
- if (forward) {
- animator.start()
- } else {
- animator.reverse()
+ sv.layout(
+ lrOut.left, lrOut.top,
+ lrOut.right, lrOut.bottom
+ )
}
}
- private fun onSVTransitionTick(fraction: Float) {
- val sv = _selectionView!!
-
- if (_hasSvClearButton) {
- prevIcon.setAnimationFraction(1f - fraction)
- nextIcon.setAnimationFraction(fraction)
- }
-
- if (fraction < 0.5f) {
- val f = fraction * -2f
- infoView.translationY = infoView.bottom * f
-
- if (isSelectionViewOnScreen) {
- setSelectionViewOnScreen(false)
- }
- } else {
- val f = 2f * fraction - 2f
- sv.translationY = f * sv.bottom
-
- if (!isSelectionViewOnScreen) {
- setSelectionViewOnScreen(true)
- }
- }
- }
/**
* Gets or sets the selection view.
@@ -971,23 +858,24 @@ class RangeCalendarView @JvmOverloads constructor(
* This might be helpful for rational use of space.
*/
var selectionView: View?
- get() = _selectionView
+ get() = toolbarManager.selectionView
set(value) {
- val oldSelectionView = _selectionView
- _selectionView = value
+ val oldSelectionView = toolbarManager.selectionView
if (oldSelectionView != null) {
- // On 0 fraction, selection view will disappear
- onSVTransitionTick(0f)
+ toolbarManager.hideSelectionView()
- // selection view is always in end
+ // selection view is always the last one.
removeViewAt(childCount - 1)
}
+ toolbarManager.selectionView = value
+
if (value != null) {
value.visibility = INVISIBLE
addView(value)
}
+
requestLayout()
}
@@ -996,25 +884,27 @@ class RangeCalendarView @JvmOverloads constructor(
* from "previous" button and year & month text view to selection view and vise verse.
*/
var selectionViewTransitionDuration: Long
- get() = svTransitionDuration
+ get() = toolbarManager.selectionViewTransitionDuration
set(duration) {
- svTransitionDuration = duration
- val stateChangeDuration = duration / 2
-
- prevIcon.setStateChangeDuration(stateChangeDuration)
- nextIcon.setStateChangeDuration(stateChangeDuration)
+ toolbarManager.selectionViewTransitionDuration = duration
}
/**
* Gets or sets layout params for selection view
*/
var selectionViewLayoutParams: CalendarSelectionViewLayoutParams
- get() = svLayoutParams
+ get() = toolbarManager.selectionViewLayoutParams
set(layoutParams) {
- svLayoutParams = layoutParams
+ toolbarManager.selectionViewLayoutParams = layoutParams
requestLayout()
}
+ var selectionViewTransitionInterpolator: TimeInterpolator
+ get() = toolbarManager.selectionViewTransitionInterpolator
+ set(value) {
+ toolbarManager.selectionViewTransitionInterpolator = value
+ }
+
/**
* Gets or sets whether selection view on selection should have clear selection button.
*
@@ -1026,13 +916,13 @@ class RangeCalendarView @JvmOverloads constructor(
* selection will be placed where year & month view. User will still be able to use move buttons
*/
var hasSelectionViewClearButton: Boolean
- get() = _hasSvClearButton
+ get() = toolbarManager.hasSelectionViewClearButton
set(value) {
- if (_hasSvClearButton == value) {
+ if (toolbarManager.hasSelectionViewClearButton == value) {
return
}
- _hasSvClearButton = value
+ toolbarManager.hasSelectionViewClearButton = value
requestLayout()
}
@@ -1395,41 +1285,27 @@ class RangeCalendarView @JvmOverloads constructor(
* Gets or sets content description for the 'previous month' button.
*/
var previousMonthButtonContentDescription: CharSequence
- get() = prevButton.contentDescription
+ get() = toolbarManager.prevMonthDescription
set(value) {
- prevButton.contentDescription = value
+ toolbarManager.prevMonthDescription = value
}
/**
* Gets or sets content description for the 'next month' button.
*/
var nextMonthButtonContentDescription: CharSequence
- get() = nextMonthDescription
+ get() = toolbarManager.nextMonthDescription
set(value) {
- nextMonthDescription = value
-
- // We can only update nextOrClearButton's content description if either we don't have selection view
- // or clear selection button is not enabled. Because if it's enabled, then nextOrClearButton is in
- // 'clear selection' state and we'd update content description to the wrong value.
- if (!isSelectionViewOnScreen || !_hasSvClearButton) {
- nextOrClearButton.contentDescription = value
- }
+ toolbarManager.nextMonthDescription = value
}
/**
* Gets or sets content description for the 'clear selection' button.
*/
var clearSelectionButtonContentDescription: CharSequence
- get() = clearSelectionDescription
+ get() = toolbarManager.clearSelectionDescription
set(value) {
- clearSelectionDescription = value
-
- // We can only update nextOrClearButton's content description to this value if
- // clear selection button is enabled and selection view is on screen, otherwise
- // the button is in 'next month' state.
- if (isSelectionViewOnScreen && _hasSvClearButton) {
- nextOrClearButton.contentDescription = value
- }
+ toolbarManager.clearSelectionDescription = value
}
/**
@@ -1792,12 +1668,7 @@ class RangeCalendarView @JvmOverloads constructor(
val count = adapter.itemCount
prevButton.isEnabled = position != 0
-
- if (!_hasSvClearButton || !isSelectionViewOnScreen) {
- nextOrClearButton.isEnabled = position != count - 1
- } else {
- nextOrClearButton.isEnabled = true
- }
+ nextOrClearButton.isEnabled = toolbarManager.isNextButtonActClear || position != count - 1
}
private fun setInfoViewYearMonth(ym: YearMonth) {
@@ -1812,13 +1683,6 @@ class RangeCalendarView @JvmOverloads constructor(
private val TAG = RangeCalendarView::class.java.simpleName
private const val DATE_FORMAT = "MMMM y"
- private const val SV_TRANSITION_DURATION: Long = 300
-
- private fun setButtonAlphaIfEnabled(button: ImageButton, alpha: Int) {
- if (button.isEnabled) {
- button.drawable?.alpha = alpha
- }
- }
private fun requireValidEpochDayOnLocalDateTransform(epochDay: Long) {
require(PackedDate.isValidEpochDay(epochDay)) { "Date is out of valid range" }
diff --git a/selectionviewtest/.gitignore b/selectionviewtest/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/selectionviewtest/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/selectionviewtest/build.gradle b/selectionviewtest/build.gradle
new file mode 100644
index 0000000..e4b7f43
--- /dev/null
+++ b/selectionviewtest/build.gradle
@@ -0,0 +1,45 @@
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ namespace 'com.github.pelmenstar1.rangecalendar.selectionviewtest'
+ compileSdk 33
+
+ defaultConfig {
+ applicationId "com.github.pelmenstar1.rangecalendar.selectionviewtest"
+ minSdk 16
+ targetSdk 33
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+}
+
+dependencies {
+ implementation project(":library")
+
+ implementation 'androidx.core:core-ktx:1.10.1'
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'com.google.android.material:material:1.9.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+}
\ No newline at end of file
diff --git a/selectionviewtest/proguard-rules.pro b/selectionviewtest/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/selectionviewtest/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/selectionviewtest/src/main/AndroidManifest.xml b/selectionviewtest/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9793515
--- /dev/null
+++ b/selectionviewtest/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/selectionviewtest/src/main/java/com/github/pelmenstar1/rangecalendar/selectionviewtest/MainActivity.kt b/selectionviewtest/src/main/java/com/github/pelmenstar1/rangecalendar/selectionviewtest/MainActivity.kt
new file mode 100644
index 0000000..54e8d60
--- /dev/null
+++ b/selectionviewtest/src/main/java/com/github/pelmenstar1/rangecalendar/selectionviewtest/MainActivity.kt
@@ -0,0 +1,169 @@
+package com.github.pelmenstar1.rangecalendar.selectionviewtest
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.widget.AdapterView
+import android.widget.CheckBox
+import android.widget.Spinner
+import android.widget.TextView
+import androidx.annotation.IdRes
+import androidx.appcompat.app.AppCompatActivity
+import com.github.pelmenstar1.rangecalendar.CalendarSelectionViewLayoutParams
+import com.github.pelmenstar1.rangecalendar.RangeCalendarView
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.slider.Slider
+
+class MainActivity : AppCompatActivity() {
+ private lateinit var rangeCalendarView: RangeCalendarView
+ private lateinit var transitionDurationTextView: TextView
+
+ private var selectionViewHorizontalGravity = Gravity.START
+ private var selectionViewVerticalGravity = Gravity.TOP
+
+ private var selectionViewWidth = ViewGroup.LayoutParams.WRAP_CONTENT
+ private var selectionViewHeight = ViewGroup.LayoutParams.WRAP_CONTENT
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContentView(R.layout.activity_main)
+
+ rangeCalendarView = findViewById(R.id.rangeCalendar)
+
+ // Init hasClearCheckBox
+
+ findViewById(R.id.hasClearCheckBox).apply {
+ setOnCheckedChangeListener { _, isChecked ->
+ rangeCalendarView.hasSelectionViewClearButton = isChecked
+ }
+ }
+
+ // Init isSelectionViewNullCheckBox
+
+ findViewById(R.id.isSelectionViewNullCheckBox).apply {
+ setOnCheckedChangeListener { _, isChecked ->
+ setIsSelectionViewNull(isChecked)
+ }
+ }
+
+ // Init transition duration views
+
+ transitionDurationTextView = findViewById(R.id.transitionDurationTextView)
+ onTransitionDurationChanged(value = 1000)
+
+ findViewById(R.id.transitionDurationSlider).apply {
+ addOnChangeListener { _, value, _ ->
+ onTransitionDurationChanged(value.toLong())
+ }
+ }
+
+ // Init gravity spinners
+
+ initGravitySpinner(
+ spinnerRes = R.id.horizontalGravitySpinner,
+ values = intArrayOf(Gravity.START, Gravity.CENTER_HORIZONTAL, Gravity.END),
+ isHorizontal = true
+ )
+
+ initGravitySpinner(
+ spinnerRes = R.id.verticalGravitySpinner,
+ values = intArrayOf(Gravity.TOP, Gravity.CENTER_VERTICAL, Gravity.BOTTOM),
+ isHorizontal = false
+ )
+
+ // Init size spinners
+
+ initSizeSpinner(spinnerRes = R.id.selectionViewWidthSpinner, isWidth = true)
+ initSizeSpinner(spinnerRes = R.id.selectionViewHeightSpinner, isWidth = false)
+
+ // Init rangeCalendarView
+
+ setIsSelectionViewNull(state = false)
+ updateSelectionViewLayoutParams()
+ }
+
+ private fun setIsSelectionViewNull(state: Boolean) {
+ rangeCalendarView.selectionView = if (state) {
+ null
+ } else {
+ MaterialButton(this).apply {
+ text = getString(R.string.action)
+ }
+ }
+ }
+
+ private fun initGravitySpinner(
+ @IdRes spinnerRes: Int,
+ values: IntArray,
+ isHorizontal: Boolean
+ ) {
+ findViewById(spinnerRes).apply {
+ onItemSelectedListener = createSpinnerSelectedListener { position ->
+ val gravity = values[position]
+
+ if (isHorizontal) {
+ selectionViewHorizontalGravity = gravity
+ } else {
+ selectionViewVerticalGravity = gravity
+ }
+
+ updateSelectionViewLayoutParams()
+ }
+ }
+ }
+
+ private fun initSizeSpinner(@IdRes spinnerRes: Int, isWidth: Boolean) {
+ findViewById(spinnerRes).apply {
+ onItemSelectedListener = createSpinnerSelectedListener { position ->
+ val size = when (position) {
+ 0 -> ViewGroup.LayoutParams.WRAP_CONTENT
+ 1 -> ViewGroup.LayoutParams.MATCH_PARENT
+ else -> throw RuntimeException("Invalid position")
+ }
+
+ if (isWidth) {
+ selectionViewWidth = size
+ } else {
+ selectionViewHeight = size
+ }
+
+ updateSelectionViewLayoutParams()
+ }
+ }
+ }
+
+ private fun createSpinnerSelectedListener(block: (position: Int) -> Unit): AdapterView.OnItemSelectedListener {
+ return object : AdapterView.OnItemSelectedListener {
+ override fun onItemSelected(
+ parent: AdapterView<*>?,
+ view: View?,
+ position: Int,
+ id: Long
+ ) {
+ block(position)
+ }
+
+ override fun onNothingSelected(parent: AdapterView<*>?) {
+ }
+ }
+ }
+
+ private fun updateSelectionViewLayoutParams() {
+ rangeCalendarView.selectionViewLayoutParams = CalendarSelectionViewLayoutParams(
+ selectionViewWidth,
+ selectionViewHeight
+ ).apply {
+ gravity = selectionViewHorizontalGravity or selectionViewVerticalGravity
+ }
+ }
+
+ @SuppressLint("SetTextI18n")
+ private fun onTransitionDurationChanged(value: Long) {
+ rangeCalendarView.selectionViewTransitionDuration = value
+
+ transitionDurationTextView.text = "${getString(R.string.transitionDuration)}: $value"
+ }
+}
\ No newline at end of file
diff --git a/selectionviewtest/src/main/res/drawable-v24/ic_launcher_foreground.xml b/selectionviewtest/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/selectionviewtest/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/selectionviewtest/src/main/res/drawable/ic_launcher_background.xml b/selectionviewtest/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/selectionviewtest/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/selectionviewtest/src/main/res/layout/activity_main.xml b/selectionviewtest/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..612421c
--- /dev/null
+++ b/selectionviewtest/src/main/res/layout/activity_main.xml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/selectionviewtest/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/selectionviewtest/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/selectionviewtest/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/selectionviewtest/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/selectionviewtest/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/selectionviewtest/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/selectionviewtest/src/main/res/mipmap-hdpi/ic_launcher.webp b/selectionviewtest/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/selectionviewtest/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/selectionviewtest/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/selectionviewtest/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/selectionviewtest/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/selectionviewtest/src/main/res/mipmap-mdpi/ic_launcher.webp b/selectionviewtest/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/selectionviewtest/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/selectionviewtest/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/selectionviewtest/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/selectionviewtest/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/selectionviewtest/src/main/res/mipmap-xhdpi/ic_launcher.webp b/selectionviewtest/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/selectionviewtest/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/selectionviewtest/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/selectionviewtest/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/selectionviewtest/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/selectionviewtest/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/selectionviewtest/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/selectionviewtest/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/selectionviewtest/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/selectionviewtest/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/selectionviewtest/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/selectionviewtest/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/selectionviewtest/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/selectionviewtest/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/selectionviewtest/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/selectionviewtest/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/selectionviewtest/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/selectionviewtest/src/main/res/values-night/themes.xml b/selectionviewtest/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..ed62b13
--- /dev/null
+++ b/selectionviewtest/src/main/res/values-night/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/selectionviewtest/src/main/res/values/colors.xml b/selectionviewtest/src/main/res/values/colors.xml
new file mode 100644
index 0000000..c8524cd
--- /dev/null
+++ b/selectionviewtest/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/selectionviewtest/src/main/res/values/strings.xml b/selectionviewtest/src/main/res/values/strings.xml
new file mode 100644
index 0000000..cb94723
--- /dev/null
+++ b/selectionviewtest/src/main/res/values/strings.xml
@@ -0,0 +1,30 @@
+
+ selectionviewtest
+
+ Has clear button
+ Action
+ Transition duration
+ Is selection view null
+
+ Horizontal gravity:
+ Vertical gravity:
+ Selection view width:
+ Selection view height:
+
+
+ - Start
+ - Center-horizontal
+ - End
+
+
+
+ - Top
+ - Center-vertical
+ - Bottom
+
+
+
+ - Wrap content
+ - Match parent
+
+
\ No newline at end of file
diff --git a/selectionviewtest/src/main/res/values/themes.xml b/selectionviewtest/src/main/res/values/themes.xml
new file mode 100644
index 0000000..a36537b
--- /dev/null
+++ b/selectionviewtest/src/main/res/values/themes.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 269570a..c5f8206 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -19,3 +19,4 @@ include ':library'
include ':demo'
include ':decortest'
include ':movebuttontest'
+include ':selectionviewtest'