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