Merge "Add a stickiness factor to Back arrow on entry" into udc-dev

This commit is contained in:
Marvin Bernal
2023-04-26 22:18:50 +00:00
committed by Android (Google) Code Review
3 changed files with 98 additions and 77 deletions

View File

@@ -60,15 +60,21 @@ private const val MIN_DURATION_CANCELLED_ANIMATION = 200L
private const val MIN_DURATION_COMMITTED_ANIMATION = 80L
private const val MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION = 120L
private const val MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION = 50L
private const val MIN_DURATION_INACTIVE_BEFORE_ACTIVE_ANIMATION = 80L
private const val MIN_DURATION_INACTIVE_BEFORE_ACTIVE_ANIMATION = 160F
private const val MIN_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION = 10F
internal const val MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION = 100F
private const val MIN_DURATION_FLING_ANIMATION = 160L
private const val MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING = 100L
private const val MIN_DURATION_INACTIVE_TO_ACTIVE_CONSIDERED_AS_FLING = 400L
private const val POP_ON_FLING_DELAY = 60L
private const val POP_ON_FLING_SCALE = 2f
private const val POP_ON_COMMITTED_SCALE = 3f
private const val POP_ON_FLING_VELOCITY = 2f
private const val POP_ON_COMMITTED_VELOCITY = 3f
private const val POP_ON_ACTIVE_MAX_VELOCITY = 2.5f
private const val POP_ON_ACTIVE_MIN_VELOCITY = 4.5f
private const val POP_ON_INACTIVE_MAX_VELOCITY = -1.05f
private const val POP_ON_INACTIVE_MIN_VELOCITY = -1.5f
internal val VIBRATE_ACTIVATED_EFFECT =
VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
@@ -156,13 +162,22 @@ class BackPanelController internal constructor(
private var gestureEntryTime = 0L
private var gestureInactiveTime = 0L
private var gesturePastActiveThresholdWhileInactiveTime = 0L
private val elapsedTimeSinceInactive
get() = SystemClock.uptimeMillis() - gestureInactiveTime
private val elapsedTimeSinceEntry
get() = SystemClock.uptimeMillis() - gestureEntryTime
private var pastThresholdWhileEntryOrInactiveTime = 0L
private var entryToActiveDelay = 0F
private val entryToActiveDelayCalculation = {
convertVelocityToAnimationFactor(
valueOnFastVelocity = MIN_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION,
valueOnSlowVelocity = MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION,
)
}
// Whether the current gesture has moved a sufficiently large amount,
// so that we can unambiguously start showing the ENTRY animation
private var hasPassedDragSlop = false
@@ -306,7 +321,9 @@ class BackPanelController internal constructor(
MotionEvent.ACTION_UP -> {
when (currentState) {
GestureState.ENTRY -> {
if (isFlungAwayFromEdge(endX = event.x)) {
if (isFlungAwayFromEdge(endX = event.x) ||
previousXTranslation > params.staticTriggerThreshold
) {
updateArrowState(GestureState.ACTIVE)
updateArrowState(GestureState.FLUNG)
} else {
@@ -394,12 +411,29 @@ class BackPanelController internal constructor(
}
private fun updateArrowStateOnMove(yTranslation: Float, xTranslation: Float) {
val isWithinYActivationThreshold = xTranslation * 2 >= yTranslation
val isPastStaticThreshold = xTranslation > params.staticTriggerThreshold
when (currentState) {
GestureState.ENTRY -> {
if (xTranslation > params.staticTriggerThreshold) {
if (isPastThresholdToActive(
isPastThreshold = isPastStaticThreshold,
dynamicDelay = entryToActiveDelayCalculation
)
) {
updateArrowState(GestureState.ACTIVE)
}
}
GestureState.INACTIVE -> {
val isPastDynamicReactivationThreshold =
totalTouchDeltaInactive >= params.reactivationTriggerThreshold
if (isPastThresholdToActive(
isPastThreshold = isPastStaticThreshold &&
isPastDynamicReactivationThreshold &&
isWithinYActivationThreshold,
delay = MIN_DURATION_INACTIVE_BEFORE_ACTIVE_ANIMATION
)
) {
updateArrowState(GestureState.ACTIVE)
}
}
@@ -408,43 +442,12 @@ class BackPanelController internal constructor(
totalTouchDeltaActive <= params.deactivationTriggerThreshold
val isMinDurationElapsed =
elapsedTimeSinceEntry > MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION
if (isMinDurationElapsed && (!isWithinYActivationThreshold ||
isPastDynamicDeactivationThreshold)
) {
val isPastAllThresholds =
!isWithinYActivationThreshold || isPastDynamicDeactivationThreshold
if (isPastAllThresholds && isMinDurationElapsed) {
updateArrowState(GestureState.INACTIVE)
}
}
GestureState.INACTIVE -> {
val isPastStaticThreshold =
xTranslation > params.staticTriggerThreshold
val isPastDynamicReactivationThreshold =
totalTouchDeltaInactive >= params.reactivationTriggerThreshold
val isPastAllThresholds = isPastStaticThreshold &&
isPastDynamicReactivationThreshold &&
isWithinYActivationThreshold
val isPastAllThresholdsForFirstTime = isPastAllThresholds &&
gesturePastActiveThresholdWhileInactiveTime == 0L
gesturePastActiveThresholdWhileInactiveTime = when {
isPastAllThresholdsForFirstTime -> SystemClock.uptimeMillis()
isPastAllThresholds -> gesturePastActiveThresholdWhileInactiveTime
else -> 0L
}
val elapsedTimePastAllThresholds =
SystemClock.uptimeMillis() - gesturePastActiveThresholdWhileInactiveTime
val isPastMinimumInactiveToActiveDuration =
elapsedTimePastAllThresholds > MIN_DURATION_INACTIVE_BEFORE_ACTIVE_ANIMATION
if (isPastAllThresholds && isPastMinimumInactiveToActiveDuration) {
// The minimum duration adds the 'edge stickiness'
// factor before pulling it off edge
updateArrowState(GestureState.ACTIVE)
}
}
else -> {}
}
}
@@ -672,6 +675,28 @@ class BackPanelController internal constructor(
return flingDistance > minFlingDistance && isPastFlingVelocityThreshold
}
private fun isPastThresholdToActive(
isPastThreshold: Boolean,
delay: Float? = null,
dynamicDelay: () -> Float = { delay ?: 0F }
): Boolean {
val resetValue = 0L
val isPastThresholdForFirstTime = pastThresholdWhileEntryOrInactiveTime == resetValue
if (!isPastThreshold) {
pastThresholdWhileEntryOrInactiveTime = resetValue
return false
}
if (isPastThresholdForFirstTime) {
pastThresholdWhileEntryOrInactiveTime = SystemClock.uptimeMillis()
entryToActiveDelay = dynamicDelay()
}
val timePastThreshold = SystemClock.uptimeMillis() - pastThresholdWhileEntryOrInactiveTime
return timePastThreshold > entryToActiveDelay
}
private fun playWithBackgroundWidthAnimation(
onEnd: DelayedOnAnimationEndListener,
delay: Long = 0L
@@ -886,33 +911,16 @@ class BackPanelController internal constructor(
}
GestureState.ACTIVE -> {
previousXTranslationOnActiveOffset = previousXTranslation
updateRestingArrowDimens()
vibratorHelper.cancel()
mainHandler.postDelayed(10L) {
vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT)
}
val minimumPop = 2f
val maximumPop = 4.5f
when (previousState) {
GestureState.ENTRY -> {
val startingVelocity = convertVelocityToSpringStartingVelocity(
valueOnFastVelocity = minimumPop,
valueOnSlowVelocity = maximumPop,
fastVelocityBound = 1f,
slowVelocityBound = 0.5f,
)
mView.popOffEdge(startingVelocity)
}
GestureState.INACTIVE -> {
mView.popOffEdge(maximumPop)
}
else -> {}
}
val startingVelocity = convertVelocityToAnimationFactor(
valueOnFastVelocity = POP_ON_ACTIVE_MAX_VELOCITY,
valueOnSlowVelocity = POP_ON_ACTIVE_MIN_VELOCITY,
)
mView.popOffEdge(startingVelocity)
}
GestureState.INACTIVE -> {
@@ -925,9 +933,9 @@ class BackPanelController internal constructor(
// so that gesture progress in this state is consistent regardless of entry
totalTouchDeltaInactive = params.deactivationTriggerThreshold
val startingVelocity = convertVelocityToSpringStartingVelocity(
valueOnFastVelocity = -1.05f,
valueOnSlowVelocity = -1.50f
val startingVelocity = convertVelocityToAnimationFactor(
valueOnFastVelocity = POP_ON_INACTIVE_MAX_VELOCITY,
valueOnSlowVelocity = POP_ON_INACTIVE_MIN_VELOCITY
)
mView.popOffEdge(startingVelocity)
@@ -935,7 +943,9 @@ class BackPanelController internal constructor(
updateRestingArrowDimens()
}
GestureState.FLUNG -> {
mainHandler.postDelayed(POP_ON_FLING_DELAY) { mView.popScale(POP_ON_FLING_SCALE) }
mainHandler.postDelayed(POP_ON_FLING_DELAY) {
mView.popScale(POP_ON_FLING_VELOCITY)
}
updateRestingArrowDimens()
mainHandler.postDelayed(onEndSetCommittedStateListener.runnable,
MIN_DURATION_FLING_ANIMATION)
@@ -951,7 +961,7 @@ class BackPanelController internal constructor(
mainHandler.postDelayed(onEndSetGoneStateListener.runnable,
MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION)
} else {
mView.popScale(POP_ON_COMMITTED_SCALE)
mView.popScale(POP_ON_COMMITTED_VELOCITY)
mainHandler.postDelayed(onAlphaEndSetGoneStateListener.runnable,
MIN_DURATION_COMMITTED_ANIMATION)
}
@@ -968,11 +978,11 @@ class BackPanelController internal constructor(
}
}
private fun convertVelocityToSpringStartingVelocity(
private fun convertVelocityToAnimationFactor(
valueOnFastVelocity: Float,
valueOnSlowVelocity: Float,
fastVelocityBound: Float = 3f,
slowVelocityBound: Float = 0f,
fastVelocityBound: Float = 1f,
slowVelocityBound: Float = 0.5f,
): Float {
val factor = velocityTracker?.run {
computeCurrentVelocity(PX_PER_MS)

View File

@@ -153,7 +153,7 @@ data class EdgePanelParams(private var resources: Resources) {
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 = createSpring(500f, 0.76f),
horizontalTranslationSpring = createSpring(800f, 0.76f),
verticalTranslationSpring = createSpring(30000f, 1f),
scaleSpring = createSpring(120f, 0.8f),
arrowDimens = ArrowDimens(
@@ -205,8 +205,8 @@ data class EdgePanelParams(private var resources: Resources) {
activeIndicator = BackIndicatorDimens(
horizontalTranslation = getDimen(R.dimen.navigation_edge_active_margin),
scale = getDimenFloat(R.dimen.navigation_edge_active_scale),
horizontalTranslationSpring = createSpring(1000f, 0.7f),
scaleSpring = createSpring(450f, 0.39f),
horizontalTranslationSpring = createSpring(1000f, 0.8f),
scaleSpring = createSpring(325f, 0.55f),
scalePivotX = getDimen(R.dimen.navigation_edge_active_background_width),
arrowDimens = ArrowDimens(
length = getDimen(R.dimen.navigation_edge_active_arrow_length),
@@ -253,7 +253,7 @@ data class EdgePanelParams(private var resources: Resources) {
getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners),
farCornerRadius =
getDimen(R.dimen.navigation_edge_pre_threshold_far_corners),
widthSpring = createSpring(400f, 0.65f),
widthSpring = createSpring(650f, 1f),
heightSpring = createSpring(1500f, 0.45f),
farCornerRadiusSpring = createSpring(300f, 1f),
edgeCornerRadiusSpring = createSpring(250f, 0.5f),

View File

@@ -104,6 +104,9 @@ class BackPanelControllerTest : SysuiTestCase() {
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)
@@ -114,14 +117,22 @@ class BackPanelControllerTest : SysuiTestCase() {
finishTouchActionUp(START_X + touchSlop + triggerThreshold + 1)
assertThat(mBackPanelController.currentState)
.isEqualTo(BackPanelController.GestureState.FLUNG)
.isEqualTo(BackPanelController.GestureState.COMMITTED)
verify(backCallback).triggerBack()
}
@Test
fun handlesBackCancelled() {
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