• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.mechanics.spec
18 
19 import com.android.mechanics.spring.SpringParameters
20 
21 /**
22  * Handler to allow for custom segment-change logic.
23  *
24  * This handler is called whenever the new input (position or direction) does not match
25  * [currentSegment] anymore (see [SegmentData.isValidForInput]).
26  *
27  * This is intended to implement custom effects on direction-change.
28  *
29  * Implementations can return:
30  * 1. [currentSegment] to delay/suppress segment change.
31  * 2. `null` to use the default segment lookup based on [newPosition] and [newDirection]
32  * 3. manually looking up segments on this [MotionSpec]
33  * 4. create a [SegmentData] that is not in the spec.
34  */
35 typealias OnChangeSegmentHandler =
36     MotionSpec.(
37         currentSegment: SegmentData, newPosition: Float, newDirection: InputDirection,
38     ) -> SegmentData?
39 
40 /**
41  * Specification for the mapping of input values to output values.
42  *
43  * The spec consists of two independent directional spec's, while only one the one matching
44  * `MotionInput`'s `direction` is used at any given time.
45  *
46  * @param maxDirection spec used when the MotionInput's direction is [InputDirection.Max]
47  * @param minDirection spec used when the MotionInput's direction is [InputDirection.Min]
48  * @param resetSpring spring parameters to animate a difference in output, if the difference is
49  *   caused by setting this new spec.
50  * @param segmentHandlers allow for custom segment-change logic, when the `MotionValue` runtime
51  *   would leave the [SegmentKey].
52  */
53 data class MotionSpec(
54     val maxDirection: DirectionalMotionSpec,
55     val minDirection: DirectionalMotionSpec = maxDirection,
56     val resetSpring: SpringParameters = DefaultResetSpring,
57     val segmentHandlers: Map<SegmentKey, OnChangeSegmentHandler> = emptyMap(),
58 ) {
59 
60     /** The [DirectionalMotionSpec] for the specified [direction]. */
61     operator fun get(direction: InputDirection): DirectionalMotionSpec {
62         return when (direction) {
63             InputDirection.Min -> minDirection
64             InputDirection.Max -> maxDirection
65         }
66     }
67 
68     /** Whether this spec contains a segment with the specified [segmentKey]. */
69     fun containsSegment(segmentKey: SegmentKey): Boolean {
70         return get(segmentKey.direction).findSegmentIndex(segmentKey) != -1
71     }
72 
73     /**
74      * The [SegmentData] for an input with the specified [position] and [direction].
75      *
76      * The returned [SegmentData] will be cached while [SegmentData.isValidForInput] returns `true`.
77      */
78     fun segmentAtInput(position: Float, direction: InputDirection): SegmentData {
79         require(position.isFinite())
80 
81         return with(get(direction)) {
82             var idx = findBreakpointIndex(position)
83             if (direction == InputDirection.Min && breakpoints[idx].position == position) {
84                 // The segment starts at `position`. Since the breakpoints are sorted ascending, no
85                 // matter the spec's direction, need to return the previous segment in the min
86                 // direction.
87                 idx--
88             }
89 
90             SegmentData(
91                 this@MotionSpec,
92                 breakpoints[idx],
93                 breakpoints[idx + 1],
94                 direction,
95                 mappings[idx],
96             )
97         }
98     }
99 
100     /**
101      * Looks up the new [SegmentData] once the [currentSegment] is not valid for an input with
102      * [newPosition] and [newDirection].
103      *
104      * This will delegate to the [segmentHandlers], if registered for the [currentSegment]'s key.
105      */
106     internal fun onChangeSegment(
107         currentSegment: SegmentData,
108         newPosition: Float,
109         newDirection: InputDirection,
110     ): SegmentData {
111         val segmentChangeHandler = segmentHandlers[currentSegment.key]
112         return segmentChangeHandler?.invoke(this, currentSegment, newPosition, newDirection)
113             ?: segmentAtInput(newPosition, newDirection)
114     }
115 
116     companion object {
117         /**
118          * Default spring parameters for the reset spring. Matches the Fast Spatial spring of the
119          * standard motion scheme.
120          */
121         private val DefaultResetSpring = SpringParameters(stiffness = 1400f, dampingRatio = 1f)
122 
123         /* Empty motion spec, the output is the same as the input. */
124         val Empty = MotionSpec(DirectionalMotionSpec.Empty)
125     }
126 }
127 
128 /**
129  * Defines the [breakpoints], as well as the [mappings] in-between adjacent [Breakpoint] pairs.
130  *
131  * This [DirectionalMotionSpec] is applied in the direction defined by the containing [MotionSpec]:
132  * especially the direction in which the `breakpoint` [Guarantee] are applied depend on how this is
133  * used; this type does not have an inherit direction.
134  *
135  * All [breakpoints] are sorted in ascending order by their `position`, with the first and last
136  * breakpoints are guaranteed to be sentinel values for negative and positive infinity respectively.
137  *
138  * @param breakpoints All breakpoints in the spec, must contain [Breakpoint.minLimit] as the first
139  *   element, and [Breakpoint.maxLimit] as the last element.
140  * @param mappings All mappings in between the breakpoints, thus must always contain
141  *   `breakpoints.size - 1` elements.
142  */
143 data class DirectionalMotionSpec(val breakpoints: List<Breakpoint>, val mappings: List<Mapping>) {
144     init {
145         require(breakpoints.size >= 2)
146         require(breakpoints.first() == Breakpoint.minLimit)
147         require(breakpoints.last() == Breakpoint.maxLimit)
<lambda>null148         require(breakpoints.zipWithNext { a, b -> a <= b }.all { it }) {
<lambda>null149             "Breakpoints are not sorted ascending ${breakpoints.map { "${it.key}@${it.position}" }}"
150         }
151         require(mappings.size == breakpoints.size - 1)
152     }
153 
154     /**
155      * Returns the index of the closest breakpoint where `Breakpoint.position <= position`.
156      *
157      * Guaranteed to be a valid index into [breakpoints], and guaranteed to be neither the first nor
158      * the last element.
159      *
160      * @param position the position in the input domain.
161      * @return Index into [breakpoints], guaranteed to be in range `1..breakpoints.size - 2`
162      */
findBreakpointIndexnull163     fun findBreakpointIndex(position: Float): Int {
164         require(position.isFinite())
165         val breakpointPosition = breakpoints.binarySearchBy(position) { it.position }
166 
167         val result =
168             when {
169                 // position is between two anchors, return the min one.
170                 breakpointPosition < 0 -> -breakpointPosition - 2
171                 else -> breakpointPosition
172             }
173 
174         check(result >= 0)
175         check(result < breakpoints.size - 1)
176 
177         return result
178     }
179 
180     /**
181      * The index of the breakpoint with the specified [breakpointKey], or `-1` if no such breakpoint
182      * exists.
183      */
findBreakpointIndexnull184     fun findBreakpointIndex(breakpointKey: BreakpointKey): Int {
185         return breakpoints.indexOfFirst { it.key == breakpointKey }
186     }
187 
188     /** Index into [mappings] for the specified [segmentKey], or `-1` if no such segment exists. */
findSegmentIndexnull189     fun findSegmentIndex(segmentKey: SegmentKey): Int {
190         val result = breakpoints.indexOfFirst { it.key == segmentKey.minBreakpoint }
191         if (result < 0 || breakpoints[result + 1].key != segmentKey.maxBreakpoint) return -1
192 
193         return result
194     }
195 
196     companion object {
197         /* Empty spec, the full input domain is mapped to output using [Mapping.identity]. */
198         val Empty =
199             DirectionalMotionSpec(
200                 listOf(Breakpoint.minLimit, Breakpoint.maxLimit),
201                 listOf(Mapping.Identity),
202             )
203     }
204 }
205