Merge "Add warp effect when user is in magnification mode" into main

This commit is contained in:
Kiara Patricia
2023-07-27 19:21:01 +00:00
committed by Android (Google) Code Review
2 changed files with 270 additions and 45 deletions

View File

@@ -33,6 +33,7 @@ import static java.util.Arrays.asList;
import static java.util.Arrays.copyOfRange;
import android.accessibilityservice.MagnificationConfig;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiContext;
@@ -41,6 +42,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Handler;
import android.os.Looper;
@@ -147,6 +149,20 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
private PointerCoords[] mTempPointerCoords;
private PointerProperties[] mTempPointerProperties;
@VisibleForTesting static final int OVERSCROLL_NONE = 0;
@VisibleForTesting static final int OVERSCROLL_LEFT_EDGE = 1;
@VisibleForTesting static final int OVERSCROLL_RIGHT_EDGE = 2;
@VisibleForTesting static final int OVERSCROLL_VERTICAL_EDGE = 3;
@IntDef({
OVERSCROLL_NONE,
OVERSCROLL_LEFT_EDGE,
OVERSCROLL_RIGHT_EDGE,
OVERSCROLL_VERTICAL_EDGE
})
public @interface OverscrollState {}
@VisibleForTesting boolean mIsSinglePanningEnabled;
public FullScreenMagnificationGestureHandler(@UiContext Context context,
FullScreenMagnificationController fullScreenMagnificationController,
@@ -842,8 +858,11 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
transitToPanningScalingStateAndClear();
} else if (mIsSinglePanningEnabled
&& isActivated()
&& event.getPointerCount() == 1
&& !isOverscroll(event)) {
&& event.getPointerCount() == 1) {
if (overscrollState(event, mFirstPointerDownLocation)
== OVERSCROLL_VERTICAL_EDGE) {
transitionToDelegatingStateAndClear();
}
transitToSinglePanningStateAndClear();
} else {
transitionToDelegatingStateAndClear();
@@ -881,20 +900,6 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
}
}
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 void storePointerDownLocation(PointF pointerDownLocation, MotionEvent event) {
final int index = event.getActionIndex();
pointerDownLocation.set(event.getX(index), event.getY(index));
@@ -1172,7 +1177,9 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
}
private void zoomOn(float centerX, float centerY) {
if (DEBUG_DETECTING) Slog.i(mLogTag, "zoomOn(" + centerX + ", " + centerY + ")");
if (DEBUG_DETECTING) {
Slog.i(mLogTag, "zoomOn(" + centerX + ", " + centerY + ")");
}
final float scale = MathUtils.constrain(
mFullScreenMagnificationController.getPersistedScale(mDisplayId),
@@ -1189,7 +1196,9 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
}
private void zoomOff() {
if (DEBUG_DETECTING) Slog.i(mLogTag, "zoomOff()");
if (DEBUG_DETECTING) {
Slog.i(mLogTag, "zoomOff()");
}
mFullScreenMagnificationController.reset(mDisplayId, /* animate */ true);
}
@@ -1218,6 +1227,28 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
+ '}';
}
private int overscrollState(MotionEvent event, PointF firstPointerDownLocation) {
if (!pointerValid(firstPointerDownLocation)) {
return OVERSCROLL_NONE;
}
float dX = event.getX() - firstPointerDownLocation.x;
float dY = event.getY() - firstPointerDownLocation.y;
if (mFullScreenMagnificationController.isAtLeftEdge(mDisplayId) && dX > 0) {
return OVERSCROLL_LEFT_EDGE;
} else if (mFullScreenMagnificationController.isAtRightEdge(mDisplayId) && dX < 0) {
return OVERSCROLL_RIGHT_EDGE;
} else if (mFullScreenMagnificationController.isAtTopEdge(mDisplayId) && dY > 0
|| mFullScreenMagnificationController.isAtBottomEdge(mDisplayId) && dY < 0) {
return OVERSCROLL_VERTICAL_EDGE;
}
return OVERSCROLL_NONE;
}
private boolean pointerValid(PointF pointerDownLocation) {
return !(Float.isNaN(pointerDownLocation.x) && Float.isNaN(pointerDownLocation.y));
}
private static final class MotionEventInfo {
private static final int MAX_POOL_SIZE = 10;
@@ -1343,10 +1374,29 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
private final FullScreenMagnificationVibrationHelper
mFullScreenMagnificationVibrationHelper;
SinglePanningState(Context context, FullScreenMagnificationVibrationHelper
fullScreenMagnificationVibrationHelper) {
@VisibleForTesting int mOverscrollState;
// mPivotEdge is the point on the edge of the screen when the magnified view hits the edge
// This point sets the center of magnified view when warp/scale effect is triggered
private final PointF mPivotEdge;
// mReachedEdgeCoord is the user's pointer location on the screen when the magnified view
// has hit the edge
private final PointF mReachedEdgeCoord;
// mEdgeCooldown value will be set to true when user hits the edge and will be set to false
// once the user moves x distance away from the edge. This is so that vibrating haptic
// doesn't get triggered by slight movements
private boolean mEdgeCooldown;
SinglePanningState(
Context context,
FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper) {
mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain());
mFullScreenMagnificationVibrationHelper = fullScreenMagnificationVibrationHelper;
mOverscrollState = OVERSCROLL_NONE;
mPivotEdge = new PointF(Float.NaN, Float.NaN);
mReachedEdgeCoord = new PointF(Float.NaN, Float.NaN);
mEdgeCooldown = false;
}
@Override
@@ -1355,6 +1405,16 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
switch (action) {
case ACTION_UP:
case ACTION_CANCEL:
if (mOverscrollState == OVERSCROLL_LEFT_EDGE
|| mOverscrollState == OVERSCROLL_RIGHT_EDGE) {
mFullScreenMagnificationController.setScaleAndCenter(
mDisplayId,
mFullScreenMagnificationController.getPersistedScale(mDisplayId),
mPivotEdge.x,
mPivotEdge.y,
true,
AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
}
clear();
transitionTo(mDetectingState);
break;
@@ -1382,11 +1442,22 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
+ " isAtEdge: "
+ mFullScreenMagnificationController.isAtEdge(mDisplayId));
}
// TODO: b/280812104 Dispatch events before Delegation
if (mFullScreenMagnificationController.isAtEdge(mDisplayId)) {
playEdgeVibration(second);
setPivotEdge();
}
if (mOverscrollState == OVERSCROLL_NONE) {
mOverscrollState = overscrollState(second, new PointF(first.getX(), first.getY()));
} else if (mOverscrollState == OVERSCROLL_VERTICAL_EDGE) {
clear();
transitionTo(mDelegatingState);
vibrateIfNeeded();
} else {
boolean reset = warpEffectReset(second);
if (reset) {
mFullScreenMagnificationController.reset(mDisplayId, /* animate */ true);
clear();
transitionTo(mDetectingState);
}
}
return /* event consumed: */ true;
}
@@ -1405,7 +1476,116 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
return "SinglePanningState{"
+ "isEdgeOfView="
+ mFullScreenMagnificationController.isAtEdge(mDisplayId)
+ "overscrollStatus="
+ mOverscrollState
+ "}";
}
private void playEdgeVibration(MotionEvent event) {
if (mOverscrollState == OVERSCROLL_NONE) {
vibrateIfNeeded(event);
}
}
private void setPivotEdge() {
if (!pointerValid(mPivotEdge)) {
Rect bounds = new Rect();
mFullScreenMagnificationController.getMagnificationBounds(mDisplayId, bounds);
if (mOverscrollState == OVERSCROLL_LEFT_EDGE) {
mPivotEdge.set(
bounds.left,
mFullScreenMagnificationController.getCenterY(mDisplayId));
} else if (mOverscrollState == OVERSCROLL_RIGHT_EDGE) {
mPivotEdge.set(
bounds.right,
mFullScreenMagnificationController.getCenterY(mDisplayId));
}
}
}
private boolean warpEffectReset(MotionEvent second) {
float scale = calculateOverscrollScale(second);
if (scale < 0) return false;
mFullScreenMagnificationController.setScaleAndCenter(
/* displayId= */ mDisplayId,
/* scale= */ scale,
/* centerX= */ mPivotEdge.x,
/* centerY= */ mPivotEdge.y,
/* animate= */ true,
/* id= */ AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
if (scale == 1.0f) {
return true;
}
return false;
}
private float calculateOverscrollScale(MotionEvent second) {
// if at left and overshootDistX is negative or if at right and overshootDistX is
// positive then user is not in overscroll state anymore overscroll state. Reset
// overscroll values by clearing
float overshootDistX = second.getX() - mReachedEdgeCoord.x;
if ((mOverscrollState == OVERSCROLL_LEFT_EDGE && overshootDistX < 0)
|| (mOverscrollState == OVERSCROLL_RIGHT_EDGE && overshootDistX > 0)) {
clear();
return -1.0f;
}
float overshootDistY = second.getY() - mReachedEdgeCoord.y;
float overshootDist = (float) (Math.hypot(abs(overshootDistX), abs(overshootDistY)));
Rect bounds = new Rect();
mFullScreenMagnificationController.getMagnificationBounds(mDisplayId, bounds);
float overShootFraction = overshootDist / (float) bounds.width();
float minDist = 0.05f * bounds.width();
if (mEdgeCooldown && (overshootDist > minDist)) {
mEdgeCooldown = false;
}
float scale = (1 - overShootFraction) * getSensitivityScale();
scale =
MathUtils.constrain(
/* amount= */ scale,
/* low= */ 1.0f,
/* high= */ mFullScreenMagnificationController.getPersistedScale(
mDisplayId));
return scale;
}
private float getSensitivityScale() {
float magnificationScale =
mFullScreenMagnificationController.getPersistedScale(mDisplayId);
float sensitivityFactor = 0.0f;
if (magnificationScale < 1.7f) {
sensitivityFactor = 1.0f;
} else if (magnificationScale < 2.0f) {
sensitivityFactor = 1.0f;
} else if (magnificationScale < 2.2f) {
sensitivityFactor = 0.95f;
} else if (magnificationScale < 2.5f) {
sensitivityFactor = 1.1f;
} else if (magnificationScale < 2.7f) {
sensitivityFactor = 1.3f;
} else if (magnificationScale < 3.0f) {
sensitivityFactor = 1.0f;
} else {
sensitivityFactor = 1.0f;
}
return magnificationScale * sensitivityFactor;
}
private void vibrateIfNeeded(MotionEvent event) {
if ((mFullScreenMagnificationController.isAtLeftEdge(mDisplayId)
|| mFullScreenMagnificationController.isAtRightEdge(mDisplayId))
&& !mEdgeCooldown) {
mFullScreenMagnificationVibrationHelper.vibrateIfSettingEnabled();
mReachedEdgeCoord.set(event.getX(), event.getY());
mEdgeCooldown = true;
}
}
@Override
public void clear() {
mOverscrollState = OVERSCROLL_NONE;
mPivotEdge.set(Float.NaN, Float.NaN);
mReachedEdgeCoord.set(Float.NaN, Float.NaN);
mEdgeCooldown = false;
}
}
}

View File

@@ -589,51 +589,91 @@ public class FullScreenMagnificationGestureHandlerTest {
}
@Test
public void testScroll_zoomedStateAndAtEdge_delegatingState() {
goFromStateIdleTo(STATE_ACTIVATED);
@FlakyTest
public void testScroll_singleHorizontalPanningAndAtEdge_leftEdgeOverscroll() {
goFromStateIdleTo(STATE_SINGLE_PANNING);
float centerY =
(INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.bottom) / 2.0f;
mFullScreenMagnificationController.setCenter(
DISPLAY_0,
INITIAL_MAGNIFICATION_BOUNDS.left,
INITIAL_MAGNIFICATION_BOUNDS.top / 2,
false,
1);
DISPLAY_0, INITIAL_MAGNIFICATION_BOUNDS.left, centerY, 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();
PointF edgeCoords = new PointF(initCoords.x, initCoords.y);
edgeCoords.offset(swipeMinDistance, 0);
swipeAndHold(initCoords, endCoords);
swipeAndHold(initCoords, edgeCoords);
assertTrue(mMgh.mCurrentState == mMgh.mDelegatingState);
assertTrue(mMgh.mCurrentState == mMgh.mSinglePanningState);
assertTrue(mMgh.mSinglePanningState.mOverscrollState == mMgh.OVERSCROLL_LEFT_EDGE);
assertTrue(isZoomed());
}
@Test
@FlakyTest
public void testScroll_singlePanningAndAtEdge_delegatingState() {
public void testScroll_singleHorizontalPanningAndAtEdge_rightEdgeOverscroll() {
goFromStateIdleTo(STATE_SINGLE_PANNING);
float centerY =
(INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.bottom) / 2.0f;
mFullScreenMagnificationController.setCenter(
DISPLAY_0,
INITIAL_MAGNIFICATION_BOUNDS.left,
INITIAL_MAGNIFICATION_BOUNDS.top / 2,
false,
1);
DISPLAY_0, INITIAL_MAGNIFICATION_BOUNDS.right, centerY, 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();
PointF edgeCoords = new PointF(initCoords.x, initCoords.y);
edgeCoords.offset(-swipeMinDistance, 0);
swipeAndHold(initCoords, endCoords);
swipeAndHold(initCoords, edgeCoords);
assertTrue(mMgh.mCurrentState == mMgh.mDelegatingState);
assertTrue(mMgh.mCurrentState == mMgh.mSinglePanningState);
assertTrue(mMgh.mSinglePanningState.mOverscrollState == mMgh.OVERSCROLL_RIGHT_EDGE);
assertTrue(isZoomed());
}
@Test
@FlakyTest
public void testScroll_singleVerticalPanningAndAtEdge_verticalOverscroll() {
goFromStateIdleTo(STATE_SINGLE_PANNING);
float centerX =
(INITIAL_MAGNIFICATION_BOUNDS.right + INITIAL_MAGNIFICATION_BOUNDS.left) / 2.0f;
mFullScreenMagnificationController.setCenter(
DISPLAY_0, centerX, INITIAL_MAGNIFICATION_BOUNDS.top, false, 1);
final float swipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1;
PointF initCoords =
new PointF(
mFullScreenMagnificationController.getCenterX(DISPLAY_0),
mFullScreenMagnificationController.getCenterY(DISPLAY_0));
PointF edgeCoords = new PointF(initCoords.x, initCoords.y);
edgeCoords.offset(0, swipeMinDistance);
swipeAndHold(initCoords, edgeCoords);
assertTrue(mMgh.mSinglePanningState.mOverscrollState == mMgh.OVERSCROLL_VERTICAL_EDGE);
assertTrue(isZoomed());
}
@Test
public void testScroll_singlePanningAndAtEdge_noOverscroll() {
goFromStateIdleTo(STATE_SINGLE_PANNING);
float centerY =
(INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.bottom) / 2.0f;
mFullScreenMagnificationController.setCenter(
DISPLAY_0, INITIAL_MAGNIFICATION_BOUNDS.left, centerY, false, 1);
final float swipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1;
PointF initCoords =
new PointF(
mFullScreenMagnificationController.getCenterX(DISPLAY_0),
mFullScreenMagnificationController.getCenterY(DISPLAY_0));
PointF edgeCoords = new PointF(initCoords.x, initCoords.y);
edgeCoords.offset(-swipeMinDistance, 0);
swipeAndHold(initCoords, edgeCoords);
assertTrue(mMgh.mSinglePanningState.mOverscrollState == mMgh.OVERSCROLL_NONE);
assertTrue(isZoomed());
}
@@ -1105,6 +1145,11 @@ public class FullScreenMagnificationGestureHandlerTest {
send(upEvent());
}
private void swipe(PointF start, PointF end) {
swipeAndHold(start, end);
send(upEvent(end.x, end.y));
}
private void swipeAndHold() {
send(downEvent());
send(moveEvent(DEFAULT_X * 2, DEFAULT_Y * 2));