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