• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.systemui.navigationbar.gestural
2 
3 import android.content.Context
4 import android.content.res.Configuration
5 import android.graphics.Canvas
6 import android.graphics.Paint
7 import android.graphics.Path
8 import android.graphics.RectF
9 import android.util.MathUtils.min
10 import android.view.View
11 import androidx.dynamicanimation.animation.FloatPropertyCompat
12 import androidx.dynamicanimation.animation.SpringAnimation
13 import androidx.dynamicanimation.animation.SpringForce
14 import com.android.internal.util.LatencyTracker
15 import com.android.systemui.navigationbar.gestural.BackPanelController.DelayedOnAnimationEndListener
16 
17 private const val TAG = "BackPanel"
18 private const val DEBUG = false
19 
20 class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : View(context) {
21 
22     var arrowsPointLeft = false
23         set(value) {
24             if (field != value) {
25                 invalidate()
26                 field = value
27             }
28         }
29 
30     // Arrow color and shape
31     private val arrowPath = Path()
32     private val arrowPaint = Paint()
33 
34     // Arrow background color and shape
35     private var arrowBackgroundRect = RectF()
36     private var arrowBackgroundPaint = Paint()
37 
38     // True if the panel is currently on the left of the screen
39     var isLeftPanel = false
40 
41     /** Used to track back arrow latency from [android.view.MotionEvent.ACTION_DOWN] to [onDraw] */
42     private var trackingBackArrowLatency = false
43 
44     /** The length of the arrow measured horizontally. Used for animating [arrowPath] */
45     private var arrowLength =
46         AnimatedFloat(
47             name = "arrowLength",
48             minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
49         )
50 
51     /**
52      * The height of the arrow measured vertically from its center to its top (i.e. half the total
53      * height). Used for animating [arrowPath]
54      */
55     var arrowHeight =
56         AnimatedFloat(
57             name = "arrowHeight",
58             minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ROTATION_DEGREES,
59         )
60 
61     val backgroundWidth =
62         AnimatedFloat(
63             name = "backgroundWidth",
64             minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
65             minimumValue = 0f,
66         )
67 
68     val backgroundHeight =
69         AnimatedFloat(
70             name = "backgroundHeight",
71             minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
72             minimumValue = 0f,
73         )
74 
75     /**
76      * Corners of the background closer to the edge of the screen (where the arrow appeared from).
77      * Used for animating [arrowBackgroundRect]
78      */
79     val backgroundEdgeCornerRadius = AnimatedFloat("backgroundEdgeCornerRadius")
80 
81     /**
82      * Corners of the background further from the edge of the screens (toward the direction the
83      * arrow is being dragged). Used for animating [arrowBackgroundRect]
84      */
85     val backgroundFarCornerRadius = AnimatedFloat("backgroundFarCornerRadius")
86 
87     var scale =
88         AnimatedFloat(
89             name = "scale",
90             minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_SCALE,
91             minimumValue = 0f,
92         )
93 
94     val scalePivotX =
95         AnimatedFloat(
96             name = "scalePivotX",
97             minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
98             minimumValue = backgroundWidth.pos / 2,
99         )
100 
101     /**
102      * Left/right position of the background relative to the canvas. Also corresponds with the
103      * background's margin relative to the screen edge. The arrow will be centered within the
104      * background.
105      */
106     var horizontalTranslation = AnimatedFloat(name = "horizontalTranslation")
107 
108     var arrowAlpha =
109         AnimatedFloat(
110             name = "arrowAlpha",
111             minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA,
112             minimumValue = 0f,
113             maximumValue = 1f,
114         )
115 
116     val backgroundAlpha =
117         AnimatedFloat(
118             name = "backgroundAlpha",
119             minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA,
120             minimumValue = 0f,
121             maximumValue = 1f,
122         )
123 
124     private val allAnimatedFloat =
125         setOf(
126             arrowLength,
127             arrowHeight,
128             backgroundWidth,
129             backgroundEdgeCornerRadius,
130             backgroundFarCornerRadius,
131             scalePivotX,
132             scale,
133             horizontalTranslation,
134             arrowAlpha,
135             backgroundAlpha,
136         )
137 
138     /**
139      * Canvas vertical translation. How far up/down the arrow and background appear relative to the
140      * canvas.
141      */
142     var verticalTranslation = AnimatedFloat("verticalTranslation")
143 
144     /** Use for drawing debug info. Can only be set if [DEBUG]=true */
145     var drawDebugInfo: ((canvas: Canvas) -> Unit)? = null
146         set(value) {
147             if (DEBUG) field = value
148         }
149 
updateArrowPaintnull150     internal fun updateArrowPaint(arrowThickness: Float) {
151         arrowPaint.strokeWidth = arrowThickness
152 
153         val isDeviceInNightTheme =
154             resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
155                 Configuration.UI_MODE_NIGHT_YES
156 
157         arrowPaint.color =
158             context.getColor(
159                 if (isDeviceInNightTheme) {
160                     com.android.internal.R.color.materialColorOnSecondaryContainer
161                 } else {
162                     com.android.internal.R.color.materialColorOnSecondaryFixed
163                 }
164             )
165 
166         arrowBackgroundPaint.color =
167             context.getColor(
168                 if (isDeviceInNightTheme) {
169                     com.android.internal.R.color.materialColorSecondaryContainer
170                 } else {
171                     com.android.internal.R.color.materialColorSecondaryFixedDim
172                 }
173             )
174     }
175 
176     inner class AnimatedFloat(
177         name: String,
178         private val minimumVisibleChange: Float? = null,
179         private val minimumValue: Float? = null,
180         private val maximumValue: Float? = null,
181     ) {
182 
183         // The resting position when not stretched by a touch drag
184         private var restingPosition = 0f
185 
186         // The current position as updated by the SpringAnimation
187         var pos = 0f
188             private set(v) {
189                 if (field != v) {
190                     field = v
191                     invalidate()
192                 }
193             }
194 
195         private val animation: SpringAnimation
196         var spring: SpringForce
197             get() = animation.spring
198             set(value) {
199                 animation.cancel()
200                 animation.spring = value
201             }
202 
203         val isRunning: Boolean
204             get() = animation.isRunning
205 
addEndListenernull206         fun addEndListener(listener: DelayedOnAnimationEndListener) {
207             animation.addEndListener(listener)
208         }
209 
210         init {
211             val floatProp =
212                 object : FloatPropertyCompat<AnimatedFloat>(name) {
setValuenull213                     override fun setValue(animatedFloat: AnimatedFloat, value: Float) {
214                         animatedFloat.pos = value
215                     }
216 
getValuenull217                     override fun getValue(animatedFloat: AnimatedFloat): Float = animatedFloat.pos
218                 }
219             animation =
220                 SpringAnimation(this, floatProp).apply {
221                     spring = SpringForce()
222                     this@AnimatedFloat.minimumValue?.let { setMinValue(it) }
223                     this@AnimatedFloat.maximumValue?.let { setMaxValue(it) }
224                     this@AnimatedFloat.minimumVisibleChange?.let { minimumVisibleChange = it }
225                 }
226         }
227 
snapTonull228         fun snapTo(newPosition: Float) {
229             animation.cancel()
230             restingPosition = newPosition
231             animation.spring.finalPosition = newPosition
232             pos = newPosition
233         }
234 
snapToRestingPositionnull235         fun snapToRestingPosition() {
236             snapTo(restingPosition)
237         }
238 
stretchTonull239         fun stretchTo(
240             stretchAmount: Float,
241             startingVelocity: Float? = null,
242             springForce: SpringForce? = null,
243         ) {
244             animation.apply {
245                 startingVelocity?.let {
246                     cancel()
247                     setStartVelocity(it)
248                 }
249                 springForce?.let { spring = springForce }
250                 animateToFinalPosition(restingPosition + stretchAmount)
251             }
252         }
253 
254         /**
255          * Animates to a new position ([finalPosition]) that is the given fraction ([amount])
256          * between the existing [restingPosition] and the new [finalPosition].
257          *
258          * The [restingPosition] will remain unchanged. Only the animation is updated.
259          */
stretchBynull260         fun stretchBy(finalPosition: Float?, amount: Float) {
261             val stretchedAmount = amount * ((finalPosition ?: 0f) - restingPosition)
262             animation.animateToFinalPosition(restingPosition + stretchedAmount)
263         }
264 
updateRestingPositionnull265         fun updateRestingPosition(pos: Float?, animated: Boolean = true) {
266             if (pos == null) return
267 
268             restingPosition = pos
269             if (animated) {
270                 animation.animateToFinalPosition(restingPosition)
271             } else {
272                 snapTo(restingPosition)
273             }
274         }
275 
cancelnull276         fun cancel() = animation.cancel()
277     }
278 
279     init {
280         visibility = GONE
281         arrowPaint.apply {
282             style = Paint.Style.STROKE
283             strokeCap = Paint.Cap.SQUARE
284         }
285         arrowBackgroundPaint.apply {
286             style = Paint.Style.FILL
287             strokeJoin = Paint.Join.ROUND
288             strokeCap = Paint.Cap.ROUND
289         }
290     }
291 
calculateArrowPathnull292     private fun calculateArrowPath(dx: Float, dy: Float): Path {
293         arrowPath.reset()
294         arrowPath.moveTo(dx, -dy)
295         arrowPath.lineTo(0f, 0f)
296         arrowPath.lineTo(dx, dy)
297         arrowPath.moveTo(dx, -dy)
298         return arrowPath
299     }
300 
addAnimationEndListenernull301     fun addAnimationEndListener(
302         animatedFloat: AnimatedFloat,
303         endListener: DelayedOnAnimationEndListener,
304     ): Boolean {
305         return if (animatedFloat.isRunning) {
306             animatedFloat.addEndListener(endListener)
307             true
308         } else {
309             endListener.run()
310             false
311         }
312     }
313 
cancelAnimationsnull314     fun cancelAnimations() {
315         allAnimatedFloat.forEach { it.cancel() }
316     }
317 
setStretchnull318     fun setStretch(
319         horizontalTranslationStretchAmount: Float,
320         arrowStretchAmount: Float,
321         arrowAlphaStretchAmount: Float,
322         backgroundAlphaStretchAmount: Float,
323         backgroundWidthStretchAmount: Float,
324         backgroundHeightStretchAmount: Float,
325         edgeCornerStretchAmount: Float,
326         farCornerStretchAmount: Float,
327         fullyStretchedDimens: EdgePanelParams.BackIndicatorDimens,
328     ) {
329         horizontalTranslation.stretchBy(
330             finalPosition = fullyStretchedDimens.horizontalTranslation,
331             amount = horizontalTranslationStretchAmount,
332         )
333         arrowLength.stretchBy(
334             finalPosition = fullyStretchedDimens.arrowDimens.length,
335             amount = arrowStretchAmount,
336         )
337         arrowHeight.stretchBy(
338             finalPosition = fullyStretchedDimens.arrowDimens.height,
339             amount = arrowStretchAmount,
340         )
341         arrowAlpha.stretchBy(
342             finalPosition = fullyStretchedDimens.arrowDimens.alpha,
343             amount = arrowAlphaStretchAmount,
344         )
345         backgroundAlpha.stretchBy(
346             finalPosition = fullyStretchedDimens.backgroundDimens.alpha,
347             amount = backgroundAlphaStretchAmount,
348         )
349         backgroundWidth.stretchBy(
350             finalPosition = fullyStretchedDimens.backgroundDimens.width,
351             amount = backgroundWidthStretchAmount,
352         )
353         backgroundHeight.stretchBy(
354             finalPosition = fullyStretchedDimens.backgroundDimens.height,
355             amount = backgroundHeightStretchAmount,
356         )
357         backgroundEdgeCornerRadius.stretchBy(
358             finalPosition = fullyStretchedDimens.backgroundDimens.edgeCornerRadius,
359             amount = edgeCornerStretchAmount,
360         )
361         backgroundFarCornerRadius.stretchBy(
362             finalPosition = fullyStretchedDimens.backgroundDimens.farCornerRadius,
363             amount = farCornerStretchAmount,
364         )
365     }
366 
popOffEdgenull367     fun popOffEdge(startingVelocity: Float) {
368         scale.stretchTo(stretchAmount = 0f, startingVelocity = startingVelocity * -.8f)
369         horizontalTranslation.stretchTo(stretchAmount = 0f, startingVelocity * 200f)
370     }
371 
popScalenull372     fun popScale(startingVelocity: Float) {
373         scalePivotX.snapTo(backgroundWidth.pos / 2)
374         scale.stretchTo(stretchAmount = 0f, startingVelocity = startingVelocity)
375     }
376 
popArrowAlphanull377     fun popArrowAlpha(startingVelocity: Float, springForce: SpringForce? = null) {
378         arrowAlpha.stretchTo(
379             stretchAmount = 0f,
380             startingVelocity = startingVelocity,
381             springForce = springForce,
382         )
383     }
384 
resetStretchnull385     fun resetStretch() {
386         backgroundAlpha.snapTo(1f)
387         verticalTranslation.snapTo(0f)
388         scale.snapTo(1f)
389 
390         horizontalTranslation.snapToRestingPosition()
391         arrowLength.snapToRestingPosition()
392         arrowHeight.snapToRestingPosition()
393         arrowAlpha.snapToRestingPosition()
394         backgroundWidth.snapToRestingPosition()
395         backgroundHeight.snapToRestingPosition()
396         backgroundEdgeCornerRadius.snapToRestingPosition()
397         backgroundFarCornerRadius.snapToRestingPosition()
398     }
399 
400     /** Updates resting arrow and background size not accounting for stretch */
setRestingDimensnull401     internal fun setRestingDimens(
402         restingParams: EdgePanelParams.BackIndicatorDimens,
403         animate: Boolean = true,
404     ) {
405         horizontalTranslation.updateRestingPosition(restingParams.horizontalTranslation)
406         scale.updateRestingPosition(restingParams.scale)
407         backgroundAlpha.updateRestingPosition(restingParams.backgroundDimens.alpha)
408 
409         arrowAlpha.updateRestingPosition(restingParams.arrowDimens.alpha, animate)
410         arrowLength.updateRestingPosition(restingParams.arrowDimens.length, animate)
411         arrowHeight.updateRestingPosition(restingParams.arrowDimens.height, animate)
412         scalePivotX.updateRestingPosition(restingParams.scalePivotX, animate)
413         backgroundWidth.updateRestingPosition(restingParams.backgroundDimens.width, animate)
414         backgroundHeight.updateRestingPosition(restingParams.backgroundDimens.height, animate)
415         backgroundEdgeCornerRadius.updateRestingPosition(
416             restingParams.backgroundDimens.edgeCornerRadius,
417             animate,
418         )
419         backgroundFarCornerRadius.updateRestingPosition(
420             restingParams.backgroundDimens.farCornerRadius,
421             animate,
422         )
423     }
424 
animateVerticallynull425     fun animateVertically(yPos: Float) = verticalTranslation.stretchTo(yPos)
426 
427     fun setSpring(
428         horizontalTranslation: SpringForce? = null,
429         verticalTranslation: SpringForce? = null,
430         scale: SpringForce? = null,
431         arrowLength: SpringForce? = null,
432         arrowHeight: SpringForce? = null,
433         arrowAlpha: SpringForce? = null,
434         backgroundAlpha: SpringForce? = null,
435         backgroundFarCornerRadius: SpringForce? = null,
436         backgroundEdgeCornerRadius: SpringForce? = null,
437         backgroundWidth: SpringForce? = null,
438         backgroundHeight: SpringForce? = null,
439     ) {
440         arrowLength?.let { this.arrowLength.spring = it }
441         arrowHeight?.let { this.arrowHeight.spring = it }
442         arrowAlpha?.let { this.arrowAlpha.spring = it }
443         backgroundAlpha?.let { this.backgroundAlpha.spring = it }
444         backgroundFarCornerRadius?.let { this.backgroundFarCornerRadius.spring = it }
445         backgroundEdgeCornerRadius?.let { this.backgroundEdgeCornerRadius.spring = it }
446         scale?.let { this.scale.spring = it }
447         backgroundWidth?.let { this.backgroundWidth.spring = it }
448         backgroundHeight?.let { this.backgroundHeight.spring = it }
449         horizontalTranslation?.let { this.horizontalTranslation.spring = it }
450         verticalTranslation?.let { this.verticalTranslation.spring = it }
451     }
452 
hasOverlappingRenderingnull453     override fun hasOverlappingRendering() = false
454 
455     override fun onDraw(canvas: Canvas) {
456         val edgeCorner = backgroundEdgeCornerRadius.pos
457         val farCorner = backgroundFarCornerRadius.pos
458         val halfHeight = backgroundHeight.pos / 2
459         val canvasWidth = width
460         val backgroundWidth = backgroundWidth.pos
461         val scalePivotX = scalePivotX.pos
462 
463         canvas.save()
464 
465         if (!isLeftPanel) canvas.scale(-1f, 1f, canvasWidth / 2.0f, 0f)
466 
467         canvas.translate(horizontalTranslation.pos, height * 0.5f + verticalTranslation.pos)
468 
469         canvas.scale(scale.pos, scale.pos, scalePivotX, 0f)
470 
471         val arrowBackground =
472             arrowBackgroundRect
473                 .apply {
474                     left = 0f
475                     top = -halfHeight
476                     right = backgroundWidth
477                     bottom = halfHeight
478                 }
479                 .toPathWithRoundCorners(
480                     topLeft = edgeCorner,
481                     bottomLeft = edgeCorner,
482                     topRight = farCorner,
483                     bottomRight = farCorner,
484                 )
485         canvas.drawPath(
486             arrowBackground,
487             arrowBackgroundPaint.apply { alpha = (255 * backgroundAlpha.pos).toInt() },
488         )
489 
490         val dx = arrowLength.pos
491         val dy = arrowHeight.pos
492 
493         // How far the arrow bounding box should be from the edge of the screen. Measured from
494         // either the tip or the back of the arrow, whichever is closer
495         val arrowOffset = (backgroundWidth - dx) / 2
496         canvas.translate(
497             /* dx= */ arrowOffset,
498             /* dy= */ 0f, /* pass 0 for the y position since the canvas was already translated */
499         )
500 
501         val arrowPointsAwayFromEdge = !arrowsPointLeft.xor(isLeftPanel)
502         if (arrowPointsAwayFromEdge) {
503             canvas.apply {
504                 scale(-1f, 1f, 0f, 0f)
505                 translate(-dx, 0f)
506             }
507         }
508 
509         val arrowPath = calculateArrowPath(dx = dx, dy = dy)
510         val arrowPaint =
511             arrowPaint.apply { alpha = (255 * min(arrowAlpha.pos, backgroundAlpha.pos)).toInt() }
512         canvas.drawPath(arrowPath, arrowPaint)
513         canvas.restore()
514 
515         if (trackingBackArrowLatency) {
516             latencyTracker.onActionEnd(LatencyTracker.ACTION_SHOW_BACK_ARROW)
517             trackingBackArrowLatency = false
518         }
519 
520         if (DEBUG) drawDebugInfo?.invoke(canvas)
521     }
522 
startTrackingShowBackArrowLatencynull523     fun startTrackingShowBackArrowLatency() {
524         latencyTracker.onActionStart(LatencyTracker.ACTION_SHOW_BACK_ARROW)
525         trackingBackArrowLatency = true
526     }
527 
RectFnull528     private fun RectF.toPathWithRoundCorners(
529         topLeft: Float = 0f,
530         topRight: Float = 0f,
531         bottomRight: Float = 0f,
532         bottomLeft: Float = 0f,
533     ): Path =
534         Path().apply {
535             val corners =
536                 floatArrayOf(
537                     topLeft,
538                     topLeft,
539                     topRight,
540                     topRight,
541                     bottomRight,
542                     bottomRight,
543                     bottomLeft,
544                     bottomLeft,
545                 )
546             addRoundRect(this@toPathWithRoundCorners, corners, Path.Direction.CW)
547         }
548 }
549