• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * 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  * Fluent builder for [DirectionalMotionSpec].
23  *
24  * This builder ensures correctness at compile-time, and simplifies the expression of the
25  * input-to-output mapping.
26  *
27  * The [MotionSpec] is defined by specify interleaved [Mapping]s and [Breakpoint]s. [Breakpoint]s
28  * must be specified in ascending order.
29  *
30  * NOTE: The returned fluent interfaces must only be used for chaining calls to build exactly one
31  * [DirectionalMotionSpec], otherwise resulting behavior is undefined, since the builder is
32  * internally mutated.
33  *
34  * @param defaultSpring spring to use for all breakpoints by default.
35  * @param initialMapping the [Mapping] from [Breakpoint.minLimit] to the next [Breakpoint].
36  * @see reverseBuilder to specify [Breakpoint]s in descending order.
37  */
DirectionalMotionSpecnull38 fun DirectionalMotionSpec.Companion.builder(
39     defaultSpring: SpringParameters,
40     initialMapping: Mapping = Mapping.Identity,
41 ): FluentSpecEndSegmentWithNextBreakpoint<DirectionalMotionSpec> {
42     return FluentSpecBuilder(defaultSpring, InputDirection.Max) { it }
43         .apply { mappings.add(initialMapping) }
44 }
45 
46 /**
47  * Fluent builder for [DirectionalMotionSpec], specifying breakpoints and mappings in reverse order.
48  *
49  * Variant of [DirectionalMotionSpec.Companion.builder], where [Breakpoint]s must be specified in
50  * *descending* order. The resulting [DirectionalMotionSpec] will contain the breakpoints in
51  * ascending order.
52  *
53  * @param defaultSpring spring to use for all breakpoints by default.
54  * @param initialMapping the [Mapping] from [Breakpoint.maxLimit] to the next [Breakpoint].
55  * @see DirectionalMotionSpec.Companion.builder for more documentation.
56  */
DirectionalMotionSpecnull57 fun DirectionalMotionSpec.Companion.reverseBuilder(
58     defaultSpring: SpringParameters,
59     initialMapping: Mapping = Mapping.Identity,
60 ): FluentSpecEndSegmentWithNextBreakpoint<DirectionalMotionSpec> {
61     return FluentSpecBuilder(defaultSpring, InputDirection.Min) { it }
62         .apply { mappings.add(initialMapping) }
63 }
64 
65 /**
66  * Fluent builder for a [MotionSpec], which uses the same spec in both directions.
67  *
68  * @param defaultSpring spring to use for all breakpoints by default.
69  * @param initialMapping [Mapping] for the first segment
70  * @param resetSpring the [MotionSpec.resetSpring].
71  */
MotionSpecnull72 fun MotionSpec.Companion.builder(
73     defaultSpring: SpringParameters,
74     initialMapping: Mapping = Mapping.Identity,
75     resetSpring: SpringParameters = defaultSpring,
76 ): FluentSpecEndSegmentWithNextBreakpoint<MotionSpec> {
77     return FluentSpecBuilder(defaultSpring, InputDirection.Max) {
78             MotionSpec(it, resetSpring = resetSpring)
79         }
80         .apply { mappings.add(initialMapping) }
81 }
82 
83 /** Fluent-interface to end the current segment, by placing the next [Breakpoint]. */
84 interface FluentSpecEndSegmentWithNextBreakpoint<R> {
85     /**
86      * Adds a new [Breakpoint] at the specified position.
87      *
88      * @param atPosition The position of the breakpoint, in the input domain of the [MotionValue].
89      * @param key identifies the breakpoint in the [DirectionalMotionSpec]. Must be specified to
90      *   reference the breakpoint or segment.
91      */
toBreakpointnull92     fun toBreakpoint(
93         atPosition: Float,
94         key: BreakpointKey = BreakpointKey(),
95     ): FluentSpecDefineBreakpointAndStartNextSegment<R>
96 
97     /** Completes the spec by placing the last, implicit [Breakpoint]. */
98     fun complete(): R
99 }
100 
101 /** Fluent-interface to define the [Breakpoint]'s properties and start to start the next segment. */
102 interface FluentSpecDefineBreakpointAndStartNextSegment<R> {
103     /**
104      * Default spring parameters for breakpoint, as specified at creation time of the builder.
105      *
106      * Used as the default `spring` parameters.
107      */
108     val defaultSpring: SpringParameters
109 
110     /**
111      * Starts the next segment, using the specified mapping.
112      *
113      * @param mapping the mapping to use for the next segment.
114      * @param spring the spring to animate this breakpoints discontinuity.
115      * @param guarantee a guarantee by when the animation must be complete
116      */
117     fun continueWith(
118         mapping: Mapping,
119         spring: SpringParameters = defaultSpring,
120         guarantee: Guarantee = Guarantee.None,
121     ): FluentSpecEndSegmentWithNextBreakpoint<R>
122 
123     /**
124      * Starts the next linear-mapped segment, by specifying the output [value] this breakpoint.
125      *
126      * @param value the output value the new mapping will produce at this breakpoints position.
127      * @param spring the spring to animate this breakpoints discontinuity.
128      * @param guarantee a guarantee by when the animation must be complete
129      */
130     fun jumpTo(
131         value: Float,
132         spring: SpringParameters = defaultSpring,
133         guarantee: Guarantee = Guarantee.None,
134     ): FluentSpecDefineLinearSegmentMapping<R>
135 
136     /**
137      * Starts the next linear-mapped segment, by offsetting the output by [delta] from the incoming
138      * mapping.
139      *
140      * @param delta the delta in output from the previous mapping's output.
141      * @param spring the spring to animate this breakpoints discontinuity.
142      * @param guarantee a guarantee by when the animation must be complete
143      */
144     fun jumpBy(
145         delta: Float,
146         spring: SpringParameters = defaultSpring,
147         guarantee: Guarantee = Guarantee.None,
148     ): FluentSpecDefineLinearSegmentMapping<R>
149 
150     /**
151      * Completes the spec by using [mapping] between the this and the implicit sentinel breakpoint
152      * at infinity.
153      *
154      * @param mapping the mapping to use for the final segment.
155      * @param spring the spring to animate this breakpoints discontinuity.
156      * @param guarantee a guarantee by when the animation must be complete
157      */
158     fun completeWith(
159         mapping: Mapping,
160         spring: SpringParameters = defaultSpring,
161         guarantee: Guarantee = Guarantee.None,
162     ): R
163 }
164 
165 /** Fluent-interface to define a linear mapping between two breakpoints. */
166 interface FluentSpecDefineLinearSegmentMapping<R> {
167     /**
168      * The linear-mapping will produce the specified [target] output at the next breakpoint
169      * position.
170      *
171      * @param target the output value the new mapping will produce at the next breakpoint position.
172      */
continueWithTargetValuenull173     fun continueWithTargetValue(target: Float): FluentSpecEndSegmentWithNextBreakpoint<R>
174 
175     /**
176      * Defines the slope for the linear mapping, as a fraction of the input value.
177      *
178      * @param fraction the multiplier applied to the input value..
179      */
180     fun continueWithFractionalInput(fraction: Float): FluentSpecEndSegmentWithNextBreakpoint<R>
181 
182     /**
183      * The linear-mapping will produce a constant value, as defined at the source breakpoint of this
184      * segment.
185      */
186     fun continueWithConstantValue(): FluentSpecEndSegmentWithNextBreakpoint<R>
187 }
188 
189 /** Implements the fluent spec builder logic. */
190 private class FluentSpecBuilder<R>(
191     override val defaultSpring: SpringParameters,
192     buildDirection: InputDirection = InputDirection.Max,
193     private val toResult: (DirectionalMotionSpec) -> R,
194 ) :
195     FluentSpecDefineLinearSegmentMapping<R>,
196     FluentSpecDefineBreakpointAndStartNextSegment<R>,
197     FluentSpecEndSegmentWithNextBreakpoint<R> {
198     private val buildForward = buildDirection == InputDirection.Max
199 
200     val breakpoints = mutableListOf<Breakpoint>()
201     val mappings = mutableListOf<Mapping>()
202 
203     var sourceValue: Float = Float.NaN
204     var targetValue: Float = Float.NaN
205     var fractionalMapping: Float = Float.NaN
206     var breakpointPosition: Float = Float.NaN
207     var breakpointKey: BreakpointKey? = null
208 
209     init {
210         val initialBreakpoint = if (buildForward) Breakpoint.minLimit else Breakpoint.maxLimit
211         breakpoints.add(initialBreakpoint)
212     }
213 
214     //  FluentSpecDefineLinearSegmentMapping
215 
216     override fun continueWithTargetValue(target: Float): FluentSpecEndSegmentWithNextBreakpoint<R> {
217         check(sourceValue.isFinite())
218 
219         // memoize for FluentSpecEndSegmentWithNextBreakpoint
220         targetValue = target
221 
222         return this
223     }
224 
225     override fun continueWithFractionalInput(
226         fraction: Float
227     ): FluentSpecEndSegmentWithNextBreakpoint<R> {
228         check(sourceValue.isFinite())
229 
230         // memoize for FluentSpecEndSegmentWithNextBreakpoint
231         fractionalMapping = fraction
232 
233         return this
234     }
235 
236     override fun continueWithConstantValue(): FluentSpecEndSegmentWithNextBreakpoint<R> {
237         check(sourceValue.isFinite())
238 
239         mappings.add(Mapping.Fixed(sourceValue))
240 
241         sourceValue = Float.NaN
242         return this
243     }
244 
245     // FluentSpecDefineBreakpointAndStartNextSegment implementation
246 
247     override fun jumpTo(
248         value: Float,
249         spring: SpringParameters,
250         guarantee: Guarantee,
251     ): FluentSpecDefineLinearSegmentMapping<R> {
252         check(sourceValue.isNaN())
253 
254         doAddBreakpoint(spring, guarantee)
255         sourceValue = value
256 
257         return this
258     }
259 
260     override fun jumpBy(
261         delta: Float,
262         spring: SpringParameters,
263         guarantee: Guarantee,
264     ): FluentSpecDefineLinearSegmentMapping<R> {
265         check(sourceValue.isNaN())
266 
267         val breakpoint = doAddBreakpoint(spring, guarantee)
268         sourceValue = mappings.last().map(breakpoint.position) + delta
269 
270         return this
271     }
272 
273     override fun continueWith(
274         mapping: Mapping,
275         spring: SpringParameters,
276         guarantee: Guarantee,
277     ): FluentSpecEndSegmentWithNextBreakpoint<R> {
278         check(sourceValue.isNaN())
279 
280         doAddBreakpoint(spring, guarantee)
281         mappings.add(mapping)
282 
283         return this
284     }
285 
286     override fun completeWith(mapping: Mapping, spring: SpringParameters, guarantee: Guarantee): R {
287         check(sourceValue.isNaN())
288 
289         doAddBreakpoint(spring, guarantee)
290         mappings.add(mapping)
291 
292         return complete()
293     }
294 
295     // FluentSpecEndSegmentWithNextBreakpoint implementation
296 
297     override fun toBreakpoint(
298         atPosition: Float,
299         key: BreakpointKey,
300     ): FluentSpecDefineBreakpointAndStartNextSegment<R> {
301         check(breakpointPosition.isNaN())
302         check(breakpointKey == null)
303 
304         if (!targetValue.isNaN() || !fractionalMapping.isNaN()) {
305             check(!sourceValue.isNaN())
306 
307             val sourcePosition = breakpoints.last().position
308             val breakpointDistance = atPosition - sourcePosition
309             val mapping =
310                 if (breakpointDistance == 0f) {
311                     Mapping.Fixed(sourceValue)
312                 } else {
313                     if (fractionalMapping.isNaN()) {
314                         val delta = targetValue - sourceValue
315                         fractionalMapping = delta / breakpointDistance
316                     } else {
317                         val delta = breakpointDistance * fractionalMapping
318                         targetValue = sourceValue + delta
319                     }
320 
321                     val offset =
322                         if (buildForward) sourceValue - (sourcePosition * fractionalMapping)
323                         else targetValue - (atPosition * fractionalMapping)
324                     Mapping.Linear(fractionalMapping, offset)
325                 }
326 
327             mappings.add(mapping)
328             targetValue = Float.NaN
329             sourceValue = Float.NaN
330             fractionalMapping = Float.NaN
331         }
332 
333         breakpointPosition = atPosition
334         breakpointKey = key
335 
336         return this
337     }
338 
339     override fun complete(): R {
340         check(targetValue.isNaN()) { "cant specify target value for last segment" }
341 
342         if (!fractionalMapping.isNaN()) {
343             check(!sourceValue.isNaN())
344 
345             val sourcePosition = breakpoints.last().position
346 
347             mappings.add(
348                 Mapping.Linear(
349                     fractionalMapping,
350                     sourceValue - (sourcePosition * fractionalMapping),
351                 )
352             )
353         }
354 
355         if (buildForward) {
356             breakpoints.add(Breakpoint.maxLimit)
357         } else {
358             breakpoints.add(Breakpoint.minLimit)
359             breakpoints.reverse()
360             mappings.reverse()
361         }
362 
363         return toResult(DirectionalMotionSpec(breakpoints.toList(), mappings.toList()))
364     }
365 
366     private fun doAddBreakpoint(springSpec: SpringParameters, guarantee: Guarantee): Breakpoint {
367         check(breakpointPosition.isFinite())
368         return Breakpoint(checkNotNull(breakpointKey), breakpointPosition, springSpec, guarantee)
369             .also {
370                 breakpoints.add(it)
371                 breakpointPosition = Float.NaN
372                 breakpointKey = null
373             }
374     }
375 }
376