• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.inputmethod.latin.utils;
18 
19 import android.graphics.Matrix;
20 import android.graphics.Rect;
21 import android.inputmethodservice.ExtractEditText;
22 import android.inputmethodservice.InputMethodService;
23 import android.text.Layout;
24 import android.text.Spannable;
25 import android.view.View;
26 import android.view.ViewParent;
27 import android.view.inputmethod.CursorAnchorInfo;
28 import 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  */
37 public final class CursorAnchorInfoUtils {
CursorAnchorInfoUtils()38     private CursorAnchorInfoUtils() {
39         // This helper class is not instantiable.
40     }
41 
isPositionVisible(final View view, final float positionX, final float positionY)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      */
getCursorAnchorInfo(final TextView textView)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