diff options
author | Yohei Yukawa <yukawa@google.com> | 2014-09-02 14:18:40 -0700 |
---|---|---|
committer | Yohei Yukawa <yukawa@google.com> | 2014-09-08 02:17:54 +0000 |
commit | 5f183f0671dfa1d87ca6d741deb457170c432493 (patch) | |
tree | bbe26c8efa6ac5b36856227c2f58b437321aee9f | |
parent | a3ca5a31a5db14e7c4597de074f01c1af6161872 (diff) | |
download | frameworks_base-5f183f0671dfa1d87ca6d741deb457170c432493.zip frameworks_base-5f183f0671dfa1d87ca6d741deb457170c432493.tar.gz frameworks_base-5f183f0671dfa1d87ca6d741deb457170c432493.tar.bz2 |
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
-rw-r--r-- | api/current.txt | 7 | ||||
-rw-r--r-- | api/removed.txt | 3 | ||||
-rw-r--r-- | core/java/android/view/inputmethod/CursorAnchorInfo.java | 171 | ||||
-rw-r--r-- | core/java/android/widget/Editor.java | 105 |
4 files changed, 192 insertions, 94 deletions
diff --git a/api/current.txt b/api/current.txt index f59fd4f..9d89bdf 100644 --- a/api/current.txt +++ b/api/current.txt @@ -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); diff --git a/api/removed.txt b/api/removed.txt index 7f5f46c..3c16276 100644 --- a/api/removed.txt +++ b/api/removed.txt @@ -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); } diff --git a/core/java/android/view/inputmethod/CursorAnchorInfo.java b/core/java/android/view/inputmethod/CursorAnchorInfo.java index fe0f5b9..600fffe 100644 --- a/core/java/android/view/inputmethod/CursorAnchorInfo.java +++ b/core/java/android/view/inputmethod/CursorAnchorInfo.java @@ -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,18 +103,24 @@ 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 */ public static final int CHARACTER_RECT_TYPE_MASK = 0x0f; @@ -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."); - } - if (mCharacterRectBuilder == null) { - mCharacterRectBuilder = new SparseRectFArrayBuilder(); + 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; } - mCharacterRectBuilder.append(index, leadingEdgeX, leadingEdgeY, trailingEdgeX, - trailingEdgeY, flags); - return this; + return addCharacterBounds(index, left, leadingEdgeY, right, trailingEdgeY, newFlags); } - 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); } @@ -539,6 +591,19 @@ public final class CursorAnchorInfo implements Parcelable { /** * 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. * <p> * Note that coordinates are not necessarily contiguous or even monotonous, especially when * RTL text and LTR text are mixed. @@ -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); } /** diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 22138d0..3f168e8 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -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 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); - } else { - localTrailingEdgeX = layout.getLineRight(line); - } - 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; - } - if (!isLeadingEdgeTopVisible || !isTrailingEdgeBottomVisible) { - characterRectFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION; + + 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 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 { + left = primary; + right = primary + charWidth; + } + } 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 (isRtl) { + characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL; + } + // Here offset is the index in Java chars. + builder.addCharacterBounds(offset, localLeft, localTop, localRight, + localBottom, characterBoundsFlags); + } } - // 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); } } @@ -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); } |