From c62589cbecef6e748bcc6c6f4ea6a8ff7656923f Mon Sep 17 00:00:00 2001 From: Gilles Debunne Date: Thu, 12 Apr 2012 14:50:23 -0700 Subject: Editor uses a SpanWatcher to track EasyEditSpans Will also fix Bug 6344997 The previous TextWatcher mechanism was inneficient. It require an expensive getSpans() call to retrieve all the spans and then search for the one we're interested in in case it has been changed. The SpanWatcher is faster, it will broadcast the add/changed/removed events we're interested in. Now that we can rely on SpanWatcher, use it to directly track addition and removals of EasyEditSpans. No unit test for this feature which require an integration with the voice IME. Easy to test manually though. Change-Id: Idabcacc48c479bf9868d5204c0b0ca709207ede2 --- .../android/view/inputmethod/ExtractedText.java | 2 +- core/java/android/widget/Editor.java | 187 ++++++++------------- core/java/android/widget/SpellChecker.java | 5 +- core/java/android/widget/TextView.java | 19 +-- 4 files changed, 81 insertions(+), 132 deletions(-) (limited to 'core/java/android') diff --git a/core/java/android/view/inputmethod/ExtractedText.java b/core/java/android/view/inputmethod/ExtractedText.java index 662ba3f..3b2508c 100644 --- a/core/java/android/view/inputmethod/ExtractedText.java +++ b/core/java/android/view/inputmethod/ExtractedText.java @@ -45,7 +45,7 @@ public class ExtractedText implements Parcelable { /** * If the content is a report of a partial text change, this is the offset * where the change ends. Note that the actual text may be larger or - * smaller than the difference between this and {@link #partialEndOffset}, + * smaller than the difference between this and {@link #partialStartOffset}, * meaning a reduction or increase, respectively, in the total text. */ public int partialEndOffset; diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index cbff58c..d16e9da 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -47,7 +47,6 @@ import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.StaticLayout; import android.text.TextUtils; -import android.text.TextWatcher; import android.text.method.KeyListener; import android.text.method.MetaKeyKeyListener; import android.text.method.MovementMethod; @@ -183,8 +182,6 @@ public class Editor { Editor(TextView textView) { mTextView = textView; - mEasyEditSpanController = new EasyEditSpanController(); - mTextView.addTextChangedListener(mEasyEditSpanController); } void onAttachedToWindow() { @@ -1120,7 +1117,7 @@ public class Editor { if (contentChanged || ims.mSelectionModeChanged) { ims.mContentChanged = false; ims.mSelectionModeChanged = false; - final ExtractedTextRequest req = ims.mExtracting; + final ExtractedTextRequest req = ims.mExtractedTextRequest; if (req != null) { InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null) { @@ -1132,13 +1129,14 @@ public class Editor { ims.mChangedStart = EXTRACT_NOTHING; } if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd, - ims.mChangedDelta, ims.mTmpExtracted)) { + ims.mChangedDelta, ims.mExtractedText)) { if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG, "Reporting extracted start=" + - ims.mTmpExtracted.partialStartOffset + - " end=" + ims.mTmpExtracted.partialEndOffset + - ": " + ims.mTmpExtracted.text); - imm.updateExtractedText(mTextView, req.token, ims.mTmpExtracted); + ims.mExtractedText.partialStartOffset + + " end=" + ims.mExtractedText.partialEndOffset + + ": " + ims.mExtractedText.text); + + imm.updateExtractedText(mTextView, req.token, ims.mExtractedText); ims.mChangedStart = EXTRACT_UNKNOWN; ims.mChangedEnd = EXTRACT_UNKNOWN; ims.mChangedDelta = 0; @@ -1734,138 +1732,92 @@ public class Editor { } } + public void addSpanWatchers(Spannable text) { + final int textLength = text.length(); + + if (mKeyListener != null) { + text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + + if (mEasyEditSpanController == null) { + mEasyEditSpanController = new EasyEditSpanController(); + } + text.setSpan(mEasyEditSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + /** * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related * pop-up should be displayed. */ - class EasyEditSpanController implements TextWatcher { + class EasyEditSpanController implements SpanWatcher { private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs private EasyEditPopupWindow mPopupWindow; - private EasyEditSpan mEasyEditSpan; - private Runnable mHidePopup; - public void hide() { - if (mPopupWindow != null) { - mPopupWindow.hide(); - mTextView.removeCallbacks(mHidePopup); - } - removeSpans(mTextView.getText()); - mEasyEditSpan = null; - } - - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Intentionally empty - } - - public void afterTextChanged(Editable s) { - // Intentionally empty - } + @Override + public void onSpanAdded(Spannable text, Object span, int start, int end) { + if (span instanceof EasyEditSpan) { + if (mPopupWindow == null) { + mPopupWindow = new EasyEditPopupWindow(); + mHidePopup = new Runnable() { + @Override + public void run() { + hide(); + } + }; + } - /** - * Monitors the changes in the text. - * - *

{@link SpanWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used, - * as the notifications are not sent when a spannable (with spans) is inserted. - */ - public void onTextChanged(CharSequence buffer, int start, int before, int after) { - adjustSpans(buffer, start, after); + // Make sure there is only at most one EasyEditSpan in the text + if (mPopupWindow.mEasyEditSpan != null) { + text.removeSpan(mPopupWindow.mEasyEditSpan); + } - if (mTextView.getWindowVisibility() != View.VISIBLE) { - // The window is not visible yet, ignore the text change. - return; - } + mPopupWindow.setEasyEditSpan((EasyEditSpan) span); - if (mTextView.getLayout() == null) { - // The view has not been layout yet, ignore the text change - return; - } - - InputMethodManager imm = InputMethodManager.peekInstance(); - if (!(mTextView instanceof ExtractEditText) && imm != null && imm.isFullscreenMode()) { - // The input is in extract mode. We do not have to handle the easy edit in the - // original TextView, as the ExtractEditText will do - return; - } + if (mTextView.getWindowVisibility() != View.VISIBLE) { + // The window is not visible yet, ignore the text change. + return; + } - // Remove the current easy edit span, as the text changed, and remove the pop-up - // (if any) - if (mEasyEditSpan != null) { - if (buffer instanceof Spannable) { - ((Spannable) buffer).removeSpan(mEasyEditSpan); + if (mTextView.getLayout() == null) { + // The view has not been laid out yet, ignore the text change + return; } - mEasyEditSpan = null; - } - if (mPopupWindow != null && mPopupWindow.isShowing()) { - mPopupWindow.hide(); - } - // Display the new easy edit span (if any). - if (buffer instanceof Spanned) { - mEasyEditSpan = getSpan((Spanned) buffer); - if (mEasyEditSpan != null) { - if (mPopupWindow == null) { - mPopupWindow = new EasyEditPopupWindow(); - mHidePopup = new Runnable() { - @Override - public void run() { - hide(); - } - }; - } - mPopupWindow.show(mEasyEditSpan); - mTextView.removeCallbacks(mHidePopup); - mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS); + if (extractedTextModeWillBeStarted()) { + // The input is in extract mode. Do not handle the easy edit in + // the original TextView, as the ExtractEditText will do + return; } + + mPopupWindow.show(); + mTextView.removeCallbacks(mHidePopup); + mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS); } } - /** - * Adjusts the spans by removing all of them except the last one. - */ - private void adjustSpans(CharSequence buffer, int start, int after) { - // This method enforces that only one easy edit span is attached to the text. - // A better way to enforce this would be to listen for onSpanAdded, but this method - // cannot be used in this scenario as no notification is triggered when a text with - // spans is inserted into a text. - if (buffer instanceof Spannable) { - Spannable spannable = (Spannable) buffer; - EasyEditSpan[] spans = spannable.getSpans(start, start + after, EasyEditSpan.class); - if (spans.length > 0) { - // Assuming there was only one EasyEditSpan before, we only need check to - // check for a duplicate if a new one is found in the modified interval - spans = spannable.getSpans(0, spannable.length(), EasyEditSpan.class); - for (int i = 1; i < spans.length; i++) { - spannable.removeSpan(spans[i]); - } - } + @Override + public void onSpanRemoved(Spannable text, Object span, int start, int end) { + if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) { + hide(); } } - /** - * Removes all the {@link EasyEditSpan} currently attached. - */ - private void removeSpans(CharSequence buffer) { - if (buffer instanceof Spannable) { - Spannable spannable = (Spannable) buffer; - EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(), - EasyEditSpan.class); - for (int i = 0; i < spans.length; i++) { - spannable.removeSpan(spans[i]); - } + @Override + public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd, + int newStart, int newEnd) { + if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) { + text.removeSpan(mPopupWindow.mEasyEditSpan); } } - private EasyEditSpan getSpan(Spanned spanned) { - EasyEditSpan[] easyEditSpans = spanned.getSpans(0, spanned.length(), - EasyEditSpan.class); - if (easyEditSpans.length == 0) { - return null; - } else { - return easyEditSpans[0]; + public void hide() { + if (mPopupWindow != null) { + mPopupWindow.hide(); + mTextView.removeCallbacks(mHidePopup); } } } @@ -1910,9 +1862,8 @@ public class Editor { mContentView.addView(mDeleteTextView); } - public void show(EasyEditSpan easyEditSpan) { + public void setEasyEditSpan(EasyEditSpan easyEditSpan) { mEasyEditSpan = easyEditSpan; - super.show(); } @Override @@ -3738,8 +3689,8 @@ public class Editor { Rect mCursorRectInWindow = new Rect(); RectF mTmpRectF = new RectF(); float[] mTmpOffset = new float[2]; - ExtractedTextRequest mExtracting; - final ExtractedText mTmpExtracted = new ExtractedText(); + ExtractedTextRequest mExtractedTextRequest; + final ExtractedText mExtractedText = new ExtractedText(); int mBatchEditNesting; boolean mCursorChanged; boolean mSelectionModeChanged; diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java index c725b64..f033072 100644 --- a/core/java/android/widget/SpellChecker.java +++ b/core/java/android/widget/SpellChecker.java @@ -207,6 +207,7 @@ public class SpellChecker implements SpellCheckerSessionListener { public void spellCheck(int start, int end) { final Locale locale = mTextView.getTextServicesLocale(); + final boolean isSessionActive = isSessionActive(); if (mCurrentLocale == null || (!(mCurrentLocale.equals(locale)))) { setLocale(locale); // Re-check the entire text @@ -214,13 +215,13 @@ public class SpellChecker implements SpellCheckerSessionListener { end = mTextView.getText().length(); } else { final boolean spellCheckerActivated = mTextServicesManager.isSpellCheckerEnabled(); - if (isSessionActive() != spellCheckerActivated) { + if (isSessionActive != spellCheckerActivated) { // Spell checker has been turned of or off since last spellCheck resetSession(); } } - if (!isSessionActive()) return; + if (!isSessionActive) return; // Find first available SpellParser from pool final int length = mSpellParsers.length; diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 9867e47..5164c7a 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -3176,22 +3176,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (text instanceof Spannable && !mAllowTransformationLengthChange) { Spannable sp = (Spannable) text; - // Remove any ChangeWatchers that might have come - // from other TextViews. + // Remove any ChangeWatchers that might have come from other TextViews. final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class); final int count = watchers.length; - for (int i = 0; i < count; i++) + for (int i = 0; i < count; i++) { sp.removeSpan(watchers[i]); + } - if (mChangeWatcher == null) - mChangeWatcher = new ChangeWatcher(); + if (mChangeWatcher == null) mChangeWatcher = new ChangeWatcher(); sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE | (CHANGE_WATCHER_PRIORITY << Spanned.SPAN_PRIORITY_SHIFT)); - if (mEditor != null && getEditor().mKeyListener != null) { - sp.setSpan(getEditor().mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - } + if (mEditor != null) mEditor.addSpanWatchers(sp); if (mTransformation != null) { sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); @@ -5231,7 +5228,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public void setExtracting(ExtractedTextRequest req) { if (getEditor().mInputMethodState != null) { - getEditor().mInputMethodState.mExtracting = req; + getEditor().mInputMethodState.mExtractedTextRequest = req; } // This would stop a possible selection mode, but no such mode is started in case // extracted mode will start. Some text is selected though, and will trigger an action mode @@ -6876,7 +6873,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (what instanceof ParcelableSpan) { // If this is a span that can be sent to a remote process, // the current extract editor would be interested in it. - if (ims != null && ims.mExtracting != null) { + if (ims != null && ims.mExtractedTextRequest != null) { if (ims.mBatchEditNesting != 0) { if (oldStart >= 0) { if (ims.mChangedStart > oldStart) { @@ -6897,7 +6894,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } else { if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Span change outside of batch: " + oldStart + "-" + oldEnd + "," - + newStart + "-" + newEnd + what); + + newStart + "-" + newEnd + " " + what); ims.mContentChanged = true; } } -- cgit v1.1