Merge "Base: Predictive Back, add support for large screens when swiping" into udc-dev

This commit is contained in:
Omar Miatello
2023-05-16 09:39:21 +00:00
committed by Android (Google) Code Review
7 changed files with 293 additions and 166 deletions

View File

@@ -54,10 +54,42 @@ public interface BackAnimation {
void setTriggerBack(boolean triggerBack);
/**
* Sets the threshold values that defining edge swipe behavior.
* @param progressThreshold the max threshold to keep linear progressing back animation.
* Sets the threshold values that define edge swipe behavior.<br>
* <br>
* <h1>How does {@code nonLinearFactor} work?</h1>
* <pre>
* screen screen screen
* width width width
* |——————| |————————————| |————————————————————|
* A B A B C A
* 1 +——————+—————+ 1 +————————————+ 1 +————————————+———————+
* | / | | —/| | | —————/|
* | / | | —/ | | ——/ |
* | / | | —/ | | ——/ | |
* | / | | —/ | | ——/ | |
* | / | | —/ | | ——/ | |
* |/ | |—/ | |—/ | |
* 0 +————————————+ 0 +————————————+ 0 +————————————+———————+
* B B B
* </pre>
* Three devices with different widths (smaller, equal, and wider) relative to the progress
* threshold are shown in the graphs.<br>
* - A is the width of the screen<br>
* - B is the progress threshold (horizontal swipe distance where progress is linear)<br>
* - C equals B + (A - B) * nonLinearFactor<br>
* <br>
* If A is less than or equal to B, {@code progress} for the swipe distance between:<br>
* - [0, A] will scale linearly between [0, 1].<br>
* If A is greater than B, {@code progress} for swipe distance between:<br>
* - [0, B] will scale linearly between [0, B / C]<br>
* - (B, A] will scale non-linearly and reach 1.
*
* @param linearDistance up to this distance progress continues linearly. B in the graph above.
* @param maxDistance distance at which the progress will be 1f. A in the graph above.
* @param nonLinearFactor This value is used to calculate the target if the screen is wider
* than the progress threshold.
*/
void setSwipeThresholds(float progressThreshold);
void setSwipeThresholds(float linearDistance, float maxDistance, float nonLinearFactor);
/**
* Sets the system bar listener to control the system bar color.

View File

@@ -301,9 +301,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
}
@Override
public void setSwipeThresholds(float progressThreshold) {
public void setSwipeThresholds(
float linearDistance,
float maxDistance,
float nonLinearFactor) {
mShellExecutor.execute(() -> BackAnimationController.this.setSwipeThresholds(
progressThreshold));
linearDistance, maxDistance, nonLinearFactor));
}
@Override
@@ -509,7 +512,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
// Constraints - absolute values
float minVelocity = mFlingAnimationUtils.getMinVelocityPxPerSecond();
float maxVelocity = mFlingAnimationUtils.getHighVelocityPxPerSecond();
float maxX = mTouchTracker.getMaxX(); // px
float maxX = mTouchTracker.getMaxDistance(); // px
float maxFlingDistance = maxX * MAX_FLING_PROGRESS; // px
// Current state
@@ -605,8 +608,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
mTouchTracker.setTriggerBack(triggerBack);
}
private void setSwipeThresholds(float progressThreshold) {
mTouchTracker.setProgressThreshold(progressThreshold);
private void setSwipeThresholds(
float linearDistance,
float maxDistance,
float nonLinearFactor) {
mTouchTracker.setProgressThresholds(linearDistance, maxDistance, nonLinearFactor);
}
private void invokeOrCancelBack() {

View File

@@ -28,11 +28,13 @@ import android.window.BackMotionEvent;
* Helper class to record the touch location for gesture and generate back events.
*/
class TouchTracker {
private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP =
"persist.wm.debug.predictive_back_progress_threshold";
private static final int PROGRESS_THRESHOLD = SystemProperties
.getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1);
private float mProgressThreshold;
private static final String PREDICTIVE_BACK_LINEAR_DISTANCE_PROP =
"persist.wm.debug.predictive_back_linear_distance";
private static final int LINEAR_DISTANCE = SystemProperties
.getInt(PREDICTIVE_BACK_LINEAR_DISTANCE_PROP, -1);
private float mLinearDistance = LINEAR_DISTANCE;
private float mMaxDistance;
private float mNonLinearFactor;
/**
* Location of the latest touch event
*/
@@ -125,17 +127,42 @@ class TouchTracker {
// the location everytime back is restarted after being cancelled.
float startX = mTriggerBack ? mInitTouchX : mStartThresholdX;
float deltaX = Math.abs(startX - touchX);
float maxX = getMaxX();
maxX = maxX == 0 ? 1 : maxX;
return MathUtils.constrain(deltaX / maxX, 0, 1);
float linearDistance = mLinearDistance;
float maxDistance = getMaxDistance();
maxDistance = maxDistance == 0 ? 1 : maxDistance;
float progress;
if (linearDistance < maxDistance) {
// Up to linearDistance it behaves linearly, then slowly reaches 1f.
// maxDistance is composed of linearDistance + nonLinearDistance
float nonLinearDistance = maxDistance - linearDistance;
float initialTarget = linearDistance + nonLinearDistance * mNonLinearFactor;
boolean isLinear = deltaX <= linearDistance;
if (isLinear) {
progress = deltaX / initialTarget;
} else {
float nonLinearDeltaX = deltaX - linearDistance;
float nonLinearProgress = nonLinearDeltaX / nonLinearDistance;
float currentTarget = MathUtils.lerp(
/* start = */ initialTarget,
/* stop = */ maxDistance,
/* amount = */ nonLinearProgress);
progress = deltaX / currentTarget;
}
} else {
// Always linear behavior.
progress = deltaX / maxDistance;
}
return MathUtils.constrain(progress, 0, 1);
}
/**
* Maximum X value (in pixels).
* Maximum distance in pixels.
* Progress is considered to be completed (1f) when this limit is exceeded.
*/
float getMaxX() {
return PROGRESS_THRESHOLD >= 0 ? PROGRESS_THRESHOLD : mProgressThreshold;
float getMaxDistance() {
return mMaxDistance;
}
BackMotionEvent createProgressEvent(float progress) {
@@ -149,7 +176,14 @@ class TouchTracker {
/* departingAnimationTarget = */ null);
}
public void setProgressThreshold(float progressThreshold) {
mProgressThreshold = progressThreshold;
public void setProgressThresholds(float linearDistance, float maxDistance,
float nonLinearFactor) {
if (LINEAR_DISTANCE >= 0) {
mLinearDistance = LINEAR_DISTANCE;
} else {
mLinearDistance = linearDistance;
}
mMaxDistance = maxDistance;
mNonLinearFactor = nonLinearFactor;
}
}

View File

@@ -1,141 +0,0 @@
/*
* 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.wm.shell.back;
import static org.junit.Assert.assertEquals;
import android.window.BackEvent;
import android.window.BackMotionEvent;
import org.junit.Before;
import org.junit.Test;
public class TouchTrackerTest {
private static final float FAKE_THRESHOLD = 400;
private static final float INITIAL_X_LEFT_EDGE = 5;
private static final float INITIAL_X_RIGHT_EDGE = FAKE_THRESHOLD - INITIAL_X_LEFT_EDGE;
private TouchTracker mTouchTracker;
@Before
public void setUp() throws Exception {
mTouchTracker = new TouchTracker();
mTouchTracker.setProgressThreshold(FAKE_THRESHOLD);
}
@Test
public void generatesProgress_onStart() {
mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT);
BackMotionEvent event = mTouchTracker.createStartEvent(null);
assertEquals(event.getProgress(), 0f, 0f);
}
@Test
public void generatesProgress_leftEdge() {
mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT);
float touchX = 10;
float velocityX = 0;
float velocityY = 0;
// Pre-commit
mTouchTracker.update(touchX, 0, velocityX, velocityY);
assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f);
// Post-commit
touchX += 100;
mTouchTracker.setTriggerBack(true);
mTouchTracker.update(touchX, 0, velocityX, velocityY);
assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f);
// Cancel
touchX -= 10;
mTouchTracker.setTriggerBack(false);
mTouchTracker.update(touchX, 0, velocityX, velocityY);
assertEquals(getProgress(), 0, 0f);
// Cancel more
touchX -= 10;
mTouchTracker.update(touchX, 0, velocityX, velocityY);
assertEquals(getProgress(), 0, 0f);
// Restart
touchX += 10;
mTouchTracker.update(touchX, 0, velocityX, velocityY);
assertEquals(getProgress(), 0, 0f);
// Restarted, but pre-commit
float restartX = touchX;
touchX += 10;
mTouchTracker.update(touchX, 0, velocityX, velocityY);
assertEquals(getProgress(), (touchX - restartX) / FAKE_THRESHOLD, 0f);
// Restarted, post-commit
touchX += 10;
mTouchTracker.setTriggerBack(true);
mTouchTracker.update(touchX, 0, velocityX, velocityY);
assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f);
}
@Test
public void generatesProgress_rightEdge() {
mTouchTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0, BackEvent.EDGE_RIGHT);
float touchX = INITIAL_X_RIGHT_EDGE - 10; // Fake right edge
float velocityX = 0f;
float velocityY = 0f;
// Pre-commit
mTouchTracker.update(touchX, 0, velocityX, velocityY);
assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f);
// Post-commit
touchX -= 100;
mTouchTracker.setTriggerBack(true);
mTouchTracker.update(touchX, 0, velocityX, velocityY);
assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f);
// Cancel
touchX += 10;
mTouchTracker.setTriggerBack(false);
mTouchTracker.update(touchX, 0, velocityX, velocityY);
assertEquals(getProgress(), 0, 0f);
// Cancel more
touchX += 10;
mTouchTracker.update(touchX, 0, velocityX, velocityY);
assertEquals(getProgress(), 0, 0f);
// Restart
touchX -= 10;
mTouchTracker.update(touchX, 0, velocityX, velocityY);
assertEquals(getProgress(), 0, 0f);
// Restarted, but pre-commit
float restartX = touchX;
touchX -= 10;
mTouchTracker.update(touchX, 0, velocityX, velocityY);
assertEquals(getProgress(), (restartX - touchX) / FAKE_THRESHOLD, 0f);
// Restarted, post-commit
touchX -= 10;
mTouchTracker.setTriggerBack(true);
mTouchTracker.update(touchX, 0, velocityX, velocityY);
assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f);
}
private float getProgress() {
return mTouchTracker.createProgressEvent().getProgress();
}
}

View File

@@ -0,0 +1,181 @@
/*
* 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.wm.shell.back
import android.util.MathUtils
import android.window.BackEvent
import org.junit.Assert.assertEquals
import org.junit.Test
class TouchTrackerTest {
private fun linearTouchTracker(): TouchTracker = TouchTracker().apply {
setProgressThresholds(MAX_DISTANCE, MAX_DISTANCE, NON_LINEAR_FACTOR)
}
private fun nonLinearTouchTracker(): TouchTracker = TouchTracker().apply {
setProgressThresholds(LINEAR_DISTANCE, MAX_DISTANCE, NON_LINEAR_FACTOR)
}
private fun TouchTracker.assertProgress(expected: Float) {
val actualProgress = createProgressEvent().progress
assertEquals(expected, actualProgress, /* delta = */ 0f)
}
@Test
fun generatesProgress_onStart() {
val linearTracker = linearTouchTracker()
linearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT)
val event = linearTracker.createStartEvent(null)
assertEquals(0f, event.progress, 0f)
}
@Test
fun generatesProgress_leftEdge() {
val linearTracker = linearTouchTracker()
linearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT)
var touchX = 10f
val velocityX = 0f
val velocityY = 0f
// Pre-commit
linearTracker.update(touchX, 0f, velocityX, velocityY)
linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE)
// Post-commit
touchX += 100f
linearTracker.setTriggerBack(true)
linearTracker.update(touchX, 0f, velocityX, velocityY)
linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE)
// Cancel
touchX -= 10f
linearTracker.setTriggerBack(false)
linearTracker.update(touchX, 0f, velocityX, velocityY)
linearTracker.assertProgress(0f)
// Cancel more
touchX -= 10f
linearTracker.update(touchX, 0f, velocityX, velocityY)
linearTracker.assertProgress(0f)
// Restart
touchX += 10f
linearTracker.update(touchX, 0f, velocityX, velocityY)
linearTracker.assertProgress(0f)
// Restarted, but pre-commit
val restartX = touchX
touchX += 10f
linearTracker.update(touchX, 0f, velocityX, velocityY)
linearTracker.assertProgress((touchX - restartX) / MAX_DISTANCE)
// Restarted, post-commit
touchX += 10f
linearTracker.setTriggerBack(true)
linearTracker.update(touchX, 0f, velocityX, velocityY)
linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE)
}
@Test
fun generatesProgress_rightEdge() {
val linearTracker = linearTouchTracker()
linearTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0f, BackEvent.EDGE_RIGHT)
var touchX = INITIAL_X_RIGHT_EDGE - 10 // Fake right edge
val velocityX = 0f
val velocityY = 0f
val target = MAX_DISTANCE
// Pre-commit
linearTracker.update(touchX, 0f, velocityX, velocityY)
linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / target)
// Post-commit
touchX -= 100f
linearTracker.setTriggerBack(true)
linearTracker.update(touchX, 0f, velocityX, velocityY)
linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / target)
// Cancel
touchX += 10f
linearTracker.setTriggerBack(false)
linearTracker.update(touchX, 0f, velocityX, velocityY)
linearTracker.assertProgress(0f)
// Cancel more
touchX += 10f
linearTracker.update(touchX, 0f, velocityX, velocityY)
linearTracker.assertProgress(0f)
// Restart
touchX -= 10f
linearTracker.update(touchX, 0f, velocityX, velocityY)
linearTracker.assertProgress(0f)
// Restarted, but pre-commit
val restartX = touchX
touchX -= 10f
linearTracker.update(touchX, 0f, velocityX, velocityY)
linearTracker.assertProgress((restartX - touchX) / target)
// Restarted, post-commit
touchX -= 10f
linearTracker.setTriggerBack(true)
linearTracker.update(touchX, 0f, velocityX, velocityY)
linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / target)
}
@Test
fun generatesNonLinearProgress_leftEdge() {
val nonLinearTracker = nonLinearTouchTracker()
nonLinearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT)
var touchX = 10f
val velocityX = 0f
val velocityY = 0f
val linearTarget = LINEAR_DISTANCE + (MAX_DISTANCE - LINEAR_DISTANCE) * NON_LINEAR_FACTOR
// Pre-commit: linear progress
nonLinearTracker.update(touchX, 0f, velocityX, velocityY)
nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / linearTarget)
// Post-commit: still linear progress
touchX += 100f
nonLinearTracker.setTriggerBack(true)
nonLinearTracker.update(touchX, 0f, velocityX, velocityY)
nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / linearTarget)
// still linear progress
touchX = INITIAL_X_LEFT_EDGE + LINEAR_DISTANCE
nonLinearTracker.update(touchX, 0f, velocityX, velocityY)
nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / linearTarget)
// non linear progress
touchX += 10
nonLinearTracker.update(touchX, 0f, velocityX, velocityY)
val nonLinearTouch = (touchX - INITIAL_X_LEFT_EDGE) - LINEAR_DISTANCE
val nonLinearProgress = nonLinearTouch / NON_LINEAR_DISTANCE
val nonLinearTarget = MathUtils.lerp(linearTarget, MAX_DISTANCE, nonLinearProgress)
nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / nonLinearTarget)
}
companion object {
private const val MAX_DISTANCE = 500f
private const val LINEAR_DISTANCE = 400f
private const val NON_LINEAR_DISTANCE = MAX_DISTANCE - LINEAR_DISTANCE
private const val NON_LINEAR_FACTOR = 0.2f
private const val INITIAL_X_LEFT_EDGE = 5f
private const val INITIAL_X_RIGHT_EDGE = MAX_DISTANCE - INITIAL_X_LEFT_EDGE
}
}

View File

@@ -45,6 +45,9 @@
<dimen name="navigation_edge_action_drag_threshold">16dp</dimen>
<!-- The threshold to progress back animation for edge swipe -->
<dimen name="navigation_edge_action_progress_threshold">412dp</dimen>
<!-- This value is used to calculate the target if the screen is wider than the
navigation_edge_action_progress_threshold. See BackAnimation#setSwipeThresholds -->
<item name="back_progress_non_linear_factor" format="float" type="dimen">0.2</item>
<!-- The minimum display position of the arrow on the screen -->
<dimen name="navigation_edge_arrow_min_y">64dp</dimen>
<!-- The amount by which the arrow is shifted to avoid the finger-->

View File

@@ -63,6 +63,8 @@ import android.view.WindowInsets;
import android.view.WindowManager;
import android.window.BackEvent;
import androidx.annotation.DimenRes;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.policy.GestureNavigationSettingsObserver;
import com.android.systemui.R;
@@ -225,7 +227,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
// The slop to distinguish between horizontal and vertical motion
private float mTouchSlop;
// The threshold for back swipe full progress.
private float mBackSwipeProgressThreshold;
private float mBackSwipeLinearThreshold;
private float mNonLinearFactor;
// Duration after which we consider the event as longpress.
private final int mLongPressTimeout;
private int mStartingQuickstepRotation = -1;
@@ -471,11 +474,19 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
final float backGestureSlop = DeviceConfig.getFloat(DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.BACK_GESTURE_SLOP_MULTIPLIER, 0.75f);
mTouchSlop = mViewConfiguration.getScaledTouchSlop() * backGestureSlop;
mBackSwipeProgressThreshold = res.getDimension(
mBackSwipeLinearThreshold = res.getDimension(
R.dimen.navigation_edge_action_progress_threshold);
mNonLinearFactor = getDimenFloat(res,
R.dimen.back_progress_non_linear_factor);
updateBackAnimationThresholds();
}
private float getDimenFloat(Resources res, @DimenRes int resId) {
TypedValue typedValue = new TypedValue();
res.getValue(resId, typedValue, true);
return typedValue.getFloat();
}
public void updateNavigationBarOverlayExcludeRegion(Rect exclude) {
mNavBarOverlayExcludedBounds.set(exclude);
}
@@ -1116,8 +1127,9 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
if (mBackAnimation == null) {
return;
}
mBackAnimation.setSwipeThresholds(
Math.min(mDisplaySize.x, mBackSwipeProgressThreshold));
int maxDistance = mDisplaySize.x;
float linearDistance = Math.min(maxDistance, mBackSwipeLinearThreshold);
mBackAnimation.setSwipeThresholds(linearDistance, maxDistance, mNonLinearFactor);
}
private boolean sendEvent(int action, int code) {