Merge "Using View.performHapticFeedback on Back Panel Controller when the back gesture is handled" into udc-qpr-dev

This commit is contained in:
Juan Sebastian Martinez
2023-07-24 20:20:21 +00:00
committed by Android (Google) Code Review
2 changed files with 418 additions and 315 deletions

View File

@@ -26,6 +26,7 @@ import android.os.VibrationEffect
import android.util.Log
import android.util.MathUtils
import android.view.Gravity
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.ViewConfiguration
@@ -36,6 +37,8 @@ import androidx.core.view.isVisible
import androidx.dynamicanimation.animation.DynamicAnimation
import com.android.internal.util.LatencyTracker
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
import com.android.systemui.plugins.NavigationEdgeBackPlugin
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -76,27 +79,24 @@ private const val POP_ON_INACTIVE_TO_ACTIVE_VELOCITY = 4.7f
private const val POP_ON_INACTIVE_VELOCITY = -1.5f
internal val VIBRATE_ACTIVATED_EFFECT =
VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
internal val VIBRATE_DEACTIVATED_EFFECT =
VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)
VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)
private const val DEBUG = false
class BackPanelController internal constructor(
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>(
BackPanel(
context,
latencyTracker
)
), NavigationEdgeBackPlugin {
class BackPanelController
internal constructor(
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,
private val featureFlags: FeatureFlags
) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin {
/**
* Injectable instance to create a new BackPanelController.
@@ -104,34 +104,37 @@ class BackPanelController internal constructor(
* 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,
private val viewConfiguration: ViewConfiguration,
@Main private val mainHandler: Handler,
private val vibratorHelper: VibratorHelper,
private val configurationController: ConfigurationController,
private val latencyTracker: LatencyTracker
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 featureFlags: FeatureFlags
) {
/** Construct a [BackPanelController]. */
/** Construct a [BackPanelController]. */
fun create(context: Context): BackPanelController {
val backPanelController = BackPanelController(
val backPanelController =
BackPanelController(
context,
windowManager,
viewConfiguration,
mainHandler,
vibratorHelper,
configurationController,
latencyTracker
)
latencyTracker,
featureFlags
)
backPanelController.init()
return backPanelController
}
}
@VisibleForTesting
internal var params: EdgePanelParams = EdgePanelParams(resources)
@VisibleForTesting
internal var currentState: GestureState = GestureState.GONE
@VisibleForTesting internal var params: EdgePanelParams = EdgePanelParams(resources)
@VisibleForTesting internal var currentState: GestureState = GestureState.GONE
private var previousState: GestureState = GestureState.GONE
// Screen attributes
@@ -167,7 +170,6 @@ class BackPanelController internal constructor(
private val elapsedTimeSinceEntry
get() = SystemClock.uptimeMillis() - gestureEntryTime
private var pastThresholdWhileEntryOrInactiveTime = 0L
private var entryToActiveDelay = 0F
private val entryToActiveDelayCalculation = {
@@ -206,24 +208,25 @@ class BackPanelController internal constructor(
COMMITTED,
/* back action currently cancelling, arrow soon to be GONE */
CANCELLED;
CANCELLED
}
/**
* Wrapper around OnAnimationEndListener which runs the given runnable after a delay. The
* runnable is not called if the animation is cancelled
*/
inner class DelayedOnAnimationEndListener internal constructor(
private val handler: Handler,
private val runnableDelay: Long,
val runnable: Runnable,
inner class DelayedOnAnimationEndListener
internal constructor(
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)
@@ -239,45 +242,43 @@ class BackPanelController internal constructor(
internal fun run() = runnable.run()
}
private val onEndSetCommittedStateListener = DelayedOnAnimationEndListener(mainHandler, 0L) {
updateArrowState(GestureState.COMMITTED)
}
private val onEndSetCommittedStateListener =
DelayedOnAnimationEndListener(mainHandler, 0L) { updateArrowState(GestureState.COMMITTED) }
private val onEndSetGoneStateListener =
DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) {
cancelFailsafe()
updateArrowState(GestureState.GONE)
}
private val onAlphaEndSetGoneStateListener = DelayedOnAnimationEndListener(mainHandler, 0L) {
updateRestingArrowDimens()
if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) {
scheduleFailsafe()
DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) {
cancelFailsafe()
updateArrowState(GestureState.GONE)
}
private val onAlphaEndSetGoneStateListener =
DelayedOnAnimationEndListener(mainHandler, 0L) {
updateRestingArrowDimens()
if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) {
scheduleFailsafe()
}
}
}
// Minimum of the screen's width or the predefined threshold
private var fullyStretchedThreshold = 0f
/**
* Used for initialization and configuration changes
*/
/** Used for initialization and configuration changes */
private fun updateConfiguration() {
params.update(resources)
mView.updateArrowPaint(params.arrowThickness)
minFlingDistance = viewConfiguration.scaledTouchSlop * 3
}
private val configurationListener = object : ConfigurationController.ConfigurationListener {
override fun onConfigChanged(newConfig: Configuration?) {
updateConfiguration()
}
private val configurationListener =
object : ConfigurationController.ConfigurationListener {
override fun onConfigChanged(newConfig: Configuration?) {
updateConfiguration()
}
override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) {
updateArrowDirection(isLayoutRtl)
override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) {
updateArrowDirection(isLayoutRtl)
}
}
}
override fun onViewAttached() {
updateConfiguration()
@@ -320,8 +321,9 @@ class BackPanelController internal constructor(
MotionEvent.ACTION_UP -> {
when (currentState) {
GestureState.ENTRY -> {
if (isFlungAwayFromEdge(endX = event.x) ||
previousXTranslation > params.staticTriggerThreshold
if (
isFlungAwayFromEdge(endX = event.x) ||
previousXTranslation > params.staticTriggerThreshold
) {
updateArrowState(GestureState.FLUNG)
} else {
@@ -342,14 +344,16 @@ class BackPanelController internal constructor(
}
}
GestureState.ACTIVE -> {
if (previousState == GestureState.ENTRY &&
elapsedTimeSinceEntry
< MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING
if (
previousState == GestureState.ENTRY &&
elapsedTimeSinceEntry <
MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING
) {
updateArrowState(GestureState.FLUNG)
} else if (previousState == GestureState.INACTIVE &&
elapsedTimeSinceInactive
< MIN_DURATION_INACTIVE_TO_ACTIVE_CONSIDERED_AS_FLING
} else if (
previousState == GestureState.INACTIVE &&
elapsedTimeSinceInactive <
MIN_DURATION_INACTIVE_TO_ACTIVE_CONSIDERED_AS_FLING
) {
// A delay is added to allow the background to transition back to ACTIVE
// since it was briefly in INACTIVE. Without this delay, setting it
@@ -390,10 +394,10 @@ class BackPanelController internal constructor(
}
/**
* Returns false until the current gesture exceeds the touch slop threshold,
* and returns true thereafter (we reset on the subsequent back gesture).
* The moment it switches from false -> true is important,
* because that's when we switch state, from GONE -> ENTRY.
* Returns false until the current gesture exceeds the touch slop threshold, and returns true
* thereafter (we reset on the subsequent back gesture). The moment it switches from false ->
* true is important, because that's when we switch state, from GONE -> ENTRY.
*
* @return whether the current gesture has moved past a minimum threshold.
*/
private fun dragSlopExceeded(curX: Float, startX: Float): Boolean {
@@ -416,7 +420,8 @@ class BackPanelController internal constructor(
val isPastStaticThreshold = xTranslation > params.staticTriggerThreshold
when (currentState) {
GestureState.ENTRY -> {
if (isPastThresholdToActive(
if (
isPastThresholdToActive(
isPastThreshold = isPastStaticThreshold,
dynamicDelay = entryToActiveDelayCalculation
)
@@ -428,8 +433,10 @@ class BackPanelController internal constructor(
val isPastDynamicReactivationThreshold =
totalTouchDeltaInactive >= params.reactivationTriggerThreshold
if (isPastThresholdToActive(
isPastThreshold = isPastStaticThreshold &&
if (
isPastThresholdToActive(
isPastThreshold =
isPastStaticThreshold &&
isPastDynamicReactivationThreshold &&
isWithinYActivationThreshold,
delay = MIN_DURATION_INACTIVE_BEFORE_ACTIVE_ANIMATION
@@ -489,19 +496,19 @@ class BackPanelController internal constructor(
// Add a slop to to prevent small jitters when arrow is at edge in
// emitting small values that cause the arrow to poke out slightly
val minimumDelta = -viewConfiguration.scaledTouchSlop.toFloat()
totalTouchDeltaInactive = totalTouchDeltaInactive
.plus(xDelta)
.coerceAtLeast(minimumDelta)
totalTouchDeltaInactive =
totalTouchDeltaInactive.plus(xDelta).coerceAtLeast(minimumDelta)
}
updateArrowStateOnMove(yTranslation, xTranslation)
val gestureProgress = when (currentState) {
GestureState.ACTIVE -> fullScreenProgress(xTranslation)
GestureState.ENTRY -> staticThresholdProgress(xTranslation)
GestureState.INACTIVE -> reactivationThresholdProgress(totalTouchDeltaInactive)
else -> null
}
val gestureProgress =
when (currentState) {
GestureState.ACTIVE -> fullScreenProgress(xTranslation)
GestureState.ENTRY -> staticThresholdProgress(xTranslation)
GestureState.INACTIVE -> reactivationThresholdProgress(totalTouchDeltaInactive)
else -> null
}
gestureProgress?.let {
when (currentState) {
@@ -517,27 +524,30 @@ class BackPanelController internal constructor(
}
private fun setArrowStrokeAlpha(gestureProgress: Float?) {
val strokeAlphaProgress = when (currentState) {
GestureState.ENTRY -> gestureProgress
GestureState.INACTIVE -> gestureProgress
GestureState.ACTIVE,
GestureState.FLUNG,
GestureState.COMMITTED -> 1f
GestureState.CANCELLED,
GestureState.GONE -> 0f
}
val strokeAlphaProgress =
when (currentState) {
GestureState.ENTRY -> gestureProgress
GestureState.INACTIVE -> gestureProgress
GestureState.ACTIVE,
GestureState.FLUNG,
GestureState.COMMITTED -> 1f
GestureState.CANCELLED,
GestureState.GONE -> 0f
}
val indicator = when (currentState) {
GestureState.ENTRY -> params.entryIndicator
GestureState.INACTIVE -> params.preThresholdIndicator
GestureState.ACTIVE -> params.activeIndicator
else -> params.preThresholdIndicator
}
val indicator =
when (currentState) {
GestureState.ENTRY -> params.entryIndicator
GestureState.INACTIVE -> params.preThresholdIndicator
GestureState.ACTIVE -> params.activeIndicator
else -> params.preThresholdIndicator
}
strokeAlphaProgress?.let { progress ->
indicator.arrowDimens.alphaSpring?.get(progress)?.takeIf { it.isNewState }?.let {
mView.popArrowAlpha(0f, it.value)
}
indicator.arrowDimens.alphaSpring
?.get(progress)
?.takeIf { it.isNewState }
?.let { mView.popArrowAlpha(0f, it.value) }
}
}
@@ -546,15 +556,16 @@ class BackPanelController internal constructor(
val maxYOffset = (mView.height - params.entryIndicator.backgroundDimens.height) / 2f
val rubberbandAmount = 15f
val yProgress = MathUtils.saturate(yTranslation / (maxYOffset * rubberbandAmount))
val yPosition = params.verticalTranslationInterpolator.getInterpolation(yProgress) *
val yPosition =
params.verticalTranslationInterpolator.getInterpolation(yProgress) *
maxYOffset *
sign(yOffset)
mView.animateVertically(yPosition)
}
/**
* 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)
* 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 fullScreenProgress(xTranslation: Float): Float {
val progress = (xTranslation - previousXTranslationOnActiveOffset) / fullyStretchedThreshold
@@ -575,35 +586,32 @@ class BackPanelController internal constructor(
private fun stretchActiveBackIndicator(progress: Float) {
mView.setStretch(
horizontalTranslationStretchAmount = params.horizontalTranslationInterpolator
.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
horizontalTranslationStretchAmount =
params.horizontalTranslationInterpolator.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 = params.arrowAngleInterpolator
.getInterpolation(progress),
backgroundWidthStretchAmount = params.entryWidthInterpolator
.getInterpolation(progress),
backgroundHeightStretchAmount = params.heightInterpolator
.getInterpolation(progress),
backgroundAlphaStretchAmount = 1f,
arrowAlphaStretchAmount = params.entryIndicator.arrowDimens
.alphaInterpolator?.get(progress)?.value ?: 0f,
edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
fullyStretchedDimens = params.preThresholdIndicator
horizontalTranslationStretchAmount = 0f,
arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
backgroundWidthStretchAmount = params.entryWidthInterpolator.getInterpolation(progress),
backgroundHeightStretchAmount = params.heightInterpolator.getInterpolation(progress),
backgroundAlphaStretchAmount = 1f,
arrowAlphaStretchAmount =
params.entryIndicator.arrowDimens.alphaInterpolator?.get(progress)?.value ?: 0f,
edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
fullyStretchedDimens = params.preThresholdIndicator
)
}
@@ -612,31 +620,32 @@ class BackPanelController internal constructor(
val interpolator = run {
val isPastSlop = totalTouchDeltaInactive > viewConfiguration.scaledTouchSlop
if (isPastSlop) {
if (totalTouchDeltaInactive > 0) {
params.entryWidthInterpolator
if (totalTouchDeltaInactive > 0) {
params.entryWidthInterpolator
} else {
params.entryWidthTowardsEdgeInterpolator
}
} else {
params.entryWidthTowardsEdgeInterpolator
previousPreThresholdWidthInterpolator
}
} else {
previousPreThresholdWidthInterpolator
}.also { previousPreThresholdWidthInterpolator = it }
.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.preThresholdIndicator.arrowDimens
.alphaInterpolator?.get(progress)?.value ?: 0f,
edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
fullyStretchedDimens = params.preThresholdIndicator
horizontalTranslationStretchAmount = 0f,
arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress),
backgroundHeightStretchAmount = params.heightInterpolator.getInterpolation(progress),
backgroundAlphaStretchAmount = 1f,
arrowAlphaStretchAmount =
params.preThresholdIndicator.arrowDimens.alphaInterpolator?.get(progress)?.value
?: 0f,
edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
fullyStretchedDimens = params.preThresholdIndicator
)
}
@@ -647,11 +656,12 @@ class BackPanelController internal constructor(
override fun setIsLeftPanel(isLeftPanel: Boolean) {
mView.isLeftPanel = isLeftPanel
layoutParams.gravity = if (isLeftPanel) {
Gravity.LEFT or Gravity.TOP
} else {
Gravity.RIGHT or Gravity.TOP
}
layoutParams.gravity =
if (isLeftPanel) {
Gravity.LEFT or Gravity.TOP
} else {
Gravity.RIGHT or Gravity.TOP
}
}
override fun setInsets(insetLeft: Int, insetRight: Int) = Unit
@@ -667,12 +677,14 @@ class BackPanelController internal constructor(
private fun isFlungAwayFromEdge(endX: Float, startX: Float = touchDeltaStartX): Boolean {
val flingDistance = if (mView.isLeftPanel) endX - startX else startX - endX
val flingVelocity = velocityTracker?.run {
computeCurrentVelocity(PX_PER_SEC)
xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1)
} ?: 0f
val flingVelocity =
velocityTracker?.run {
computeCurrentVelocity(PX_PER_SEC)
xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1)
}
?: 0f
val isPastFlingVelocityThreshold =
flingVelocity > viewConfiguration.scaledMinimumFlingVelocity
flingVelocity > viewConfiguration.scaledMinimumFlingVelocity
return flingDistance > minFlingDistance && isPastFlingVelocityThreshold
}
@@ -699,8 +711,8 @@ class BackPanelController internal constructor(
}
private fun playWithBackgroundWidthAnimation(
onEnd: DelayedOnAnimationEndListener,
delay: Long = 0L
onEnd: DelayedOnAnimationEndListener,
delay: Long = 0L
) {
if (delay == 0L) {
updateRestingArrowDimens()
@@ -724,104 +736,103 @@ class BackPanelController internal constructor(
fullyStretchedThreshold = min(displaySize.x.toFloat(), params.swipeProgressThreshold)
}
/**
* Updates resting arrow and background size not accounting for stretch
*/
/** Updates resting arrow and background size not accounting for stretch */
private fun updateRestingArrowDimens() {
when (currentState) {
GestureState.GONE,
GestureState.ENTRY -> {
mView.setSpring(
arrowLength = params.entryIndicator.arrowDimens.lengthSpring,
arrowHeight = params.entryIndicator.arrowDimens.heightSpring,
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,
arrowLength = params.entryIndicator.arrowDimens.lengthSpring,
arrowHeight = params.entryIndicator.arrowDimens.heightSpring,
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,
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,
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,
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,
backgroundAlpha = params.committedIndicator.backgroundDimens.alphaSpring,
backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring,
backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring,
backgroundEdgeCornerRadius = params.committedIndicator.backgroundDimens
.edgeCornerRadiusSpring,
backgroundFarCornerRadius = params.committedIndicator.backgroundDimens
.farCornerRadiusSpring,
arrowLength = params.committedIndicator.arrowDimens.lengthSpring,
arrowHeight = params.committedIndicator.arrowDimens.heightSpring,
scale = params.committedIndicator.scaleSpring,
backgroundAlpha = params.committedIndicator.backgroundDimens.alphaSpring,
backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring,
backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring,
backgroundEdgeCornerRadius =
params.committedIndicator.backgroundDimens.edgeCornerRadiusSpring,
backgroundFarCornerRadius =
params.committedIndicator.backgroundDimens.farCornerRadiusSpring,
)
}
GestureState.CANCELLED -> {
mView.setSpring(
backgroundAlpha = params.cancelledIndicator.backgroundDimens.alphaSpring)
backgroundAlpha = params.cancelledIndicator.backgroundDimens.alphaSpring
)
}
else -> {}
}
mView.setRestingDimens(
animate = !(currentState == GestureState.FLUNG ||
currentState == GestureState.COMMITTED),
restingParams = EdgePanelParams.BackIndicatorDimens(
scale = when (currentState) {
animate =
!(currentState == GestureState.FLUNG || currentState == GestureState.COMMITTED),
restingParams =
EdgePanelParams.BackIndicatorDimens(
scale =
when (currentState) {
GestureState.ACTIVE,
GestureState.FLUNG,
-> params.activeIndicator.scale
GestureState.FLUNG, -> params.activeIndicator.scale
GestureState.COMMITTED -> params.committedIndicator.scale
else -> params.preThresholdIndicator.scale
},
scalePivotX = when (currentState) {
scalePivotX =
when (currentState) {
GestureState.GONE,
GestureState.ENTRY,
GestureState.INACTIVE,
@@ -830,7 +841,8 @@ class BackPanelController internal constructor(
GestureState.FLUNG,
GestureState.COMMITTED -> params.committedIndicator.scalePivotX
},
horizontalTranslation = when (currentState) {
horizontalTranslation =
when (currentState) {
GestureState.GONE -> {
params.activeIndicator.backgroundDimens.width?.times(-1)
}
@@ -843,7 +855,8 @@ class BackPanelController internal constructor(
}
else -> null
},
arrowDimens = when (currentState) {
arrowDimens =
when (currentState) {
GestureState.GONE,
GestureState.ENTRY,
GestureState.INACTIVE -> params.entryIndicator.arrowDimens
@@ -852,7 +865,8 @@ class BackPanelController internal constructor(
GestureState.COMMITTED -> params.committedIndicator.arrowDimens
GestureState.CANCELLED -> params.cancelledIndicator.arrowDimens
},
backgroundDimens = when (currentState) {
backgroundDimens =
when (currentState) {
GestureState.GONE,
GestureState.ENTRY,
GestureState.INACTIVE -> params.entryIndicator.backgroundDimens
@@ -894,7 +908,7 @@ class BackPanelController internal constructor(
GestureState.ACTIVE -> {
backCallback.setTriggerBack(true)
}
GestureState.GONE -> { }
GestureState.GONE -> {}
}
when (currentState) {
@@ -913,18 +927,25 @@ class BackPanelController internal constructor(
GestureState.ACTIVE -> {
previousXTranslationOnActiveOffset = previousXTranslation
updateRestingArrowDimens()
vibratorHelper.cancel()
mainHandler.postDelayed(10L) {
vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT)
}
val popVelocity = if (previousState == GestureState.INACTIVE) {
POP_ON_INACTIVE_TO_ACTIVE_VELOCITY
if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
vibratorHelper.performHapticFeedback(
mView,
HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE
)
} else {
POP_ON_ENTRY_TO_ACTIVE_VELOCITY
vibratorHelper.cancel()
mainHandler.postDelayed(10L) {
vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT)
}
}
val popVelocity =
if (previousState == GestureState.INACTIVE) {
POP_ON_INACTIVE_TO_ACTIVE_VELOCITY
} else {
POP_ON_ENTRY_TO_ACTIVE_VELOCITY
}
mView.popOffEdge(popVelocity)
}
GestureState.INACTIVE -> {
gestureInactiveTime = SystemClock.uptimeMillis()
@@ -937,7 +958,14 @@ class BackPanelController internal constructor(
mView.popOffEdge(POP_ON_INACTIVE_VELOCITY)
vibratorHelper.vibrate(VIBRATE_DEACTIVATED_EFFECT)
if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
vibratorHelper.performHapticFeedback(
mView,
HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE
)
} else {
vibratorHelper.vibrate(VIBRATE_DEACTIVATED_EFFECT)
}
updateRestingArrowDimens()
}
GestureState.FLUNG -> {
@@ -945,8 +973,10 @@ class BackPanelController internal constructor(
mView.popScale(POP_ON_FLING_VELOCITY)
}
updateRestingArrowDimens()
mainHandler.postDelayed(onEndSetCommittedStateListener.runnable,
MIN_DURATION_FLING_ANIMATION)
mainHandler.postDelayed(
onEndSetCommittedStateListener.runnable,
MIN_DURATION_FLING_ANIMATION
)
}
GestureState.COMMITTED -> {
// In most cases, animating between states is handled via `updateRestingArrowDimens`
@@ -956,36 +986,43 @@ class BackPanelController internal constructor(
// manually play these kinds of animations in parallel.
if (previousState == GestureState.FLUNG) {
updateRestingArrowDimens()
mainHandler.postDelayed(onEndSetGoneStateListener.runnable,
MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION)
mainHandler.postDelayed(
onEndSetGoneStateListener.runnable,
MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION
)
} else {
mView.popScale(POP_ON_COMMITTED_VELOCITY)
mainHandler.postDelayed(onAlphaEndSetGoneStateListener.runnable,
MIN_DURATION_COMMITTED_ANIMATION)
mainHandler.postDelayed(
onAlphaEndSetGoneStateListener.runnable,
MIN_DURATION_COMMITTED_ANIMATION
)
}
}
GestureState.CANCELLED -> {
val delay = max(0, MIN_DURATION_CANCELLED_ANIMATION - elapsedTimeSinceEntry)
playWithBackgroundWidthAnimation(onEndSetGoneStateListener, delay)
val springForceOnCancelled = params.cancelledIndicator
.arrowDimens.alphaSpring?.get(0f)?.value
val springForceOnCancelled =
params.cancelledIndicator.arrowDimens.alphaSpring?.get(0f)?.value
mView.popArrowAlpha(0f, springForceOnCancelled)
mainHandler.postDelayed(10L) { vibratorHelper.cancel() }
if (!featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION))
mainHandler.postDelayed(10L) { vibratorHelper.cancel() }
}
}
}
private fun convertVelocityToAnimationFactor(
valueOnFastVelocity: Float,
valueOnSlowVelocity: Float,
fastVelocityBound: Float = 1f,
slowVelocityBound: Float = 0.5f,
valueOnFastVelocity: Float,
valueOnSlowVelocity: Float,
fastVelocityBound: Float = 1f,
slowVelocityBound: Float = 0.5f,
): Float {
val factor = velocityTracker?.run {
computeCurrentVelocity(PX_PER_MS)
MathUtils.smoothStep(slowVelocityBound, fastVelocityBound, abs(xVelocity))
} ?: valueOnFastVelocity
val factor =
velocityTracker?.run {
computeCurrentVelocity(PX_PER_MS)
MathUtils.smoothStep(slowVelocityBound, fastVelocityBound, abs(xVelocity))
}
?: valueOnFastVelocity
return MathUtils.lerp(valueOnFastVelocity, valueOnSlowVelocity, 1 - factor)
}
@@ -1014,77 +1051,76 @@ class BackPanelController internal constructor(
}
init {
if (DEBUG) mView.drawDebugInfo = { canvas ->
val debugStrings = listOf(
"$currentState",
"startX=$startX",
"startY=$startY",
"xDelta=${"%.1f".format(totalTouchDeltaActive)}",
"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(
if (DEBUG)
mView.drawDebugInfo = { canvas ->
val preProgress = staticThresholdProgress(previousXTranslation) * 100
val postProgress = fullScreenProgress(previousXTranslation) * 100
val debugStrings =
listOf(
"$currentState",
"startX=$startX",
"startY=$startY",
"xDelta=${"%.1f".format(totalTouchDeltaActive)}",
"xTranslation=${"%.1f".format(previousXTranslation)}",
"pre=${"%.0f".format(preProgress)}%",
"post=${"%.0f".format(postProgress)}%"
)
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)
)
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)
}
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.staticTriggerThreshold, color = Color.BLUE)
drawVerticalLine(x = params.deactivationTriggerThreshold, color = Color.BLUE)
drawVerticalLine(x = startX, color = Color.GREEN)
drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY)
}
drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE)
drawVerticalLine(x = params.deactivationTriggerThreshold, 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 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.
* 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<T>(
private val threshold: Float,
private val factor: Float = 1.1f,
private val postThreshold: T,
private val preThreshold: T
private val threshold: Float,
private val factor: Float = 1.1f,
private val postThreshold: T,
private val preThreshold: T
) {
data class Value<T>(val value: T, val isNewState: Boolean)

View File

@@ -20,6 +20,7 @@ import android.os.Handler
import android.os.Looper
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_MOVE
@@ -29,6 +30,8 @@ import android.view.WindowManager
import androidx.test.filters.SmallTest
import com.android.internal.util.LatencyTracker
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
import com.android.systemui.plugins.NavigationEdgeBackPlugin
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -36,6 +39,8 @@ import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.verify
@@ -58,6 +63,7 @@ class BackPanelControllerTest : SysuiTestCase() {
@Mock private lateinit var latencyTracker: LatencyTracker
@Mock private lateinit var layoutParams: WindowManager.LayoutParams
@Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
private val featureFlags = FakeFeatureFlags()
@Before
fun setup() {
@@ -70,7 +76,8 @@ class BackPanelControllerTest : SysuiTestCase() {
Handler.createAsync(Looper.myLooper()),
vibratorHelper,
configurationController,
latencyTracker
latencyTracker,
featureFlags
)
mBackPanelController.setLayoutParams(layoutParams)
mBackPanelController.setBackCallback(backCallback)
@@ -99,6 +106,7 @@ class BackPanelControllerTest : SysuiTestCase() {
@Test
fun handlesBackCommitted() {
featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false)
startTouch()
// Move once to cross the touch slop
continueTouch(START_X + touchSlop.toFloat() + 1)
@@ -121,8 +129,35 @@ class BackPanelControllerTest : SysuiTestCase() {
verify(backCallback).triggerBack()
}
@Test
fun handlesBackCommitted_withOneWayHapticsAPI() {
featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true)
startTouch()
// Move once to cross the touch slop
continueTouch(START_X + touchSlop.toFloat() + 1)
// Move again to cross the back trigger threshold
continueTouch(START_X + touchSlop + triggerThreshold + 1)
// Wait threshold duration and hold touch past trigger threshold
Thread.sleep((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong())
continueTouch(START_X + touchSlop + triggerThreshold + 1)
assertThat(mBackPanelController.currentState)
.isEqualTo(BackPanelController.GestureState.ACTIVE)
verify(backCallback).setTriggerBack(true)
testableLooper.moveTimeForward(100)
testableLooper.processAllMessages()
verify(vibratorHelper)
.performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE))
finishTouchActionUp(START_X + touchSlop + triggerThreshold + 1)
assertThat(mBackPanelController.currentState)
.isEqualTo(BackPanelController.GestureState.COMMITTED)
verify(backCallback).triggerBack()
}
@Test
fun handlesBackCancelled() {
featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false)
startTouch()
// Move once to cross the touch slop
continueTouch(START_X + touchSlop.toFloat() + 1)
@@ -151,6 +186,38 @@ class BackPanelControllerTest : SysuiTestCase() {
verify(backCallback).cancelBack()
}
@Test
fun handlesBackCancelled_withOneWayHapticsAPI() {
featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true)
startTouch()
// Move once to cross the touch slop
continueTouch(START_X + touchSlop.toFloat() + 1)
// Move again to cross the back trigger threshold
continueTouch(
START_X + touchSlop + triggerThreshold -
mBackPanelController.params.deactivationTriggerThreshold
)
// Wait threshold duration and hold touch before trigger threshold
Thread.sleep((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong())
continueTouch(
START_X + touchSlop + triggerThreshold -
mBackPanelController.params.deactivationTriggerThreshold
)
clearInvocations(backCallback)
Thread.sleep(MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION)
// Move in the opposite direction to cross the deactivation threshold and cancel back
continueTouch(START_X)
assertThat(mBackPanelController.currentState)
.isEqualTo(BackPanelController.GestureState.INACTIVE)
verify(backCallback).setTriggerBack(false)
verify(vibratorHelper)
.performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE))
finishTouchActionUp(START_X)
verify(backCallback).cancelBack()
}
private fun startTouch() {
mBackPanelController.onMotionEvent(createMotionEvent(ACTION_DOWN, START_X, 0f))
}