Loading core/java/android/text/DynamicLayout.java +24 −3 Original line number Diff line number Diff line Loading @@ -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); } } Loading @@ -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) { 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); } Loading core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java +124 −0 Original line number Diff line number Diff line Loading @@ -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); Loading @@ -128,6 +208,50 @@ public class DynamicLayoutOffsetMappingTest { assertThat(layout.getLineEnd(lineCount - 1)).isEqualTo(lineBreaks[lineCount]); } /** * 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. */ Loading Loading
core/java/android/text/DynamicLayout.java +24 −3 Original line number Diff line number Diff line Loading @@ -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); } } Loading @@ -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) { 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); } Loading
core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java +124 −0 Original line number Diff line number Diff line Loading @@ -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); Loading @@ -128,6 +208,50 @@ public class DynamicLayoutOffsetMappingTest { assertThat(layout.getLineEnd(lineCount - 1)).isEqualTo(lineBreaks[lineCount]); } /** * 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. */ Loading