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

Commit a0d043ef authored by Longbo Wei's avatar Longbo Wei Committed by Android (Google) Code Review
Browse files

Merge "a11y: panel - Allow drag and snap to nearest side" into main

parents 3e9d30d8 cca02369
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -123,6 +123,7 @@ public class AutoclickIndicatorView extends View {
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Get the screen dimensions.
        // TODO(b/397944891): Handle device rotation case.
        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
        int screenWidth = displayMetrics.widthPixels;
        int screenHeight = displayMetrics.heightPixels;
+118 −11
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
@@ -51,6 +52,16 @@ public class AutoclickTypePanel {
    public static final int CORNER_TOP_LEFT = 2;
    public static final int CORNER_TOP_RIGHT = 3;

    // Distance between panel and screen edge.
    // TODO(b/396402941): Finalize edge margin.
    private static final int PANEL_EDGE_MARGIN = 15;

    // Touch point when drag starts, it can be anywhere inside the panel.
    private float mTouchStartX, mTouchStartY;
    // Initial panel position in screen coordinates.
    private int mPanelStartX, mPanelStartY;
    private boolean mIsDragging = false;

    // Types of click the AutoclickTypePanel supports.
    @IntDef({
        AUTOCLICK_TYPE_LEFT_CLICK,
@@ -101,6 +112,8 @@ public class AutoclickTypePanel {

    private final WindowManager mWindowManager;

    private WindowManager.LayoutParams mParams;

    private final ClickPanelControllerInterface mClickPanelController;

    // Whether the panel is expanded or not.
@@ -133,6 +146,7 @@ public class AutoclickTypePanel {
        mContext = context;
        mWindowManager = windowManager;
        mClickPanelController = clickPanelController;
        mParams = getDefaultLayoutParams();

        mPauseButtonDrawable = mContext.getDrawable(
                R.drawable.accessibility_autoclick_pause);
@@ -154,6 +168,91 @@ public class AutoclickTypePanel {
        mPositionButton = mContentView.findViewById(R.id.accessibility_autoclick_position_layout);

        initializeButtonState();

        // Set up touch event handling for the panel to allow the user to drag and reposition the
        // panel by touching and moving it.
        mContentView.setOnTouchListener(this::onPanelTouch);
    }

    /**
     * Handles touch events on the panel, enabling the user to drag and reposition it.
     * This function supports the draggable panel feature, allowing users to move the panel
     * to different screen locations for better usability and customization.
     */
    private boolean onPanelTouch(View v, MotionEvent event) {
        // TODO(b/397681794): Make sure this works on multiple screens.
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // Store initial touch positions.
                mTouchStartX = event.getRawX();
                mTouchStartY = event.getRawY();

                // Store initial panel position relative to screen's top-left corner.
                // getLocationOnScreen provides coordinates relative to the top-left corner of the
                // screen's display. We are using this coordinate system to consistently track the
                // panel's position during drag operations.
                int[] location = new int[2];
                v.getLocationOnScreen(location);
                mPanelStartX = location[0];
                mPanelStartY = location[1];
                return true;
            case MotionEvent.ACTION_MOVE:
                mIsDragging = true;

                // Set panel gravity to TOP|LEFT to match getLocationOnScreen's coordinate system
                mParams.gravity = Gravity.LEFT | Gravity.TOP;

                if (mIsDragging) {
                    // Calculate touch distance moved from start position.
                    float deltaX = event.getRawX() - mTouchStartX;
                    float deltaY = event.getRawY() - mTouchStartY;

                    // Update panel position, based on Top-Left absolute positioning.
                    mParams.x = mPanelStartX + (int) deltaX;
                    mParams.y = mPanelStartY + (int) deltaY;
                    mWindowManager.updateViewLayout(mContentView, mParams);
                }
                return true;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (mIsDragging) {
                    // When drag ends, snap panel to nearest edge.
                    snapToNearestEdge(mParams);
                }
                mIsDragging = false;
                return true;
        }
        return false;
    }

    private void snapToNearestEdge(WindowManager.LayoutParams params) {
        // Get screen width to determine which side to snap to.
        // TODO(b/397944891): Handle device rotation case.
        int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels;
        int yPosition = params.y;

        // Determine which half of the screen the panel is on.
        boolean isOnLeftHalf = params.x < screenWidth / 2;

        if (isOnLeftHalf) {
            // Snap to left edge. Set params.gravity to make sure x, y offsets from correct anchor.
            params.gravity = Gravity.START | Gravity.TOP;
            // Set the current corner to be bottom-left to ensure that the subsequent reposition
            // action rotates the panel clockwise from bottom-left towards top-left.
            mCurrentCornerIndex = 1;
        } else {
            // Snap to right edge. Set params.gravity to make sure x, y offsets from correct anchor.
            params.gravity = Gravity.END | Gravity.TOP;
            // Set the current corner to be top-right to ensure that the subsequent reposition
            // action rotates the panel clockwise from top-right towards bottom-right.
            mCurrentCornerIndex = 3;
        }

        // Apply final position: set params.x to be edge margin, params.y to maintain vertical
        // position.
        params.x = PANEL_EDGE_MARGIN;
        params.y = yPosition;
        mWindowManager.updateViewLayout(mContentView, params);
    }

    private void initializeButtonState() {
@@ -209,7 +308,7 @@ public class AutoclickTypePanel {
    }

    public void show() {
        mWindowManager.addView(mContentView, getLayoutParams());
        mWindowManager.addView(mContentView, mParams);
    }

    public void hide() {
@@ -291,9 +390,8 @@ public class AutoclickTypePanel {
        @Corner int nextCornerIndex = (mCurrentCornerIndex + 1) % CORNER_ROTATION_ORDER.length;
        mCurrentCornerIndex = nextCornerIndex;

        // getLayoutParams() will update the panel position based on current corner.
        WindowManager.LayoutParams params = getLayoutParams();
        mWindowManager.updateViewLayout(mContentView, params);
        setPanelPositionForCorner(mParams, mCurrentCornerIndex);
        mWindowManager.updateViewLayout(mContentView, mParams);
    }

    private void setPanelPositionForCorner(WindowManager.LayoutParams params, @Corner int corner) {
@@ -303,22 +401,22 @@ public class AutoclickTypePanel {
        switch (corner) {
            case CORNER_BOTTOM_RIGHT:
                params.gravity = Gravity.END | Gravity.BOTTOM;
                params.x = 15;
                params.x = PANEL_EDGE_MARGIN;
                params.y = 90;
                break;
            case CORNER_BOTTOM_LEFT:
                params.gravity = Gravity.START | Gravity.BOTTOM;
                params.x = 15;
                params.x = PANEL_EDGE_MARGIN;
                params.y = 90;
                break;
            case CORNER_TOP_LEFT:
                params.gravity = Gravity.START | Gravity.TOP;
                params.x = 15;
                params.x = PANEL_EDGE_MARGIN;
                params.y = 30;
                break;
            case CORNER_TOP_RIGHT:
                params.gravity = Gravity.END | Gravity.TOP;
                params.x = 15;
                params.x = PANEL_EDGE_MARGIN;
                params.y = 30;
                break;
            default:
@@ -343,13 +441,22 @@ public class AutoclickTypePanel {
        return mCurrentCornerIndex;
    }

    @VisibleForTesting
    WindowManager.LayoutParams getLayoutParamsForTesting() {
        return mParams;
    }

    @VisibleForTesting
    boolean getIsDraggingForTesting() {
        return mIsDragging;
    }

    /**
     * Retrieves the layout params for AutoclickIndicatorView, used when it's added to the Window
     * Manager.
     */
    @VisibleForTesting
    @NonNull
    WindowManager.LayoutParams getLayoutParams() {
    private WindowManager.LayoutParams getDefaultLayoutParams() {
        final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
        layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
@@ -362,7 +469,7 @@ public class AutoclickTypePanel {
                mContext.getString(R.string.accessibility_autoclick_type_settings_panel_title);
        layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
        setPanelPositionForCorner(layoutParams, mCurrentCornerIndex);
        setPanelPositionForCorner(layoutParams, CORNER_BOTTOM_RIGHT);
        return layoutParams;
    }
}
+104 −1
Original line number Diff line number Diff line
@@ -21,6 +21,9 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AutoclickType;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.CORNER_BOTTOM_LEFT;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.CORNER_BOTTOM_RIGHT;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.CORNER_TOP_RIGHT;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.ClickPanelControllerInterface;

import static com.google.common.truth.Truth.assertThat;
@@ -31,6 +34,7 @@ import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.LinearLayout;
@@ -213,6 +217,105 @@ public class AutoclickTypePanelTest {
        assertThat(mAutoclickTypePanel.isPaused()).isFalse();
    }

    @Test
    public void onTouch_dragMove_updatesPosition() {
        View contentView = mAutoclickTypePanel.getContentViewForTesting();
        WindowManager.LayoutParams params = mAutoclickTypePanel.getLayoutParamsForTesting();
        int[] panelLocation = new int[2];
        contentView.getLocationOnScreen(panelLocation);

        // Define movement delta for both x and y directions.
        int delta = 15;

        // Dispatch initial down event.
        float touchX = panelLocation[0] + 10;
        float touchY = panelLocation[1] + 10;
        MotionEvent downEvent = MotionEvent.obtain(
                0, 0,
                MotionEvent.ACTION_DOWN, touchX, touchY, 0);
        contentView.dispatchTouchEvent(downEvent);

        // Create move event with delta, move from (x, y) to (x + delta, y + delta)
        MotionEvent moveEvent = MotionEvent.obtain(
                0, 0,
                MotionEvent.ACTION_MOVE, touchX + delta, touchY + delta, 0);
        contentView.dispatchTouchEvent(moveEvent);

        // Verify position update.
        assertThat(mAutoclickTypePanel.getIsDraggingForTesting()).isTrue();
        assertThat(params.gravity).isEqualTo(Gravity.LEFT | Gravity.TOP);
        assertThat(params.x).isEqualTo(panelLocation[0] + delta);
        assertThat(params.y).isEqualTo(panelLocation[1] + delta);
    }

    @Test
    public void dragAndEndAtRight_snapsToRightSide() {
        View contentView = mAutoclickTypePanel.getContentViewForTesting();
        WindowManager.LayoutParams params = mAutoclickTypePanel.getLayoutParamsForTesting();
        int[] panelLocation = new int[2];
        contentView.getLocationOnScreen(panelLocation);

        int screenWidth = mTestableContext.getResources().getDisplayMetrics().widthPixels;

        // Verify initial corner is bottom-right.
        assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting())
                .isEqualTo(CORNER_BOTTOM_RIGHT);

        dispatchDragSequence(contentView,
                /* startX =*/ panelLocation[0] + 10, /* startY =*/ panelLocation[1] + 10,
                /* endX =*/ (float) (screenWidth * 3) / 4, /* endY =*/ panelLocation[1] + 10);

        // Verify snapping to the right.
        assertThat(params.gravity).isEqualTo(Gravity.END | Gravity.TOP);
        assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting())
                .isEqualTo(CORNER_TOP_RIGHT);
    }

    @Test
    public void dragAndEndAtLeft_snapsToLeftSide() {
        View contentView = mAutoclickTypePanel.getContentViewForTesting();
        WindowManager.LayoutParams params = mAutoclickTypePanel.getLayoutParamsForTesting();
        int[] panelLocation = new int[2];
        contentView.getLocationOnScreen(panelLocation);

        int screenWidth = mTestableContext.getResources().getDisplayMetrics().widthPixels;

        // Verify initial corner is bottom-right.
        assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting())
                .isEqualTo(CORNER_BOTTOM_RIGHT);

        dispatchDragSequence(contentView,
                /* startX =*/ panelLocation[0] + 10, /* startY =*/ panelLocation[1] + 10,
                /* endX =*/ (float) screenWidth / 4, /* endY =*/ panelLocation[1] + 10);

        // Verify snapping to the left.
        assertThat(params.gravity).isEqualTo(Gravity.START | Gravity.TOP);
        assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting())
                .isEqualTo(CORNER_BOTTOM_LEFT);
    }

    // Helper method to handle drag event sequences
    private void dispatchDragSequence(View view, float startX, float startY, float endX,
            float endY) {
        // Down event
        MotionEvent downEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, startX, startY,
                0);
        view.dispatchTouchEvent(downEvent);

        // Move event
        MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, endX, endY, 0);
        view.dispatchTouchEvent(moveEvent);

        // Up event
        MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, endX, endY, 0);
        view.dispatchTouchEvent(upEvent);

        // Clean up
        downEvent.recycle();
        moveEvent.recycle();
        upEvent.recycle();
    }

    private void verifyButtonHasSelectedStyle(@NonNull LinearLayout button) {
        GradientDrawable gradientDrawable = (GradientDrawable) button.getBackground();
        assertThat(gradientDrawable.getColor().getDefaultColor())
@@ -220,7 +323,7 @@ public class AutoclickTypePanelTest {
    }

    private void verifyPanelPosition(int[] expectedPosition) {
        WindowManager.LayoutParams params = mAutoclickTypePanel.getLayoutParams();
        WindowManager.LayoutParams params = mAutoclickTypePanel.getLayoutParamsForTesting();
        assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting()).isEqualTo(
                expectedPosition[0]);
        assertThat(params.gravity).isEqualTo(expectedPosition[1]);