1 /*
<lambda>null2  * Copyright 2022 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 androidx.compose.foundation.layout
18 
19 import android.graphics.Insets
20 import android.os.Build
21 import android.os.CancellationSignal
22 import android.view.View
23 import android.view.ViewConfiguration
24 import android.view.WindowInsetsAnimationControlListener
25 import android.view.WindowInsetsAnimationController
26 import androidx.annotation.RequiresApi
27 import androidx.compose.animation.core.Animatable
28 import androidx.compose.animation.core.FloatDecayAnimationSpec
29 import androidx.compose.animation.core.animateDecay
30 import androidx.compose.runtime.Composable
31 import androidx.compose.runtime.DisposableEffect
32 import androidx.compose.runtime.remember
33 import androidx.compose.ui.Modifier
34 import androidx.compose.ui.composed
35 import androidx.compose.ui.geometry.Offset
36 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
37 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
38 import androidx.compose.ui.input.nestedscroll.nestedScroll
39 import androidx.compose.ui.platform.LocalDensity
40 import androidx.compose.ui.platform.LocalLayoutDirection
41 import androidx.compose.ui.platform.LocalView
42 import androidx.compose.ui.platform.debugInspectorInfo
43 import androidx.compose.ui.unit.Density
44 import androidx.compose.ui.unit.LayoutDirection
45 import androidx.compose.ui.unit.Velocity
46 import androidx.compose.ui.util.fastRoundToInt
47 import androidx.compose.ui.util.packFloats
48 import androidx.compose.ui.util.unpackFloat1
49 import androidx.compose.ui.util.unpackFloat2
50 import kotlin.math.abs
51 import kotlin.math.exp
52 import kotlin.math.ln
53 import kotlin.math.sign
54 import kotlinx.coroutines.CancellableContinuation
55 import kotlinx.coroutines.CancellationException
56 import kotlinx.coroutines.ExperimentalCoroutinesApi
57 import kotlinx.coroutines.Job
58 import kotlinx.coroutines.coroutineScope
59 import kotlinx.coroutines.launch
60 import kotlinx.coroutines.suspendCancellableCoroutine
61 
62 /**
63  * Controls the soft keyboard as a nested scrolling on Android [R][Build.VERSION_CODES.R] and later.
64  * This allows the user to drag the soft keyboard up and down.
65  *
66  * After scrolling, the IME will animate either to the fully shown or fully hidden position,
67  * depending on the position and fling.
68  *
69  * @sample androidx.compose.foundation.layout.samples.windowInsetsNestedScrollDemo
70  */
71 @ExperimentalLayoutApi
72 fun Modifier.imeNestedScroll(): Modifier {
73     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
74         return this
75     }
76     return composed(debugInspectorInfo { name = "imeNestedScroll" }) {
77         val nestedScrollConnection =
78             rememberWindowInsetsConnection(
79                 WindowInsetsHolder.current().ime,
80                 WindowInsetsSides.Bottom
81             )
82         nestedScroll(nestedScrollConnection)
83     }
84 }
85 
86 /**
87  * Returns a [NestedScrollConnection] that can be used with [WindowInsets] on Android
88  * [R][Build.VERSION_CODES.R] and later.
89  *
90  * The [NestedScrollConnection] can be used when a developer wants to control a [WindowInsets],
91  * either directly animating it or allowing the user to manually manipulate it. User interactions
92  * will result in the [WindowInsets] animating either hidden or shown, depending on its current
93  * position and the fling velocity received in [NestedScrollConnection.onPreFling] and
94  * [NestedScrollConnection.onPostFling].
95  *
96  * @param windowInsets The insets to be changed by the scroll effect
97  * @param side The side of the [windowInsets] that is to be affected. Can only be one of
98  *   [WindowInsetsSides.Left], [WindowInsetsSides.Top], [WindowInsetsSides.Right],
99  *   [WindowInsetsSides.Bottom], [WindowInsetsSides.Start], [WindowInsetsSides.End].
100  */
101 @ExperimentalLayoutApi
102 @Composable
rememberWindowInsetsConnectionnull103 internal fun rememberWindowInsetsConnection(
104     windowInsets: AndroidWindowInsets,
105     side: WindowInsetsSides
106 ): NestedScrollConnection {
107     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
108         return DoNothingNestedScrollConnection
109     }
110     val layoutDirection = LocalLayoutDirection.current
111     val sideCalculator = SideCalculator.chooseCalculator(side, layoutDirection)
112     val view = LocalView.current
113     val density = LocalDensity.current
114     val connection =
115         remember(windowInsets, view, sideCalculator, density) {
116             WindowInsetsNestedScrollConnection(windowInsets, view, sideCalculator, density)
117         }
118     DisposableEffect(connection) { onDispose { connection.dispose() } }
119     return connection
120 }
121 
122 /** A [NestedScrollConnection] that does nothing, for versions before R. */
123 private object DoNothingNestedScrollConnection : NestedScrollConnection
124 
125 /**
126  * Used in place of the standard Job cancellation pathway to avoid reflective javaClass.simpleName
127  * lookups to build the exception message and stack trace collection. Remove if these are changed in
128  * kotlinx.coroutines.
129  */
130 private class WindowInsetsAnimationCancelledException :
131     CancellationException("Window insets animation cancelled") {
fillInStackTracenull132     override fun fillInStackTrace(): Throwable {
133         // Avoid null.clone() on Android <= 6.0 when accessing stackTrace
134         stackTrace = emptyArray()
135         return this
136     }
137 }
138 
139 @OptIn(ExperimentalCoroutinesApi::class, ExperimentalLayoutApi::class)
140 @RequiresApi(Build.VERSION_CODES.R)
141 private class WindowInsetsNestedScrollConnection(
142     val windowInsets: AndroidWindowInsets,
143     val view: View,
144     val sideCalculator: SideCalculator,
145     val density: Density
146 ) : NestedScrollConnection, WindowInsetsAnimationControlListener {
147 
148     /**
149      * The [WindowInsetsAnimationController] is only available once the insets are starting to be
150      * manipulated. This is used to set the current insets position.
151      */
152     private var animationController: WindowInsetsAnimationController? = null
153 
154     /**
155      * `true` when we've requested a [WindowInsetsAnimationController] so that we don't ask for one
156      * when we've already asked for one. This should be `false` until we've made a request or when
157      * we've cleared [animationController] after it is finished.
158      */
159     private var isControllerRequested = false
160 
161     /**
162      * We never need to cancel the animation because we always control it directly instead of using
163      * the [WindowInsetsAnimationController] to animate its value.
164      */
165     private val cancellationSignal = CancellationSignal()
166 
167     /**
168      * Because touch motion has finer granularity than integers, we capture the fractions of
169      * integers here so that we can keep the finger more in line with the touch. Without this, we'd
170      * accumulate error.
171      */
172     private var partialConsumption = 0f
173 
174     /**
175      * The [Job] that is launched to animate the insets during a fling. This can be canceled when
176      * the user touches the screen.
177      */
178     private var animationJob: Job? = null
179 
180     /**
181      * Request an animation controller because it is `null`. If one has already been requested, this
182      * method does nothing.
183      */
requestAnimationControllernull184     private fun requestAnimationController() {
185         if (!isControllerRequested) {
186             isControllerRequested = true
187             view.windowInsetsController?.controlWindowInsetsAnimation(
188                 windowInsets.type, // type
189                 -1, // durationMillis
190                 null, // interpolator
191                 cancellationSignal,
192                 this
193             )
194         }
195     }
196 
197     private var continuation: CancellableContinuation<WindowInsetsAnimationController?>? = null
198 
199     /** Allows us to suspend, waiting for the animation controller to be returned. */
getAnimationControllernull200     private suspend fun getAnimationController(): WindowInsetsAnimationController? =
201         animationController
202             ?: suspendCancellableCoroutine { continuation ->
203                 this.continuation = continuation
204                 requestAnimationController()
205             }
206 
207     /** Handle the dragging that hides the WindowInsets. */
onPreScrollnull208     override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset =
209         scroll(available, sideCalculator.hideMotion(available.x, available.y))
210 
211     /** Handle the dragging that exposes the WindowInsets. */
212     override fun onPostScroll(
213         consumed: Offset,
214         available: Offset,
215         source: NestedScrollSource
216     ): Offset = scroll(available, sideCalculator.showMotion(available.x, available.y))
217 
218     /** Scrolls [scrollAmount] and returns the consumed amount of [available]. */
219     private fun scroll(available: Offset, scrollAmount: Float): Offset {
220         animationJob?.let {
221             it.cancel(WindowInsetsAnimationCancelledException())
222             animationJob = null
223         }
224 
225         val animationController = animationController
226 
227         if (
228             scrollAmount == 0f ||
229                 (windowInsets.isVisible == (scrollAmount > 0f) && animationController == null)
230         ) {
231             // No motion in the right direction or this is already fully shown/hidden.
232             return Offset.Zero
233         }
234 
235         if (animationController == null) {
236             partialConsumption = 0f
237             // The animation controller isn't ready yet. Just consume the scroll.
238             requestAnimationController()
239             return sideCalculator.consumedOffsets(available)
240         }
241 
242         val hidden = sideCalculator.valueOf(animationController.hiddenStateInsets)
243         val shown = sideCalculator.valueOf(animationController.shownStateInsets)
244         val currentInsets = animationController.currentInsets
245         val current = sideCalculator.valueOf(currentInsets)
246 
247         val target = if (scrollAmount > 0f) shown else hidden
248 
249         if (current == target) {
250             // This is already correct, so nothing to consume
251             partialConsumption = 0f
252             return Offset.Zero
253         }
254 
255         val total = current + scrollAmount + partialConsumption
256         val next = total.fastRoundToInt().coerceIn(hidden, shown)
257         partialConsumption = total - total.fastRoundToInt()
258 
259         if (next != current) {
260             animationController.setInsetsAndAlpha(
261                 sideCalculator.adjustInsets(currentInsets, next),
262                 1f, // alpha
263                 0f, // progress
264             )
265         }
266         return sideCalculator.consumedOffsets(available)
267     }
268 
269     /** Handle flinging toward hiding the insets. */
onPreFlingnull270     override suspend fun onPreFling(available: Velocity): Velocity =
271         fling(available, sideCalculator.hideMotion(available.x, available.y), false)
272 
273     /** Handle flinging toward showing the insets. */
274     override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity =
275         fling(available, sideCalculator.showMotion(available.x, available.y), true)
276 
277     /**
278      * Handle flinging by [flingAmount] and return the consumed velocity of [available].
279      * [towardShown] should be `true` when the intended motion is to show the insets or `false` if
280      * to hide them. We always handle flinging toward the insets if the [flingAmount] is `0` so that
281      * the insets animate to a fully-shown or fully-hidden state.
282      */
283     private suspend fun fling(
284         available: Velocity,
285         flingAmount: Float,
286         towardShown: Boolean
287     ): Velocity {
288         animationJob?.cancel(WindowInsetsAnimationCancelledException())
289         animationJob = null
290         partialConsumption = 0f
291 
292         if (
293             (flingAmount == 0f && !towardShown) ||
294                 (animationController == null && windowInsets.isVisible == towardShown)
295         ) {
296             // Either there's no motion to hide or we're certain that
297             // the inset is already correct.
298             return Velocity.Zero
299         }
300 
301         val animationController = getAnimationController() ?: return Velocity.Zero
302 
303         val hidden = sideCalculator.valueOf(animationController.hiddenStateInsets)
304         val shown = sideCalculator.valueOf(animationController.shownStateInsets)
305         val currentInsets = animationController.currentInsets
306         val current = sideCalculator.valueOf(currentInsets)
307 
308         if ((flingAmount <= 0 && current == hidden) || (flingAmount >= 0 && current == shown)) {
309             // We've already reached the destination
310             animationController.finish(current == shown)
311             this@WindowInsetsNestedScrollConnection.animationController = null
312             return Velocity.Zero
313         }
314 
315         // Let's see if the velocity is enough to get open
316         val spec = SplineBasedFloatDecayAnimationSpec(density)
317         val distance = current + spec.flingDistance(flingAmount)
318 
319         val endPercent = (distance - hidden) / (shown - hidden)
320         val targetShown = endPercent > 0.5f
321         val target = if (targetShown) shown else hidden
322 
323         if (distance > shown || distance < hidden) {
324             var endVelocity = 0f
325             // This is enough to reach hidden or shown state, so we can use the Android
326             // spline animation.
327             coroutineScope {
328                 animationJob = launch {
329                     animateDecay(
330                         initialValue = current.toFloat(),
331                         initialVelocity = flingAmount,
332                         animationSpec = spec
333                     ) { value, velocity ->
334                         if (value in hidden.toFloat()..shown.toFloat()) {
335                             adjustInsets(value)
336                         } else {
337                             // We've reached the end
338                             endVelocity = velocity
339                             animationController.finish(targetShown)
340                             this@WindowInsetsNestedScrollConnection.animationController = null
341                             animationJob?.cancel(WindowInsetsAnimationCancelledException())
342                         }
343                     }
344                 }
345                 animationJob?.join()
346                 animationJob = null
347             }
348             return sideCalculator.consumedVelocity(available, endVelocity)
349         } else {
350             // This fling won't make it to the end, so animate to shown or hidden state using
351             // a spring animation
352             coroutineScope {
353                 animationJob = launch {
354                     val animatedValue = Animatable(current.toFloat())
355                     animatedValue.animateTo(target.toFloat(), initialVelocity = flingAmount) {
356                         adjustInsets(value)
357                     }
358                     animationController.finish(targetShown)
359                     this@WindowInsetsNestedScrollConnection.animationController = null
360                 }
361             }
362             return sideCalculator.consumedVelocity(available, 0f)
363         }
364     }
365 
366     /** Change the inset's side to [inset]. */
adjustInsetsnull367     private fun adjustInsets(inset: Float) {
368         animationController?.let {
369             val currentInsets = it.currentInsets
370             val nextInsets = sideCalculator.adjustInsets(currentInsets, inset.fastRoundToInt())
371             it.setInsetsAndAlpha(
372                 nextInsets,
373                 1f, // alpha
374                 0f, // progress
375             )
376         }
377     }
378 
379     /** Called after [requestAnimationController] and the [animationController] is ready. */
onReadynull380     override fun onReady(controller: WindowInsetsAnimationController, types: Int) {
381         animationController = controller
382         isControllerRequested = false
383         continuation?.resume(controller) {}
384         continuation = null
385     }
386 
disposenull387     fun dispose() {
388         continuation?.resume(null) {}
389         animationJob?.cancel()
390         val animationController = animationController
391         if (animationController != null) {
392             // We don't want to leave the insets in a partially open or closed state, so finish
393             // the animation
394             val visible = animationController.currentInsets != animationController.hiddenStateInsets
395             animationController.finish(visible)
396         }
397     }
398 
onFinishednull399     override fun onFinished(controller: WindowInsetsAnimationController) {
400         animationEnded()
401     }
402 
onCancellednull403     override fun onCancelled(controller: WindowInsetsAnimationController?) {
404         animationEnded()
405     }
406 
407     /** The controlled animation has been terminated. */
animationEndednull408     private fun animationEnded() {
409         if (animationController?.isReady == true) {
410             animationController?.finish(windowInsets.isVisible)
411         }
412         animationController = null
413 
414         // The animation controller may not have been given to us, so we have to cancel animations
415         // waiting for it.
416         continuation?.resume(null) {}
417         continuation = null
418 
419         // Cancel any animation that's running.
420         animationJob?.cancel(WindowInsetsAnimationCancelledException())
421         animationJob = null
422 
423         partialConsumption = 0f
424         isControllerRequested = false
425     }
426 }
427 
428 /**
429  * This interface allows logic for the specific side (left, top, right, bottom) to be extracted from
430  * the logic controlling showing and hiding insets. For example, an inset at the top will show when
431  * dragging down, while an inset at the bottom will hide when dragging down.
432  */
433 @RequiresApi(Build.VERSION_CODES.R)
434 private interface SideCalculator {
435     /** Returns the insets value for the side that this [SideCalculator] is associated with. */
valueOfnull436     fun valueOf(insets: Insets): Int
437 
438     /**
439      * Returns the motion, adjusted for side direction, that the [x], and [y] grant. A positive
440      * result indicates that it is in the direction of opening the insets on that side and a
441      * negative result indicates a closing of the insets on that side.
442      */
443     fun motionOf(x: Float, y: Float): Float
444 
445     /**
446      * The motion of [x], [y] that indicates showing more of the insets on the side or `0` if no
447      * motion is given to showing more insets.
448      */
449     fun showMotion(x: Float, y: Float): Float = motionOf(x, y).coerceAtLeast(0f)
450 
451     /**
452      * The motion of [x], [y] that indicates showing less of the insets on the side or `0` if no
453      * motion is given to showing less insets.
454      */
455     fun hideMotion(x: Float, y: Float): Float = motionOf(x, y).coerceAtMost(0f)
456 
457     /**
458      * Takes all values of [oldInsets], except for this side and replaces this side with [newValue].
459      */
460     fun adjustInsets(oldInsets: Insets, newValue: Int): Insets
461 
462     /** Returns the [Offset] that consumes [available] in the direction of this side. */
463     fun consumedOffsets(available: Offset): Offset
464 
465     /** Returns the [Velocity] that consumes [available] in the direction of this side. */
466     fun consumedVelocity(available: Velocity, remaining: Float): Velocity
467 
468     companion object {
469         /**
470          * Returns a [SideCalculator] for [side] and the given [layoutDirection]. This only works
471          * for one side and no combination of sides.
472          */
473         fun chooseCalculator(side: WindowInsetsSides, layoutDirection: LayoutDirection) =
474             when (side) {
475                 WindowInsetsSides.Left -> LeftSideCalculator
476                 WindowInsetsSides.Top -> TopSideCalculator
477                 WindowInsetsSides.Right -> RightSideCalculator
478                 WindowInsetsSides.Bottom -> BottomSideCalculator
479                 WindowInsetsSides.Start ->
480                     if (layoutDirection == LayoutDirection.Ltr) {
481                         LeftSideCalculator
482                     } else {
483                         RightSideCalculator
484                     }
485                 WindowInsetsSides.End ->
486                     if (layoutDirection == LayoutDirection.Ltr) {
487                         RightSideCalculator
488                     } else {
489                         LeftSideCalculator
490                     }
491                 else -> error("Only Left, Top, Right, Bottom, Start and End are allowed")
492             }
493 
494         private val LeftSideCalculator =
495             object : SideCalculator {
496                 override fun valueOf(insets: Insets): Int = insets.left
497 
498                 override fun motionOf(x: Float, y: Float): Float = x
499 
500                 override fun adjustInsets(oldInsets: Insets, newValue: Int): Insets =
501                     Insets.of(newValue, oldInsets.top, oldInsets.right, oldInsets.bottom)
502 
503                 override fun consumedOffsets(available: Offset): Offset = Offset(available.x, 0f)
504 
505                 override fun consumedVelocity(available: Velocity, remaining: Float): Velocity =
506                     Velocity(available.x - remaining, 0f)
507             }
508 
509         private val TopSideCalculator =
510             object : SideCalculator {
511                 override fun valueOf(insets: Insets): Int = insets.top
512 
513                 override fun motionOf(x: Float, y: Float): Float = y
514 
515                 override fun adjustInsets(oldInsets: Insets, newValue: Int): Insets =
516                     Insets.of(oldInsets.left, newValue, oldInsets.right, oldInsets.bottom)
517 
518                 override fun consumedOffsets(available: Offset): Offset = Offset(0f, available.y)
519 
520                 override fun consumedVelocity(available: Velocity, remaining: Float): Velocity =
521                     Velocity(0f, available.y - remaining)
522             }
523 
524         private val RightSideCalculator =
525             object : SideCalculator {
526                 override fun valueOf(insets: Insets): Int = insets.right
527 
528                 override fun motionOf(x: Float, y: Float): Float = -x
529 
530                 override fun adjustInsets(oldInsets: Insets, newValue: Int): Insets =
531                     Insets.of(oldInsets.left, oldInsets.top, newValue, oldInsets.bottom)
532 
533                 override fun consumedOffsets(available: Offset): Offset = Offset(available.x, 0f)
534 
535                 override fun consumedVelocity(available: Velocity, remaining: Float): Velocity =
536                     Velocity(available.x + remaining, 0f)
537             }
538 
539         private val BottomSideCalculator =
540             object : SideCalculator {
541                 override fun valueOf(insets: Insets): Int = insets.bottom
542 
543                 override fun motionOf(x: Float, y: Float): Float = -y
544 
545                 override fun adjustInsets(oldInsets: Insets, newValue: Int): Insets =
546                     Insets.of(oldInsets.left, oldInsets.top, oldInsets.right, newValue)
547 
548                 override fun consumedOffsets(available: Offset): Offset = Offset(0f, available.y)
549 
550                 override fun consumedVelocity(available: Velocity, remaining: Float): Velocity =
551                     Velocity(0f, available.y + remaining)
552             }
553     }
554 }
555 
556 // SplineBasedFloatDecayAnimationSpec is in animation:animation library, which depends on
557 // foundation-layout, so I've copied it below, but a bit trimmed to only have what is needed.
558 
559 // These constants are copied from the Android spline decay rate
560 private const val Inflection = 0.35f // Tension lines cross at (Inflection, 1)
561 private val PlatformFlingScrollFriction = ViewConfiguration.getScrollFriction()
562 private const val GravityEarth = 9.80665f
563 private const val InchesPerMeter = 39.37f
564 private val DecelerationRate = ln(0.78) / ln(0.9)
565 private val DecelMinusOne = DecelerationRate - 1.0
566 private const val StartTension = 0.5f
567 private const val EndTension = 1.0f
568 private const val P1 = StartTension * Inflection
569 private const val P2 = 1.0f - EndTension * (1.0f - Inflection)
570 
571 private class SplineBasedFloatDecayAnimationSpec(density: Density) : FloatDecayAnimationSpec {
572 
573     override val absVelocityThreshold: Float
574         get() = 0f
575 
576     /** A density-specific coefficient adjusted to physical values. */
577     private val magicPhysicalCoefficient: Float =
578         GravityEarth * InchesPerMeter * density.density * 160f * 0.84f
579 
getSplineDecelerationnull580     private fun getSplineDeceleration(velocity: Float): Double =
581         AndroidFlingSpline.deceleration(
582             velocity,
583             PlatformFlingScrollFriction * magicPhysicalCoefficient
584         )
585 
586     /** Compute the distance of a fling in units given an initial [velocity] of units/second */
587     fun flingDistance(velocity: Float): Float {
588         val l = getSplineDeceleration(velocity)
589         return (PlatformFlingScrollFriction *
590                 magicPhysicalCoefficient *
591                 exp(DecelerationRate / DecelMinusOne * l))
592             .toFloat() * sign(velocity)
593     }
594 
getTargetValuenull595     override fun getTargetValue(initialValue: Float, initialVelocity: Float): Float =
596         initialValue + flingDistance(initialVelocity)
597 
598     @Suppress("MethodNameUnits")
599     override fun getValueFromNanos(
600         playTimeNanos: Long,
601         initialValue: Float,
602         initialVelocity: Float
603     ): Float {
604         val duration = getDurationNanos(0f, initialVelocity)
605         val splinePos = if (duration > 0) playTimeNanos / duration.toFloat() else 1f
606         val distance = flingDistance(initialVelocity)
607         return initialValue +
608             distance * AndroidFlingSpline.flingPosition(splinePos).distanceCoefficient
609     }
610 
611     @Suppress("MethodNameUnits")
getDurationNanosnull612     override fun getDurationNanos(initialValue: Float, initialVelocity: Float): Long {
613         val l = getSplineDeceleration(initialVelocity)
614         return (1_000_000_000.0 * exp(l / DecelMinusOne)).toLong()
615     }
616 
617     @Suppress("MethodNameUnits")
getVelocityFromNanosnull618     override fun getVelocityFromNanos(
619         playTimeNanos: Long,
620         initialValue: Float,
621         initialVelocity: Float
622     ): Float {
623         val duration = getDurationNanos(0f, initialVelocity)
624         val splinePos = if (duration > 0L) playTimeNanos / duration.toFloat() else 1f
625         val distance = flingDistance(initialVelocity)
626         return AndroidFlingSpline.flingPosition(splinePos).velocityCoefficient * distance /
627             duration * 1_000_000_000.0f
628     }
629 }
630 
631 private object AndroidFlingSpline {
632     private const val NbSamples = 100
633     private val SplinePositions = FloatArray(NbSamples + 1)
634     private val SplineTimes = FloatArray(NbSamples + 1)
635 
<lambda>null636     init {
637         var xMin = 0.0f
638         var yMin = 0.0f
639         for (i in 0 until NbSamples) {
640             val alpha = i.toFloat() / NbSamples
641             var xMax = 1.0f
642             var x: Float
643             var tx: Float
644             var coef: Float
645             while (true) {
646                 x = xMin + (xMax - xMin) / 2.0f
647                 coef = 3.0f * x * (1.0f - x)
648                 tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x
649                 if (abs(tx - alpha) < 1E-5) break
650                 if (tx > alpha) xMax = x else xMin = x
651             }
652             SplinePositions[i] = coef * ((1.0f - x) * StartTension + x) + x * x * x
653             var yMax = 1.0f
654             var y: Float
655             var dy: Float
656             while (true) {
657                 y = yMin + (yMax - yMin) / 2.0f
658                 coef = 3.0f * y * (1.0f - y)
659                 dy = coef * ((1.0f - y) * StartTension + y) + y * y * y
660                 if (abs(dy - alpha) < 1E-5) break
661                 if (dy > alpha) yMax = y else yMin = y
662             }
663             SplineTimes[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y
664         }
665         SplineTimes[NbSamples] = 1.0f
666         SplinePositions[NbSamples] = SplineTimes[NbSamples]
667     }
668 
669     /**
670      * Compute an instantaneous fling position along the scroller spline.
671      *
672      * @param time progress through the fling animation from 0-1
673      */
flingPositionnull674     fun flingPosition(time: Float): FlingResult {
675         val index = (NbSamples * time).toInt()
676         var distanceCoef = 1f
677         var velocityCoef = 0f
678         if (index < NbSamples) {
679             val tInf = index.toFloat() / NbSamples
680             val tSup = (index + 1).toFloat() / NbSamples
681             val dInf = SplinePositions[index]
682             val dSup = SplinePositions[index + 1]
683             velocityCoef = (dSup - dInf) / (tSup - tInf)
684             distanceCoef = dInf + (time - tInf) * velocityCoef
685         }
686         return FlingResult(packFloats(distanceCoef, velocityCoef))
687     }
688 
689     /** The rate of deceleration along the spline motion given [velocity] and [friction]. */
decelerationnull690     fun deceleration(velocity: Float, friction: Float): Double =
691         ln(Inflection * abs(velocity) / friction.toDouble())
692 
693     /** Result coefficients of a scroll computation */
694     @JvmInline
695     value class FlingResult(private val packedValue: Long) {
696         /** Linear distance traveled from 0-1, from source (0) to destination (1) */
697         val distanceCoefficient: Float
698             get() = unpackFloat1(packedValue)
699 
700         /**
701          * Instantaneous velocity coefficient at this point in the fling expressed in total distance
702          * per unit time
703          */
704         val velocityCoefficient: Float
705             get() = unpackFloat2(packedValue)
706     }
707 }
708