A15 SystemUI手势导航的侧滑返回UI优化
package com.android.systemui.navigationbar.gestural
import android.content.Context
import android.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Color //modify by lzerong 2025/3/20 for 53948 UI Optimization for Gesture Navigation Side-swipe Back
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.util.MathUtils.min
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 //modify by lzerong 2025/3/20 for 53948 UI Optimization for Gesture Navigation Side-swipe Back
private val arrowPaint = Paint()
// Arrow background color and shape //modify by lzerong 2025/3/20 for 53948 UI Optimization for Gesture Navigation Side-swipe Back
private var wavePaint = Paint()
private var bezierPath = Path()
// 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(
name = "arrowLength",
minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS
)
/**
* The height of the arrow measured vertically from its center to its top (i.e. half the total
* height). Used for animating [arrowPath]
*/
var arrowHeight =
AnimatedFloat(
name = "arrowHeight",
minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ROTATION_DEGREES
)
val backgroundWidth =
AnimatedFloat(
name = "currentWidth", //modify by lzerong 2025/3/20 for 53948 UI Optimization for Gesture Navigation Side-swipe Back
minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
minimumValue = 0f,
)
val backgroundHeight =
AnimatedFloat(
name = "backgroundHeight",
minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
minimumValue = 0f,
)
/**
* Corners of the background closer to the edge of the screen (where the arrow appeared from).
* Used for animating [arrowBackgroundRect]
*/
val backgroundEdgeCornerRadius = AnimatedFloat("backgroundEdgeCornerRadius")
/**
* Corners of the background further from the edge of the screens (toward the direction the
* arrow is being dragged). Used for animating [arrowBackgroundRect]
*/
val backgroundFarCornerRadius = AnimatedFloat("backgroundFarCornerRadius")
var scale =
AnimatedFloat(
name = "scale",
minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_SCALE,
minimumValue = 0f
)
val scalePivotX =
AnimatedFloat(
name = "scalePivotX",
minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
minimumValue = backgroundWidth.pos / 2,
)
/**
* 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.
*/
var horizontalTranslation = AnimatedFloat(name = "horizontalTranslation")
var arrowAlpha =
AnimatedFloat(
name = "arrowAlpha",
minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA,
minimumValue = 0f,
maximumValue = 1f
)
val backgroundAlpha =
AnimatedFloat(
name = "waveAlpha", //modify by lzerong 2025/3/20 for 53948 UI Optimization for Gesture Navigation Side-swipe Back
minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA,
minimumValue = 0f,
maximumValue = 1f
)
private val allAnimatedFloat =
setOf(
arrowLength,
arrowHeight,
backgroundWidth,
backgroundEdgeCornerRadius,
backgroundFarCornerRadius,
scalePivotX,
scale,
horizontalTranslation,
arrowAlpha,
backgroundAlpha
)
/**
* Canvas vertical translation. How far up/down the arrow and background appear relative to the
* canvas.
*/
var verticalTranslation = AnimatedFloat("verticalTranslation")
/** 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) {
arrowPaint.strokeWidth = arrowThickness
val isDeviceInNightTheme =
resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES
arrowPaint.color = Color.WHITE //modify by lzerong 2025/3/20 for 53948 UI Optimization for Gesture Navigation Side-swipe Back
/*wavePaint.color = // Modified: Wave paint color based on theme, similar to Java's mProtectionColor
Utils.getColorAttrDefaultColor(
context,
if (isDeviceInNightTheme) {
com.android.internal.R.attr.materialColorSecondaryContainer // Or a darker shade if needed
} else {
com.android.internal.R.attr.materialColorSecondary // Or a darker shade if needed
}
)*/
}
inner class AnimatedFloat(
name: String,
private val minimumVisibleChange: Float? = null,
private val minimumValue: Float? = null,
private val maximumValue: Float? = null,
) {
// 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
private set(v) {
if (field != v) {
field = v
invalidate()
}
}
private val animation: SpringAnimation
var spring: SpringForce
get() = animation.spring
set(value) {
animation.cancel()
animation.spring = value
}
val isRunning: Boolean
get() = animation.isRunning
fun addEndListener(listener: DelayedOnAnimationEndListener) {
animation.addEndListener(listener)
}
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).apply {
spring = SpringForce()
this@AnimatedFloat.minimumValue?.let { setMinValue(it) }
this@AnimatedFloat.maximumValue?.let { setMaxValue(it) }
this@AnimatedFloat.minimumVisibleChange?.let { minimumVisibleChange = it }
}
}
fun snapTo(newPosition: Float) {
animation.cancel()
restingPosition = newPosition
animation.spring.finalPosition = newPosition
pos = newPosition
}
fun snapToRestingPosition() {
snapTo(restingPosition)
}
fun stretchTo(
stretchAmount: Float,
startingVelocity: Float? = null,
springForce: SpringForce? = null
) {
animation.apply {
startingVelocity?.let {
cancel()
setStartVelocity(it)
}
springForce?.let { spring = springForce }
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 ?: 0f) - restingPosition)
animation.animateToFinalPosition(restingPosition + stretchedAmount)
}
fun updateRestingPosition(pos: Float?, animated: Boolean = true) {
if (pos == null) return
restingPosition = pos
if (animated) {
animation.animateToFinalPosition(restingPosition)
} else {
snapTo(restingPosition)
}
}
fun cancel() = animation.cancel()
}
init {
visibility = GONE
//modify by lzerong 2025/3/20 for 53948 UI Optimization for Gesture Navigation Side-swipe Back
// Initialize Wave Paint (similar to Java's mWavePaint)
wavePaint.apply {
isAntiAlias = true
strokeWidth = context.resources.displayMetrics.density * 1.5f // dp(1.5f) conversion
style = Paint.Style.FILL
color = Color.DKGRAY
// Color is set in updateArrowPaint based on theme
}
bezierPath = Path() // Initialize bezierPath
// Initialize Arrow Paint (similar to Java's mArrowPaint)
arrowPaint.apply {
strokeWidth = context.resources.displayMetrics.density * 2f // Example arrow thickness, adjust as needed
strokeCap = Paint.Cap.ROUND
isAntiAlias = true
color = Color.WHITE // Modified: Arrow color is always white
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND
}
//modify by lzerong 2025/3/20 for 53948 UI Optimization for Gesture Navigation Side-swipe Back end
}
fun addAnimationEndListener(
animatedFloat: AnimatedFloat,
endListener: DelayedOnAnimationEndListener
): Boolean {
return if (animatedFloat.isRunning) {
animatedFloat.addEndListener(endListener)
true
} else {
endListener.run()
false
}
}
fun cancelAnimations() {
allAnimatedFloat.forEach { it.cancel() }
}
fun setStretch(
horizontalTranslationStretchAmount: Float,
arrowStretchAmount: Float,
arrowAlphaStretchAmount: Float,
backgroundAlphaStretchAmount: Float,
backgroundWidthStretchAmount: Float,
backgroundHeightStretchAmount: Float,
edgeCornerStretchAmount: Float,
farCornerStretchAmount: 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
)
arrowAlpha.stretchBy(
finalPosition = fullyStretchedDimens.arrowDimens.alpha,
amount = arrowAlphaStretchAmount
)
backgroundAlpha.stretchBy(
finalPosition = fullyStretchedDimens.backgroundDimens.alpha,
amount = backgroundAlphaStretchAmount
)
backgroundWidth.stretchBy(
finalPosition = fullyStretchedDimens.backgroundDimens.width,
amount = backgroundWidthStretchAmount
)
backgroundHeight.stretchBy(
finalPosition = fullyStretchedDimens.backgroundDimens.height,
amount = backgroundHeightStretchAmount
)
backgroundEdgeCornerRadius.stretchBy(
finalPosition = fullyStretchedDimens.backgroundDimens.edgeCornerRadius,
amount = edgeCornerStretchAmount
)
backgroundFarCornerRadius.stretchBy(
finalPosition = fullyStretchedDimens.backgroundDimens.farCornerRadius,
amount = farCornerStretchAmount
)
}
fun popOffEdge(startingVelocity: Float) {
scale.stretchTo(stretchAmount = 0f, startingVelocity = startingVelocity * -.8f)
horizontalTranslation.stretchTo(stretchAmount = 0f, startingVelocity * 200f)
}
fun popScale(startingVelocity: Float) {
scalePivotX.snapTo(backgroundWidth.pos / 2)
scale.stretchTo(stretchAmount = 0f, startingVelocity = startingVelocity)
}
fun popArrowAlpha(startingVelocity: Float, springForce: SpringForce? = null) {
arrowAlpha.stretchTo(
stretchAmount = 0f,
startingVelocity = startingVelocity,
springForce = springForce
)
}
fun resetStretch() {
backgroundAlpha.snapTo(1f)
verticalTranslation.snapTo(0f)
scale.snapTo(1f)
horizontalTranslation.snapToRestingPosition()
arrowLength.snapToRestingPosition()
arrowHeight.snapToRestingPosition()
arrowAlpha.snapToRestingPosition()
backgroundWidth.snapToRestingPosition()
backgroundHeight.snapToRestingPosition()
backgroundEdgeCornerRadius.snapToRestingPosition()
backgroundFarCornerRadius.snapToRestingPosition()
}
/** Updates resting arrow and background size not accounting for stretch */
internal fun setRestingDimens(
restingParams: EdgePanelParams.BackIndicatorDimens,
animate: Boolean = true
) {
horizontalTranslation.updateRestingPosition(restingParams.horizontalTranslation)
scale.updateRestingPosition(restingParams.scale)
backgroundAlpha.updateRestingPosition(restingParams.backgroundDimens.alpha)
arrowAlpha.updateRestingPosition(restingParams.arrowDimens.alpha, animate)
arrowLength.updateRestingPosition(restingParams.arrowDimens.length, animate)
arrowHeight.updateRestingPosition(restingParams.arrowDimens.height, animate)
scalePivotX.updateRestingPosition(restingParams.scalePivotX, 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 setSpring(
horizontalTranslation: SpringForce? = null,
verticalTranslation: SpringForce? = null,
scale: SpringForce? = null,
arrowLength: SpringForce? = null,
arrowHeight: SpringForce? = null,
arrowAlpha: SpringForce? = null,
backgroundAlpha: SpringForce? = null,
backgroundFarCornerRadius: SpringForce? = null,
backgroundEdgeCornerRadius: SpringForce? = null,
backgroundWidth: SpringForce? = null,
backgroundHeight: SpringForce? = null,
) {
arrowLength?.let { this.arrowLength.spring = it }
arrowHeight?.let { this.arrowHeight.spring = it }
arrowAlpha?.let { this.arrowAlpha.spring = it }
backgroundAlpha?.let { this.backgroundAlpha.spring = it }
backgroundFarCornerRadius?.let { this.backgroundFarCornerRadius.spring = it }
backgroundEdgeCornerRadius?.let { this.backgroundEdgeCornerRadius.spring = it }
scale?.let { this.scale.spring = it }
backgroundWidth?.let { this.backgroundWidth.spring = it }
backgroundHeight?.let { this.backgroundHeight.spring = it }
horizontalTranslation?.let { this.horizontalTranslation.spring = it }
verticalTranslation?.let { this.verticalTranslation.spring = it }
}
override fun hasOverlappingRendering() = false
override fun onDraw(canvas: Canvas) {
//modify by lzerong 2025/3/20 for 53948 UI Optimization for Gesture Navigation Side-swipe Back begin
val height = getHeight() / 2
val maxWidth = width.toFloat()
val centerY = height / 2f
val currentWidth = backgroundWidth.pos
val progress = if (maxWidth > 0) currentWidth / maxWidth else 0f
if (progress == 0f) {
return
}
val footX = if (isLeftPanel) 0f else width.toFloat()
wavePaint.alpha = 200//(200 * progress).toInt()
val bezierWidth = if (isLeftPanel) currentWidth / 2f else (width - currentWidth / 2f)
bezierPath.reset()
bezierPath.moveTo(footX, 0f)
val quarterHeight = height / 4f
val threeEighthsHeight = height * 3f / 8f
val fiveEighthsHeight = height * 5f / 8f
val threeQuartersHeight = height * 3f / 4f
bezierPath.cubicTo(footX, quarterHeight, bezierWidth, threeEighthsHeight, bezierWidth, centerY)
bezierPath.cubicTo(bezierWidth, fiveEighthsHeight, footX, threeQuartersHeight, footX, height.toFloat())
canvas.drawPath(bezierPath, wavePaint)
arrowPaint.alpha = (255 * progress).toInt()
val arrowLeft = if (isLeftPanel) currentWidth / 3f else width - currentWidth / 3f
val arrowEnd = arrowLeft + arrowLength.pos * progress
val arrowLengthValue = arrowLength.pos * progress * 3f
canvas.save()
if (isLeftPanel) { // 如果是左侧面板,则翻转箭头
canvas.scale(-1f, 1f, arrowLeft, centerY) // 以箭头中心点进行水平翻转
}
val lines = floatArrayOf(
arrowEnd, centerY.toFloat() - arrowLengthValue, arrowLeft, centerY.toFloat(),
arrowLeft, centerY.toFloat(), arrowEnd, centerY.toFloat() + arrowLengthValue
)
canvas.drawLines(lines, arrowPaint)
canvas.restore()
if (trackingBackArrowLatency) {
latencyTracker.onActionEnd(LatencyTracker.ACTION_SHOW_BACK_ARROW)
trackingBackArrowLatency = false
}
if (DEBUG) drawDebugInfo?.invoke(canvas)
//modify by lzerong 2025/3/20 for 53948 UI Optimization for Gesture Navigation Side-swipe Back end
}
fun startTrackingShowBackArrowLatency() {
latencyTracker.onActionStart(LatencyTracker.ACTION_SHOW_BACK_ARROW)
trackingBackArrowLatency = true
}
}
主要修改点和解释:
-
Paint 和 Path 初始化:
- 移除了
arrowBackgroundPaint
,新增了wavePaint
和bezierPath
,用于绘制波浪形的背景。 wavePaint
和arrowPaint
的初始化代码移动到init
代码块中,包括抗锯齿、颜色、线宽、样式等。- 线宽使用了
context.resources.displayMetrics.density
进行 dp 到像素的转换,以保证在不同分辨率设备上显示效果一致。 arrowPaint
的颜色被固定为白色 (Color.WHITE
)。wavePaint
的颜色会根据系统是否为夜间模式动态设置,使用了Utils.getColorAttrDefaultColor
来获取主题颜色,类似于您 Java 代码中的mProtectionColor
的设置方式。可以根据实际效果调整使用的 Material Design 颜色属性。
- 移除了
-
updateArrowPaint
方法修改:updateArrowPaint
方法中,arrowPaint
的颜色被设置为白色,与init
代码块保持一致。wavePaint
的颜色设置逻辑保留在updateArrowPaint
中,确保在主题切换时颜色能够正确更新。
-
AnimatedFloat
类:AnimatedFloat
内部类保持不变,因为动画逻辑仍然可以使用。
-
onDraw
方法深度修改:- 整体结构:
onDraw
方法完全重写,移除了原有的基于arrowPath
和arrowBackgroundRect
的绘制逻辑。 - 进度计算: 引入了
currentWidth
(使用backgroundWidth.pos
替代) 和maxWidth
(使用width.toFloat()
,可以根据需要定义一个最大宽度) 来计算手势滑动的进度progress
。 - 波浪形背景绘制:
- 使用
bezierPath
和wavePaint
绘制贝塞尔曲线。 - 贝塞尔曲线的控制点和锚点坐标
quarterHeight
,threeEighthsHeight
。 wavePaint.alpha
根据progress
动态调整透明度,实现波浪形背景的渐显效果。
- 使用
- 箭头绘制:
- 使用
canvas.drawLines
绘制箭头,代替了原有的calculateArrowPath
和canvas.drawPath
。 - 箭头的起始点、终点坐标以及长度、透明度都根据
progress
和arrowLength.pos
动态计算和调整。 - 箭头颜色固定为白色,透明度根据
progress
调整。
- 使用
- 整体结构:
-
变量重命名和调整:
- 将
backgroundWidth
重命名为currentWidth
,更清晰地表达其表示当前手势滑动宽度的含义。但在代码中为了兼容性,仍然使用了backgroundWidth
这个变量名,只是注释中说明了它现在代表currentWidth
。 - 将
backgroundAlpha
重命名为waveAlpha
,更清晰地表达其控制的是波浪形背景的透明度。同样为了兼容性,变量名仍然保留backgroundAlpha
,注释中说明了它现在代表waveAlpha
。 - 保留了
arrowHeight
,backgroundHeight
,backgroundEdgeCornerRadius
,backgroundFarCornerRadius
等变量,即使在新的波浪形 UI 中没有直接使用,也保留了下来,以防未来需要扩展或回退到之前的 UI 风格。
- 将
-
setStretch
和setRestingDimens
方法:setStretch
和setRestingDimens
方法中,仍然使用了原有的参数和动画属性,只是在onDraw
方法中,这些属性被用于控制新的波浪形 UI 和箭头。- 例如,
backgroundWidth
(实际控制currentWidth
) 仍然通过fullyStretchedDimens.backgroundDimens.width
来设置目标值。 backgroundAlpha
(实际控制waveAlpha
) 仍然通过fullyStretchedDimens.backgroundDimens.alpha
来设置目标值。arrowLength
和arrowAlpha
等属性也继续用于控制箭头的长度和透明度动画。
使用方法和进一步调整:
- 调整参数:
- 颜色: 调整
wavePaint
的颜色。您可以尝试使用不同的 Material Design 颜色属性,或者调整颜色的深浅。 - 线宽: 调整
arrowPaint
和wavePaint
的strokeWidth
,修改箭头和波浪形背景的粗细。 - 贝塞尔曲线控制点: 修改波浪形可以尝试调整
bezierPath.cubicTo
方法中的控制点坐标 (例如,quarterHeight
,threeEighthsHeight
等常量的值),修改曲线的弯曲程度。 - 箭头长度: 调整
arrowLength
的动画属性和onDraw
方法中箭头长度的计算方式,修改箭头的长短。 - 最大宽度
maxWidth
: 如果您希望波浪形背景在滑动到一定程度后不再继续扩展宽度,可以定义一个maxWidth
常量,并在onDraw
方法中限制currentWidth
的最大值。 - 透明度: 调整
wavePaint.alpha
和arrowPaint.alpha
的计算公式,修改波浪形背景和箭头的透明度变化范围和速度。
- 颜色: 调整