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 151 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.materialColorOnSecondaryContainer 161 } else { 162 com.android.internal.R.attr.materialColorOnSecondaryFixed 163 } 164 ) 165 166 arrowBackgroundPaint.color = Utils.getColorAttrDefaultColor(context, 167 if (isDeviceInNightTheme) { 168 com.android.internal.R.attr.materialColorSecondaryContainer 169 } else { 170 com.android.internal.R.attr.materialColorSecondaryFixedDim 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 205 fun addEndListener(listener: DelayedOnAnimationEndListener) { 206 animation.addEndListener(listener) 207 } 208 209 init { 210 val floatProp = object : FloatPropertyCompat<AnimatedFloat>(name) { 211 override fun setValue(animatedFloat: AnimatedFloat, value: Float) { 212 animatedFloat.pos = value 213 } 214 215 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 225 fun snapTo(newPosition: Float) { 226 animation.cancel() 227 restingPosition = newPosition 228 animation.spring.finalPosition = newPosition 229 pos = newPosition 230 } 231 232 fun snapToRestingPosition() { 233 snapTo(restingPosition) 234 } 235 236 237 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 */ 258 fun stretchBy(finalPosition: Float?, amount: Float) { 259 val stretchedAmount = amount * ((finalPosition ?: 0f) - restingPosition) 260 animation.animateToFinalPosition(restingPosition + stretchedAmount) 261 } 262 263 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 274 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 290 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 299 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 312 fun cancelAnimations() { 313 allAnimatedFloat.forEach { it.cancel() } 314 } 315 316 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 365 fun popOffEdge(startingVelocity: Float) { 366 scale.stretchTo(stretchAmount = 0f, startingVelocity = startingVelocity * -.8f) 367 horizontalTranslation.stretchTo(stretchAmount = 0f, startingVelocity * 200f) 368 } 369 370 fun popScale(startingVelocity: Float) { 371 scalePivotX.snapTo(backgroundWidth.pos / 2) 372 scale.stretchTo(stretchAmount = 0f, startingVelocity = startingVelocity) 373 } 374 375 fun popArrowAlpha(startingVelocity: Float, springForce: SpringForce? = null) { 376 arrowAlpha.stretchTo(stretchAmount = 0f, startingVelocity = startingVelocity, 377 springForce = springForce) 378 } 379 380 fun resetStretch() { 381 backgroundAlpha.snapTo(1f) 382 verticalTranslation.snapTo(0f) 383 scale.snapTo(1f) 384 385 horizontalTranslation.snapToRestingPosition() 386 arrowLength.snapToRestingPosition() 387 arrowHeight.snapToRestingPosition() 388 arrowAlpha.snapToRestingPosition() 389 backgroundWidth.snapToRestingPosition() 390 backgroundHeight.snapToRestingPosition() 391 backgroundEdgeCornerRadius.snapToRestingPosition() 392 backgroundFarCornerRadius.snapToRestingPosition() 393 } 394 395 /** 396 * Updates resting arrow and background size not accounting for stretch 397 */ 398 internal fun setRestingDimens( 399 restingParams: EdgePanelParams.BackIndicatorDimens, 400 animate: Boolean = true 401 ) { 402 horizontalTranslation.updateRestingPosition(restingParams.horizontalTranslation) 403 scale.updateRestingPosition(restingParams.scale) 404 backgroundAlpha.updateRestingPosition(restingParams.backgroundDimens.alpha) 405 406 arrowAlpha.updateRestingPosition(restingParams.arrowDimens.alpha, animate) 407 arrowLength.updateRestingPosition(restingParams.arrowDimens.length, animate) 408 arrowHeight.updateRestingPosition(restingParams.arrowDimens.height, animate) 409 scalePivotX.updateRestingPosition(restingParams.scalePivotX, animate) 410 backgroundWidth.updateRestingPosition(restingParams.backgroundDimens.width, animate) 411 backgroundHeight.updateRestingPosition(restingParams.backgroundDimens.height, animate) 412 backgroundEdgeCornerRadius.updateRestingPosition( 413 restingParams.backgroundDimens.edgeCornerRadius, animate 414 ) 415 backgroundFarCornerRadius.updateRestingPosition( 416 restingParams.backgroundDimens.farCornerRadius, animate 417 ) 418 } 419 420 fun animateVertically(yPos: Float) = verticalTranslation.stretchTo(yPos) 421 422 fun setSpring( 423 horizontalTranslation: SpringForce? = null, 424 verticalTranslation: SpringForce? = null, 425 scale: SpringForce? = null, 426 arrowLength: SpringForce? = null, 427 arrowHeight: SpringForce? = null, 428 arrowAlpha: SpringForce? = null, 429 backgroundAlpha: SpringForce? = null, 430 backgroundFarCornerRadius: SpringForce? = null, 431 backgroundEdgeCornerRadius: SpringForce? = null, 432 backgroundWidth: SpringForce? = null, 433 backgroundHeight: SpringForce? = null, 434 ) { 435 arrowLength?.let { this.arrowLength.spring = it } 436 arrowHeight?.let { this.arrowHeight.spring = it } 437 arrowAlpha?.let { this.arrowAlpha.spring = it } 438 backgroundAlpha?.let { this.backgroundAlpha.spring = it } 439 backgroundFarCornerRadius?.let { this.backgroundFarCornerRadius.spring = it } 440 backgroundEdgeCornerRadius?.let { this.backgroundEdgeCornerRadius.spring = it } 441 scale?.let { this.scale.spring = it } 442 backgroundWidth?.let { this.backgroundWidth.spring = it } 443 backgroundHeight?.let { this.backgroundHeight.spring = it } 444 horizontalTranslation?.let { this.horizontalTranslation.spring = it } 445 verticalTranslation?.let { this.verticalTranslation.spring = it } 446 } 447 448 override fun hasOverlappingRendering() = false 449 450 override fun onDraw(canvas: Canvas) { 451 val edgeCorner = backgroundEdgeCornerRadius.pos 452 val farCorner = backgroundFarCornerRadius.pos 453 val halfHeight = backgroundHeight.pos / 2 454 val canvasWidth = width 455 val backgroundWidth = backgroundWidth.pos 456 val scalePivotX = scalePivotX.pos 457 458 canvas.save() 459 460 if (!isLeftPanel) canvas.scale(-1f, 1f, canvasWidth / 2.0f, 0f) 461 462 canvas.translate( 463 horizontalTranslation.pos, 464 height * 0.5f + verticalTranslation.pos 465 ) 466 467 canvas.scale(scale.pos, scale.pos, scalePivotX, 0f) 468 469 val arrowBackground = arrowBackgroundRect.apply { 470 left = 0f 471 top = -halfHeight 472 right = backgroundWidth 473 bottom = halfHeight 474 }.toPathWithRoundCorners( 475 topLeft = edgeCorner, 476 bottomLeft = edgeCorner, 477 topRight = farCorner, 478 bottomRight = farCorner 479 ) 480 canvas.drawPath(arrowBackground, 481 arrowBackgroundPaint.apply { alpha = (255 * backgroundAlpha.pos).toInt() }) 482 483 val dx = arrowLength.pos 484 val dy = arrowHeight.pos 485 486 // How far the arrow bounding box should be from the edge of the screen. Measured from 487 // either the tip or the back of the arrow, whichever is closer 488 val arrowOffset = (backgroundWidth - dx) / 2 489 canvas.translate( 490 /* dx= */ arrowOffset, 491 /* dy= */ 0f /* pass 0 for the y position since the canvas was already translated */ 492 ) 493 494 val arrowPointsAwayFromEdge = !arrowsPointLeft.xor(isLeftPanel) 495 if (arrowPointsAwayFromEdge) { 496 canvas.apply { 497 scale(-1f, 1f, 0f, 0f) 498 translate(-dx, 0f) 499 } 500 } 501 502 val arrowPath = calculateArrowPath(dx = dx, dy = dy) 503 val arrowPaint = arrowPaint 504 .apply { alpha = (255 * min(arrowAlpha.pos, backgroundAlpha.pos)).toInt() } 505 canvas.drawPath(arrowPath, arrowPaint) 506 canvas.restore() 507 508 if (trackingBackArrowLatency) { 509 latencyTracker.onActionEnd(LatencyTracker.ACTION_SHOW_BACK_ARROW) 510 trackingBackArrowLatency = false 511 } 512 513 if (DEBUG) drawDebugInfo?.invoke(canvas) 514 } 515 516 fun startTrackingShowBackArrowLatency() { 517 latencyTracker.onActionStart(LatencyTracker.ACTION_SHOW_BACK_ARROW) 518 trackingBackArrowLatency = true 519 } 520 521 private fun RectF.toPathWithRoundCorners( 522 topLeft: Float = 0f, 523 topRight: Float = 0f, 524 bottomRight: Float = 0f, 525 bottomLeft: Float = 0f 526 ): Path = Path().apply { 527 val corners = floatArrayOf( 528 topLeft, topLeft, 529 topRight, topRight, 530 bottomRight, bottomRight, 531 bottomLeft, bottomLeft 532 ) 533 addRoundRect(this@toPathWithRoundCorners, corners, Path.Direction.CW) 534 } 535 }