Merge "Fix: insert mode crash when EmojiCompat is used" into udc-dev
This commit is contained in:
@@ -1106,6 +1106,16 @@ public class DynamicLayout extends Layout {
|
|||||||
mTransformedTextUpdate.before = before;
|
mTransformedTextUpdate.before = before;
|
||||||
mTransformedTextUpdate.after = after;
|
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);
|
transformedText.originalToTransformed(mTransformedTextUpdate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1113,9 +1123,20 @@ public class DynamicLayout extends Layout {
|
|||||||
public void onTextChanged(CharSequence s, int where, int before, int after) {
|
public void onTextChanged(CharSequence s, int where, int before, int after) {
|
||||||
final DynamicLayout dynamicLayout = mLayout.get();
|
final DynamicLayout dynamicLayout = mLayout.get();
|
||||||
if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) {
|
if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) {
|
||||||
where = mTransformedTextUpdate.where;
|
if (mTransformedTextUpdate != null && mTransformedTextUpdate.where >= 0) {
|
||||||
before = mTransformedTextUpdate.before;
|
where = mTransformedTextUpdate.where;
|
||||||
after = mTransformedTextUpdate.after;
|
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);
|
reflow(s, where, before, after);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,6 +119,86 @@ public class DynamicLayoutOffsetMappingTest {
|
|||||||
assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 8);
|
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) {
|
private void assertLineRange(Layout layout, int... lineBreaks) {
|
||||||
final int lineCount = lineBreaks.length - 1;
|
final int lineCount = lineBreaks.length - 1;
|
||||||
assertThat(layout.getLineCount()).isEqualTo(lineCount);
|
assertThat(layout.getLineCount()).isEqualTo(lineCount);
|
||||||
@@ -128,6 +208,50 @@ public class DynamicLayoutOffsetMappingTest {
|
|||||||
assertThat(layout.getLineEnd(lineCount - 1)).isEqualTo(lineBreaks[lineCount]);
|
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.
|
* A test TransformedText that inserts some text at the given offset.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user