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