Merge "Create SinglePanningState that will handle single pointer/finger panning" into main

This commit is contained in:
Kiara Patricia
2023-07-19 17:32:10 +00:00
committed by Android (Google) Code Review
3 changed files with 325 additions and 20 deletions

View File

@@ -245,6 +245,31 @@ public class FullScreenMagnificationController implements
return mCurrentMagnificationSpec.offsetY;
}
@GuardedBy("mLock")
boolean isAtEdge() {
return isAtLeftEdge() || isAtRightEdge() || isAtTopEdge() || isAtBottomEdge();
}
@GuardedBy("mLock")
boolean isAtLeftEdge() {
return getOffsetX() == getMaxOffsetXLocked();
}
@GuardedBy("mLock")
boolean isAtRightEdge() {
return getOffsetX() == getMinOffsetXLocked();
}
@GuardedBy("mLock")
boolean isAtTopEdge() {
return getOffsetY() == getMaxOffsetYLocked();
}
@GuardedBy("mLock")
boolean isAtBottomEdge() {
return getOffsetY() == getMinOffsetYLocked();
}
@GuardedBy("mLock")
float getCenterX() {
return (mMagnificationBounds.width() / 2.0f
@@ -1085,6 +1110,87 @@ public class FullScreenMagnificationController implements
}
}
/**
* Returns whether the user is at one of the edges (left, right, top, bottom)
* of the magnification viewport
*
* @param displayId
* @return if user is at the edge of the view
*/
public boolean isAtEdge(int displayId) {
synchronized (mLock) {
final DisplayMagnification display = mDisplays.get(displayId);
if (display == null) {
return false;
}
return display.isAtEdge();
}
}
/**
* Returns whether the user is at the left edge of the viewport
*
* @param displayId
* @return if user is at left edge of view
*/
public boolean isAtLeftEdge(int displayId) {
synchronized (mLock) {
final DisplayMagnification display = mDisplays.get(displayId);
if (display == null) {
return false;
}
return display.isAtLeftEdge();
}
}
/**
* Returns whether the user is at the right edge of the viewport
*
* @param displayId
* @return if user is at right edge of view
*/
public boolean isAtRightEdge(int displayId) {
synchronized (mLock) {
final DisplayMagnification display = mDisplays.get(displayId);
if (display == null) {
return false;
}
return display.isAtRightEdge();
}
}
/**
* Returns whether the user is at the top edge of the viewport
*
* @param displayId
* @return if user is at top edge of view
*/
public boolean isAtTopEdge(int displayId) {
synchronized (mLock) {
final DisplayMagnification display = mDisplays.get(displayId);
if (display == null) {
return false;
}
return display.isAtTopEdge();
}
}
/**
* Returns whether the user is at the bottom edge of the viewport
*
* @param displayId
* @return if user is at bottom edge of view
*/
public boolean isAtBottomEdge(int displayId) {
synchronized (mLock) {
final DisplayMagnification display = mDisplays.get(displayId);
if (display == null) {
return false;
}
return display.isAtBottomEdge();
}
}
/**
* Returns the Y offset of the magnification viewport. If an animation
* is in progress, this reflects the end state of the animation.

View File

@@ -137,6 +137,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
@VisibleForTesting final DetectingState mDetectingState;
@VisibleForTesting final PanningScalingState mPanningScalingState;
@VisibleForTesting final ViewportDraggingState mViewportDraggingState;
@VisibleForTesting final SinglePanningState mSinglePanningState;
private final ScreenStateReceiver mScreenStateReceiver;
private final WindowMagnificationPromptController mPromptController;
@@ -146,7 +147,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
private PointerCoords[] mTempPointerCoords;
private PointerProperties[] mTempPointerProperties;
@VisibleForTesting boolean mIsSinglePanningEnabled;
public FullScreenMagnificationGestureHandler(@UiContext Context context,
FullScreenMagnificationController fullScreenMagnificationController,
AccessibilityTraceManager trace,
@@ -202,6 +203,8 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
mDetectingState = new DetectingState(context);
mViewportDraggingState = new ViewportDraggingState();
mPanningScalingState = new PanningScalingState(context);
mSinglePanningState = new SinglePanningState(context);
setSinglePanningEnabled(false);
if (mDetectShortcutTrigger) {
mScreenStateReceiver = new ScreenStateReceiver(context, this);
@@ -213,6 +216,11 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
transitionTo(mDetectingState);
}
@VisibleForTesting
void setSinglePanningEnabled(boolean isEnabled) {
mIsSinglePanningEnabled = isEnabled;
}
@Override
void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
handleEventWith(mCurrentState, event, rawEvent, policyFlags);
@@ -223,6 +231,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
// To keep InputEventConsistencyVerifiers within GestureDetectors happy
mPanningScalingState.mScrollGestureDetector.onTouchEvent(event);
mPanningScalingState.mScaleGestureDetector.onTouchEvent(event);
mSinglePanningState.mScrollGestureDetector.onTouchEvent(event);
try {
stateHandler.onMotionEvent(event, rawEvent, policyFlags);
@@ -669,7 +678,6 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
@Override
public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
// Ensures that the state at the end of delegation is consistent with the last delegated
// UP/DOWN event in queue: still delegating if pointer is down, detecting otherwise
switch (event.getActionMasked()) {
@@ -726,6 +734,8 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
@VisibleForTesting Handler mHandler = new Handler(Looper.getMainLooper(), this);
private PointF mFirstPointerDownLocation = new PointF(Float.NaN, Float.NaN);
DetectingState(Context context) {
mLongTapMinDelay = ViewConfiguration.getLongPressTimeout();
mMultiTapMaxDelay = ViewConfiguration.getDoubleTapTimeout()
@@ -765,10 +775,11 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
cacheDelayedMotionEvent(event, rawEvent, policyFlags);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
mLastDetectingDownEventTime = event.getDownTime();
mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
mFirstPointerDownLocation.set(event.getX(), event.getY());
if (!mFullScreenMagnificationController.magnificationRegionContains(
mDisplayId, event.getX(), event.getY())) {
@@ -800,7 +811,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
break;
case ACTION_POINTER_DOWN: {
if (isActivated() && event.getPointerCount() == 2) {
storeSecondPointerDownLocation(event);
storePointerDownLocation(mSecondPointerDownLocation, event);
mHandler.sendEmptyMessageDelayed(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE,
ViewConfiguration.getTapTimeout());
} else {
@@ -815,7 +826,6 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
case ACTION_MOVE: {
if (isFingerDown()
&& distance(mLastDown, /* move */ event) > mSwipeMinDistance) {
// Swipe detected - transition immediately
// For convenience, viewport dragging takes precedence
@@ -826,10 +836,15 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
} else if (isActivated() && event.getPointerCount() == 2) {
//Primary pointer is swiping, so transit to PanningScalingState
transitToPanningScalingStateAndClear();
} else if (mIsSinglePanningEnabled
&& isActivated()
&& event.getPointerCount() == 1
&& !isOverscroll(event)) {
transitToSinglePanningStateAndClear();
} else {
transitionToDelegatingStateAndClear();
}
} else if (isActivated() && secondPointerDownValid()
} else if (isActivated() && pointerDownValid(mSecondPointerDownLocation)
&& distanceClosestPointerToPoint(
mSecondPointerDownLocation, /* move */ event) > mSwipeMinDistance) {
//Second pointer is swiping, so transit to PanningScalingState
@@ -843,11 +858,9 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
if (!mFullScreenMagnificationController.magnificationRegionContains(
mDisplayId, event.getX(), event.getY())) {
transitionToDelegatingStateAndClear();
} else if (isMultiTapTriggered(3 /* taps */)) {
onTripleTap(/* up */ event);
} else if (
@@ -856,7 +869,6 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
//TODO long tap should never happen here
&& ((timeBetween(mLastDown, mLastUp) >= mLongTapMinDelay)
|| (distance(mLastDown, mLastUp) >= mSwipeMinDistance))) {
transitionToDelegatingStateAndClear();
}
@@ -865,14 +877,28 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
}
}
private void storeSecondPointerDownLocation(MotionEvent event) {
final int index = event.getActionIndex();
mSecondPointerDownLocation.set(event.getX(index), event.getY(index));
private boolean isOverscroll(MotionEvent event) {
if (!pointerDownValid(mFirstPointerDownLocation)) {
return false;
}
float dX = event.getX() - mFirstPointerDownLocation.x;
float dY = event.getY() - mFirstPointerDownLocation.y;
boolean didOverscroll =
mFullScreenMagnificationController.isAtLeftEdge(mDisplayId) && dX > 0
|| mFullScreenMagnificationController.isAtRightEdge(mDisplayId) && dX < 0
|| mFullScreenMagnificationController.isAtTopEdge(mDisplayId) && dY > 0
|| mFullScreenMagnificationController.isAtBottomEdge(mDisplayId) && dY < 0;
return didOverscroll;
}
private boolean secondPointerDownValid() {
return !(Float.isNaN(mSecondPointerDownLocation.x) && Float.isNaN(
mSecondPointerDownLocation.y));
private void storePointerDownLocation(PointF pointerDownLocation, MotionEvent event) {
final int index = event.getActionIndex();
pointerDownLocation.set(event.getX(index), event.getY(index));
}
private boolean pointerDownValid(PointF pointerDownLocation) {
return !(Float.isNaN(pointerDownLocation.x) && Float.isNaN(
pointerDownLocation.y));
}
private void transitToPanningScalingStateAndClear() {
@@ -880,6 +906,11 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
clear();
}
private void transitToSinglePanningStateAndClear() {
transitionTo(mSinglePanningState);
clear();
}
public boolean isMultiTapTriggered(int numTaps) {
// Shortcut acts as the 2 initial taps
@@ -947,6 +978,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
setShortcutTriggered(false);
removePendingDelayedMessages();
clearDelayedMotionEvents();
mFirstPointerDownLocation.set(Float.NaN, Float.NaN);
mSecondPointerDownLocation.set(Float.NaN, Float.NaN);
}
@@ -1165,12 +1197,14 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
+ ", mDelegatingState=" + mDelegatingState
+ ", mMagnifiedInteractionState=" + mPanningScalingState
+ ", mViewportDraggingState=" + mViewportDraggingState
+ ", mSinglePanningState=" + mSinglePanningState
+ ", mDetectTripleTap=" + mDetectTripleTap
+ ", mDetectShortcutTrigger=" + mDetectShortcutTrigger
+ ", mCurrentState=" + State.nameOf(mCurrentState)
+ ", mPreviousState=" + State.nameOf(mPreviousState)
+ ", mMagnificationController=" + mFullScreenMagnificationController
+ ", mDisplayId=" + mDisplayId
+ ", mIsSinglePanningEnabled=" + mIsSinglePanningEnabled
+ '}';
}
@@ -1285,8 +1319,67 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
* Indicates an error with a gesture handler or state.
*/
private static class GestureException extends Exception {
GestureException(String message) {
super(message);
}
}
final class SinglePanningState extends SimpleOnGestureListener implements State {
private final GestureDetector mScrollGestureDetector;
private MotionEventInfo mEvent;
SinglePanningState(Context context) {
mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain());
}
@Override
public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
int action = event.getActionMasked();
switch (action) {
case ACTION_UP:
case ACTION_CANCEL:
clear();
transitionTo(mDetectingState);
break;
}
}
@Override
public boolean onScroll(
MotionEvent first, MotionEvent second, float distanceX, float distanceY) {
if (mCurrentState != mSinglePanningState) {
return true;
}
mFullScreenMagnificationController.offsetMagnifiedRegion(
mDisplayId,
distanceX,
distanceY,
AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
if (DEBUG_PANNING_SCALING) {
Slog.i(
mLogTag,
"SinglePanningState Panned content by scrollX: "
+ distanceX
+ " scrollY: "
+ distanceY
+ " isAtEdge: "
+ mFullScreenMagnificationController.isAtEdge(mDisplayId));
}
// TODO: b/280812104 Dispatch events before Delegation
if (mFullScreenMagnificationController.isAtEdge(mDisplayId)) {
clear();
transitionTo(mDelegatingState);
}
return /* event consumed: */ true;
}
@Override
public String toString() {
return "SinglePanningState{"
+ "isEdgeOfView="
+ mFullScreenMagnificationController.isAtEdge(mDisplayId)
+ "}";
}
}
}

View File

@@ -33,6 +33,7 @@ import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
@@ -42,6 +43,8 @@ import static org.mockito.Mockito.when;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Handler;
import android.os.Message;
import android.os.UserHandle;
@@ -67,11 +70,13 @@ import com.android.server.wm.WindowManagerInternal;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
import java.util.ArrayList;
import java.util.List;
@@ -123,9 +128,10 @@ public class FullScreenMagnificationGestureHandlerTest {
public static final int STATE_SHORTCUT_TRIGGERED_ZOOMED_TMP = 8;
public static final int STATE_PANNING = 9;
public static final int STATE_SCALING_AND_PANNING = 10;
public static final int STATE_SINGLE_PANNING = 11;
public static final int FIRST_STATE = STATE_IDLE;
public static final int LAST_STATE = STATE_SCALING_AND_PANNING;
public static final int LAST_STATE = STATE_SINGLE_PANNING;
// Co-prime x and y, to potentially catch x-y-swapped errors
public static final float DEFAULT_X = 301;
@@ -155,6 +161,10 @@ public class FullScreenMagnificationGestureHandlerTest {
private float mOriginalMagnificationPersistedScale;
static final Rect INITIAL_MAGNIFICATION_BOUNDS = new Rect(0, 0, 800, 800);
static final Region INITIAL_MAGNIFICATION_REGION = new Region(INITIAL_MAGNIFICATION_BOUNDS);
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -182,11 +192,19 @@ public class FullScreenMagnificationGestureHandlerTest {
new MagnificationScaleProvider(mContext),
() -> null,
ConcurrentUtils.DIRECT_EXECUTOR) {
@Override
public boolean magnificationRegionContains(int displayId, float x, float y) {
return true;
}
@Override
public boolean magnificationRegionContains(int displayId, float x, float y) {
return true;
}
};
doAnswer((Answer<Void>) invocationOnMock -> {
Object[] args = invocationOnMock.getArguments();
Region regionArg = (Region) args[1];
regionArg.set(new Rect(INITIAL_MAGNIFICATION_BOUNDS));
return null;
}).when(mockWindowManager).getMagnificationRegion(anyInt(), any(Region.class));
mFullScreenMagnificationController.register(DISPLAY_0);
mFullScreenMagnificationController.setAlwaysOnMagnificationEnabled(true);
mClock = new OffsettableClock.Stopped();
@@ -214,6 +232,7 @@ public class FullScreenMagnificationGestureHandlerTest {
mContext, mFullScreenMagnificationController, mMockTraceManager, mMockCallback,
detectTripleTap, detectShortcutTrigger,
mWindowMagnificationPromptController, DISPLAY_0);
h.setSinglePanningEnabled(true);
mHandler = new TestHandler(h.mDetectingState, mClock) {
@Override
protected String messageToString(Message m) {
@@ -239,6 +258,7 @@ public class FullScreenMagnificationGestureHandlerTest {
* {@link #returnToNormalFrom} (for navigating back to {@link #STATE_IDLE})
*/
@Test
@Ignore("b/291925580")
public void testEachState_isReachableAndRecoverable() {
forEachState(state -> {
goFromStateIdleTo(state);
@@ -525,6 +545,75 @@ public class FullScreenMagnificationGestureHandlerTest {
verify(mWindowMagnificationPromptController).showNotificationIfNeeded();
}
@Test
public void testActionUpNotAtEdge_singlePanningState_detectingState() {
goFromStateIdleTo(STATE_SINGLE_PANNING);
send(upEvent());
check(mMgh.mCurrentState == mMgh.mDetectingState, STATE_IDLE);
assertTrue(isZoomed());
}
@Test
public void testScroll_SinglePanningDisabled_delegatingState() {
mMgh.setSinglePanningEnabled(false);
goFromStateIdleTo(STATE_ACTIVATED);
allowEventDelegation();
swipeAndHold();
assertTrue(mMgh.mCurrentState == mMgh.mDelegatingState);
}
@Test
public void testScroll_zoomedStateAndAtEdge_delegatingState() {
goFromStateIdleTo(STATE_ACTIVATED);
mFullScreenMagnificationController.setCenter(
DISPLAY_0,
INITIAL_MAGNIFICATION_BOUNDS.left,
INITIAL_MAGNIFICATION_BOUNDS.top / 2,
false,
1);
final float swipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1;
PointF initCoords =
new PointF(
mFullScreenMagnificationController.getCenterX(DISPLAY_0),
mFullScreenMagnificationController.getCenterY(DISPLAY_0));
PointF endCoords = new PointF(initCoords.x, initCoords.y);
endCoords.offset(swipeMinDistance, 0);
allowEventDelegation();
swipeAndHold(initCoords, endCoords);
assertTrue(mMgh.mCurrentState == mMgh.mDelegatingState);
assertTrue(isZoomed());
}
@Test
public void testScroll_singlePanningAndAtEdge_delegatingState() {
goFromStateIdleTo(STATE_SINGLE_PANNING);
mFullScreenMagnificationController.setCenter(
DISPLAY_0,
INITIAL_MAGNIFICATION_BOUNDS.left,
INITIAL_MAGNIFICATION_BOUNDS.top / 2,
false,
1);
final float swipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1;
PointF initCoords =
new PointF(
mFullScreenMagnificationController.getCenterX(DISPLAY_0),
mFullScreenMagnificationController.getCenterY(DISPLAY_0));
PointF endCoords = new PointF(initCoords.x, initCoords.y);
endCoords.offset(swipeMinDistance, 0);
allowEventDelegation();
swipeAndHold(initCoords, endCoords);
assertTrue(mMgh.mCurrentState == mMgh.mDelegatingState);
assertTrue(isZoomed());
}
@Test
public void testShortcutTriggered_invokeShowWindowPromptAction() {
goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED);
@@ -740,6 +829,10 @@ public class FullScreenMagnificationGestureHandlerTest {
state);
check(mMgh.mPanningScalingState.mScaling, state);
} break;
case STATE_SINGLE_PANNING: {
check(isZoomed(), state);
check(mMgh.mCurrentState == mMgh.mSinglePanningState, state);
} break;
default: throw new IllegalArgumentException("Illegal state: " + state);
}
}
@@ -803,6 +896,10 @@ public class FullScreenMagnificationGestureHandlerTest {
send(pointerEvent(ACTION_MOVE, DEFAULT_X * 2, DEFAULT_Y * 4));
send(pointerEvent(ACTION_MOVE, DEFAULT_X * 2, DEFAULT_Y * 5));
} break;
case STATE_SINGLE_PANNING: {
goFromStateIdleTo(STATE_ACTIVATED);
swipeAndHold();
} break;
default:
throw new IllegalArgumentException("Illegal state: " + state);
}
@@ -859,6 +956,10 @@ public class FullScreenMagnificationGestureHandlerTest {
case STATE_SCALING_AND_PANNING: {
returnToNormalFrom(STATE_PANNING);
} break;
case STATE_SINGLE_PANNING: {
send(upEvent());
returnToNormalFrom(STATE_ACTIVATED);
} break;
default: throw new IllegalArgumentException("Illegal state: " + state);
}
}
@@ -906,6 +1007,11 @@ public class FullScreenMagnificationGestureHandlerTest {
send(moveEvent(DEFAULT_X * 2, DEFAULT_Y * 2));
}
private void swipeAndHold(PointF start, PointF end) {
send(downEvent(start.x, start.y));
send(moveEvent(end.x, end.y));
}
private void longTap() {
send(downEvent());
fastForward(2000);