Loading mechanics/TEST_MAPPING +10 −2 Original line number Diff line number Diff line { "postsubmit": [ "presubmit": [ { "name": "mechanics_tests" "name": "mechanics_tests", "options": [ { "exclude-annotation": "org.junit.Ignore" }, { "exclude-annotation": "androidx.test.filters.FlakyTest" } ] } ] } mechanics/src/com/android/mechanics/spec/Breakpoint.kt 0 → 100644 +91 −0 Original line number Diff line number Diff line /* * 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 com.android.mechanics.spring.SpringParameters /** * Key to identify a breakpoint in a [DirectionalMotionSpec]. * * @param debugLabel name of the breakpoint, for tooling and debugging. * @param identity is used to check the equality of two key instances. */ class BreakpointKey(val debugLabel: String? = null, val identity: Any = Object()) { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as BreakpointKey return identity == other.identity } override fun hashCode(): Int { return identity.hashCode() } override fun toString(): String { return if (debugLabel != null) "BreakpointKey(label=$debugLabel)" else "BreakpointKey()" } } /** * Specification of a breakpoint, in the context of a [DirectionalMotionSpec]. * * The [spring] and [guarantee] define the physics animation for the discontinuity at this * breakpoint.They are applied in the direction of the containing [DirectionalMotionSpec]. * * This [Breakpoint]'s animation definition is valid while the input is within the next segment. If * the animation is still in progress when the input value reaches the next breakpoint, the * remaining animation will be blended with the animation starting at the next breakpoint. * * @param key Identity of the [Breakpoint], unique within a [DirectionalMotionSpec]. * @param position The position of the [Breakpoint], in the domain of the `MotionValue`'s input. * @param spring Parameters of the spring used to animate the breakpoints discontinuity. * @param guarantee Optional constraints to accelerate the completion of the spring motion, based on * `MotionValue`'s input or other non-time signals. */ data class Breakpoint( val key: BreakpointKey, val position: Float, val spring: SpringParameters, val guarantee: Guarantee, ) : Comparable<Breakpoint> { companion object { /** First breakpoint of each spec. */ val minLimit = Breakpoint( BreakpointKey("built-in::min"), Float.NEGATIVE_INFINITY, SpringParameters.Snap, Guarantee.None, ) /** Last breakpoint of each spec. */ val maxLimit = Breakpoint( BreakpointKey("built-in::max"), Float.POSITIVE_INFINITY, SpringParameters.Snap, Guarantee.None, ) } override fun compareTo(other: Breakpoint): Int { return position.compareTo(other.position) } } mechanics/src/com/android/mechanics/spec/Guarantee.kt 0 → 100644 +45 −0 Original line number Diff line number Diff line /* * 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 /** * Describes the condition by which a discontinuity at a breakpoint must have finished animating. * * With a guarantee in effect, the spring parameters will be continuously adjusted, ensuring the * guarantee's target will be met. */ sealed class Guarantee { /** * No guarantee is provided. * * The spring animation will proceed at its natural pace, regardless of the input or gesture's * progress. */ data object None : Guarantee() /** * Guarantees that the animation will be complete before the input value is [delta] away from * the [Breakpoint] position. */ data class InputDelta(val delta: Float) : Guarantee() /** * Guarantees to complete the animation before the gesture is [distance] away from the gesture * position captured when the breakpoint was crossed. */ data class GestureDistance(val distance: Float) : Guarantee() } mechanics/src/com/android/mechanics/spec/InputDirection.kt 0 → 100644 +31 −0 Original line number Diff line number Diff line /* * 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 /** * The intrinsic direction of the input value. * * It reflects the user's intent, that is its meant to be derived from a gesture. If the input is * driven by an animation, the direction is expected to not change. * * 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, } mechanics/src/com/android/mechanics/spec/MotionSpec.kt 0 → 100644 +105 −0 Original line number Diff line number Diff line /* * 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 com.android.mechanics.spring.SpringParameters /** * Handler to allow for custom segment-change logic. * * This handler is called whenever the new input (position or direction) does not match * [currentSegment] anymore (see [SegmentData.isValidForInput]). * * This is intended to implement custom effects on direction-change. * * Implementations can return: * 1. [currentSegment] to delay/suppress segment change. * 2. `null` to use the default segment lookup based on [newPosition] and [newDirection] * 3. manually looking up segments on this [MotionSpec] * 4. create a [SegmentData] that is not in the spec. */ typealias OnChangeSegmentHandler = MotionSpec.( currentSegment: SegmentData, newPosition: Float, newDirection: InputDirection, ) -> SegmentData? /** * Specification for the mapping of input values to output values. * * The spec consists of two independent directional spec's, while only one the one matching * `MotionInput`'s `direction` is used at any given time. * * @param maxDirection spec used when the MotionInput's direction is [InputDirection.Max] * @param minDirection spec used when the MotionInput's direction is [InputDirection.Min] * @param resetSpring spring parameters to animate a difference in output, if the difference is * caused by setting this new spec. * @param segmentHandlers allow for custom segment-change logic, when the `MotionValue` runtime * would leave the [SegmentKey]. */ data class MotionSpec( val maxDirection: DirectionalMotionSpec, val minDirection: DirectionalMotionSpec = maxDirection, val resetSpring: SpringParameters = DefaultResetSpring, val segmentHandlers: Map<SegmentKey, OnChangeSegmentHandler> = emptyMap(), ) { companion object { /** * Default spring parameters for the reset spring. Matches the Fast Spatial spring of the * standard motion scheme. */ private val DefaultResetSpring = SpringParameters(stiffness = 1400f, dampingRatio = 1f) /* Empty motion spec, the output is the same as the input. */ val Empty = MotionSpec(DirectionalMotionSpec.Empty) } } /** * Defines the [breakpoints], as well as the [mappings] in-between adjacent [Breakpoint] pairs. * * This [DirectionalMotionSpec] is applied in the direction defined by the containing [MotionSpec]: * especially the direction in which the `breakpoint` [Guarantee] are applied depend on how this is * used; this type does not have an inherit direction. * * All [breakpoints] are sorted in ascending order by their `position`, with the first and last * breakpoints are guaranteed to be sentinel values for negative and positive infinity respectively. * * @param breakpoints All breakpoints in the spec, must contain [Breakpoint.minLimit] as the first * element, and [Breakpoint.maxLimit] as the last element. * @param mappings All mappings in between the breakpoints, thus must always contain * `breakpoints.size - 1` elements. */ data class DirectionalMotionSpec(val breakpoints: List<Breakpoint>, val mappings: List<Mapping>) { init { require(breakpoints.size >= 2) require(breakpoints.first() == Breakpoint.minLimit) require(breakpoints.last() == Breakpoint.maxLimit) require(breakpoints.zipWithNext { a, b -> a <= b }.all { it }) { "Breakpoints are not sorted ascending ${breakpoints.map { "${it.key}@${it.position}" }}" } require(mappings.size == breakpoints.size - 1) } companion object { /* Empty spec, the full input domain is mapped to output using [Mapping.identity]. */ val Empty = DirectionalMotionSpec( listOf(Breakpoint.minLimit, Breakpoint.maxLimit), listOf(Mapping.Identity), ) } } Loading
mechanics/TEST_MAPPING +10 −2 Original line number Diff line number Diff line { "postsubmit": [ "presubmit": [ { "name": "mechanics_tests" "name": "mechanics_tests", "options": [ { "exclude-annotation": "org.junit.Ignore" }, { "exclude-annotation": "androidx.test.filters.FlakyTest" } ] } ] }
mechanics/src/com/android/mechanics/spec/Breakpoint.kt 0 → 100644 +91 −0 Original line number Diff line number Diff line /* * 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 com.android.mechanics.spring.SpringParameters /** * Key to identify a breakpoint in a [DirectionalMotionSpec]. * * @param debugLabel name of the breakpoint, for tooling and debugging. * @param identity is used to check the equality of two key instances. */ class BreakpointKey(val debugLabel: String? = null, val identity: Any = Object()) { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as BreakpointKey return identity == other.identity } override fun hashCode(): Int { return identity.hashCode() } override fun toString(): String { return if (debugLabel != null) "BreakpointKey(label=$debugLabel)" else "BreakpointKey()" } } /** * Specification of a breakpoint, in the context of a [DirectionalMotionSpec]. * * The [spring] and [guarantee] define the physics animation for the discontinuity at this * breakpoint.They are applied in the direction of the containing [DirectionalMotionSpec]. * * This [Breakpoint]'s animation definition is valid while the input is within the next segment. If * the animation is still in progress when the input value reaches the next breakpoint, the * remaining animation will be blended with the animation starting at the next breakpoint. * * @param key Identity of the [Breakpoint], unique within a [DirectionalMotionSpec]. * @param position The position of the [Breakpoint], in the domain of the `MotionValue`'s input. * @param spring Parameters of the spring used to animate the breakpoints discontinuity. * @param guarantee Optional constraints to accelerate the completion of the spring motion, based on * `MotionValue`'s input or other non-time signals. */ data class Breakpoint( val key: BreakpointKey, val position: Float, val spring: SpringParameters, val guarantee: Guarantee, ) : Comparable<Breakpoint> { companion object { /** First breakpoint of each spec. */ val minLimit = Breakpoint( BreakpointKey("built-in::min"), Float.NEGATIVE_INFINITY, SpringParameters.Snap, Guarantee.None, ) /** Last breakpoint of each spec. */ val maxLimit = Breakpoint( BreakpointKey("built-in::max"), Float.POSITIVE_INFINITY, SpringParameters.Snap, Guarantee.None, ) } override fun compareTo(other: Breakpoint): Int { return position.compareTo(other.position) } }
mechanics/src/com/android/mechanics/spec/Guarantee.kt 0 → 100644 +45 −0 Original line number Diff line number Diff line /* * 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 /** * Describes the condition by which a discontinuity at a breakpoint must have finished animating. * * With a guarantee in effect, the spring parameters will be continuously adjusted, ensuring the * guarantee's target will be met. */ sealed class Guarantee { /** * No guarantee is provided. * * The spring animation will proceed at its natural pace, regardless of the input or gesture's * progress. */ data object None : Guarantee() /** * Guarantees that the animation will be complete before the input value is [delta] away from * the [Breakpoint] position. */ data class InputDelta(val delta: Float) : Guarantee() /** * Guarantees to complete the animation before the gesture is [distance] away from the gesture * position captured when the breakpoint was crossed. */ data class GestureDistance(val distance: Float) : Guarantee() }
mechanics/src/com/android/mechanics/spec/InputDirection.kt 0 → 100644 +31 −0 Original line number Diff line number Diff line /* * 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 /** * The intrinsic direction of the input value. * * It reflects the user's intent, that is its meant to be derived from a gesture. If the input is * driven by an animation, the direction is expected to not change. * * 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, }
mechanics/src/com/android/mechanics/spec/MotionSpec.kt 0 → 100644 +105 −0 Original line number Diff line number Diff line /* * 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 com.android.mechanics.spring.SpringParameters /** * Handler to allow for custom segment-change logic. * * This handler is called whenever the new input (position or direction) does not match * [currentSegment] anymore (see [SegmentData.isValidForInput]). * * This is intended to implement custom effects on direction-change. * * Implementations can return: * 1. [currentSegment] to delay/suppress segment change. * 2. `null` to use the default segment lookup based on [newPosition] and [newDirection] * 3. manually looking up segments on this [MotionSpec] * 4. create a [SegmentData] that is not in the spec. */ typealias OnChangeSegmentHandler = MotionSpec.( currentSegment: SegmentData, newPosition: Float, newDirection: InputDirection, ) -> SegmentData? /** * Specification for the mapping of input values to output values. * * The spec consists of two independent directional spec's, while only one the one matching * `MotionInput`'s `direction` is used at any given time. * * @param maxDirection spec used when the MotionInput's direction is [InputDirection.Max] * @param minDirection spec used when the MotionInput's direction is [InputDirection.Min] * @param resetSpring spring parameters to animate a difference in output, if the difference is * caused by setting this new spec. * @param segmentHandlers allow for custom segment-change logic, when the `MotionValue` runtime * would leave the [SegmentKey]. */ data class MotionSpec( val maxDirection: DirectionalMotionSpec, val minDirection: DirectionalMotionSpec = maxDirection, val resetSpring: SpringParameters = DefaultResetSpring, val segmentHandlers: Map<SegmentKey, OnChangeSegmentHandler> = emptyMap(), ) { companion object { /** * Default spring parameters for the reset spring. Matches the Fast Spatial spring of the * standard motion scheme. */ private val DefaultResetSpring = SpringParameters(stiffness = 1400f, dampingRatio = 1f) /* Empty motion spec, the output is the same as the input. */ val Empty = MotionSpec(DirectionalMotionSpec.Empty) } } /** * Defines the [breakpoints], as well as the [mappings] in-between adjacent [Breakpoint] pairs. * * This [DirectionalMotionSpec] is applied in the direction defined by the containing [MotionSpec]: * especially the direction in which the `breakpoint` [Guarantee] are applied depend on how this is * used; this type does not have an inherit direction. * * All [breakpoints] are sorted in ascending order by their `position`, with the first and last * breakpoints are guaranteed to be sentinel values for negative and positive infinity respectively. * * @param breakpoints All breakpoints in the spec, must contain [Breakpoint.minLimit] as the first * element, and [Breakpoint.maxLimit] as the last element. * @param mappings All mappings in between the breakpoints, thus must always contain * `breakpoints.size - 1` elements. */ data class DirectionalMotionSpec(val breakpoints: List<Breakpoint>, val mappings: List<Mapping>) { init { require(breakpoints.size >= 2) require(breakpoints.first() == Breakpoint.minLimit) require(breakpoints.last() == Breakpoint.maxLimit) require(breakpoints.zipWithNext { a, b -> a <= b }.all { it }) { "Breakpoints are not sorted ascending ${breakpoints.map { "${it.key}@${it.position}" }}" } require(mappings.size == breakpoints.size - 1) } companion object { /* Empty spec, the full input domain is mapped to output using [Mapping.identity]. */ val Empty = DirectionalMotionSpec( listOf(Breakpoint.minLimit, Breakpoint.maxLimit), listOf(Mapping.Identity), ) } }