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

Commit 1e632ba4 authored by Keisuke Kuroyanagi's avatar Keisuke Kuroyanagi Committed by Android (Google) Code Review
Browse files

Merge "Improve selection handle behavior for bidi text." into nyc-dev

parents 77a1c656 f0bb87b7
Loading
Loading
Loading
Loading
+49 −6
Original line number Diff line number Diff line
@@ -799,6 +799,31 @@ public abstract class Layout {
        return false;
    }

    /**
     * Returns the range of the run that the character at offset belongs to.
     * @param offset the offset
     * @return The range of the run
     * @hide
     */
    public long getRunRange(int offset) {
        int line = getLineForOffset(offset);
        Directions dirs = getLineDirections(line);
        if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) {
            return TextUtils.packRangeInLong(0, getLineEnd(line));
        }
        int[] runs = dirs.mDirections;
        int lineStart = getLineStart(line);
        for (int i = 0; i < runs.length; i += 2) {
            int start = lineStart + runs[i];
            int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
            if (offset >= start && offset < limit) {
                return TextUtils.packRangeInLong(start, limit);
            }
        }
        // Should happen only if the offset is "out of bounds"
        return TextUtils.packRangeInLong(0, getLineEnd(line));
    }

    private boolean primaryIsTrailingPrevious(int offset) {
        int line = getLineForOffset(offset);
        int lineStart = getLineStart(line);
@@ -886,6 +911,10 @@ public abstract class Layout {
        return getHorizontal(offset, !trailing, clamped);
    }

    private float getHorizontal(int offset, boolean primary) {
        return primary ? getPrimaryHorizontal(offset) : getSecondaryHorizontal(offset);
    }

    private float getHorizontal(int offset, boolean trailing, boolean clamped) {
        int line = getLineForOffset(offset);

@@ -1114,6 +1143,20 @@ public abstract class Layout {
     * closest to the specified horizontal position.
     */
    public int getOffsetForHorizontal(int line, float horiz) {
        return getOffsetForHorizontal(line, horiz, true);
    }

    /**
     * Get the character offset on the specified line whose position is
     * closest to the specified horizontal position.
     *
     * @param line the line used to find the closest offset
     * @param horiz the horizontal position used to find the closest offset
     * @param primary whether to use the primary position or secondary position to find the offset
     *
     * @hide
     */
    public int getOffsetForHorizontal(int line, float horiz, boolean primary) {
        // TODO: use Paint.getOffsetForAdvance to avoid binary search
        final int lineEndOffset = getLineEnd(line);
        final int lineStartOffset = getLineStart(line);
@@ -1133,7 +1176,7 @@ public abstract class Layout {
                    !isRtlCharAt(lineEndOffset - 1)) + lineStartOffset;
        }
        int best = lineStartOffset;
        float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz);
        float bestdist = Math.abs(getHorizontal(best, primary) - horiz);

        for (int i = 0; i < dirs.mDirections.length; i += 2) {
            int here = lineStartOffset + dirs.mDirections[i];
@@ -1149,7 +1192,7 @@ public abstract class Layout {
                guess = (high + low) / 2;
                int adguess = getOffsetAtStartOf(guess);

                if (getPrimaryHorizontal(adguess) * swap >= horiz * swap)
                if (getHorizontal(adguess, primary) * swap >= horiz * swap)
                    high = guess;
                else
                    low = guess;
@@ -1162,9 +1205,9 @@ public abstract class Layout {
                int aft = tl.getOffsetToLeftRightOf(low - lineStartOffset, isRtl) + lineStartOffset;
                low = tl.getOffsetToLeftRightOf(aft - lineStartOffset, !isRtl) + lineStartOffset;
                if (low >= here && low < there) {
                    float dist = Math.abs(getPrimaryHorizontal(low) - horiz);
                    float dist = Math.abs(getHorizontal(low, primary) - horiz);
                    if (aft < there) {
                        float other = Math.abs(getPrimaryHorizontal(aft) - horiz);
                        float other = Math.abs(getHorizontal(aft, primary) - horiz);

                        if (other < dist) {
                            dist = other;
@@ -1179,7 +1222,7 @@ public abstract class Layout {
                }
            }

            float dist = Math.abs(getPrimaryHorizontal(here) - horiz);
            float dist = Math.abs(getHorizontal(here, primary) - horiz);

            if (dist < bestdist) {
                bestdist = dist;
@@ -1187,7 +1230,7 @@ public abstract class Layout {
            }
        }

        float dist = Math.abs(getPrimaryHorizontal(max) - horiz);
        float dist = Math.abs(getHorizontal(max, primary) - horiz);

        if (dist <= bestdist) {
            bestdist = dist;
+105 −22
Original line number Diff line number Diff line
@@ -4034,14 +4034,17 @@ public class Editor {
                // Don't update drawable during dragging.
                return;
            }
            final Layout layout = mTextView.getLayout();
            if (layout == null) {
                return;
            }
            final int offset = getCurrentCursorOffset();
            final boolean isRtlCharAtOffset = mTextView.getLayout().isRtlCharAt(offset);
            final boolean isRtlCharAtOffset = isAtRtlRun(layout, offset);
            final Drawable oldDrawable = mDrawable;
            mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
            mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
            mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
            final Layout layout = mTextView.getLayout();
            if (layout != null && oldDrawable != mDrawable && isShowing()) {
            if (oldDrawable != mDrawable && isShowing()) {
                // Update popup window position.
                mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX -
                        getHorizontalOffset() + getCursorOffset();
@@ -4154,6 +4157,19 @@ public class Editor {

        public abstract void updatePosition(float x, float y);

        protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
            return layout.isRtlCharAt(offset);
        }

        @VisibleForTesting
        public float getHorizontal(@NonNull Layout layout, int offset) {
            return layout.getPrimaryHorizontal(offset);
        }

        protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
            return mTextView.getOffsetAtCoordinate(line, x);
        }

        protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
            // A HandleView relies on the layout, which may be nulled by external methods
            Layout layout = mTextView.getLayout();
@@ -4194,7 +4210,7 @@ public class Editor {
         * @return The clamped horizontal position for the cursor.
         */
        int getCursorHorizontalPosition(Layout layout, int offset) {
            return (int) (layout.getPrimaryHorizontal(offset) - 0.5f);
            return (int) (getHorizontal(layout, offset) - 0.5f);
        }

        public void updatePosition(int parentPositionX, int parentPositionY,
@@ -4427,7 +4443,7 @@ public class Editor {
        int getCursorHorizontalPosition(Layout layout, int offset) {
            final Drawable drawable = mCursorCount > 0 ? mCursorDrawable[0] : null;
            if (drawable != null) {
                final float horizontal = layout.getPrimaryHorizontal(offset);
                final float horizontal = getHorizontal(layout, offset);
                return clampHorizontalPosition(drawable, horizontal) + mTempRect.left;
            }
            return super.getCursorHorizontalPosition(layout, offset);
@@ -4499,10 +4515,10 @@ public class Editor {
                    mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
                }
                int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
                offset = mTextView.getOffsetAtCoordinate(currLine, x);
                offset = getOffsetAtCoordinate(layout, currLine, x);
                mPreviousLineTouched = currLine;
            } else {
                offset = mTextView.getOffsetForPosition(x, y);
                offset = -1;
            }
            positionAtCursorOffset(offset, false);
            if (mTextActionMode != null) {
@@ -4612,14 +4628,14 @@ public class Editor {
            final int anotherHandleOffset =
                    isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
            int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
            int initialOffset = mTextView.getOffsetAtCoordinate(currLine, x);
            int initialOffset = getOffsetAtCoordinate(layout, currLine, x);

            if (isStartHandle() && initialOffset >= anotherHandleOffset
                    || !isStartHandle() && initialOffset <= anotherHandleOffset) {
                // Handles have crossed, bound it to the first selected line and
                // adjust by word / char as normal.
                currLine = layout.getLineForOffset(anotherHandleOffset);
                initialOffset = mTextView.getOffsetAtCoordinate(currLine, x);
                initialOffset = getOffsetAtCoordinate(layout, currLine, x);
            }

            int offset = initialOffset;
@@ -4631,8 +4647,8 @@ public class Editor {
            }

            final int currentOffset = getCurrentCursorOffset();
            final boolean rtlAtCurrentOffset = layout.isRtlCharAt(currentOffset);
            final boolean atRtl = layout.isRtlCharAt(offset);
            final boolean rtlAtCurrentOffset = isAtRtlRun(layout, currentOffset);
            final boolean atRtl = isAtRtlRun(layout, offset);
            final boolean isLvlBoundary = layout.isLevelBoundary(offset);

            // We can't determine if the user is expanding or shrinking the selection if they're
@@ -4689,14 +4705,15 @@ public class Editor {

            if (isExpanding) {
                // User is increasing the selection.
                final boolean snapToWord = !mInWord
                        || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine);
                int wordBoundary = isStartHandle() ? wordStart : wordEnd;
                final boolean snapToWord = (!mInWord
                        || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
                                && atRtl == isAtRtlRun(layout, wordBoundary);
                if (snapToWord) {
                    // Sometimes words can be broken across lines (Chinese, hyphenation).
                    // We still snap to the word boundary but we only use the letters on the
                    // current line to determine if the user is far enough into the word to snap.
                    int wordBoundary = isStartHandle() ? wordStart : wordEnd;
                    if (layout != null && layout.getLineForOffset(wordBoundary) != currLine) {
                    if (layout.getLineForOffset(wordBoundary) != currLine) {
                        wordBoundary = isStartHandle() ?
                                layout.getLineStart(currLine) : layout.getLineEnd(currLine);
                    }
@@ -4717,9 +4734,9 @@ public class Editor {
                        offset = mPreviousOffset;
                    }
                }
                if (layout != null && (isStartHandle() && offset < initialOffset)
                if ((isStartHandle() && offset < initialOffset)
                        || (!isStartHandle() && offset > initialOffset)) {
                    final float adjustedX = layout.getPrimaryHorizontal(offset);
                    final float adjustedX = getHorizontal(layout, offset);
                    mTouchWordDelta =
                            mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
                } else {
@@ -4728,7 +4745,7 @@ public class Editor {
                positionCursor = true;
            } else {
                final int adjustedOffset =
                        mTextView.getOffsetAtCoordinate(currLine, x - mTouchWordDelta);
                        getOffsetAtCoordinate(layout, currLine, x - mTouchWordDelta);
                final boolean shrinking = isStartHandle()
                        ? adjustedOffset > mPreviousOffset || currLine > mPrevLine
                        : adjustedOffset < mPreviousOffset || currLine < mPrevLine;
@@ -4737,9 +4754,9 @@ public class Editor {
                    if (currLine != mPrevLine) {
                        // We're on a different line, so we'll snap to word boundaries.
                        offset = isStartHandle() ? wordStart : wordEnd;
                        if (layout != null && (isStartHandle() && offset < initialOffset)
                        if ((isStartHandle() && offset < initialOffset)
                                || (!isStartHandle() && offset > initialOffset)) {
                            final float adjustedX = layout.getPrimaryHorizontal(offset);
                            final float adjustedX = getHorizontal(layout, offset);
                            mTouchWordDelta =
                                    mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
                        } else {
@@ -4754,7 +4771,7 @@ public class Editor {
                    // Handle has jumped to the word boundary, and the user is moving
                    // their finger towards the handle, the delta should be updated.
                    mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x) -
                            layout.getPrimaryHorizontal(mPreviousOffset);
                            getHorizontal(layout, mPreviousOffset);
                }
            }

@@ -4792,9 +4809,32 @@ public class Editor {
                    isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
            if ((isStartHandle() && offset >= anotherHandleOffset)
                    || (!isStartHandle() && offset <= anotherHandleOffset)) {
                mTouchWordDelta = 0.0f;
                final Layout layout = mTextView.getLayout();
                if (layout != null && offset != anotherHandleOffset) {
                    final float horiz = getHorizontal(layout, offset);
                    final float anotherHandleHoriz = getHorizontal(layout, anotherHandleOffset,
                            !isStartHandle());
                    final float currentHoriz = getHorizontal(layout, mPreviousOffset);
                    if (currentHoriz < anotherHandleHoriz && horiz < anotherHandleHoriz
                            || currentHoriz > anotherHandleHoriz && horiz > anotherHandleHoriz) {
                        // This handle passes another one as it crossed a direction boundary.
                        // Don't minimize the selection, but keep the handle at the run boundary.
                        final int currentOffset = getCurrentCursorOffset();
                        final int offsetToGetRunRange = isStartHandle() ?
                                currentOffset : Math.max(currentOffset - 1, 0);
                        final long range = layout.getRunRange(offsetToGetRunRange);
                        if (isStartHandle()) {
                            offset = TextUtils.unpackRangeStartFromLong(range);
                        } else {
                            offset = TextUtils.unpackRangeEndFromLong(range);
                        }
                        positionAtCursorOffset(offset, false);
                        return;
                    }
                }
                // Handles can not cross and selection is at least one character.
                offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
                mTouchWordDelta = 0.0f;
            }
            positionAtCursorOffset(offset, false);
        }
@@ -4812,6 +4852,49 @@ public class Editor {
            }
            return nearEdge;
        }

        @Override
        protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
            final int offsetToCheck = isStartHandle() ? offset : Math.max(offset - 1, 0);
            return layout.isRtlCharAt(offsetToCheck);
        }

        @Override
        public float getHorizontal(@NonNull Layout layout, int offset) {
            return getHorizontal(layout, offset, isStartHandle());
        }

        private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) {
            final int line = layout.getLineForOffset(offset);
            final int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0);
            final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
            final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
            return (isRtlChar == isRtlParagraph) ?
                    layout.getPrimaryHorizontal(offset) : layout.getSecondaryHorizontal(offset);
        }

        @Override
        protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
            final int primaryOffset = layout.getOffsetForHorizontal(line, x, true);
            if (!layout.isLevelBoundary(primaryOffset)) {
                return primaryOffset;
            }
            final int secondaryOffset = layout.getOffsetForHorizontal(line, x, false);
            final int currentOffset = getCurrentCursorOffset();
            final int primaryDiff = Math.abs(primaryOffset - currentOffset);
            final int secondaryDiff = Math.abs(secondaryOffset - currentOffset);
            if (primaryDiff < secondaryDiff) {
                return primaryOffset;
            } else if (primaryDiff > secondaryDiff) {
                return secondaryOffset;
            } else {
                final int offsetToCheck = isStartHandle() ?
                        currentOffset : Math.max(currentOffset - 1, 0);
                final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
                final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
                return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset;
            }
        }
    }

    private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) {
+45 −0
Original line number Diff line number Diff line
@@ -384,6 +384,51 @@ public class TextViewActivityTest extends ActivityInstrumentationTestCase2<TextV
        onView(withId(R.id.textview)).check(hasSelection("abcd efg hijk"));
    }

    @SmallTest
    public void testSelectionHandles_bidi() throws Exception {
        final String text = "abc \u0621\u0622\u0623 def";
        onView(withId(R.id.textview)).perform(click());
        onView(withId(R.id.textview)).perform(replaceText(text));

        assertNoSelectionHandles();

        onView(withId(R.id.textview)).perform(doubleClickOnTextAtIndex(text.indexOf('\u0622')));

        onHandleView(com.android.internal.R.id.selection_start_handle)
                .check(matches(isDisplayed()));
        onHandleView(com.android.internal.R.id.selection_end_handle)
                .check(matches(isDisplayed()));

        onView(withId(R.id.textview)).check(hasSelection("\u0621\u0622\u0623"));

        final TextView textView = (TextView) getActivity().findViewById(R.id.textview);
        onHandleView(com.android.internal.R.id.selection_start_handle)
                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('f')));
        onView(withId(R.id.textview)).check(hasSelection("\u0621\u0622\u0623"));

        onHandleView(com.android.internal.R.id.selection_end_handle)
                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('a')));
        onView(withId(R.id.textview)).check(hasSelection("\u0621\u0622\u0623"));

        onHandleView(com.android.internal.R.id.selection_start_handle)
                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('\u0623') + 1,
                        false));
        onView(withId(R.id.textview)).check(hasSelection("\u0623"));

        onHandleView(com.android.internal.R.id.selection_start_handle)
                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('\u0621'),
                        false));
        onView(withId(R.id.textview)).check(hasSelection("\u0621\u0622\u0623"));

        onHandleView(com.android.internal.R.id.selection_start_handle)
                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('a')));
        onView(withId(R.id.textview)).check(hasSelection("abc \u0621\u0622\u0623"));

        onHandleView(com.android.internal.R.id.selection_end_handle)
                .perform(dragHandle(textView, Handle.SELECTION_END, text.length()));
        onView(withId(R.id.textview)).check(hasSelection("abc \u0621\u0622\u0623 def"));
    }

    @SmallTest
    public void testSelectionHandles_multiLine() throws Exception {
        final String text = "abcd\n" + "efg\n" + "hijk\n" + "lmn\n" + "opqr";
+123 −19

File changed.

Preview size limit exceeded, changes collapsed.