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 }