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