Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c013b4b1 authored by Kiara Patricia's avatar Kiara Patricia Committed by Android (Google) Code Review
Browse files

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

parents 7a49a37a cd0678f8
Loading
Loading
Loading
Loading
+202 −22
Original line number Diff line number Diff line
@@ -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;
        }
    }
}
+68 −23
Original line number Diff line number Diff line
@@ -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));