Fix: insert mode crash when EmojiCompat is used
Bug: 279057703 Test: atest DynamicLayoutOffsetMappingTest Change-Id: I2469cdb0c4cba5ef91799b79fad3155e65ff24bb
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user