CursorAnchorInfo.java revision b5268dcc17cd9ecb540b06ad59bd74188b57a069
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            mMatrixInitialized = true;
295            return this;
296        }
297        private final Matrix mMatrix = new Matrix(Matrix.IDENTITY_MATRIX);
298        private boolean mMatrixInitialized = false;
299
300        /**
301         * @return {@link CursorAnchorInfo} using parameters in this {@link Builder}.
302         * @throws IllegalArgumentException if one or more positional parameters are specified but
303         * the coordinate transformation matrix is not provided via {@link #setMatrix(Matrix)}.
304         */
305        public CursorAnchorInfo build() {
306            if (!mMatrixInitialized) {
307                // Coordinate transformation matrix is mandatory when positional parameters are
308                // specified.
309                if ((mCharacterRectBuilder != null && !mCharacterRectBuilder.isEmpty()) ||
310                        !Float.isNaN(mInsertionMarkerHorizontal) ||
311                        !Float.isNaN(mInsertionMarkerTop) ||
312                        !Float.isNaN(mInsertionMarkerBaseline) ||
313                        !Float.isNaN(mInsertionMarkerBottom)) {
314                    throw new IllegalArgumentException("Coordinate transformation matrix is " +
315                            "required when positional parameters are specified.");
316                }
317            }
318            return new CursorAnchorInfo(this);
319        }
320
321        /**
322         * Resets the internal state so that this instance can be reused to build another
323         * instance of {@link CursorAnchorInfo}.
324         */
325        public void reset() {
326            mSelectionStart = -1;
327            mSelectionEnd = -1;
328            mComposingTextStart = -1;
329            mComposingText = null;
330            mInsertionMarkerHorizontal = Float.NaN;
331            mInsertionMarkerTop = Float.NaN;
332            mInsertionMarkerBaseline = Float.NaN;
333            mInsertionMarkerBottom = Float.NaN;
334            mMatrix.set(Matrix.IDENTITY_MATRIX);
335            mMatrixInitialized = false;
336            if (mCharacterRectBuilder != null) {
337                mCharacterRectBuilder.reset();
338            }
339        }
340    }
341
342    private CursorAnchorInfo(final Builder builder) {
343        mSelectionStart = builder.mSelectionStart;
344        mSelectionEnd = builder.mSelectionEnd;
345        mComposingTextStart = builder.mComposingTextStart;
346        mComposingText = builder.mComposingText;
347        mInsertionMarkerHorizontal = builder.mInsertionMarkerHorizontal;
348        mInsertionMarkerTop = builder.mInsertionMarkerTop;
349        mInsertionMarkerBaseline = builder.mInsertionMarkerBaseline;
350        mInsertionMarkerBottom = builder.mInsertionMarkerBottom;
351        mCharacterRects = builder.mCharacterRectBuilder != null ?
352                builder.mCharacterRectBuilder.build() : null;
353        mMatrix = new Matrix(builder.mMatrix);
354    }
355
356    /**
357     * Returns the index where the selection starts.
358     * @return -1 if there is no selection.
359     */
360    public int getSelectionStart() {
361        return mSelectionStart;
362    }
363
364    /**
365     * Returns the index where the selection ends.
366     * @return -1 if there is no selection.
367     */
368    public int getSelectionEnd() {
369        return mSelectionEnd;
370    }
371
372    /**
373     * Returns the index where the composing text starts.
374     * @return -1 if there is no composing text.
375     */
376    public int getComposingTextStart() {
377        return mComposingTextStart;
378    }
379
380    /**
381     * Returns the entire composing text.
382     * @return null if there is no composition.
383     */
384    public CharSequence getComposingText() {
385        return mComposingText;
386    }
387
388    /**
389     * Returns the horizontal start of the insertion marker, in the local coordinates that will
390     * be transformed with {@link #getMatrix()} when rendered on the screen.
391     * @return x coordinate that is compatible with {@link Layout#getPrimaryHorizontal(int)}.
392     * Pay special care to RTL/LTR handling.
393     * {@code java.lang.Float.NaN} if not specified.
394     * @see Layout#getPrimaryHorizontal(int)
395     */
396    public float getInsertionMarkerHorizontal() {
397        return mInsertionMarkerHorizontal;
398    }
399    /**
400     * Returns the vertical top position of the insertion marker, in the local coordinates that
401     * will be transformed with {@link #getMatrix()} when rendered on the screen.
402     * @return y coordinate that is compatible with {@link Layout#getLineTop(int)}.
403     * {@code java.lang.Float.NaN} if not specified.
404     */
405    public float getInsertionMarkerTop() {
406        return mInsertionMarkerTop;
407    }
408    /**
409     * Returns the vertical baseline position of the insertion marker, in the local coordinates
410     * that will be transformed with {@link #getMatrix()} when rendered on the screen.
411     * @return y coordinate that is compatible with {@link Layout#getLineBaseline(int)}.
412     * {@code java.lang.Float.NaN} if not specified.
413     */
414    public float getInsertionMarkerBaseline() {
415        return mInsertionMarkerBaseline;
416    }
417    /**
418     * Returns the vertical bottom position of the insertion marker, in the local coordinates
419     * that will be transformed with {@link #getMatrix()} when rendered on the screen.
420     * @return y coordinate that is compatible with {@link Layout#getLineBottom(int)}.
421     * {@code java.lang.Float.NaN} if not specified.
422     */
423    public float getInsertionMarkerBottom() {
424        return mInsertionMarkerBottom;
425    }
426
427    /**
428     * Returns a new instance of {@link RectF} that indicates the location of the character
429     * specified with the index.
430     * <p>
431     * Note that coordinates are not necessarily contiguous or even monotonous, especially when
432     * RTL text and LTR text are mixed.
433     * </p>
434     * @param index index of the character in a Java chars.
435     * @return a new instance of {@link RectF} that represents the location of the character in
436     * local coordinates. null if the character is invisible or the application did not provide
437     * the location. Note that the {@code left} field can be greater than the {@code right} field
438     * if the character is in RTL text.
439     */
440    // TODO: Prepare a document about the expected behavior for surrogate pairs, combining
441    // characters, and non-graphical chars.
442    public RectF getCharacterRect(final int index) {
443        if (mCharacterRects == null) {
444            return null;
445        }
446        return mCharacterRects.get(index);
447    }
448
449    /**
450     * Returns a new instance of {@link android.graphics.Matrix} that indicates the transformation
451     * matrix that is to be applied other positional data in this class.
452     * @return a new instance (copy) of the transformation matrix.
453     */
454    public Matrix getMatrix() {
455        return new Matrix(mMatrix);
456    }
457
458    /**
459     * Used to make this class parcelable.
460     */
461    public static final Parcelable.Creator<CursorAnchorInfo> CREATOR
462            = new Parcelable.Creator<CursorAnchorInfo>() {
463        @Override
464        public CursorAnchorInfo createFromParcel(Parcel source) {
465            return new CursorAnchorInfo(source);
466        }
467
468        @Override
469        public CursorAnchorInfo[] newArray(int size) {
470            return new CursorAnchorInfo[size];
471        }
472    };
473
474    @Override
475    public int describeContents() {
476        return 0;
477    }
478}
479