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 
CursorAnchorInfo(final Parcel source)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
writeToParcel(Parcel dest, int flags)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
hashCode()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      */
areSameFloatImpl(final float a, final float b)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
equals(Object obj)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
toString()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          */
setSelectionRange(final int newStart, final int newEnd)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          */
setComposingText(final int composingTextStart, final CharSequence composingText)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          */
setInsertionMarkerLocation(final float horizontalPosition, final float lineTop, final float lineBaseline, final float lineBottom, final int flags)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          */
addCharacterBounds(final int index, final float left, final float top, final float right, final float bottom, final int flags)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          */
setMatrix(final Matrix matrix)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          */
build()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          */
reset()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 
CursorAnchorInfo(final Builder builder)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      */
getSelectionStart()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      */
getSelectionEnd()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      */
getComposingTextStart()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      */
getComposingText()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      */
getInsertionMarkerFlags()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      */
getInsertionMarkerHorizontal()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      */
getInsertionMarkerTop()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      */
getInsertionMarkerBaseline()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      */
getInsertionMarkerBottom()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      */
getCharacterBounds(final int index)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      */
getCharacterBoundsFlags(final int index)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      */
getMatrix()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
describeContents()576     public int describeContents() {
577         return 0;
578     }
579 }
580