Snap for 12770256 from 31161792a2727f369781cf663d4caa60defabcac to 25Q1-release Change-Id: I6966f72db1c68d7b39d44c3393a3c71626e2e323
diff --git a/mechanics/src/com/android/mechanics/GestureContext.kt b/mechanics/src/com/android/mechanics/GestureContext.kt new file mode 100644 index 0000000..00665f8 --- /dev/null +++ b/mechanics/src/com/android/mechanics/GestureContext.kt
@@ -0,0 +1,171 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.mechanics + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.android.mechanics.spec.InputDirection +import kotlin.math.max +import kotlin.math.min + +/** + * Gesture-specific context to augment [MotionValue.currentInput]. + * + * This context helps to capture the user's intent, and should be provided to [MotionValue]s that + * respond to a user gesture. + */ +@Stable +interface GestureContext { + + /** + * The intrinsic direction of the [MotionValue.currentInput]. + * + * This property determines which of the [DirectionalMotionSpec] from the [MotionSpec] is used, + * and also prevents flip-flopping of the output value on tiny input-changes around a + * breakpoint. + * + * If the [MotionValue.currentInput] is driven - directly or indirectly - by a user gesture, + * this property should only change direction after the gesture travelled a significant distance + * in the opposite direction. + * + * @see DistanceGestureContext for a default implementation. + */ + val direction: InputDirection + + /** + * The gesture distance of the current gesture, in pixels. + * + * Used solely for the [GestureDistance] [Guarantee]. Can be hard-coded to a static value if + * this type of [Guarantee] is not used. + */ + val distance: Float +} + +/** [GestureContext] implementation for manually set values. */ +class ProvidedGestureContext(direction: InputDirection, distance: Float) : GestureContext { + override var direction by mutableStateOf(direction) + override var distance by mutableFloatStateOf(distance) +} + +/** + * [GestureContext] driven by a gesture distance. + * + * The direction is determined from the gesture input, where going further than + * [directionChangeSlop] in the opposite direction toggles the direction. + * + * @param initialDistance The initial [distance] of the [GestureContext] + * @param initialDirection The initial [direction] of the [GestureContext] + * @param directionChangeSlop the amount [distance] must be moved in the opposite direction for the + * [direction] to flip. + */ +class DistanceGestureContext( + initialDistance: Float, + initialDirection: InputDirection, + directionChangeSlop: Float, +) : GestureContext { + init { + require(directionChangeSlop > 0) { + "directionChangeSlop must be greater than 0, was $directionChangeSlop" + } + } + + override var direction by mutableStateOf(initialDirection) + private set + + private var furthestDistance by mutableFloatStateOf(initialDistance) + private var _distance by mutableFloatStateOf(initialDistance) + + override var distance: Float + get() = _distance + /** + * Updates the [distance]. + * + * This flips the [direction], if the [value] is further than [directionChangeSlop] away + * from the furthest recorded value regarding to the current [direction]. + */ + set(value) { + _distance = value + this.direction = + when (direction) { + InputDirection.Max -> { + if (furthestDistance - value > directionChangeSlop) { + furthestDistance = value + InputDirection.Min + } else { + furthestDistance = max(value, furthestDistance) + InputDirection.Max + } + } + + InputDirection.Min -> { + if (value - furthestDistance > directionChangeSlop) { + furthestDistance = value + InputDirection.Max + } else { + furthestDistance = min(value, furthestDistance) + InputDirection.Min + } + } + } + } + + private var _directionChangeSlop by mutableFloatStateOf(directionChangeSlop) + + var directionChangeSlop: Float + get() = _directionChangeSlop + + /** + * This flips the [direction], if the current [direction] is further than the new + * directionChangeSlop [value] away from the furthest recorded value regarding to the + * current [direction]. + */ + set(value) { + require(value > 0) { "directionChangeSlop must be greater than 0, was $value" } + + _directionChangeSlop = value + + when (direction) { + InputDirection.Max -> { + if (furthestDistance - distance > directionChangeSlop) { + furthestDistance = distance + direction = InputDirection.Min + } + } + InputDirection.Min -> { + if (distance - furthestDistance > directionChangeSlop) { + furthestDistance = value + direction = InputDirection.Max + } + } + } + } + + /** + * Sets [distance] and [direction] to the specified values. + * + * This also resets memoized [furthestDistance], which is used to determine the direction + * change. + */ + fun reset(distance: Float, direction: InputDirection) { + this.distance = distance + this.direction = direction + this.furthestDistance = distance + } +}
diff --git a/mechanics/src/com/android/mechanics/spec/InputDirection.kt b/mechanics/src/com/android/mechanics/spec/InputDirection.kt index 5ff77e7..58fa590 100644 --- a/mechanics/src/com/android/mechanics/spec/InputDirection.kt +++ b/mechanics/src/com/android/mechanics/spec/InputDirection.kt
@@ -25,7 +25,7 @@ * The directions are labelled [Min] and [Max] to reflect descending and ascending input values * respectively, but it does not imply an spatial direction. */ -enum class InputDirection { - Min, - Max, +enum class InputDirection(val sign: Int) { + Min(sign = -1), + Max(sign = +1), }
diff --git a/mechanics/src/com/android/mechanics/spec/MotionSpec.kt b/mechanics/src/com/android/mechanics/spec/MotionSpec.kt index 7471398..4bd4240 100644 --- a/mechanics/src/com/android/mechanics/spec/MotionSpec.kt +++ b/mechanics/src/com/android/mechanics/spec/MotionSpec.kt
@@ -56,6 +56,63 @@ val resetSpring: SpringParameters = DefaultResetSpring, val segmentHandlers: Map<SegmentKey, OnChangeSegmentHandler> = emptyMap(), ) { + + /** The [DirectionalMotionSpec] for the specified [direction]. */ + operator fun get(direction: InputDirection): DirectionalMotionSpec { + return when (direction) { + InputDirection.Min -> minDirection + InputDirection.Max -> maxDirection + } + } + + /** Whether this spec contains a segment with the specified [segmentKey]. */ + fun containsSegment(segmentKey: SegmentKey): Boolean { + return get(segmentKey.direction).findSegmentIndex(segmentKey) != -1 + } + + /** + * The [SegmentData] for an input with the specified [position] and [direction]. + * + * The returned [SegmentData] will be cached while [SegmentData.isValidForInput] returns `true`. + */ + fun segmentAtInput(position: Float, direction: InputDirection): SegmentData { + require(position.isFinite()) + + return with(get(direction)) { + var idx = findBreakpointIndex(position) + if (direction == InputDirection.Min && breakpoints[idx].position == position) { + // The segment starts at `position`. Since the breakpoints are sorted ascending, no + // matter the spec's direction, need to return the previous segment in the min + // direction. + idx-- + } + + SegmentData( + this@MotionSpec, + breakpoints[idx], + breakpoints[idx + 1], + direction, + mappings[idx], + ) + } + } + + /** + * Looks up the new [SegmentData] once the [currentSegment] is not valid for an input with + * [newPosition] and [newDirection]. + * + * This will delegate to the [segmentHandlers], if registered for the [currentSegment]'s key. + */ + internal fun onChangeSegment( + currentSegment: SegmentData, + newPosition: Float, + newDirection: InputDirection, + ): SegmentData { + val segmentChangeHandler = segmentHandlers[currentSegment.key] + return segmentChangeHandler?.invoke(this, currentSegment, newPosition, newDirection) + ?: segmentAtInput(newPosition, newDirection) + } + companion object { /** * Default spring parameters for the reset spring. Matches the Fast Spatial spring of the @@ -97,10 +154,11 @@ /** * Returns the index of the closest breakpoint where `Breakpoint.position <= position`. * - * Guaranteed to be a valid index into [breakpoints], and guaranteed not to be the last element. + * Guaranteed to be a valid index into [breakpoints], and guaranteed to be neither the first nor + * the last element. * * @param position the position in the input domain. - * @return Index into [breakpoints], guaranteed to be in range `0..breakpoints.size - 2` + * @return Index into [breakpoints], guaranteed to be in range `1..breakpoints.size - 2` */ fun findBreakpointIndex(position: Float): Int { require(position.isFinite()) @@ -119,6 +177,22 @@ return result } + /** + * The index of the breakpoint with the specified [breakpointKey], or `-1` if no such breakpoint + * exists. + */ + fun findBreakpointIndex(breakpointKey: BreakpointKey): Int { + return breakpoints.indexOfFirst { it.key == breakpointKey } + } + + /** Index into [mappings] for the specified [segmentKey], or `-1` if no such segment exists. */ + fun findSegmentIndex(segmentKey: SegmentKey): Int { + val result = breakpoints.indexOfFirst { it.key == segmentKey.minBreakpoint } + if (result < 0 || breakpoints[result + 1].key != segmentKey.maxBreakpoint) return -1 + + return result + } + companion object { /* Empty spec, the full input domain is mapped to output using [Mapping.identity]. */ val Empty =
diff --git a/mechanics/src/com/android/mechanics/spec/Segment.kt b/mechanics/src/com/android/mechanics/spec/Segment.kt index 29ead4d..14b1f40 100644 --- a/mechanics/src/com/android/mechanics/spec/Segment.kt +++ b/mechanics/src/com/android/mechanics/spec/Segment.kt
@@ -40,6 +40,7 @@ * respective breakpoint. */ data class SegmentData( + val spec: MotionSpec, val minBreakpoint: Breakpoint, val maxBreakpoint: Breakpoint, val direction: InputDirection, @@ -62,6 +63,18 @@ InputDirection.Min -> inputPosition > minBreakpoint.position } } + + /** + * The breakpoint at the side of the segment's start. + * + * The [entryBreakpoint]'s [Guarantee] is the relevant guarantee for this segment. + */ + val entryBreakpoint: Breakpoint + get() = + when (direction) { + InputDirection.Max -> minBreakpoint + InputDirection.Min -> maxBreakpoint + } } /**
diff --git a/mechanics/tests/src/com/android/mechanics/DistanceGestureContextTest.kt b/mechanics/tests/src/com/android/mechanics/DistanceGestureContextTest.kt new file mode 100644 index 0000000..4784f9e --- /dev/null +++ b/mechanics/tests/src/com/android/mechanics/DistanceGestureContextTest.kt
@@ -0,0 +1,151 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.mechanics + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.mechanics.spec.InputDirection +import com.google.common.truth.Truth.assertThat +import kotlin.math.nextDown +import kotlin.math.nextUp +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DistanceGestureContextTest { + + @Test + fun setDistance_maxDirection_increasingInput_keepsDirection() { + val underTest = + DistanceGestureContext( + initialDistance = 0f, + initialDirection = InputDirection.Max, + directionChangeSlop = 5f, + ) + + for (value in 0..6) { + underTest.distance = value.toFloat() + assertThat(underTest.direction).isEqualTo(InputDirection.Max) + } + } + + @Test + fun setDistance_minDirection_decreasingInput_keepsDirection() { + val underTest = + DistanceGestureContext( + initialDistance = 0f, + initialDirection = InputDirection.Min, + directionChangeSlop = 5f, + ) + + for (value in 0 downTo -6) { + underTest.distance = value.toFloat() + assertThat(underTest.direction).isEqualTo(InputDirection.Min) + } + } + + @Test + fun setDistance_maxDirection_decreasingInput_keepsDirection_belowDirectionChangeSlop() { + val underTest = + DistanceGestureContext( + initialDistance = 0f, + initialDirection = InputDirection.Max, + directionChangeSlop = 5f, + ) + + underTest.distance = -5f + assertThat(underTest.direction).isEqualTo(InputDirection.Max) + } + + @Test + fun setDistance_maxDirection_decreasingInput_switchesDirection_aboveDirectionChangeSlop() { + val underTest = + DistanceGestureContext( + initialDistance = 0f, + initialDirection = InputDirection.Max, + directionChangeSlop = 5f, + ) + + underTest.distance = (-5f).nextDown() + assertThat(underTest.direction).isEqualTo(InputDirection.Min) + } + + @Test + fun setDistance_minDirection_increasingInput_keepsDirection_belowDirectionChangeSlop() { + val underTest = + DistanceGestureContext( + initialDistance = 0f, + initialDirection = InputDirection.Min, + directionChangeSlop = 5f, + ) + + underTest.distance = 5f + assertThat(underTest.direction).isEqualTo(InputDirection.Min) + } + + @Test + fun setDistance_minDirection_decreasingInput_switchesDirection_aboveDirectionChangeSlop() { + val underTest = + DistanceGestureContext( + initialDistance = 0f, + initialDirection = InputDirection.Min, + directionChangeSlop = 5f, + ) + + underTest.distance = 5f.nextUp() + assertThat(underTest.direction).isEqualTo(InputDirection.Max) + } + + @Test + fun reset_resetsFurthestValue() { + val underTest = + DistanceGestureContext( + initialDistance = 10f, + initialDirection = InputDirection.Max, + directionChangeSlop = 1f, + ) + + underTest.reset(5f, direction = InputDirection.Max) + assertThat(underTest.direction).isEqualTo(InputDirection.Max) + assertThat(underTest.distance).isEqualTo(5f) + + underTest.distance -= 1f + assertThat(underTest.direction).isEqualTo(InputDirection.Max) + assertThat(underTest.distance).isEqualTo(4f) + + underTest.distance = underTest.distance.nextDown() + assertThat(underTest.direction).isEqualTo(InputDirection.Min) + assertThat(underTest.distance).isWithin(0.0001f).of(4f) + } + + @Test + fun setDirectionChangeSlop_smallerThanCurrentDelta_switchesDirection() { + val underTest = + DistanceGestureContext( + initialDistance = 10f, + initialDirection = InputDirection.Max, + directionChangeSlop = 5f, + ) + + underTest.distance -= 2f + assertThat(underTest.direction).isEqualTo(InputDirection.Max) + assertThat(underTest.distance).isEqualTo(8f) + + underTest.directionChangeSlop = 1f + assertThat(underTest.direction).isEqualTo(InputDirection.Min) + assertThat(underTest.distance).isEqualTo(8f) + } +}
diff --git a/mechanics/tests/src/com/android/mechanics/spec/DirectionalMotionSpecTest.kt b/mechanics/tests/src/com/android/mechanics/spec/DirectionalMotionSpecTest.kt new file mode 100644 index 0000000..d73f39b --- /dev/null +++ b/mechanics/tests/src/com/android/mechanics/spec/DirectionalMotionSpecTest.kt
@@ -0,0 +1,177 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.mechanics.spec + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.mechanics.spring.SpringParameters +import com.google.common.truth.Truth.assertThat +import kotlin.math.nextDown +import kotlin.math.nextUp +import kotlin.test.assertFailsWith +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DirectionalMotionSpecTest { + + @Test + fun noBreakpoints_throws() { + assertFailsWith<IllegalArgumentException> { + DirectionalMotionSpec(emptyList(), emptyList()) + } + } + + @Test + fun wrongSentinelBreakpoints_throws() { + val breakpoint1 = Breakpoint(B1, position = 10f, Spring, Guarantee.None) + val breakpoint2 = Breakpoint(B2, position = 20f, Spring, Guarantee.None) + + assertFailsWith<IllegalArgumentException> { + DirectionalMotionSpec(listOf(breakpoint1, breakpoint2), listOf(Mapping.Identity)) + } + } + + @Test + fun tooFewMappings_throws() { + assertFailsWith<IllegalArgumentException> { + DirectionalMotionSpec(listOf(Breakpoint.minLimit, Breakpoint.maxLimit), emptyList()) + } + } + + @Test + fun tooManyMappings_throws() { + assertFailsWith<IllegalArgumentException> { + DirectionalMotionSpec( + listOf(Breakpoint.minLimit, Breakpoint.maxLimit), + listOf(Mapping.One, Mapping.Two), + ) + } + } + + @Test + fun breakpointsOutOfOrder_throws() { + val breakpoint1 = Breakpoint(B1, position = 10f, Spring, Guarantee.None) + val breakpoint2 = Breakpoint(B2, position = 20f, Spring, Guarantee.None) + assertFailsWith<IllegalArgumentException> { + DirectionalMotionSpec( + listOf(Breakpoint.minLimit, breakpoint2, breakpoint1, Breakpoint.maxLimit), + listOf(Mapping.Zero, Mapping.One, Mapping.Two), + ) + } + } + + @Test + fun findBreakpointIndex_returnsMinForEmptySpec() { + val underTest = DirectionalMotionSpec.builder(Spring).complete() + + assertThat(underTest.findBreakpointIndex(0f)).isEqualTo(0) + assertThat(underTest.findBreakpointIndex(Float.MAX_VALUE)).isEqualTo(0) + assertThat(underTest.findBreakpointIndex(-Float.MAX_VALUE)).isEqualTo(0) + } + + @Test + fun findBreakpointIndex_throwsForNonFiniteInput() { + val underTest = DirectionalMotionSpec.builder(Spring).complete() + + assertFailsWith<IllegalArgumentException> { underTest.findBreakpointIndex(Float.NaN) } + assertFailsWith<IllegalArgumentException> { + underTest.findBreakpointIndex(Float.NEGATIVE_INFINITY) + } + assertFailsWith<IllegalArgumentException> { + underTest.findBreakpointIndex(Float.POSITIVE_INFINITY) + } + } + + @Test + fun findBreakpointIndex_atBreakpoint_returnsIndex() { + val underTest = + DirectionalMotionSpec.builder(Spring).toBreakpoint(10f).completeWith(Mapping.Identity) + + assertThat(underTest.findBreakpointIndex(10f)).isEqualTo(1) + } + + @Test + fun findBreakpointIndex_afterBreakpoint_returnsPreviousIndex() { + val underTest = + DirectionalMotionSpec.builder(Spring).toBreakpoint(10f).completeWith(Mapping.Identity) + + assertThat(underTest.findBreakpointIndex(10f.nextUp())).isEqualTo(1) + } + + @Test + fun findBreakpointIndex_beforeBreakpoint_returnsIndex() { + val underTest = + DirectionalMotionSpec.builder(Spring).toBreakpoint(10f).completeWith(Mapping.Identity) + + assertThat(underTest.findBreakpointIndex(10f.nextDown())).isEqualTo(0) + } + + @Test + fun findBreakpointIndexByKey_returnsIndex() { + val underTest = + DirectionalMotionSpec.builder(Spring) + .toBreakpoint(10f, key = B1) + .completeWith(Mapping.Identity) + + assertThat(underTest.findBreakpointIndex(B1)).isEqualTo(1) + } + + @Test + fun findBreakpointIndexByKey_unknown_returnsMinusOne() { + val underTest = + DirectionalMotionSpec.builder(Spring) + .toBreakpoint(10f, key = B1) + .completeWith(Mapping.Identity) + + assertThat(underTest.findBreakpointIndex(B2)).isEqualTo(-1) + } + + @Test + fun findSegmentIndex_returnsIndexForSegment_ignoringDirection() { + val underTest = + DirectionalMotionSpec.builder(Spring) + .toBreakpoint(10f, key = B1) + .continueWith(Mapping.One) + .toBreakpoint(20f, key = B2) + .completeWith(Mapping.Identity) + + assertThat(underTest.findSegmentIndex(SegmentKey(B1, B2, InputDirection.Max))).isEqualTo(1) + assertThat(underTest.findSegmentIndex(SegmentKey(B1, B2, InputDirection.Min))).isEqualTo(1) + } + + @Test + fun findSegmentIndex_forInvalidKeys_returnsMinusOne() { + val underTest = + DirectionalMotionSpec.builder(Spring) + .toBreakpoint(10f, key = B1) + .continueWith(Mapping.One) + .toBreakpoint(20f, key = B2) + .continueWith(Mapping.One) + .toBreakpoint(30f, key = B3) + .completeWith(Mapping.Identity) + + assertThat(underTest.findSegmentIndex(SegmentKey(B2, B1, InputDirection.Max))).isEqualTo(-1) + assertThat(underTest.findSegmentIndex(SegmentKey(B1, B3, InputDirection.Max))).isEqualTo(-1) + } + + companion object { + val B1 = BreakpointKey("one") + val B2 = BreakpointKey("two") + val B3 = BreakpointKey("three") + val Spring = SpringParameters(stiffness = 100f, dampingRatio = 1f) + } +}
diff --git a/mechanics/tests/src/com/android/mechanics/spec/FluentSpecBuilderTest.kt b/mechanics/tests/src/com/android/mechanics/spec/FluentSpecBuilderTest.kt index 697b6e1..e950bc7 100644 --- a/mechanics/tests/src/com/android/mechanics/spec/FluentSpecBuilderTest.kt +++ b/mechanics/tests/src/com/android/mechanics/spec/FluentSpecBuilderTest.kt
@@ -27,7 +27,7 @@ @Test fun directionalSpec_buildEmptySpec() { - val result = DirectionalMotionSpec.builder(spring).complete() + val result = DirectionalMotionSpec.builder(Spring).complete() assertThat(result).breakpoints().isEmpty() assertThat(result).mappings().containsExactly(Mapping.Identity) @@ -35,7 +35,7 @@ @Test fun directionalSpec_buildEmptySpec_inReverse() { - val result = DirectionalMotionSpec.reverseBuilder(spring).complete() + val result = DirectionalMotionSpec.reverseBuilder(Spring).complete() assertThat(result).breakpoints().isEmpty() assertThat(result).mappings().containsExactly(Mapping.Identity) @@ -44,15 +44,15 @@ @Test fun motionSpec_sameSpecInBothDirections() { val result = - MotionSpec.builder(spring, Mapping.Zero) - .toBreakpoint(0f, b1) + MotionSpec.builder(Spring, Mapping.Zero) + .toBreakpoint(0f, B1) .continueWith(Mapping.One) - .toBreakpoint(10f, b2) + .toBreakpoint(10f, B2) .completeWith(Mapping.Two) assertThat(result.maxDirection).isSameInstanceAs(result.minDirection) - assertThat(result.minDirection).breakpoints().keys().containsExactly(b1, b2).inOrder() + assertThat(result.minDirection).breakpoints().keys().containsExactly(B1, B2).inOrder() assertThat(result.minDirection) .mappings() .containsExactly(Mapping.Zero, Mapping.One, Mapping.Two) @@ -62,15 +62,15 @@ @Test fun directionalSpec_addBreakpointsAndMappings() { val result = - DirectionalMotionSpec.builder(spring, Mapping.Zero) - .toBreakpoint(0f, b1) + DirectionalMotionSpec.builder(Spring, Mapping.Zero) + .toBreakpoint(0f, B1) .continueWith(Mapping.One) - .toBreakpoint(10f, b2) + .toBreakpoint(10f, B2) .completeWith(Mapping.Two) - assertThat(result).breakpoints().keys().containsExactly(b1, b2).inOrder() - assertThat(result).breakpoints().withKey(b1).isAt(0f) - assertThat(result).breakpoints().withKey(b2).isAt(10f) + assertThat(result).breakpoints().keys().containsExactly(B1, B2).inOrder() + assertThat(result).breakpoints().withKey(B1).isAt(0f) + assertThat(result).breakpoints().withKey(B2).isAt(10f) assertThat(result) .mappings() .containsExactly(Mapping.Zero, Mapping.One, Mapping.Two) @@ -80,15 +80,15 @@ @Test fun directionalSpec_addBreakpointsAndMappings_inReverse() { val result = - DirectionalMotionSpec.reverseBuilder(spring, Mapping.Two) - .toBreakpoint(10f, b2) + DirectionalMotionSpec.reverseBuilder(Spring, Mapping.Two) + .toBreakpoint(10f, B2) .continueWith(Mapping.One) - .toBreakpoint(0f, b1) + .toBreakpoint(0f, B1) .completeWith(Mapping.Zero) - assertThat(result).breakpoints().keys().containsExactly(b1, b2).inOrder() - assertThat(result).breakpoints().withKey(b1).isAt(0f) - assertThat(result).breakpoints().withKey(b2).isAt(10f) + assertThat(result).breakpoints().keys().containsExactly(B1, B2).inOrder() + assertThat(result).breakpoints().withKey(B1).isAt(0f) + assertThat(result).breakpoints().withKey(B2).isAt(10f) assertThat(result) .mappings() .containsExactly(Mapping.Zero, Mapping.One, Mapping.Two) @@ -98,20 +98,20 @@ @Test fun directionalSpec_mappingBuilder_setsDefaultSpring() { val result = - DirectionalMotionSpec.builder(spring) + DirectionalMotionSpec.builder(Spring) .toBreakpoint(10f) .jumpTo(20f) .continueWithConstantValue() .complete() - assertThat(result).breakpoints().atPosition(10f).spring().isEqualTo(spring) + assertThat(result).breakpoints().atPosition(10f).spring().isEqualTo(Spring) } @Test fun directionalSpec_mappingBuilder_canOverrideDefaultSpring() { val otherSpring = SpringParameters(stiffness = 10f, dampingRatio = 0.1f) val result = - DirectionalMotionSpec.builder(spring) + DirectionalMotionSpec.builder(Spring) .toBreakpoint(10f) .jumpTo(20f, spring = otherSpring) .continueWithConstantValue() @@ -123,7 +123,7 @@ @Test fun directionalSpec_mappingBuilder_defaultsToNoGuarantee() { val result = - DirectionalMotionSpec.builder(spring) + DirectionalMotionSpec.builder(Spring) .toBreakpoint(10f) .jumpTo(20f) .continueWithConstantValue() @@ -136,7 +136,7 @@ fun directionalSpec_mappingBuilder_canSetGuarantee() { val guarantee = Guarantee.InputDelta(10f) val result = - DirectionalMotionSpec.builder(spring) + DirectionalMotionSpec.builder(Spring) .toBreakpoint(10f) .jumpTo(20f, guarantee = guarantee) .continueWithConstantValue() @@ -148,7 +148,7 @@ @Test fun directionalSpec_mappingBuilder_jumpTo_setsAbsoluteValue() { val result = - DirectionalMotionSpec.builder(spring, Mapping.Fixed(99f)) + DirectionalMotionSpec.builder(Spring, Mapping.Fixed(99f)) .toBreakpoint(10f) .jumpTo(20f) .continueWithConstantValue() @@ -161,7 +161,7 @@ @Test fun directionalSpec_mappingBuilder_jumpBy_setsRelativeValue() { val result = - DirectionalMotionSpec.builder(spring, Mapping.Linear(factor = 0.5f)) + DirectionalMotionSpec.builder(Spring, Mapping.Linear(factor = 0.5f)) .toBreakpoint(10f) .jumpBy(30f) .continueWithConstantValue() @@ -174,7 +174,7 @@ @Test fun directionalSpec_mappingBuilder_continueWithConstantValue_usesSourceValue() { val result = - DirectionalMotionSpec.builder(spring, Mapping.Linear(factor = 0.5f)) + DirectionalMotionSpec.builder(Spring, Mapping.Linear(factor = 0.5f)) .toBreakpoint(5f) .jumpBy(0f) .continueWithConstantValue() @@ -186,7 +186,7 @@ @Test fun directionalSpec_mappingBuilder_continueWithFractionalInput_matchesLinearMapping() { val result = - DirectionalMotionSpec.builder(spring) + DirectionalMotionSpec.builder(Spring) .toBreakpoint(5f) .jumpTo(1f) .continueWithFractionalInput(fraction = .1f) @@ -201,7 +201,7 @@ @Test fun directionalSpec_mappingBuilder_reverse_continueWithFractionalInput_matchesLinearMapping() { val result = - DirectionalMotionSpec.reverseBuilder(spring) + DirectionalMotionSpec.reverseBuilder(Spring) .toBreakpoint(15f) .jumpTo(2f) .continueWithFractionalInput(fraction = .1f) @@ -216,7 +216,7 @@ @Test fun directionalSpec_mappingBuilder_continueWithTargetValue_matchesLinearMapping() { val result = - DirectionalMotionSpec.builder(spring) + DirectionalMotionSpec.builder(Spring) .toBreakpoint(5f) .jumpTo(1f) .continueWithTargetValue(target = 20f) @@ -232,7 +232,7 @@ @Test fun directionalSpec_mappingBuilder_reverse_continueWithTargetValue_matchesLinearMapping() { val result = - DirectionalMotionSpec.reverseBuilder(spring) + DirectionalMotionSpec.reverseBuilder(Spring) .toBreakpoint(30f) .jumpTo(20f) .continueWithTargetValue(target = 1f) @@ -246,9 +246,8 @@ } companion object { - val spring = SpringParameters(stiffness = 100f, dampingRatio = 1f) - val b1 = BreakpointKey("One") - val b2 = BreakpointKey("Two") - val b3 = BreakpointKey("Three") + val Spring = SpringParameters(stiffness = 100f, dampingRatio = 1f) + val B1 = BreakpointKey("One") + val B2 = BreakpointKey("Two") } }
diff --git a/mechanics/tests/src/com/android/mechanics/spec/MotionSpecTest.kt b/mechanics/tests/src/com/android/mechanics/spec/MotionSpecTest.kt index e14d007..3254695 100644 --- a/mechanics/tests/src/com/android/mechanics/spec/MotionSpecTest.kt +++ b/mechanics/tests/src/com/android/mechanics/spec/MotionSpecTest.kt
@@ -18,10 +18,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.mechanics.spring.SpringParameters +import com.android.mechanics.testing.BreakpointSubject.Companion.assertThat import com.google.common.truth.Truth.assertThat -import kotlin.math.nextDown -import kotlin.math.nextUp -import kotlin.test.assertFailsWith import org.junit.Test import org.junit.runner.RunWith @@ -29,100 +27,225 @@ class MotionSpecTest { @Test - fun directionalMotionSpec_noBreakpoints_throws() { - assertFailsWith<IllegalArgumentException> { - DirectionalMotionSpec(emptyList(), emptyList()) - } + fun containsSegment_unknownSegment_returnsFalse() { + val underTest = MotionSpec.builder(Spring).complete() + assertThat(underTest.containsSegment(SegmentKey(B1, B2, InputDirection.Max))).isFalse() } @Test - fun directionalMotionSpec_wrongSentinelBreakpoints_throws() { - val breakpoint1 = Breakpoint(b1, position = 10f, spring, Guarantee.None) - val breakpoint2 = Breakpoint(b2, position = 20f, spring, Guarantee.None) - - assertFailsWith<IllegalArgumentException> { - DirectionalMotionSpec(listOf(breakpoint1, breakpoint2), listOf(Mapping.Identity)) - } - } - - @Test - fun directionalMotionSpec_tooFewMappings_throws() { - assertFailsWith<IllegalArgumentException> { - DirectionalMotionSpec(listOf(Breakpoint.minLimit, Breakpoint.maxLimit), emptyList()) - } - } - - @Test - fun directionalMotionSpec_tooManyMappings_throws() { - assertFailsWith<IllegalArgumentException> { - DirectionalMotionSpec( - listOf(Breakpoint.minLimit, Breakpoint.maxLimit), - listOf(Mapping.One, Mapping.Two), - ) - } - } - - @Test - fun directionalMotionSpec_breakpointsOutOfOrder_throws() { - val breakpoint1 = Breakpoint(b1, position = 10f, spring, Guarantee.None) - val breakpoint2 = Breakpoint(b2, position = 20f, spring, Guarantee.None) - assertFailsWith<IllegalArgumentException> { - DirectionalMotionSpec( - listOf(Breakpoint.minLimit, breakpoint2, breakpoint1, Breakpoint.maxLimit), - listOf(Mapping.Zero, Mapping.One, Mapping.Two), - ) - } - } - - @Test - fun directionalMotionSpec_findBreakpointIndex_returnsMinForEmptySpec() { - val underTest = DirectionalMotionSpec.builder(spring).complete() - - assertThat(underTest.findBreakpointIndex(0f)).isEqualTo(0) - assertThat(underTest.findBreakpointIndex(Float.MAX_VALUE)).isEqualTo(0) - assertThat(underTest.findBreakpointIndex(-Float.MAX_VALUE)).isEqualTo(0) - } - - @Test - fun directionalMotionSpec_findBreakpointIndex_throwsForNonFiniteInput() { - val underTest = DirectionalMotionSpec.builder(spring).complete() - - assertFailsWith<IllegalArgumentException> { underTest.findBreakpointIndex(Float.NaN) } - assertFailsWith<IllegalArgumentException> { - underTest.findBreakpointIndex(Float.NEGATIVE_INFINITY) - } - assertFailsWith<IllegalArgumentException> { - underTest.findBreakpointIndex(Float.POSITIVE_INFINITY) - } - } - - @Test - fun directionalMotionSpec_findBreakpointIndex_atBreakpoint_returnsIndex() { + fun containsSegment_symmetricSpec_knownSegment_returnsTrue() { val underTest = - DirectionalMotionSpec.builder(spring).toBreakpoint(10f).completeWith(Mapping.Identity) + MotionSpec.builder(Spring) + .toBreakpoint(10f, key = B1) + .continueWith(Mapping.One) + .toBreakpoint(20f, key = B2) + .completeWith(Mapping.Identity) - assertThat(underTest.findBreakpointIndex(10f)).isEqualTo(1) + assertThat(underTest.containsSegment(SegmentKey(B1, B2, InputDirection.Max))).isTrue() + assertThat(underTest.containsSegment(SegmentKey(B1, B2, InputDirection.Min))).isTrue() } @Test - fun directionalMotionSpec_findBreakpointIndex_afterBreakpoint_returnsPreviousIndex() { - val underTest = - DirectionalMotionSpec.builder(spring).toBreakpoint(10f).completeWith(Mapping.Identity) + fun containsSegment_asymmetricSpec_knownMaxDirectionSegment_trueOnlyInMaxDirection() { + val forward = + DirectionalMotionSpec.builder(Spring) + .toBreakpoint(10f, key = B1) + .continueWith(Mapping.One) + .toBreakpoint(20f, key = B2) + .completeWith(Mapping.Identity) + val reverse = DirectionalMotionSpec.builder(Spring).complete() - assertThat(underTest.findBreakpointIndex(10f.nextUp())).isEqualTo(1) + val underTest = MotionSpec(forward, reverse) + + assertThat(underTest.containsSegment(SegmentKey(B1, B2, InputDirection.Max))).isTrue() + assertThat(underTest.containsSegment(SegmentKey(B1, B2, InputDirection.Min))).isFalse() } @Test - fun directionalMotionSpec_findBreakpointIndex_beforeBreakpoint_returnsIndex() { - val underTest = - DirectionalMotionSpec.builder(spring).toBreakpoint(10f).completeWith(Mapping.Identity) + fun containsSegment_asymmetricSpec_knownMinDirectionSegment_trueOnlyInMinDirection() { + val forward = DirectionalMotionSpec.builder(Spring).complete() + val reverse = + DirectionalMotionSpec.builder(Spring) + .toBreakpoint(10f, key = B1) + .continueWith(Mapping.One) + .toBreakpoint(20f, key = B2) + .completeWith(Mapping.Identity) - assertThat(underTest.findBreakpointIndex(10f.nextDown())).isEqualTo(0) + val underTest = MotionSpec(forward, reverse) + + assertThat(underTest.containsSegment(SegmentKey(B1, B2, InputDirection.Max))).isFalse() + assertThat(underTest.containsSegment(SegmentKey(B1, B2, InputDirection.Min))).isTrue() + } + + @Test + fun segmentAtInput_emptySpec_maxDirection_segmentDataIsCorrect() { + val underTest = MotionSpec.builder(Spring).complete() + + val segmentAtInput = underTest.segmentAtInput(0f, InputDirection.Max) + + assertThat(segmentAtInput.spec).isSameInstanceAs(underTest) + assertThat(segmentAtInput.minBreakpoint).isSameInstanceAs(Breakpoint.minLimit) + assertThat(segmentAtInput.maxBreakpoint).isSameInstanceAs(Breakpoint.maxLimit) + assertThat(segmentAtInput.direction).isEqualTo(InputDirection.Max) + assertThat(segmentAtInput.mapping).isEqualTo(Mapping.Identity) + } + + @Test + fun segmentAtInput_emptySpec_minDirection_segmentDataIsCorrect() { + val underTest = MotionSpec.builder(Spring).complete() + + val segmentAtInput = underTest.segmentAtInput(0f, InputDirection.Min) + + assertThat(segmentAtInput.spec).isSameInstanceAs(underTest) + assertThat(segmentAtInput.minBreakpoint).isSameInstanceAs(Breakpoint.minLimit) + assertThat(segmentAtInput.maxBreakpoint).isSameInstanceAs(Breakpoint.maxLimit) + assertThat(segmentAtInput.direction).isEqualTo(InputDirection.Min) + assertThat(segmentAtInput.mapping).isEqualTo(Mapping.Identity) + } + + @Test + fun segmentAtInput_atBreakpointPosition() { + val underTest = + MotionSpec.builder(Spring) + .toBreakpoint(10f, key = B1) + .continueWith(Mapping.One) + .toBreakpoint(20f, key = B2) + .completeWith(Mapping.Identity) + + val segmentAtInput = underTest.segmentAtInput(10f, InputDirection.Max) + + assertThat(segmentAtInput.key).isEqualTo(SegmentKey(B1, B2, InputDirection.Max)) + assertThat(segmentAtInput.minBreakpoint).isAt(10f) + assertThat(segmentAtInput.maxBreakpoint).isAt(20f) + assertThat(segmentAtInput.mapping).isEqualTo(Mapping.One) + } + + @Test + fun segmentAtInput_reverse_atBreakpointPosition() { + val underTest = + MotionSpec.builder(Spring) + .toBreakpoint(10f, key = B1) + .continueWith(Mapping.One) + .toBreakpoint(20f, key = B2) + .completeWith(Mapping.Identity) + + val segmentAtInput = underTest.segmentAtInput(20f, InputDirection.Min) + + assertThat(segmentAtInput.key).isEqualTo(SegmentKey(B1, B2, InputDirection.Min)) + assertThat(segmentAtInput.minBreakpoint).isAt(10f) + assertThat(segmentAtInput.maxBreakpoint).isAt(20f) + assertThat(segmentAtInput.mapping).isEqualTo(Mapping.One) + } + + @Test + fun containsSegment_asymmetricSpec_readsFromIndicatedDirection() { + val forward = + DirectionalMotionSpec.builder(Spring) + .toBreakpoint(10f, key = B1) + .continueWith(Mapping.One) + .toBreakpoint(20f, key = B2) + .completeWith(Mapping.Identity) + val reverse = + DirectionalMotionSpec.builder(Spring) + .toBreakpoint(5f, key = B1) + .continueWith(Mapping.Two) + .toBreakpoint(25f, key = B2) + .completeWith(Mapping.Identity) + + val underTest = MotionSpec(forward, reverse) + + val segmentAtInputMax = underTest.segmentAtInput(15f, InputDirection.Max) + assertThat(segmentAtInputMax.key).isEqualTo(SegmentKey(B1, B2, InputDirection.Max)) + assertThat(segmentAtInputMax.minBreakpoint).isAt(10f) + assertThat(segmentAtInputMax.maxBreakpoint).isAt(20f) + assertThat(segmentAtInputMax.mapping).isEqualTo(Mapping.One) + + val segmentAtInputMin = underTest.segmentAtInput(15f, InputDirection.Min) + assertThat(segmentAtInputMin.key).isEqualTo(SegmentKey(B1, B2, InputDirection.Min)) + assertThat(segmentAtInputMin.minBreakpoint).isAt(5f) + assertThat(segmentAtInputMin.maxBreakpoint).isAt(25f) + assertThat(segmentAtInputMin.mapping).isEqualTo(Mapping.Two) + } + + @Test + fun onSegmentChanged_noHandler_returnsEqualSegmentForSameInput() { + val underTest = + MotionSpec.builder(Spring) + .toBreakpoint(10f, key = B1) + .continueWith(Mapping.One) + .toBreakpoint(20f, key = B2) + .completeWith(Mapping.Identity) + + val segmentAtInput = underTest.segmentAtInput(15f, InputDirection.Max) + val onChangedResult = underTest.onChangeSegment(segmentAtInput, 15f, InputDirection.Max) + assertThat(segmentAtInput).isEqualTo(onChangedResult) + } + + @Test + fun onSegmentChanged_noHandler_returnsNewSegmentForNewInput() { + val underTest = + MotionSpec.builder(Spring) + .toBreakpoint(10f, key = B1) + .continueWith(Mapping.One) + .toBreakpoint(20f, key = B2) + .completeWith(Mapping.Identity) + + val segmentAtInput = underTest.segmentAtInput(15f, InputDirection.Max) + val onChangedResult = underTest.onChangeSegment(segmentAtInput, 15f, InputDirection.Min) + assertThat(segmentAtInput).isNotEqualTo(onChangedResult) + + assertThat(onChangedResult.key).isEqualTo(SegmentKey(B1, B2, InputDirection.Min)) + } + + @Test + fun onSegmentChanged_withHandlerReturningNull_returnsSegmentAtInput() { + val underTest = + MotionSpec.builder(Spring) + .toBreakpoint(10f, key = B1) + .continueWith(Mapping.One) + .toBreakpoint(20f, key = B2) + .completeWith(Mapping.Identity) + .copy( + segmentHandlers = + mapOf(SegmentKey(B1, B2, InputDirection.Max) to { _, _, _ -> null }) + ) + + val segmentAtInput = underTest.segmentAtInput(15f, InputDirection.Max) + val onChangedResult = underTest.onChangeSegment(segmentAtInput, 15f, InputDirection.Min) + + assertThat(segmentAtInput).isNotEqualTo(onChangedResult) + assertThat(onChangedResult.key).isEqualTo(SegmentKey(B1, B2, InputDirection.Min)) + } + + @Test + fun onSegmentChanged_withHandlerReturningSegment_returnsHandlerResult() { + val underTest = + MotionSpec.builder(Spring) + .toBreakpoint(10f, key = B1) + .continueWith(Mapping.One) + .toBreakpoint(20f, key = B2) + .completeWith(Mapping.Identity) + .copy( + segmentHandlers = + mapOf( + SegmentKey(B1, B2, InputDirection.Max) to + { _, _, _ -> + segmentAtInput(0f, InputDirection.Min) + } + ) + ) + + val segmentAtInput = underTest.segmentAtInput(15f, InputDirection.Max) + val onChangedResult = underTest.onChangeSegment(segmentAtInput, 15f, InputDirection.Min) + + assertThat(segmentAtInput).isNotEqualTo(onChangedResult) + assertThat(onChangedResult.key) + .isEqualTo(SegmentKey(Breakpoint.minLimit.key, B1, InputDirection.Min)) } companion object { - val b1 = BreakpointKey("one") - val b2 = BreakpointKey("two") - val spring = SpringParameters(stiffness = 100f, dampingRatio = 1f) + val B1 = BreakpointKey("one") + val B2 = BreakpointKey("two") + val Spring = SpringParameters(stiffness = 100f, dampingRatio = 1f) } }
diff --git a/mechanics/tests/src/com/android/mechanics/spec/SegmentTest.kt b/mechanics/tests/src/com/android/mechanics/spec/SegmentTest.kt index 748ac1d..f66991c 100644 --- a/mechanics/tests/src/com/android/mechanics/spec/SegmentTest.kt +++ b/mechanics/tests/src/com/android/mechanics/spec/SegmentTest.kt
@@ -26,29 +26,34 @@ @RunWith(AndroidJUnit4::class) class SegmentTest { + private val fakeSpec = MotionSpec.Empty + @Test fun segmentData_isValidForInput_betweenBreakpointsSameDirection_isTrue() { - val breakpoint1 = Breakpoint(b1, position = 10f, spring, Guarantee.None) - val breakpoint2 = Breakpoint(b2, position = 20f, spring, Guarantee.None) - val underTest = SegmentData(breakpoint1, breakpoint2, InputDirection.Max, Mapping.Identity) + val breakpoint1 = Breakpoint(B1, position = 10f, Spring, Guarantee.None) + val breakpoint2 = Breakpoint(B2, position = 20f, Spring, Guarantee.None) + val underTest = + SegmentData(fakeSpec, breakpoint1, breakpoint2, InputDirection.Max, Mapping.Identity) assertThat(underTest.isValidForInput(15f, InputDirection.Max)).isTrue() } @Test fun segmentData_isValidForInput_betweenBreakpointsOppositeDirection_isFalse() { - val breakpoint1 = Breakpoint(b1, position = 10f, spring, Guarantee.None) - val breakpoint2 = Breakpoint(b2, position = 20f, spring, Guarantee.None) - val underTest = SegmentData(breakpoint1, breakpoint2, InputDirection.Max, Mapping.Identity) + val breakpoint1 = Breakpoint(B1, position = 10f, Spring, Guarantee.None) + val breakpoint2 = Breakpoint(B2, position = 20f, Spring, Guarantee.None) + val underTest = + SegmentData(fakeSpec, breakpoint1, breakpoint2, InputDirection.Max, Mapping.Identity) assertThat(underTest.isValidForInput(15f, InputDirection.Min)).isFalse() } @Test fun segmentData_isValidForInput_inMaxDirection_sampledAtVariousPositions_matchesExpectation() { - val breakpoint1 = Breakpoint(b1, position = 10f, spring, Guarantee.None) - val breakpoint2 = Breakpoint(b2, position = 20f, spring, Guarantee.None) - val underTest = SegmentData(breakpoint1, breakpoint2, InputDirection.Max, Mapping.Identity) + val breakpoint1 = Breakpoint(B1, position = 10f, Spring, Guarantee.None) + val breakpoint2 = Breakpoint(B2, position = 20f, Spring, Guarantee.None) + val underTest = + SegmentData(fakeSpec, breakpoint1, breakpoint2, InputDirection.Max, Mapping.Identity) for ((samplePosition, expectedResult) in listOf(5f to true, 10f to true, 15f to true, 20f to false, 25f to false)) { @@ -60,9 +65,10 @@ @Test fun segmentData_isValidForInput_inMinDirection_sampledAtVariousPositions_matchesExpectation() { - val breakpoint1 = Breakpoint(b1, position = 10f, spring, Guarantee.None) - val breakpoint2 = Breakpoint(b2, position = 20f, spring, Guarantee.None) - val underTest = SegmentData(breakpoint1, breakpoint2, InputDirection.Min, Mapping.Identity) + val breakpoint1 = Breakpoint(B1, position = 10f, Spring, Guarantee.None) + val breakpoint2 = Breakpoint(B2, position = 20f, Spring, Guarantee.None) + val underTest = + SegmentData(fakeSpec, breakpoint1, breakpoint2, InputDirection.Min, Mapping.Identity) for ((samplePosition, expectedResult) in listOf(5f to false, 10f to false, 15f to true, 20f to true, 25f to true)) { @@ -72,9 +78,29 @@ } } + @Test + fun segmentData_entryBreakpoint_maxDirection_returnsMinBreakpoint() { + val breakpoint1 = Breakpoint(B1, position = 10f, Spring, Guarantee.None) + val breakpoint2 = Breakpoint(B2, position = 20f, Spring, Guarantee.None) + val underTest = + SegmentData(fakeSpec, breakpoint1, breakpoint2, InputDirection.Max, Mapping.Identity) + + assertThat(underTest.entryBreakpoint).isSameInstanceAs(breakpoint1) + } + + @Test + fun segmentData_entryBreakpoint_minDirection_returnsMaxBreakpoint() { + val breakpoint1 = Breakpoint(B1, position = 10f, Spring, Guarantee.None) + val breakpoint2 = Breakpoint(B2, position = 20f, Spring, Guarantee.None) + val underTest = + SegmentData(fakeSpec, breakpoint1, breakpoint2, InputDirection.Min, Mapping.Identity) + + assertThat(underTest.entryBreakpoint).isSameInstanceAs(breakpoint2) + } + companion object { - val b1 = BreakpointKey("one") - val b2 = BreakpointKey("two") - val spring = SpringParameters(stiffness = 100f, dampingRatio = 1f) + val B1 = BreakpointKey("one") + val B2 = BreakpointKey("two") + val Spring = SpringParameters(stiffness = 100f, dampingRatio = 1f) } }
diff --git a/mechanics/tests/src/com/android/mechanics/spring/ComposeAndMechanicsSpringCompatibilityTest.kt b/mechanics/tests/src/com/android/mechanics/spring/ComposeAndMechanicsSpringCompatibilityTest.kt index b0d0833..d06012d 100644 --- a/mechanics/tests/src/com/android/mechanics/spring/ComposeAndMechanicsSpringCompatibilityTest.kt +++ b/mechanics/tests/src/com/android/mechanics/spring/ComposeAndMechanicsSpringCompatibilityTest.kt
@@ -84,7 +84,7 @@ initialVelocity: Float, parameters: SpringParameters, ) = buildList { - Animatable(displacement, displacementThreshold).animateTo( + Animatable(displacement, DisplacementThreshold).animateTo( 0f, parameters.asSpringSpec(), initialVelocity, @@ -99,9 +99,9 @@ parameters: SpringParameters, ) = buildList { var state = SpringState(displacement, initialVelocity) - while (!state.isStable(parameters, displacementThreshold)) { + while (!state.isStable(parameters, DisplacementThreshold)) { add(state) - state = state.calculateUpdatedState(frameDelayNanos, parameters) + state = state.calculateUpdatedState(FrameDelayNanos, parameters) } } @@ -131,7 +131,7 @@ val mechanics = byMechanics.elementAtOrNull(i) ?: SpringState.AtRest val compose = byCompose.elementAtOrNull(i) ?: SpringState.AtRest assertThat(mechanics.displacement) - .isWithin(displacementThreshold) + .isWithin(DisplacementThreshold) .of(compose.displacement) } } @@ -142,11 +142,11 @@ private fun runTestWithFrameClock(testBody: suspend () -> Unit) = runTest { val testScope: TestScope = this - withContext(TestMonotonicFrameClock(testScope, frameDelayNanos)) { testBody() } + withContext(TestMonotonicFrameClock(testScope, FrameDelayNanos)) { testBody() } } companion object { - private val frameDelayNanos: Long = 16_000_000L - private val displacementThreshold: Float = 0.01f + private val FrameDelayNanos: Long = 16_000_000L + private val DisplacementThreshold: Float = 0.01f } }
diff --git a/mechanics/tests/src/com/android/mechanics/testing/MotionSpecSubject.kt b/mechanics/tests/src/com/android/mechanics/testing/MotionSpecSubject.kt index a3f7a6b..1a83e06 100644 --- a/mechanics/tests/src/com/android/mechanics/testing/MotionSpecSubject.kt +++ b/mechanics/tests/src/com/android/mechanics/testing/MotionSpecSubject.kt
@@ -39,14 +39,14 @@ fun breakpoints(): BreakpointsSubject { isNotNull() - return check("breakpoints").about(BreakpointsSubject.subjectFactory).that(actual) + return check("breakpoints").about(BreakpointsSubject.SubjectFactory).that(actual) } /** Assert on the mappings. */ fun mappings(): MappingsSubject { isNotNull() - return check("mappings").about(MappingsSubject.subjectFactory).that(actual) + return check("mappings").about(MappingsSubject.SubjectFactory).that(actual) } companion object { @@ -76,20 +76,20 @@ fun atPosition(position: Float): BreakpointSubject { return check("breakpoint @ $position") - .about(BreakpointSubject.subjectFactory) + .about(BreakpointSubject.SubjectFactory) .that(actual?.breakpoints?.find { it.position == position }) } fun withKey(key: BreakpointKey): BreakpointSubject { return check("breakpoint with $key]") - .about(BreakpointSubject.subjectFactory) + .about(BreakpointSubject.SubjectFactory) .that(actual?.breakpoints?.find { it.key == key }) } companion object { /** Returns a factory to be used with [Truth.assertAbout]. */ - val subjectFactory = + val SubjectFactory = Factory<BreakpointsSubject, DirectionalMotionSpec> { failureMetadata, subject -> BreakpointsSubject(failureMetadata, subject) } @@ -132,10 +132,14 @@ Correspondence.transforming<Breakpoint, Float>({ it?.position }, "position") /** Returns a factory to be used with [Truth.assertAbout]. */ - val subjectFactory = + val SubjectFactory = Factory<BreakpointSubject, Breakpoint> { failureMetadata, subject -> BreakpointSubject(failureMetadata, subject) } + + /** Shortcut for `Truth.assertAbout(subjectFactory).that(breakpoint)`. */ + fun assertThat(breakpoint: Breakpoint): BreakpointSubject = + Truth.assertAbout(SubjectFactory).that(breakpoint) } } @@ -148,13 +152,13 @@ /** Assert on the mapping at or after the specified position. */ fun atOrAfter(position: Float): MappingSubject { return check("mapping @ $position") - .about(MappingSubject.subjectFactory) + .about(MappingSubject.SubjectFactory) .that(actual?.run { mappings[findBreakpointIndex(position)] }) } companion object { /** Returns a factory to be used with [Truth.assertAbout]. */ - val subjectFactory = + val SubjectFactory = Factory<MappingsSubject, DirectionalMotionSpec> { failureMetadata, subject -> MappingsSubject(failureMetadata, subject) } @@ -187,9 +191,13 @@ companion object { /** Returns a factory to be used with [Truth.assertAbout]. */ - val subjectFactory = + val SubjectFactory = Factory<MappingSubject, Mapping> { failureMetadata, subject -> MappingSubject(failureMetadata, subject) } + + /** Shortcut for `Truth.assertAbout(subjectFactory).that(mapping)`. */ + fun assertThat(mapping: Mapping): MappingSubject = + Truth.assertAbout(SubjectFactory).that(mapping) } }