Add warp effect when user is in magnification mode
When user is single finger panning in magnified view and hits one of the horizontal edges magnified view will scale out. When fully scaled out it will transition to delegating state Test: manual Bug: 278271684, 279498927 Change-Id: I3c8896956279592e5360eed8830c5a3b3fdbb425 (cherry picked from commit a1fbfa60124229118be1d1b06d73ba2ee6b80991)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user