CursorAnchorInfoUtils.java revision cd11905022306c9b95f8781f0f8b23a3570f30e9
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.latin.utils;
18
19import android.graphics.Matrix;
20import android.graphics.Rect;
21import android.inputmethodservice.ExtractEditText;
22import android.inputmethodservice.InputMethodService;
23import android.text.Layout;
24import android.text.Spannable;
25import android.view.View;
26import android.view.ViewParent;
27import android.view.inputmethod.CursorAnchorInfo;
28import android.widget.TextView;
29
30/**
31 * This class allows input methods to extract {@link CursorAnchorInfo} directly from the given
32 * {@link TextView}. This is useful and even necessary to support full-screen mode where the default
33 * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} event callback must be
34 * ignored because it reports the character locations of the target application rather than
35 * characters on {@link ExtractEditText}.
36 */
37public final class CursorAnchorInfoUtils {
38    private CursorAnchorInfoUtils() {
39        // This helper class is not instantiable.
40    }
41
42    private static boolean isPositionVisible(final View view, final float positionX,
43            final float positionY) {
44        final float[] position = new float[] { positionX, positionY };
45        View currentView = view;
46
47        while (currentView != null) {
48            if (currentView != view) {
49                // Local scroll is already taken into account in positionX/Y
50                position[0] -= currentView.getScrollX();
51                position[1] -= currentView.getScrollY();
52            }
53
54            if (position[0] < 0 || position[1] < 0 ||
55                    position[0] > currentView.getWidth() || position[1] > currentView.getHeight()) {
56                return false;
57            }
58
59            if (!currentView.getMatrix().isIdentity()) {
60                currentView.getMatrix().mapPoints(position);
61            }
62
63            position[0] += currentView.getLeft();
64            position[1] += currentView.getTop();
65
66            final ViewParent parent = currentView.getParent();
67            if (parent instanceof View) {
68                currentView = (View) parent;
69            } else {
70                // We've reached the ViewRoot, stop iterating
71                currentView = null;
72            }
73        }
74
75        // We've been able to walk up the view hierarchy and the position was never clipped
76        return true;
77    }
78
79    /**
80     * Returns {@link CursorAnchorInfo} from the given {@link TextView}.
81     * @param textView the target text view from which {@link CursorAnchorInfo} is to be extracted.
82     * @return the {@link CursorAnchorInfo} object based on the current layout. {@code null} if it
83     * is not feasible.
84     */
85    public static CursorAnchorInfo getCursorAnchorInfo(final TextView textView) {
86        Layout layout = textView.getLayout();
87        if (layout == null) {
88            return null;
89        }
90
91        final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
92
93        final int selectionStart = textView.getSelectionStart();
94        builder.setSelectionRange(selectionStart, textView.getSelectionEnd());
95
96        // Construct transformation matrix from view local coordinates to screen coordinates.
97        final Matrix viewToScreenMatrix = new Matrix(textView.getMatrix());
98        final int[] viewOriginInScreen = new int[2];
99        textView.getLocationOnScreen(viewOriginInScreen);
100        viewToScreenMatrix.postTranslate(viewOriginInScreen[0], viewOriginInScreen[1]);
101        builder.setMatrix(viewToScreenMatrix);
102
103        if (layout.getLineCount() == 0) {
104            return null;
105        }
106        final Rect lineBoundsWithoutOffset = new Rect();
107        final Rect lineBoundsWithOffset = new Rect();
108        layout.getLineBounds(0, lineBoundsWithoutOffset);
109        textView.getLineBounds(0, lineBoundsWithOffset);
110        final float viewportToContentHorizontalOffset = lineBoundsWithOffset.left
111                - lineBoundsWithoutOffset.left - textView.getScrollX();
112        final float viewportToContentVerticalOffset = lineBoundsWithOffset.top
113                - lineBoundsWithoutOffset.top - textView.getScrollY();
114
115        final CharSequence text = textView.getText();
116        if (text instanceof Spannable) {
117            // Here we assume that the composing text is marked as SPAN_COMPOSING flag. This is not
118            // necessarily true, but basically works.
119            int composingTextStart = text.length();
120            int composingTextEnd = 0;
121            final Spannable spannable = (Spannable) text;
122            final Object[] spans = spannable.getSpans(0, text.length(), Object.class);
123            for (Object span : spans) {
124                final int spanFlag = spannable.getSpanFlags(span);
125                if ((spanFlag & Spannable.SPAN_COMPOSING) != 0) {
126                    composingTextStart = Math.min(composingTextStart,
127                            spannable.getSpanStart(span));
128                    composingTextEnd = Math.max(composingTextEnd, spannable.getSpanEnd(span));
129                }
130            }
131
132            final boolean hasComposingText =
133                    (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
134            if (hasComposingText) {
135                final CharSequence composingText = text.subSequence(composingTextStart,
136                        composingTextEnd);
137                builder.setComposingText(composingTextStart, composingText);
138
139                final int minLine = layout.getLineForOffset(composingTextStart);
140                final int maxLine = layout.getLineForOffset(composingTextEnd - 1);
141                for (int line = minLine; line <= maxLine; ++line) {
142                    final int lineStart = layout.getLineStart(line);
143                    final int lineEnd = layout.getLineEnd(line);
144                    final int offsetStart = Math.max(lineStart, composingTextStart);
145                    final int offsetEnd = Math.min(lineEnd, composingTextEnd);
146                    final boolean ltrLine =
147                            layout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT;
148                    final float[] widths = new float[offsetEnd - offsetStart];
149                    layout.getPaint().getTextWidths(text, offsetStart, offsetEnd, widths);
150                    final float top = layout.getLineTop(line);
151                    final float bottom = layout.getLineBottom(line);
152                    for (int offset = offsetStart; offset < offsetEnd; ++offset) {
153                        final float charWidth = widths[offset - offsetStart];
154                        final boolean isRtl = layout.isRtlCharAt(offset);
155                        final float primary = layout.getPrimaryHorizontal(offset);
156                        final float secondary = layout.getSecondaryHorizontal(offset);
157                        // TODO: This doesn't work perfectly for text with custom styles and TAB
158                        // chars.
159                        final float left;
160                        final float right;
161                        if (ltrLine) {
162                            if (isRtl) {
163                                left = secondary - charWidth;
164                                right = secondary;
165                            } else {
166                                left = primary;
167                                right = primary + charWidth;
168                            }
169                        } else {
170                            if (!isRtl) {
171                                left = secondary;
172                                right = secondary + charWidth;
173                            } else {
174                                left = primary - charWidth;
175                                right = primary;
176                            }
177                        }
178                        // TODO: Check top-right and bottom-left as well.
179                        final float localLeft = left + viewportToContentHorizontalOffset;
180                        final float localRight = right + viewportToContentHorizontalOffset;
181                        final float localTop = top + viewportToContentVerticalOffset;
182                        final float localBottom = bottom + viewportToContentVerticalOffset;
183                        final boolean isTopLeftVisible = isPositionVisible(textView,
184                                localLeft, localTop);
185                        final boolean isBottomRightVisible =
186                                isPositionVisible(textView, localRight, localBottom);
187                        int characterBoundsFlags = 0;
188                        if (isTopLeftVisible || isBottomRightVisible) {
189                            characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
190                        }
191                        if (!isTopLeftVisible || !isTopLeftVisible) {
192                            characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
193                        }
194                        if (isRtl) {
195                            characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL;
196                        }
197                        // Here offset is the index in Java chars.
198                        builder.addCharacterBounds(offset, localLeft, localTop, localRight,
199                                localBottom, characterBoundsFlags);
200                    }
201                }
202            }
203        }
204
205        // Treat selectionStart as the insertion point.
206        if (0 <= selectionStart) {
207            final int offset = selectionStart;
208            final int line = layout.getLineForOffset(offset);
209            final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
210                    + viewportToContentHorizontalOffset;
211            final float insertionMarkerTop = layout.getLineTop(line)
212                    + viewportToContentVerticalOffset;
213            final float insertionMarkerBaseline = layout.getLineBaseline(line)
214                    + viewportToContentVerticalOffset;
215            final float insertionMarkerBottom = layout.getLineBottom(line)
216                    + viewportToContentVerticalOffset;
217            final boolean isTopVisible =
218                    isPositionVisible(textView, insertionMarkerX, insertionMarkerTop);
219            final boolean isBottomVisible =
220                    isPositionVisible(textView, insertionMarkerX, insertionMarkerBottom);
221            int insertionMarkerFlags = 0;
222            if (isTopVisible || isBottomVisible) {
223                insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
224            }
225            if (!isTopVisible || !isBottomVisible) {
226                insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
227            }
228            if (layout.isRtlCharAt(offset)) {
229                insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
230            }
231            builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
232                    insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
233        }
234        return builder.build();
235    }
236}
237