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