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