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