Home | History | Annotate | Download | only in inputmethod
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 
     17 package android.view.inputmethod;
     18 
     19 import android.graphics.Matrix;
     20 import android.graphics.RectF;
     21 import android.os.Parcel;
     22 import android.os.Parcelable;
     23 import android.text.Layout;
     24 import android.text.SpannedString;
     25 import android.text.TextUtils;
     26 import android.view.inputmethod.SparseRectFArray.SparseRectFArrayBuilder;
     27 
     28 import java.util.Objects;
     29 
     30 /**
     31  * Positional information about the text insertion point and characters in the composition string.
     32  *
     33  * <p>This class encapsulates locations of the text insertion point and the composition string in
     34  * the screen coordinates so that IMEs can render their UI components near where the text is
     35  * actually inserted.</p>
     36  */
     37 public final class CursorAnchorInfo implements Parcelable {
     38     /**
     39      * The index of the first character of the selected text (inclusive). {@code -1} when there is
     40      * no text selection.
     41      */
     42     private final int mSelectionStart;
     43     /**
     44      * The index of the first character of the selected text (exclusive). {@code -1} when there is
     45      * no text selection.
     46      */
     47     private final int mSelectionEnd;
     48 
     49     /**
     50      * The index of the first character of the composing text (inclusive). {@code -1} when there is
     51      * no composing text.
     52      */
     53     private final int mComposingTextStart;
     54     /**
     55      * The text, tracked as a composing region.
     56      */
     57     private final CharSequence mComposingText;
     58 
     59     /**
     60      * Flags of the insertion marker. See {@link #FLAG_HAS_VISIBLE_REGION} for example.
     61      */
     62     private final int mInsertionMarkerFlags;
     63     /**
     64      * Horizontal position of the insertion marker, in the local coordinates that will be
     65      * transformed with the transformation matrix when rendered on the screen. This should be
     66      * calculated or compatible with {@link Layout#getPrimaryHorizontal(int)}. This can be
     67      * {@code java.lang.Float.NaN} when no value is specified.
     68      */
     69     private final float mInsertionMarkerHorizontal;
     70     /**
     71      * Vertical position of the insertion marker, in the local coordinates that will be
     72      * transformed with the transformation matrix when rendered on the screen. This should be
     73      * calculated or compatible with {@link Layout#getLineTop(int)}. This can be
     74      * {@code java.lang.Float.NaN} when no value is specified.
     75      */
     76     private final float mInsertionMarkerTop;
     77     /**
     78      * Vertical position of the insertion marker, in the local coordinates that will be
     79      * transformed with the transformation matrix when rendered on the screen. This should be
     80      * calculated or compatible with {@link Layout#getLineBaseline(int)}. This can be
     81      * {@code java.lang.Float.NaN} when no value is specified.
     82      */
     83     private final float mInsertionMarkerBaseline;
     84     /**
     85      * Vertical position of the insertion marker, in the local coordinates that will be
     86      * transformed with the transformation matrix when rendered on the screen. This should be
     87      * calculated or compatible with {@link Layout#getLineBottom(int)}. This can be
     88      * {@code java.lang.Float.NaN} when no value is specified.
     89      */
     90     private final float mInsertionMarkerBottom;
     91 
     92     /**
     93      * Container of rectangular position of characters, keyed with character index in a unit of
     94      * Java chars, in the local coordinates that will be transformed with the transformation matrix
     95      * when rendered on the screen.
     96      */
     97     private final SparseRectFArray mCharacterBoundsArray;
     98 
     99     /**
    100      * Transformation matrix that is applied to any positional information of this class to
    101      * transform local coordinates into screen coordinates.
    102      */
    103     private final Matrix mMatrix;
    104 
    105     /**
    106      * Flag for {@link #getInsertionMarkerFlags()} and {@link #getCharacterBoundsFlags(int)}: the
    107      * insertion marker or character bounds have at least one visible region.
    108      */
    109     public static final int FLAG_HAS_VISIBLE_REGION = 0x01;
    110 
    111     /**
    112      * Flag for {@link #getInsertionMarkerFlags()} and {@link #getCharacterBoundsFlags(int)}: the
    113      * insertion marker or character bounds have at least one invisible (clipped) region.
    114      */
    115     public static final int FLAG_HAS_INVISIBLE_REGION = 0x02;
    116 
    117     /**
    118      * Flag for {@link #getInsertionMarkerFlags()} and {@link #getCharacterBoundsFlags(int)}: the
    119      * insertion marker or character bounds is placed at right-to-left (RTL) character.
    120      */
    121     public static final int FLAG_IS_RTL = 0x04;
    122 
    123     public CursorAnchorInfo(final Parcel source) {
    124         mSelectionStart = source.readInt();
    125         mSelectionEnd = source.readInt();
    126         mComposingTextStart = source.readInt();
    127         mComposingText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
    128         mInsertionMarkerFlags = source.readInt();
    129         mInsertionMarkerHorizontal = source.readFloat();
    130         mInsertionMarkerTop = source.readFloat();
    131         mInsertionMarkerBaseline = source.readFloat();
    132         mInsertionMarkerBottom = source.readFloat();
    133         mCharacterBoundsArray = source.readParcelable(SparseRectFArray.class.getClassLoader());
    134         mMatrix = new Matrix();
    135         mMatrix.setValues(source.createFloatArray());
    136     }
    137 
    138     /**
    139      * Used to package this object into a {@link Parcel}.
    140      *
    141      * @param dest The {@link Parcel} to be written.
    142      * @param flags The flags used for parceling.
    143      */
    144     @Override
    145     public void writeToParcel(Parcel dest, int flags) {
    146         dest.writeInt(mSelectionStart);
    147         dest.writeInt(mSelectionEnd);
    148         dest.writeInt(mComposingTextStart);
    149         TextUtils.writeToParcel(mComposingText, dest, flags);
    150         dest.writeInt(mInsertionMarkerFlags);
    151         dest.writeFloat(mInsertionMarkerHorizontal);
    152         dest.writeFloat(mInsertionMarkerTop);
    153         dest.writeFloat(mInsertionMarkerBaseline);
    154         dest.writeFloat(mInsertionMarkerBottom);
    155         dest.writeParcelable(mCharacterBoundsArray, flags);
    156         final float[] matrixArray = new float[9];
    157         mMatrix.getValues(matrixArray);
    158         dest.writeFloatArray(matrixArray);
    159     }
    160 
    161     @Override
    162     public int hashCode(){
    163         final float floatHash = mInsertionMarkerHorizontal + mInsertionMarkerTop
    164                 + mInsertionMarkerBaseline + mInsertionMarkerBottom;
    165         int hash = floatHash > 0 ? (int) floatHash : (int)(-floatHash);
    166         hash *= 31;
    167         hash += mInsertionMarkerFlags;
    168         hash *= 31;
    169         hash += mSelectionStart + mSelectionEnd + mComposingTextStart;
    170         hash *= 31;
    171         hash += Objects.hashCode(mComposingText);
    172         hash *= 31;
    173         hash += Objects.hashCode(mCharacterBoundsArray);
    174         hash *= 31;
    175         hash += Objects.hashCode(mMatrix);
    176         return hash;
    177     }
    178 
    179     /**
    180      * Compares two float values. Returns {@code true} if {@code a} and {@code b} are
    181      * {@link Float#NaN} at the same time.
    182      */
    183     private static boolean areSameFloatImpl(final float a, final float b) {
    184         if (Float.isNaN(a) && Float.isNaN(b)) {
    185             return true;
    186         }
    187         return a == b;
    188     }
    189 
    190     @Override
    191     public boolean equals(Object obj){
    192         if (obj == null) {
    193             return false;
    194         }
    195         if (this == obj) {
    196             return true;
    197         }
    198         if (!(obj instanceof CursorAnchorInfo)) {
    199             return false;
    200         }
    201         final CursorAnchorInfo that = (CursorAnchorInfo) obj;
    202         if (hashCode() != that.hashCode()) {
    203             return false;
    204         }
    205         if (mSelectionStart != that.mSelectionStart || mSelectionEnd != that.mSelectionEnd) {
    206             return false;
    207         }
    208         if (mComposingTextStart != that.mComposingTextStart
    209                 || !Objects.equals(mComposingText, that.mComposingText)) {
    210             return false;
    211         }
    212         if (mInsertionMarkerFlags != that.mInsertionMarkerFlags
    213                 || !areSameFloatImpl(mInsertionMarkerHorizontal, that.mInsertionMarkerHorizontal)
    214                 || !areSameFloatImpl(mInsertionMarkerTop, that.mInsertionMarkerTop)
    215                 || !areSameFloatImpl(mInsertionMarkerBaseline, that.mInsertionMarkerBaseline)
    216                 || !areSameFloatImpl(mInsertionMarkerBottom, that.mInsertionMarkerBottom)) {
    217             return false;
    218         }
    219         if (!Objects.equals(mCharacterBoundsArray, that.mCharacterBoundsArray)) {
    220             return false;
    221         }
    222         if (!Objects.equals(mMatrix, that.mMatrix)) {
    223             return false;
    224         }
    225         return true;
    226     }
    227 
    228     @Override
    229     public String toString() {
    230         return "SelectionInfo{mSelection=" + mSelectionStart + "," + mSelectionEnd
    231                 + " mComposingTextStart=" + mComposingTextStart
    232                 + " mComposingText=" + Objects.toString(mComposingText)
    233                 + " mInsertionMarkerFlags=" + mInsertionMarkerFlags
    234                 + " mInsertionMarkerHorizontal=" + mInsertionMarkerHorizontal
    235                 + " mInsertionMarkerTop=" + mInsertionMarkerTop
    236                 + " mInsertionMarkerBaseline=" + mInsertionMarkerBaseline
    237                 + " mInsertionMarkerBottom=" + mInsertionMarkerBottom
    238                 + " mCharacterBoundsArray=" + Objects.toString(mCharacterBoundsArray)
    239                 + " mMatrix=" + Objects.toString(mMatrix)
    240                 + "}";
    241     }
    242 
    243     /**
    244      * Builder for {@link CursorAnchorInfo}. This class is not designed to be thread-safe.
    245      */
    246     public static final class Builder {
    247         private int mSelectionStart = -1;
    248         private int mSelectionEnd = -1;
    249         private int mComposingTextStart = -1;
    250         private CharSequence mComposingText = null;
    251         private float mInsertionMarkerHorizontal = Float.NaN;
    252         private float mInsertionMarkerTop = Float.NaN;
    253         private float mInsertionMarkerBaseline = Float.NaN;
    254         private float mInsertionMarkerBottom = Float.NaN;
    255         private int mInsertionMarkerFlags = 0;
    256         private SparseRectFArrayBuilder mCharacterBoundsArrayBuilder = null;
    257         private final Matrix mMatrix = new Matrix(Matrix.IDENTITY_MATRIX);
    258         private boolean mMatrixInitialized = false;
    259 
    260         /**
    261          * Sets the text range of the selection. Calling this can be skipped if there is no
    262          * selection.
    263          */
    264         public Builder setSelectionRange(final int newStart, final int newEnd) {
    265             mSelectionStart = newStart;
    266             mSelectionEnd = newEnd;
    267             return this;
    268         }
    269 
    270         /**
    271          * Sets the text range of the composing text. Calling this can be skipped if there is
    272          * no composing text.
    273          * @param composingTextStart index where the composing text starts.
    274          * @param composingText the entire composing text.
    275          */
    276         public Builder setComposingText(final int composingTextStart,
    277             final CharSequence composingText) {
    278             mComposingTextStart = composingTextStart;
    279             if (composingText == null) {
    280                 mComposingText = null;
    281             } else {
    282                 // Make a snapshot of the given char sequence.
    283                 mComposingText = new SpannedString(composingText);
    284             }
    285             return this;
    286         }
    287 
    288         /**
    289          * Sets the location of the text insertion point (zero width cursor) as a rectangle in
    290          * local coordinates. Calling this can be skipped when there is no text insertion point;
    291          * however if there is an insertion point, editors must call this method.
    292          * @param horizontalPosition horizontal position of the insertion marker, in the local
    293          * coordinates that will be transformed with the transformation matrix when rendered on the
    294          * screen. This should be calculated or compatible with
    295          * {@link Layout#getPrimaryHorizontal(int)}.
    296          * @param lineTop vertical position of the insertion marker, in the local coordinates that
    297          * will be transformed with the transformation matrix when rendered on the screen. This
    298          * should be calculated or compatible with {@link Layout#getLineTop(int)}.
    299          * @param lineBaseline vertical position of the insertion marker, in the local coordinates
    300          * that will be transformed with the transformation matrix when rendered on the screen. This
    301          * should be calculated or compatible with {@link Layout#getLineBaseline(int)}.
    302          * @param lineBottom vertical position of the insertion marker, in the local coordinates
    303          * that will be transformed with the transformation matrix when rendered on the screen. This
    304          * should be calculated or compatible with {@link Layout#getLineBottom(int)}.
    305          * @param flags flags of the insertion marker. See {@link #FLAG_HAS_VISIBLE_REGION} for
    306          * example.
    307          */
    308         public Builder setInsertionMarkerLocation(final float horizontalPosition,
    309                 final float lineTop, final float lineBaseline, final float lineBottom,
    310                 final int flags){
    311             mInsertionMarkerHorizontal = horizontalPosition;
    312             mInsertionMarkerTop = lineTop;
    313             mInsertionMarkerBaseline = lineBaseline;
    314             mInsertionMarkerBottom = lineBottom;
    315             mInsertionMarkerFlags = flags;
    316             return this;
    317         }
    318 
    319         /**
    320          * Adds the bounding box of the character specified with the index.
    321          *
    322          * @param index index of the character in Java chars units. Must be specified in
    323          * ascending order across successive calls.
    324          * @param left x coordinate of the left edge of the character in local coordinates.
    325          * @param top y coordinate of the top edge of the character in local coordinates.
    326          * @param right x coordinate of the right edge of the character in local coordinates.
    327          * @param bottom y coordinate of the bottom edge of the character in local coordinates.
    328          * @param flags flags for this character bounds. See {@link #FLAG_HAS_VISIBLE_REGION},
    329          * {@link #FLAG_HAS_INVISIBLE_REGION} and {@link #FLAG_IS_RTL}. These flags must be
    330          * specified when necessary.
    331          * @throws IllegalArgumentException If the index is a negative value, or not greater than
    332          * all of the previously called indices.
    333          */
    334         public Builder addCharacterBounds(final int index, final float left, final float top,
    335                 final float right, final float bottom, final int flags) {
    336             if (index < 0) {
    337                 throw new IllegalArgumentException("index must not be a negative integer.");
    338             }
    339             if (mCharacterBoundsArrayBuilder == null) {
    340                 mCharacterBoundsArrayBuilder = new SparseRectFArrayBuilder();
    341             }
    342             mCharacterBoundsArrayBuilder.append(index, left, top, right, bottom, flags);
    343             return this;
    344         }
    345 
    346         /**
    347          * Sets the matrix that transforms local coordinates into screen coordinates.
    348          * @param matrix transformation matrix from local coordinates into screen coordinates. null
    349          * is interpreted as an identity matrix.
    350          */
    351         public Builder setMatrix(final Matrix matrix) {
    352             mMatrix.set(matrix != null ? matrix : Matrix.IDENTITY_MATRIX);
    353             mMatrixInitialized = true;
    354             return this;
    355         }
    356 
    357         /**
    358          * @return {@link CursorAnchorInfo} using parameters in this {@link Builder}.
    359          * @throws IllegalArgumentException if one or more positional parameters are specified but
    360          * the coordinate transformation matrix is not provided via {@link #setMatrix(Matrix)}.
    361          */
    362         public CursorAnchorInfo build() {
    363             if (!mMatrixInitialized) {
    364                 // Coordinate transformation matrix is mandatory when at least one positional
    365                 // parameter is specified.
    366                 final boolean hasCharacterBounds = (mCharacterBoundsArrayBuilder != null
    367                         && !mCharacterBoundsArrayBuilder.isEmpty());
    368                 if (hasCharacterBounds
    369                         || !Float.isNaN(mInsertionMarkerHorizontal)
    370                         || !Float.isNaN(mInsertionMarkerTop)
    371                         || !Float.isNaN(mInsertionMarkerBaseline)
    372                         || !Float.isNaN(mInsertionMarkerBottom)) {
    373                     throw new IllegalArgumentException("Coordinate transformation matrix is " +
    374                             "required when positional parameters are specified.");
    375                 }
    376             }
    377             return new CursorAnchorInfo(this);
    378         }
    379 
    380         /**
    381          * Resets the internal state so that this instance can be reused to build another
    382          * instance of {@link CursorAnchorInfo}.
    383          */
    384         public void reset() {
    385             mSelectionStart = -1;
    386             mSelectionEnd = -1;
    387             mComposingTextStart = -1;
    388             mComposingText = null;
    389             mInsertionMarkerFlags = 0;
    390             mInsertionMarkerHorizontal = Float.NaN;
    391             mInsertionMarkerTop = Float.NaN;
    392             mInsertionMarkerBaseline = Float.NaN;
    393             mInsertionMarkerBottom = Float.NaN;
    394             mMatrix.set(Matrix.IDENTITY_MATRIX);
    395             mMatrixInitialized = false;
    396             if (mCharacterBoundsArrayBuilder != null) {
    397                 mCharacterBoundsArrayBuilder.reset();
    398             }
    399         }
    400     }
    401 
    402     private CursorAnchorInfo(final Builder builder) {
    403         mSelectionStart = builder.mSelectionStart;
    404         mSelectionEnd = builder.mSelectionEnd;
    405         mComposingTextStart = builder.mComposingTextStart;
    406         mComposingText = builder.mComposingText;
    407         mInsertionMarkerFlags = builder.mInsertionMarkerFlags;
    408         mInsertionMarkerHorizontal = builder.mInsertionMarkerHorizontal;
    409         mInsertionMarkerTop = builder.mInsertionMarkerTop;
    410         mInsertionMarkerBaseline = builder.mInsertionMarkerBaseline;
    411         mInsertionMarkerBottom = builder.mInsertionMarkerBottom;
    412         mCharacterBoundsArray = builder.mCharacterBoundsArrayBuilder != null ?
    413                 builder.mCharacterBoundsArrayBuilder.build() : null;
    414         mMatrix = new Matrix(builder.mMatrix);
    415     }
    416 
    417     /**
    418      * Returns the index where the selection starts.
    419      * @return {@code -1} if there is no selection.
    420      */
    421     public int getSelectionStart() {
    422         return mSelectionStart;
    423     }
    424 
    425     /**
    426      * Returns the index where the selection ends.
    427      * @return {@code -1} if there is no selection.
    428      */
    429     public int getSelectionEnd() {
    430         return mSelectionEnd;
    431     }
    432 
    433     /**
    434      * Returns the index where the composing text starts.
    435      * @return {@code -1} if there is no composing text.
    436      */
    437     public int getComposingTextStart() {
    438         return mComposingTextStart;
    439     }
    440 
    441     /**
    442      * Returns the entire composing text.
    443      * @return {@code null} if there is no composition.
    444      */
    445     public CharSequence getComposingText() {
    446         return mComposingText;
    447     }
    448 
    449     /**
    450      * Returns the flag of the insertion marker.
    451      * @return the flag of the insertion marker. {@code 0} if no flag is specified.
    452      */
    453     public int getInsertionMarkerFlags() {
    454         return mInsertionMarkerFlags;
    455     }
    456 
    457     /**
    458      * Returns the horizontal start of the insertion marker, in the local coordinates that will
    459      * be transformed with {@link #getMatrix()} when rendered on the screen.
    460      * @return x coordinate that is compatible with {@link Layout#getPrimaryHorizontal(int)}.
    461      * Pay special care to RTL/LTR handling.
    462      * {@code java.lang.Float.NaN} if not specified.
    463      * @see Layout#getPrimaryHorizontal(int)
    464      */
    465     public float getInsertionMarkerHorizontal() {
    466         return mInsertionMarkerHorizontal;
    467     }
    468 
    469     /**
    470      * Returns the vertical top position of the insertion marker, in the local coordinates that
    471      * will be transformed with {@link #getMatrix()} when rendered on the screen.
    472      * @return y coordinate that is compatible with {@link Layout#getLineTop(int)}.
    473      * {@code java.lang.Float.NaN} if not specified.
    474      */
    475     public float getInsertionMarkerTop() {
    476         return mInsertionMarkerTop;
    477     }
    478 
    479     /**
    480      * Returns the vertical baseline position of the insertion marker, in the local coordinates
    481      * that will be transformed with {@link #getMatrix()} when rendered on the screen.
    482      * @return y coordinate that is compatible with {@link Layout#getLineBaseline(int)}.
    483      * {@code java.lang.Float.NaN} if not specified.
    484      */
    485     public float getInsertionMarkerBaseline() {
    486         return mInsertionMarkerBaseline;
    487     }
    488 
    489     /**
    490      * Returns the vertical bottom position of the insertion marker, in the local coordinates
    491      * that will be transformed with {@link #getMatrix()} when rendered on the screen.
    492      * @return y coordinate that is compatible with {@link Layout#getLineBottom(int)}.
    493      * {@code java.lang.Float.NaN} if not specified.
    494      */
    495     public float getInsertionMarkerBottom() {
    496         return mInsertionMarkerBottom;
    497     }
    498 
    499     /**
    500      * Returns a new instance of {@link RectF} that indicates the location of the character
    501      * specified with the index.
    502      * @param index index of the character in a Java chars.
    503      * @return the character bounds in local coordinates as a new instance of {@link RectF}.
    504      */
    505     public RectF getCharacterBounds(final int index) {
    506         if (mCharacterBoundsArray == null) {
    507             return null;
    508         }
    509         return mCharacterBoundsArray.get(index);
    510     }
    511 
    512     /**
    513      * Returns the flags associated with the character bounds specified with the index.
    514      * @param index index of the character in a Java chars.
    515      * @return {@code 0} if no flag is specified.
    516      */
    517     public int getCharacterBoundsFlags(final int index) {
    518         if (mCharacterBoundsArray == null) {
    519             return 0;
    520         }
    521         return mCharacterBoundsArray.getFlags(index, 0);
    522     }
    523 
    524     /**
    525      * Returns a new instance of {@link android.graphics.Matrix} that indicates the transformation
    526      * matrix that is to be applied other positional data in this class.
    527      * @return a new instance (copy) of the transformation matrix.
    528      */
    529     public Matrix getMatrix() {
    530         return new Matrix(mMatrix);
    531     }
    532 
    533     /**
    534      * Used to make this class parcelable.
    535      */
    536     public static final Parcelable.Creator<CursorAnchorInfo> CREATOR
    537             = new Parcelable.Creator<CursorAnchorInfo>() {
    538         @Override
    539         public CursorAnchorInfo createFromParcel(Parcel source) {
    540             return new CursorAnchorInfo(source);
    541         }
    542 
    543         @Override
    544         public CursorAnchorInfo[] newArray(int size) {
    545             return new CursorAnchorInfo[size];
    546         }
    547     };
    548 
    549     @Override
    550     public int describeContents() {
    551         return 0;
    552     }
    553 }
    554