From 259c1268857f10b09894c63b5b17e0734f423d73 Mon Sep 17 00:00:00 2001 From: Oleh Khmaruk Date: Thu, 13 Jul 2023 13:34:06 +0300 Subject: [PATCH] Join selection transitions (#30) * Add joining range-to-range animations * Set cell ranges in CellMoveToCell * Fix TickCallback * Join move-cell-to-cell transitions * Join move-cell-to-cell and range-to-range transitions * Add docs * Fix joining cell-move-to-cell transitions * Small refactor * Fix docs * Generalize models * Remove hasTransition() * Refactor * Fix docs --- README.md | 5 +- .../rangecalendar/CellMeasureManager.kt | 22 +- .../rangecalendar/RangeCalendarGridView.kt | 213 ++++++++++---- .../gesture/RangeCalendarGestureDetector.kt | 6 +- .../selection/DefaultSelectionManager.kt | 277 +++++++++++++----- .../selection/DefaultSelectionRenderer.kt | 6 +- .../selection/DefaultSelectionState.kt | 112 +++++-- .../DefaultSelectionTransitionController.kt | 33 ++- .../selection/SelectionManager.kt | 24 +- .../selection/SelectionShapeInfo.kt | 58 +++- .../rangecalendar/selection/SelectionState.kt | 21 +- .../rangecalendar/utils/MathUtils.kt | 15 +- 12 files changed, 567 insertions(+), 225 deletions(-) diff --git a/README.md b/README.md index 352a68f..63c6fcb 100644 --- a/README.md +++ b/README.md @@ -165,10 +165,7 @@ There's a description of methods of `SelectionManager` and what they are expecte arguments. Note that, rangeEnd is **inclusive**. measureManager should be used to determine bounds of a cell. - `updateConfiguration(measureManager)` - updates internal measurements and computation based on measureManager results of both previousState and currentState. Change of measureManager result means that cells might be moved or resized. -- `hasTransition()` - returns whether there's a transition between previousState and currentState. -- `createTransition(measureManager, options)` - creates a transitive state between previousState and currentState. It's - only called if hasTransition() returns true. -- `options` is used to stylize the selection as selection state shouldn't contain any style-related information. +- `createTransition(measureManager, options)` - creates a transitive state between previousState and currentState Selection renderer is responsible for drawing simple selection state or transitive state, that is created to save information about transition between two selection states. When selection is to be drawn, the canvas' matrix is translated in such way that coordinates will be relative to the grid's leftmost point on top. diff --git a/library/src/main/java/com/github/pelmenstar1/rangecalendar/CellMeasureManager.kt b/library/src/main/java/com/github/pelmenstar1/rangecalendar/CellMeasureManager.kt index 2f0b00e..0aa2456 100644 --- a/library/src/main/java/com/github/pelmenstar1/rangecalendar/CellMeasureManager.kt +++ b/library/src/main/java/com/github/pelmenstar1/rangecalendar/CellMeasureManager.kt @@ -8,6 +8,11 @@ import android.graphics.PointF * **API surface of this class is not stable and new members might be added or removed.** */ interface CellMeasureManager { + enum class CoordinateRelativity { + VIEW, + GRID + } + /** * Width of cell (in pixels). */ @@ -24,7 +29,7 @@ interface CellMeasureManager { val roundRadius: Float /** - * Gets x-axis value of the coordinate that specifies left corner of the cell. + * Gets x-axis value of the coordinate that specifies left corner of the cell. The coordinate is relative to the grid. * * @param cellIndex index of the cell, should be in range 0..41 * @throws IllegalArgumentException if [cellIndex] is out of the range 0..41 @@ -32,7 +37,7 @@ interface CellMeasureManager { fun getCellLeft(cellIndex: Int): Float /** - * Gets y-axis value of the coordinate that specifies top corner of the cell. + * Gets y-axis value of the coordinate that specifies top corner of the cell. The coordinate is relative to the grid. * * @param cellIndex index of the cell, should be in range 0..41 * @throws IllegalArgumentException if [cellIndex] is out of the range 0..41 @@ -50,6 +55,7 @@ interface CellMeasureManager { /** * Gets points on the calendar view and cell's index nearest to the point by cell [distance]. The point is set to [outPoint]. + * The coordinates of the point are relative to the grid. * * @param distance cell distance, expected to be non-negative * @param outPoint the resulting point is set to this point @@ -59,9 +65,17 @@ interface CellMeasureManager { fun getCellAndPointByDistance(distance: Float, outPoint: PointF): Int /** - * Gets index of a cell nearest to a point with specified coordinates. If there's no such cell, returns `-1` + * Gets 'cell distance' using a point ([x], [y]) relative to the grid. + * The [x] can be arbitrary but the [y] **must** be aligned to the row's top point. + * + * @see getCellDistance + */ + fun getCellDistanceByPoint(x: Float, y: Float): Float + + /** + * Gets index of a cell nearest to a point with specified coordinates. If there's no such cell, returns `-1`. */ - fun getCellAt(x: Float, y: Float): Int + fun getCellAt(x: Float, y: Float, relativity: CoordinateRelativity): Int /** * Returns the absolute value of a dimension specified by the [anchor]. diff --git a/library/src/main/java/com/github/pelmenstar1/rangecalendar/RangeCalendarGridView.kt b/library/src/main/java/com/github/pelmenstar1/rangecalendar/RangeCalendarGridView.kt index 262c4c1..3180140 100644 --- a/library/src/main/java/com/github/pelmenstar1/rangecalendar/RangeCalendarGridView.kt +++ b/library/src/main/java/com/github/pelmenstar1/rangecalendar/RangeCalendarGridView.kt @@ -53,13 +53,17 @@ internal class RangeCalendarGridView( fun range(range: CellRange): Boolean } + private fun interface TickCallback { + fun onTick(fraction: Float) + } + private class TouchHelper(private val grid: RangeCalendarGridView) : ExploreByTouchHelper(grid) { private val tempRect = Rect() override fun getVirtualViewAt(x: Float, y: Float): Int { return if (grid.isXInActiveZone(x) && y > grid.gridTop()) { - grid.getCellByPointOnScreen(x, y).index + grid.getCellByPointOnScreen(x, y, CellMeasureManager.CoordinateRelativity.VIEW) } else { INVALID_ID } @@ -162,8 +166,11 @@ internal class RangeCalendarGridView( override fun getCellAndPointByDistance(distance: Float, outPoint: PointF): Int = view.getCellAndPointByCellDistanceRelativeToGrid(distance, outPoint) - override fun getCellAt(x: Float, y: Float): Int = - view.getCellByPointOnScreen(x, y).index + override fun getCellDistanceByPoint(x: Float, y: Float): Float = + view.getCellDistanceByPoint(x, y) + + override fun getCellAt(x: Float, y: Float, relativity: CellMeasureManager.CoordinateRelativity): Int = + view.getCellByPointOnScreen(x, y, relativity) override fun getRelativeAnchorValue(anchor: Distance.RelativeAnchor): Float = view.getRelativeAnchorValue(anchor) @@ -226,7 +233,7 @@ internal class RangeCalendarGridView( var onSelectionListener: OnSelectionListener? = null var selectionGate: SelectionGate? = null - private var selectionTransitionHandler: (() -> Unit)? = null + private var selectionTransitionHandler: TickCallback? = null private var onSelectionTransitionEnd: (() -> Unit)? = null private var selectionTransitiveState: SelectionState.Transitive? = null @@ -235,6 +242,8 @@ internal class RangeCalendarGridView( private var selectionRenderer = selectionManager.renderer private var selectionRenderOptions: SelectionRenderOptions? = null + private var hoverAnimationHandler: TickCallback? = null + private val cellMeasureManager = CellMeasureManagerImpl(this) private val cellPropertiesProvider = CellPropertiesProviderImpl(this) private val gestureEventHandler = GestureEventHandlerImpl(this) @@ -242,10 +251,10 @@ internal class RangeCalendarGridView( private var gestureDetector: RangeCalendarGestureDetector? = null private var animType = 0 - private var animFraction = 0f private var animator: ValueAnimator? = null + + private var animationHandler: TickCallback? = null private var onAnimationEnd: (() -> Unit)? = null - private var animationHandler: (() -> Unit)? = null private val touchHelper = TouchHelper(this) @@ -266,7 +275,7 @@ internal class RangeCalendarGridView( private var decorAnimFractionInterpolator: DecorAnimationFractionInterpolator? = null private var decorAnimatedCell = Cell.Undefined private var decorAnimatedRange = PackedIntRange(0) - private var decorAnimationHandler: (() -> Unit)? = null + private var decorAnimationHandler: TickCallback? = null init { ViewCompat.setAccessibilityDelegate(this, touchHelper) @@ -291,6 +300,7 @@ internal class RangeCalendarGridView( private fun rrRadius(): Float = style.getFloat { CELL_ROUND_RADIUS } private fun cellWidth(): Float = style.getFloat { CELL_WIDTH } private fun cellHeight(): Float = style.getFloat { CELL_HEIGHT } + private fun hoverAlpha(): Float = style.getFloat { HOVER_ALPHA } private fun selectionFill() = style.getObject { SELECTION_FILL } private fun selectionFillGradientBoundsType() = @@ -674,7 +684,7 @@ internal class RangeCalendarGridView( selectionManager.setState(intersection, cellMeasureManager) onSelectionListener?.onSelection(intersection) - if (withAnimation && selectionManager.hasTransition()) { + if (withAnimation) { startSelectionTransition() } else { invalidate() @@ -700,35 +710,75 @@ internal class RangeCalendarGridView( } } - private fun startSelectionTransition() { - val controller = selectionManager.transitionController + private fun createSelectionTransitionHandler(): TickCallback { + return TickCallback { fraction -> + selectionTransitiveState?.let { state -> + selectionManager.transitionController.handleTransition( + state, + cellMeasureManager, + fraction + ) + } + } + } - val handler = getLazyValue( + private fun getSelectionTransitionHandler(): TickCallback { + return getLazyValue( selectionTransitionHandler, - { - { - controller.handleTransition( - selectionTransitiveState!!, - cellMeasureManager, - animFraction - ) - } - }, - { selectionTransitionHandler = it } - ) - val onEnd = getLazyValue( + ::createSelectionTransitionHandler + ) { selectionTransitionHandler = it } + } + + private fun getSelectionOnEndHandler(): () -> Unit { + return getLazyValue( onSelectionTransitionEnd, { { selectionTransitiveState = null } }, { onSelectionTransitionEnd = it } ) + } + + private fun startSelectionTransition() { + val handler = getSelectionTransitionHandler() + val onEnd = getSelectionOnEndHandler() + + val selManager = selectionManager + val measureManager = cellMeasureManager + + val isSelectionAnimRunning = animType == SELECTION_ANIMATION + val prevTransitiveState = selectionTransitiveState + var newTransitiveState: SelectionState.Transitive? = null + + if (isSelectionAnimRunning && prevTransitiveState != null) { + newTransitiveState = selManager.joinTransition(prevTransitiveState, selManager.currentState, measureManager) + } + + if (newTransitiveState == null) { + newTransitiveState = selManager.createTransition(measureManager, selectionRenderOptions!!) + } + + if (newTransitiveState == null) { + // We can't create simple transition between states. We have nothing to do except calling invalidate() + // to redraw. + invalidate() + + return + } - // Before changing selectionTransitiveState, previous animation (which may be selection-like) should be stopped. - endCalendarAnimation() + // Cancel animation instead of ending it because ending the animation causes end value of the animation to be assigned + // but we don't need that because old selectionTransitiveState should not be mutated after joining transitions + // + // Call cancelCalendarAnimation() before changing selectionTransitiveState. Otherwise, it'd mutate newTransitiveState + // which is undesired. + cancelCalendarAnimation() - selectionTransitiveState = - selectionManager.createTransition(cellMeasureManager, selectionRenderOptions!!) + selectionTransitiveState = newTransitiveState - startCalendarAnimation(SELECTION_ANIMATION, handler, onEnd) + startCalendarAnimation( + SELECTION_ANIMATION, + isReversed = false, + handler, onEnd, + endPrevAnimation = false + ) } private fun setHoverCell(cell: Cell) { @@ -740,7 +790,7 @@ internal class RangeCalendarGridView( hoverCell = cell if (isHoverAnimationEnabled()) { - startCalendarAnimation(HOVER_ANIMATION) + startHoverAnimation(isReversed = false) } else { invalidate() } @@ -751,13 +801,28 @@ internal class RangeCalendarGridView( hoverCell = Cell.Undefined if (isHoverAnimationEnabled()) { - startCalendarAnimation(HOVER_ANIMATION or ANIMATION_REVERSE_BIT) + startHoverAnimation(isReversed = true) } else { invalidate() } } } + private fun handleHoverAnimation(fraction: Float) { + val alpha = hoverAlpha() * fraction + + cellHoverPaint.alpha = (alpha * 255f + 0.5f).toInt() + } + + private fun startHoverAnimation(isReversed: Boolean) { + val handler = getLazyValue( + hoverAnimationHandler, + { TickCallback { handleHoverAnimation(it) } }, + ) { hoverAnimationHandler = it } + + startCalendarAnimation(HOVER_ANIMATION, isReversed, handler) + } + fun clearSelection(fireEvent: Boolean, withAnimation: Boolean) { // No sense to clear selection if there's none. if (selectionManager.currentState.isNone) { @@ -872,6 +937,7 @@ internal class RangeCalendarGridView( startCalendarAnimation( DECOR_ANIMATION, + isReversed = false, handler = getDecorAnimationHandler(), onEnd = { val transitive = decorVisualStates[cell] as CellDecor.VisualState.Transitive @@ -935,6 +1001,7 @@ internal class RangeCalendarGridView( startCalendarAnimation( DECOR_ANIMATION, + isReversed = false, handler = getDecorAnimationHandler(), onEnd = { val transitive = decorVisualStates[cell] as CellDecor.VisualState.Transitive @@ -977,6 +1044,7 @@ internal class RangeCalendarGridView( startCalendarAnimation( DECOR_ANIMATION, + isReversed = false, handler = getDecorAnimationHandler(), onEnd = { val transitive = decorVisualStates[cell] as CellDecor.VisualState.Transitive @@ -1003,30 +1071,43 @@ internal class RangeCalendarGridView( } } + private fun cancelCalendarAnimation() { + animator?.let { + if (it.isRunning) { + it.cancel() + } + } + } + // It could be startAnimation(), but this name would interfere with View's startAnimation(Animation) private fun startCalendarAnimation( type: Int, - handler: (() -> Unit)? = null, - onEnd: (() -> Unit)? = null + isReversed: Boolean, + handler: TickCallback?, + onEnd: (() -> Unit)? = null, + endPrevAnimation: Boolean = true ) { var animator = animator - endCalendarAnimation() + + if (endPrevAnimation) { + endCalendarAnimation() + } animType = type onAnimationEnd = onEnd animationHandler = handler - animFraction = if ((animType and ANIMATION_REVERSE_BIT) != 0) 1f else 0f if (animator == null) { - animator = AnimationHelper.createFractionAnimator { value: Float -> - animFraction = value - animationHandler?.invoke() + animator = AnimationHelper.createFractionAnimator { fraction -> + //Log.i("RangeCalendarGridView", "onTick: $fraction") + animationHandler?.onTick(fraction) invalidate() } animator.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(a: Animator) { + //Log.i("RangeCalendarView", "onAnimationEnd") animType = NO_ANIMATION onAnimationEnd?.invoke() @@ -1037,7 +1118,7 @@ internal class RangeCalendarGridView( this.animator = animator } - if (type and ANIMATION_DATA_MASK == HOVER_ANIMATION) { + if (type == HOVER_ANIMATION) { animator.duration = hoverAnimationDuration().toLong() animator.interpolator = hoverAnimationInterpolator() } else { @@ -1045,7 +1126,7 @@ internal class RangeCalendarGridView( animator.interpolator = commonAnimationInterpolator() } - if ((type and ANIMATION_REVERSE_BIT) != 0) { + if (isReversed) { animator.reverse() } else { animator.start() @@ -1077,7 +1158,7 @@ internal class RangeCalendarGridView( val renderer = selectionRenderer val options = selectionRenderOptions!! - if ((animType and ANIMATION_DATA_MASK) == SELECTION_ANIMATION) { + if (animType == SELECTION_ANIMATION) { selectionTransitiveState?.let { canvas.withTranslation(x = cr.hPadding, y = gridTop()) { renderer.drawTransition(canvas, it, options) @@ -1100,19 +1181,11 @@ internal class RangeCalendarGridView( } private fun drawHover(c: Canvas) { - val isHoverAnimation = (animType and ANIMATION_DATA_MASK) == HOVER_ANIMATION + val isHoverAnimation = animType == HOVER_ANIMATION if ((isHoverAnimation && animationHoverCell.isDefined) || hoverCell.isDefined) { val cell = if (isHoverAnimation) animationHoverCell else hoverCell - var alpha = style.getFloat { HOVER_ALPHA } - - if (isHoverAnimation) { - alpha *= animFraction - } - - cellHoverPaint.alpha = (alpha * 255f + 0.5f).toInt() - val halfCellWidth = cellWidth() * 0.5f val cellHeight = cellHeight() @@ -1230,20 +1303,20 @@ internal class RangeCalendarGridView( } } - private fun getDecorAnimationHandler(): () -> Unit { + private fun getDecorAnimationHandler(): TickCallback { return getLazyValue( decorAnimationHandler, - { this::handleDecorationAnimation }, + { TickCallback { handleDecorationAnimation(it) } }, { decorAnimationHandler = it } ) } - private fun handleDecorationAnimation() { - if ((animType and ANIMATION_DATA_MASK) == DECOR_ANIMATION) { + private fun handleDecorationAnimation(fraction: Float) { + if (animType == DECOR_ANIMATION) { val state = decorVisualStates[decorAnimatedCell] if (state is CellDecor.VisualState.Transitive) { - state.handleAnimation(animFraction, decorAnimFractionInterpolator!!) + state.handleAnimation(fraction, decorAnimFractionInterpolator!!) } } } @@ -1376,6 +1449,10 @@ internal class RangeCalendarGridView( return distance } + private fun getCellDistanceByPoint(x: Float, y: Float): Float { + return (y / cellHeight()) * rowWidth() + x + } + private fun getCellAndPointByCellDistanceRelativeToGrid(distance: Float, outPoint: PointF): Int { val rw = rowWidth() @@ -1394,18 +1471,31 @@ internal class RangeCalendarGridView( return gridY * 7 + gridX } - private fun getCellByPointOnScreen(x: Float, y: Float): Cell { + private fun getCellByPointOnScreen(x: Float, y: Float, relativity: CellMeasureManager.CoordinateRelativity): Int { + var translatedX = x + var translatedY = y + val hPadding = cr.hPadding val gridTop = gridTop() - if (x < hPadding || x > width - hPadding || y < gridTop) { - return Cell.Undefined + val rowWidth = rowWidth() + val columnWidth = rowWidth / 7f + val gridHeight = height - gridTop + + if (relativity == CellMeasureManager.CoordinateRelativity.VIEW) { + // Translate to grid's coordinates + translatedX -= hPadding + translatedY -= gridTop } - val gridX = ((x - hPadding) / columnWidth()).toInt() - val gridY = ((y - gridTop) / cellHeight()).toInt() + if (translatedX !in 0f..rowWidth || translatedY !in 0f..gridHeight) { + return -1 + } - return Cell(gridY * 7 + gridX) + val gridX = (translatedX / columnWidth).toInt() + val gridY = (translatedY / cellHeight()).toInt() + + return gridY * 7 + gridX } private fun getRelativeAnchorValue(anchor: Distance.RelativeAnchor): Float { @@ -1452,9 +1542,6 @@ internal class RangeCalendarGridView( private const val TAG = "RangeCalendarGridView" - private const val ANIMATION_REVERSE_BIT = 1 shl 31 - private const val ANIMATION_DATA_MASK = ANIMATION_REVERSE_BIT.inv() - private const val NO_ANIMATION = 0 private const val SELECTION_ANIMATION = 1 private const val HOVER_ANIMATION = 2 diff --git a/library/src/main/java/com/github/pelmenstar1/rangecalendar/gesture/RangeCalendarGestureDetector.kt b/library/src/main/java/com/github/pelmenstar1/rangecalendar/gesture/RangeCalendarGestureDetector.kt index cc9f41a..fc48d4f 100644 --- a/library/src/main/java/com/github/pelmenstar1/rangecalendar/gesture/RangeCalendarGestureDetector.kt +++ b/library/src/main/java/com/github/pelmenstar1/rangecalendar/gesture/RangeCalendarGestureDetector.kt @@ -70,10 +70,12 @@ abstract class RangeCalendarGestureDetector { cellPropertiesProvider.isSelectableCell(cell) /** - * Shortcut for `measureManager.getCellAt(x, y)` + * Shortcut for `measureManager.getCellAt(x, y, CellMeasureManager.Coordinate.VIEW)`. + * + * The relativity is [CellMeasureManager.CoordinateRelativity.VIEW] by default as the coordinates in [MotionEvent]s are relative to the view. */ protected fun getCellAt(x: Float, y: Float): Int = - measureManager.getCellAt(x, y) + measureManager.getCellAt(x, y, CellMeasureManager.CoordinateRelativity.VIEW) /** * Shortcut for `gestureEventHandler.reportStartHovering(cell)` diff --git a/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/DefaultSelectionManager.kt b/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/DefaultSelectionManager.kt index 8e0b677..49c65e9 100644 --- a/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/DefaultSelectionManager.kt +++ b/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/DefaultSelectionManager.kt @@ -68,20 +68,25 @@ internal class DefaultSelectionManager : SelectionManager { val startLeft = measureManager.getCellLeft(rangeStart) val startTop = measureManager.getCellTop(rangeStart) - val endRight = measureManager.getCellLeft(rangeEnd) + cellWidth - val endTop = measureManager.getCellTop(rangeEnd) + val endRight: Float + val endTop: Float + + if (rangeStart == rangeEnd) { + endRight = startLeft + cellWidth + endTop = startTop + } else { + endRight = measureManager.getCellLeft(rangeEnd) + cellWidth + endTop = measureManager.getCellTop(rangeEnd) + } val firstCellOnRowLeft = measureManager.getCellLeft(0) val lastCellOnRowRight = measureManager.getCellLeft(6) + cellWidth - val gridTop = measureManager.getCellTop(0) - val pathInfo = SelectionShapeInfo( range = CellRange(rangeStart, rangeEnd), startLeft, startTop, endRight, endTop, firstCellOnRowLeft, lastCellOnRowRight, - gridTop, cellWidth, cellHeight, measureManager.roundRadius ) @@ -94,22 +99,10 @@ internal class DefaultSelectionManager : SelectionManager { _currentState = state } - override fun hasTransition(): Boolean { - val prevState = _prevState - val currentState = _currentState - - return when { - // If previous state is none, it can be transitioned to any state except none. - prevState.range.isInvalid -> currentState.range.isValid - - else -> true - } - } - override fun createTransition( measureManager: CellMeasureManager, options: SelectionRenderOptions - ): SelectionState.Transitive { + ): SelectionState.Transitive? { val prevState = _prevState val currentState = _currentState @@ -119,51 +112,147 @@ internal class DefaultSelectionManager : SelectionManager { val currentStart = currentState.rangeStart val currentEnd = currentState.rangeEnd - // previous state is none. - return if (prevStart > prevEnd) { - // transition between none and none is forbidden. - if (currentStart > currentEnd) { - throw IllegalStateException("$prevState can't be transitioned to $currentState by this manager") + return when { + // previous state is none + prevStart > prevEnd -> when { + // There's no transition between none and none. + currentStart > currentEnd -> null + + // current state is single-cell + currentStart == currentEnd -> createCellAppearTransition(currentState, options, isReversed = false) + else -> createRangeToRangeTransition(prevState, currentState, measureManager) } - // current state is single-cell. - if (currentStart == currentEnd) { - createCellAppearTransition(currentState, options, isReversed = false) - } else { - createRangeToRangeTransition(prevState, currentState, measureManager) + // previous state is single-cell + prevStart == prevEnd -> when { + // current state is none + currentStart > currentEnd -> createCellAppearTransition(prevState, options, isReversed = true) + + // current state is single-cell + currentStart == currentEnd -> { + val prevCell = Cell(prevStart) + val currentCell = Cell(currentStart) + + if (prevCell.sameX(currentCell) || prevCell.sameY(currentCell)) { + createCellMoveToCellTransition(prevState, currentState) + } else { + createDualCellAppearTransition(prevState, currentState, options) + } + } + + else -> createRangeToRangeTransition(prevState, currentState, measureManager) } - } else if (prevStart == prevEnd) { // previous state is single-cell - // current state is none. - return if (currentStart > currentEnd) { - createCellAppearTransition(prevState, options, isReversed = true) - } else if (currentStart == currentEnd) { // current state is single-cell - val prevCell = Cell(prevStart) - val currentCell = Cell(currentStart) - - if (prevCell.sameX(currentCell) || prevCell.sameY(currentCell)) { - DefaultSelectionState.CellMoveToCell(prevState, currentState) + + else -> { + // current state is none + if (currentStart > currentEnd) { + DefaultSelectionState.AppearAlpha(prevState, isReversed = true) } else { - when (options.cellAnimationType) { - CellAnimationType.ALPHA -> - DefaultSelectionState.DualAlpha(prevState, currentState) + createRangeToRangeTransition(prevState, currentState, measureManager) + } + } + } + } - CellAnimationType.BUBBLE -> - DefaultSelectionState.CellDualBubble(prevState, currentState) + override fun joinTransition( + current: SelectionState.Transitive, + end: SelectionState, + measureManager: CellMeasureManager + ): SelectionState.Transitive? { + end as DefaultSelectionState + + val endStateStart = end.rangeStart + val endStateEnd = end.rangeEnd + + return when (current) { + is DefaultSelectionState.RangeToRange -> { + if (endStateStart > endStateEnd) { // end state is none + DefaultSelectionState.AppearAlpha(current, isReversed = true) + } else { // end state is single cell + val endStateStartCellDist = measureManager.getCellDistance(endStateStart) + + var endStateEndCellDist = if (endStateStart == endStateEnd) { + // Do not compute cell distance of endStateEnd if we already know endStateStartCellDist + endStateStartCellDist + } else { + measureManager.getCellDistance(endStateEnd) } + + endStateEndCellDist += measureManager.cellWidth + + DefaultSelectionState.RangeToRange( + current, end, + current.currentStartCellDistance, current.currentEndCellDistance, + endStateStartCellDist, endStateEndCellDist, + current.shapeInfo // reuse shapeInfo of current + ) } - } else { - createRangeToRangeTransition(prevState, currentState, measureManager) } - } else { - // current state is none - if (currentStart > currentEnd) { - DefaultSelectionState.AppearAlpha(prevState, isReversed = true) - } else { - createRangeToRangeTransition(prevState, currentState, measureManager) + + is DefaultSelectionState.CellMoveToCell -> { + if (endStateStart > endStateEnd) { // end state is none + DefaultSelectionState.AppearAlpha(current, isReversed = true) + } else { + val currentStateStart = current.start.shapeInfo.range.start + val currentStateEnd = current.end.shapeInfo.range.end + + val currentStateStartY = currentStateStart.gridY + val currentStateEndY = currentStateEnd.gridY + + var result: SelectionState.Transitive? = null + + if (endStateStart == endStateEnd) { // end state is single cell + // Create CellMoveToCell only if end cell is on the same row/column as current CellMoveToCell state. + val canCreateCellMoveToCell = if (currentStateStartY == currentStateEndY) { + currentStateEndY == Cell(endStateStart).gridY + } else { + currentStateStart.gridX == Cell(endStateStart).gridX + } + + if (canCreateCellMoveToCell) { + result = DefaultSelectionState.CellMoveToCell(current, end, current.shapeInfo.clone()) + } + } else { + val cellGridY = Cell(endStateStart).gridY + + if (currentStateStartY == currentStateEndY && currentStateStartY == cellGridY) { + val currentShapeInfo = current.shapeInfo + val cellWidth = measureManager.cellWidth + + val currentStateStartDist = measureManager.getCellDistanceByPoint( + currentShapeInfo.startLeft, currentShapeInfo.startTop + ) + val currentStateEndDist = currentStateStartDist + cellWidth + + val endStateStartCellDist = measureManager.getCellDistance(endStateStart) + val endStateEndCellDist = measureManager.getCellDistance(endStateEnd) + cellWidth + + result = DefaultSelectionState.RangeToRange( + current, end, + currentStateStartDist, currentStateEndDist, + endStateStartCellDist, endStateEndCellDist, + current.shapeInfo + ) + } + } + + // Fallback to using dual alpha when we can't use anything else. + if (result == null) { + result = DefaultSelectionState.DualAlpha(current, end) + } + + result + } } + + else -> return null } } + private fun isCellMoveToCellTransitionOnRow(state: DefaultSelectionState.CellMoveToCell): Boolean { + return state.start.range.start.sameY(state.end.range.start) + } + private fun createCellAppearTransition( state: DefaultSelectionState, options: SelectionRenderOptions, @@ -175,6 +264,40 @@ internal class DefaultSelectionManager : SelectionManager { } } + private fun createDualCellAppearTransition( + prevState: DefaultSelectionState, + currentState: DefaultSelectionState, + options: SelectionRenderOptions + ): SelectionState.Transitive { + return when (options.cellAnimationType) { + CellAnimationType.ALPHA -> DefaultSelectionState.DualAlpha(prevState, currentState) + CellAnimationType.BUBBLE -> DefaultSelectionState.CellDualBubble(prevState, currentState) + } + } + + private fun createCellMoveToCellTransition( + startState: SelectionShapeBasedState, + endState: SelectionShapeBasedState + ): DefaultSelectionState.CellMoveToCell { + val startShapeInfo = startState.shapeInfo + + val startLeft = startShapeInfo.startLeft + val startTop = startShapeInfo.startTop + val cellWidth = startShapeInfo.cellWidth + + val shapeInfo = SelectionShapeInfo( + range = startShapeInfo.range, + startLeft, startTop, + endRight = startLeft + cellWidth, endTop = startTop, + firstCellOnRowLeft = 0f, lastCellOnRowRight = 0f, + + cellWidth, cellHeight = startShapeInfo.cellHeight, + roundRadius = startShapeInfo.roundRadius + ) + + return DefaultSelectionState.CellMoveToCell(startState, endState, shapeInfo) + } + private fun createRangeToRangeTransition( prevState: DefaultSelectionState, currentState: DefaultSelectionState, @@ -187,32 +310,48 @@ internal class DefaultSelectionManager : SelectionManager { val (prevStart, prevEnd) = prevRange val (currentStart, currentEnd) = currentRange - val cw = measureManager.cellWidth - - val startStateStartCellDistance = measureManager.getCellDistance(prevStart.index) - val startStateEndCellDistance = measureManager.getCellDistance(prevEnd.index) + cw - - val endStateStartCellDistance = measureManager.getCellDistance(currentStart.index) - val endStateEndCellDistance = measureManager.getCellDistance(currentEnd.index) + cw - val prevShapeInfo = prevState.shapeInfo + val cw = prevShapeInfo.cellWidth - val shapeInfo = SelectionShapeInfo().apply { - // These are not supposed to be changed during the animation. Init them now. - firstCellOnRowLeft = prevShapeInfo.firstCellOnRowLeft - lastCellOnRowRight = prevShapeInfo.lastCellOnRowRight - gridTop = prevShapeInfo.gridTop + // Do not compute the cell distance if we already know one. + val startStateStartCellDist = measureManager.getCellDistance(prevStart.index) + var startStateEndCellDist = if (prevStart.index == prevEnd.index) { + startStateStartCellDist + } else { + measureManager.getCellDistance(prevEnd.index) + } - cellWidth = cw - cellHeight = measureManager.cellHeight + // end cell distance should point to the right side of the cell + startStateEndCellDist += cw + + val endStateStartCellDist = if (currentStart.index == prevStart.index) { + startStateStartCellDist + } else { + measureManager.getCellDistance(currentStart.index) + } - roundRadius = measureManager.roundRadius + val endStateEndCellDist = if (currentEnd.index == prevEnd.index) { + startStateEndCellDist + } else { + measureManager.getCellDistance(currentEnd.index) + cw } + val shapeInfo = SelectionShapeInfo( + // range, startLeft, startTop, endRight, endTop are changed on the first transition frame. + // Do not init them. + range = CellRange.Invalid, + startLeft = 0f, startTop = 0f, + endRight = 0f, endTop = 0f, + firstCellOnRowLeft = prevShapeInfo.firstCellOnRowLeft, + lastCellOnRowRight = prevShapeInfo.lastCellOnRowRight, + cellWidth = cw, cellHeight = prevShapeInfo.cellHeight, + roundRadius = prevShapeInfo.roundRadius + ) + DefaultSelectionState.RangeToRange( prevState, currentState, - startStateStartCellDistance, startStateEndCellDistance, - endStateStartCellDistance, endStateEndCellDistance, + startStateStartCellDist, startStateEndCellDist, + endStateStartCellDist, endStateEndCellDist, shapeInfo ) } else { diff --git a/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/DefaultSelectionRenderer.kt b/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/DefaultSelectionRenderer.kt index c5809a6..de6472f 100644 --- a/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/DefaultSelectionRenderer.kt +++ b/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/DefaultSelectionRenderer.kt @@ -59,11 +59,11 @@ internal class DefaultSelectionRenderer : SelectionRenderer { } is DefaultSelectionState.CellMoveToCell -> { - val shapeInfo = state.start.shapeInfo + val shapeInfo = state.shapeInfo val width = shapeInfo.cellWidth val height = shapeInfo.cellHeight - drawRect(canvas, state.left, state.top, width, height, options, alpha = 1f) + drawRect(canvas, shapeInfo.startLeft, shapeInfo.startTop, width, height, options, alpha = 1f) } is DefaultSelectionState.RangeToRange -> { @@ -97,7 +97,7 @@ internal class DefaultSelectionRenderer : SelectionRenderer { private fun drawRange( canvas: Canvas, - state: DefaultSelectionState, + state: SelectionShapeBasedState, options: SelectionRenderOptions, alpha: Float, isPrimary: Boolean diff --git a/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/DefaultSelectionState.kt b/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/DefaultSelectionState.kt index 0737488..96b0bf0 100644 --- a/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/DefaultSelectionState.kt +++ b/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/DefaultSelectionState.kt @@ -1,9 +1,15 @@ package com.github.pelmenstar1.rangecalendar.selection import android.graphics.RectF -import com.github.pelmenstar1.rangecalendar.utils.rangeContains -internal class DefaultSelectionState(val shapeInfo: SelectionShapeInfo) : SelectionState { +internal interface SelectionShapeBasedState : SelectionState { + val shapeInfo: SelectionShapeInfo +} + +internal val SelectionShapeBasedState.range: CellRange + get() = shapeInfo.range + +internal class DefaultSelectionState(override val shapeInfo: SelectionShapeInfo) : SelectionShapeBasedState { override val rangeStart: Int get() = shapeInfo.range.start.index @@ -11,21 +17,33 @@ internal class DefaultSelectionState(val shapeInfo: SelectionShapeInfo) : Select get() = shapeInfo.range.end.index class RangeToRange( - override val start: DefaultSelectionState, - override val end: DefaultSelectionState, + override val start: SelectionShapeBasedState, + override val end: SelectionShapeBasedState, val startStateStartCellDistance: Float, val startStateEndCellDistance: Float, val endStateStartCellDistance: Float, val endStateEndCellDistance: Float, - val shapeInfo: SelectionShapeInfo - ) : SelectionState.Transitive { + override val shapeInfo: SelectionShapeInfo + ) : SelectionShapeBasedState, SelectionState.Transitive { + override val rangeStart: Int + get() = shapeInfo.range.start.index + + override val rangeEnd: Int + get() = shapeInfo.range.end.index + + override val isRangeDefined: Boolean + get() = true + + var currentStartCellDistance = 0f + var currentEndCellDistance = 0f + override fun overlaysCell(cellIndex: Int): Boolean { return shapeInfo.range.contains(Cell(cellIndex)) } } class AppearAlpha( - val baseState: DefaultSelectionState, + val baseState: SelectionShapeBasedState, val isReversed: Boolean ) : SelectionState.Transitive { override val start: SelectionState @@ -34,6 +52,15 @@ internal class DefaultSelectionState(val shapeInfo: SelectionShapeInfo) : Select override val end: SelectionState get() = baseState + override val rangeStart: Int + get() = baseState.rangeStart + + override val rangeEnd: Int + get() = baseState.rangeEnd + + override val isRangeDefined: Boolean + get() = true + var alpha = 0f override fun overlaysCell(cellIndex: Int): Boolean { @@ -42,12 +69,21 @@ internal class DefaultSelectionState(val shapeInfo: SelectionShapeInfo) : Select } class DualAlpha( - override val start: DefaultSelectionState, - override val end: DefaultSelectionState + override val start: SelectionShapeBasedState, + override val end: SelectionShapeBasedState ) : SelectionState.Transitive { var startAlpha = Float.NaN var endAlpha = Float.NaN + override val rangeStart: Int + get() = throwUndefinedRange() + + override val rangeEnd: Int + get() = throwUndefinedRange() + + override val isRangeDefined: Boolean + get() = false + override fun overlaysCell(cellIndex: Int): Boolean { return start.contains(Cell(cellIndex)) || end.contains(Cell(cellIndex)) } @@ -63,6 +99,15 @@ internal class DefaultSelectionState(val shapeInfo: SelectionShapeInfo) : Select override val end: SelectionState get() = baseState + override val isRangeDefined: Boolean + get() = true + + override val rangeStart: Int + get() = baseState.rangeStart + + override val rangeEnd: Int + get() = baseState.rangeEnd + val bounds = RectF() override fun overlaysCell(cellIndex: Int): Boolean { @@ -74,40 +119,39 @@ internal class DefaultSelectionState(val shapeInfo: SelectionShapeInfo) : Select override val start: DefaultSelectionState, override val end: DefaultSelectionState ) : SelectionState.Transitive { + override val isRangeDefined: Boolean + get() = false + + override val rangeStart: Int + get() = throwUndefinedRange() + + override val rangeEnd: Int + get() = throwUndefinedRange() + val startBounds = RectF() val endBounds = RectF() override fun overlaysCell(cellIndex: Int): Boolean { - return start.rangeStart == cellIndex || end.rangeEnd == cellIndex + return start.rangeStart == cellIndex || end.rangeStart == cellIndex } } class CellMoveToCell( - override val start: DefaultSelectionState, - override val end: DefaultSelectionState - ) : SelectionState.Transitive { - var left: Float = Float.NaN - var top: Float = Float.NaN - - override fun overlaysCell(cellIndex: Int): Boolean { - val startCell = start.startCell - val endCell = end.startCell + override val start: SelectionShapeBasedState, + override val end: SelectionShapeBasedState, + override val shapeInfo: SelectionShapeInfo + ) : SelectionShapeBasedState, SelectionState.Transitive { + override val isRangeDefined: Boolean + get() = true - val cellX = Cell(cellIndex).gridX - val cellY = Cell(cellIndex).gridY + override val rangeStart: Int + get() = shapeInfo.range.start.index - val startX = startCell.gridX - val startY = startCell.gridY + override val rangeEnd: Int + get() = rangeStart - val endX = endCell.gridX - val endY = endCell.gridY - - return if (startY == endY) { - cellY == startY && rangeContains(startX, endX, cellX) - } else { - // Then cells have same x on grid. - cellX == startX && rangeContains(startY, endY, cellY) - } + override fun overlaysCell(cellIndex: Int): Boolean { + return rangeStart == cellIndex } } @@ -128,5 +172,9 @@ internal class DefaultSelectionState(val shapeInfo: SelectionShapeInfo) : Select companion object { val None = DefaultSelectionState(SelectionShapeInfo()) + + private fun throwUndefinedRange(): Nothing { + throw IllegalStateException("The selection transitive state doesn't have defined range") + } } } \ No newline at end of file diff --git a/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/DefaultSelectionTransitionController.kt b/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/DefaultSelectionTransitionController.kt index 444b0cc..940541e 100644 --- a/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/DefaultSelectionTransitionController.kt +++ b/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/DefaultSelectionTransitionController.kt @@ -2,13 +2,18 @@ package com.github.pelmenstar1.rangecalendar.selection import android.graphics.PointF import android.graphics.RectF +import android.util.Log import androidx.core.graphics.component1 import androidx.core.graphics.component2 import com.github.pelmenstar1.rangecalendar.CellMeasureManager import com.github.pelmenstar1.rangecalendar.utils.lerp class DefaultSelectionTransitionController : SelectionTransitionController { - override fun handleTransition(state: SelectionState.Transitive, measureManager: CellMeasureManager, fraction: Float) { + override fun handleTransition( + state: SelectionState.Transitive, + measureManager: CellMeasureManager, + fraction: Float + ) { when (state) { // Cell transitions is DefaultSelectionState.AppearAlpha -> { @@ -32,17 +37,33 @@ class DefaultSelectionTransitionController : SelectionTransitionController { } is DefaultSelectionState.CellMoveToCell -> { - val start = state.start.shapeInfo - val end = state.end.shapeInfo + val startShapeInfo = state.start.shapeInfo + val endShapeInfo = state.end.shapeInfo + val currentShapeInfo = state.shapeInfo - state.left = lerp(start.startLeft, end.startLeft, fraction) - state.top = lerp(start.startTop, end.startTop, fraction) + val currentLeft = lerp(startShapeInfo.startLeft, endShapeInfo.startLeft, fraction) + val currentTop = lerp(startShapeInfo.startTop, endShapeInfo.startTop, fraction) + + currentShapeInfo.startLeft = currentLeft + currentShapeInfo.startTop = currentTop + currentShapeInfo.endRight = currentLeft + currentShapeInfo.cellWidth + + val cell = measureManager.getCellAt( + currentLeft, currentTop, + relativity = CellMeasureManager.CoordinateRelativity.GRID + ) + + currentShapeInfo.range = CellRange.single(cell) } is DefaultSelectionState.RangeToRange -> { - val newStartCellDist = lerp(state.startStateStartCellDistance, state.endStateStartCellDistance, fraction) + val newStartCellDist = + lerp(state.startStateStartCellDistance, state.endStateStartCellDistance, fraction) val newEndCellDist = lerp(state.startStateEndCellDistance, state.endStateEndCellDistance, fraction) + state.currentStartCellDistance = newStartCellDist + state.currentEndCellDistance = newEndCellDist + val newStartCell = measureManager.getCellAndPointByDistance(newStartCellDist, point) val (newStartCellLeft, newStartCellTop) = point diff --git a/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/SelectionManager.kt b/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/SelectionManager.kt index 7994b73..0f00ba2 100644 --- a/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/SelectionManager.kt +++ b/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/SelectionManager.kt @@ -48,20 +48,28 @@ interface SelectionManager { */ fun updateConfiguration(measureManager: CellMeasureManager) - /** - * Returns whether there is a transition between [previousState] and [currentState]. - */ - fun hasTransition(): Boolean - /** * Creates a transitive state between [previousState] and [currentState]. - * - * It should only be called if [hasTransition] returns true. + * If there's no transition between states, returns `null`. */ fun createTransition( measureManager: CellMeasureManager, options: SelectionRenderOptions - ): SelectionState.Transitive + ): SelectionState.Transitive? + + /** + * Transforms the [current] transition and [end] state in such way that the end state of [current] transition become given [end] state. + * If there's no such transition, returns `null`. + * Note that the resulting transition may be used in [joinTransition] again as a [current] transition. + * + * The implementation is allowed to mutate and reuse internal information of [current] and/or [end] states. + * Thus, after creating the joined transition, the calling code can't use [current] and [end] states. + */ + fun joinTransition( + current: SelectionState.Transitive, + end: SelectionState, + measureManager: CellMeasureManager + ): SelectionState.Transitive? } internal fun SelectionManager.setState( diff --git a/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/SelectionShapeInfo.kt b/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/SelectionShapeInfo.kt index 1342f8f..547dc09 100644 --- a/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/SelectionShapeInfo.kt +++ b/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/SelectionShapeInfo.kt @@ -1,19 +1,18 @@ package com.github.pelmenstar1.rangecalendar.selection -internal data class SelectionShapeInfo( +internal class SelectionShapeInfo( var range: CellRange, - var startLeft: Float, - var startTop: Float, - var endRight: Float, - var endTop: Float, - var firstCellOnRowLeft: Float, - var lastCellOnRowRight: Float, - var gridTop: Float, - var cellWidth: Float, - var cellHeight: Float, - var roundRadius: Float, + @JvmField var startLeft: Float, + @JvmField var startTop: Float, + @JvmField var endRight: Float, + @JvmField var endTop: Float, + @JvmField var firstCellOnRowLeft: Float, + @JvmField var lastCellOnRowRight: Float, + @JvmField var cellWidth: Float, + @JvmField var cellHeight: Float, + @JvmField var roundRadius: Float, ) { - constructor() : this(CellRange.Invalid, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f) + constructor() : this(CellRange.Invalid, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f) fun set(other: SelectionShapeInfo) { range = other.range @@ -23,9 +22,42 @@ internal data class SelectionShapeInfo( endTop = other.endTop firstCellOnRowLeft = other.firstCellOnRowLeft lastCellOnRowRight = other.lastCellOnRowRight - gridTop = other.gridTop cellWidth = other.cellWidth cellHeight = other.cellHeight roundRadius = other.roundRadius } + + override fun equals(other: Any?): Boolean { + if (other === this) return true + if (other == null || javaClass != other.javaClass) return false + + other as SelectionShapeInfo + + return range == other.range && + startLeft == other.startLeft && startTop == other.startTop && + endRight == other.endRight && endTop == other.endTop && + firstCellOnRowLeft == other.firstCellOnRowLeft && lastCellOnRowRight == other.lastCellOnRowRight && + cellWidth == other.cellWidth && cellHeight == other.cellHeight && + roundRadius == other.roundRadius + + } + + override fun hashCode(): Int { + var result = range.hashCode() + result = result * 31 + startLeft.toBits() + result = result * 31 + startTop.toBits() + result = result * 31 + endRight.toBits() + result = result * 31 + endTop.toBits() + result = result * 31 + firstCellOnRowLeft.toBits() + result = result * 31 + lastCellOnRowRight.toBits() + result = result * 31 + cellWidth.toBits() + result = result * 31 + cellHeight.toBits() + result = result * 31 + roundRadius.toBits() + + return result + } + + fun clone(): SelectionShapeInfo { + return SelectionShapeInfo().also { it.set(this) } + } } \ No newline at end of file diff --git a/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/SelectionState.kt b/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/SelectionState.kt index 112c7c8..328e7ac 100644 --- a/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/SelectionState.kt +++ b/library/src/main/java/com/github/pelmenstar1/rangecalendar/selection/SelectionState.kt @@ -1,20 +1,21 @@ package com.github.pelmenstar1.rangecalendar.selection /** - * A set of properties which are needed to represent selection state. + * A set of properties which are needed to represent a selection. + * The data in the implementation should be enough to render the state on a canvas. * - * Implementation of the interface is excepted to be immutable. - * Also, data in the implementation should be enough to render the state on canvas. - * Exception is only transition between state. + * The implementations is excepted to be immutable. */ interface SelectionState { /** * Represents a transitive state between two [SelectionState] instances. * - * It does not implements [SelectionState] interface, because it might not have any selection type or definitive range. - * But it should contain enough information to render itself on canvas. + * It still implements [SelectionState], although the transitive state is allowed not to have a notion of + * a cell range on which the visual representation of the state is located because this range might be undefined. + * + * The implementation is expected to be mutable. */ - interface Transitive { + interface Transitive : SelectionState { /** * The state from which the transition starts. */ @@ -25,6 +26,12 @@ interface SelectionState { */ val end: SelectionState + /** + * Returns whether the transitive state has the range ([rangeStart] and [rangeEnd] properties) defined. + * If it returns `false`, [rangeStart] and [rangeEnd] properties are expected to throw. + */ + val isRangeDefined: Boolean + /** * Determines whether selection determined by the transitive state overlays a cell specified by [cellIndex]. * diff --git a/library/src/main/java/com/github/pelmenstar1/rangecalendar/utils/MathUtils.kt b/library/src/main/java/com/github/pelmenstar1/rangecalendar/utils/MathUtils.kt index 2ab1517..40f7a21 100644 --- a/library/src/main/java/com/github/pelmenstar1/rangecalendar/utils/MathUtils.kt +++ b/library/src/main/java/com/github/pelmenstar1/rangecalendar/utils/MathUtils.kt @@ -1,8 +1,6 @@ package com.github.pelmenstar1.rangecalendar.utils import kotlin.math.ceil -import kotlin.math.max -import kotlin.math.min import kotlin.math.sqrt /** @@ -15,17 +13,6 @@ internal inline fun ceilToInt(value: Float): Int { return ceil(value.toDouble()).toInt() } -/** - * Determines whether range `[start; endInclusive]` contains given [value]. - * The method correctly handles a case when `start > endInclusive` - */ -internal fun rangeContains(start: Int, endInclusive: Int, value: Int): Boolean { - val s = min(start, endInclusive) - val e = max(start, endInclusive) - - return value in s..e -} - internal fun floorMod(x: Long, y: Long): Long { val r = x / y var aligned = r * y @@ -54,7 +41,7 @@ internal fun lerpFloatArray( } } -fun getDistance(x0: Float, y0: Float, x1: Float, y1: Float): Float { +internal fun getDistance(x0: Float, y0: Float, x1: Float, y1: Float): Float { val dx = x0 - x1 val dy = y0 - y1