Loading java/src/com/android/inputmethod/keyboard/TextDecorator.java +5 −18 Original line number Diff line number Diff line Loading @@ -24,7 +24,6 @@ import android.os.Message; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.inputmethod.CursorAnchorInfo; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper; Loading Loading @@ -154,13 +153,11 @@ public class TextDecorator { * {@code false} is the input method is finishing the full screen mode. */ public void notifyFullScreenMode(final boolean fullScreenMode) { final boolean currentFullScreenMode = mIsFullScreenMode; if (!currentFullScreenMode && fullScreenMode) { // Currently full screen mode is not supported. // TODO: Support full screen mode. mUiOperator.hideUi(); } final boolean fullScreenModeChanged = (mIsFullScreenMode != fullScreenMode); mIsFullScreenMode = fullScreenMode; if (fullScreenModeChanged) { layoutLater(); } } /** Loading @@ -183,11 +180,6 @@ public class TextDecorator { * @param info the compatibility wrapper object for the received {@link CursorAnchorInfo}. */ public void onUpdateCursorAnchorInfo(final CursorAnchorInfoCompatWrapper info) { if (mIsFullScreenMode) { // TODO: Consider to call InputConnection#requestCursorAnchorInfo to disable the // event callback to suppress unnecessary event callbacks. return; } mCursorAnchorInfoWrapper = info; // Do not use layoutLater() to minimize the latency. layoutImmediately(); Loading Loading @@ -240,11 +232,6 @@ public class TextDecorator { } private void layoutMain() { if (mIsFullScreenMode) { cancelLayoutInternalUnexpectedly("Full screen mode isn't yet supported."); return; } if (mMode != MODE_COMMIT && mMode != MODE_ADD_TO_DICTIONARY) { if (mMode == MODE_NONE) { cancelLayoutInternalExpectedly("Not ready for layouting."); Loading Loading @@ -328,7 +315,7 @@ public class TextDecorator { return; } } else { if (!TextUtils.isEmpty(composingText)) { if (!mIsFullScreenMode && !TextUtils.isEmpty(composingText)) { // This is an unexpected case. // TODO: Document this. mUiOperator.hideUi(); Loading java/src/com/android/inputmethod/latin/LatinIME.java +50 −2 Original line number Diff line number Diff line Loading @@ -46,6 +46,7 @@ import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; Loading @@ -53,6 +54,7 @@ import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethod; import android.view.inputmethod.InputMethodSubtype; import android.widget.TextView; import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.annotations.UsedForTesting; Loading Loading @@ -85,6 +87,7 @@ import com.android.inputmethod.latin.suggestions.SuggestionStripView; import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor; import com.android.inputmethod.latin.utils.ApplicationUtils; import com.android.inputmethod.latin.utils.CapsModeUtils; import com.android.inputmethod.latin.utils.CursorAnchorInfoUtils; import com.android.inputmethod.latin.utils.CoordinateUtils; import com.android.inputmethod.latin.utils.DialogUtils; import com.android.inputmethod.latin.utils.DistracterFilterCheckingExactMatchesAndSuggestions; Loading Loading @@ -152,6 +155,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // TODO: Move these {@link View}s to {@link KeyboardSwitcher}. private View mInputView; private SuggestionStripView mSuggestionStripView; private TextView mExtractEditText; private RichInputMethodManager mRichImm; @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher; Loading Loading @@ -755,6 +759,49 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mInputLogic.setTextDecoratorUi(new TextDecoratorUi(this, view)); } @Override public void setExtractView(final View view) { final TextView prevExtractEditText = mExtractEditText; super.setExtractView(view); TextView nextExtractEditText = null; if (view != null) { final View extractEditText = view.findViewById(android.R.id.inputExtractEditText); if (extractEditText instanceof TextView) { nextExtractEditText = (TextView)extractEditText; } } if (prevExtractEditText == nextExtractEditText) { return; } if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK && prevExtractEditText != null) { prevExtractEditText.getViewTreeObserver().removeOnPreDrawListener( mExtractTextViewPreDrawListener); } mExtractEditText = nextExtractEditText; if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK && mExtractEditText != null) { mExtractEditText.getViewTreeObserver().addOnPreDrawListener( mExtractTextViewPreDrawListener); } } private final ViewTreeObserver.OnPreDrawListener mExtractTextViewPreDrawListener = new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { onExtractTextViewPreDraw(); return true; } }; private void onExtractTextViewPreDraw() { if (!ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK || !isFullscreenMode() || mExtractEditText == null) { return; } final CursorAnchorInfo info = CursorAnchorInfoUtils.getCursorAnchorInfo(mExtractEditText); mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.fromObject(info)); } @Override public void setCandidatesView(final View view) { // To ensure that CandidatesView will never be set. Loading Loading @@ -1023,9 +1070,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // We cannot mark this method as @Override until new SDK becomes publicly available. // @Override public void onUpdateCursorAnchorInfo(final CursorAnchorInfo info) { if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK) { mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.fromObject(info)); if (!ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK || isFullscreenMode()) { return; } mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.fromObject(info)); } /** Loading java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java 0 → 100644 +236 −0 Original line number Diff line number Diff line /* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.inputmethod.latin.utils; import android.graphics.Matrix; import android.graphics.Rect; import android.inputmethodservice.ExtractEditText; import android.inputmethodservice.InputMethodService; import android.text.Layout; import android.text.Spannable; import android.view.View; import android.view.ViewParent; import android.view.inputmethod.CursorAnchorInfo; import android.widget.TextView; /** * This class allows input methods to extract {@link CursorAnchorInfo} directly from the given * {@link TextView}. This is useful and even necessary to support full-screen mode where the default * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} event callback must be * ignored because it reports the character locations of the target application rather than * characters on {@link ExtractEditText}. */ public final class CursorAnchorInfoUtils { private CursorAnchorInfoUtils() { // This helper class is not instantiable. } private static boolean isPositionVisible(final View view, final float positionX, final float positionY) { final float[] position = new float[] { positionX, positionY }; View currentView = view; while (currentView != null) { if (currentView != view) { // Local scroll is already taken into account in positionX/Y position[0] -= currentView.getScrollX(); position[1] -= currentView.getScrollY(); } if (position[0] < 0 || position[1] < 0 || position[0] > currentView.getWidth() || position[1] > currentView.getHeight()) { return false; } if (!currentView.getMatrix().isIdentity()) { currentView.getMatrix().mapPoints(position); } position[0] += currentView.getLeft(); position[1] += currentView.getTop(); final ViewParent parent = currentView.getParent(); if (parent instanceof View) { currentView = (View) parent; } else { // We've reached the ViewRoot, stop iterating currentView = null; } } // We've been able to walk up the view hierarchy and the position was never clipped return true; } /** * Returns {@link CursorAnchorInfo} from the given {@link TextView}. * @param textView the target text view from which {@link CursorAnchorInfo} is to be extracted. * @return the {@link CursorAnchorInfo} object based on the current layout. {@code null} if it * is not feasible. */ public static CursorAnchorInfo getCursorAnchorInfo(final TextView textView) { Layout layout = textView.getLayout(); if (layout == null) { return null; } final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder(); final int selectionStart = textView.getSelectionStart(); builder.setSelectionRange(selectionStart, textView.getSelectionEnd()); // Construct transformation matrix from view local coordinates to screen coordinates. final Matrix viewToScreenMatrix = new Matrix(textView.getMatrix()); final int[] viewOriginInScreen = new int[2]; textView.getLocationOnScreen(viewOriginInScreen); viewToScreenMatrix.postTranslate(viewOriginInScreen[0], viewOriginInScreen[1]); builder.setMatrix(viewToScreenMatrix); if (layout.getLineCount() == 0) { return null; } final Rect lineBoundsWithoutOffset = new Rect(); final Rect lineBoundsWithOffset = new Rect(); layout.getLineBounds(0, lineBoundsWithoutOffset); textView.getLineBounds(0, lineBoundsWithOffset); final float viewportToContentHorizontalOffset = lineBoundsWithOffset.left - lineBoundsWithoutOffset.left - textView.getScrollX(); final float viewportToContentVerticalOffset = lineBoundsWithOffset.top - lineBoundsWithoutOffset.top - textView.getScrollY(); final CharSequence text = textView.getText(); if (text instanceof Spannable) { // Here we assume that the composing text is marked as SPAN_COMPOSING flag. This is not // necessarily true, but basically works. int composingTextStart = text.length(); int composingTextEnd = 0; final Spannable spannable = (Spannable) text; final Object[] spans = spannable.getSpans(0, text.length(), Object.class); for (Object span : spans) { final int spanFlag = spannable.getSpanFlags(span); if ((spanFlag & Spannable.SPAN_COMPOSING) != 0) { composingTextStart = Math.min(composingTextStart, spannable.getSpanStart(span)); composingTextEnd = Math.max(composingTextEnd, spannable.getSpanEnd(span)); } } final boolean hasComposingText = (0 <= composingTextStart) && (composingTextStart < composingTextEnd); if (hasComposingText) { final CharSequence composingText = text.subSequence(composingTextStart, composingTextEnd); builder.setComposingText(composingTextStart, composingText); final int minLine = layout.getLineForOffset(composingTextStart); final int maxLine = layout.getLineForOffset(composingTextEnd - 1); for (int line = minLine; line <= maxLine; ++line) { final int lineStart = layout.getLineStart(line); final int lineEnd = layout.getLineEnd(line); final int offsetStart = Math.max(lineStart, composingTextStart); final int offsetEnd = Math.min(lineEnd, composingTextEnd); final boolean ltrLine = layout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT; final float[] widths = new float[offsetEnd - offsetStart]; layout.getPaint().getTextWidths(text, offsetStart, offsetEnd, widths); final float top = layout.getLineTop(line); final float bottom = layout.getLineBottom(line); for (int offset = offsetStart; offset < offsetEnd; ++offset) { final float charWidth = widths[offset - offsetStart]; final boolean isRtl = layout.isRtlCharAt(offset); final float primary = layout.getPrimaryHorizontal(offset); final float secondary = layout.getSecondaryHorizontal(offset); // TODO: This doesn't work perfectly for text with custom styles and TAB // chars. final float left; final float right; if (ltrLine) { if (isRtl) { left = secondary - charWidth; right = secondary; } else { left = primary; right = primary + charWidth; } } else { if (!isRtl) { left = secondary; right = secondary + charWidth; } else { left = primary - charWidth; right = primary; } } // TODO: Check top-right and bottom-left as well. final float localLeft = left + viewportToContentHorizontalOffset; final float localRight = right + viewportToContentHorizontalOffset; final float localTop = top + viewportToContentVerticalOffset; final float localBottom = bottom + viewportToContentVerticalOffset; final boolean isTopLeftVisible = isPositionVisible(textView, localLeft, localTop); final boolean isBottomRightVisible = isPositionVisible(textView, localRight, localBottom); int characterBoundsFlags = 0; if (isTopLeftVisible || isBottomRightVisible) { characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION; } if (!isTopLeftVisible || !isTopLeftVisible) { characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION; } if (isRtl) { characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL; } // Here offset is the index in Java chars. builder.addCharacterBounds(offset, localLeft, localTop, localRight, localBottom, characterBoundsFlags); } } } } // Treat selectionStart as the insertion point. if (0 <= selectionStart) { final int offset = selectionStart; final int line = layout.getLineForOffset(offset); final float insertionMarkerX = layout.getPrimaryHorizontal(offset) + viewportToContentHorizontalOffset; final float insertionMarkerTop = layout.getLineTop(line) + viewportToContentVerticalOffset; final float insertionMarkerBaseline = layout.getLineBaseline(line) + viewportToContentVerticalOffset; final float insertionMarkerBottom = layout.getLineBottom(line) + viewportToContentVerticalOffset; final boolean isTopVisible = isPositionVisible(textView, insertionMarkerX, insertionMarkerTop); final boolean isBottomVisible = isPositionVisible(textView, insertionMarkerX, insertionMarkerBottom); int insertionMarkerFlags = 0; if (isTopVisible || isBottomVisible) { insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION; } if (!isTopVisible || !isBottomVisible) { insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION; } if (layout.isRtlCharAt(offset)) { insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL; } builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop, insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags); } return builder.build(); } } Loading
java/src/com/android/inputmethod/keyboard/TextDecorator.java +5 −18 Original line number Diff line number Diff line Loading @@ -24,7 +24,6 @@ import android.os.Message; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.inputmethod.CursorAnchorInfo; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper; Loading Loading @@ -154,13 +153,11 @@ public class TextDecorator { * {@code false} is the input method is finishing the full screen mode. */ public void notifyFullScreenMode(final boolean fullScreenMode) { final boolean currentFullScreenMode = mIsFullScreenMode; if (!currentFullScreenMode && fullScreenMode) { // Currently full screen mode is not supported. // TODO: Support full screen mode. mUiOperator.hideUi(); } final boolean fullScreenModeChanged = (mIsFullScreenMode != fullScreenMode); mIsFullScreenMode = fullScreenMode; if (fullScreenModeChanged) { layoutLater(); } } /** Loading @@ -183,11 +180,6 @@ public class TextDecorator { * @param info the compatibility wrapper object for the received {@link CursorAnchorInfo}. */ public void onUpdateCursorAnchorInfo(final CursorAnchorInfoCompatWrapper info) { if (mIsFullScreenMode) { // TODO: Consider to call InputConnection#requestCursorAnchorInfo to disable the // event callback to suppress unnecessary event callbacks. return; } mCursorAnchorInfoWrapper = info; // Do not use layoutLater() to minimize the latency. layoutImmediately(); Loading Loading @@ -240,11 +232,6 @@ public class TextDecorator { } private void layoutMain() { if (mIsFullScreenMode) { cancelLayoutInternalUnexpectedly("Full screen mode isn't yet supported."); return; } if (mMode != MODE_COMMIT && mMode != MODE_ADD_TO_DICTIONARY) { if (mMode == MODE_NONE) { cancelLayoutInternalExpectedly("Not ready for layouting."); Loading Loading @@ -328,7 +315,7 @@ public class TextDecorator { return; } } else { if (!TextUtils.isEmpty(composingText)) { if (!mIsFullScreenMode && !TextUtils.isEmpty(composingText)) { // This is an unexpected case. // TODO: Document this. mUiOperator.hideUi(); Loading
java/src/com/android/inputmethod/latin/LatinIME.java +50 −2 Original line number Diff line number Diff line Loading @@ -46,6 +46,7 @@ import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; Loading @@ -53,6 +54,7 @@ import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethod; import android.view.inputmethod.InputMethodSubtype; import android.widget.TextView; import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.annotations.UsedForTesting; Loading Loading @@ -85,6 +87,7 @@ import com.android.inputmethod.latin.suggestions.SuggestionStripView; import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor; import com.android.inputmethod.latin.utils.ApplicationUtils; import com.android.inputmethod.latin.utils.CapsModeUtils; import com.android.inputmethod.latin.utils.CursorAnchorInfoUtils; import com.android.inputmethod.latin.utils.CoordinateUtils; import com.android.inputmethod.latin.utils.DialogUtils; import com.android.inputmethod.latin.utils.DistracterFilterCheckingExactMatchesAndSuggestions; Loading Loading @@ -152,6 +155,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // TODO: Move these {@link View}s to {@link KeyboardSwitcher}. private View mInputView; private SuggestionStripView mSuggestionStripView; private TextView mExtractEditText; private RichInputMethodManager mRichImm; @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher; Loading Loading @@ -755,6 +759,49 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mInputLogic.setTextDecoratorUi(new TextDecoratorUi(this, view)); } @Override public void setExtractView(final View view) { final TextView prevExtractEditText = mExtractEditText; super.setExtractView(view); TextView nextExtractEditText = null; if (view != null) { final View extractEditText = view.findViewById(android.R.id.inputExtractEditText); if (extractEditText instanceof TextView) { nextExtractEditText = (TextView)extractEditText; } } if (prevExtractEditText == nextExtractEditText) { return; } if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK && prevExtractEditText != null) { prevExtractEditText.getViewTreeObserver().removeOnPreDrawListener( mExtractTextViewPreDrawListener); } mExtractEditText = nextExtractEditText; if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK && mExtractEditText != null) { mExtractEditText.getViewTreeObserver().addOnPreDrawListener( mExtractTextViewPreDrawListener); } } private final ViewTreeObserver.OnPreDrawListener mExtractTextViewPreDrawListener = new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { onExtractTextViewPreDraw(); return true; } }; private void onExtractTextViewPreDraw() { if (!ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK || !isFullscreenMode() || mExtractEditText == null) { return; } final CursorAnchorInfo info = CursorAnchorInfoUtils.getCursorAnchorInfo(mExtractEditText); mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.fromObject(info)); } @Override public void setCandidatesView(final View view) { // To ensure that CandidatesView will never be set. Loading Loading @@ -1023,9 +1070,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // We cannot mark this method as @Override until new SDK becomes publicly available. // @Override public void onUpdateCursorAnchorInfo(final CursorAnchorInfo info) { if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK) { mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.fromObject(info)); if (!ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK || isFullscreenMode()) { return; } mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.fromObject(info)); } /** Loading
java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java 0 → 100644 +236 −0 Original line number Diff line number Diff line /* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.inputmethod.latin.utils; import android.graphics.Matrix; import android.graphics.Rect; import android.inputmethodservice.ExtractEditText; import android.inputmethodservice.InputMethodService; import android.text.Layout; import android.text.Spannable; import android.view.View; import android.view.ViewParent; import android.view.inputmethod.CursorAnchorInfo; import android.widget.TextView; /** * This class allows input methods to extract {@link CursorAnchorInfo} directly from the given * {@link TextView}. This is useful and even necessary to support full-screen mode where the default * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} event callback must be * ignored because it reports the character locations of the target application rather than * characters on {@link ExtractEditText}. */ public final class CursorAnchorInfoUtils { private CursorAnchorInfoUtils() { // This helper class is not instantiable. } private static boolean isPositionVisible(final View view, final float positionX, final float positionY) { final float[] position = new float[] { positionX, positionY }; View currentView = view; while (currentView != null) { if (currentView != view) { // Local scroll is already taken into account in positionX/Y position[0] -= currentView.getScrollX(); position[1] -= currentView.getScrollY(); } if (position[0] < 0 || position[1] < 0 || position[0] > currentView.getWidth() || position[1] > currentView.getHeight()) { return false; } if (!currentView.getMatrix().isIdentity()) { currentView.getMatrix().mapPoints(position); } position[0] += currentView.getLeft(); position[1] += currentView.getTop(); final ViewParent parent = currentView.getParent(); if (parent instanceof View) { currentView = (View) parent; } else { // We've reached the ViewRoot, stop iterating currentView = null; } } // We've been able to walk up the view hierarchy and the position was never clipped return true; } /** * Returns {@link CursorAnchorInfo} from the given {@link TextView}. * @param textView the target text view from which {@link CursorAnchorInfo} is to be extracted. * @return the {@link CursorAnchorInfo} object based on the current layout. {@code null} if it * is not feasible. */ public static CursorAnchorInfo getCursorAnchorInfo(final TextView textView) { Layout layout = textView.getLayout(); if (layout == null) { return null; } final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder(); final int selectionStart = textView.getSelectionStart(); builder.setSelectionRange(selectionStart, textView.getSelectionEnd()); // Construct transformation matrix from view local coordinates to screen coordinates. final Matrix viewToScreenMatrix = new Matrix(textView.getMatrix()); final int[] viewOriginInScreen = new int[2]; textView.getLocationOnScreen(viewOriginInScreen); viewToScreenMatrix.postTranslate(viewOriginInScreen[0], viewOriginInScreen[1]); builder.setMatrix(viewToScreenMatrix); if (layout.getLineCount() == 0) { return null; } final Rect lineBoundsWithoutOffset = new Rect(); final Rect lineBoundsWithOffset = new Rect(); layout.getLineBounds(0, lineBoundsWithoutOffset); textView.getLineBounds(0, lineBoundsWithOffset); final float viewportToContentHorizontalOffset = lineBoundsWithOffset.left - lineBoundsWithoutOffset.left - textView.getScrollX(); final float viewportToContentVerticalOffset = lineBoundsWithOffset.top - lineBoundsWithoutOffset.top - textView.getScrollY(); final CharSequence text = textView.getText(); if (text instanceof Spannable) { // Here we assume that the composing text is marked as SPAN_COMPOSING flag. This is not // necessarily true, but basically works. int composingTextStart = text.length(); int composingTextEnd = 0; final Spannable spannable = (Spannable) text; final Object[] spans = spannable.getSpans(0, text.length(), Object.class); for (Object span : spans) { final int spanFlag = spannable.getSpanFlags(span); if ((spanFlag & Spannable.SPAN_COMPOSING) != 0) { composingTextStart = Math.min(composingTextStart, spannable.getSpanStart(span)); composingTextEnd = Math.max(composingTextEnd, spannable.getSpanEnd(span)); } } final boolean hasComposingText = (0 <= composingTextStart) && (composingTextStart < composingTextEnd); if (hasComposingText) { final CharSequence composingText = text.subSequence(composingTextStart, composingTextEnd); builder.setComposingText(composingTextStart, composingText); final int minLine = layout.getLineForOffset(composingTextStart); final int maxLine = layout.getLineForOffset(composingTextEnd - 1); for (int line = minLine; line <= maxLine; ++line) { final int lineStart = layout.getLineStart(line); final int lineEnd = layout.getLineEnd(line); final int offsetStart = Math.max(lineStart, composingTextStart); final int offsetEnd = Math.min(lineEnd, composingTextEnd); final boolean ltrLine = layout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT; final float[] widths = new float[offsetEnd - offsetStart]; layout.getPaint().getTextWidths(text, offsetStart, offsetEnd, widths); final float top = layout.getLineTop(line); final float bottom = layout.getLineBottom(line); for (int offset = offsetStart; offset < offsetEnd; ++offset) { final float charWidth = widths[offset - offsetStart]; final boolean isRtl = layout.isRtlCharAt(offset); final float primary = layout.getPrimaryHorizontal(offset); final float secondary = layout.getSecondaryHorizontal(offset); // TODO: This doesn't work perfectly for text with custom styles and TAB // chars. final float left; final float right; if (ltrLine) { if (isRtl) { left = secondary - charWidth; right = secondary; } else { left = primary; right = primary + charWidth; } } else { if (!isRtl) { left = secondary; right = secondary + charWidth; } else { left = primary - charWidth; right = primary; } } // TODO: Check top-right and bottom-left as well. final float localLeft = left + viewportToContentHorizontalOffset; final float localRight = right + viewportToContentHorizontalOffset; final float localTop = top + viewportToContentVerticalOffset; final float localBottom = bottom + viewportToContentVerticalOffset; final boolean isTopLeftVisible = isPositionVisible(textView, localLeft, localTop); final boolean isBottomRightVisible = isPositionVisible(textView, localRight, localBottom); int characterBoundsFlags = 0; if (isTopLeftVisible || isBottomRightVisible) { characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION; } if (!isTopLeftVisible || !isTopLeftVisible) { characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION; } if (isRtl) { characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL; } // Here offset is the index in Java chars. builder.addCharacterBounds(offset, localLeft, localTop, localRight, localBottom, characterBoundsFlags); } } } } // Treat selectionStart as the insertion point. if (0 <= selectionStart) { final int offset = selectionStart; final int line = layout.getLineForOffset(offset); final float insertionMarkerX = layout.getPrimaryHorizontal(offset) + viewportToContentHorizontalOffset; final float insertionMarkerTop = layout.getLineTop(line) + viewportToContentVerticalOffset; final float insertionMarkerBaseline = layout.getLineBaseline(line) + viewportToContentVerticalOffset; final float insertionMarkerBottom = layout.getLineBottom(line) + viewportToContentVerticalOffset; final boolean isTopVisible = isPositionVisible(textView, insertionMarkerX, insertionMarkerTop); final boolean isBottomVisible = isPositionVisible(textView, insertionMarkerX, insertionMarkerBottom); int insertionMarkerFlags = 0; if (isTopVisible || isBottomVisible) { insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION; } if (!isTopVisible || !isBottomVisible) { insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION; } if (layout.isRtlCharAt(offset)) { insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL; } builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop, insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags); } return builder.build(); } }