• 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.wm.shell.back
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.ValueAnimator
22 import android.content.Context
23 import android.content.res.Configuration
24 import android.graphics.Color
25 import android.graphics.Matrix
26 import android.graphics.PointF
27 import android.graphics.Rect
28 import android.graphics.RectF
29 import android.os.Handler
30 import android.os.RemoteException
31 import android.util.TimeUtils
32 import android.view.Choreographer
33 import android.view.Display
34 import android.view.IRemoteAnimationFinishedCallback
35 import android.view.IRemoteAnimationRunner
36 import android.view.RemoteAnimationTarget
37 import android.view.SurfaceControl
38 import android.view.animation.DecelerateInterpolator
39 import android.view.animation.Interpolator
40 import android.view.animation.Transformation
41 import android.window.BackEvent
42 import android.window.BackEvent.EDGE_LEFT
43 import android.window.BackEvent.EDGE_RIGHT
44 import android.window.BackMotionEvent
45 import android.window.BackNavigationInfo
46 import android.window.BackProgressAnimator
47 import android.window.IOnBackInvokedCallback
48 import com.android.internal.dynamicanimation.animation.FloatValueHolder
49 import com.android.internal.dynamicanimation.animation.SpringAnimation
50 import com.android.internal.dynamicanimation.animation.SpringForce
51 import com.android.internal.jank.Cuj
52 import com.android.internal.policy.ScreenDecorationsUtils
53 import com.android.internal.policy.SystemBarUtils
54 import com.android.internal.protolog.ProtoLog
55 import com.android.window.flags.Flags.enableMultidisplayTrackpadBackGesture
56 import com.android.window.flags.Flags.predictiveBackTimestampApi
57 import com.android.wm.shell.R
58 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
59 import com.android.wm.shell.protolog.ShellProtoLogGroup
60 import com.android.wm.shell.shared.animation.Interpolators
61 import com.android.wm.shell.shared.annotations.ShellMainThread
62 import kotlin.math.abs
63 import kotlin.math.max
64 import kotlin.math.min
65 
66 abstract class CrossActivityBackAnimation(
67     private val context: Context,
68     private val background: BackAnimationBackground,
69     private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
70     protected val transaction: SurfaceControl.Transaction,
71     @ShellMainThread handler: Handler,
72 ) : ShellBackAnimation() {
73 
74     protected val startClosingRect = RectF()
75     protected val targetClosingRect = RectF()
76     protected val currentClosingRect = RectF()
77 
78     protected val startEnteringRect = RectF()
79     protected val targetEnteringRect = RectF()
80     protected val currentEnteringRect = RectF()
81 
82     protected val backAnimRect = Rect()
83     private val cropRect = Rect()
84     private val tempRectF = RectF()
85 
86     private var cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
87     private var statusbarHeight = SystemBarUtils.getStatusBarHeight(context)
88 
89     private val backAnimationRunner =
90         BackAnimationRunner(
91             Callback(),
92             Runner(),
93             context,
94             Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY,
95             handler,
96         )
97     private val initialTouchPos = PointF()
98     private val transformMatrix = Matrix()
99     private val tmpFloat9 = FloatArray(9)
100     protected var enteringTarget: RemoteAnimationTarget? = null
101     protected var closingTarget: RemoteAnimationTarget? = null
102     private var triggerBack = false
103     private var finishCallback: IRemoteAnimationFinishedCallback? = null
104     private val progressAnimator = BackProgressAnimator()
105     protected val displayBoundsMargin =
106         context.resources.getDimension(R.dimen.cross_task_back_vertical_margin)
107 
108     private val gestureInterpolator = Interpolators.BACK_GESTURE
109     private val verticalMoveInterpolator: Interpolator = DecelerateInterpolator()
110 
111     private var scrimLayer: SurfaceControl? = null
112     private var maxScrimAlpha: Float = 0f
113 
114     private var isLetterboxed = false
115     private var enteringHasSameLetterbox = false
116     private var leftLetterboxLayer: SurfaceControl? = null
117     private var rightLetterboxLayer: SurfaceControl? = null
118     private var letterboxColor: Int = 0
119 
120     private val postCommitFlingScale = FloatValueHolder(SPRING_SCALE)
121     private var lastPostCommitFlingScale = SPRING_SCALE
122     private val postCommitFlingSpring = SpringForce(SPRING_SCALE)
123             .setStiffness(SpringForce.STIFFNESS_LOW)
124             .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
125     private var swipeEdge = EDGE_LEFT
126     protected var gestureProgress = 0f
127     private val velocityTracker = ProgressVelocityTracker()
128 
129     /** Background color to be used during the animation, also see [getBackgroundColor] */
130     protected var customizedBackgroundColor = 0
131 
132     /**
133      * Whether the entering target should be shifted vertically with the user gesture in pre-commit
134      */
135     abstract val allowEnteringYShift: Boolean
136 
137     /**
138      * Subclasses must set the [startClosingRect] and [targetClosingRect] to define the movement
139      * of the closingTarget during pre-commit phase.
140      */
141     abstract fun preparePreCommitClosingRectMovement(@BackEvent.SwipeEdge swipeEdge: Int)
142 
143     /**
144      * Subclasses must set the [startEnteringRect] and [targetEnteringRect] to define the movement
145      * of the enteringTarget during pre-commit phase.
146      */
147     abstract fun preparePreCommitEnteringRectMovement()
148 
149     /**
150      * Subclasses must provide a duration (in ms) for the post-commit part of the animation
151      */
152     abstract fun getPostCommitAnimationDuration(): Long
153 
154     /**
155      * Returns a base transformation to apply to the entering target during pre-commit. The system
156      * will apply the default animation on top of it.
157      */
158     protected open fun getPreCommitEnteringBaseTransformation(progress: Float): Transformation? =
159         null
160 
161     override fun onConfigurationChanged(newConfiguration: Configuration) {
162         cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
163         statusbarHeight = SystemBarUtils.getStatusBarHeight(context)
164     }
165 
166     override fun getRunner() = backAnimationRunner
167 
168     private fun getBackgroundColor(): Int =
169         when {
170             customizedBackgroundColor != 0 -> customizedBackgroundColor
171             isLetterboxed -> letterboxColor
172             enteringTarget != null -> enteringTarget!!.taskInfo.taskDescription!!.backgroundColor
173             else -> 0
174         }
175 
176     protected open fun startBackAnimation(backMotionEvent: BackMotionEvent) {
177         if (enteringTarget == null || closingTarget == null) {
178             ProtoLog.d(
179                 ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW,
180                 "Entering target or closing target is null."
181             )
182             return
183         }
184         swipeEdge = backMotionEvent.swipeEdge
185         triggerBack = backMotionEvent.triggerBack
186         initialTouchPos.set(backMotionEvent.touchX, backMotionEvent.touchY)
187 
188         transaction.setAnimationTransaction()
189         isLetterboxed = closingTarget!!.taskInfo.appCompatTaskInfo.isTopActivityLetterboxed
190         enteringHasSameLetterbox =
191             isLetterboxed && closingTarget!!.localBounds.equals(enteringTarget!!.localBounds)
192 
193         if (isLetterboxed && !enteringHasSameLetterbox) {
194             // Play animation with letterboxes, if closing and entering target have mismatching
195             // letterboxes
196             backAnimRect.set(closingTarget!!.windowConfiguration.bounds)
197         } else {
198             // otherwise play animation on localBounds only
199             backAnimRect.set(closingTarget!!.localBounds)
200         }
201         // Offset start rectangle to align task bounds.
202         backAnimRect.offsetTo(0, 0)
203 
204         preparePreCommitClosingRectMovement(backMotionEvent.swipeEdge)
205         preparePreCommitEnteringRectMovement()
206 
207         background.ensureBackground(
208                 closingTarget!!.windowConfiguration.bounds,
209                 getBackgroundColor(),
210                 transaction,
211                 statusbarHeight,
212                 if (closingTarget!!.windowConfiguration.tasksAreFloating())
213                     closingTarget!!.localBounds else null,
214                 cornerRadius,
215                 closingTarget!!.taskInfo.getDisplayId()
216         )
217         ensureScrimLayer()
218         if (isLetterboxed && enteringHasSameLetterbox) {
219             // crop left and right letterboxes
220             cropRect.set(
221                 closingTarget!!.localBounds.left,
222                 0,
223                 closingTarget!!.localBounds.right,
224                 closingTarget!!.windowConfiguration.bounds.height()
225             )
226             // and add fake letterbox square surfaces instead
227             ensureLetterboxes()
228         } else {
229             cropRect.set(backAnimRect)
230         }
231         applyTransaction()
232     }
233 
234     private fun onGestureProgress(backEvent: BackEvent) {
235         val progress = gestureInterpolator.getInterpolation(backEvent.progress)
236         gestureProgress = progress
237         currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
238         val yOffset = getYOffset(currentClosingRect, backEvent.touchY)
239         currentClosingRect.offset(0f, yOffset)
240         applyTransform(closingTarget?.leash, currentClosingRect, 1f)
241         currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress)
242         if (allowEnteringYShift) currentEnteringRect.offset(0f, yOffset)
243         val enteringTransformation = getPreCommitEnteringBaseTransformation(progress)
244         applyTransform(
245             enteringTarget?.leash,
246             currentEnteringRect,
247             enteringTransformation?.alpha ?: 1f,
248             enteringTransformation
249         )
250         applyTransaction()
251         background.customizeStatusBarAppearance(currentClosingRect.top.toInt())
252         if (predictiveBackTimestampApi()) {
253             velocityTracker.addPosition(backEvent.frameTimeMillis, progress)
254         }
255     }
256 
257     private fun getYOffset(centeredRect: RectF, touchY: Float): Float {
258         val screenHeight = backAnimRect.height()
259         // Base the window movement in the Y axis on the touch movement in the Y axis.
260         val rawYDelta = touchY - initialTouchPos.y
261         val yDirection = (if (rawYDelta < 0) -1 else 1)
262         // limit yDelta interpretation to 1/2 of screen height in either direction
263         val deltaYRatio = min(screenHeight / 2f, abs(rawYDelta)) / (screenHeight / 2f)
264         val interpolatedYRatio: Float = verticalMoveInterpolator.getInterpolation(deltaYRatio)
265         // limit y-shift so surface never passes 8dp screen margin
266         val deltaY =
267             max(0f, (screenHeight - centeredRect.height()) / 2f - displayBoundsMargin) *
268                 interpolatedYRatio *
269                 yDirection
270         return deltaY
271     }
272 
273     protected open fun onGestureCommitted(velocity: Float) {
274         if (
275             closingTarget?.leash == null ||
276                 enteringTarget?.leash == null ||
277                 !enteringTarget!!.leash.isValid ||
278                 !closingTarget!!.leash.isValid
279         ) {
280             finishAnimation()
281             return
282         }
283 
284         // kick off spring animation with the current velocity from the pre-commit phase, this
285         // affects the scaling of the closing and/or opening activity during post-commit
286 
287         var startVelocity = if (predictiveBackTimestampApi()) {
288             // pronounce fling animation more for gestures
289             val velocityFactor = if (swipeEdge == EDGE_LEFT || swipeEdge == EDGE_RIGHT) 2f else 1f
290             velocity * SPRING_SCALE * (1f - MAX_SCALE) * velocityFactor
291         } else {
292             velocity * SPRING_SCALE
293         }
294         if (gestureProgress < 0.1f) {
295             startVelocity = startVelocity.coerceAtLeast(DEFAULT_FLING_VELOCITY)
296         }
297         val flingAnimation = SpringAnimation(postCommitFlingScale, SPRING_SCALE)
298             .setStartVelocity(-startVelocity.coerceIn(0f, MAX_FLING_VELOCITY))
299             .setStartValue(SPRING_SCALE)
300             .setSpring(postCommitFlingSpring)
301         flingAnimation.start()
302         // do an animation-frame immediately to prevent idle frame
303         flingAnimation.doAnimationFrame(
304             Choreographer.getInstance().lastFrameTimeNanos / TimeUtils.NANOS_PER_MS
305         )
306 
307         val valueAnimator =
308             ValueAnimator.ofFloat(1f, 0f).setDuration(getPostCommitAnimationDuration())
309         valueAnimator.addUpdateListener { animation: ValueAnimator ->
310             val progress = animation.animatedFraction
311             onPostCommitProgress(progress)
312             if (progress > 1 - BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD) {
313                 background.resetStatusBarCustomization()
314             }
315         }
316         valueAnimator.addListener(
317             object : AnimatorListenerAdapter() {
318                 override fun onAnimationEnd(animation: Animator) {
319                     background.resetStatusBarCustomization()
320                     finishAnimation()
321                 }
322             }
323         )
324         valueAnimator.start()
325     }
326 
327     protected open fun onPostCommitProgress(linearProgress: Float) {
328         scrimLayer?.let { transaction.setAlpha(it, maxScrimAlpha * (1f - linearProgress)) }
329     }
330 
331     protected open fun finishAnimation() {
332         enteringTarget?.let {
333             if (it.leash != null && it.leash.isValid) {
334                 transaction.setCornerRadius(it.leash, 0f)
335                 if (!triggerBack) transaction.setAlpha(it.leash, 0f)
336                 it.leash.release()
337             }
338             enteringTarget = null
339         }
340 
341         closingTarget?.leash?.release()
342         closingTarget = null
343 
344         background.removeBackground(transaction)
345         applyTransaction()
346         transformMatrix.reset()
347         initialTouchPos.set(0f, 0f)
348         try {
349             finishCallback?.onAnimationFinished()
350         } catch (e: RemoteException) {
351             e.printStackTrace()
352         }
353         finishCallback = null
354         removeScrimLayer()
355         removeLetterbox()
356         isLetterboxed = false
357         enteringHasSameLetterbox = false
358         lastPostCommitFlingScale = SPRING_SCALE
359         gestureProgress = 0f
360         triggerBack = false
361         velocityTracker.resetTracking()
362     }
363 
364     protected fun applyTransform(
365         leash: SurfaceControl?,
366         rect: RectF,
367         alpha: Float,
368         baseTransformation: Transformation? = null,
369         flingMode: FlingMode = FlingMode.NO_FLING
370     ) {
371         if (leash == null || !leash.isValid) return
372         tempRectF.set(rect)
373         if (flingMode != FlingMode.NO_FLING) {
374             lastPostCommitFlingScale = min(
375                 postCommitFlingScale.value / SPRING_SCALE,
376                 if (flingMode == FlingMode.FLING_BOUNCE) 1f else lastPostCommitFlingScale
377             )
378             // apply an additional scale to the closing target to account for fling velocity
379             tempRectF.scaleCentered(lastPostCommitFlingScale)
380         }
381         val scale = tempRectF.width() / backAnimRect.width()
382         val matrix = baseTransformation?.matrix ?: transformMatrix.apply { reset() }
383         val scalePivotX =
384             if (isLetterboxed && enteringHasSameLetterbox) {
385                 closingTarget!!.localBounds.left.toFloat()
386             } else {
387                 0f
388             }
389         matrix.postScale(scale, scale, scalePivotX, 0f)
390         matrix.postTranslate(tempRectF.left, tempRectF.top)
391         transaction
392             .setAlpha(leash, alpha)
393             .setMatrix(leash, matrix, tmpFloat9)
394             .setCrop(leash, cropRect)
395             .setCornerRadius(leash, cornerRadius)
396     }
397 
398     protected fun applyTransaction() {
399         transaction.setFrameTimelineVsync(Choreographer.getInstance().vsyncId)
400         transaction.apply()
401     }
402 
403     private fun ensureScrimLayer() {
404         if (scrimLayer != null) return
405         val isDarkTheme: Boolean = isDarkMode(context)
406         val scrimBuilder =
407             SurfaceControl.Builder()
408                 .setName("Cross-Activity back animation scrim")
409                 .setCallsite("CrossActivityBackAnimation")
410                 .setColorLayer()
411                 .setOpaque(false)
412                 .setHidden(false)
413 
414         if (enableMultidisplayTrackpadBackGesture()) {
415             rootTaskDisplayAreaOrganizer.attachToDisplayArea(
416                 closingTarget!!.taskInfo.getDisplayId(), scrimBuilder)
417         } else {
418             rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, scrimBuilder)
419         }
420         scrimLayer = scrimBuilder.build()
421         val colorComponents = floatArrayOf(0f, 0f, 0f)
422         maxScrimAlpha = if (isDarkTheme) MAX_SCRIM_ALPHA_DARK else MAX_SCRIM_ALPHA_LIGHT
423         val scrimCrop =
424             if (isLetterboxed) {
425                 closingTarget!!.windowConfiguration.bounds
426             } else {
427                 closingTarget!!.localBounds
428             }
429         transaction
430             .setColor(scrimLayer, colorComponents)
431             .setAlpha(scrimLayer!!, maxScrimAlpha)
432             .setCrop(scrimLayer!!, scrimCrop)
433             .setRelativeLayer(scrimLayer!!, closingTarget!!.leash, -1)
434             .show(scrimLayer)
435     }
436 
437     private fun removeScrimLayer() {
438         if (removeLayer(scrimLayer)) applyTransaction()
439         scrimLayer = null
440     }
441 
442     /**
443      * Adds two "fake" letterbox square surfaces to the left and right of the localBounds of the
444      * closing target
445      */
446     private fun ensureLetterboxes() {
447         closingTarget?.let { t ->
448             if (t.localBounds.left != 0 && leftLetterboxLayer == null) {
449                 val bounds =
450                     Rect(
451                         0,
452                         t.windowConfiguration.bounds.top,
453                         t.localBounds.left,
454                         t.windowConfiguration.bounds.bottom
455                     )
456                 leftLetterboxLayer = ensureLetterbox(bounds)
457             }
458             if (
459                 t.localBounds.right != t.windowConfiguration.bounds.right &&
460                     rightLetterboxLayer == null
461             ) {
462                 val bounds =
463                     Rect(
464                         t.localBounds.right,
465                         t.windowConfiguration.bounds.top,
466                         t.windowConfiguration.bounds.right,
467                         t.windowConfiguration.bounds.bottom
468                     )
469                 rightLetterboxLayer = ensureLetterbox(bounds)
470             }
471         }
472     }
473 
474     private fun ensureLetterbox(bounds: Rect): SurfaceControl {
475         val letterboxBuilder =
476             SurfaceControl.Builder()
477                 .setName("Cross-Activity back animation letterbox")
478                 .setCallsite("CrossActivityBackAnimation")
479                 .setColorLayer()
480                 .setOpaque(true)
481                 .setHidden(false)
482 
483         if (enableMultidisplayTrackpadBackGesture()) {
484             rootTaskDisplayAreaOrganizer.attachToDisplayArea(
485                 closingTarget!!.taskInfo.getDisplayId(), letterboxBuilder)
486         } else {
487             rootTaskDisplayAreaOrganizer.attachToDisplayArea(
488                 Display.DEFAULT_DISPLAY, letterboxBuilder)
489         }
490         val layer = letterboxBuilder.build()
491         val colorComponents =
492             floatArrayOf(
493                 Color.red(letterboxColor) / 255f,
494                 Color.green(letterboxColor) / 255f,
495                 Color.blue(letterboxColor) / 255f
496             )
497         transaction
498             .setColor(layer, colorComponents)
499             .setCrop(layer, bounds)
500             .setRelativeLayer(layer, closingTarget!!.leash, 1)
501             .show(layer)
502         return layer
503     }
504 
505     private fun removeLetterbox() {
506         if (removeLayer(leftLetterboxLayer) || removeLayer(rightLetterboxLayer)) applyTransaction()
507         leftLetterboxLayer = null
508         rightLetterboxLayer = null
509     }
510 
511     private fun removeLayer(layer: SurfaceControl?): Boolean {
512         layer?.let {
513             if (it.isValid) {
514                 transaction.remove(it)
515                 return true
516             }
517         }
518         return false
519     }
520 
521     override fun prepareNextAnimation(
522         animationInfo: BackNavigationInfo.CustomAnimationInfo?,
523         letterboxColor: Int
524     ): Boolean {
525         this.letterboxColor = letterboxColor
526         return false
527     }
528 
529     private inner class Callback : IOnBackInvokedCallback.Default() {
530         override fun onBackStarted(backMotionEvent: BackMotionEvent) {
531             // in case we're still animating an onBackCancelled event, let's remove the finish-
532             // callback from the progress animator to prevent calling finishAnimation() before
533             // restarting a new animation
534             progressAnimator.removeOnBackCancelledFinishCallback()
535 
536             startBackAnimation(backMotionEvent)
537             progressAnimator.onBackStarted(backMotionEvent) { backEvent: BackEvent ->
538                 onGestureProgress(backEvent)
539             }
540         }
541 
542         override fun onBackProgressed(backEvent: BackMotionEvent) {
543             triggerBack = backEvent.triggerBack
544             progressAnimator.onBackProgressed(backEvent)
545         }
546 
547         override fun onBackCancelled() {
548             triggerBack = false
549             progressAnimator.onBackCancelled { finishAnimation() }
550         }
551 
552         override fun onBackInvoked() {
553             triggerBack = true
554             progressAnimator.reset()
555             if (predictiveBackTimestampApi()) {
556                 onGestureCommitted(velocityTracker.calculateVelocity())
557             } else {
558                 onGestureCommitted(progressAnimator.velocity)
559             }
560         }
561     }
562 
563     private inner class Runner : IRemoteAnimationRunner.Default() {
564         override fun onAnimationStart(
565             transit: Int,
566             apps: Array<RemoteAnimationTarget>,
567             wallpapers: Array<RemoteAnimationTarget>?,
568             nonApps: Array<RemoteAnimationTarget>?,
569             finishedCallback: IRemoteAnimationFinishedCallback
570         ) {
571             ProtoLog.d(
572                 ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW,
573                 "Start back to activity animation."
574             )
575             for (a in apps) {
576                 when (a.mode) {
577                     RemoteAnimationTarget.MODE_CLOSING -> closingTarget = a
578                     RemoteAnimationTarget.MODE_OPENING -> enteringTarget = a
579                 }
580             }
581             finishCallback = finishedCallback
582         }
583 
584         override fun onAnimationCancelled() {
585             finishAnimation()
586         }
587     }
588 
589     companion object {
590         /** Max scale of the closing window. */
591         internal const val MAX_SCALE = 0.9f
592         private const val MAX_SCRIM_ALPHA_DARK = 0.8f
593         private const val MAX_SCRIM_ALPHA_LIGHT = 0.2f
594         private const val SPRING_SCALE = 100f
595         private const val MAX_FLING_VELOCITY = 1000f
596         private const val DEFAULT_FLING_VELOCITY = 120f
597     }
598 
599     enum class FlingMode {
600         NO_FLING,
601 
602         /**
603          * This is used for the closing target in custom cross-activity back animations. When the
604          * back gesture is flung, the closing target shrinks a bit further with a spring motion.
605          */
606         FLING_SHRINK,
607 
608         /**
609          * This is used for the closing and opening target in the default cross-activity back
610          * animation. When the back gesture is flung, the closing and opening targets shrink a
611          * bit further and then bounce back with a spring motion.
612          */
613         FLING_BOUNCE
614     }
615 }
616 
isDarkModenull617 private fun isDarkMode(context: Context): Boolean {
618     return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
619         Configuration.UI_MODE_NIGHT_YES
620 }
621 
setInterpolatedRectFnull622 internal fun RectF.setInterpolatedRectF(start: RectF, target: RectF, progress: Float) {
623     require(!(progress < 0 || progress > 1)) { "Progress value must be between 0 and 1" }
624     left = start.left + (target.left - start.left) * progress
625     top = start.top + (target.top - start.top) * progress
626     right = start.right + (target.right - start.right) * progress
627     bottom = start.bottom + (target.bottom - start.bottom) * progress
628 }
629 
scaleCenterednull630 internal fun RectF.scaleCentered(
631     scale: Float,
632     pivotX: Float = left + width() / 2,
633     pivotY: Float = top + height() / 2
634 ) {
635     offset(-pivotX, -pivotY) // move pivot to origin
636     scale(scale)
637     offset(pivotX, pivotY) // Move back to the original position
638 }
639