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

Commit 1b54146d authored by “Longbo's avatar “Longbo Committed by Longbo Wei
Browse files

autoclick: Improve Scroll Panel Positioning

This CL addresses 2 critical issues with the scroll panel positioning
logic

Issues:
1. Inconsistent cursor-to-panel distance
When the cursor is at the top-left or top-right, the panel appears farther away. When it’s at the bottom-left or bottom-right, it appears closer. This inconsistency is caused by incorrect Y-offset calculation that doesn’t account for the status bar height.

2. Panel overlapping with cursor near bottom-left
When the cursor is near the bottom-left corner of the screen, the panel may overlap with the cursor point after repositioning. This overlap may not be visually noticeable, but since the panel still occupies that rectangle space, it can block scroll interactions.

Fixes:
1. Consistent positioning
We now subtract the status bar height when calculating the Y-coordinate to ensure the panel is placed at a consistent distance from the cursor.

2. Avoid overlap
Instead of using a simple placement approach, we now calculate (cursorX ± xOffset, cursorY ± yOffset) as the target center of the panel. From there, we can get the top-left corner (panelX, panelY) and attempt 4 directions to position the panel while avoiding overlap with the cursor point.

Video:
Before: http://shortn/_o6O0mvF4Fo
After:  http://shortn/_fFwqLYmoXy

Bug: b/421239851
Test: atest
Flag: com.android.server.accessibility.enable_autoclick_indicator
Change-Id: Id2e6d25425dafa64dad6024439bf72c04869bec5
parent 484668de
Loading
Loading
Loading
Loading
+46 −18
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;

import com.android.internal.R;
import com.android.internal.policy.SystemBarUtils;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -89,6 +90,8 @@ public class AutoclickScrollPanel {
    private final ImageButton mRightButton;
    private final ImageButton mExitButton;

    private final int mStatusBarHeight;

    private boolean mInScrollMode = false;

    // Panel size determined after measuring.
@@ -122,6 +125,7 @@ public class AutoclickScrollPanel {
        mContentView = (AutoclickLinearLayout) LayoutInflater.from(context).inflate(
                R.layout.accessibility_autoclick_scroll_panel, null);
        mParams = getDefaultLayoutParams();
        mStatusBarHeight = SystemBarUtils.getStatusBarHeight(context);

        // Initialize buttons.
        mUpButton = mContentView.findViewById(R.id.scroll_up);
@@ -190,8 +194,7 @@ public class AutoclickScrollPanel {
    /**
     * Positions the panel at the bottom right of the cursor coordinates,
     * ensuring it stays within the screen boundaries.
     * If the panel would go off the right or bottom edge, it's repositioned
     * to the left or above the cursor, respectively.
     * If the panel would go off the right or bottom edge, tries other diagonal directions.
     * The panel's gravity is set to TOP|LEFT for absolute positioning.
     */
    protected void positionPanelAtCursor(float cursorX, float cursorY) {
@@ -204,24 +207,34 @@ public class AutoclickScrollPanel {
        int screenWidth = displayMetrics.widthPixels;
        int screenHeight = displayMetrics.heightPixels;

        // Calculate initial position.
        int panelX = (int) cursorX;
        int panelY = (int) cursorY;

        // Check if panel would go off right edge of screen.
        if (panelX + mPanelWidth > screenWidth - PANEL_EDGE_MARGIN) {
            // Place to the left of cursor instead if no space left for right edge.
            panelX = (int) cursorX - mPanelWidth;
        // Adjust Y for status bar height.
        float adjustedCursorY = cursorY - mStatusBarHeight;

        // Offset from cursor point to panel center.
        int margin = 10;
        int xOffset = mPanelWidth / 2 + margin;
        int yOffset = mPanelHeight / 2 + margin;

        // Try 4 diagonal positions: bottom-right, bottom-left, top-right, top-left.
        int[][] directions = {{+1, +1}, {-1, +1}, {+1, -1}, {-1, -1}};
        for (int[] dir : directions) {
            // (panelX, panelY) is the top-left point of the panel.
            int panelX = (int) (cursorX + dir[0] * xOffset - mPanelWidth / 2);
            int panelY = (int) (adjustedCursorY + dir[1] * yOffset - mPanelHeight / 2);
            if (isWithinBounds(panelX, panelY, screenWidth, screenHeight)) {
                mParams.x = panelX;
                mParams.y = panelY;
                return;
            }
        }

        // Check if panel would go off bottom edge of screen.
        if (panelY + mPanelHeight > screenHeight - PANEL_EDGE_MARGIN) {
            // Place above cursor instead if no space left for bottom edge.
            panelY = (int) cursorY - mPanelHeight;
    }

        mParams.x = panelX;
        mParams.y = panelY;
    /**
     * Returns true if the panel fits on screen with margin.
     */
    private boolean isWithinBounds(int x, int y, int screenWidth, int screenHeight) {
        return x > PANEL_EDGE_MARGIN && x + mPanelWidth + PANEL_EDGE_MARGIN < screenWidth
                && y > PANEL_EDGE_MARGIN && y + mPanelHeight + PANEL_EDGE_MARGIN < screenHeight;
    }

    /**
@@ -336,4 +349,19 @@ public class AutoclickScrollPanel {
    public WindowManager.LayoutParams getLayoutParamsForTesting() {
        return mParams;
    }

    @VisibleForTesting
    public int getPanelWidthForTesting() {
        return mPanelWidth;
    }

    @VisibleForTesting
    public int getPanelHeightForTesting() {
        return mPanelHeight;
    }

    @VisibleForTesting
    public int getStatusBarHeightForTesting() {
        return mStatusBarHeight;
    }
}
+40 −8
Original line number Diff line number Diff line
@@ -274,17 +274,25 @@ public class AutoclickScrollPanelTest {

    @Test
    public void showPanel_normalCase() {
        // Normal case, position at (10, 10).
        int cursorX = 10;
        int cursorY = 10;
        // Normal case, position at (100, 100).
        int cursorX = 100;
        int cursorY = 100;

        // Capture the current layout params before positioning.
        WindowManager.LayoutParams params = mScrollPanel.getLayoutParamsForTesting();
        mScrollPanel.positionPanelAtCursor(cursorX, cursorY);

        // Panel should be at cursor position (gravity is LEFT|TOP).
        assertThat(params.x).isEqualTo(cursorX);
        assertThat(params.y).isEqualTo(cursorY);
        // Calculate expected position for bottom-right placement.
        int margin = 10;
        int xOffset = mScrollPanel.getPanelWidthForTesting() / 2 + margin;
        int yOffset = mScrollPanel.getPanelHeightForTesting() / 2 + margin;
        int expectedX = cursorX + xOffset - mScrollPanel.getPanelWidthForTesting() / 2;
        int expectedY = (cursorY - mScrollPanel.getStatusBarHeightForTesting()) + yOffset
                - mScrollPanel.getPanelHeightForTesting() / 2;

        // Verify panel's position.
        assertThat(params.x).isEqualTo(expectedX);
        assertThat(params.y).isEqualTo(expectedY);
    }

    @Test
@@ -316,7 +324,7 @@ public class AutoclickScrollPanelTest {
        mScrollPanel.positionPanelAtCursor(cursorX, cursorY);

        // Panel should be above cursor.
        assertThat(params.y).isLessThan(cursorY);
        assertThat(params.y).isLessThan(cursorY - mScrollPanel.getStatusBarHeightForTesting());
    }

    @Test
@@ -333,7 +341,31 @@ public class AutoclickScrollPanelTest {

        // Panel should be left of and above cursor.
        assertThat(params.x).isLessThan(cursorX);
        assertThat(params.y).isLessThan(cursorY);
        assertThat(params.y).isLessThan(cursorY - mScrollPanel.getStatusBarHeightForTesting());
    }

    @Test
    public void showPanel_closeToEdge_withinBounds() {
        // Test edge case where cursor is very close to edge, panel should still be positioned
        // within PANEL_EDGE_MARGIN (15px).
        int edgeMargin = 15;

        // Near bottom-right corner case.
        // 10px from right edge.
        int cursorX = mScreenWidth - 10;
        // 10px from bottom edge.
        int cursorY = mScreenHeight - 10;

        WindowManager.LayoutParams params = mScrollPanel.getLayoutParamsForTesting();
        mScrollPanel.positionPanelAtCursor(cursorX, cursorY);

        // Verify panel is within bounds with margin.
        assertThat(params.x).isGreaterThan(edgeMargin);
        assertThat(params.y).isGreaterThan(edgeMargin);
        assertThat(params.x + mScrollPanel.getPanelWidthForTesting() + edgeMargin)
                .isLessThan(mScreenWidth);
        assertThat(params.y + mScrollPanel.getPanelHeightForTesting() + edgeMargin)
                .isLessThan(mScreenHeight);
    }

    @Test