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