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