diff options
| author | 2023-05-05 23:29:13 +0000 | |
|---|---|---|
| committer | 2023-05-05 23:29:13 +0000 | |
| commit | 74ad406b7cc2f19096780487178f4f673ce60095 (patch) | |
| tree | 05be4a7de46927066dce297ff903894f187323d4 | |
| parent | 3146826cf00125304b9db58ff3d4b97de4f6c65c (diff) | |
| parent | 47ef59ecf0be9e88ff20320100a2d86317464b59 (diff) | |
Merge "Fix: insert mode crash when EmojiCompat is used" into udc-dev
| -rw-r--r-- | core/java/android/text/DynamicLayout.java | 27 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java | 124 |
2 files changed, 148 insertions, 3 deletions
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index 196bac21ad74..cd767541e13a 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -1106,6 +1106,16 @@ public class DynamicLayout extends Layout { mTransformedTextUpdate.before = before; mTransformedTextUpdate.after = after; } + // When there is a transformed text, we have to reflow the DynamicLayout based on + // the transformed indices instead of the range in base text. + // For example, + // base text: abcd > abce + // updated range: where = 3, before = 1, after = 1 + // transformed text: abxxcd > abxxce + // updated range: where = 5, before = 1, after = 1 + // + // Because the transformedText is udapted simultaneously with the base text, + // the range must be transformed before the base text changes. transformedText.originalToTransformed(mTransformedTextUpdate); } } @@ -1113,9 +1123,20 @@ public class DynamicLayout extends Layout { public void onTextChanged(CharSequence s, int where, int before, int after) { final DynamicLayout dynamicLayout = mLayout.get(); if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) { - where = mTransformedTextUpdate.where; - before = mTransformedTextUpdate.before; - after = mTransformedTextUpdate.after; + if (mTransformedTextUpdate != null && mTransformedTextUpdate.where >= 0) { + where = mTransformedTextUpdate.where; + before = mTransformedTextUpdate.before; + after = mTransformedTextUpdate.after; + // Set where to -1 so that we know if beforeTextChanged is called. + mTransformedTextUpdate.where = -1; + } else { + // onTextChanged is called without beforeTextChanged. Reflow the entire text. + where = 0; + // We can't get the before length from the text, use the line end of the + // last line instead. + before = dynamicLayout.getLineEnd(dynamicLayout.getLineCount() - 1); + after = dynamicLayout.mDisplay.length(); + } } reflow(s, where, before, after); } diff --git a/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java b/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java index 76f417151001..5939c0609e18 100644 --- a/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java +++ b/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java @@ -119,6 +119,86 @@ public class DynamicLayoutOffsetMappingTest { assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 8); } + @Test + public void textWithOffsetMapping_blockBeforeTextChanged_deletion() { + final String text = "abcdef"; + final SpannableStringBuilder spannable = new TestNoBeforeTextChangeSpannableString(text); + final CharSequence transformedText = + new TestOffsetMapping(spannable, 5, "\n\n"); + + final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH) + .setAlignment(ALIGN_NORMAL) + .setIncludePad(false) + .setDisplayText(transformedText) + .build(); + + // delete "cd", original text becomes "abef" + spannable.delete(2, 4); + assertThat(transformedText.toString()).isEqualTo("abe\n\nf"); + assertLineRange(layout, /* lineBreaks */ 0, 4, 5, 6); + + // delete "abe", original text becomes "f" + spannable.delete(0, 3); + assertThat(transformedText.toString()).isEqualTo("\n\nf"); + assertLineRange(layout, /* lineBreaks */ 0, 1, 2, 3); + } + + @Test + public void textWithOffsetMapping_blockBeforeTextChanged_insertion() { + final String text = "abcdef"; + final SpannableStringBuilder spannable = new TestNoBeforeTextChangeSpannableString(text); + final CharSequence transformedText = new TestOffsetMapping(spannable, 3, "\n\n"); + + final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH) + .setAlignment(ALIGN_NORMAL) + .setIncludePad(false) + .setDisplayText(transformedText) + .build(); + + spannable.insert(3, "x"); + assertThat(transformedText.toString()).isEqualTo("abcx\n\ndef"); + assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 9); + + spannable.insert(5, "x"); + assertThat(transformedText.toString()).isEqualTo("abcx\n\ndxef"); + assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 10); + } + + @Test + public void textWithOffsetMapping_blockBeforeTextChanged_replace() { + final String text = "abcdef"; + final SpannableStringBuilder spannable = new TestNoBeforeTextChangeSpannableString(text); + final CharSequence transformedText = new TestOffsetMapping(spannable, 3, "\n\n"); + + final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH) + .setAlignment(ALIGN_NORMAL) + .setIncludePad(false) + .setDisplayText(transformedText) + .build(); + + spannable.replace(2, 4, "xx"); + assertThat(transformedText.toString()).isEqualTo("abxx\n\nef"); + assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 8); + } + + @Test + public void textWithOffsetMapping_onlyCallOnTextChanged_notCrash() { + String text = "abcdef"; + SpannableStringBuilder spannable = new SpannableStringBuilder(text); + CharSequence transformedText = new TestOffsetMapping(spannable, 3, "\n\n"); + + DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH) + .setAlignment(ALIGN_NORMAL) + .setIncludePad(false) + .setDisplayText(transformedText) + .build(); + + TextWatcher[] textWatcher = spannable.getSpans(0, spannable.length(), TextWatcher.class); + assertThat(textWatcher.length).isEqualTo(1); + + textWatcher[0].onTextChanged(spannable, 0, 2, 2); + } + private void assertLineRange(Layout layout, int... lineBreaks) { final int lineCount = lineBreaks.length - 1; assertThat(layout.getLineCount()).isEqualTo(lineCount); @@ -129,6 +209,50 @@ public class DynamicLayoutOffsetMappingTest { } /** + * A test SpannableStringBuilder that doesn't call beforeTextChanged. It's used to test + * DynamicLayout against some special cases where beforeTextChanged callback is not properly + * called. + */ + private static class TestNoBeforeTextChangeSpannableString extends SpannableStringBuilder { + + TestNoBeforeTextChangeSpannableString(CharSequence text) { + super(text); + } + + @Override + public void setSpan(Object what, int start, int end, int flags) { + if (what instanceof TextWatcher) { + super.setSpan(new TestNoBeforeTextChangeWatcherWrapper((TextWatcher) what), start, + end, flags); + } else { + super.setSpan(what, start, end, flags); + } + } + } + + /** A TextWatcherWrapper that blocks beforeTextChanged callback. */ + private static class TestNoBeforeTextChangeWatcherWrapper implements TextWatcher { + private final TextWatcher mTextWatcher; + + TestNoBeforeTextChangeWatcherWrapper(TextWatcher textWatcher) { + mTextWatcher = textWatcher; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + mTextWatcher.onTextChanged(s, start, before, count); + } + + @Override + public void afterTextChanged(Editable s) { + mTextWatcher.afterTextChanged(s); + } + } + + /** * A test TransformedText that inserts some text at the given offset. */ private static class TestOffsetMapping implements OffsetMapping, CharSequence { |