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