CursorAnchorInfo.java revision a1fda005f9583486f4372ecbb0eec503ee9cece8
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     * Horizontal position of the insertion marker, in the local coordinates that will be
49     * transformed with the transformation matrix when rendered on the screen. This should be
50     * calculated or compatible with {@link Layout#getPrimaryHorizontal(int)}. This can be
51     * {@code java.lang.Float.NaN} when no value is specified.
52     */
53    private final float mInsertionMarkerHorizontal;
54    /**
55     * Vertical position of the insertion marker, in the local coordinates that will be
56     * transformed with the transformation matrix when rendered on the screen. This should be
57     * calculated or compatible with {@link Layout#getLineTop(int)}. This can be
58     * {@code java.lang.Float.NaN} when no value is specified.
59     */
60    private final float mInsertionMarkerTop;
61    /**
62     * Vertical position of the insertion marker, in the local coordinates that will be
63     * transformed with the transformation matrix when rendered on the screen. This should be
64     * calculated or compatible with {@link Layout#getLineBaseline(int)}. This can be
65     * {@code java.lang.Float.NaN} when no value is specified.
66     */
67    private final float mInsertionMarkerBaseline;
68    /**
69     * Vertical position of the insertion marker, in the local coordinates that will be
70     * transformed with the transformation matrix when rendered on the screen. This should be
71     * calculated or compatible with {@link Layout#getLineBottom(int)}. This can be
72     * {@code java.lang.Float.NaN} when no value is specified.
73     */
74    private final float mInsertionMarkerBottom;
75
76    /**
77     * Container of rectangular position of characters, keyed with character index in a unit of
78     * Java chars, in the local coordinates that will be transformed with the transformation matrix
79     * when rendered on the screen.
80     */
81    private final SparseRectFArray mCharacterRects;
82
83    /**
84     * Transformation matrix that is applied to any positional information of this class to
85     * transform local coordinates into screen coordinates.
86     */
87    private final Matrix mMatrix;
88
89    public CursorAnchorInfo(final Parcel source) {
90        mSelectionStart = source.readInt();
91        mSelectionEnd = source.readInt();
92        mComposingTextStart = source.readInt();
93        mComposingText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
94        mInsertionMarkerHorizontal = source.readFloat();
95        mInsertionMarkerTop = source.readFloat();
96        mInsertionMarkerBaseline = source.readFloat();
97        mInsertionMarkerBottom = source.readFloat();
98        mCharacterRects = source.readParcelable(SparseRectFArray.class.getClassLoader());
99        mMatrix = new Matrix();
100        mMatrix.setValues(source.createFloatArray());
101    }
102
103    /**
104     * Used to package this object into a {@link Parcel}.
105     *
106     * @param dest The {@link Parcel} to be written.
107     * @param flags The flags used for parceling.
108     */
109    @Override
110    public void writeToParcel(Parcel dest, int flags) {
111        dest.writeInt(mSelectionStart);
112        dest.writeInt(mSelectionEnd);
113        dest.writeInt(mComposingTextStart);
114        TextUtils.writeToParcel(mComposingText, dest, flags);
115        dest.writeFloat(mInsertionMarkerHorizontal);
116        dest.writeFloat(mInsertionMarkerTop);
117        dest.writeFloat(mInsertionMarkerBaseline);
118        dest.writeFloat(mInsertionMarkerBottom);
119        dest.writeParcelable(mCharacterRects, flags);
120        final float[] matrixArray = new float[9];
121        mMatrix.getValues(matrixArray);
122        dest.writeFloatArray(matrixArray);
123    }
124
125    @Override
126    public int hashCode(){
127        // TODO: Improve the hash function.
128        final float floatHash = mInsertionMarkerHorizontal + mInsertionMarkerTop
129                + mInsertionMarkerBaseline + mInsertionMarkerBottom;
130        int hash = floatHash > 0 ? (int) floatHash : (int)(-floatHash);
131        hash *= 31;
132        hash += mSelectionStart + mSelectionEnd + mComposingTextStart;
133        hash *= 31;
134        hash += Objects.hashCode(mComposingText);
135        hash *= 31;
136        hash += Objects.hashCode(mCharacterRects);
137        hash *= 31;
138        hash += Objects.hashCode(mMatrix);
139        return hash;
140    }
141
142    @Override
143    public boolean equals(Object obj){
144        if (obj == null) {
145            return false;
146        }
147        if (this == obj) {
148            return true;
149        }
150        if (!(obj instanceof CursorAnchorInfo)) {
151            return false;
152        }
153        final CursorAnchorInfo that = (CursorAnchorInfo) obj;
154        if (hashCode() != that.hashCode()) {
155            return false;
156        }
157        if (mSelectionStart != that.mSelectionStart
158                || mSelectionEnd != that.mSelectionEnd
159                || mComposingTextStart != that.mComposingTextStart) {
160            return false;
161        }
162        if (!Objects.equals(mComposingTextStart, that.mComposingTextStart)) {
163            return false;
164        }
165        if (!Objects.equals(mCharacterRects, that.mCharacterRects)) {
166            return false;
167        }
168        if (!Objects.equals(mMatrix, that.mMatrix)) {
169            return false;
170        }
171        return true;
172    }
173
174    @Override
175    public String toString() {
176        return "SelectionInfo{mSelection=" + mSelectionStart + "," + mSelectionEnd
177                + " mComposingTextStart=" + mComposingTextStart
178                + " mComposingText=" + Objects.toString(mComposingText)
179                + " mInsertionMarkerHorizontal=" + mInsertionMarkerHorizontal
180                + " mInsertionMarkerTop=" + mInsertionMarkerTop
181                + " mInsertionMarkerBaseline=" + mInsertionMarkerBaseline
182                + " mInsertionMarkerBottom=" + mInsertionMarkerBottom
183                + " mCharacterRects=" + Objects.toString(mCharacterRects)
184                + " mMatrix=" + Objects.toString(mMatrix)
185                + "}";
186    }
187
188    /**
189     * Builder for {@link CursorAnchorInfo}. This class is not designed to be thread-safe.
190     */
191    public static final class Builder {
192        /**
193         * Sets the text range of the selection. Calling this can be skipped if there is no
194         * selection.
195         */
196        public Builder setSelectionRange(final int newStart, final int newEnd) {
197            mSelectionStart = newStart;
198            mSelectionEnd = newEnd;
199            return this;
200        }
201        private int mSelectionStart = -1;
202        private int mSelectionEnd = -1;
203
204        /**
205         * Sets the text range of the composing text. Calling this can be skipped if there is
206         * no composing text.
207         * @param index index where the composing text starts.
208         * @param composingText the entire composing text.
209         */
210        public Builder setComposingText(final int index, final CharSequence composingText) {
211            mComposingTextStart = index;
212            if (composingText == null) {
213                mComposingText = null;
214            } else {
215                // Make a snapshot of the given char sequence.
216                mComposingText = new SpannedString(composingText);
217            }
218            return this;
219        }
220        private int mComposingTextStart = -1;
221        private CharSequence mComposingText = null;
222
223        /**
224         * Sets the location of the text insertion point (zero width cursor) as a rectangle in
225         * local coordinates. Calling this can be skipped when there is no text insertion point;
226         * however if there is an insertion point, editors must call this method.
227         * @param horizontalPosition horizontal position of the insertion marker, in the local
228         * coordinates that will be transformed with the transformation matrix when rendered on the
229         * screen. This should be calculated or compatible with
230         * {@link Layout#getPrimaryHorizontal(int)}.
231         * @param lineTop vertical position of the insertion marker, in the local coordinates that
232         * will be transformed with the transformation matrix when rendered on the screen. This
233         * should be calculated or compatible with {@link Layout#getLineTop(int)}.
234         * @param lineBaseline vertical position of the insertion marker, in the local coordinates
235         * that will be transformed with the transformation matrix when rendered on the screen. This
236         * should be calculated or compatible with {@link Layout#getLineBaseline(int)}.
237         * @param lineBottom vertical position of the insertion marker, in the local coordinates
238         * that will be transformed with the transformation matrix when rendered on the screen. This
239         * should be calculated or compatible with {@link Layout#getLineBottom(int)}.
240         */
241        public Builder setInsertionMarkerLocation(final float horizontalPosition,
242                final float lineTop, final float lineBaseline, final float lineBottom){
243            mInsertionMarkerHorizontal = horizontalPosition;
244            mInsertionMarkerTop = lineTop;
245            mInsertionMarkerBaseline = lineBaseline;
246            mInsertionMarkerBottom = lineBottom;
247            return this;
248        }
249        private float mInsertionMarkerHorizontal = Float.NaN;
250        private float mInsertionMarkerTop = Float.NaN;
251        private float mInsertionMarkerBaseline = Float.NaN;
252        private float mInsertionMarkerBottom = Float.NaN;
253
254        /**
255         * Adds the bounding box of the character specified with the index.
256         * <p>
257         * Editor authors should not call this method for characters that are invisible.
258         * </p>
259         *
260         * @param index index of the character in Java chars units. Must be specified in
261         * ascending order across successive calls.
262         * @param leadingEdgeX x coordinate of the leading edge of the character in local
263         * coordinates, that is, left edge for LTR text and right edge for RTL text.
264         * @param leadingEdgeY y coordinate of the leading edge of the character in local
265         * coordinates.
266         * @param trailingEdgeX x coordinate of the trailing edge of the character in local
267         * coordinates, that is, right edge for LTR text and left edge for RTL text.
268         * @param trailingEdgeY y coordinate of the trailing edge of the character in local
269         * coordinates.
270         * @throws IllegalArgumentException If the index is a negative value, or not greater than
271         * all of the previously called indices.
272         */
273        public Builder addCharacterRect(final int index, final float leadingEdgeX,
274                final float leadingEdgeY, final float trailingEdgeX, final float trailingEdgeY) {
275            if (index < 0) {
276                throw new IllegalArgumentException("index must not be a negative integer.");
277            }
278            if (mCharacterRectBuilder == null) {
279                mCharacterRectBuilder = new SparseRectFArrayBuilder();
280            }
281            mCharacterRectBuilder.append(index, leadingEdgeX, leadingEdgeY, trailingEdgeX,
282                    trailingEdgeY);
283            return this;
284        }
285        private SparseRectFArrayBuilder mCharacterRectBuilder = null;
286
287        /**
288         * Sets the matrix that transforms local coordinates into screen coordinates.
289         * @param matrix transformation matrix from local coordinates into screen coordinates. null
290         * is interpreted as an identity matrix.
291         */
292        public Builder setMatrix(final Matrix matrix) {
293            mMatrix.set(matrix != null ? matrix : Matrix.IDENTITY_MATRIX);
294            return this;
295        }
296        private final Matrix mMatrix = new Matrix(Matrix.IDENTITY_MATRIX);
297
298        /**
299         * @return {@link CursorAnchorInfo} using parameters in this
300         * {@link Builder}.
301         */
302        public CursorAnchorInfo build() {
303            return new CursorAnchorInfo(this);
304        }
305
306        /**
307         * Resets the internal state so that this instance can be reused to build another
308         * instance of {@link CursorAnchorInfo}.
309         */
310        public void reset() {
311            mSelectionStart = -1;
312            mSelectionEnd = -1;
313            mComposingTextStart = -1;
314            mComposingText = null;
315            mInsertionMarkerHorizontal = Float.NaN;
316            mInsertionMarkerTop = Float.NaN;
317            mInsertionMarkerBaseline = Float.NaN;
318            mInsertionMarkerBottom = Float.NaN;
319            mMatrix.set(Matrix.IDENTITY_MATRIX);
320            if (mCharacterRectBuilder != null) {
321                mCharacterRectBuilder.reset();
322            }
323        }
324    }
325
326    private CursorAnchorInfo(final Builder builder) {
327        mSelectionStart = builder.mSelectionStart;
328        mSelectionEnd = builder.mSelectionEnd;
329        mComposingTextStart = builder.mComposingTextStart;
330        mComposingText = builder.mComposingText;
331        mInsertionMarkerHorizontal = builder.mInsertionMarkerHorizontal;
332        mInsertionMarkerTop = builder.mInsertionMarkerTop;
333        mInsertionMarkerBaseline = builder.mInsertionMarkerBaseline;
334        mInsertionMarkerBottom = builder.mInsertionMarkerBottom;
335        mCharacterRects = builder.mCharacterRectBuilder != null ?
336                builder.mCharacterRectBuilder.build() : null;
337        mMatrix = new Matrix(builder.mMatrix);
338    }
339
340    /**
341     * Returns the index where the selection starts.
342     * @return -1 if there is no selection.
343     */
344    public int getSelectionStart() {
345        return mSelectionStart;
346    }
347
348    /**
349     * Returns the index where the selection ends.
350     * @return -1 if there is no selection.
351     */
352    public int getSelectionEnd() {
353        return mSelectionEnd;
354    }
355
356    /**
357     * Returns the index where the composing text starts.
358     * @return -1 if there is no composing text.
359     */
360    public int getComposingTextStart() {
361        return mComposingTextStart;
362    }
363
364    /**
365     * Returns the entire composing text.
366     * @return null if there is no composition.
367     */
368    public CharSequence getComposingText() {
369        return mComposingText;
370    }
371
372    /**
373     * Returns the horizontal start of the insertion marker, in the local coordinates that will
374     * be transformed with {@link #getMatrix()} when rendered on the screen.
375     * @return x coordinate that is compatible with {@link Layout#getPrimaryHorizontal(int)}.
376     * Pay special care to RTL/LTR handling.
377     * {@code java.lang.Float.NaN} if not specified.
378     * @see Layout#getPrimaryHorizontal(int)
379     */
380    public float getInsertionMarkerHorizontal() {
381        return mInsertionMarkerHorizontal;
382    }
383    /**
384     * Returns the vertical top position of the insertion marker, in the local coordinates that
385     * will be transformed with {@link #getMatrix()} when rendered on the screen.
386     * @return y coordinate that is compatible with {@link Layout#getLineTop(int)}.
387     * {@code java.lang.Float.NaN} if not specified.
388     */
389    public float getInsertionMarkerTop() {
390        return mInsertionMarkerTop;
391    }
392    /**
393     * Returns the vertical baseline position of the insertion marker, in the local coordinates
394     * that will be transformed with {@link #getMatrix()} when rendered on the screen.
395     * @return y coordinate that is compatible with {@link Layout#getLineBaseline(int)}.
396     * {@code java.lang.Float.NaN} if not specified.
397     */
398    public float getInsertionMarkerBaseline() {
399        return mInsertionMarkerBaseline;
400    }
401    /**
402     * Returns the vertical bottom position of the insertion marker, in the local coordinates
403     * that will be transformed with {@link #getMatrix()} when rendered on the screen.
404     * @return y coordinate that is compatible with {@link Layout#getLineBottom(int)}.
405     * {@code java.lang.Float.NaN} if not specified.
406     */
407    public float getInsertionMarkerBottom() {
408        return mInsertionMarkerBottom;
409    }
410
411    /**
412     * Returns a new instance of {@link RectF} that indicates the location of the character
413     * specified with the index.
414     * <p>
415     * Note that coordinates are not necessarily contiguous or even monotonous, especially when
416     * RTL text and LTR text are mixed.
417     * </p>
418     * @param index index of the character in a Java chars.
419     * @return a new instance of {@link RectF} that represents the location of the character in
420     * local coordinates. null if the character is invisible or the application did not provide
421     * the location. Note that the {@code left} field can be greater than the {@code right} field
422     * if the character is in RTL text.
423     */
424    // TODO: Prepare a document about the expected behavior for surrogate pairs, combining
425    // characters, and non-graphical chars.
426    public RectF getCharacterRect(final int index) {
427        if (mCharacterRects == null) {
428            return null;
429        }
430        return mCharacterRects.get(index);
431    }
432
433    /**
434     * Returns a new instance of {@link android.graphics.Matrix} that indicates the transformation
435     * matrix that is to be applied other positional data in this class.
436     * @return a new instance (copy) of the transformation matrix.
437     */
438    public Matrix getMatrix() {
439        return new Matrix(mMatrix);
440    }
441
442    /**
443     * Used to make this class parcelable.
444     */
445    public static final Parcelable.Creator<CursorAnchorInfo> CREATOR
446            = new Parcelable.Creator<CursorAnchorInfo>() {
447        @Override
448        public CursorAnchorInfo createFromParcel(Parcel source) {
449            return new CursorAnchorInfo(source);
450        }
451
452        @Override
453        public CursorAnchorInfo[] newArray(int size) {
454            return new CursorAnchorInfo[size];
455        }
456    };
457
458    @Override
459    public int describeContents() {
460        return 0;
461    }
462}
463