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 { 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