diff options
Diffstat (limited to 'java/src')
-rw-r--r-- | java/src/com/android/inputmethod/keyboard/TextDecorator.java | 23 | ||||
-rw-r--r-- | java/src/com/android/inputmethod/latin/DictionaryFacilitator.java | 146 | ||||
-rw-r--r-- | java/src/com/android/inputmethod/latin/LatinIME.java | 59 | ||||
-rw-r--r-- | java/src/com/android/inputmethod/latin/PersonalizationHelperForDictionaryFacilitator.java (renamed from java/src/com/android/inputmethod/latin/PersonalizationDictionaryFacilitator.java) | 6 | ||||
-rw-r--r-- | java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java | 236 |
5 files changed, 390 insertions, 80 deletions
diff --git a/java/src/com/android/inputmethod/keyboard/TextDecorator.java b/java/src/com/android/inputmethod/keyboard/TextDecorator.java index cf58d6a09..f614b2257 100644 --- a/java/src/com/android/inputmethod/keyboard/TextDecorator.java +++ b/java/src/com/android/inputmethod/keyboard/TextDecorator.java @@ -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; @@ -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(); + } } /** @@ -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(); @@ -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."); @@ -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(); diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java index 46428839f..c20546607 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java @@ -67,7 +67,7 @@ public class DictionaryFacilitator { // To synchronize assigning mDictionaryGroup to ensure closing dictionaries. private final Object mLock = new Object(); private final DistracterFilter mDistracterFilter; - private final PersonalizationDictionaryFacilitator mPersonalizationDictionaryFacilitator; + private final PersonalizationHelperForDictionaryFacilitator mPersonalizationHelper; private static final String[] DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS = new String[] { @@ -175,22 +175,22 @@ public class DictionaryFacilitator { public DictionaryFacilitator() { mDistracterFilter = DistracterFilter.EMPTY_DISTRACTER_FILTER; - mPersonalizationDictionaryFacilitator = null; + mPersonalizationHelper = null; } public DictionaryFacilitator(final Context context) { mDistracterFilter = new DistracterFilterCheckingExactMatchesAndSuggestions(context); - mPersonalizationDictionaryFacilitator = - new PersonalizationDictionaryFacilitator(context, mDistracterFilter); + mPersonalizationHelper = + new PersonalizationHelperForDictionaryFacilitator(context, mDistracterFilter); } public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) { mDistracterFilter.updateEnabledSubtypes(enabledSubtypes); - mPersonalizationDictionaryFacilitator.updateEnabledSubtypes(enabledSubtypes); + mPersonalizationHelper.updateEnabledSubtypes(enabledSubtypes); } public void setIsMonolingualUser(final boolean isMonolingualUser) { - mPersonalizationDictionaryFacilitator.setIsMonolingualUser(isMonolingualUser); + mPersonalizationHelper.setIsMonolingualUser(isMonolingualUser); } public Locale getLocale() { @@ -226,93 +226,125 @@ public class DictionaryFacilitator { usePersonalizedDicts, forceReloadMainDictionary, listener, "" /* dictNamePrefix */); } - public void resetDictionariesWithDictNamePrefix(final Context context, final Locale newLocale, + public void resetDictionariesWithDictNamePrefix(final Context context, + final Locale newLocaleToUse, final boolean useContactsDict, final boolean usePersonalizedDicts, final boolean forceReloadMainDictionary, final DictionaryInitializationListener listener, final String dictNamePrefix) { - final boolean localeHasBeenChanged = !newLocale.equals(mDictionaryGroup.mLocale); - // We always try to have the main dictionary. Other dictionaries can be unused. - final boolean reloadMainDictionary = localeHasBeenChanged || forceReloadMainDictionary; + final HashMap<Locale, ArrayList<String>> existingDictsToCleanup = new HashMap<>(); + // TODO: use several locales + final Locale[] newLocales = new Locale[] { newLocaleToUse }; // TODO: Make subDictTypesToUse configurable by resource or a static final list. final HashSet<String> subDictTypesToUse = new HashSet<>(); + subDictTypesToUse.add(Dictionary.TYPE_USER); if (useContactsDict) { subDictTypesToUse.add(Dictionary.TYPE_CONTACTS); } - subDictTypesToUse.add(Dictionary.TYPE_USER); if (usePersonalizedDicts) { subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY); subDictTypesToUse.add(Dictionary.TYPE_PERSONALIZATION); subDictTypesToUse.add(Dictionary.TYPE_CONTEXTUAL); } - final Dictionary newMainDict; - if (reloadMainDictionary) { - // The main dictionary will be asynchronously loaded. - newMainDict = null; - } else { - newMainDict = mDictionaryGroup.getDict(Dictionary.TYPE_MAIN); - } - - final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); - for (final String dictType : SUB_DICT_TYPES) { - if (!subDictTypesToUse.contains(dictType)) { - // This dictionary will not be used. + // Gather all dictionaries. We'll remove them from the list to clean up later. + for (final Locale newLocale : newLocales) { + final ArrayList<String> dictsForLocale = new ArrayList<>(); + existingDictsToCleanup.put(newLocale, dictsForLocale); + final DictionaryGroup currentDictionaryGroupForLocale = + newLocale.equals(mDictionaryGroup.mLocale) ? mDictionaryGroup : null; + if (null == currentDictionaryGroupForLocale) { continue; } - final ExpandableBinaryDictionary dict; - if (!localeHasBeenChanged && mDictionaryGroup.hasDict(dictType)) { - // Continue to use current dictionary. - dict = mDictionaryGroup.getSubDict(dictType); + for (final String dictType : SUB_DICT_TYPES) { + if (currentDictionaryGroupForLocale.hasDict(dictType)) { + dictsForLocale.add(dictType); + } + } + if (currentDictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN)) { + dictsForLocale.add(Dictionary.TYPE_MAIN); + } + } + + final HashMap<Locale, DictionaryGroup> newDictionaryGroups = new HashMap<>(); + for (final Locale newLocale : newLocales) { + final DictionaryGroup dictionaryGroupForLocale = + newLocale.equals(mDictionaryGroup.mLocale) ? mDictionaryGroup : null; + final ArrayList<String> dictsToCleanupForLocale = existingDictsToCleanup.get(newLocale); + final boolean noExistingDictsForThisLocale = (null == dictionaryGroupForLocale); + + final Dictionary mainDict; + if (forceReloadMainDictionary || noExistingDictsForThisLocale + || !dictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN)) { + mainDict = null; } else { - // Start to use new dictionary. - dict = getSubDict(dictType, context, newLocale, null /* dictFile */, - dictNamePrefix); + mainDict = dictionaryGroupForLocale.getDict(Dictionary.TYPE_MAIN); + dictsToCleanupForLocale.remove(Dictionary.TYPE_MAIN); + } + + final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); + for (final String subDictType : subDictTypesToUse) { + final ExpandableBinaryDictionary subDict; + if (noExistingDictsForThisLocale + || !dictionaryGroupForLocale.hasDict(subDictType)) { + // Create a new dictionary. + subDict = getSubDict(subDictType, context, newLocale, null /* dictFile */, + dictNamePrefix); + } else { + // Reuse the existing dictionary, and don't close it at the end + subDict = dictionaryGroupForLocale.getSubDict(subDictType); + dictsToCleanupForLocale.remove(subDictType); + } + subDicts.put(subDictType, subDict); } - subDicts.put(dictType, dict); + newDictionaryGroups.put(newLocale, new DictionaryGroup(newLocale, mainDict, subDicts)); } - // Replace DictionaryGroup. - final DictionaryGroup newDictionaryGroup = new DictionaryGroup(newLocale, newMainDict, subDicts); + // Replace Dictionaries. + // TODO: use multiple locales. + final DictionaryGroup newDictionaryGroup = newDictionaryGroups.get(newLocaleToUse); final DictionaryGroup oldDictionaryGroup; synchronized (mLock) { oldDictionaryGroup = mDictionaryGroup; mDictionaryGroup = newDictionaryGroup; mIsUserDictEnabled = UserBinaryDictionary.isEnabled(context); - if (reloadMainDictionary) { - asyncReloadMainDictionary(context, newLocale, listener); + if (null == newDictionaryGroup.getDict(Dictionary.TYPE_MAIN)) { + asyncReloadUninitializedMainDictionaries(context, newLocales, listener); } } if (listener != null) { listener.onUpdateMainDictionaryAvailability(hasInitializedMainDictionary()); } + // Clean up old dictionaries. - if (reloadMainDictionary) { - oldDictionaryGroup.closeDict(Dictionary.TYPE_MAIN); - } - for (final String dictType : SUB_DICT_TYPES) { - if (localeHasBeenChanged || !subDictTypesToUse.contains(dictType)) { - oldDictionaryGroup.closeDict(dictType); + for (final Locale localeToCleanUp : existingDictsToCleanup.keySet()) { + final ArrayList<String> dictTypesToCleanUp = + existingDictsToCleanup.get(localeToCleanUp); + final DictionaryGroup dictionarySetToCleanup = oldDictionaryGroup; + for (final String dictType : dictTypesToCleanUp) { + dictionarySetToCleanup.closeDict(dictType); } } - oldDictionaryGroup.mSubDictMap.clear(); } - private void asyncReloadMainDictionary(final Context context, final Locale locale, - final DictionaryInitializationListener listener) { + private void asyncReloadUninitializedMainDictionaries(final Context context, + final Locale[] locales, final DictionaryInitializationListener listener) { final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1); mLatchForWaitingLoadingMainDictionary = latchForWaitingLoadingMainDictionary; ExecutorUtils.getExecutor("InitializeBinaryDictionary").execute(new Runnable() { @Override public void run() { - final Dictionary mainDict = - DictionaryFactory.createMainDictionaryFromManager(context, locale); - synchronized (mLock) { - if (locale.equals(mDictionaryGroup.mLocale)) { - mDictionaryGroup.setMainDict(mainDict); - } else { - // Dictionary facilitator has been reset for another locale. - mainDict.close(); + for (final Locale locale : locales) { + final DictionaryGroup dictionaryGroup = mDictionaryGroup; + final Dictionary mainDict = + DictionaryFactory.createMainDictionaryFromManager(context, locale); + synchronized (mLock) { + if (locale.equals(dictionaryGroup.mLocale)) { + dictionaryGroup.setMainDict(mainDict); + } else { + // Dictionary facilitator has been reset for another locale. + mainDict.close(); + } } } if (listener != null) { @@ -362,8 +394,8 @@ public class DictionaryFacilitator { dictionaryGroup.closeDict(dictType); } mDistracterFilter.close(); - if (mPersonalizationDictionaryFacilitator != null) { - mPersonalizationDictionaryFacilitator.close(); + if (mPersonalizationHelper != null) { + mPersonalizationHelper.close(); } } @@ -386,7 +418,7 @@ public class DictionaryFacilitator { public void flushPersonalizationDictionary() { final ExpandableBinaryDictionary personalizationDictUsedForSuggestion = mDictionaryGroup.getSubDict(Dictionary.TYPE_PERSONALIZATION); - mPersonalizationDictionaryFacilitator.flushPersonalizationDictionariesToUpdate( + mPersonalizationHelper.flushPersonalizationDictionariesToUpdate( personalizationDictUsedForSuggestion); mDistracterFilter.close(); } @@ -592,7 +624,7 @@ public class DictionaryFacilitator { // personalization dictionary. public void clearPersonalizationDictionary() { clearSubDictionary(Dictionary.TYPE_PERSONALIZATION); - mPersonalizationDictionaryFacilitator.clearDictionariesToUpdate(); + mPersonalizationHelper.clearDictionariesToUpdate(); } public void clearContextualDictionary() { @@ -603,7 +635,7 @@ public class DictionaryFacilitator { final PersonalizationDataChunk personalizationDataChunk, final SpacingAndPunctuations spacingAndPunctuations, final AddMultipleDictionaryEntriesCallback callback) { - mPersonalizationDictionaryFacilitator.addEntriesToPersonalizationDictionariesToUpdate( + mPersonalizationHelper.addEntriesToPersonalizationDictionariesToUpdate( getLocale(), personalizationDataChunk, spacingAndPunctuations, callback); } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index d2104997c..aac6276bc 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -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; @@ -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; @@ -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; @@ -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; @@ -740,6 +744,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public View onCreateInputView() { + StatsUtils.onCreateInputView(); return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled); } @@ -755,6 +760,49 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } @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. return; @@ -772,6 +820,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onFinishInputView(final boolean finishingInput) { + StatsUtils.onFinishInputView(); mHandler.onFinishInputView(finishingInput); } @@ -849,6 +898,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo); final boolean isDifferentTextField = !restarting || inputTypeChanged; + + StatsUtils.onStartInputView(editorInfo.inputType, + Settings.getInstance().getCurrent().mDisplayOrientation, + !isDifferentTextField); + if (isDifferentTextField) { mSubtypeSwitcher.updateParametersOnStartInputView(); } @@ -1016,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)); } /** diff --git a/java/src/com/android/inputmethod/latin/PersonalizationDictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/PersonalizationHelperForDictionaryFacilitator.java index aa8e312a4..43cebdfa4 100644 --- a/java/src/com/android/inputmethod/latin/PersonalizationDictionaryFacilitator.java +++ b/java/src/com/android/inputmethod/latin/PersonalizationHelperForDictionaryFacilitator.java @@ -38,15 +38,15 @@ import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; /** * Class for managing and updating personalization dictionaries. */ -public class PersonalizationDictionaryFacilitator { +public class PersonalizationHelperForDictionaryFacilitator { private final Context mContext; private final DistracterFilter mDistracterFilter; private final HashMap<String, HashSet<Locale>> mLangToLocalesMap = new HashMap<>(); private final HashMap<Locale, ExpandableBinaryDictionary> mPersonalizationDictsToUpdate = new HashMap<>(); - private boolean mIsMonolingualUser = false;; + private boolean mIsMonolingualUser = false; - PersonalizationDictionaryFacilitator(final Context context, + PersonalizationHelperForDictionaryFacilitator(final Context context, final DistracterFilter distracterFilter) { mContext = context; mDistracterFilter = distracterFilter; diff --git a/java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java new file mode 100644 index 000000000..9dc0524a2 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java @@ -0,0 +1,236 @@ +/* + * 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(); + } +} |