/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package android.view.inputmethod; import android.graphics.Matrix; import android.graphics.RectF; import android.os.Parcel; import android.os.Parcelable; import android.text.Layout; import android.view.inputmethod.SparseRectFArray.SparseRectFArrayBuilder; import java.util.Objects; /** * Positional information about the text insertion point and characters in the composition string. * *

This class encapsulates locations of the text insertion point and the composition string in * the screen coordinates so that IMEs can render their UI components near where the text is * actually inserted.

*/ public final class CursorAnchorInfo implements Parcelable { private final int mSelectionStart; private final int mSelectionEnd; private final int mComposingTextStart; /** * The text, tracked as a composing region. */ private final String mComposingText; /** * Horizontal position of the insertion marker, in the local coordinates that will be * transformed with the transformation matrix when rendered on the screen. This should be * calculated or compatible with {@link Layout#getPrimaryHorizontal(int)}. This can be * {@code java.lang.Float.NaN} when no value is specified. */ private final float mInsertionMarkerHorizontal; /** * Vertical position of the insertion marker, in the local coordinates that will be * transformed with the transformation matrix when rendered on the screen. This should be * calculated or compatible with {@link Layout#getLineTop(int)}. This can be * {@code java.lang.Float.NaN} when no value is specified. */ private final float mInsertionMarkerTop; /** * Vertical position of the insertion marker, in the local coordinates that will be * transformed with the transformation matrix when rendered on the screen. This should be * calculated or compatible with {@link Layout#getLineBaseline(int)}. This can be * {@code java.lang.Float.NaN} when no value is specified. */ private final float mInsertionMarkerBaseline; /** * Vertical position of the insertion marker, in the local coordinates that will be * transformed with the transformation matrix when rendered on the screen. This should be * calculated or compatible with {@link Layout#getLineBottom(int)}. This can be * {@code java.lang.Float.NaN} when no value is specified. */ private final float mInsertionMarkerBottom; /** * Container of rectangular position of characters, keyed with character index in a unit of * Java chars, in the local coordinates that will be transformed with the transformation matrix * when rendered on the screen. */ private final SparseRectFArray mCharacterRects; /** * Transformation matrix that is applied to any positional information of this class to * transform local coordinates into screen coordinates. */ private final Matrix mMatrix; public CursorAnchorInfo(final Parcel source) { mSelectionStart = source.readInt(); mSelectionEnd = source.readInt(); mComposingTextStart = source.readInt(); mComposingText = source.readString(); mInsertionMarkerHorizontal = source.readFloat(); mInsertionMarkerTop = source.readFloat(); mInsertionMarkerBaseline = source.readFloat(); mInsertionMarkerBottom = source.readFloat(); mCharacterRects = source.readParcelable(SparseRectFArray.class.getClassLoader()); mMatrix = new Matrix(); mMatrix.setValues(source.createFloatArray()); } /** * Used to package this object into a {@link Parcel}. * * @param dest The {@link Parcel} to be written. * @param flags The flags used for parceling. */ @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mSelectionStart); dest.writeInt(mSelectionEnd); dest.writeInt(mComposingTextStart); dest.writeString(mComposingText); dest.writeFloat(mInsertionMarkerHorizontal); dest.writeFloat(mInsertionMarkerTop); dest.writeFloat(mInsertionMarkerBaseline); dest.writeFloat(mInsertionMarkerBottom); dest.writeParcelable(mCharacterRects, flags); final float[] matrixArray = new float[9]; mMatrix.getValues(matrixArray); dest.writeFloatArray(matrixArray); } @Override public int hashCode(){ // TODO: Improve the hash function. final float floatHash = mInsertionMarkerHorizontal + mInsertionMarkerTop + mInsertionMarkerBaseline + mInsertionMarkerBottom; int hash = floatHash > 0 ? (int) floatHash : (int)(-floatHash); hash *= 31; hash += mSelectionStart + mSelectionEnd + mComposingTextStart; hash *= 31; hash += Objects.hashCode(mComposingText); hash *= 31; hash += Objects.hashCode(mCharacterRects); hash *= 31; hash += Objects.hashCode(mMatrix); return hash; } @Override public boolean equals(Object obj){ if (obj == null) { return false; } if (this == obj) { return true; } if (!(obj instanceof CursorAnchorInfo)) { return false; } final CursorAnchorInfo that = (CursorAnchorInfo) obj; if (hashCode() != that.hashCode()) { return false; } if (mSelectionStart != that.mSelectionStart || mSelectionEnd != that.mSelectionEnd || mComposingTextStart != that.mComposingTextStart) { return false; } if (!Objects.equals(mComposingTextStart, that.mComposingTextStart)) { return false; } if (!Objects.equals(mCharacterRects, that.mCharacterRects)) { return false; } if (!Objects.equals(mMatrix, that.mMatrix)) { return false; } return true; } @Override public String toString() { return "SelectionInfo{mSelection=" + mSelectionStart + "," + mSelectionEnd + " mComposingTextStart=" + mComposingTextStart + " mComposingText=" + Objects.toString(mComposingText) + " mInsertionMarkerHorizontal=" + mInsertionMarkerHorizontal + " mInsertionMarkerTop=" + mInsertionMarkerTop + " mInsertionMarkerBaseline=" + mInsertionMarkerBaseline + " mInsertionMarkerBottom=" + mInsertionMarkerBottom + " mCharacterRects=" + Objects.toString(mCharacterRects) + " mMatrix=" + Objects.toString(mMatrix) + "}"; } /** * Builder for {@link CursorAnchorInfo}. This class is not designed to be thread-safe. */ public static final class CursorAnchorInfoBuilder { /** * Sets the text range of the selection. Calling this can be skipped if there is no * selection. */ public CursorAnchorInfoBuilder setSelectionRange(final int newStart, final int newEnd) { mSelectionStart = newStart; 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 * no composing text. * @param index index where the composing text starts. * @param composingText the entire composing text. */ public CursorAnchorInfoBuilder setComposingText(final int index, final CharSequence composingText) { mComposingTextStart = index; if (composingText == null) { mComposingText = null; } else { mComposingText = composingText.toString(); } return this; } private int mComposingTextStart = -1; private String mComposingText = null; /** * Sets the location of the text insertion point (zero width cursor) as a rectangle in * local coordinates. Calling this can be skipped when there is no text insertion point; * however if there is an insertion point, editors must call this method. * @param horizontalPosition horizontal position of the insertion marker, in the local * coordinates that will be transformed with the transformation matrix when rendered on the * screen. This should be calculated or compatible with * {@link Layout#getPrimaryHorizontal(int)}. * @param lineTop vertical position of the insertion marker, in the local coordinates that * will be transformed with the transformation matrix when rendered on the screen. This * should be calculated or compatible with {@link Layout#getLineTop(int)}. * @param lineBaseline vertical position of the insertion marker, in the local coordinates * that will be transformed with the transformation matrix when rendered on the screen. This * should be calculated or compatible with {@link Layout#getLineBaseline(int)}. * @param lineBottom vertical position of the insertion marker, in the local coordinates * that will be transformed with the transformation matrix when rendered on the screen. This * should be calculated or compatible with {@link Layout#getLineBottom(int)}. */ public CursorAnchorInfoBuilder setInsertionMarkerLocation( final float horizontalPosition, final float lineTop, final float lineBaseline, final float lineBottom){ mInsertionMarkerHorizontal = horizontalPosition; mInsertionMarkerTop = lineTop; mInsertionMarkerBaseline = lineBaseline; mInsertionMarkerBottom = lineBottom; return this; } private float mInsertionMarkerHorizontal = Float.NaN; private float mInsertionMarkerTop = Float.NaN; private float mInsertionMarkerBaseline = Float.NaN; private float mInsertionMarkerBottom = Float.NaN; /** * Adds the bounding box of the character specified with the index. *

* Editor authors should not call this method for characters that are invisible. *

* * @param index index of the character in Java chars units. Must be specified in * ascending order across successive calls. * @param leadingEdgeX x coordinate of the leading edge of the character in local * coordinates, that is, left edge for LTR text and right edge for RTL text. * @param leadingEdgeY y coordinate of the leading edge of the character in local * coordinates. * @param trailingEdgeX x coordinate of the trailing edge of the character in local * coordinates, that is, right edge for LTR text and left edge for RTL text. * @param trailingEdgeY y coordinate of the trailing edge of the character in local * coordinates. * @throws IllegalArgumentException If the index is a negative value, or not greater than * all of the previously called indices. */ public CursorAnchorInfoBuilder addCharacterRect(final int index, final float leadingEdgeX, final float leadingEdgeY, final float trailingEdgeX, final float trailingEdgeY) { if (index < 0) { throw new IllegalArgumentException("index must not be a negative integer."); } if (mCharacterRectBuilder == null) { mCharacterRectBuilder = new SparseRectFArrayBuilder(); } mCharacterRectBuilder.append(index, leadingEdgeX, leadingEdgeY, trailingEdgeX, trailingEdgeY); return this; } private SparseRectFArrayBuilder mCharacterRectBuilder = null; /** * Sets the matrix that transforms local coordinates into screen coordinates. * @param matrix transformation matrix from local coordinates into screen coordinates. null * is interpreted as an identity matrix. */ public CursorAnchorInfoBuilder setMatrix(final Matrix matrix) { if (matrix != null) { mMatrix = matrix; } else { mMatrix = Matrix.IDENTITY_MATRIX; } return this; } private Matrix mMatrix = Matrix.IDENTITY_MATRIX; /** * @return {@link CursorAnchorInfo} using parameters in this * {@link CursorAnchorInfoBuilder}. */ public CursorAnchorInfo build() { return new CursorAnchorInfo(this); } /** * Resets the internal state so that this instance can be reused to build another * instance of {@link CursorAnchorInfo}. */ public void reset() { mSelectionStart = -1; mSelectionEnd = -1; mComposingTextStart = -1; mComposingText = null; mInsertionMarkerHorizontal = Float.NaN; mInsertionMarkerTop = Float.NaN; mInsertionMarkerBaseline = Float.NaN; mInsertionMarkerBottom = Float.NaN; mMatrix = Matrix.IDENTITY_MATRIX; if (mCharacterRectBuilder != null) { mCharacterRectBuilder.reset(); } } } private CursorAnchorInfo(final CursorAnchorInfoBuilder builder) { mSelectionStart = builder.mSelectionStart; mSelectionEnd = builder.mSelectionEnd; mComposingTextStart = builder.mComposingTextStart; mComposingText = builder.mComposingText; mInsertionMarkerHorizontal = builder.mInsertionMarkerHorizontal; mInsertionMarkerTop = builder.mInsertionMarkerTop; mInsertionMarkerBaseline = builder.mInsertionMarkerBaseline; mInsertionMarkerBottom = builder.mInsertionMarkerBottom; mCharacterRects = builder.mCharacterRectBuilder != null ? builder.mCharacterRectBuilder.build() : null; mMatrix = builder.mMatrix; } /** * Returns the index where the selection starts. * @return -1 if there is no selection. */ public int getSelectionStart() { return mSelectionStart; } /** * Returns the index where the selection ends. * @return -1 if there is no selection. */ public int getSelectionEnd() { return mSelectionEnd; } /** * Returns the index where the composing text starts. * @return -1 if there is no composing text. */ public int getComposingTextStart() { return mComposingTextStart; } /** * Returns the entire composing text. * @return null if there is no composition. */ public String getComposingText() { return mComposingText; } /** * Returns the horizontal start of the insertion marker, in the local coordinates that will * be transformed with {@link #getMatrix()} when rendered on the screen. * @return x coordinate that is compatible with {@link Layout#getPrimaryHorizontal(int)}. * Pay special care to RTL/LTR handling. * {@code java.lang.Float.NaN} if not specified. * @see Layout#getPrimaryHorizontal(int) */ public float getInsertionMarkerHorizontal() { return mInsertionMarkerHorizontal; } /** * Returns the vertical top position of the insertion marker, in the local coordinates that * will be transformed with {@link #getMatrix()} when rendered on the screen. * @return y coordinate that is compatible with {@link Layout#getLineTop(int)}. * {@code java.lang.Float.NaN} if not specified. */ public float getInsertionMarkerTop() { return mInsertionMarkerTop; } /** * Returns the vertical baseline position of the insertion marker, in the local coordinates * that will be transformed with {@link #getMatrix()} when rendered on the screen. * @return y coordinate that is compatible with {@link Layout#getLineBaseline(int)}. * {@code java.lang.Float.NaN} if not specified. */ public float getInsertionMarkerBaseline() { return mInsertionMarkerBaseline; } /** * Returns the vertical bottom position of the insertion marker, in the local coordinates * that will be transformed with {@link #getMatrix()} when rendered on the screen. * @return y coordinate that is compatible with {@link Layout#getLineBottom(int)}. * {@code java.lang.Float.NaN} if not specified. */ public float getInsertionMarkerBottom() { return mInsertionMarkerBottom; } /** * Returns a new instance of {@link RectF} that indicates the location of the character * specified with the index. *

* Note that coordinates are not necessarily contiguous or even monotonous, especially when * RTL text and LTR text are mixed. *

* @param index index of the character in a Java chars. * @return a new instance of {@link RectF} that represents the location of the character in * local coordinates. null if the character is invisible or the application did not provide * the location. Note that the {@code left} field can be greater than the {@code right} field * if the character is in RTL text. */ // 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 mCharacterRects.get(index); } /** * Returns a new instance of {@link android.graphics.Matrix} that indicates the transformation * matrix that is to be applied other positional data in this class. * @return a new instance (copy) of the transformation matrix. */ public Matrix getMatrix() { return new Matrix(mMatrix); } /** * Used to make this class parcelable. */ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public CursorAnchorInfo createFromParcel(Parcel source) { return new CursorAnchorInfo(source); } @Override public CursorAnchorInfo[] newArray(int size) { return new CursorAnchorInfo[size]; } }; @Override public int describeContents() { return 0; } }