blob: 2822435639524837344985835e3a7698626e51ee [file] [log] [blame]
package com.android.systemui.navigationbar.gestural
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.view.View
import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
import com.android.internal.util.LatencyTracker
import com.android.settingslib.Utils
import com.android.systemui.navigationbar.gestural.BackPanelController.DelayedOnAnimationEndListener
private const val TAG = "BackPanel"
private const val DEBUG = false
class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : View(context) {
var arrowsPointLeft = false
set(value) {
if (field != value) {
invalidate()
field = value
}
}
// Arrow color and shape
private val arrowPath = Path()
private val arrowPaint = Paint()
// Arrow background color and shape
private var arrowBackgroundRect = RectF()
private var arrowBackgroundPaint = Paint()
// True if the panel is currently on the left of the screen
var isLeftPanel = false
/**
* Used to track back arrow latency from [android.view.MotionEvent.ACTION_DOWN] to [onDraw]
*/
private var trackingBackArrowLatency = false
/**
* The length of the arrow measured horizontally. Used for animating [arrowPath]
*/
private var arrowLength = AnimatedFloat("arrowLength", SpringForce())
/**
* The height of the arrow measured vertically from its center to its top (i.e. half the total
* height). Used for animating [arrowPath]
*/
private var arrowHeight = AnimatedFloat("arrowHeight", SpringForce())
private val backgroundWidth = AnimatedFloat(
name = "backgroundWidth",
SpringForce().apply {
stiffness = 600f
dampingRatio = 0.65f
}
)
private val backgroundHeight = AnimatedFloat(
name = "backgroundHeight",
SpringForce().apply {
stiffness = 600f
dampingRatio = 0.65f
}
)
/**
* Corners of the background closer to the edge of the screen (where the arrow appeared from).
* Used for animating [arrowBackgroundRect]
*/
private val backgroundEdgeCornerRadius = AnimatedFloat(
name = "backgroundEdgeCornerRadius",
SpringForce().apply {
stiffness = 400f
dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
}
)
/**
* Corners of the background further from the edge of the screens (toward the direction the
* arrow is being dragged). Used for animating [arrowBackgroundRect]
*/
private val backgroundFarCornerRadius = AnimatedFloat(
name = "backgroundDragCornerRadius",
SpringForce().apply {
stiffness = 2200f
dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
}
)
/**
* Left/right position of the background relative to the canvas. Also corresponds with the
* background's margin relative to the screen edge. The arrow will be centered within the
* background.
*/
private var horizontalTranslation = AnimatedFloat("horizontalTranslation", SpringForce())
private val currentAlpha: FloatPropertyCompat<BackPanel> =
object : FloatPropertyCompat<BackPanel>("currentAlpha") {
override fun setValue(panel: BackPanel, value: Float) {
panel.alpha = value
}
override fun getValue(panel: BackPanel): Float = panel.alpha
}
private val alphaAnimation = SpringAnimation(this, currentAlpha)
.setSpring(
SpringForce()
.setStiffness(60f)
.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
)
/**
* Canvas vertical translation. How far up/down the arrow and background appear relative to the
* canvas.
*/
private var verticalTranslation: AnimatedFloat = AnimatedFloat(
name = "verticalTranslation",
SpringForce().apply {
stiffness = SpringForce.STIFFNESS_MEDIUM
}
)
/**
* Use for drawing debug info. Can only be set if [DEBUG]=true
*/
var drawDebugInfo: ((canvas: Canvas) -> Unit)? = null
set(value) {
if (DEBUG) field = value
}
internal fun updateArrowPaint(arrowThickness: Float) {
// Arrow constants
arrowPaint.strokeWidth = arrowThickness
arrowPaint.color =
Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary)
arrowBackgroundPaint.color = Utils.getColorAccentDefaultColor(context)
}
private inner class AnimatedFloat(name: String, springForce: SpringForce) {
// The resting position when not stretched by a touch drag
private var restingPosition = 0f
// The current position as updated by the SpringAnimation
var pos = 0f
set(v) {
if (field != v) {
field = v
invalidate()
}
}
val animation: SpringAnimation
init {
val floatProp = object : FloatPropertyCompat<AnimatedFloat>(name) {
override fun setValue(animatedFloat: AnimatedFloat, value: Float) {
animatedFloat.pos = value
}
override fun getValue(animatedFloat: AnimatedFloat): Float = animatedFloat.pos
}
animation = SpringAnimation(this, floatProp)
animation.spring = springForce
}
fun snapTo(newPosition: Float) {
animation.cancel()
restingPosition = newPosition
animation.spring.finalPosition = newPosition
pos = newPosition
}
fun stretchTo(stretchAmount: Float) {
animation.animateToFinalPosition(restingPosition + stretchAmount)
}
/**
* Animates to a new position ([finalPosition]) that is the given fraction ([amount])
* between the existing [restingPosition] and the new [finalPosition].
*
* The [restingPosition] will remain unchanged. Only the animation is updated.
*/
fun stretchBy(finalPosition: Float, amount: Float) {
val stretchedAmount = amount * (finalPosition - restingPosition)
animation.animateToFinalPosition(restingPosition + stretchedAmount)
}
fun updateRestingPosition(pos: Float, animated: Boolean) {
restingPosition = pos
if (animated)
animation.animateToFinalPosition(restingPosition)
else
snapTo(restingPosition)
}
}
init {
visibility = GONE
arrowPaint.apply {
style = Paint.Style.STROKE
strokeCap = Paint.Cap.SQUARE
}
arrowBackgroundPaint.apply {
style = Paint.Style.FILL
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
}
}
private fun calculateArrowPath(dx: Float, dy: Float): Path {
arrowPath.reset()
arrowPath.moveTo(dx, -dy)
arrowPath.lineTo(0f, 0f)
arrowPath.lineTo(dx, dy)
arrowPath.moveTo(dx, -dy)
return arrowPath
}
fun addEndListener(endListener: DelayedOnAnimationEndListener): Boolean {
return if (alphaAnimation.isRunning) {
alphaAnimation.addEndListener(endListener)
true
} else if (horizontalTranslation.animation.isRunning) {
horizontalTranslation.animation.addEndListener(endListener)
true
} else {
endListener.runNow()
false
}
}
fun setStretch(
horizontalTranslationStretchAmount: Float,
arrowStretchAmount: Float,
backgroundWidthStretchAmount: Float,
fullyStretchedDimens: EdgePanelParams.BackIndicatorDimens
) {
horizontalTranslation.stretchBy(
finalPosition = fullyStretchedDimens.horizontalTranslation,
amount = horizontalTranslationStretchAmount
)
arrowLength.stretchBy(
finalPosition = fullyStretchedDimens.arrowDimens.length,
amount = arrowStretchAmount
)
arrowHeight.stretchBy(
finalPosition = fullyStretchedDimens.arrowDimens.height,
amount = arrowStretchAmount
)
backgroundWidth.stretchBy(
finalPosition = fullyStretchedDimens.backgroundDimens.width,
amount = backgroundWidthStretchAmount
)
}
fun resetStretch() {
horizontalTranslation.stretchTo(0f)
arrowLength.stretchTo(0f)
arrowHeight.stretchTo(0f)
backgroundWidth.stretchTo(0f)
backgroundHeight.stretchTo(0f)
backgroundEdgeCornerRadius.stretchTo(0f)
backgroundFarCornerRadius.stretchTo(0f)
}
/**
* Updates resting arrow and background size not accounting for stretch
*/
internal fun setRestingDimens(
restingParams: EdgePanelParams.BackIndicatorDimens,
animate: Boolean
) {
horizontalTranslation.updateRestingPosition(restingParams.horizontalTranslation, animate)
arrowLength.updateRestingPosition(restingParams.arrowDimens.length, animate)
arrowHeight.updateRestingPosition(restingParams.arrowDimens.height, animate)
backgroundWidth.updateRestingPosition(restingParams.backgroundDimens.width, animate)
backgroundHeight.updateRestingPosition(restingParams.backgroundDimens.height, animate)
backgroundEdgeCornerRadius.updateRestingPosition(
restingParams.backgroundDimens.edgeCornerRadius,
animate
)
backgroundFarCornerRadius.updateRestingPosition(
restingParams.backgroundDimens.farCornerRadius,
animate
)
}
fun animateVertically(yPos: Float) = verticalTranslation.stretchTo(yPos)
fun setArrowStiffness(arrowStiffness: Float, arrowDampingRatio: Float) {
arrowLength.animation.spring.apply {
stiffness = arrowStiffness
dampingRatio = arrowDampingRatio
}
arrowHeight.animation.spring.apply {
stiffness = arrowStiffness
dampingRatio = arrowDampingRatio
}
}
override fun hasOverlappingRendering() = false
override fun onDraw(canvas: Canvas) {
var edgeCorner = backgroundEdgeCornerRadius.pos
val farCorner = backgroundFarCornerRadius.pos
val halfHeight = backgroundHeight.pos / 2
canvas.save()
if (!isLeftPanel) canvas.scale(-1f, 1f, width / 2.0f, 0f)
canvas.translate(
horizontalTranslation.pos,
height * 0.5f + verticalTranslation.pos
)
val arrowBackground = arrowBackgroundRect.apply {
left = 0f
top = -halfHeight
right = backgroundWidth.pos
bottom = halfHeight
}.toPathWithRoundCorners(
topLeft = edgeCorner,
bottomLeft = edgeCorner,
topRight = farCorner,
bottomRight = farCorner
)
canvas.drawPath(arrowBackground, arrowBackgroundPaint)
val dx = arrowLength.pos
val dy = arrowHeight.pos
// How far the arrow bounding box should be from the edge of the screen. Measured from
// either the tip or the back of the arrow, whichever is closer
var arrowOffset = (backgroundWidth.pos - dx) / 2
canvas.translate(
/* dx= */ arrowOffset,
/* dy= */ 0f /* pass 0 for the y position since the canvas was already translated */
)
val arrowPointsAwayFromEdge = !arrowsPointLeft.xor(isLeftPanel)
if (arrowPointsAwayFromEdge) {
canvas.apply {
scale(-1f, 1f, 0f, 0f)
translate(-dx, 0f)
}
}
val arrowPath = calculateArrowPath(dx = dx, dy = dy)
canvas.drawPath(arrowPath, arrowPaint)
canvas.restore()
if (trackingBackArrowLatency) {
latencyTracker.onActionEnd(LatencyTracker.ACTION_SHOW_BACK_ARROW)
trackingBackArrowLatency = false
}
if (DEBUG) drawDebugInfo?.invoke(canvas)
}
fun startTrackingShowBackArrowLatency() {
latencyTracker.onActionStart(LatencyTracker.ACTION_SHOW_BACK_ARROW)
trackingBackArrowLatency = true
}
private fun RectF.toPathWithRoundCorners(
topLeft: Float = 0f,
topRight: Float = 0f,
bottomRight: Float = 0f,
bottomLeft: Float = 0f
): Path = Path().apply {
val corners = floatArrayOf(
topLeft, topLeft,
topRight, topRight,
bottomRight, bottomRight,
bottomLeft, bottomLeft
)
addRoundRect(this@toPathWithRoundCorners, corners, Path.Direction.CW)
}
fun cancelAlphaAnimations() {
alphaAnimation.cancel()
alpha = 1f
}
fun fadeOut() {
alphaAnimation.animateToFinalPosition(0f)
}
}