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 @@ + + + + +