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