diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 7cf3e101c5e40..7b1acb15cc155 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -644,8 +644,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ private Layout mSavedMarqueeModeLayout; + // Do not update following mText/mSpannable/mPrecomputed except for setTextInternal() @ViewDebug.ExportedProperty(category = "text") - private CharSequence mText; + private @Nullable CharSequence mText; + private @Nullable Spannable mSpannable; + private @Nullable PrecomputedText mPrecomputed; + private CharSequence mTransformed; private BufferType mBufferType = BufferType.NORMAL; @@ -874,7 +878,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES); } - mText = ""; + setTextInternal(""); final Resources res = getResources(); final CompatibilityInfo compat = res.getCompatibilityInfo(); @@ -1615,6 +1619,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + // Update mText and mPrecomputed + private void setTextInternal(@Nullable CharSequence text) { + mText = text; + mSpannable = (text instanceof Spannable) ? (Spannable) text : null; + mPrecomputed = (text instanceof PrecomputedText) ? (PrecomputedText) text : null; + } + /** * Specify whether this widget should automatically scale the text to try to perfectly fit * within the layout bounds by using the default auto-size configuration. @@ -1973,9 +1984,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } } - } else if (mText instanceof Spannable) { + } else if (mSpannable != null) { // Reset the selection. - Selection.setSelection((Spannable) mText, getSelectionEnd()); + Selection.setSelection(mSpannable, getSelectionEnd()); } } } @@ -2359,7 +2370,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mMovement != movement) { mMovement = movement; - if (movement != null && !(mText instanceof Spannable)) { + if (movement != null && mSpannable == null) { setText(mText); } @@ -2409,8 +2420,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return; } if (mTransformation != null) { - if (mText instanceof Spannable) { - ((Spannable) mText).removeSpan(mTransformation); + if (mSpannable != null) { + mSpannable.removeSpan(mTransformation); } } @@ -5254,7 +5265,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener ((Editable) mText).append(text, start, end); if (mAutoLinkMask != 0) { - boolean linksWereAdded = Linkify.addLinks((Spannable) mText, mAutoLinkMask); + boolean linksWereAdded = Linkify.addLinks(mSpannable, mAutoLinkMask); // Do not change the movement method for text that support text selection as it // would prevent an arbitrary cursor displacement. if (linksWereAdded && mLinksClickable && !textCanBeSelected()) { @@ -5413,7 +5424,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (ss.selStart >= 0 && ss.selEnd >= 0) { - if (mText instanceof Spannable) { + if (mSpannable != null) { int len = mText.length(); if (ss.selStart > len || ss.selEnd > len) { @@ -5426,7 +5437,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Log.e(LOG_TAG, "Saved cursor position " + ss.selStart + "/" + ss.selEnd + " out of range for " + restored + "text " + mText); } else { - Selection.setSelection((Spannable) mText, ss.selStart, ss.selEnd); + Selection.setSelection(mSpannable, ss.selStart, ss.selEnd); if (ss.frozenWithFocus) { createEditorIfNeeded(); @@ -5688,7 +5699,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * movement method, because setMovementMethod() may call * setText() again to try to upgrade the buffer type. */ - mText = text; + setTextInternal(text); // Do not change the movement method for text that support text selection as it // would prevent an arbitrary cursor displacement. @@ -5699,7 +5710,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } mBufferType = type; - mText = text; + setTextInternal(text); if (mTransformation == null) { mTransformed = text; @@ -5825,8 +5836,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setText(text, type); if (start >= 0 || end >= 0) { - if (mText instanceof Spannable) { - Selection.setSelection((Spannable) mText, + if (mSpannable != null) { + Selection.setSelection(mSpannable, Math.max(0, Math.min(start, len)), Math.max(0, Math.min(end, len))); } @@ -6020,7 +6031,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (!isSuggestionsEnabled()) { - mText = removeSuggestionSpans(mText); + setTextInternal(removeSuggestionSpans(mText)); } InputMethodManager imm = InputMethodManager.peekInstance(); @@ -6948,8 +6959,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public boolean hasOverlappingRendering() { // horizontal fading edge causes SaveLayerAlpha, which doesn't support alpha modulation return ((getBackground() != null && getBackground().getCurrent() != null) - || mText instanceof Spannable || hasSelection() - || isHorizontalFadingEdgeEnabled()); + || mSpannable != null || hasSelection() || isHorizontalFadingEdgeEnabled()); } /** @@ -7399,11 +7409,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) { - if (mText instanceof Spannable && mLinksClickable) { + if (mSpannable != null && mLinksClickable) { final float x = event.getX(pointerIndex); final float y = event.getY(pointerIndex); final int offset = getOffsetForPosition(x, y); - final ClickableSpan[] clickables = ((Spannable) mText).getSpans(offset, offset, + final ClickableSpan[] clickables = mSpannable.getSpans(offset, offset, ClickableSpan.class); if (clickables.length > 0) { return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_HAND); @@ -7496,10 +7506,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } else if (which == KEY_DOWN_HANDLED_BY_MOVEMENT_METHOD) { // mMovement is not null from doKeyDown - mMovement.onKeyUp(this, (Spannable) mText, keyCode, up); + mMovement.onKeyUp(this, mSpannable, keyCode, up); while (--repeatCount > 0) { - mMovement.onKeyDown(this, (Spannable) mText, keyCode, down); - mMovement.onKeyUp(this, (Spannable) mText, keyCode, up); + mMovement.onKeyDown(this, mSpannable, keyCode, down); + mMovement.onKeyUp(this, mSpannable, keyCode, up); } } @@ -7694,8 +7704,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean doDown = true; if (otherEvent != null) { try { - boolean handled = mMovement.onKeyOther(this, (Spannable) mText, - otherEvent); + boolean handled = mMovement.onKeyOther(this, mSpannable, otherEvent); doDown = false; if (handled) { return KEY_EVENT_HANDLED; @@ -7706,7 +7715,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } if (doDown) { - if (mMovement.onKeyDown(this, (Spannable) mText, keyCode, event)) { + if (mMovement.onKeyDown(this, mSpannable, keyCode, event)) { if (event.getRepeatCount() == 0 && !KeyEvent.isModifierKey(keyCode)) { mPreventDefaultMovement = true; } @@ -7848,7 +7857,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (mMovement != null && mLayout != null) { - if (mMovement.onKeyUp(this, (Spannable) mText, keyCode, event)) { + if (mMovement.onKeyUp(this, mSpannable, keyCode, event)) { return true; } } @@ -8313,6 +8322,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mEditor != null) mEditor.prepareCursorControllers(); } + /** + * Returns true if DynamicLayout is required + * + * @hide + */ + @VisibleForTesting + public boolean useDynamicLayout() { + return isTextSelectable() || (mSpannable != null && mPrecomputed == null); + } + /** * @hide */ @@ -8320,7 +8339,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize, boolean useSaved) { Layout result = null; - if (mText instanceof Spannable) { + if (useDynamicLayout()) { final DynamicLayout.Builder builder = DynamicLayout.Builder.obtain(mText, mTextPaint, wantWidth) .setDisplayText(mTransformed) @@ -9262,7 +9281,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (newStart != start) { - Selection.setSelection((Spannable) mText, newStart); + Selection.setSelection(mSpannable, newStart); return true; } @@ -9999,9 +10018,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mEditor != null) mEditor.onFocusChanged(focused, direction); if (focused) { - if (mText instanceof Spannable) { - Spannable sp = (Spannable) mText; - MetaKeyKeyListener.resetMetaState(sp); + if (mSpannable != null) { + MetaKeyKeyListener.resetMetaState(mSpannable); } } @@ -10039,7 +10057,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public void clearComposingText() { if (mText instanceof Spannable) { - BaseInputConnection.removeComposingSpans((Spannable) mText); + BaseInputConnection.removeComposingSpans(mSpannable); } } @@ -10095,7 +10113,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean handled = false; if (mMovement != null) { - handled |= mMovement.onTouchEvent(this, (Spannable) mText, event); + handled |= mMovement.onTouchEvent(this, mSpannable, event); } final boolean textIsSelectable = isTextSelectable(); @@ -10103,7 +10121,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // The LinkMovementMethod which should handle taps on links has not been installed // on non editable text that support text selection. // We reproduce its behavior here to open links for these. - ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(), + ClickableSpan[] links = mSpannable.getSpans(getSelectionStart(), getSelectionEnd(), ClickableSpan.class); if (links.length > 0) { @@ -10138,7 +10156,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public boolean onGenericMotionEvent(MotionEvent event) { if (mMovement != null && mText instanceof Spannable && mLayout != null) { try { - if (mMovement.onGenericMotionEvent(this, (Spannable) mText, event)) { + if (mMovement.onGenericMotionEvent(this, mSpannable, event)) { return true; } } catch (AbstractMethodError ex) { @@ -10199,8 +10217,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public boolean onTrackballEvent(MotionEvent event) { - if (mMovement != null && mText instanceof Spannable && mLayout != null) { - if (mMovement.onTrackballEvent(this, (Spannable) mText, event)) { + if (mMovement != null && mSpannable != null && mLayout != null) { + if (mMovement.onTrackballEvent(this, mSpannable, event)) { return true; } } @@ -11121,7 +11139,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mText != null) { int updatedTextLength = mText.length(); if (updatedTextLength > 0) { - Selection.setSelection((Spannable) mText, updatedTextLength); + Selection.setSelection(mSpannable, updatedTextLength); } } } return true; @@ -11697,7 +11715,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener hideFloatingToolbar(FLOATING_TOOLBAR_SELECT_ALL_REFRESH_DELAY); } final int length = mText.length(); - Selection.setSelection((Spannable) mText, 0, length); + Selection.setSelection(mSpannable, 0, length); return length > 0; } @@ -11725,7 +11743,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (paste != null) { if (!didFirst) { - Selection.setSelection((Spannable) mText, max); + Selection.setSelection(mSpannable, max); ((Editable) mText).replace(min, max, paste); didFirst = true; } else { @@ -11747,7 +11765,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener selectedText = TextUtils.trimToParcelableSize(selectedText); sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, selectedText); getContext().startActivity(Intent.createChooser(sharingIntent, null)); - Selection.setSelection((Spannable) mText, getSelectionEnd()); + Selection.setSelection(mSpannable, getSelectionEnd()); } } @@ -11822,7 +11840,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case DragEvent.ACTION_DRAG_LOCATION: if (mText instanceof Spannable) { final int offset = getOffsetForPosition(event.getX(), event.getY()); - Selection.setSelection((Spannable) mText, offset); + Selection.setSelection(mSpannable, offset); } return true; diff --git a/core/tests/coretests/src/android/widget/TextViewTest.java b/core/tests/coretests/src/android/widget/TextViewTest.java index 2b5b27b527b76..4f1efbfed5f70 100644 --- a/core/tests/coretests/src/android/widget/TextViewTest.java +++ b/core/tests/coretests/src/android/widget/TextViewTest.java @@ -17,6 +17,7 @@ package android.widget; import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; @@ -32,9 +33,11 @@ import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; import android.text.GetChars; import android.text.Layout; +import android.text.PrecomputedText; import android.text.Selection; import android.text.Spannable; import android.view.View; +import android.widget.TextView.BufferType; import org.junit.Before; import org.junit.Rule; @@ -241,6 +244,82 @@ public class TextViewTest { mTextView.onTextContextMenuItem(TextView.ID_CUT); } + @Test + public void testUseDynamicLayout() { + mTextView = new TextView(mActivity); + mTextView.setTextIsSelectable(true); + String text = "HelloWorld"; + PrecomputedText precomputed = + PrecomputedText.create(text, mTextView.getTextMetricsParams()); + + mTextView.setTextIsSelectable(false); + mTextView.setText(text); + assertFalse(mTextView.useDynamicLayout()); + + mTextView.setTextIsSelectable(true); + mTextView.setText(text); + assertTrue(mTextView.useDynamicLayout()); + + mTextView.setTextIsSelectable(false); + mTextView.setText(precomputed); + assertFalse(mTextView.useDynamicLayout()); + + mTextView.setTextIsSelectable(true); + mTextView.setText(precomputed); + assertTrue(mTextView.useDynamicLayout()); + } + + @Test + public void testUseDynamicLayout_SPANNABLE() { + mTextView = new TextView(mActivity); + mTextView.setTextIsSelectable(true); + String text = "HelloWorld"; + PrecomputedText precomputed = + PrecomputedText.create(text, mTextView.getTextMetricsParams()); + + mTextView.setTextIsSelectable(false); + mTextView.setText(text, BufferType.SPANNABLE); + android.util.Log.e("TextViewTest", "Text:" + mTextView.getText().getClass().getName()); + assertTrue(mTextView.useDynamicLayout()); + + mTextView.setTextIsSelectable(true); + mTextView.setText(text, BufferType.SPANNABLE); + assertTrue(mTextView.useDynamicLayout()); + + mTextView.setTextIsSelectable(false); + mTextView.setText(precomputed, BufferType.SPANNABLE); + assertFalse(mTextView.useDynamicLayout()); + + mTextView.setTextIsSelectable(true); + mTextView.setText(precomputed, BufferType.SPANNABLE); + assertTrue(mTextView.useDynamicLayout()); + } + + @Test + public void testUseDynamicLayout_EDITABLE() { + mTextView = new TextView(mActivity); + mTextView.setTextIsSelectable(true); + String text = "HelloWorld"; + PrecomputedText precomputed = + PrecomputedText.create(text, mTextView.getTextMetricsParams()); + + mTextView.setTextIsSelectable(false); + mTextView.setText(text, BufferType.EDITABLE); + assertTrue(mTextView.useDynamicLayout()); + + mTextView.setTextIsSelectable(true); + mTextView.setText(text, BufferType.EDITABLE); + assertTrue(mTextView.useDynamicLayout()); + + mTextView.setTextIsSelectable(false); + mTextView.setText(precomputed, BufferType.EDITABLE); + assertTrue(mTextView.useDynamicLayout()); + + mTextView.setTextIsSelectable(true); + mTextView.setText(precomputed, BufferType.EDITABLE); + assertTrue(mTextView.useDynamicLayout()); + } + private String createLongText() { int size = 600 * 1000; final StringBuilder builder = new StringBuilder(size);