diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 74965a557e016..d492e53c53909 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -43,12 +43,18 @@ 268dp 16dp + + 24dp 412dp 64dp 64dp + + 32dp + + 32dp 4dp @@ -56,37 +62,61 @@ 32dp + 0.98 4dp - 8dp - 60dp - 30dp - 30dp - 10dp - 7dp + 1.0 + 0dp + 48dp + 6dp + 6dp + 0.0 + 8.6dp + 5dp 4dp - 64dp - 60dp - 22dp - 26dp + 1.0 + + 0.98 + 51dp + 46dp + 16dp + 20dp + 1.0 + 8dp + 5.6dp - + + 1.0 14dp - 60dp - 60dp - 30dp - 30dp - 8dp - 9dp + 1.0 + 48dp + 48dp + 24dp + 24dp + 1.0 + 6.4dp + 7.2dp + + 0.85 + 0 + + + 0dp + + 1.0 18dp - 74dp - 60dp - 30dp - 30dp - 7dp - 10dp + 60dp + + @dimen/navigation_edge_entry_background_alpha + + 48dp + 24dp + 24dp + 1.0 + 5.6dp + 8dp 12dp 0dp diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt index 2822435639524..f335733b430c8 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt @@ -1,14 +1,20 @@ package com.android.systemui.navigationbar.gestural import android.content.Context +import android.content.res.Configuration +import android.content.res.TypedArray import android.graphics.Canvas import android.graphics.Paint import android.graphics.Path import android.graphics.RectF +import android.util.MathUtils.min +import android.util.TypedValue import android.view.View +import androidx.appcompat.view.ContextThemeWrapper import androidx.dynamicanimation.animation.FloatPropertyCompat import androidx.dynamicanimation.animation.SpringAnimation import androidx.dynamicanimation.animation.SpringForce +import com.android.internal.R.style.Theme_DeviceDefault import com.android.internal.util.LatencyTracker import com.android.settingslib.Utils import com.android.systemui.navigationbar.gestural.BackPanelController.DelayedOnAnimationEndListener @@ -16,7 +22,10 @@ import com.android.systemui.navigationbar.gestural.BackPanelController.DelayedOn private const val TAG = "BackPanel" private const val DEBUG = false -class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : View(context) { +class BackPanel( + context: Context, + private val latencyTracker: LatencyTracker +) : View(context) { var arrowsPointLeft = false set(value) { @@ -45,52 +54,54 @@ class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : /** * The length of the arrow measured horizontally. Used for animating [arrowPath] */ - private var arrowLength = AnimatedFloat("arrowLength", SpringForce()) + 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] */ - private var arrowHeight = AnimatedFloat("arrowHeight", SpringForce()) - - private val backgroundWidth = AnimatedFloat( - name = "backgroundWidth", - SpringForce().apply { - stiffness = 600f - dampingRatio = 0.65f - } + var arrowHeight = AnimatedFloat( + name = "arrowHeight", + minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ROTATION_DEGREES ) - private val backgroundHeight = AnimatedFloat( - name = "backgroundHeight", - SpringForce().apply { - stiffness = 600f - dampingRatio = 0.65f - } + val backgroundWidth = AnimatedFloat( + name = "backgroundWidth", + 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] */ - private val backgroundEdgeCornerRadius = AnimatedFloat( - name = "backgroundEdgeCornerRadius", - SpringForce().apply { - stiffness = 400f - dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY - } - ) + 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] */ - private val backgroundFarCornerRadius = AnimatedFloat( - name = "backgroundDragCornerRadius", - SpringForce().apply { - stiffness = 2200f - dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY - } + 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, ) /** @@ -98,34 +109,40 @@ class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : * background's margin relative to the screen edge. The arrow will be centered within the * background. */ - private var horizontalTranslation = AnimatedFloat("horizontalTranslation", SpringForce()) + var horizontalTranslation = AnimatedFloat(name = "horizontalTranslation") - private val currentAlpha: FloatPropertyCompat = - object : FloatPropertyCompat("currentAlpha") { - override fun setValue(panel: BackPanel, value: Float) { - panel.alpha = value - } + var arrowAlpha = AnimatedFloat( + name = "arrowAlpha", + minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA, + minimumValue = 0f, + maximumValue = 1f + ) - override fun getValue(panel: BackPanel): Float = panel.alpha - } + val backgroundAlpha = AnimatedFloat( + name = "backgroundAlpha", + minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA, + minimumValue = 0f, + maximumValue = 1f + ) - private val alphaAnimation = SpringAnimation(this, currentAlpha) - .setSpring( - SpringForce() - .setStiffness(60f) - .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY) - ) + 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. */ - private var verticalTranslation: AnimatedFloat = AnimatedFloat( - name = "verticalTranslation", - SpringForce().apply { - stiffness = SpringForce.STIFFNESS_MEDIUM - } - ) + var verticalTranslation = AnimatedFloat("verticalTranslation") /** * Use for drawing debug info. Can only be set if [DEBUG]=true @@ -136,28 +153,67 @@ class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : } 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) + val isDeviceInNightTheme = resources.configuration.uiMode and + Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + + val colorControlActivated = ContextThemeWrapper(context, Theme_DeviceDefault) + .run { + val typedValue = TypedValue() + val a: TypedArray = obtainStyledAttributes(typedValue.data, + intArrayOf(android.R.attr.colorControlActivated)) + val color = a.getColor(0, 0) + a.recycle() + color + } + + val colorPrimary = + Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary) + + arrowPaint.color = Utils.getColorAccentDefaultColor(context) + + arrowBackgroundPaint.color = if (isDeviceInNightTheme) { + colorPrimary + } else { + colorControlActivated + } } - private inner class AnimatedFloat(name: String, springForce: SpringForce) { + 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 - set(v) { + private set(v) { if (field != v) { field = v invalidate() } } - val animation: SpringAnimation + 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(name) { @@ -167,8 +223,12 @@ class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : override fun getValue(animatedFloat: AnimatedFloat): Float = animatedFloat.pos } - animation = SpringAnimation(this, floatProp) - animation.spring = springForce + 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) { @@ -178,8 +238,24 @@ class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : pos = newPosition } - fun stretchTo(stretchAmount: Float) { - animation.animateToFinalPosition(restingPosition + stretchAmount) + 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) + } } /** @@ -188,18 +264,23 @@ class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : * * The [restingPosition] will remain unchanged. Only the animation is updated. */ - fun stretchBy(finalPosition: Float, amount: Float) { - val stretchedAmount = amount * (finalPosition - restingPosition) + fun stretchBy(finalPosition: Float?, amount: Float) { + val stretchedAmount = amount * ((finalPosition ?: 0f) - restingPosition) animation.animateToFinalPosition(restingPosition + stretchedAmount) } - fun updateRestingPosition(pos: Float, animated: Boolean) { + fun updateRestingPosition(pos: Float?, animated: Boolean = true) { + if (pos == null) return + restingPosition = pos - if (animated) + if (animated) { animation.animateToFinalPosition(restingPosition) - else + } else { snapTo(restingPosition) + } } + + fun cancel() = animation.cancel() } init { @@ -224,126 +305,203 @@ class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : return arrowPath } - fun addEndListener(endListener: DelayedOnAnimationEndListener): Boolean { - return if (alphaAnimation.isRunning) { - alphaAnimation.addEndListener(endListener) - true - } else if (horizontalTranslation.animation.isRunning) { - horizontalTranslation.animation.addEndListener(endListener) + fun addAnimationEndListener( + animatedFloat: AnimatedFloat, + endListener: DelayedOnAnimationEndListener + ): Boolean { + return if (animatedFloat.isRunning) { + animatedFloat.addEndListener(endListener) true } else { - endListener.runNow() + endListener.run() false } } + fun cancelAnimations() { + allAnimatedFloat.forEach { it.cancel() } + } + fun setStretch( - horizontalTranslationStretchAmount: Float, - arrowStretchAmount: Float, - backgroundWidthStretchAmount: Float, - fullyStretchedDimens: EdgePanelParams.BackIndicatorDimens + 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 + finalPosition = fullyStretchedDimens.horizontalTranslation, + amount = horizontalTranslationStretchAmount ) arrowLength.stretchBy( - finalPosition = fullyStretchedDimens.arrowDimens.length, - amount = arrowStretchAmount + finalPosition = fullyStretchedDimens.arrowDimens.length, + amount = arrowStretchAmount ) arrowHeight.stretchBy( - finalPosition = fullyStretchedDimens.arrowDimens.height, - amount = arrowStretchAmount + 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 + 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) { + val heightStretchAmount = startingVelocity * 50 + val widthStretchAmount = startingVelocity * 150 + val scaleStretchAmount = startingVelocity * 0.8f + backgroundHeight.stretchTo(stretchAmount = 0f, startingVelocity = -heightStretchAmount) + backgroundWidth.stretchTo(stretchAmount = 0f, startingVelocity = widthStretchAmount) + scale.stretchTo(stretchAmount = 0f, startingVelocity = -scaleStretchAmount) + } + + 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() { - horizontalTranslation.stretchTo(0f) - arrowLength.stretchTo(0f) - arrowHeight.stretchTo(0f) - backgroundWidth.stretchTo(0f) - backgroundHeight.stretchTo(0f) - backgroundEdgeCornerRadius.stretchTo(0f) - backgroundFarCornerRadius.stretchTo(0f) + 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 + restingParams: EdgePanelParams.BackIndicatorDimens, + animate: Boolean = true ) { - horizontalTranslation.updateRestingPosition(restingParams.horizontalTranslation, animate) + horizontalTranslation.updateRestingPosition(restingParams.horizontalTranslation) + scale.updateRestingPosition(restingParams.scale) + arrowAlpha.updateRestingPosition(restingParams.arrowDimens.alpha) + backgroundAlpha.updateRestingPosition(restingParams.backgroundDimens.alpha) + arrowLength.updateRestingPosition(restingParams.arrowDimens.length, animate) arrowHeight.updateRestingPosition(restingParams.arrowDimens.height, animate) + scalePivotX.updateRestingPosition(restingParams.backgroundDimens.width, animate) backgroundWidth.updateRestingPosition(restingParams.backgroundDimens.width, animate) backgroundHeight.updateRestingPosition(restingParams.backgroundDimens.height, animate) backgroundEdgeCornerRadius.updateRestingPosition( - restingParams.backgroundDimens.edgeCornerRadius, - animate + restingParams.backgroundDimens.edgeCornerRadius, animate ) backgroundFarCornerRadius.updateRestingPosition( - restingParams.backgroundDimens.farCornerRadius, - animate + 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 - } + 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) { - var edgeCorner = backgroundEdgeCornerRadius.pos + val edgeCorner = backgroundEdgeCornerRadius.pos val farCorner = backgroundFarCornerRadius.pos val halfHeight = backgroundHeight.pos / 2 + val canvasWidth = width + val backgroundWidth = backgroundWidth.pos + val scalePivotX = scalePivotX.pos canvas.save() - if (!isLeftPanel) canvas.scale(-1f, 1f, width / 2.0f, 0f) + if (!isLeftPanel) canvas.scale(-1f, 1f, canvasWidth / 2.0f, 0f) canvas.translate( - horizontalTranslation.pos, - height * 0.5f + verticalTranslation.pos + horizontalTranslation.pos, + height * 0.5f + verticalTranslation.pos ) + canvas.scale(scale.pos, scale.pos, scalePivotX, 0f) + val arrowBackground = arrowBackgroundRect.apply { left = 0f top = -halfHeight - right = backgroundWidth.pos + right = backgroundWidth bottom = halfHeight }.toPathWithRoundCorners( - topLeft = edgeCorner, - bottomLeft = edgeCorner, - topRight = farCorner, - bottomRight = farCorner + topLeft = edgeCorner, + bottomLeft = edgeCorner, + topRight = farCorner, + bottomRight = farCorner ) - canvas.drawPath(arrowBackground, arrowBackgroundPaint) + canvas.drawPath(arrowBackground, + arrowBackgroundPaint.apply { alpha = (255 * backgroundAlpha.pos).toInt() }) 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 + val arrowOffset = (backgroundWidth - dx) / 2 canvas.translate( - /* dx= */ arrowOffset, - /* dy= */ 0f /* pass 0 for the y position since the canvas was already translated */ + /* dx= */ arrowOffset, + /* dy= */ 0f /* pass 0 for the y position since the canvas was already translated */ ) val arrowPointsAwayFromEdge = !arrowsPointLeft.xor(isLeftPanel) @@ -355,6 +513,8 @@ class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : } val arrowPath = calculateArrowPath(dx = dx, dy = dy) + val arrowPaint = arrowPaint + .apply { alpha = (255 * min(arrowAlpha.pos, backgroundAlpha.pos)).toInt() } canvas.drawPath(arrowPath, arrowPaint) canvas.restore() @@ -372,26 +532,17 @@ class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : } private fun RectF.toPathWithRoundCorners( - topLeft: Float = 0f, - topRight: Float = 0f, - bottomRight: Float = 0f, - bottomLeft: Float = 0f + 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 + 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) - } -} +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt index 6e927b071727e..ca1a49a6751ec 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt @@ -24,18 +24,15 @@ import android.os.Handler import android.os.SystemClock import android.os.VibrationEffect import android.util.Log -import android.util.MathUtils.constrain -import android.util.MathUtils.saturate +import android.util.MathUtils import android.view.Gravity import android.view.MotionEvent import android.view.VelocityTracker -import android.view.View import android.view.ViewConfiguration import android.view.WindowManager -import android.view.animation.DecelerateInterpolator -import android.view.animation.PathInterpolator +import androidx.core.os.postDelayed +import androidx.core.view.isVisible import androidx.dynamicanimation.animation.DynamicAnimation -import androidx.dynamicanimation.animation.SpringForce import com.android.internal.util.LatencyTracker import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.NavigationEdgeBackPlugin @@ -50,58 +47,42 @@ import kotlin.math.min import kotlin.math.sign private const val TAG = "BackPanelController" -private const val DEBUG = false - private const val ENABLE_FAILSAFE = true -private const val FAILSAFE_DELAY_MS: Long = 350 +private const val PX_PER_SEC = 1000 +private const val PX_PER_MS = 1 -/** - * The time required between the arrow-appears vibration effect and the back-committed vibration - * effect. If the arrow is flung quickly, the phone only vibrates once. However, if the arrow is - * held on the screen for a long time, it will vibrate a second time when the back gesture is - * committed. - */ -private const val GESTURE_DURATION_FOR_CLICK_MS = 400 +private const val MIN_DURATION_CANCELLED_ANIMATION = 200L +private const val MIN_DURATION_COMMITTED_ANIMATION = 200L +private const val MIN_DURATION_ACTIVE_ANIMATION = 300L +private const val MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION = 50L +private const val MIN_DURATION_CONSIDERED_AS_FLING = 100L -/** - * The min duration arrow remains on screen during a fling event. - */ -private const val FLING_MIN_APPEARANCE_DURATION = 235L +private const val FAILSAFE_DELAY_MS = 350L +private const val POP_ON_FLING_DELAY = 160L -/** - * The min duration arrow remains on screen during a fling event. - */ -private const val MIN_FLING_VELOCITY = 3000 +private val VIBRATE_ACTIVATED_EFFECT = + VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK) -/** - * The amount of rubber banding we do for the vertical translation - */ -private const val RUBBER_BAND_AMOUNT = 15 +private val VIBRATE_DEACTIVATED_EFFECT = + VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK) -private const val ARROW_APPEAR_STIFFNESS = 600f -private const val ARROW_APPEAR_DAMPING_RATIO = 0.4f -private const val ARROW_DISAPPEAR_STIFFNESS = 1200f -private const val ARROW_DISAPPEAR_DAMPING_RATIO = SpringForce.DAMPING_RATIO_NO_BOUNCY - -/** - * The interpolator used to rubber band - */ -private val RUBBER_BAND_INTERPOLATOR = PathInterpolator(1.0f / 5.0f, 1.0f, 1.0f, 1.0f) - -private val DECELERATE_INTERPOLATOR = DecelerateInterpolator() - -private val DECELERATE_INTERPOLATOR_SLOW = DecelerateInterpolator(0.7f) +private const val DEBUG = false class BackPanelController private constructor( - context: Context, - private val windowManager: WindowManager, - private val viewConfiguration: ViewConfiguration, - @Main private val mainHandler: Handler, - private val vibratorHelper: VibratorHelper, - private val configurationController: ConfigurationController, - latencyTracker: LatencyTracker -) : ViewController(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin { + context: Context, + private val windowManager: WindowManager, + private val viewConfiguration: ViewConfiguration, + @Main private val mainHandler: Handler, + private val vibratorHelper: VibratorHelper, + private val configurationController: ConfigurationController, + private val latencyTracker: LatencyTracker +) : ViewController( + BackPanel( + context, + latencyTracker + ) +), NavigationEdgeBackPlugin { /** * Injectable instance to create a new BackPanelController. @@ -110,23 +91,23 @@ class BackPanelController private constructor( * BackPanelController, and we need to match EdgeBackGestureHandler's context. */ class Factory @Inject constructor( - private val windowManager: WindowManager, - private val viewConfiguration: ViewConfiguration, - @Main private val mainHandler: Handler, - private val vibratorHelper: VibratorHelper, - private val configurationController: ConfigurationController, - private val latencyTracker: LatencyTracker + private val windowManager: WindowManager, + private val viewConfiguration: ViewConfiguration, + @Main private val mainHandler: Handler, + private val vibratorHelper: VibratorHelper, + private val configurationController: ConfigurationController, + private val latencyTracker: LatencyTracker ) { /** Construct a [BackPanelController]. */ fun create(context: Context): BackPanelController { val backPanelController = BackPanelController( - context, - windowManager, - viewConfiguration, - mainHandler, - vibratorHelper, - configurationController, - latencyTracker + context, + windowManager, + viewConfiguration, + mainHandler, + vibratorHelper, + configurationController, + latencyTracker ) backPanelController.init() return backPanelController @@ -137,17 +118,15 @@ class BackPanelController private constructor( private var currentState: GestureState = GestureState.GONE private var previousState: GestureState = GestureState.GONE - // Phone should only vibrate the first time the arrow is activated - private var hasHapticPlayed = false - // Screen attributes private lateinit var layoutParams: WindowManager.LayoutParams private val displaySize = Point() private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback - + private var previousXTranslationOnActiveOffset = 0f private var previousXTranslation = 0f private var totalTouchDelta = 0f + private var touchDeltaStartX = 0f private var velocityTracker: VelocityTracker? = null set(value) { if (field != value) field?.recycle() @@ -161,8 +140,18 @@ class BackPanelController private constructor( // The x,y position of the first touch event private var startX = 0f private var startY = 0f + private var startIsLeft: Boolean? = null - private var gestureStartTime = 0L + private var gestureSinceActionDown = 0L + private var gestureEntryTime = 0L + private var gestureActiveTime = 0L + private var gestureInactiveOrEntryTime = 0L + private var gestureArrowStrokeVisibleTime = 0L + + private val elapsedTimeSinceActionDown + get() = SystemClock.uptimeMillis() - gestureSinceActionDown + private val elapsedTimeSinceEntry + get() = SystemClock.uptimeMillis() - gestureEntryTime // Whether the current gesture has moved a sufficiently large amount, // so that we can unambiguously start showing the ENTRY animation @@ -191,17 +180,6 @@ class BackPanelController private constructor( /* back action currently cancelling, arrow soon to be GONE */ CANCELLED; - - /** - * @return true if the current state responds to touch move events in some way (e.g. by - * stretching the back indicator) - */ - fun isInteractive(): Boolean { - return when (this) { - ENTRY, ACTIVE, INACTIVE -> true - GONE, FLUNG, COMMITTED, CANCELLED -> false - } - } } /** @@ -209,50 +187,43 @@ class BackPanelController private constructor( * runnable is not called if the animation is cancelled */ inner class DelayedOnAnimationEndListener internal constructor( - private val handler: Handler, - private val runnable: Runnable, - private val minDuration: Long + private val handler: Handler, + private val runnableDelay: Long, + val runnable: Runnable, ) : DynamicAnimation.OnAnimationEndListener { + override fun onAnimationEnd( - animation: DynamicAnimation<*>, - canceled: Boolean, - value: Float, - velocity: Float + animation: DynamicAnimation<*>, + canceled: Boolean, + value: Float, + velocity: Float ) { animation.removeEndListener(this) + if (!canceled) { - // Total elapsed time of the gesture and the animation - val totalElapsedTime = SystemClock.uptimeMillis() - gestureStartTime + // The delay between finishing this animation and starting the runnable - val delay = max(0, minDuration - totalElapsedTime) + val delay = max(0, runnableDelay - elapsedTimeSinceEntry) + handler.postDelayed(runnable, delay) } } - internal fun runNow() { - runnable.run() - } + internal fun run() = runnable.run() } - private val setCommittedEndListener = - DelayedOnAnimationEndListener( - mainHandler, - { updateArrowState(GestureState.COMMITTED) }, - minDuration = FLING_MIN_APPEARANCE_DURATION - ) + private val onEndSetCommittedStateListener = DelayedOnAnimationEndListener(mainHandler, 0L) { + updateArrowState(GestureState.COMMITTED) + } - private val setGoneEndListener = - DelayedOnAnimationEndListener( - mainHandler, - { + + private val onEndSetGoneStateListener = + DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) { cancelFailsafe() updateArrowState(GestureState.GONE) - }, - minDuration = 0 - ) + } - // Vibration - private var vibrationTime: Long = 0 + private val playAnimationThenSetGoneOnAlphaEnd = Runnable { playAnimationThenSetGoneEnd() } // Minimum of the screen's width or the predefined threshold private var fullyStretchedThreshold = 0f @@ -279,7 +250,7 @@ class BackPanelController private constructor( updateConfiguration() updateArrowDirection(configurationController.isLayoutRtl) updateArrowState(GestureState.GONE, force = true) - updateRestingArrowDimens(animated = false, currentState) + updateRestingArrowDimens() configurationController.addCallback(configurationListener) } @@ -296,22 +267,57 @@ class BackPanelController private constructor( velocityTracker!!.addMovement(event) when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { - resetOnDown() + gestureSinceActionDown = SystemClock.uptimeMillis() + cancelAllPendingAnimations() startX = event.x startY = event.y - gestureStartTime = SystemClock.uptimeMillis() + + updateArrowState(GestureState.GONE) + updateYStartPosition(startY) + + // reset animation properties + startIsLeft = mView.isLeftPanel + hasPassedDragSlop = false + mView.resetStretch() } MotionEvent.ACTION_MOVE -> { - // only go to the ENTRY state after some minimum motion has occurred if (dragSlopExceeded(event.x, startX)) { handleMoveEvent(event) } } MotionEvent.ACTION_UP -> { - if (currentState == GestureState.ACTIVE) { - updateArrowState(if (isFlung()) GestureState.FLUNG else GestureState.COMMITTED) - } else if (currentState != GestureState.GONE) { // if invisible, skip animation - updateArrowState(GestureState.CANCELLED) + when (currentState) { + GestureState.ENTRY -> { + if (isFlungAwayFromEdge(endX = event.x)) { + updateArrowState(GestureState.ACTIVE) + updateArrowState(GestureState.FLUNG) + } else { + updateArrowState(GestureState.CANCELLED) + } + } + GestureState.INACTIVE -> { + if (isFlungAwayFromEdge(endX = event.x)) { + mainHandler.postDelayed(MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION) { + updateArrowState(GestureState.ACTIVE) + updateArrowState(GestureState.FLUNG) + } + } else { + updateArrowState(GestureState.CANCELLED) + } + } + GestureState.ACTIVE -> { + if (elapsedTimeSinceEntry < MIN_DURATION_CONSIDERED_AS_FLING) { + updateArrowState(GestureState.FLUNG) + } else { + updateArrowState(GestureState.COMMITTED) + } + } + GestureState.GONE, + GestureState.FLUNG, + GestureState.COMMITTED, + GestureState.CANCELLED -> { + updateArrowState(GestureState.CANCELLED) + } } velocityTracker = null } @@ -325,6 +331,14 @@ class BackPanelController private constructor( } } + private fun cancelAllPendingAnimations() { + cancelFailsafe() + mView.cancelAnimations() + mainHandler.removeCallbacks(onEndSetCommittedStateListener.runnable) + mainHandler.removeCallbacks(onEndSetGoneStateListener.runnable) + mainHandler.removeCallbacks(playAnimationThenSetGoneOnAlphaEnd) + } + /** * Returns false until the current gesture exceeds the touch slop threshold, * and returns true thereafter (we reset on the subsequent back gesture). @@ -335,7 +349,7 @@ class BackPanelController private constructor( private fun dragSlopExceeded(curX: Float, startX: Float): Boolean { if (hasPassedDragSlop) return true - if (abs(curX - startX) > viewConfiguration.scaledTouchSlop) { + if (abs(curX - startX) > viewConfiguration.scaledEdgeSlop) { // Reset the arrow to the side updateArrowState(GestureState.ENTRY) @@ -348,39 +362,46 @@ class BackPanelController private constructor( } private fun updateArrowStateOnMove(yTranslation: Float, xTranslation: Float) { - if (!currentState.isInteractive()) - return + + val isWithinYActivationThreshold = xTranslation * 2 >= yTranslation when (currentState) { - // Check if we should transition from ENTRY to ACTIVE - GestureState.ENTRY -> - if (xTranslation > params.swipeTriggerThreshold) { + GestureState.ENTRY -> { + if (xTranslation > params.staticTriggerThreshold) { updateArrowState(GestureState.ACTIVE) } + } + GestureState.ACTIVE -> { + val isPastDynamicDeactivationThreshold = + totalTouchDelta <= params.deactivationSwipeTriggerThreshold + val isMinDurationElapsed = + elapsedTimeSinceActionDown > MIN_DURATION_ACTIVE_ANIMATION - // Abort if we had continuous motion toward the edge for a while, OR the direction - // in Y is bigger than X * 2 - GestureState.ACTIVE -> - if ((totalTouchDelta < 0 && -totalTouchDelta > params.minDeltaForSwitch) || - (yTranslation > xTranslation * 2) + if (isMinDurationElapsed && (!isWithinYActivationThreshold || + isPastDynamicDeactivationThreshold) ) { updateArrowState(GestureState.INACTIVE) } + } + GestureState.INACTIVE -> { + val isPastStaticThreshold = + xTranslation > params.staticTriggerThreshold + val isPastDynamicReactivationThreshold = totalTouchDelta > 0 && + abs(totalTouchDelta) >= + params.reactivationTriggerThreshold - // Re-activate if we had continuous motion away from the edge for a while - GestureState.INACTIVE -> - if (totalTouchDelta > 0 && totalTouchDelta > params.minDeltaForSwitch) { + if (isPastStaticThreshold && + isPastDynamicReactivationThreshold && + isWithinYActivationThreshold + ) { updateArrowState(GestureState.ACTIVE) } - - // By default assume the current direction is kept + } else -> {} } } private fun handleMoveEvent(event: MotionEvent) { - if (!currentState.isInteractive()) - return val x = event.x val y = event.y @@ -400,23 +421,44 @@ class BackPanelController private constructor( previousXTranslation = xTranslation if (abs(xDelta) > 0) { - if (sign(xDelta) == sign(totalTouchDelta)) { + val range = + params.run { deactivationSwipeTriggerThreshold..reactivationTriggerThreshold } + val isTouchInContinuousDirection = + sign(xDelta) == sign(totalTouchDelta) || totalTouchDelta in range + + if (isTouchInContinuousDirection) { // Direction has NOT changed, so keep counting the delta totalTouchDelta += xDelta } else { // Direction has changed, so reset the delta totalTouchDelta = xDelta + touchDeltaStartX = x } } updateArrowStateOnMove(yTranslation, xTranslation) + when (currentState) { - GestureState.ACTIVE -> - stretchActiveBackIndicator(fullScreenStretchProgress(xTranslation)) - GestureState.ENTRY -> - stretchEntryBackIndicator(preThresholdStretchProgress(xTranslation)) - GestureState.INACTIVE -> - mView.resetStretch() + GestureState.ACTIVE -> { + stretchActiveBackIndicator(fullScreenProgress(xTranslation)) + } + GestureState.ENTRY -> { + val progress = staticThresholdProgress(xTranslation) + stretchEntryBackIndicator(progress) + + params.arrowStrokeAlphaSpring.get(progress).takeIf { it.isNewState }?.let { + mView.popArrowAlpha(0f, it.value) + } + } + GestureState.INACTIVE -> { + val progress = reactivationThresholdProgress(totalTouchDelta) + stretchInactiveBackIndicator(progress) + + params.arrowStrokeAlphaSpring.get(progress).takeIf { it.isNewState }?.let { + gestureArrowStrokeVisibleTime = SystemClock.uptimeMillis() + mView.popArrowAlpha(0f, it.value) + } + } else -> {} } @@ -427,21 +469,22 @@ class BackPanelController private constructor( private fun setVerticalTranslation(yOffset: Float) { val yTranslation = abs(yOffset) val maxYOffset = (mView.height - params.entryIndicator.backgroundDimens.height) / 2f - val yProgress = saturate(yTranslation / (maxYOffset * RUBBER_BAND_AMOUNT)) - mView.animateVertically( - RUBBER_BAND_INTERPOLATOR.getInterpolation(yProgress) * maxYOffset * + val rubberbandAmount = 15f + val yProgress = MathUtils.saturate(yTranslation / (maxYOffset * rubberbandAmount)) + val yPosition = params.translationInterpolator.getInterpolation(yProgress) * + maxYOffset * sign(yOffset) - ) + mView.animateVertically(yPosition) } /** - * @return the relative position of the drag from the time after the arrow is activated until + * Tracks the relative position of the drag from the time after the arrow is activated until * the arrow is fully stretched (between 0.0 - 1.0f) */ - private fun fullScreenStretchProgress(xTranslation: Float): Float { - return saturate( - (xTranslation - params.swipeTriggerThreshold) / - (fullyStretchedThreshold - params.swipeTriggerThreshold) + private fun fullScreenProgress(xTranslation: Float): Float { + return MathUtils.saturate( + (xTranslation - previousXTranslationOnActiveOffset) / + (fullyStretchedThreshold - previousXTranslationOnActiveOffset) ) } @@ -449,26 +492,74 @@ class BackPanelController private constructor( * Tracks the relative position of the drag from the entry until the threshold where the arrow * activates (between 0.0 - 1.0f) */ - private fun preThresholdStretchProgress(xTranslation: Float): Float { - return saturate(xTranslation / params.swipeTriggerThreshold) + private fun staticThresholdProgress(xTranslation: Float): Float { + return MathUtils.saturate(xTranslation / params.staticTriggerThreshold) + } + + private fun reactivationThresholdProgress(totalTouchDelta: Float): Float { + return MathUtils.saturate(totalTouchDelta / params.reactivationTriggerThreshold) } private fun stretchActiveBackIndicator(progress: Float) { - val rubberBandIterpolation = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress) mView.setStretch( - horizontalTranslationStretchAmount = rubberBandIterpolation, - arrowStretchAmount = rubberBandIterpolation, - backgroundWidthStretchAmount = DECELERATE_INTERPOLATOR_SLOW.getInterpolation(progress), - params.fullyStretchedIndicator + horizontalTranslationStretchAmount = params.translationInterpolator + .getInterpolation(progress), + arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress), + backgroundWidthStretchAmount = params.activeWidthInterpolator + .getInterpolation(progress), + backgroundAlphaStretchAmount = 1f, + backgroundHeightStretchAmount = 1f, + arrowAlphaStretchAmount = 1f, + edgeCornerStretchAmount = 1f, + farCornerStretchAmount = 1f, + fullyStretchedDimens = params.fullyStretchedIndicator ) } private fun stretchEntryBackIndicator(progress: Float) { mView.setStretch( - horizontalTranslationStretchAmount = 0f, - arrowStretchAmount = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress), - backgroundWidthStretchAmount = DECELERATE_INTERPOLATOR.getInterpolation(progress), - params.preThresholdIndicator + horizontalTranslationStretchAmount = 0f, + arrowStretchAmount = params.arrowAngleInterpolator + .getInterpolation(progress), + backgroundWidthStretchAmount = params.entryWidthInterpolator + .getInterpolation(progress), + backgroundHeightStretchAmount = params.heightInterpolator + .getInterpolation(progress), + backgroundAlphaStretchAmount = 1f, + arrowAlphaStretchAmount = params.arrowStrokeAlphaInterpolator.get(progress).value, + edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress), + farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress), + fullyStretchedDimens = params.preThresholdIndicator + ) + } + + private var previousPreThresholdWidthInterpolator = params.entryWidthTowardsEdgeInterpolator + fun preThresholdWidthStretchAmount(progress: Float): Float { + val interpolator = run { + val isPastSlop = abs(totalTouchDelta) > ViewConfiguration.get(context).scaledTouchSlop + if (isPastSlop) { + if (totalTouchDelta > 0) { + params.entryWidthInterpolator + } else params.entryWidthTowardsEdgeInterpolator + } else { + previousPreThresholdWidthInterpolator + }.also { previousPreThresholdWidthInterpolator = it } + } + return interpolator.getInterpolation(progress).coerceAtLeast(0f) + } + + private fun stretchInactiveBackIndicator(progress: Float) { + mView.setStretch( + horizontalTranslationStretchAmount = 0f, + arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress), + backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress), + backgroundHeightStretchAmount = params.heightInterpolator + .getInterpolation(progress), + backgroundAlphaStretchAmount = 1f, + arrowAlphaStretchAmount = params.arrowStrokeAlphaInterpolator.get(progress).value, + edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress), + farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress), + fullyStretchedDimens = params.preThresholdIndicator ) } @@ -486,8 +577,7 @@ class BackPanelController private constructor( } } - override fun setInsets(insetLeft: Int, insetRight: Int) { - } + override fun setInsets(insetLeft: Int, insetRight: Int) = Unit override fun setBackCallback(callback: NavigationEdgeBackPlugin.BackCallback) { backCallback = callback @@ -498,62 +588,54 @@ class BackPanelController private constructor( windowManager.addView(mView, layoutParams) } - private fun isFlung() = velocityTracker!!.run { - computeCurrentVelocity(1000) - abs(xVelocity) > MIN_FLING_VELOCITY + private fun isDragAwayFromEdge(velocityPxPerSecThreshold: Int = 0) = velocityTracker!!.run { + computeCurrentVelocity(PX_PER_SEC) + val velocity = xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1) + velocity > velocityPxPerSecThreshold } - private fun playFlingBackAnimation() { - playAnimation(setCommittedEndListener) + private fun isFlungAwayFromEdge(endX: Float, startX: Float = touchDeltaStartX): Boolean { + val minDistanceConsideredForFling = ViewConfiguration.get(context).scaledTouchSlop + val flingDistance = abs(endX - startX) + val isPastFlingVelocity = isDragAwayFromEdge( + velocityPxPerSecThreshold = + ViewConfiguration.get(context).scaledMinimumFlingVelocity) + return flingDistance > minDistanceConsideredForFling && isPastFlingVelocity } - private fun playCommitBackAnimation() { - // Check if we should vibrate again - if (previousState != GestureState.FLUNG) { - velocityTracker!!.computeCurrentVelocity(1000) - val isSlow = abs(velocityTracker!!.xVelocity) < 500 - val hasNotVibratedRecently = - SystemClock.uptimeMillis() - vibrationTime >= GESTURE_DURATION_FOR_CLICK_MS - if (isSlow || hasNotVibratedRecently) { - vibratorHelper.vibrate(VibrationEffect.EFFECT_CLICK) - } - } - // Dispatch the actual back trigger - if (DEBUG) Log.d(TAG, "playCommitBackAnimation() invoked triggerBack() on backCallback") - backCallback.triggerBack() - - playAnimation(setGoneEndListener) - } - - private fun playCancelBackAnimation() { - backCallback.cancelBack() - playAnimation(setGoneEndListener) - } - - /** - * @return true if the animation is running, false otherwise. Some transitions don't animate - */ - private fun playAnimation(endListener: DelayedOnAnimationEndListener) { - updateRestingArrowDimens(animated = true, currentState) - - if (!mView.addEndListener(endListener)) { + private fun playHorizontalAnimationThen(onEnd: DelayedOnAnimationEndListener) { + updateRestingArrowDimens() + if (!mView.addAnimationEndListener(mView.horizontalTranslation, onEnd)) { scheduleFailsafe() } } - private fun resetOnDown() { - hasPassedDragSlop = false - hasHapticPlayed = false - totalTouchDelta = 0f - vibrationTime = 0 - cancelFailsafe() + private fun playAnimationThenSetGoneEnd() { + updateRestingArrowDimens() + if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) { + scheduleFailsafe() + } } - private fun updateYPosition(touchY: Float) { + private fun playWithBackgroundWidthAnimation( + onEnd: DelayedOnAnimationEndListener, + delay: Long = 0L + ) { + if (delay == 0L) { + updateRestingArrowDimens() + if (!mView.addAnimationEndListener(mView.backgroundWidth, onEnd)) { + scheduleFailsafe() + } + } else { + mainHandler.postDelayed(delay) { playWithBackgroundWidthAnimation(onEnd, delay = 0L) } + } + } + + private fun updateYStartPosition(touchY: Float) { var yPosition = touchY - params.fingerOffset yPosition = max(yPosition, params.minArrowYPosition.toFloat()) yPosition -= layoutParams.height / 2.0f - layoutParams.y = constrain(yPosition.toInt(), 0, displaySize.y) + layoutParams.y = MathUtils.constrain(yPosition.toInt(), 0, displaySize.y) } override fun setDisplaySize(displaySize: Point) { @@ -564,53 +646,135 @@ class BackPanelController private constructor( /** * Updates resting arrow and background size not accounting for stretch */ - private fun updateRestingArrowDimens(animated: Boolean, currentState: GestureState) { - if (animated) { - when (currentState) { - GestureState.ENTRY, GestureState.ACTIVE, GestureState.FLUNG -> - mView.setArrowStiffness(ARROW_APPEAR_STIFFNESS, ARROW_APPEAR_DAMPING_RATIO) - GestureState.CANCELLED -> mView.fadeOut() - else -> - mView.setArrowStiffness( - ARROW_DISAPPEAR_STIFFNESS, - ARROW_DISAPPEAR_DAMPING_RATIO - ) + private fun updateRestingArrowDimens() { + when (currentState) { + GestureState.GONE, + GestureState.ENTRY -> { + mView.setSpring( + arrowLength = params.entryIndicator.arrowDimens.lengthSpring, + arrowHeight = params.entryIndicator.arrowDimens.heightSpring, + arrowAlpha = params.entryIndicator.arrowDimens.alphaSpring, + scale = params.entryIndicator.scaleSpring, + verticalTranslation = params.entryIndicator.verticalTranslationSpring, + horizontalTranslation = params.entryIndicator.horizontalTranslationSpring, + backgroundAlpha = params.entryIndicator.backgroundDimens.alphaSpring, + backgroundWidth = params.entryIndicator.backgroundDimens.widthSpring, + backgroundHeight = params.entryIndicator.backgroundDimens.heightSpring, + backgroundEdgeCornerRadius = params.entryIndicator.backgroundDimens + .edgeCornerRadiusSpring, + backgroundFarCornerRadius = params.entryIndicator.backgroundDimens + .farCornerRadiusSpring, + ) } + GestureState.INACTIVE -> { + mView.setSpring( + arrowLength = params.preThresholdIndicator.arrowDimens.lengthSpring, + arrowHeight = params.preThresholdIndicator.arrowDimens.heightSpring, + horizontalTranslation = params.preThresholdIndicator + .horizontalTranslationSpring, + scale = params.preThresholdIndicator.scaleSpring, + backgroundWidth = params.preThresholdIndicator.backgroundDimens + .widthSpring, + backgroundHeight = params.preThresholdIndicator.backgroundDimens + .heightSpring, + backgroundEdgeCornerRadius = params.preThresholdIndicator.backgroundDimens + .edgeCornerRadiusSpring, + backgroundFarCornerRadius = params.preThresholdIndicator.backgroundDimens + .farCornerRadiusSpring, + ) + } + GestureState.ACTIVE -> { + mView.setSpring( + arrowLength = params.activeIndicator.arrowDimens.lengthSpring, + arrowHeight = params.activeIndicator.arrowDimens.heightSpring, + scale = params.activeIndicator.scaleSpring, + horizontalTranslation = params.activeIndicator.horizontalTranslationSpring, + backgroundWidth = params.activeIndicator.backgroundDimens.widthSpring, + backgroundHeight = params.activeIndicator.backgroundDimens.heightSpring, + backgroundEdgeCornerRadius = params.activeIndicator.backgroundDimens + .edgeCornerRadiusSpring, + backgroundFarCornerRadius = params.activeIndicator.backgroundDimens + .farCornerRadiusSpring, + ) + } + GestureState.FLUNG -> { + mView.setSpring( + arrowLength = params.flungIndicator.arrowDimens.lengthSpring, + arrowHeight = params.flungIndicator.arrowDimens.heightSpring, + backgroundWidth = params.flungIndicator.backgroundDimens.widthSpring, + backgroundHeight = params.flungIndicator.backgroundDimens.heightSpring, + backgroundEdgeCornerRadius = params.flungIndicator.backgroundDimens + .edgeCornerRadiusSpring, + backgroundFarCornerRadius = params.flungIndicator.backgroundDimens + .farCornerRadiusSpring, + ) + } + GestureState.COMMITTED -> { + mView.setSpring( + arrowLength = params.committedIndicator.arrowDimens.lengthSpring, + arrowHeight = params.committedIndicator.arrowDimens.heightSpring, + scale = params.committedIndicator.scaleSpring, + backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring, + backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring, + backgroundEdgeCornerRadius = params.committedIndicator.backgroundDimens + .edgeCornerRadiusSpring, + backgroundFarCornerRadius = params.committedIndicator.backgroundDimens + .farCornerRadiusSpring, + ) + } + else -> {} } + mView.setRestingDimens( - restingParams = EdgePanelParams.BackIndicatorDimens( - horizontalTranslation = when (currentState) { - GestureState.GONE -> -params.activeIndicator.backgroundDimens.width - // Position the committed arrow slightly further off the screen so we do not - // see part of it bouncing - GestureState.COMMITTED -> - -params.activeIndicator.backgroundDimens.width * 1.5f - GestureState.FLUNG -> params.fullyStretchedIndicator.horizontalTranslation - GestureState.ACTIVE -> params.activeIndicator.horizontalTranslation - GestureState.ENTRY, GestureState.INACTIVE, GestureState.CANCELLED -> - params.entryIndicator.horizontalTranslation - }, - arrowDimens = when (currentState) { - GestureState.ACTIVE, GestureState.INACTIVE, - GestureState.COMMITTED, GestureState.FLUNG -> params.activeIndicator.arrowDimens - GestureState.CANCELLED -> params.cancelledArrowDimens - GestureState.GONE, GestureState.ENTRY -> params.entryIndicator.arrowDimens - }, - backgroundDimens = when (currentState) { - GestureState.GONE, GestureState.ENTRY -> params.entryIndicator.backgroundDimens - else -> - params.activeIndicator.backgroundDimens.copy( - edgeCornerRadius = - if (currentState == GestureState.INACTIVE || - currentState == GestureState.CANCELLED - ) - params.cancelledEdgeCornerRadius - else - params.activeIndicator.backgroundDimens.edgeCornerRadius - ) - } - ), - animate = animated + animate = !(currentState == GestureState.FLUNG || + currentState == GestureState.COMMITTED), + restingParams = EdgePanelParams.BackIndicatorDimens( + scale = when (currentState) { + GestureState.ACTIVE, + GestureState.FLUNG, + -> params.activeIndicator.scale + GestureState.COMMITTED -> params.committedIndicator.scale + else -> params.preThresholdIndicator.scale + }, + scalePivotX = when (currentState) { + GestureState.GONE, + GestureState.ENTRY, + GestureState.INACTIVE, + GestureState.CANCELLED -> params.preThresholdIndicator.scalePivotX + else -> params.committedIndicator.scalePivotX + }, + horizontalTranslation = when (currentState) { + GestureState.GONE -> { + params.activeIndicator.backgroundDimens.width?.times(-1) + } + GestureState.ENTRY, + GestureState.INACTIVE -> params.entryIndicator.horizontalTranslation + GestureState.FLUNG -> params.activeIndicator.horizontalTranslation + GestureState.ACTIVE -> params.activeIndicator.horizontalTranslation + GestureState.CANCELLED -> { + params.cancelledIndicator.horizontalTranslation + } + else -> null + }, + arrowDimens = when (currentState) { + GestureState.GONE, + GestureState.ENTRY, + GestureState.INACTIVE -> params.entryIndicator.arrowDimens + GestureState.ACTIVE -> params.activeIndicator.arrowDimens + GestureState.FLUNG, + GestureState.COMMITTED -> params.committedIndicator.arrowDimens + GestureState.CANCELLED -> params.cancelledIndicator.arrowDimens + }, + backgroundDimens = when (currentState) { + GestureState.GONE, + GestureState.ENTRY, + GestureState.INACTIVE -> params.entryIndicator.backgroundDimens + GestureState.ACTIVE -> params.activeIndicator.backgroundDimens + GestureState.FLUNG -> params.activeIndicator.backgroundDimens + GestureState.COMMITTED -> params.committedIndicator.backgroundDimens + GestureState.CANCELLED -> params.cancelledIndicator.backgroundDimens + } + ) ) } @@ -623,42 +787,123 @@ class BackPanelController private constructor( private fun updateArrowState(newState: GestureState, force: Boolean = false) { if (!force && currentState == newState) return - if (DEBUG) Log.d(TAG, "updateArrowState $currentState -> $newState") previousState = currentState currentState = newState - if (currentState == GestureState.GONE) { - mView.cancelAlphaAnimations() - mView.visibility = View.GONE - } else { - mView.visibility = View.VISIBLE + + when (currentState) { + GestureState.CANCELLED -> { + backCallback.cancelBack() + } + GestureState.FLUNG, + GestureState.COMMITTED -> { + // When flung, trigger back immediately but don't fire again + // once state resolves to committed. + if (previousState != GestureState.FLUNG) backCallback.triggerBack() + } + GestureState.ENTRY, + GestureState.INACTIVE -> { + backCallback.setTriggerBack(false) + } + GestureState.ACTIVE -> { + backCallback.setTriggerBack(true) + } + GestureState.GONE -> { } } when (currentState) { // Transitioning to GONE never animates since the arrow is (presumably) already off the // screen - GestureState.GONE -> updateRestingArrowDimens(animated = false, currentState) + GestureState.GONE -> { + updateRestingArrowDimens() + mView.isVisible = false + } GestureState.ENTRY -> { - updateYPosition(startY) - updateRestingArrowDimens(animated = true, currentState) + mView.isVisible = true + + updateRestingArrowDimens() + gestureEntryTime = SystemClock.uptimeMillis() + gestureInactiveOrEntryTime = SystemClock.uptimeMillis() } GestureState.ACTIVE -> { - updateRestingArrowDimens(animated = true, currentState) - // Vibrate the first time we transition to ACTIVE - if (!hasHapticPlayed) { - hasHapticPlayed = true - vibrationTime = SystemClock.uptimeMillis() - vibratorHelper.vibrate(VibrationEffect.EFFECT_TICK) + previousXTranslationOnActiveOffset = previousXTranslation + gestureActiveTime = SystemClock.uptimeMillis() + + updateRestingArrowDimens() + + vibratorHelper.cancel() + mainHandler.postDelayed(10L) { + vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT) + } + + val startingVelocity = convertVelocityToSpringStartingVelocity( + valueOnFastVelocity = 0f, + valueOnSlowVelocity = if (previousState == GestureState.ENTRY) 2f else 4.5f + ) + + when (previousState) { + GestureState.ENTRY, + GestureState.INACTIVE -> { + mView.popOffEdge(startingVelocity) + } + GestureState.COMMITTED -> { + // if previous state was committed then this activation + // was due to a quick second swipe. Don't pop the arrow this time + } + else -> { } } } + GestureState.INACTIVE -> { - updateRestingArrowDimens(animated = true, currentState) + gestureInactiveOrEntryTime = SystemClock.uptimeMillis() + + val startingVelocity = convertVelocityToSpringStartingVelocity( + valueOnFastVelocity = -1.05f, + valueOnSlowVelocity = -1.50f + ) + mView.popOffEdge(startingVelocity) + + vibratorHelper.vibrate(VIBRATE_DEACTIVATED_EFFECT) + updateRestingArrowDimens() + } + GestureState.FLUNG -> { + mainHandler.postDelayed(POP_ON_FLING_DELAY) { mView.popScale(1.9f) } + playHorizontalAnimationThen(onEndSetCommittedStateListener) + } + GestureState.COMMITTED -> { + if (previousState == GestureState.FLUNG) { + playAnimationThenSetGoneEnd() + } else { + mView.popScale(3f) + mainHandler.postDelayed( + playAnimationThenSetGoneOnAlphaEnd, + MIN_DURATION_COMMITTED_ANIMATION + ) + } + } + GestureState.CANCELLED -> { + val delay = max(0, MIN_DURATION_CANCELLED_ANIMATION - elapsedTimeSinceEntry) + playWithBackgroundWidthAnimation(onEndSetGoneStateListener, delay) + + params.arrowStrokeAlphaSpring.get(0f).takeIf { it.isNewState }?.let { + mView.popArrowAlpha(0f, it.value) + } + mainHandler.postDelayed(10L) { vibratorHelper.cancel() } } - GestureState.FLUNG -> playFlingBackAnimation() - GestureState.COMMITTED -> playCommitBackAnimation() - GestureState.CANCELLED -> playCancelBackAnimation() } } + private fun convertVelocityToSpringStartingVelocity( + valueOnFastVelocity: Float, + valueOnSlowVelocity: Float, + ): Float { + val factor = velocityTracker?.run { + computeCurrentVelocity(PX_PER_MS) + MathUtils.smoothStep(0f, 3f, abs(xVelocity)) + } ?: valueOnFastVelocity + + return MathUtils.lerp(valueOnFastVelocity, valueOnSlowVelocity, 1 - factor) + } + private fun scheduleFailsafe() { if (!ENABLE_FAILSAFE) return cancelFailsafe() @@ -685,24 +930,24 @@ class BackPanelController private constructor( init { if (DEBUG) mView.drawDebugInfo = { canvas -> val debugStrings = listOf( - "$currentState", - "startX=$startX", - "startY=$startY", - "xDelta=${"%.1f".format(totalTouchDelta)}", - "xTranslation=${"%.1f".format(previousXTranslation)}", - "pre=${"%.0f".format(preThresholdStretchProgress(previousXTranslation) * 100)}%", - "post=${"%.0f".format(fullScreenStretchProgress(previousXTranslation) * 100)}%" + "$currentState", + "startX=$startX", + "startY=$startY", + "xDelta=${"%.1f".format(totalTouchDelta)}", + "xTranslation=${"%.1f".format(previousXTranslation)}", + "pre=${"%.0f".format(staticThresholdProgress(previousXTranslation) * 100)}%", + "post=${"%.0f".format(fullScreenProgress(previousXTranslation) * 100)}%" ) val debugPaint = Paint().apply { color = Color.WHITE } val debugInfoBottom = debugStrings.size * 32f + 4f canvas.drawRect( - 4f, - 4f, - canvas.width.toFloat(), - debugStrings.size * 32f + 4f, - debugPaint + 4f, + 4f, + canvas.width.toFloat(), + debugStrings.size * 32f + 4f, + debugPaint ) debugPaint.apply { color = Color.BLACK @@ -728,9 +973,71 @@ class BackPanelController private constructor( canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint) } - drawVerticalLine(x = params.swipeTriggerThreshold, color = Color.BLUE) + drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE) + drawVerticalLine(x = params.deactivationSwipeTriggerThreshold, color = Color.BLUE) drawVerticalLine(x = startX, color = Color.GREEN) drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY) } } } + +/** + * In addition to a typical step function which returns one or two + * values based on a threshold, `Step` also gracefully handles quick + * changes in input near the threshold value that would typically + * result in the output rapidly changing. + * + * In the context of Back arrow, the arrow's stroke opacity should + * always appear transparent or opaque. Using a typical Step function, + * this would resulting in a flickering appearance as the output would + * change rapidly. `Step` addresses this by moving the threshold after + * it is crossed so it cannot be easily crossed again with small changes + * in touch events. + */ +class Step( + private val threshold: Float, + private val factor: Float = 1.1f, + private val postThreshold: T, + private val preThreshold: T +) { + + data class Value(val value: T, val isNewState: Boolean) + + private val lowerFactor = 2 - factor + + private lateinit var startValue: Value + private lateinit var previousValue: Value + private var hasCrossedUpperBoundAtLeastOnce = false + private var progress: Float = 0f + + init { + reset() + } + + fun reset() { + hasCrossedUpperBoundAtLeastOnce = false + progress = 0f + startValue = Value(preThreshold, false) + previousValue = startValue + } + + fun get(progress: Float): Value { + this.progress = progress + + val hasCrossedUpperBound = progress > threshold * factor + val hasCrossedLowerBound = progress > threshold * lowerFactor + + return when { + hasCrossedUpperBound && !hasCrossedUpperBoundAtLeastOnce -> { + hasCrossedUpperBoundAtLeastOnce = true + Value(postThreshold, true) + } + hasCrossedLowerBound -> previousValue.copy(isNewState = false) + hasCrossedUpperBoundAtLeastOnce -> { + hasCrossedUpperBoundAtLeastOnce = false + Value(preThreshold, true) + } + else -> startValue + }.also { previousValue = it } + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt index d56537b694dac..0c0002221244d 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt @@ -1,52 +1,82 @@ package com.android.systemui.navigationbar.gestural import android.content.res.Resources +import android.util.TypedValue +import androidx.core.animation.PathInterpolator +import androidx.dynamicanimation.animation.SpringForce import com.android.systemui.R data class EdgePanelParams(private var resources: Resources) { data class ArrowDimens( - val length: Float = 0f, - val height: Float = 0f + val length: Float = 0f, + val height: Float = 0f, + val alpha: Float = 0f, + var alphaSpring: SpringForce? = null, + val heightSpring: SpringForce? = null, + val lengthSpring: SpringForce? = null, ) data class BackgroundDimens( - val width: Float = 0f, - val height: Float = 0f, - val edgeCornerRadius: Float = 0f, - val farCornerRadius: Float = 0f + val width: Float? = 0f, + val height: Float = 0f, + val edgeCornerRadius: Float = 0f, + val farCornerRadius: Float = 0f, + val alpha: Float = 0f, + val widthSpring: SpringForce? = null, + val heightSpring: SpringForce? = null, + val farCornerRadiusSpring: SpringForce? = null, + val edgeCornerRadiusSpring: SpringForce? = null, + val alphaSpring: SpringForce? = null, ) data class BackIndicatorDimens( - val horizontalTranslation: Float = 0f, - val arrowDimens: ArrowDimens = ArrowDimens(), - val backgroundDimens: BackgroundDimens = BackgroundDimens() + val horizontalTranslation: Float? = 0f, + val scale: Float = 0f, + val scalePivotX: Float = 0f, + val arrowDimens: ArrowDimens, + val backgroundDimens: BackgroundDimens, + val verticalTranslationSpring: SpringForce? = null, + val horizontalTranslationSpring: SpringForce? = null, + val scaleSpring: SpringForce? = null, ) - var arrowThickness: Float = 0f + lateinit var entryIndicator: BackIndicatorDimens private set - var entryIndicator = BackIndicatorDimens() + lateinit var activeIndicator: BackIndicatorDimens private set - var activeIndicator = BackIndicatorDimens() + lateinit var cancelledIndicator: BackIndicatorDimens private set - var preThresholdIndicator = BackIndicatorDimens() + lateinit var flungIndicator: BackIndicatorDimens private set - var fullyStretchedIndicator = BackIndicatorDimens() + lateinit var committedIndicator: BackIndicatorDimens private set - var cancelledEdgeCornerRadius: Float = 0f + lateinit var preThresholdIndicator: BackIndicatorDimens + private set + lateinit var fullyStretchedIndicator: BackIndicatorDimens private set - var cancelledArrowDimens = ArrowDimens() // navigation bar edge constants var arrowPaddingEnd: Int = 0 private set + var arrowThickness: Float = 0f + private set + lateinit var arrowStrokeAlphaSpring: Step + private set + lateinit var arrowStrokeAlphaInterpolator: Step + private set // The closest to y var minArrowYPosition: Int = 0 private set var fingerOffset: Int = 0 private set - var swipeTriggerThreshold: Float = 0f + var staticTriggerThreshold: Float = 0f + private set + var reactivationTriggerThreshold: Float = 0f + private set + var deactivationSwipeTriggerThreshold: Float = 0f + get() = -field private set var swipeProgressThreshold: Float = 0f private set @@ -55,6 +85,26 @@ data class EdgePanelParams(private var resources: Resources) { var minDeltaForSwitch: Int = 0 private set + var minDragToStartAnimation: Float = 0f + private set + + lateinit var entryWidthInterpolator: PathInterpolator + private set + lateinit var entryWidthTowardsEdgeInterpolator: PathInterpolator + private set + lateinit var activeWidthInterpolator: PathInterpolator + private set + lateinit var arrowAngleInterpolator: PathInterpolator + private set + lateinit var translationInterpolator: PathInterpolator + private set + lateinit var farCornerInterpolator: PathInterpolator + private set + lateinit var edgeCornerInterpolator: PathInterpolator + private set + lateinit var heightInterpolator: PathInterpolator + private set + init { update(resources) } @@ -63,6 +113,10 @@ data class EdgePanelParams(private var resources: Resources) { return resources.getDimension(id) } + private fun getDimenFloat(id: Int): Float { + return TypedValue().run { resources.getValue(id, this, true); float } + } + private fun getPx(id: Int): Int { return resources.getDimensionPixelSize(id) } @@ -73,72 +127,200 @@ data class EdgePanelParams(private var resources: Resources) { arrowPaddingEnd = getPx(R.dimen.navigation_edge_panel_padding) minArrowYPosition = getPx(R.dimen.navigation_edge_arrow_min_y) fingerOffset = getPx(R.dimen.navigation_edge_finger_offset) - swipeTriggerThreshold = getDimen(R.dimen.navigation_edge_action_drag_threshold) + staticTriggerThreshold = getDimen(R.dimen.navigation_edge_action_drag_threshold) + reactivationTriggerThreshold = + getDimen(R.dimen.navigation_edge_action_reactivation_drag_threshold) + deactivationSwipeTriggerThreshold = + getDimen(R.dimen.navigation_edge_action_deactivation_drag_threshold) swipeProgressThreshold = getDimen(R.dimen.navigation_edge_action_progress_threshold) minDeltaForSwitch = getPx(R.dimen.navigation_edge_minimum_x_delta_for_switch) + minDragToStartAnimation = + getDimen(R.dimen.navigation_edge_action_min_distance_to_start_animation) + + entryWidthInterpolator = PathInterpolator(.19f, 1.27f, .71f, .86f) + entryWidthTowardsEdgeInterpolator = PathInterpolator(1f, -3f, 1f, 1.2f) + activeWidthInterpolator = PathInterpolator(.15f, .48f, .46f, .89f) + arrowAngleInterpolator = entryWidthInterpolator + translationInterpolator = PathInterpolator(0.2f, 1.0f, 1.0f, 1.0f) + farCornerInterpolator = PathInterpolator(.03f, .19f, .14f, 1.09f) + edgeCornerInterpolator = PathInterpolator(0f, 1.11f, .85f, .84f) + heightInterpolator = PathInterpolator(1f, .05f, .9f, -0.29f) + + val showArrowOnProgressValue = .2f + val showArrowOnProgressValueFactor = 1.05f + + val entryActiveHorizontalTranslationSpring = createSpring(675f, 0.8f) + val activeCommittedArrowLengthSpring = createSpring(1500f, 0.29f) + val activeCommittedArrowHeightSpring = createSpring(1500f, 0.29f) + val flungCommittedEdgeCornerSpring = createSpring(10000f, 1f) + val flungCommittedFarCornerSpring = createSpring(10000f, 1f) + val flungCommittedWidthSpring = createSpring(10000f, 1f) + val flungCommittedHeightSpring = createSpring(10000f, 1f) entryIndicator = BackIndicatorDimens( - horizontalTranslation = getDimen(R.dimen.navigation_edge_entry_margin), - arrowDimens = ArrowDimens( - length = getDimen(R.dimen.navigation_edge_entry_arrow_length), - height = getDimen(R.dimen.navigation_edge_entry_arrow_height), - ), - backgroundDimens = BackgroundDimens( - width = getDimen(R.dimen.navigation_edge_entry_background_width), - height = getDimen(R.dimen.navigation_edge_entry_background_height), - edgeCornerRadius = getDimen(R.dimen.navigation_edge_entry_edge_corners), - farCornerRadius = getDimen(R.dimen.navigation_edge_entry_far_corners) - ) + horizontalTranslation = getDimen(R.dimen.navigation_edge_entry_margin), + scale = getDimenFloat(R.dimen.navigation_edge_entry_scale), + scalePivotX = getDimen(R.dimen.navigation_edge_pre_threshold_background_width), + horizontalTranslationSpring = entryActiveHorizontalTranslationSpring, + verticalTranslationSpring = createSpring(10000f, 0.9f), + scaleSpring = createSpring(120f, 0.8f), + arrowDimens = ArrowDimens( + length = getDimen(R.dimen.navigation_edge_entry_arrow_length), + height = getDimen(R.dimen.navigation_edge_entry_arrow_height), + alpha = 0f, + alphaSpring = createSpring(200f, 1f), + lengthSpring = createSpring(600f, 0.4f), + heightSpring = createSpring(600f, 0.4f), + ), + backgroundDimens = BackgroundDimens( + alpha = 1f, + width = getDimen(R.dimen.navigation_edge_entry_background_width), + height = getDimen(R.dimen.navigation_edge_entry_background_height), + edgeCornerRadius = getDimen(R.dimen.navigation_edge_entry_edge_corners), + farCornerRadius = getDimen(R.dimen.navigation_edge_entry_far_corners), + alphaSpring = createSpring(900f, 1f), + widthSpring = createSpring(450f, 0.65f), + heightSpring = createSpring(1500f, 0.45f), + farCornerRadiusSpring = createSpring(300f, 0.5f), + edgeCornerRadiusSpring = createSpring(150f, 0.5f), + ) ) activeIndicator = BackIndicatorDimens( - horizontalTranslation = getDimen(R.dimen.navigation_edge_active_margin), - arrowDimens = ArrowDimens( - length = getDimen(R.dimen.navigation_edge_active_arrow_length), - height = getDimen(R.dimen.navigation_edge_active_arrow_height), - ), - backgroundDimens = BackgroundDimens( - width = getDimen(R.dimen.navigation_edge_active_background_width), - height = getDimen(R.dimen.navigation_edge_active_background_height), - edgeCornerRadius = getDimen(R.dimen.navigation_edge_active_edge_corners), - farCornerRadius = getDimen(R.dimen.navigation_edge_active_far_corners) - - ) + horizontalTranslation = getDimen(R.dimen.navigation_edge_active_margin), + scale = getDimenFloat(R.dimen.navigation_edge_active_scale), + horizontalTranslationSpring = entryActiveHorizontalTranslationSpring, + scaleSpring = createSpring(450f, 0.415f), + arrowDimens = ArrowDimens( + length = getDimen(R.dimen.navigation_edge_active_arrow_length), + height = getDimen(R.dimen.navigation_edge_active_arrow_height), + alpha = 1f, + lengthSpring = activeCommittedArrowLengthSpring, + heightSpring = activeCommittedArrowHeightSpring, + ), + backgroundDimens = BackgroundDimens( + alpha = 1f, + width = getDimen(R.dimen.navigation_edge_active_background_width), + height = getDimen(R.dimen.navigation_edge_active_background_height), + edgeCornerRadius = getDimen(R.dimen.navigation_edge_active_edge_corners), + farCornerRadius = getDimen(R.dimen.navigation_edge_active_far_corners), + widthSpring = createSpring(375f, 0.675f), + heightSpring = createSpring(10000f, 1f), + edgeCornerRadiusSpring = createSpring(600f, 0.36f), + farCornerRadiusSpring = createSpring(2500f, 0.855f), + ) ) preThresholdIndicator = BackIndicatorDimens( - horizontalTranslation = getDimen(R.dimen.navigation_edge_pre_threshold_margin), - arrowDimens = ArrowDimens( - length = entryIndicator.arrowDimens.length, - height = entryIndicator.arrowDimens.height, - ), - backgroundDimens = BackgroundDimens( - width = getDimen(R.dimen.navigation_edge_pre_threshold_background_width), - height = getDimen(R.dimen.navigation_edge_pre_threshold_background_height), - edgeCornerRadius = getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners), - farCornerRadius = getDimen(R.dimen.navigation_edge_pre_threshold_far_corners) - ) + horizontalTranslation = getDimen(R.dimen.navigation_edge_pre_threshold_margin), + scale = getDimenFloat(R.dimen.navigation_edge_pre_threshold_scale), + scalePivotX = getDimen(R.dimen.navigation_edge_pre_threshold_background_width), + scaleSpring = createSpring(120f, 0.8f), + horizontalTranslationSpring = createSpring(6000f, 1f), + arrowDimens = ArrowDimens( + length = getDimen(R.dimen.navigation_edge_pre_threshold_arrow_length), + height = getDimen(R.dimen.navigation_edge_pre_threshold_arrow_height), + alpha = 1f, + lengthSpring = createSpring(100f, 0.6f), + heightSpring = createSpring(100f, 0.6f), + ), + backgroundDimens = BackgroundDimens( + alpha = 1f, + width = getDimen(R.dimen.navigation_edge_pre_threshold_background_width), + height = getDimen(R.dimen.navigation_edge_pre_threshold_background_height), + edgeCornerRadius = + getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners), + farCornerRadius = + getDimen(R.dimen.navigation_edge_pre_threshold_far_corners), + widthSpring = createSpring(200f, 0.65f), + heightSpring = createSpring(1500f, 0.45f), + farCornerRadiusSpring = createSpring(200f, 1f), + edgeCornerRadiusSpring = createSpring(150f, 0.5f), + ) + ) + + committedIndicator = activeIndicator.copy( + horizontalTranslation = null, + arrowDimens = activeIndicator.arrowDimens.copy( + lengthSpring = activeCommittedArrowLengthSpring, + heightSpring = activeCommittedArrowHeightSpring, + ), + backgroundDimens = activeIndicator.backgroundDimens.copy( + alpha = 0f, + // explicitly set to null to preserve previous width upon state change + width = null, + widthSpring = flungCommittedWidthSpring, + heightSpring = flungCommittedHeightSpring, + edgeCornerRadiusSpring = flungCommittedEdgeCornerSpring, + farCornerRadiusSpring = flungCommittedFarCornerSpring, + ), + scale = 0.85f, + scaleSpring = createSpring(650f, 1f), + ) + + flungIndicator = committedIndicator.copy( + arrowDimens = committedIndicator.arrowDimens.copy( + lengthSpring = createSpring(850f, 0.46f), + heightSpring = createSpring(850f, 0.46f), + ), + backgroundDimens = committedIndicator.backgroundDimens.copy( + widthSpring = flungCommittedWidthSpring, + heightSpring = flungCommittedHeightSpring, + edgeCornerRadiusSpring = flungCommittedEdgeCornerSpring, + farCornerRadiusSpring = flungCommittedFarCornerSpring, + ) + ) + + cancelledIndicator = entryIndicator.copy( + backgroundDimens = entryIndicator.backgroundDimens.copy(width = 0f) ) fullyStretchedIndicator = BackIndicatorDimens( - horizontalTranslation = getDimen(R.dimen.navigation_edge_stretch_margin), - arrowDimens = ArrowDimens( - length = getDimen(R.dimen.navigation_edge_stretched_arrow_length), - height = getDimen(R.dimen.navigation_edge_stretched_arrow_height), - ), - backgroundDimens = BackgroundDimens( - width = getDimen(R.dimen.navigation_edge_stretch_background_width), - height = getDimen(R.dimen.navigation_edge_stretch_background_height), - edgeCornerRadius = getDimen(R.dimen.navigation_edge_stretch_edge_corners), - farCornerRadius = getDimen(R.dimen.navigation_edge_stretch_far_corners) + horizontalTranslation = getDimen(R.dimen.navigation_edge_stretch_margin), + scale = getDimenFloat(R.dimen.navigation_edge_stretch_scale), + horizontalTranslationSpring = null, + verticalTranslationSpring = null, + scaleSpring = null, + arrowDimens = ArrowDimens( + length = getDimen(R.dimen.navigation_edge_stretched_arrow_length), + height = getDimen(R.dimen.navigation_edge_stretched_arrow_height), + alpha = 1f, + alphaSpring = null, + heightSpring = null, + lengthSpring = null, + ), + backgroundDimens = BackgroundDimens( + alpha = 1f, + width = getDimen(R.dimen.navigation_edge_stretch_background_width), + height = getDimen(R.dimen.navigation_edge_stretch_background_height), + edgeCornerRadius = getDimen(R.dimen.navigation_edge_stretch_edge_corners), + farCornerRadius = getDimen(R.dimen.navigation_edge_stretch_far_corners), + alphaSpring = null, + widthSpring = null, + heightSpring = null, + edgeCornerRadiusSpring = null, + farCornerRadiusSpring = null, + ) + ) + + arrowStrokeAlphaInterpolator = Step( + threshold = showArrowOnProgressValue, + factor = showArrowOnProgressValueFactor, + postThreshold = 1f, + preThreshold = 0f + ) + + entryIndicator.arrowDimens.alphaSpring?.let { alphaSpring -> + arrowStrokeAlphaSpring = Step( + threshold = showArrowOnProgressValue, + factor = showArrowOnProgressValueFactor, + postThreshold = alphaSpring, + preThreshold = SpringForce().setStiffness(2000f).setDampingRatio(1f) ) - ) - - cancelledEdgeCornerRadius = getDimen(R.dimen.navigation_edge_cancelled_edge_corners) - - cancelledArrowDimens = ArrowDimens( - length = getDimen(R.dimen.navigation_edge_cancelled_arrow_length), - height = getDimen(R.dimen.navigation_edge_cancelled_arrow_height) - ) + } } } + +fun createSpring(stiffness: Float, dampingRatio: Float): SpringForce { + return SpringForce().setStiffness(stiffness).setDampingRatio(dampingRatio) +} \ No newline at end of file