diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index a014efb7d1761..732764ee38fa3 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -48,7 +48,50 @@
64dp
- 48dp
+ 64dp
+
+
+ 4dp
+
+ 32dp
+
+
+ 4dp
+ 8dp
+ 60dp
+ 6dp
+ 6dp
+ 10dp
+ 7dp
+
+
+ 4dp
+ 64dp
+ 60dp
+ 22dp
+ 26dp
+
+
+ 14dp
+ 60dp
+ 60dp
+ 30dp
+ 30dp
+ 8dp
+ 9dp
+
+
+ 412dp
+ 18dp
+ 74dp
+ 60dp
+ 30dp
+ 30dp
+ 7dp
+ 10dp
+
+ 12dp
+ 0dp
@*android:dimen/status_bar_icon_size
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
new file mode 100644
index 0000000000000..56ad19ae89ca6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
@@ -0,0 +1,340 @@
+package com.android.systemui.navigationbar.gestural
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.RectF
+import android.view.View
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.SpringAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.internal.util.LatencyTracker
+import com.android.settingslib.Utils
+import com.android.systemui.navigationbar.gestural.BackPanelController.DelayedOnAnimationEndListener
+
+private const val TAG = "BackPanel"
+private const val DEBUG = false
+
+class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : View(context) {
+
+ var arrowsPointLeft = false
+ set(value) {
+ if (field != value) {
+ invalidate()
+ field = value
+ }
+ }
+
+ // Arrow color and shape
+ private val arrowPath = Path()
+ private val arrowPaint = Paint()
+
+ // Arrow background color and shape
+ private var arrowBackgroundRect = RectF()
+ private var arrowBackgroundPaint = Paint()
+
+ // True if the panel is currently on the left of the screen
+ var isLeftPanel = false
+
+ /**
+ * Used to track back arrow latency from [android.view.MotionEvent.ACTION_DOWN] to [onDraw]
+ */
+ private var trackingBackArrowLatency = false
+
+ /**
+ * The length of the arrow measured horizontally. Used for animating [arrowPath]
+ */
+ private var arrowLength = AnimatedFloat("arrowLength", SpringForce())
+
+ /**
+ * The height of the arrow measured vertically from its center to its top (i.e. half the total
+ * height). Used for animating [arrowPath]
+ */
+ private var arrowHeight = AnimatedFloat("arrowHeight", SpringForce())
+
+ private val backgroundWidth = AnimatedFloat(
+ name = "backgroundWidth",
+ SpringForce().apply {
+ stiffness = 600f
+ dampingRatio = 0.65f
+ })
+
+ private val backgroundHeight = AnimatedFloat(
+ name = "backgroundHeight",
+ SpringForce().apply {
+ stiffness = 600f
+ dampingRatio = 0.65f
+ })
+
+ /**
+ * Corners of the background closer to the edge of the screen (where the arrow appeared from).
+ * Used for animating [arrowBackgroundRect]
+ */
+ private val backgroundEdgeCornerRadius = AnimatedFloat(
+ name = "backgroundEdgeCornerRadius",
+ SpringForce().apply {
+ stiffness = 400f
+ dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
+ })
+
+ /**
+ * Corners of the background further from the edge of the screens (toward the direction the
+ * arrow is being dragged). Used for animating [arrowBackgroundRect]
+ */
+ private val backgroundDragCornerRadius = AnimatedFloat(
+ name = "backgroundDragCornerRadius",
+ SpringForce().apply {
+ stiffness = 2200f
+ dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
+ })
+
+ /**
+ * Left/right position of the background relative to the canvas. Also corresponds with the
+ * background's margin relative to the screen edge. The arrow will be centered within the
+ * background.
+ */
+ private var horizontalTranslation = AnimatedFloat("horizontalTranslation", SpringForce())
+
+ /**
+ * Canvas vertical translation. How far up/down the arrow and background appear relative to the
+ * canvas.
+ */
+ private var verticalTranslation: AnimatedFloat =
+ AnimatedFloat("verticalTranslation", SpringForce().apply {
+ stiffness = SpringForce.STIFFNESS_MEDIUM
+ })
+
+ /**
+ * Use for drawing debug info. Can only be set if [DEBUG]=true
+ */
+ var drawDebugInfo: ((canvas: Canvas) -> Unit)? = null
+ set(value) {
+ if (DEBUG) field = value
+ }
+
+ internal fun updateArrowPaint(arrowThickness: Float) {
+ // Arrow constants
+ arrowPaint.strokeWidth = arrowThickness
+
+ arrowPaint.color =
+ Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary)
+ arrowBackgroundPaint.color = Utils.getColorAccentDefaultColor(context)
+ }
+
+ private inner class AnimatedFloat(name: String, springForce: SpringForce) {
+ // The resting position when not stretched by a touch drag
+ private var restingPosition = 0f
+
+ // The current position as updated by the SpringAnimation
+ var pos = 0f
+ set(v) {
+ if (field != v) {
+ field = v
+ invalidate()
+ }
+ }
+
+ val animation: SpringAnimation
+
+ init {
+ val floatProp = object : FloatPropertyCompat(name) {
+ override fun setValue(animatedFloat: AnimatedFloat, value: Float) {
+ animatedFloat.pos = value
+ }
+
+ override fun getValue(animatedFloat: AnimatedFloat): Float = animatedFloat.pos
+ }
+ animation = SpringAnimation(this, floatProp)
+ animation.spring = springForce
+ }
+
+ fun snapTo(newPosition: Float) {
+ animation.cancel()
+ restingPosition = newPosition
+ animation.spring.finalPosition = newPosition
+ pos = newPosition
+ }
+
+ fun stretchTo(stretchAmount: Float) {
+ animation.animateToFinalPosition(restingPosition + stretchAmount)
+ }
+
+ fun updateRestingPosition(pos: Float, animated: Boolean) {
+ restingPosition = pos
+ if (animated)
+ animation.animateToFinalPosition(restingPosition)
+ else
+ snapTo(restingPosition)
+ }
+ }
+
+ init {
+ visibility = GONE
+ arrowPaint.apply {
+ style = Paint.Style.STROKE
+ strokeCap = Paint.Cap.SQUARE
+ }
+ arrowBackgroundPaint.apply {
+ style = Paint.Style.FILL
+ strokeJoin = Paint.Join.ROUND
+ strokeCap = Paint.Cap.ROUND
+ }
+ }
+
+ private fun calculateArrowPath(dx: Float, dy: Float): Path {
+ arrowPath.reset()
+ arrowPath.moveTo(dx, -dy)
+ arrowPath.lineTo(0f, 0f)
+ arrowPath.lineTo(dx, dy)
+ arrowPath.moveTo(dx, -dy)
+ return arrowPath
+ }
+
+ fun addEndListener(endListener: DelayedOnAnimationEndListener): Boolean {
+ return if (horizontalTranslation.animation.isRunning) {
+ horizontalTranslation.animation.addEndListener(endListener)
+ true
+ } else {
+ endListener.runNow()
+ false
+ }
+ }
+
+ fun setStretch(
+ arrowLengthStretch: Float,
+ arrowHeightStretch: Float,
+ backgroundWidthStretch: Float,
+ backgroundHeightStretch: Float,
+ backgroundEdgeCornerRadiusStretch: Float,
+ backgroundDragCornerRadiusStretch: Float,
+ horizontalTranslationStretch: Float
+ ) {
+ arrowLength.stretchTo(arrowLengthStretch)
+ arrowHeight.stretchTo(arrowHeightStretch)
+ backgroundWidth.stretchTo(backgroundWidthStretch)
+ backgroundHeight.stretchTo(backgroundHeightStretch)
+ backgroundEdgeCornerRadius.stretchTo(backgroundEdgeCornerRadiusStretch)
+ backgroundDragCornerRadius.stretchTo(backgroundDragCornerRadiusStretch)
+ horizontalTranslation.stretchTo(horizontalTranslationStretch)
+ }
+
+ fun resetStretch() {
+ setStretch(0f, 0f, 0f, 0f, 0f, 0f, 0f)
+ }
+
+ /**
+ * Updates resting arrow and background size not accounting for stretch
+ */
+ internal fun updateRestingArrowDimens(
+ backgroundWidth: Float,
+ backgroundHeight: Float,
+ backgroundEdgeCornerRadius: Float,
+ backgroundDragCornerRadius: Float,
+ arrowLength: Float,
+ arrowHeight: Float,
+ horizontalTranslation: Float,
+ animate: Boolean
+ ) {
+ this.arrowLength.updateRestingPosition(arrowLength, animate)
+ this.arrowHeight.updateRestingPosition(arrowHeight, animate)
+ this.backgroundWidth.updateRestingPosition(backgroundWidth, animate)
+ this.backgroundHeight.updateRestingPosition(backgroundHeight, animate)
+ this.backgroundEdgeCornerRadius.updateRestingPosition(backgroundEdgeCornerRadius, animate)
+ this.backgroundDragCornerRadius.updateRestingPosition(backgroundDragCornerRadius, animate)
+ this.horizontalTranslation.updateRestingPosition(horizontalTranslation, animate)
+ }
+
+ fun animateVertically(yPos: Float) = verticalTranslation.stretchTo(yPos)
+
+ fun setArrowStiffness(arrowStiffness: Float, arrowDampingRatio: Float) {
+ arrowLength.animation.spring.apply {
+ stiffness = arrowStiffness
+ dampingRatio = arrowDampingRatio
+ }
+ arrowHeight.animation.spring.apply {
+ stiffness = arrowStiffness
+ dampingRatio = arrowDampingRatio
+ }
+ }
+
+ override fun hasOverlappingRendering() = false
+
+ override fun onDraw(canvas: Canvas) {
+ var edgeCorner = backgroundEdgeCornerRadius.pos
+ val farCorner = backgroundDragCornerRadius.pos
+ val halfHeight = backgroundHeight.pos / 2
+
+ canvas.save()
+
+ if (!isLeftPanel) canvas.scale(-1f, 1f, width / 2.0f, 0f)
+
+ canvas.translate(
+ horizontalTranslation.pos,
+ height * 0.5f + verticalTranslation.pos
+ )
+
+ val arrowBackground = arrowBackgroundRect.apply {
+ left = 0f
+ top = -halfHeight
+ right = backgroundWidth.pos
+ bottom = halfHeight
+ }.toPathWithRoundCorners(
+ topLeft = edgeCorner,
+ bottomLeft = edgeCorner,
+ topRight = farCorner,
+ bottomRight = farCorner
+ )
+ canvas.drawPath(arrowBackground, arrowBackgroundPaint)
+
+ val dx = arrowLength.pos
+ val dy = arrowHeight.pos
+
+ // How far the arrow bounding box should be from the edge of the screen. Measured from
+ // either the tip or the back of the arrow, whichever is closer
+ var arrowOffset = (backgroundWidth.pos - dx) / 2
+ canvas.translate(
+ /* dx= */ arrowOffset,
+ /* dy= */ 0f /* pass 0 for the y position since the canvas was already translated */
+ )
+
+ val arrowPointsAwayFromEdge = !arrowsPointLeft.xor(isLeftPanel)
+ if (arrowPointsAwayFromEdge) {
+ canvas.apply {
+ scale(-1f, 1f, 0f, 0f)
+ translate(-dx, 0f)
+ }
+ }
+
+ val arrowPath = calculateArrowPath(dx = dx, dy = dy)
+ canvas.drawPath(arrowPath, arrowPaint)
+ canvas.restore()
+
+ if (trackingBackArrowLatency) {
+ latencyTracker.onActionEnd(LatencyTracker.ACTION_SHOW_BACK_ARROW)
+ trackingBackArrowLatency = false
+ }
+
+ if (DEBUG) drawDebugInfo?.invoke(canvas)
+ }
+
+ fun startTrackingShowBackArrowLatency() {
+ latencyTracker.onActionStart(LatencyTracker.ACTION_SHOW_BACK_ARROW)
+ trackingBackArrowLatency = true
+ }
+
+ private fun RectF.toPathWithRoundCorners(
+ topLeft: Float = 0f,
+ topRight: Float = 0f,
+ bottomRight: Float = 0f,
+ bottomLeft: Float = 0f
+ ): Path = Path().apply {
+ val corners = floatArrayOf(
+ topLeft, topLeft,
+ topRight, topRight,
+ bottomRight, bottomRight,
+ bottomLeft, bottomLeft
+ )
+ addRoundRect(this@toPathWithRoundCorners, corners, Path.Direction.CW)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
new file mode 100644
index 0000000000000..100411b1cb93d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -0,0 +1,735 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.navigationbar.gestural
+
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Point
+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.view.Gravity
+import android.view.MotionEvent
+import android.view.VelocityTracker
+import android.view.View
+import android.view.WindowManager
+import android.view.animation.AccelerateInterpolator
+import android.view.animation.PathInterpolator
+import android.window.BackEvent
+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
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.ViewController
+import com.android.wm.shell.back.BackAnimation
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlin.math.abs
+import kotlin.math.max
+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
+
+/**
+ * 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
+
+/**
+ * The min duration arrow remains on screen during a fling event.
+ */
+private const val FLING_PAUSE_DURATION_MS = 50L
+
+/**
+ * The min duration arrow remains on screen during a fling event.
+ */
+private const val MIN_FLING_VELOCITY = 3000
+
+/**
+ * The amount of rubber banding we do for the vertical translation
+ */
+private const val RUBBER_BAND_AMOUNT = 15
+
+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 ACCELERATE_INTERPOLATOR = AccelerateInterpolator(0.7f)
+
+class BackPanelController private constructor(
+ context: Context,
+ private var backAnimation: BackAnimation?,
+ private val windowManager: WindowManager,
+ @Main private val mainHandler: Handler,
+ private val vibratorHelper: VibratorHelper,
+ private val configurationController: ConfigurationController,
+ latencyTracker: LatencyTracker
+) : ViewController(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin {
+
+ /**
+ * Injectable instance to create a new BackPanelController.
+ *
+ * Necessary because EdgeBackGestureHandler sometimes needs to create new instances of
+ * BackPanelController, and we need to match EdgeBackGestureHandler's context.
+ */
+ class Factory @Inject constructor(
+ private val windowManager: WindowManager,
+ @Main private val mainHandler: Handler,
+ private val vibratorHelper: VibratorHelper,
+ private val configurationController: ConfigurationController,
+ private val latencyTracker: LatencyTracker
+ ) {
+ /** Construct a [BackPanelController]. */
+ fun create(context: Context, backAnimation: BackAnimation?): BackPanelController {
+ val backPanelController = BackPanelController(
+ context,
+ backAnimation,
+ windowManager,
+ mainHandler,
+ vibratorHelper,
+ configurationController,
+ latencyTracker
+ )
+ backPanelController.init()
+ return backPanelController
+ }
+ }
+
+ private var params: EdgePanelParams = EdgePanelParams(resources)
+ 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 previousXTranslation = 0f
+ private var totalTouchDelta = 0f
+ private var velocityTracker: VelocityTracker? = null
+ set(value) {
+ if (field != value) field?.recycle()
+ field = value
+ }
+ get() {
+ if (field == null) field = VelocityTracker.obtain()
+ return field
+ }
+
+ // The x,y position of the first touch event
+ private var startX = 0f
+ private var startY = 0f
+
+ private val failsafeRunnable = Runnable { onFailsafe() }
+
+ private enum class GestureState {
+ /* Arrow is off the screen and invisible */
+ GONE,
+
+ /* Arrow is animating in */
+ ENTRY,
+
+ /* could be entry, neutral, or stretched, releasing will commit back */
+ ACTIVE,
+
+ /* releasing will cancel back */
+ INACTIVE,
+
+ /* like committed, but animation takes longer */
+ FLUNG,
+
+ /* back action currently occurring, arrow soon to be GONE */
+ COMMITTED,
+
+ /* back action currently cancelling, arrow soon to be GONE */
+ CANCELLED
+ }
+
+ /**
+ * Wrapper around OnAnimationEndListener which runs the given runnable after a delay. The
+ * runnable is not called if the animation is cancelled
+ */
+ class DelayedOnAnimationEndListener(
+ private val handler: Handler,
+ private val runnable: Runnable,
+ private val delay: Long
+ ) : DynamicAnimation.OnAnimationEndListener {
+
+ override fun onAnimationEnd(
+ animation: DynamicAnimation<*>,
+ canceled: Boolean,
+ value: Float,
+ velocity: Float
+ ) {
+ animation.removeEndListener(this)
+ if (!canceled) {
+ handler.postDelayed(runnable, delay)
+ }
+ }
+
+ fun runNow() {
+ runnable.run()
+ }
+ }
+
+ private val setCommittedEndListener =
+ DelayedOnAnimationEndListener(
+ mainHandler,
+ { updateArrowState(GestureState.COMMITTED) },
+ delay = FLING_PAUSE_DURATION_MS
+ )
+
+ private val setGoneEndListener =
+ DelayedOnAnimationEndListener(
+ mainHandler,
+ {
+ cancelFailsafe()
+ updateArrowState(GestureState.GONE)
+ },
+ delay = 0
+ )
+
+ // Vibration
+ private var vibrationTime: Long = 0
+
+ // Minimum size of the screen's width or height
+ private var screenSize = 0
+
+ /**
+ * Used for initialization and configuration changes
+ */
+ private fun updateConfiguration() {
+ params.update(resources)
+ initializeBackAnimation()
+ mView.updateArrowPaint(params.arrowThickness)
+ }
+
+ private val configurationListener = object : ConfigurationController.ConfigurationListener {
+ override fun onConfigChanged(newConfig: Configuration?) {
+ updateConfiguration()
+ }
+
+ override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) {
+ updateArrowDirection(isLayoutRtl)
+ }
+ }
+
+ override fun onViewAttached() {
+ updateConfiguration()
+ updateArrowDirection(configurationController.isLayoutRtl)
+ updateArrowState(GestureState.GONE, force = true)
+ updateRestingArrowDimens(animated = false, currentState)
+ configurationController.addCallback(configurationListener)
+ }
+
+ /** Update the arrow direction. The arrow should point the same way for both panels. */
+ private fun updateArrowDirection(isLayoutRtl: Boolean) {
+ mView.arrowsPointLeft = isLayoutRtl
+ }
+
+ override fun onViewDetached() {
+ configurationController.removeCallback(configurationListener)
+ }
+
+ override fun onMotionEvent(event: MotionEvent) {
+ backAnimation?.onBackMotion(
+ event,
+ event.actionMasked,
+ if (mView.isLeftPanel) BackEvent.EDGE_LEFT else BackEvent.EDGE_RIGHT
+ )
+
+ velocityTracker!!.addMovement(event)
+ when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ resetOnDown()
+ startX = event.x
+ startY = event.y
+
+ // Reset the arrow to the side
+ updateArrowState(GestureState.ENTRY)
+
+ windowManager.updateViewLayout(mView, layoutParams)
+ mView.startTrackingShowBackArrowLatency()
+ }
+ MotionEvent.ACTION_MOVE -> handleMoveEvent(event)
+ MotionEvent.ACTION_UP -> {
+ if (currentState == GestureState.ACTIVE) {
+ updateArrowState(if (isFlung()) GestureState.FLUNG else GestureState.COMMITTED)
+ } else {
+ updateArrowState(GestureState.CANCELLED)
+ }
+ velocityTracker = null
+ }
+ MotionEvent.ACTION_CANCEL -> {
+ updateArrowState(GestureState.CANCELLED)
+ velocityTracker = null
+ }
+ }
+ }
+
+ private fun updateArrowStateOnMove(yTranslation: Float, xTranslation: Float) {
+ when (currentState) {
+ GestureState.GONE, GestureState.FLUNG, GestureState.COMMITTED, GestureState.CANCELLED ->
+ return
+ }
+
+ updateArrowState(
+ when {
+ // Check if we should transition from ENTRY to ACTIVE
+ currentState == GestureState.ENTRY && xTranslation > params.swipeTriggerThreshold ->
+ GestureState.ACTIVE
+
+ // Abort if we had continuous motion toward the edge for a while, OR the direction
+ // in Y is bigger than X * 2
+ currentState == GestureState.ACTIVE &&
+ ((totalTouchDelta < 0 && -totalTouchDelta > params.minDeltaForSwitch) ||
+ (yTranslation > xTranslation * 2)) ->
+ GestureState.INACTIVE
+
+ // Re-activate if we had continuous motion away from the edge for a while
+ currentState == GestureState.INACTIVE &&
+ (totalTouchDelta > 0 && totalTouchDelta > params.minDeltaForSwitch) ->
+ GestureState.ACTIVE
+
+ // By default assume the current direction is kept
+ else -> currentState
+ }
+ )
+ }
+
+ private fun handleMoveEvent(event: MotionEvent) {
+ when (currentState) {
+ GestureState.GONE, GestureState.FLUNG, GestureState.COMMITTED,
+ GestureState.CANCELLED -> return
+ }
+
+ val x = event.x
+ val y = event.y
+
+ val yOffset = y - startY
+
+ // How far in the y direction we are from the original touch
+ val yTranslation = abs(yOffset)
+
+ // How far in the x direction we are from the original touch ignoring motion that
+ // occurs between the screen edge and the touch start.
+ val xTranslation = max(0f, if (mView.isLeftPanel) x - startX else startX - x)
+
+ // Compared to last time, how far we moved in the x direction. If <0, we are moving closer
+ // to the edge. If >0, we are moving further from the edge
+ val xDelta = xTranslation - previousXTranslation
+ previousXTranslation = xTranslation
+
+ if (abs(xDelta) > 0) {
+ if (sign(xDelta) == sign(totalTouchDelta)) {
+ // Direction has NOT changed, so keep counting the delta
+ totalTouchDelta += xDelta
+ } else {
+ // Direction has changed, so reset the delta
+ totalTouchDelta = xDelta
+ }
+ }
+
+ updateArrowStateOnMove(yTranslation, xTranslation)
+ when (currentState) {
+ GestureState.ACTIVE -> setActiveStretch(fullScreenStretchProgress(xTranslation))
+ GestureState.ENTRY ->
+ setEntryStretch(preThresholdStretchProgress(xTranslation))
+ GestureState.INACTIVE -> mView.resetStretch()
+ }
+
+ // set y translation
+ setVerticalTranslation(yOffset)
+ }
+
+ fun setVerticalTranslation(yOffset: Float) {
+ val yTranslation = abs(yOffset)
+ val maxYOffset = (mView.height / 2) - (params.entryBackgroundHeight / 2)
+ val yProgress = saturate(yTranslation / (maxYOffset * RUBBER_BAND_AMOUNT))
+ mView.animateVertically(
+ RUBBER_BAND_INTERPOLATOR.getInterpolation(yProgress) * maxYOffset * sign(
+ yOffset
+ )
+ )
+ }
+
+ /**
+ * @return 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)
+ */
+ fun fullScreenStretchProgress(xTranslation: Float): Float {
+ return saturate(
+ (xTranslation - params.swipeTriggerThreshold) /
+ (min(
+ params.fullyStretchedThreshold,
+ screenSize.toFloat()
+ ) - params.swipeTriggerThreshold)
+ )
+ }
+
+ /**
+ * Tracks the relative position of the drag from the entry until the threshold where the arrow
+ * activates (between 0.0 - 1.0f)
+ */
+ fun preThresholdStretchProgress(xTranslation: Float): Float {
+ return saturate(xTranslation / params.swipeTriggerThreshold)
+ }
+
+ fun setActiveStretch(progress: Float) {
+ val stretch = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
+ mView.setStretch(
+ arrowLengthStretch = stretch * (params.stretchedArrowLength - params.activeArrowLength),
+ arrowHeightStretch = stretch * (params.stretchedArrowHeight - params.activeArrowHeight),
+ backgroundWidthStretch =
+ stretch * (params.stretchBackgroundWidth - params.activeBackgroundWidth),
+ backgroundHeightStretch =
+ stretch * (params.stretchBackgroundHeight - params.activeBackgroundHeight),
+ backgroundEdgeCornerRadiusStretch =
+ stretch * (params.stretchEdgeCorners - params.activeEdgeCorners),
+ backgroundDragCornerRadiusStretch =
+ stretch * (params.stretchFarCorners - params.activeFarCorners),
+ horizontalTranslationStretch = stretch * (params.stretchMargin - params.activeMargin)
+ )
+ }
+
+ fun setEntryStretch(progress: Float) {
+ val bgStretch = ACCELERATE_INTERPOLATOR.getInterpolation(progress)
+ val arrowStretch = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
+ mView.setStretch(
+ arrowLengthStretch =
+ arrowStretch * (params.activeArrowLength - params.entryArrowLength),
+ arrowHeightStretch =
+ arrowStretch * (params.activeArrowHeight - params.entryArrowHeight),
+ backgroundWidthStretch =
+ bgStretch * (params.preThresholdBackgroundWidth - params.entryBackgroundWidth),
+ backgroundHeightStretch =
+ bgStretch * (params.preThresholdBackgroundHeight - params.entryBackgroundHeight),
+ backgroundEdgeCornerRadiusStretch =
+ bgStretch * (params.preThresholdEdgeCorners - params.entryEdgeCorners),
+ backgroundDragCornerRadiusStretch =
+ bgStretch * (params.preThresholdFarCorners - params.entryFarCorners),
+ horizontalTranslationStretch =
+ bgStretch * (params.preThresholdMargin - params.entryMargin)
+ )
+ }
+
+ fun setBackAnimation(backAnimation: BackAnimation?) {
+ this.backAnimation = backAnimation
+ initializeBackAnimation()
+ }
+
+ private fun initializeBackAnimation() {
+ backAnimation?.setSwipeThresholds(
+ params.swipeTriggerThreshold,
+ params.swipeProgressThreshold
+ )
+ }
+
+ override fun onDestroy() {
+ cancelFailsafe()
+ windowManager.removeView(mView)
+ }
+
+ override fun setIsLeftPanel(isLeftPanel: Boolean) {
+ mView.isLeftPanel = isLeftPanel
+ layoutParams.gravity = if (isLeftPanel) {
+ Gravity.LEFT or Gravity.TOP
+ } else {
+ Gravity.RIGHT or Gravity.TOP
+ }
+ }
+
+ override fun setInsets(insetLeft: Int, insetRight: Int) {
+ }
+
+ override fun setBackCallback(callback: NavigationEdgeBackPlugin.BackCallback) {
+ backCallback = callback
+ }
+
+ override fun setLayoutParams(layoutParams: WindowManager.LayoutParams) {
+ this.layoutParams = layoutParams
+ windowManager.addView(mView, layoutParams)
+ }
+
+ private fun isFlung() = velocityTracker!!.run {
+ computeCurrentVelocity(1000)
+ abs(xVelocity) > MIN_FLING_VELOCITY
+ }
+
+ private fun playFlingBackAnimation() {
+ playAnimation(setCommittedEndListener)
+ }
+
+ private fun playCommitBackAnimation() {
+ // Check if we should vibrate again
+ if (previousState != GestureState.FLUNG) {
+ backCallback.triggerBack()
+ 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)
+ }
+ }
+ 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)) {
+ scheduleFailsafe()
+ }
+ }
+
+ private fun resetOnDown() {
+ hasHapticPlayed = false
+ totalTouchDelta = 0f
+ vibrationTime = 0
+ cancelFailsafe()
+ backAnimation?.setTriggerBack(false)
+ }
+
+ private fun updateYPosition(touchY: Float) {
+ var yPosition = touchY - params.fingerOffset
+ yPosition = Math.max(yPosition, params.minArrowYPosition.toFloat())
+ yPosition -= layoutParams.height / 2.0f
+ layoutParams.y = constrain(yPosition.toInt(), 0, displaySize.y)
+ }
+
+ override fun setDisplaySize(displaySize: Point) {
+ this.displaySize.set(displaySize.x, displaySize.y)
+ screenSize = Math.min(displaySize.x, displaySize.y)
+ }
+
+ /**
+ * Updates resting arrow and background size not accounting for stretch
+ */
+ private fun updateRestingArrowDimens(animated: Boolean, currentState: GestureState) {
+ mView.updateRestingArrowDimens(
+ backgroundWidth =
+ when (currentState) {
+ GestureState.GONE, GestureState.ENTRY -> params.entryBackgroundWidth
+ else -> params.activeBackgroundWidth
+ },
+ backgroundHeight =
+ when (currentState) {
+ GestureState.GONE, GestureState.ENTRY -> params.entryBackgroundHeight
+ else -> params.activeBackgroundHeight
+ },
+ backgroundEdgeCornerRadius =
+ when (currentState) {
+ GestureState.GONE, GestureState.ENTRY, GestureState.INACTIVE ->
+ params.entryEdgeCorners
+ else ->
+ params.activeEdgeCorners
+ },
+ backgroundDragCornerRadius =
+ when (currentState) {
+ GestureState.GONE, GestureState.ENTRY -> params.entryFarCorners
+ else -> params.activeFarCorners
+ },
+ arrowLength =
+ when (currentState) {
+ GestureState.ACTIVE, GestureState.INACTIVE, GestureState.COMMITTED,
+ GestureState.FLUNG -> params.activeArrowLength
+ GestureState.CANCELLED -> params.cancelledArrowLength
+ GestureState.GONE, GestureState.ENTRY -> params.entryArrowLength
+ },
+ arrowHeight =
+ when (currentState) {
+ GestureState.ACTIVE, GestureState.INACTIVE, GestureState.COMMITTED,
+ GestureState.FLUNG -> params.activeArrowHeight
+ GestureState.CANCELLED -> params.cancelledArrowHeight
+ GestureState.GONE, GestureState.ENTRY -> params.entryArrowHeight
+ },
+ horizontalTranslation =
+ when (currentState) {
+ GestureState.GONE -> -params.activeBackgroundWidth
+ // Position the cancelled/committed arrow slightly further off the screen so we
+ // do not see part of it bouncing
+ GestureState.CANCELLED, GestureState.COMMITTED ->
+ -params.activeBackgroundWidth * 1.5f
+ GestureState.FLUNG -> params.stretchMargin
+ GestureState.ACTIVE -> params.activeMargin
+ GestureState.ENTRY, GestureState.INACTIVE -> params.entryMargin
+ },
+ animate = animated
+ )
+ if (animated) {
+ when (currentState) {
+ GestureState.ENTRY, GestureState.ACTIVE, GestureState.FLUNG ->
+ mView.setArrowStiffness(ARROW_APPEAR_STIFFNESS, ARROW_APPEAR_DAMPING_RATIO)
+ else ->
+ mView.setArrowStiffness(
+ ARROW_DISAPPEAR_STIFFNESS, ARROW_DISAPPEAR_DAMPING_RATIO)
+ }
+ }
+ }
+
+ /**
+ * Update arrow state. If state has not changed, this is a no-op.
+ *
+ * Transitioning to active/inactive will indicate whether or not releasing touch will trigger
+ * the back action.
+ */
+ 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
+ mView.visibility = if (currentState == GestureState.GONE) View.GONE else View.VISIBLE
+
+ when (currentState) {
+ // Transitioning to GONE never animates since the arrow is (presumably) already off the
+ // screen
+ GestureState.GONE -> updateRestingArrowDimens(animated = false, currentState)
+ GestureState.ENTRY -> {
+ updateYPosition(startY)
+ updateRestingArrowDimens(animated = true, currentState)
+ }
+ GestureState.ACTIVE -> {
+ backAnimation?.setTriggerBack(true)
+ updateRestingArrowDimens(animated = true, currentState)
+ // Vibrate the first time we transition to ACTIVE
+ if (!hasHapticPlayed) {
+ hasHapticPlayed = true
+ vibrationTime = SystemClock.uptimeMillis()
+ vibratorHelper.vibrate(VibrationEffect.EFFECT_TICK)
+ }
+ }
+ GestureState.INACTIVE -> {
+ backAnimation?.setTriggerBack(false)
+ updateRestingArrowDimens(animated = true, currentState)
+ }
+ GestureState.FLUNG -> playFlingBackAnimation()
+ GestureState.COMMITTED -> playCommitBackAnimation()
+ GestureState.CANCELLED -> playCancelBackAnimation()
+ }
+ }
+
+ private fun scheduleFailsafe() {
+ if (!ENABLE_FAILSAFE) return
+ cancelFailsafe()
+ if (DEBUG) Log.d(TAG, "scheduleFailsafe")
+ mainHandler.postDelayed(failsafeRunnable, FAILSAFE_DELAY_MS)
+ }
+
+ private fun cancelFailsafe() {
+ if (DEBUG) Log.d(TAG, "cancelFailsafe")
+ mainHandler.removeCallbacks(failsafeRunnable)
+ }
+
+ private fun onFailsafe() {
+ if (DEBUG) Log.d(TAG, "onFailsafe")
+ updateArrowState(GestureState.GONE, force = true)
+ }
+
+ override fun dump(pw: PrintWriter) {
+ pw.println("$TAG:")
+ pw.println(" currentState=$currentState")
+ pw.println(" isLeftPanel=$mView.isLeftPanel")
+ }
+
+ 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)}%"
+ )
+ 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
+ )
+ debugPaint.apply {
+ color = Color.BLACK
+ textSize = 32f
+ }
+ var offset = 32f
+ for (debugText in debugStrings) {
+ canvas.drawText(debugText, 10f, offset, debugPaint)
+ offset += 32f
+ }
+ debugPaint.apply {
+ color = Color.RED
+ style = Paint.Style.STROKE
+ strokeWidth = 4f
+ }
+ val canvasWidth = canvas.width.toFloat()
+ val canvasHeight = canvas.height.toFloat()
+ canvas.drawRect(0f, 0f, canvasWidth, canvasHeight, debugPaint)
+
+ fun drawVerticalLine(x: Float, color: Int) {
+ debugPaint.color = color
+ val x = if (mView.isLeftPanel) x else canvasWidth - x
+ canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint)
+ }
+
+ drawVerticalLine(x = params.swipeTriggerThreshold, color = Color.BLUE)
+ drawVerticalLine(x = startX, color = Color.GREEN)
+ drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 001a4628916f3..022f9e5c066b1 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -62,6 +62,8 @@ import com.android.systemui.R;
import com.android.systemui.SystemUIFactory;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationBarView;
import com.android.systemui.navigationbar.NavigationModeController;
@@ -183,6 +185,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
private final PluginManager mPluginManager;
private final ProtoTracer mProtoTracer;
private final NavigationModeController mNavigationModeController;
+ private final BackPanelController.Factory mBackPanelControllerFactory;
private final ViewConfiguration mViewConfiguration;
private final WindowManager mWindowManager;
private final IWindowManager mWindowManagerService;
@@ -200,6 +203,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
private final Region mExcludeRegion = new Region();
private final Region mUnrestrictedExcludeRegion = new Region();
private final LatencyTracker mLatencyTracker;
+ private final FeatureFlags mFeatureFlags;
// The left side edge width where touch down is allowed
private int mEdgeWidthLeft;
@@ -231,6 +235,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
private boolean mIsBackGestureAllowed;
private boolean mGestureBlockingActivityRunning;
private boolean mIsInPipMode;
+ private boolean mIsPredictiveBackAnimEnabled;
private InputMonitor mInputMonitor;
private InputChannelCompat.InputEventReceiver mInputEventReceiver;
@@ -299,12 +304,22 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
};
- EdgeBackGestureHandler(Context context, OverviewProxyService overviewProxyService,
- SysUiState sysUiState, PluginManager pluginManager, @Main Executor executor,
- BroadcastDispatcher broadcastDispatcher, ProtoTracer protoTracer,
- NavigationModeController navigationModeController, ViewConfiguration viewConfiguration,
- WindowManager windowManager, IWindowManager windowManagerService,
- FalsingManager falsingManager, LatencyTracker latencyTracker) {
+ EdgeBackGestureHandler(
+ Context context,
+ OverviewProxyService overviewProxyService,
+ SysUiState sysUiState,
+ PluginManager pluginManager,
+ @Main Executor executor,
+ BroadcastDispatcher broadcastDispatcher,
+ ProtoTracer protoTracer,
+ NavigationModeController navigationModeController,
+ BackPanelController.Factory backPanelControllerFactory,
+ ViewConfiguration viewConfiguration,
+ WindowManager windowManager,
+ IWindowManager windowManagerService,
+ FalsingManager falsingManager,
+ LatencyTracker latencyTracker,
+ FeatureFlags featureFlags) {
super(broadcastDispatcher);
mContext = context;
mDisplayId = context.getDisplayId();
@@ -314,11 +329,13 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
mPluginManager = pluginManager;
mProtoTracer = protoTracer;
mNavigationModeController = navigationModeController;
+ mBackPanelControllerFactory = backPanelControllerFactory;
mViewConfiguration = viewConfiguration;
mWindowManager = windowManager;
mWindowManagerService = windowManagerService;
mFalsingManager = falsingManager;
mLatencyTracker = latencyTracker;
+ mFeatureFlags = featureFlags;
ComponentName recentsComponentName = ComponentName.unflattenFromString(
context.getString(com.android.internal.R.string.config_recentsComponentName));
if (recentsComponentName != null) {
@@ -508,8 +525,9 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
Choreographer.getInstance(), this::onInputEvent);
// Add a nav bar panel window
- setEdgeBackPlugin(
- new NavigationBarEdgePanel(mContext, mBackAnimation, mLatencyTracker));
+ mIsPredictiveBackAnimEnabled =
+ mFeatureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_ANIM);
+ resetEdgeBackPlugin();
mPluginManager.addPluginListener(
this, NavigationEdgeBackPlugin.class, /*allowMultiple=*/ false);
}
@@ -524,7 +542,17 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
@Override
public void onPluginDisconnected(NavigationEdgeBackPlugin plugin) {
- setEdgeBackPlugin(new NavigationBarEdgePanel(mContext, mBackAnimation, mLatencyTracker));
+ resetEdgeBackPlugin();
+ }
+
+ private void resetEdgeBackPlugin() {
+ if (mIsPredictiveBackAnimEnabled) {
+ setEdgeBackPlugin(
+ mBackPanelControllerFactory.create(mContext, mBackAnimation));
+ } else {
+ setEdgeBackPlugin(
+ new NavigationBarEdgePanel(mContext, mBackAnimation, mLatencyTracker));
+ }
}
private void setEdgeBackPlugin(NavigationEdgeBackPlugin edgeBackPlugin) {
@@ -949,8 +977,12 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
public void setBackAnimation(BackAnimation backAnimation) {
mBackAnimation = backAnimation;
- if (mEdgeBackPlugin != null && mEdgeBackPlugin instanceof NavigationBarEdgePanel) {
- ((NavigationBarEdgePanel) mEdgeBackPlugin).setBackAnimation(backAnimation);
+ if (mEdgeBackPlugin != null) {
+ if (mEdgeBackPlugin instanceof NavigationBarEdgePanel) {
+ ((NavigationBarEdgePanel) mEdgeBackPlugin).setBackAnimation(backAnimation);
+ } else if (mEdgeBackPlugin instanceof BackPanelController) {
+ ((BackPanelController) mEdgeBackPlugin).setBackAnimation(backAnimation);
+ }
}
}
@@ -968,20 +1000,29 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
private final BroadcastDispatcher mBroadcastDispatcher;
private final ProtoTracer mProtoTracer;
private final NavigationModeController mNavigationModeController;
+ private final BackPanelController.Factory mBackPanelControllerFactory;
private final ViewConfiguration mViewConfiguration;
private final WindowManager mWindowManager;
private final IWindowManager mWindowManagerService;
private final FalsingManager mFalsingManager;
private final LatencyTracker mLatencyTracker;
+ private final FeatureFlags mFeatureFlags;
@Inject
public Factory(OverviewProxyService overviewProxyService,
- SysUiState sysUiState, PluginManager pluginManager, @Main Executor executor,
- BroadcastDispatcher broadcastDispatcher, ProtoTracer protoTracer,
- NavigationModeController navigationModeController,
- ViewConfiguration viewConfiguration, WindowManager windowManager,
- IWindowManager windowManagerService, FalsingManager falsingManager,
- LatencyTracker latencyTracker) {
+ SysUiState sysUiState,
+ PluginManager pluginManager,
+ @Main Executor executor,
+ BroadcastDispatcher broadcastDispatcher,
+ ProtoTracer protoTracer,
+ NavigationModeController navigationModeController,
+ BackPanelController.Factory backPanelControllerFactory,
+ ViewConfiguration viewConfiguration,
+ WindowManager windowManager,
+ IWindowManager windowManagerService,
+ FalsingManager falsingManager,
+ LatencyTracker latencyTracker,
+ FeatureFlags featureFlags) {
mOverviewProxyService = overviewProxyService;
mSysUiState = sysUiState;
mPluginManager = pluginManager;
@@ -989,19 +1030,33 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
mBroadcastDispatcher = broadcastDispatcher;
mProtoTracer = protoTracer;
mNavigationModeController = navigationModeController;
+ mBackPanelControllerFactory = backPanelControllerFactory;
mViewConfiguration = viewConfiguration;
mWindowManager = windowManager;
mWindowManagerService = windowManagerService;
mFalsingManager = falsingManager;
mLatencyTracker = latencyTracker;
+ mFeatureFlags = featureFlags;
}
/** Construct a {@link EdgeBackGestureHandler}. */
public EdgeBackGestureHandler create(Context context) {
- return new EdgeBackGestureHandler(context, mOverviewProxyService, mSysUiState,
- mPluginManager, mExecutor, mBroadcastDispatcher, mProtoTracer,
- mNavigationModeController, mViewConfiguration, mWindowManager,
- mWindowManagerService, mFalsingManager, mLatencyTracker);
+ return new EdgeBackGestureHandler(
+ context,
+ mOverviewProxyService,
+ mSysUiState,
+ mPluginManager,
+ mExecutor,
+ mBroadcastDispatcher,
+ mProtoTracer,
+ mNavigationModeController,
+ mBackPanelControllerFactory,
+ mViewConfiguration,
+ mWindowManager,
+ mWindowManagerService,
+ mFalsingManager,
+ mLatencyTracker,
+ mFeatureFlags);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
new file mode 100644
index 0000000000000..51566b0f8393f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
@@ -0,0 +1,139 @@
+package com.android.systemui.navigationbar.gestural
+
+import android.content.res.Resources
+import com.android.systemui.R
+
+data class EdgePanelParams(private var resources: Resources) {
+ var arrowThickness: Float = 0f
+ private set
+ var entryArrowLength: Float = 0f
+ private set
+ var entryArrowHeight: Float = 0f
+ private set
+ var activeArrowLength: Float = 0f
+ private set
+ var activeArrowHeight: Float = 0f
+ private set
+ var stretchedArrowLength: Float = 0f
+ private set
+ var stretchedArrowHeight: Float = 0f
+ private set
+ var cancelledArrowLength: Float = 0f
+ private set
+ var cancelledArrowHeight: Float = 0f
+ private set
+ var entryMargin: Float = 0f
+ private set
+ var entryBackgroundWidth: Float = 0f
+ private set
+ var entryBackgroundHeight: Float = 0f
+ private set
+ var entryEdgeCorners: Float = 0f
+ private set
+ var entryFarCorners: Float = 0f
+ private set
+ var preThresholdMargin: Float = 0f
+ private set
+ var preThresholdBackgroundWidth: Float = 0f
+ private set
+ var preThresholdBackgroundHeight: Float = 0f
+ private set
+ var preThresholdEdgeCorners: Float = 0f
+ private set
+ var preThresholdFarCorners: Float = 0f
+ private set
+ var activeMargin: Float = 0f
+ private set
+ var activeBackgroundWidth: Float = 0f
+ private set
+ var activeBackgroundHeight: Float = 0f
+ private set
+ var activeEdgeCorners: Float = 0f
+ private set
+ var activeFarCorners: Float = 0f
+ private set
+ var fullyStretchedThreshold: Float = 0f
+ private set
+ var stretchMargin: Float = 0f
+ private set
+ var stretchBackgroundWidth: Float = 0f
+ private set
+ var stretchBackgroundHeight: Float = 0f
+ private set
+ var stretchEdgeCorners: Float = 0f
+ private set
+ var stretchFarCorners: Float = 0f
+ private set
+
+ // navigation bar edge constants
+ var arrowPaddingEnd: Int = 0
+ private set
+
+ // The closest to y
+ var minArrowYPosition: Int = 0
+ private set
+ var fingerOffset: Int = 0
+ private set
+ var swipeTriggerThreshold: Float = 0f
+ private set
+ var swipeProgressThreshold: Float = 0f
+ private set
+
+ // The minimum delta needed to change direction / stop triggering back
+ var minDeltaForSwitch: Int = 0
+ private set
+
+ init {
+ update(resources)
+ }
+
+ private fun getDimen(id: Int): Float {
+ return resources.getDimension(id)
+ }
+
+ private fun getPx(id: Int): Int {
+ return resources.getDimensionPixelSize(id)
+ }
+
+ fun update(resources: Resources) {
+ this.resources = resources
+ arrowThickness = getDimen(R.dimen.navigation_edge_arrow_thickness)
+ entryArrowLength = getDimen(R.dimen.navigation_edge_entry_arrow_length)
+ entryArrowHeight = getDimen(R.dimen.navigation_edge_entry_arrow_height)
+ activeArrowLength = getDimen(R.dimen.navigation_edge_active_arrow_length)
+ activeArrowHeight = getDimen(R.dimen.navigation_edge_active_arrow_height)
+ stretchedArrowLength = getDimen(R.dimen.navigation_edge_stretched_arrow_length)
+ stretchedArrowHeight = getDimen(R.dimen.navigation_edge_stretched_arrow_height)
+ cancelledArrowLength = getDimen(R.dimen.navigation_edge_cancelled_arrow_length)
+ cancelledArrowHeight = getDimen(R.dimen.navigation_edge_cancelled_arrow_height)
+ entryMargin = getDimen(R.dimen.navigation_edge_entry_margin)
+ entryBackgroundWidth = getDimen(R.dimen.navigation_edge_entry_background_width)
+ entryBackgroundHeight = getDimen(R.dimen.navigation_edge_entry_background_height)
+ entryEdgeCorners = getDimen(R.dimen.navigation_edge_entry_edge_corners)
+ entryFarCorners = getDimen(R.dimen.navigation_edge_entry_far_corners)
+ preThresholdMargin = getDimen(R.dimen.navigation_edge_pre_threshold_margin)
+ preThresholdBackgroundWidth =
+ getDimen(R.dimen.navigation_edge_pre_threshold_background_width)
+ preThresholdBackgroundHeight =
+ getDimen(R.dimen.navigation_edge_pre_threshold_background_height)
+ preThresholdEdgeCorners = getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners)
+ preThresholdFarCorners = getDimen(R.dimen.navigation_edge_pre_threshold_far_corners)
+ activeMargin = getDimen(R.dimen.navigation_edge_active_margin)
+ activeBackgroundWidth = getDimen(R.dimen.navigation_edge_active_background_width)
+ activeBackgroundHeight = getDimen(R.dimen.navigation_edge_active_background_height)
+ activeEdgeCorners = getDimen(R.dimen.navigation_edge_active_edge_corners)
+ activeFarCorners = getDimen(R.dimen.navigation_edge_active_far_corners)
+ fullyStretchedThreshold = getDimen(R.dimen.navigation_edge_stretch_threshold)
+ stretchMargin = getDimen(R.dimen.navigation_edge_stretch_margin)
+ stretchBackgroundWidth = getDimen(R.dimen.navigation_edge_stretch_background_width)
+ stretchBackgroundHeight = getDimen(R.dimen.navigation_edge_stretch_background_height)
+ stretchEdgeCorners = getDimen(R.dimen.navigation_edge_stretch_left_corners)
+ stretchFarCorners = getDimen(R.dimen.navigation_edge_stretch_right_corners)
+ 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)
+ swipeProgressThreshold = getDimen(R.dimen.navigation_edge_action_progress_threshold)
+ minDeltaForSwitch = getPx(R.dimen.navigation_edge_minimum_x_delta_for_switch)
+ }
+}