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
    }
}

主要修改点和解释:

  1. Paint 和 Path 初始化:

    • 移除了 arrowBackgroundPaint,新增了 wavePaintbezierPath,用于绘制波浪形的背景。
    • wavePaintarrowPaint 的初始化代码移动到 init 代码块中,包括抗锯齿、颜色、线宽、样式等。
    • 线宽使用了 context.resources.displayMetrics.density 进行 dp 到像素的转换,以保证在不同分辨率设备上显示效果一致。
    • arrowPaint 的颜色被固定为白色 (Color.WHITE)。
    • wavePaint 的颜色会根据系统是否为夜间模式动态设置,使用了 Utils.getColorAttrDefaultColor 来获取主题颜色,类似于您 Java 代码中的 mProtectionColor 的设置方式。可以根据实际效果调整使用的 Material Design 颜色属性。
  2. updateArrowPaint 方法修改:

    • updateArrowPaint 方法中,arrowPaint 的颜色被设置为白色,与 init 代码块保持一致。
    • wavePaint 的颜色设置逻辑保留在 updateArrowPaint 中,确保在主题切换时颜色能够正确更新。
  3. AnimatedFloat 类:

    • AnimatedFloat 内部类保持不变,因为动画逻辑仍然可以使用。
  4. onDraw 方法深度修改:

    • 整体结构: onDraw 方法完全重写,移除了原有的基于 arrowPatharrowBackgroundRect 的绘制逻辑。
    • 进度计算: 引入了 currentWidth (使用 backgroundWidth.pos 替代) 和 maxWidth (使用 width.toFloat(),可以根据需要定义一个最大宽度) 来计算手势滑动的进度 progress
    • 波浪形背景绘制:
      • 使用 bezierPathwavePaint 绘制贝塞尔曲线。
      • 贝塞尔曲线的控制点和锚点坐标quarterHeight, threeEighthsHeight
      • wavePaint.alpha 根据 progress 动态调整透明度,实现波浪形背景的渐显效果。
    • 箭头绘制:
      • 使用 canvas.drawLines 绘制箭头,代替了原有的 calculateArrowPathcanvas.drawPath
      • 箭头的起始点、终点坐标以及长度、透明度都根据 progressarrowLength.pos 动态计算和调整。
      • 箭头颜色固定为白色,透明度根据 progress 调整。
  5. 变量重命名和调整:

    • backgroundWidth 重命名为 currentWidth,更清晰地表达其表示当前手势滑动宽度的含义。但在代码中为了兼容性,仍然使用了 backgroundWidth 这个变量名,只是注释中说明了它现在代表 currentWidth
    • backgroundAlpha 重命名为 waveAlpha,更清晰地表达其控制的是波浪形背景的透明度。同样为了兼容性,变量名仍然保留 backgroundAlpha,注释中说明了它现在代表 waveAlpha
    • 保留了 arrowHeight, backgroundHeight, backgroundEdgeCornerRadius, backgroundFarCornerRadius 等变量,即使在新的波浪形 UI 中没有直接使用,也保留了下来,以防未来需要扩展或回退到之前的 UI 风格。
  6. setStretchsetRestingDimens 方法:

    • setStretchsetRestingDimens 方法中,仍然使用了原有的参数和动画属性,只是在 onDraw 方法中,这些属性被用于控制新的波浪形 UI 和箭头。
    • 例如,backgroundWidth (实际控制 currentWidth) 仍然通过 fullyStretchedDimens.backgroundDimens.width 来设置目标值。
    • backgroundAlpha (实际控制 waveAlpha) 仍然通过 fullyStretchedDimens.backgroundDimens.alpha 来设置目标值。
    • arrowLengtharrowAlpha 等属性也继续用于控制箭头的长度和透明度动画。

使用方法和进一步调整:

  1. 调整参数:
    • 颜色: 调整 wavePaint 的颜色。您可以尝试使用不同的 Material Design 颜色属性,或者调整颜色的深浅。
    • 线宽: 调整 arrowPaintwavePaintstrokeWidth,修改箭头和波浪形背景的粗细。
    • 贝塞尔曲线控制点: 修改波浪形可以尝试调整 bezierPath.cubicTo 方法中的控制点坐标 (例如,quarterHeight, threeEighthsHeight 等常量的值),修改曲线的弯曲程度。
    • 箭头长度: 调整 arrowLength 的动画属性和 onDraw 方法中箭头长度的计算方式,修改箭头的长短。
    • 最大宽度 maxWidth: 如果您希望波浪形背景在滑动到一定程度后不再继续扩展宽度,可以定义一个 maxWidth 常量,并在 onDraw 方法中限制 currentWidth 的最大值。
    • 透明度: 调整 wavePaint.alphaarrowPaint.alpha 的计算公式,修改波浪形背景和箭头的透明度变化范围和速度。

95~我想带你去海边