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