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.annotation.TargetApi;
     20 import android.graphics.Matrix;
     21 import android.graphics.Rect;
     22 import android.inputmethodservice.ExtractEditText;
     23 import android.inputmethodservice.InputMethodService;
     24 import android.os.Build;
     25 import android.text.Layout;
     26 import android.text.Spannable;
     27 import android.text.Spanned;
     28 import android.view.View;
     29 import android.view.ViewParent;
     30 import android.view.inputmethod.CursorAnchorInfo;
     31 import android.widget.TextView;
     32 
     33 import com.android.inputmethod.compat.BuildCompatUtils;
     34 import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper;
     35 
     36 import javax.annotation.Nonnull;
     37 import 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  */
     46 public 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