Home | History | Annotate | Download | only in utils
      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