blob: 56ad19ae89ca60302fc6d67b6123e9cc281cfc2f [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 backgroundDragCornerRadius = 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())
/**
* Canvas vertical translation. How far up/down the arrow and background appear relative to the
* canvas.
*/
private var verticalTranslation: AnimatedFloat =
AnimatedFloat("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)
}
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 (horizontalTranslation.animation.isRunning) {
horizontalTranslation.animation.addEndListener(endListener)
true
} else {
endListener.runNow()
false
}
}
fun setStretch(
arrowLengthStretch: Float,
arrowHeightStretch: Float,
backgroundWidthStretch: Float,
backgroundHeightStretch: Float,
backgroundEdgeCornerRadiusStretch: Float,
backgroundDragCornerRadiusStretch: Float,
horizontalTranslationStretch: Float
) {
arrowLength.stretchTo(arrowLengthStretch)
arrowHeight.stretchTo(arrowHeightStretch)
backgroundWidth.stretchTo(backgroundWidthStretch)
backgroundHeight.stretchTo(backgroundHeightStretch)
backgroundEdgeCornerRadius.stretchTo(backgroundEdgeCornerRadiusStretch)
backgroundDragCornerRadius.stretchTo(backgroundDragCornerRadiusStretch)
horizontalTranslation.stretchTo(horizontalTranslationStretch)
}
fun resetStretch() {
setStretch(0f, 0f, 0f, 0f, 0f, 0f, 0f)
}
/**
* Updates resting arrow and background size not accounting for stretch
*/
internal fun updateRestingArrowDimens(
backgroundWidth: Float,
backgroundHeight: Float,
backgroundEdgeCornerRadius: Float,
backgroundDragCornerRadius: Float,
arrowLength: Float,
arrowHeight: Float,
horizontalTranslation: Float,
animate: Boolean
) {
this.arrowLength.updateRestingPosition(arrowLength, animate)
this.arrowHeight.updateRestingPosition(arrowHeight, animate)
this.backgroundWidth.updateRestingPosition(backgroundWidth, animate)
this.backgroundHeight.updateRestingPosition(backgroundHeight, animate)
this.backgroundEdgeCornerRadius.updateRestingPosition(backgroundEdgeCornerRadius, animate)
this.backgroundDragCornerRadius.updateRestingPosition(backgroundDragCornerRadius, animate)
this.horizontalTranslation.updateRestingPosition(horizontalTranslation, 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 = backgroundDragCornerRadius.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)
}
}