Loading 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), ) } } mechanics/src/com/android/mechanics/spec/Segment.kt 0 → 100644 +102 −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 /** * Identifies a segment in a [MotionSpec]. * * A segment only exists between two adjacent [Breakpoint]s; it cannot span multiple breakpoints. * The [direction] indicates to the relevant [DirectionalMotionSpec] of the [MotionSpec]. * * The position of the [minBreakpoint] must be less or equal to the position of the [maxBreakpoint]. */ data class SegmentKey( val minBreakpoint: BreakpointKey, val maxBreakpoint: BreakpointKey, val direction: InputDirection, ) /** * Captures denormalized segment data from a [MotionSpec]. * * Instances are created by the [MotionSpec] and used by the [MotionValue] runtime to compute the * output value. By default, the [SegmentData] is cached while [isValidForInput] returns true. * * The [SegmentData] has an intrinsic direction, thus the segment has an entry and exit side, at the * respective breakpoint. */ data class SegmentData( val minBreakpoint: Breakpoint, val maxBreakpoint: Breakpoint, val direction: InputDirection, val mapping: Mapping, ) { val key = SegmentKey(minBreakpoint.key, maxBreakpoint.key, direction) /** * Whether the given [inputPosition] and [inputDirection] should be handled by this segment. * * The input is considered invalid only if the direction changes or the input is *at or outside* * the segment on the exit-side. The input remains intentionally valid outside the segment on * the entry-side, to avoid flip-flopping. */ fun isValidForInput(inputPosition: Float, inputDirection: InputDirection): Boolean { if (inputDirection != direction) return false return when (inputDirection) { InputDirection.Max -> inputPosition < maxBreakpoint.position InputDirection.Min -> inputPosition > minBreakpoint.position } } } /** * Maps the `input` of a [MotionValue] to the desired output value. * * The mapping implementation can be arbitrary, but must not produce discontinuities. */ fun interface Mapping { /** Computes the [MotionValue]'s target output, given the input. */ fun map(input: Float): Float /** `f(x) = x` */ object Identity : Mapping { override fun map(input: Float): Float { return input } } /** `f(x) = value` */ data class Fixed(val value: Float) : Mapping { override fun map(input: Float): Float { return value } } /** `f(x) = factor*x + offset` */ data class Linear(val factor: Float, val offset: Float = 0f) : Mapping { override fun map(input: Float): Float { return input * factor + offset } } companion object { val Zero = Fixed(0f) val One = Fixed(1f) val Two = Fixed(2f) } } Loading
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), ) } }
mechanics/src/com/android/mechanics/spec/Segment.kt 0 → 100644 +102 −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 /** * Identifies a segment in a [MotionSpec]. * * A segment only exists between two adjacent [Breakpoint]s; it cannot span multiple breakpoints. * The [direction] indicates to the relevant [DirectionalMotionSpec] of the [MotionSpec]. * * The position of the [minBreakpoint] must be less or equal to the position of the [maxBreakpoint]. */ data class SegmentKey( val minBreakpoint: BreakpointKey, val maxBreakpoint: BreakpointKey, val direction: InputDirection, ) /** * Captures denormalized segment data from a [MotionSpec]. * * Instances are created by the [MotionSpec] and used by the [MotionValue] runtime to compute the * output value. By default, the [SegmentData] is cached while [isValidForInput] returns true. * * The [SegmentData] has an intrinsic direction, thus the segment has an entry and exit side, at the * respective breakpoint. */ data class SegmentData( val minBreakpoint: Breakpoint, val maxBreakpoint: Breakpoint, val direction: InputDirection, val mapping: Mapping, ) { val key = SegmentKey(minBreakpoint.key, maxBreakpoint.key, direction) /** * Whether the given [inputPosition] and [inputDirection] should be handled by this segment. * * The input is considered invalid only if the direction changes or the input is *at or outside* * the segment on the exit-side. The input remains intentionally valid outside the segment on * the entry-side, to avoid flip-flopping. */ fun isValidForInput(inputPosition: Float, inputDirection: InputDirection): Boolean { if (inputDirection != direction) return false return when (inputDirection) { InputDirection.Max -> inputPosition < maxBreakpoint.position InputDirection.Min -> inputPosition > minBreakpoint.position } } } /** * Maps the `input` of a [MotionValue] to the desired output value. * * The mapping implementation can be arbitrary, but must not produce discontinuities. */ fun interface Mapping { /** Computes the [MotionValue]'s target output, given the input. */ fun map(input: Float): Float /** `f(x) = x` */ object Identity : Mapping { override fun map(input: Float): Float { return input } } /** `f(x) = value` */ data class Fixed(val value: Float) : Mapping { override fun map(input: Float): Float { return value } } /** `f(x) = factor*x + offset` */ data class Linear(val factor: Float, val offset: Float = 0f) : Mapping { override fun map(input: Float): Float { return input * factor + offset } } companion object { val Zero = Fixed(0f) val One = Fixed(1f) val Two = Fixed(2f) } }