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

Commit 5f183f06 authored by Yohei Yukawa's avatar Yohei Yukawa
Browse files

L API proposal: Introduce IS_RTL flag

This CL introduces CursorAnchorInfo.FLAG_IS_RTL for better
RTL support. This CL also renames *CharacterRect() with
*CharacterBounds() so that they can look more consistent
with other existing APIs.

Rationale:

CursorAnchorInfo.FLAG_IS_RTL addresses following issues.
1. There is no way to associate the RTL information with
   the insertion marker.
2. Returning mirrored (right < left) RectF for RTL in
   CursorAnchorInfo#getCharacterRect() is turned out
   to be bug-prone. Such usage of RectF is not fully
   supported. For example, RectF#isEmpty() always returns
   false when right < left.
3. There is no reliable to provide the RTL information
   when CursorAnchorInfo#getCharacterRect() returns an
   empty (right == left) RectF. Perhaps we could use +0.0
   and -0.0, but I'm afraid that it is also bug-prone.

BUG: 17365414
BUG: 17335734
Change-Id: Ic8c6fab58c01206872a34e7ee604cdda1581364d
parent a3ca5a31
Loading
Loading
Loading
Loading
+4 −3
Original line number Diff line number Diff line
@@ -35899,8 +35899,8 @@ package android.view.inputmethod {
  public final class CursorAnchorInfo implements android.os.Parcelable {
    ctor public CursorAnchorInfo(android.os.Parcel);
    method public int describeContents();
    method public android.graphics.RectF getCharacterRect(int);
    method public int getCharacterRectFlags(int);
    method public android.graphics.RectF getCharacterBounds(int);
    method public int getCharacterBoundsFlags(int);
    method public java.lang.CharSequence getComposingText();
    method public int getComposingTextStart();
    method public float getInsertionMarkerBaseline();
@@ -35915,11 +35915,12 @@ package android.view.inputmethod {
    field public static final android.os.Parcelable.Creator CREATOR;
    field public static final int FLAG_HAS_INVISIBLE_REGION = 2; // 0x2
    field public static final int FLAG_HAS_VISIBLE_REGION = 1; // 0x1
    field public static final int FLAG_IS_RTL = 4; // 0x4
  }
  public static final class CursorAnchorInfo.Builder {
    ctor public CursorAnchorInfo.Builder();
    method public android.view.inputmethod.CursorAnchorInfo.Builder addCharacterRect(int, float, float, float, float, int);
    method public android.view.inputmethod.CursorAnchorInfo.Builder addCharacterBounds(int, float, float, float, float, int);
    method public android.view.inputmethod.CursorAnchorInfo build();
    method public void reset();
    method public android.view.inputmethod.CursorAnchorInfo.Builder setComposingText(int, java.lang.CharSequence);
+3 −0
Original line number Diff line number Diff line
@@ -45,6 +45,8 @@ package android.view.inputmethod {
  }

  public final class CursorAnchorInfo implements android.os.Parcelable {
    method public android.graphics.RectF getCharacterRect(int);
    method public int getCharacterRectFlags(int);
    method public boolean isInsertionMarkerClipped();
    field public static final int CHARACTER_RECT_TYPE_FULLY_VISIBLE = 1; // 0x1
    field public static final int CHARACTER_RECT_TYPE_INVISIBLE = 3; // 0x3
@@ -55,6 +57,7 @@ package android.view.inputmethod {
  }

  public static final class CursorAnchorInfo.Builder {
    method public android.view.inputmethod.CursorAnchorInfo.Builder addCharacterRect(int, float, float, float, float, int);
    method public android.view.inputmethod.CursorAnchorInfo.Builder setInsertionMarkerLocation(float, float, float, float, boolean);
  }

+120 −51
Original line number Diff line number Diff line
@@ -35,9 +35,21 @@ import java.util.Objects;
 * actually inserted.</p>
 */
public final class CursorAnchorInfo implements Parcelable {
    /**
     * The index of the first character of the selected text (inclusive). {@code -1} when there is
     * no text selection.
     */
    private final int mSelectionStart;
    /**
     * The index of the first character of the selected text (exclusive). {@code -1} when there is
     * no text selection.
     */
    private final int mSelectionEnd;

    /**
     * The index of the first character of the composing text (inclusive). {@code -1} when there is
     * no composing text.
     */
    private final int mComposingTextStart;
    /**
     * The text, tracked as a composing region.
@@ -82,7 +94,7 @@ public final class CursorAnchorInfo implements Parcelable {
     * Java chars, in the local coordinates that will be transformed with the transformation matrix
     * when rendered on the screen.
     */
    private final SparseRectFArray mCharacterRects;
    private final SparseRectFArray mCharacterBoundsArray;

    /**
     * Transformation matrix that is applied to any positional information of this class to
@@ -91,17 +103,23 @@ public final class CursorAnchorInfo implements Parcelable {
    private final Matrix mMatrix;

    /**
     * Flag for {@link #getInsertionMarkerFlags()} and {@link #getCharacterRectFlags(int)}: the
     * Flag for {@link #getInsertionMarkerFlags()} and {@link #getCharacterBoundsFlags(int)}: the
     * insertion marker or character bounds have at least one visible region.
     */
    public static final int FLAG_HAS_VISIBLE_REGION = 0x01;

    /**
     * Flag for {@link #getInsertionMarkerFlags()} and {@link #getCharacterRectFlags(int)}: the
     * Flag for {@link #getInsertionMarkerFlags()} and {@link #getCharacterBoundsFlags(int)}: the
     * insertion marker or character bounds have at least one invisible (clipped) region.
     */
    public static final int FLAG_HAS_INVISIBLE_REGION = 0x02;

    /**
     * Flag for {@link #getInsertionMarkerFlags()} and {@link #getCharacterBoundsFlags(int)}: the
     * insertion marker or character bounds is placed at right-to-left (RTL) character.
     */
    public static final int FLAG_IS_RTL = 0x04;

    /**
     * @removed
     */
@@ -144,7 +162,7 @@ public final class CursorAnchorInfo implements Parcelable {
        mInsertionMarkerTop = source.readFloat();
        mInsertionMarkerBaseline = source.readFloat();
        mInsertionMarkerBottom = source.readFloat();
        mCharacterRects = source.readParcelable(SparseRectFArray.class.getClassLoader());
        mCharacterBoundsArray = source.readParcelable(SparseRectFArray.class.getClassLoader());
        mMatrix = new Matrix();
        mMatrix.setValues(source.createFloatArray());
    }
@@ -166,7 +184,7 @@ public final class CursorAnchorInfo implements Parcelable {
        dest.writeFloat(mInsertionMarkerTop);
        dest.writeFloat(mInsertionMarkerBaseline);
        dest.writeFloat(mInsertionMarkerBottom);
        dest.writeParcelable(mCharacterRects, flags);
        dest.writeParcelable(mCharacterBoundsArray, flags);
        final float[] matrixArray = new float[9];
        mMatrix.getValues(matrixArray);
        dest.writeFloatArray(matrixArray);
@@ -174,7 +192,6 @@ public final class CursorAnchorInfo implements Parcelable {

    @Override
    public int hashCode(){
        // TODO: Improve the hash function.
        final float floatHash = mInsertionMarkerHorizontal + mInsertionMarkerTop
                + mInsertionMarkerBaseline + mInsertionMarkerBottom;
        int hash = floatHash > 0 ? (int) floatHash : (int)(-floatHash);
@@ -185,7 +202,7 @@ public final class CursorAnchorInfo implements Parcelable {
        hash *= 31;
        hash += Objects.hashCode(mComposingText);
        hash *= 31;
        hash += Objects.hashCode(mCharacterRects);
        hash += Objects.hashCode(mCharacterBoundsArray);
        hash *= 31;
        hash += Objects.hashCode(mMatrix);
        return hash;
@@ -231,7 +248,7 @@ public final class CursorAnchorInfo implements Parcelable {
                || !areSameFloatImpl(mInsertionMarkerBottom, that.mInsertionMarkerBottom)) {
            return false;
        }
        if (!Objects.equals(mCharacterRects, that.mCharacterRects)) {
        if (!Objects.equals(mCharacterBoundsArray, that.mCharacterBoundsArray)) {
            return false;
        }
        if (!Objects.equals(mMatrix, that.mMatrix)) {
@@ -250,7 +267,7 @@ public final class CursorAnchorInfo implements Parcelable {
                + " mInsertionMarkerTop=" + mInsertionMarkerTop
                + " mInsertionMarkerBaseline=" + mInsertionMarkerBaseline
                + " mInsertionMarkerBottom=" + mInsertionMarkerBottom
                + " mCharacterRects=" + Objects.toString(mCharacterRects)
                + " mCharacterBoundsArray=" + Objects.toString(mCharacterBoundsArray)
                + " mMatrix=" + Objects.toString(mMatrix)
                + "}";
    }
@@ -259,6 +276,19 @@ public final class CursorAnchorInfo implements Parcelable {
     * Builder for {@link CursorAnchorInfo}. This class is not designed to be thread-safe.
     */
    public static final class Builder {
        private int mSelectionStart = -1;
        private int mSelectionEnd = -1;
        private int mComposingTextStart = -1;
        private CharSequence mComposingText = null;
        private float mInsertionMarkerHorizontal = Float.NaN;
        private float mInsertionMarkerTop = Float.NaN;
        private float mInsertionMarkerBaseline = Float.NaN;
        private float mInsertionMarkerBottom = Float.NaN;
        private int mInsertionMarkerFlags = 0;
        private SparseRectFArrayBuilder mCharacterBoundsArrayBuilder = null;
        private final Matrix mMatrix = new Matrix(Matrix.IDENTITY_MATRIX);
        private boolean mMatrixInitialized = false;

        /**
         * Sets the text range of the selection. Calling this can be skipped if there is no
         * selection.
@@ -268,8 +298,6 @@ public final class CursorAnchorInfo implements Parcelable {
            mSelectionEnd = newEnd;
            return this;
        }
        private int mSelectionStart = -1;
        private int mSelectionEnd = -1;

        /**
         * Sets the text range of the composing text. Calling this can be skipped if there is
@@ -288,8 +316,6 @@ public final class CursorAnchorInfo implements Parcelable {
            }
            return this;
        }
        private int mComposingTextStart = -1;
        private CharSequence mComposingText = null;

        /**
         * @removed
@@ -335,11 +361,33 @@ public final class CursorAnchorInfo implements Parcelable {
            mInsertionMarkerFlags = flags;
            return this;
        }
        private float mInsertionMarkerHorizontal = Float.NaN;
        private float mInsertionMarkerTop = Float.NaN;
        private float mInsertionMarkerBaseline = Float.NaN;
        private float mInsertionMarkerBottom = Float.NaN;
        private int mInsertionMarkerFlags = 0;

        /**
         * Adds the bounding box of the character specified with the index.
         *
         * @param index index of the character in Java chars units. Must be specified in
         * ascending order across successive calls.
         * @param left x coordinate of the left edge of the character in local coordinates.
         * @param top y coordinate of the top edge of the character in local coordinates.
         * @param right x coordinate of the right edge of the character in local coordinates.
         * @param bottom y coordinate of the bottom edge of the character in local coordinates.
         * @param flags flags for this character bounds. See {@link #FLAG_HAS_VISIBLE_REGION},
         * {@link #FLAG_HAS_INVISIBLE_REGION} and {@link #FLAG_IS_RTL}. These flags must be
         * specified when necessary.
         * @throws IllegalArgumentException If the index is a negative value, or not greater than
         * all of the previously called indices.
         */
        public Builder addCharacterBounds(final int index, final float left, final float top,
                final float right, final float bottom, final int flags) {
            if (index < 0) {
                throw new IllegalArgumentException("index must not be a negative integer.");
            }
            if (mCharacterBoundsArrayBuilder == null) {
                mCharacterBoundsArrayBuilder = new SparseRectFArrayBuilder();
            }
            mCharacterBoundsArrayBuilder.append(index, left, top, right, bottom, flags);
            return this;
        }

        /**
         * Adds the bounding box of the character specified with the index.
@@ -358,21 +406,25 @@ public final class CursorAnchorInfo implements Parcelable {
         * example.
         * @throws IllegalArgumentException If the index is a negative value, or not greater than
         * all of the previously called indices.
         * @removed
         */
        public Builder addCharacterRect(final int index, final float leadingEdgeX,
                final float leadingEdgeY, final float trailingEdgeX, final float trailingEdgeY,
                final int flags) {
            if (index < 0) {
                throw new IllegalArgumentException("index must not be a negative integer.");
            final int newFlags;
            final float left;
            final float right;
            if (leadingEdgeX <= trailingEdgeX) {
                newFlags = flags;
                left = leadingEdgeX;
                right = trailingEdgeX;
            } else {
                newFlags = flags | FLAG_IS_RTL;
                left = trailingEdgeX;
                right = leadingEdgeX;
            }
            if (mCharacterRectBuilder == null) {
                mCharacterRectBuilder = new SparseRectFArrayBuilder();
            return addCharacterBounds(index, left, leadingEdgeY, right, trailingEdgeY, newFlags);
        }
            mCharacterRectBuilder.append(index, leadingEdgeX, leadingEdgeY, trailingEdgeX,
                    trailingEdgeY, flags);
            return this;
        }
        private SparseRectFArrayBuilder mCharacterRectBuilder = null;

        /**
         * Sets the matrix that transforms local coordinates into screen coordinates.
@@ -384,8 +436,6 @@ public final class CursorAnchorInfo implements Parcelable {
            mMatrixInitialized = true;
            return this;
        }
        private final Matrix mMatrix = new Matrix(Matrix.IDENTITY_MATRIX);
        private boolean mMatrixInitialized = false;

        /**
         * @return {@link CursorAnchorInfo} using parameters in this {@link Builder}.
@@ -394,13 +444,15 @@ public final class CursorAnchorInfo implements Parcelable {
         */
        public CursorAnchorInfo build() {
            if (!mMatrixInitialized) {
                // Coordinate transformation matrix is mandatory when positional parameters are
                // specified.
                if ((mCharacterRectBuilder != null && !mCharacterRectBuilder.isEmpty()) ||
                        !Float.isNaN(mInsertionMarkerHorizontal) ||
                        !Float.isNaN(mInsertionMarkerTop) ||
                        !Float.isNaN(mInsertionMarkerBaseline) ||
                        !Float.isNaN(mInsertionMarkerBottom)) {
                // Coordinate transformation matrix is mandatory when at least one positional
                // parameter is specified.
                final boolean hasCharacterBounds = (mCharacterBoundsArrayBuilder != null
                        && !mCharacterBoundsArrayBuilder.isEmpty());
                if (hasCharacterBounds
                        || !Float.isNaN(mInsertionMarkerHorizontal)
                        || !Float.isNaN(mInsertionMarkerTop)
                        || !Float.isNaN(mInsertionMarkerBaseline)
                        || !Float.isNaN(mInsertionMarkerBottom)) {
                    throw new IllegalArgumentException("Coordinate transformation matrix is " +
                            "required when positional parameters are specified.");
                }
@@ -424,8 +476,8 @@ public final class CursorAnchorInfo implements Parcelable {
            mInsertionMarkerBottom = Float.NaN;
            mMatrix.set(Matrix.IDENTITY_MATRIX);
            mMatrixInitialized = false;
            if (mCharacterRectBuilder != null) {
                mCharacterRectBuilder.reset();
            if (mCharacterBoundsArrayBuilder != null) {
                mCharacterBoundsArrayBuilder.reset();
            }
        }
    }
@@ -440,8 +492,8 @@ public final class CursorAnchorInfo implements Parcelable {
        mInsertionMarkerTop = builder.mInsertionMarkerTop;
        mInsertionMarkerBaseline = builder.mInsertionMarkerBaseline;
        mInsertionMarkerBottom = builder.mInsertionMarkerBottom;
        mCharacterRects = builder.mCharacterRectBuilder != null ?
                builder.mCharacterRectBuilder.build() : null;
        mCharacterBoundsArray = builder.mCharacterBoundsArrayBuilder != null ?
                builder.mCharacterBoundsArrayBuilder.build() : null;
        mMatrix = new Matrix(builder.mMatrix);
    }

@@ -536,6 +588,19 @@ public final class CursorAnchorInfo implements Parcelable {
        return mInsertionMarkerBottom;
    }

    /**
     * Returns a new instance of {@link RectF} that indicates the location of the character
     * specified with the index.
     * @param index index of the character in a Java chars.
     * @return the character bounds in local coordinates as a new instance of {@link RectF}.
     */
    public RectF getCharacterBounds(final int index) {
        if (mCharacterBoundsArray == null) {
            return null;
        }
        return mCharacterBoundsArray.get(index);
    }

    /**
     * Returns a new instance of {@link RectF} that indicates the location of the character
     * specified with the index.
@@ -549,28 +614,32 @@ public final class CursorAnchorInfo implements Parcelable {
     * the location. Note that the {@code left} field can be greater than the {@code right} field
     * if the character is in RTL text. Returns {@code null} if no location information is
     * available.
     * @removed
     */
    // TODO: Prepare a document about the expected behavior for surrogate pairs, combining
    // characters, and non-graphical chars.
    public RectF getCharacterRect(final int index) {
        if (mCharacterRects == null) {
            return null;
        return getCharacterBounds(index);
    }

    /**
     * Returns the flags associated with the character bounds specified with the index.
     * @param index index of the character in a Java chars.
     * @return {@code 0} if no flag is specified.
     */
    public int getCharacterBoundsFlags(final int index) {
        if (mCharacterBoundsArray == null) {
            return 0;
        }
        return mCharacterRects.get(index);
        return mCharacterBoundsArray.getFlags(index, 0);
    }

    /**
     * Returns the flags associated with the character rect specified with the index.
     * @param index index of the character in a Java chars.
     * @return {@code 0} if no flag is specified.
     * @removed
     */
    // TODO: Prepare a document about the expected behavior for surrogate pairs, combining
    // characters, and non-graphical chars.
    public int getCharacterRectFlags(final int index) {
        if (mCharacterRects == null) {
            return 0;
        }
        return mCharacterRects.getFlags(index, 0);
        return getCharacterBoundsFlags(index);
    }

    /**
+65 −40
Original line number Diff line number Diff line
@@ -3060,47 +3060,69 @@ public class Editor {
                    final CharSequence composingText = text.subSequence(composingTextStart,
                            composingTextEnd);
                    builder.setComposingText(composingTextStart, composingText);
                }
                // TODO: Optimize this loop by caching the result.
                for (int offset = composingTextStart; offset < composingTextEnd; offset++) {
                    if (offset < 0) {
                        continue;
                    }

                    final int minLine = layout.getLineForOffset(composingTextStart);
                    final int maxLine = layout.getLineForOffset(composingTextEnd - 1);
                    for (int line = minLine; line <= maxLine; ++line) {
                        final int lineStart = layout.getLineStart(line);
                        final int lineEnd = layout.getLineEnd(line);
                        final int offsetStart = Math.max(lineStart, composingTextStart);
                        final int offsetEnd = Math.min(lineEnd, composingTextEnd);
                        final boolean ltrLine =
                                layout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT;
                        final float[] widths = new float[offsetEnd - offsetStart];
                        layout.getPaint().getTextWidths(text, offsetStart, offsetEnd, widths);
                        final float top = layout.getLineTop(line);
                        final float bottom = layout.getLineBottom(line);
                        for (int offset = offsetStart; offset < offsetEnd; ++offset) {
                            final float charWidth = widths[offset - offsetStart];
                            final boolean isRtl = layout.isRtlCharAt(offset);
                    final int line = layout.getLineForOffset(offset);
                    final int nextCharIndex = offset + 1;
                    final float localLeadingEdgeX = layout.getPrimaryHorizontal(offset);
                    final float localTrailingEdgeX;
                    if (nextCharIndex != layout.getLineEnd(line)) {
                        localTrailingEdgeX = layout.getPrimaryHorizontal(nextCharIndex);
                    } else if (isRtl) {
                        localTrailingEdgeX = layout.getLineLeft(line);
                            final float primary = layout.getPrimaryHorizontal(offset);
                            final float secondary = layout.getSecondaryHorizontal(offset);
                            // TODO: This doesn't work perfectly for text with custom styles and
                            // TAB chars.
                            final float left;
                            final float right;
                            if (ltrLine) {
                                if (isRtl) {
                                    left = secondary - charWidth;
                                    right = secondary;
                                } else {
                        localTrailingEdgeX = layout.getLineRight(line);
                                    left = primary;
                                    right = primary + charWidth;
                                }
                    final float leadingEdgeX = localLeadingEdgeX
                            + viewportToContentHorizontalOffset;
                    final float trailingEdgeX = localTrailingEdgeX
                            + viewportToContentHorizontalOffset;
                    final float top = layout.getLineTop(line) + viewportToContentVerticalOffset;
                    final float bottom = layout.getLineBottom(line)
                            + viewportToContentVerticalOffset;
                    // TODO: Check right-top and left-bottom as well.
                    final boolean isLeadingEdgeTopVisible = isPositionVisible(leadingEdgeX, top);
                    final boolean isTrailingEdgeBottomVisible =
                            isPositionVisible(trailingEdgeX, bottom);
                    int characterRectFlags = 0;
                    if (isLeadingEdgeTopVisible || isTrailingEdgeBottomVisible) {
                        characterRectFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
                            } else {
                                if (!isRtl) {
                                    left = secondary;
                                    right = secondary + charWidth;
                                } else {
                                    left = primary - charWidth;
                                    right = primary;
                                }
                            }
                            // TODO: Check top-right and bottom-left as well.
                            final float localLeft = left + viewportToContentHorizontalOffset;
                            final float localRight = right + viewportToContentHorizontalOffset;
                            final float localTop = top + viewportToContentVerticalOffset;
                            final float localBottom = bottom + viewportToContentVerticalOffset;
                            final boolean isTopLeftVisible = isPositionVisible(localLeft, localTop);
                            final boolean isBottomRightVisible =
                                    isPositionVisible(localRight, localBottom);
                            int characterBoundsFlags = 0;
                            if (isTopLeftVisible || isBottomRightVisible) {
                                characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
                            }
                            if (!isTopLeftVisible || !isTopLeftVisible) {
                                characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
                            }
                    if (!isLeadingEdgeTopVisible || !isTrailingEdgeBottomVisible) {
                        characterRectFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
                            if (isRtl) {
                                characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL;
                            }
                            // Here offset is the index in Java chars.
                    // TODO: We must have a well-defined specification. For example, how
                    // surrogate pairs and composition letters are handled must be documented.
                    builder.addCharacterRect(offset, leadingEdgeX, top, trailingEdgeX, bottom,
                            characterRectFlags);
                            builder.addCharacterBounds(offset, localLeft, localTop, localRight,
                                    localBottom, characterBoundsFlags);
                        }
                    }
                }
            }

@@ -3127,6 +3149,9 @@ public class Editor {
                if (!isTopVisible || !isBottomVisible) {
                    insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
                }
                if (layout.isRtlCharAt(offset)) {
                    insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
                }
                builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
                        insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
            }