Merge "Introduce InsertModeTransformationMethod"
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.text.method;
|
||||
|
||||
import android.annotation.IntRange;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.text.Editable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.style.ReplacementSpan;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.MathUtils;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.internal.util.ArrayUtils;
|
||||
import com.android.internal.util.Preconditions;
|
||||
|
||||
/**
|
||||
* The transformation method used by handwriting insert mode.
|
||||
* This transformation will insert a placeholder string to the original text at the given
|
||||
* offset. And it also provides a highlight range for the newly inserted text and the placeholder
|
||||
* text.
|
||||
*
|
||||
* For example,
|
||||
* original text: "Hello world"
|
||||
* insert mode is started at index: 5,
|
||||
* placeholder text: "\n\n"
|
||||
* The transformed text will be: "Hello\n\n world", and the highlight range will be [5, 7)
|
||||
* including the inserted placeholder text.
|
||||
*
|
||||
* If " abc" is inserted to the original text at index 5,
|
||||
* the new original text: "Hello abc world"
|
||||
* the new transformed text: "hello abc\n\n world", and the highlight range will be [5, 11).
|
||||
* @hide
|
||||
*/
|
||||
public class InsertModeTransformationMethod implements TransformationMethod, TextWatcher {
|
||||
/** The start offset of the highlight range in the original text, inclusive. */
|
||||
private int mStart;
|
||||
/**
|
||||
* The end offset of the highlight range in the original text, exclusive. The placeholder text
|
||||
* is also inserted at this index.
|
||||
*/
|
||||
private int mEnd;
|
||||
/** The transformation method that's already set on the {@link android.widget.TextView}. */
|
||||
private final TransformationMethod mOldTransformationMethod;
|
||||
/** Whether the {@link android.widget.TextView} is single-lined. */
|
||||
private final boolean mSingleLine;
|
||||
|
||||
/**
|
||||
* @param offset the original offset to start the insert mode. It must be in the range from 0
|
||||
* to the length of the transformed text.
|
||||
* @param singleLine whether the text is single line.
|
||||
* @param oldTransformationMethod the old transformation method at the
|
||||
* {@link android.widget.TextView}. If it's not null, this {@link TransformationMethod} will
|
||||
* first call {@link TransformationMethod#getTransformation(CharSequence, View)} on the old one,
|
||||
* and then do the transformation for the insert mode.
|
||||
*
|
||||
*/
|
||||
public InsertModeTransformationMethod(@IntRange(from = 0) int offset, boolean singleLine,
|
||||
@NonNull TransformationMethod oldTransformationMethod) {
|
||||
mStart = offset;
|
||||
mEnd = offset;
|
||||
mSingleLine = singleLine;
|
||||
mOldTransformationMethod = oldTransformationMethod;
|
||||
}
|
||||
|
||||
public TransformationMethod getOldTransformationMethod() {
|
||||
return mOldTransformationMethod;
|
||||
}
|
||||
|
||||
private CharSequence getPlaceholderText(View view) {
|
||||
if (!mSingleLine) {
|
||||
return "\n\n";
|
||||
}
|
||||
final SpannableString singleLinePlaceholder = new SpannableString("\uFFFD");
|
||||
final DisplayMetrics displayMetrics = view.getResources().getDisplayMetrics();
|
||||
final int widthPx = (int) Math.ceil(
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 108, displayMetrics));
|
||||
|
||||
singleLinePlaceholder.setSpan(new SingleLinePlaceholderSpan(widthPx), 0, 1,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return singleLinePlaceholder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getTransformation(CharSequence source, View view) {
|
||||
final CharSequence charSequence;
|
||||
if (mOldTransformationMethod != null) {
|
||||
charSequence = mOldTransformationMethod.getTransformation(source, view);
|
||||
if (source instanceof Spannable) {
|
||||
final Spannable spannable = (Spannable) source;
|
||||
spannable.setSpan(mOldTransformationMethod, 0, spannable.length(),
|
||||
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
}
|
||||
} else {
|
||||
charSequence = source;
|
||||
}
|
||||
|
||||
final CharSequence placeholderText = getPlaceholderText(view);
|
||||
return new TransformedText(charSequence, placeholderText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFocusChanged(View view, CharSequence sourceText, boolean focused, int direction,
|
||||
Rect previouslyFocusedRect) {
|
||||
if (mOldTransformationMethod != null) {
|
||||
mOldTransformationMethod.onFocusChanged(view, sourceText, focused, direction,
|
||||
previouslyFocusedRect);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
// The text change is after the offset where placeholder is inserted, return.
|
||||
if (start > mEnd) return;
|
||||
final int diff = count - before;
|
||||
|
||||
// Note: If start == mStart and before == 0, the change is also considered after the
|
||||
// highlight start. It won't modify the mStart in this case.
|
||||
if (start < mStart) {
|
||||
if (start + before <= mStart) {
|
||||
// The text change is before the highlight start, move the highlight start.
|
||||
mStart += diff;
|
||||
} else {
|
||||
// The text change covers the highlight start. Extend the highlight start to the
|
||||
// change start. This should be a rare case.
|
||||
mStart = start;
|
||||
}
|
||||
}
|
||||
|
||||
if (start + before <= mEnd) {
|
||||
// The text change is before the highlight end, move the highlight end.
|
||||
mEnd += diff;
|
||||
} else if (start < mEnd) {
|
||||
// The text change covers the highlight end. Extend the highlight end to the
|
||||
// change end. This should be a rare case.
|
||||
mEnd = start + count;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) { }
|
||||
|
||||
/**
|
||||
* The transformed text returned by the {@link InsertModeTransformationMethod}.
|
||||
*/
|
||||
public class TransformedText implements OffsetMapping, Spanned {
|
||||
private final CharSequence mOriginal;
|
||||
private final CharSequence mPlaceholder;
|
||||
private final Spanned mSpannedOriginal;
|
||||
private final Spanned mSpannedPlaceholder;
|
||||
|
||||
TransformedText(CharSequence original, CharSequence placeholder) {
|
||||
mOriginal = original;
|
||||
if (original instanceof Spanned) {
|
||||
mSpannedOriginal = (Spanned) original;
|
||||
} else {
|
||||
mSpannedOriginal = null;
|
||||
}
|
||||
mPlaceholder = placeholder;
|
||||
if (placeholder instanceof Spanned) {
|
||||
mSpannedPlaceholder = (Spanned) placeholder;
|
||||
} else {
|
||||
mSpannedPlaceholder = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int originalToTransformed(int offset, int strategy) {
|
||||
if (offset < 0) return offset;
|
||||
Preconditions.checkArgumentInRange(offset, 0, mOriginal.length(), "offset");
|
||||
if (offset == mEnd && strategy == OffsetMapping.MAP_STRATEGY_CURSOR) {
|
||||
// The offset equals to mEnd. For a cursor position it's considered before the
|
||||
// inserted placeholder text.
|
||||
return offset;
|
||||
}
|
||||
if (offset < mEnd) {
|
||||
return offset;
|
||||
}
|
||||
return offset + mPlaceholder.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int transformedToOriginal(int offset, int strategy) {
|
||||
if (offset < 0) return offset;
|
||||
Preconditions.checkArgumentInRange(offset, 0, length(), "offset");
|
||||
|
||||
// The placeholder text is inserted at mEnd. Because the offset is smaller than
|
||||
// mEnd, we can directly return it.
|
||||
if (offset < mEnd) return offset;
|
||||
if (offset < mEnd + mPlaceholder.length()) {
|
||||
return mEnd;
|
||||
}
|
||||
return offset - mPlaceholder.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void originalToTransformed(TextUpdate textUpdate) {
|
||||
if (textUpdate.where > mEnd) {
|
||||
textUpdate.where += mPlaceholder.length();
|
||||
} else if (textUpdate.where + textUpdate.before > mEnd) {
|
||||
// The update also covers the placeholder string.
|
||||
textUpdate.before += mPlaceholder.length();
|
||||
textUpdate.after += mPlaceholder.length();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return mOriginal.length() + mPlaceholder.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public char charAt(int index) {
|
||||
Preconditions.checkArgumentInRange(index, 0, length() - 1, "index");
|
||||
if (index < mEnd) {
|
||||
return mOriginal.charAt(index);
|
||||
}
|
||||
if (index < mEnd + mPlaceholder.length()) {
|
||||
return mPlaceholder.charAt(index - mEnd);
|
||||
}
|
||||
return mOriginal.charAt(index - mPlaceholder.length());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence subSequence(int start, int end) {
|
||||
if (end < start || start < 0 || end > length()) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
if (start == end) {
|
||||
return "";
|
||||
}
|
||||
|
||||
final int placeholderLength = mPlaceholder.length();
|
||||
|
||||
final int seg1Start = Math.min(start, mEnd);
|
||||
final int seg1End = Math.min(end, mEnd);
|
||||
|
||||
final int seg2Start = MathUtils.constrain(start - mEnd, 0, placeholderLength);
|
||||
final int seg2End = MathUtils.constrain(end - mEnd, 0, placeholderLength);
|
||||
|
||||
final int seg3Start = Math.max(start - placeholderLength, mEnd);
|
||||
final int seg3End = Math.max(end - placeholderLength, mEnd);
|
||||
|
||||
return TextUtils.concat(
|
||||
mOriginal.subSequence(seg1Start, seg1End),
|
||||
mPlaceholder.subSequence(seg2Start, seg2End),
|
||||
mOriginal.subSequence(seg3Start, seg3End));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.valueOf(mOriginal.subSequence(0, mEnd))
|
||||
+ mPlaceholder
|
||||
+ mOriginal.subSequence(mEnd, mOriginal.length());
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T[] getSpans(int start, int end, Class<T> type) {
|
||||
if (end < start) {
|
||||
return ArrayUtils.emptyArray(type);
|
||||
}
|
||||
|
||||
final T[] spansOriginal;
|
||||
if (mSpannedOriginal != null) {
|
||||
final int originalStart =
|
||||
transformedToOriginal(start, OffsetMapping.MAP_STRATEGY_CURSOR);
|
||||
final int originalEnd =
|
||||
transformedToOriginal(end, OffsetMapping.MAP_STRATEGY_CURSOR);
|
||||
spansOriginal = mSpannedOriginal.getSpans(originalStart, originalEnd, type);
|
||||
} else {
|
||||
spansOriginal = null;
|
||||
}
|
||||
|
||||
final T[] spansPlaceholder;
|
||||
if (mSpannedPlaceholder != null
|
||||
&& intersect(start, end, mEnd, mEnd + mPlaceholder.length())) {
|
||||
final int placeholderStart = Math.max(start - mEnd, 0);
|
||||
final int placeholderEnd = Math.min(end - mEnd, mPlaceholder.length());
|
||||
spansPlaceholder =
|
||||
mSpannedPlaceholder.getSpans(placeholderStart, placeholderEnd, type);
|
||||
} else {
|
||||
spansPlaceholder = null;
|
||||
}
|
||||
|
||||
// TODO: sort the spans based on their priority.
|
||||
return ArrayUtils.concat(type, spansOriginal, spansPlaceholder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSpanStart(Object tag) {
|
||||
if (mSpannedOriginal != null) {
|
||||
final int index = mSpannedOriginal.getSpanStart(tag);
|
||||
if (index >= 0) {
|
||||
if (index < mEnd) {
|
||||
return index;
|
||||
}
|
||||
return index + mPlaceholder.length();
|
||||
}
|
||||
}
|
||||
|
||||
// The span is not on original text, try find it on the placeholder.
|
||||
if (mSpannedPlaceholder != null) {
|
||||
final int index = mSpannedPlaceholder.getSpanStart(tag);
|
||||
if (index >= 0) {
|
||||
// Find the span on placeholder, transform it and return.
|
||||
return index + mEnd;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSpanEnd(Object tag) {
|
||||
if (mSpannedOriginal != null) {
|
||||
final int index = mSpannedOriginal.getSpanEnd(tag);
|
||||
if (index >= 0) {
|
||||
if (index <= mEnd) {
|
||||
return index;
|
||||
}
|
||||
return index + mPlaceholder.length();
|
||||
}
|
||||
}
|
||||
|
||||
// The span is not on original text, try find it on the placeholder.
|
||||
if (mSpannedPlaceholder != null) {
|
||||
final int index = mSpannedPlaceholder.getSpanEnd(tag);
|
||||
if (index >= 0) {
|
||||
// Find the span on placeholder, transform it and return.
|
||||
return index + mEnd;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSpanFlags(Object tag) {
|
||||
if (mSpannedOriginal != null) {
|
||||
final int flags = mSpannedOriginal.getSpanFlags(tag);
|
||||
if (flags != 0) {
|
||||
return flags;
|
||||
}
|
||||
}
|
||||
if (mSpannedPlaceholder != null) {
|
||||
return mSpannedPlaceholder.getSpanFlags(tag);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int nextSpanTransition(int start, int limit, Class type) {
|
||||
if (limit <= start) return limit;
|
||||
final Object[] spans = getSpans(start, limit, type);
|
||||
for (int i = 0; i < spans.length; ++i) {
|
||||
int spanStart = getSpanStart(spans[i]);
|
||||
int spanEnd = getSpanEnd(spans[i]);
|
||||
if (start < spanStart && spanStart < limit) {
|
||||
limit = spanStart;
|
||||
}
|
||||
if (start < spanEnd && spanEnd < limit) {
|
||||
limit = spanEnd;
|
||||
}
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the start index of the highlight range for the insert mode, inclusive.
|
||||
*/
|
||||
public int getHighlightStart() {
|
||||
return mStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the end index of the highlight range for the insert mode, exclusive.
|
||||
*/
|
||||
public int getHighlightEnd() {
|
||||
return mEnd + mPlaceholder.length();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The placeholder span used for single line
|
||||
*/
|
||||
public static class SingleLinePlaceholderSpan extends ReplacementSpan {
|
||||
private final int mWidth;
|
||||
SingleLinePlaceholderSpan(int width) {
|
||||
mWidth = width;
|
||||
}
|
||||
@Override
|
||||
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end,
|
||||
@Nullable Paint.FontMetricsInt fm) {
|
||||
return mWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x,
|
||||
int top, int y, int bottom, @NonNull Paint paint) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the given two ranges intersects. This logic is the same one used in
|
||||
* {@link Spanned} to determine whether a span range intersect with the query range.
|
||||
*/
|
||||
private static boolean intersect(int s1, int e1, int s2, int e2) {
|
||||
if (s1 > e2) return false;
|
||||
if (e1 < s2) return false;
|
||||
if (s1 != e1 && s2 != e2) {
|
||||
if (s1 == e2) return false;
|
||||
if (e1 == s2) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,796 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.text.method;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.content.Context;
|
||||
import android.platform.test.annotations.Presubmit;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ReplacementSpan;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.filters.SmallTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@Presubmit
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class InsertModeTransformationMethodTest {
|
||||
private static View sView;
|
||||
private static final String TEXT = "abc def";
|
||||
|
||||
@BeforeClass
|
||||
public static void setupClass() {
|
||||
final Context context = InstrumentationRegistry.getTargetContext();
|
||||
sView = new View(context);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_charAt() {
|
||||
for (int offset = 0; offset < TEXT.length(); ++offset) {
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(offset, false, null);
|
||||
final CharSequence transformedText =
|
||||
transformationMethod.getTransformation(TEXT, sView);
|
||||
final CharSequence expected =
|
||||
TEXT.substring(0, offset) + "\n\n" + TEXT.substring(offset);
|
||||
|
||||
assertCharSequence(transformedText, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_charAt_singleLine() {
|
||||
for (int offset = 0; offset < TEXT.length(); ++offset) {
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(offset, true, null);
|
||||
final CharSequence transformedText =
|
||||
transformationMethod.getTransformation(TEXT, sView);
|
||||
final CharSequence expected =
|
||||
TEXT.substring(0, offset) + "\uFFFD" + TEXT.substring(offset);
|
||||
|
||||
assertCharSequence(transformedText, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_charAt_editing() {
|
||||
transformedText_charAt_editing(false, "\n\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_charAt_singleLine_editing() {
|
||||
transformedText_charAt_editing(true, "\uFFFD");
|
||||
}
|
||||
|
||||
public void transformedText_charAt_editing(boolean singleLine, String placeholder) {
|
||||
final SpannableStringBuilder text = new SpannableStringBuilder(TEXT);
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(3, singleLine, null);
|
||||
final CharSequence transformedText = transformationMethod.getTransformation(text, sView);
|
||||
// TransformationMethod is set on the original text as a TextWatcher in the TextView.
|
||||
text.setSpan(transformationMethod, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
assertCharSequence(transformedText, "abc" + placeholder + " def");
|
||||
|
||||
// original text is "abcxx def" after insertion.
|
||||
text.insert(3, "xx");
|
||||
assertCharSequence(transformedText, "abcxx" + placeholder + " def");
|
||||
|
||||
// original text is "abcxx vvdef" after insertion.
|
||||
text.insert(6, "vv");
|
||||
assertCharSequence(transformedText, "abcxx" + placeholder + " vvdef");
|
||||
|
||||
// original text is "abc vvdef" after deletion.
|
||||
text.delete(3, 5);
|
||||
assertCharSequence(transformedText, "abc" + placeholder + " vvdef");
|
||||
|
||||
// original text is "abc def" after deletion.
|
||||
text.delete(4, 6);
|
||||
assertCharSequence(transformedText, "abc" + placeholder + " def");
|
||||
|
||||
// original text is "abdef" after deletion.
|
||||
// the placeholder is now inserted at index 2, since the deletion covers the index 3.
|
||||
text.delete(2, 4);
|
||||
assertCharSequence(transformedText, "ab" + placeholder + "def");
|
||||
|
||||
// original text is "axxdef" after replace.
|
||||
text.replace(1, 2, "xx");
|
||||
assertCharSequence(transformedText, "axx" + placeholder + "def");
|
||||
|
||||
// original text is "axxvvf" after replace.
|
||||
text.replace(3, 5, "vv");
|
||||
assertCharSequence(transformedText, "axx" + placeholder + "vvf");
|
||||
|
||||
// original text is "abc def" after replace.
|
||||
// the placeholder is inserted at index 6 after the insertion, since the replacement covers
|
||||
// the index 3.
|
||||
text.replace(1, 5, "bc de");
|
||||
assertCharSequence(transformedText, "abc de" + placeholder + "f");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_subSequence() {
|
||||
for (int offset = 0; offset < TEXT.length(); ++offset) {
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(offset, false, null);
|
||||
final CharSequence transformedText =
|
||||
transformationMethod.getTransformation(TEXT, sView);
|
||||
final CharSequence expected =
|
||||
TEXT.substring(0, offset) + "\n\n" + TEXT.substring(offset);
|
||||
|
||||
for (int start = 0; start < transformedText.length(); ++start) {
|
||||
for (int end = start; end <= transformedText.length(); ++end) {
|
||||
assertCharSequence(transformedText.subSequence(start, end),
|
||||
expected.subSequence(start, end));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_subSequence_singleLine() {
|
||||
for (int offset = 0; offset < TEXT.length(); ++offset) {
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(offset, true, null);
|
||||
final CharSequence transformedText =
|
||||
transformationMethod.getTransformation(TEXT, sView);
|
||||
final CharSequence expected =
|
||||
TEXT.substring(0, offset) + "\uFFFD" + TEXT.substring(offset);
|
||||
|
||||
for (int start = 0; start < transformedText.length(); ++start) {
|
||||
for (int end = start; end <= transformedText.length(); ++end) {
|
||||
assertCharSequence(transformedText.subSequence(start, end),
|
||||
expected.subSequence(start, end));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_toString() {
|
||||
for (int offset = 0; offset < TEXT.length(); ++offset) {
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(offset, false, null);
|
||||
final CharSequence transformedText =
|
||||
transformationMethod.getTransformation(TEXT, sView);
|
||||
final String expected =
|
||||
TEXT.substring(0, offset) + "\n\n" + TEXT.substring(offset);
|
||||
|
||||
assertThat(transformedText.toString()).isEqualTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_toString_singleLine() {
|
||||
for (int offset = 0; offset < TEXT.length(); ++offset) {
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(offset, true, null);
|
||||
final CharSequence transformedText =
|
||||
transformationMethod.getTransformation(TEXT, sView);
|
||||
final String expected =
|
||||
TEXT.substring(0, offset) + "\uFFFD" + TEXT.substring(offset);
|
||||
|
||||
assertThat(transformedText.toString()).isEqualTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void transformedText_getSpans() {
|
||||
final SpannableString text = new SpannableString(TEXT);
|
||||
final TestSpan span1 = new TestSpan();
|
||||
final TestSpan span2 = new TestSpan();
|
||||
final TestSpan span3 = new TestSpan();
|
||||
|
||||
text.setSpan(span1, 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
text.setSpan(span2, 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
text.setSpan(span3, 4, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
// In the transformedText "abc\n\n def", the new ranges of the spans are:
|
||||
// span1: [0, 3)
|
||||
// span2: [2, 6)
|
||||
// span3: [6, 7)
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(3, false, null);
|
||||
final Spanned transformedText =
|
||||
(Spanned) transformationMethod.getTransformation(text, sView);
|
||||
|
||||
// only span1 is in the range of [0, 2).
|
||||
final TestSpan[] spans0to2 = transformedText.getSpans(0, 2, TestSpan.class);
|
||||
assertThat(spans0to2.length).isEqualTo(1);
|
||||
assertThat(spans0to2[0]).isEqualTo(span1);
|
||||
|
||||
// span1 and span2 are in the range of [1, 6).
|
||||
final TestSpan[] spans1to6 = transformedText.getSpans(1, 6, TestSpan.class);
|
||||
assertThat(spans1to6.length).isEqualTo(2);
|
||||
assertThat(spans1to6[0]).isEqualTo(span1);
|
||||
assertThat(spans1to6[1]).isEqualTo(span2);
|
||||
|
||||
// only span2 is in the range of [4, 6).
|
||||
final TestSpan[] spans4to6 = transformedText.getSpans(4, 6, TestSpan.class);
|
||||
assertThat(spans4to6.length).isEqualTo(1);
|
||||
assertThat(spans4to6[0]).isEqualTo(span2);
|
||||
|
||||
// span2 and span3 are in the range of [4, 7).
|
||||
final TestSpan[] spans4to7 = transformedText.getSpans(4, 7, TestSpan.class);
|
||||
assertThat(spans4to7.length).isEqualTo(2);
|
||||
assertThat(spans4to7[0]).isEqualTo(span2);
|
||||
assertThat(spans4to7[1]).isEqualTo(span3);
|
||||
|
||||
// only span3 is in the range of [6, 7).
|
||||
final TestSpan[] spans6to7 = transformedText.getSpans(6, 7, TestSpan.class);
|
||||
assertThat(spans6to7.length).isEqualTo(1);
|
||||
assertThat(spans6to7[0]).isEqualTo(span3);
|
||||
|
||||
// there is no span in the range of [7, 9).
|
||||
final TestSpan[] spans7to9 = transformedText.getSpans(7, 9, TestSpan.class);
|
||||
assertThat(spans7to9.length).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_getSpans_singleLine() {
|
||||
final SpannableString text = new SpannableString(TEXT);
|
||||
final TestSpan span1 = new TestSpan();
|
||||
final TestSpan span2 = new TestSpan();
|
||||
final TestSpan span3 = new TestSpan();
|
||||
|
||||
text.setSpan(span1, 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
text.setSpan(span2, 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
text.setSpan(span3, 4, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
// In the transformedText, the new ranges of the spans are:
|
||||
// span1: [0, 3)
|
||||
// span2: [2, 5)
|
||||
// span3: [5, 6)
|
||||
// There should also be a ReplacementSpan in the range [3, 4).
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(3, true, null);
|
||||
final Spanned transformedText =
|
||||
(Spanned) transformationMethod.getTransformation(text, sView);
|
||||
|
||||
// only span1 is in the range of [0, 2).
|
||||
final TestSpan[] spans0to2 = transformedText.getSpans(0, 2, TestSpan.class);
|
||||
assertThat(spans0to2.length).isEqualTo(1);
|
||||
assertThat(spans0to2[0]).isEqualTo(span1);
|
||||
|
||||
// span1 and span2 are in the range of [1, 5).
|
||||
final TestSpan[] spans1to4 = transformedText.getSpans(1, 4, TestSpan.class);
|
||||
assertThat(spans1to4.length).isEqualTo(2);
|
||||
assertThat(spans1to4[0]).isEqualTo(span1);
|
||||
assertThat(spans1to4[1]).isEqualTo(span2);
|
||||
|
||||
// only span2 is in the range of [3, 5).
|
||||
final TestSpan[] spans3to5 = transformedText.getSpans(3, 5, TestSpan.class);
|
||||
assertThat(spans3to5.length).isEqualTo(1);
|
||||
assertThat(spans3to5[0]).isEqualTo(span2);
|
||||
|
||||
// span2 and span3 are in the range of [3, 6).
|
||||
final TestSpan[] spans3to6 = transformedText.getSpans(3, 6, TestSpan.class);
|
||||
assertThat(spans3to6.length).isEqualTo(2);
|
||||
assertThat(spans3to6[0]).isEqualTo(span2);
|
||||
assertThat(spans3to6[1]).isEqualTo(span3);
|
||||
|
||||
// only span3 is in the range of [5, 6).
|
||||
final TestSpan[] spans5to6 = transformedText.getSpans(5, 6, TestSpan.class);
|
||||
assertThat(spans5to6.length).isEqualTo(1);
|
||||
assertThat(spans5to6[0]).isEqualTo(span3);
|
||||
|
||||
// there is no span in the range of [6, 8)
|
||||
final TestSpan[] spans6to8 = transformedText.getSpans(6, 8, TestSpan.class);
|
||||
assertThat(spans6to8.length).isEqualTo(0);
|
||||
|
||||
// When it's singleLine, there should be a ReplacementSpan in the range [3, 4)
|
||||
final ReplacementSpan[] replacementSpans3to4 =
|
||||
transformedText.getSpans(3, 4, ReplacementSpan.class);
|
||||
assertThat(replacementSpans3to4.length).isEqualTo(1);
|
||||
|
||||
final ReplacementSpan[] replacementSpans0to3 =
|
||||
transformedText.getSpans(0, 3, ReplacementSpan.class);
|
||||
assertThat(replacementSpans0to3.length).isEqualTo(0);
|
||||
|
||||
final ReplacementSpan[] replacementSpans4to8 =
|
||||
transformedText.getSpans(4, 8, ReplacementSpan.class);
|
||||
assertThat(replacementSpans4to8.length).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_getSpanStartAndEnd() {
|
||||
final SpannableString text = new SpannableString(TEXT);
|
||||
final TestSpan span1 = new TestSpan();
|
||||
final TestSpan span2 = new TestSpan();
|
||||
final TestSpan span3 = new TestSpan();
|
||||
|
||||
text.setSpan(span1, 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
text.setSpan(span2, 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
text.setSpan(span3, 4, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
// In the transformedText, the new ranges of the spans are:
|
||||
// span1: [0, 3)
|
||||
// span2: [2, 6)
|
||||
// span3: [6, 7)
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(3, false, null);
|
||||
final Spanned transformedText =
|
||||
(Spanned) transformationMethod.getTransformation(text, sView);
|
||||
|
||||
assertThat(transformedText.getSpanStart(span1)).isEqualTo(0);
|
||||
assertThat(transformedText.getSpanEnd(span1)).isEqualTo(3);
|
||||
|
||||
assertThat(transformedText.getSpanStart(span2)).isEqualTo(2);
|
||||
assertThat(transformedText.getSpanEnd(span2)).isEqualTo(6);
|
||||
|
||||
assertThat(transformedText.getSpanStart(span3)).isEqualTo(6);
|
||||
assertThat(transformedText.getSpanEnd(span3)).isEqualTo(7);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_getSpanStartAndEnd_singleLine() {
|
||||
final SpannableString text = new SpannableString(TEXT);
|
||||
final TestSpan span1 = new TestSpan();
|
||||
final TestSpan span2 = new TestSpan();
|
||||
final TestSpan span3 = new TestSpan();
|
||||
|
||||
text.setSpan(span1, 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
text.setSpan(span2, 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
text.setSpan(span3, 4, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
// In the transformedText, the new ranges of the spans are:
|
||||
// span1: [0, 3)
|
||||
// span2: [2, 5)
|
||||
// span3: [5, 6)
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(3, true, null);
|
||||
final Spanned transformedText =
|
||||
(Spanned) transformationMethod.getTransformation(text, sView);
|
||||
|
||||
assertThat(transformedText.getSpanStart(span1)).isEqualTo(0);
|
||||
assertThat(transformedText.getSpanEnd(span1)).isEqualTo(3);
|
||||
|
||||
assertThat(transformedText.getSpanStart(span2)).isEqualTo(2);
|
||||
assertThat(transformedText.getSpanEnd(span2)).isEqualTo(5);
|
||||
|
||||
assertThat(transformedText.getSpanStart(span3)).isEqualTo(5);
|
||||
assertThat(transformedText.getSpanEnd(span3)).isEqualTo(6);
|
||||
|
||||
final ReplacementSpan[] replacementSpans =
|
||||
transformedText.getSpans(0, 8, ReplacementSpan.class);
|
||||
assertThat(transformedText.getSpanStart(replacementSpans[0])).isEqualTo(3);
|
||||
assertThat(transformedText.getSpanEnd(replacementSpans[0])).isEqualTo(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_getSpanFlag() {
|
||||
transformedText_getSpanFlag(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_getSpanFlag_singleLine() {
|
||||
transformedText_getSpanFlag(true);
|
||||
}
|
||||
|
||||
public void transformedText_getSpanFlag(boolean singleLine) {
|
||||
final SpannableString text = new SpannableString(TEXT);
|
||||
final TestSpan span1 = new TestSpan();
|
||||
final TestSpan span2 = new TestSpan();
|
||||
final TestSpan span3 = new TestSpan();
|
||||
|
||||
text.setSpan(span1, 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
text.setSpan(span2, 2, 4, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
|
||||
text.setSpan(span3, 4, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(3, singleLine, null);
|
||||
final Spanned transformedText =
|
||||
(Spanned) transformationMethod.getTransformation(text, sView);
|
||||
|
||||
assertThat(transformedText.getSpanFlags(span1)).isEqualTo(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
assertThat(transformedText.getSpanFlags(span2)).isEqualTo(Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
|
||||
assertThat(transformedText.getSpanFlags(span3)).isEqualTo(Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_nextSpanTransition() {
|
||||
final SpannableString text = new SpannableString(TEXT);
|
||||
final TestSpan span1 = new TestSpan();
|
||||
final TestSpan span2 = new TestSpan();
|
||||
final TestSpan span3 = new TestSpan();
|
||||
|
||||
|
||||
text.setSpan(span1, 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
text.setSpan(span2, 1, 4, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
|
||||
text.setSpan(span3, 4, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
// In the transformedText, the new ranges of the spans are:
|
||||
// span1: [0, 3)
|
||||
// span2: [1, 6)
|
||||
// span3: [6, 7)
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(3, false, null);
|
||||
final Spanned transformedText =
|
||||
(Spanned) transformationMethod.getTransformation(text, sView);
|
||||
final int[] expectedTransitions = new int[] { 0, 1, 3, 6, 7 };
|
||||
assertNextSpanTransition(transformedText, expectedTransitions, TestSpan.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_nextSpanTransition_singleLine() {
|
||||
final SpannableString text = new SpannableString(TEXT);
|
||||
final TestSpan span1 = new TestSpan();
|
||||
final TestSpan span2 = new TestSpan();
|
||||
final TestSpan span3 = new TestSpan();
|
||||
|
||||
|
||||
text.setSpan(span1, 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
text.setSpan(span2, 1, 4, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
|
||||
text.setSpan(span3, 4, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
// In the transformedText, the new ranges of the spans are:
|
||||
// span1: [0, 3)
|
||||
// span2: [1, 5)
|
||||
// span3: [5, 6)
|
||||
// there is also a ReplacementSpan at range [3, 4)
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(3, true, null);
|
||||
final Spanned transformedText =
|
||||
(Spanned) transformationMethod.getTransformation(text, sView);
|
||||
final int[] expectedTransitions = new int[] { 0, 1, 3, 4, 5, 6 };
|
||||
assertNextSpanTransition(transformedText, expectedTransitions, Object.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_originalToTransformed() {
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(2, false, null);
|
||||
final OffsetMapping transformedText =
|
||||
(OffsetMapping) transformationMethod.getTransformation(TEXT, sView);
|
||||
|
||||
// "abc def" is transformed to "ab\n\nc def".
|
||||
final int[] mappedCharacterOffsets = new int[] { 0, 1, 4, 5, 6, 7, 8 };
|
||||
assertOriginalToTransformed(transformedText, OffsetMapping.MAP_STRATEGY_CHARACTER,
|
||||
mappedCharacterOffsets);
|
||||
|
||||
// "abc def" is transformed to "ab\n\nc def".
|
||||
// the cursor before 'c' is mapped to index position before "\n\n".
|
||||
final int[] mappedCursorOffsets = new int[] { 0, 1, 2, 5, 6, 7, 8 };
|
||||
assertOriginalToTransformed(transformedText, OffsetMapping.MAP_STRATEGY_CURSOR,
|
||||
mappedCursorOffsets);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_originalToTransformed_singleLine() {
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(2, true, null);
|
||||
final OffsetMapping transformedText =
|
||||
(OffsetMapping) transformationMethod.getTransformation(TEXT, sView);
|
||||
|
||||
// "abc def" is transformed to "ab\uFFFDc def".
|
||||
final int[] mappedCharacterOffsets = new int[] { 0, 1, 3, 4, 5, 6, 7 };
|
||||
assertOriginalToTransformed(transformedText, OffsetMapping.MAP_STRATEGY_CHARACTER,
|
||||
mappedCharacterOffsets);
|
||||
|
||||
// "abc def" is transformed to "ab\uFFFDc def".
|
||||
// the cursor before 'c' is mapped to index position before "\uFFFD".
|
||||
final int[] mappedCursorOffsets = new int[] { 0, 1, 3, 4, 5, 6, 7 };
|
||||
assertOriginalToTransformed(transformedText, OffsetMapping.MAP_STRATEGY_CHARACTER,
|
||||
mappedCursorOffsets);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_transformedToOriginal() {
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(2, false, null);
|
||||
final OffsetMapping transformedText =
|
||||
(OffsetMapping) transformationMethod.getTransformation(TEXT, sView);
|
||||
|
||||
// "abc def" is transformed to "ab\n\nc def".
|
||||
// the two '\n' characters have no corresponding character; map them to 'c'.
|
||||
final int[] mappedCharacterOffsets = new int[] { 0, 1, 2, 2, 2, 3, 4, 5, 6 };
|
||||
assertTransformedToOriginal(transformedText, OffsetMapping.MAP_STRATEGY_CHARACTER,
|
||||
mappedCharacterOffsets);
|
||||
|
||||
// offset 2 and 3 (cursor positions before the two '\n' characters) are mapped to index 2
|
||||
// (cursor position before 'c' in the original text)
|
||||
final int[] mappedCursorOffsets = new int[] { 0, 1, 2, 2, 2, 3, 4, 5, 6 };
|
||||
assertTransformedToOriginal(transformedText, OffsetMapping.MAP_STRATEGY_CURSOR,
|
||||
mappedCursorOffsets);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_transformedToOriginal_singleLine() {
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(2, true, null);
|
||||
final OffsetMapping transformedText =
|
||||
(OffsetMapping) transformationMethod.getTransformation(TEXT, sView);
|
||||
|
||||
// "abc def" is transformed to "ab\uFFFDc def".
|
||||
// '\uFFFD' has no corresponding character; map it to 'c'.
|
||||
final int[] mappedCharacterOffsets = new int[] { 0, 1, 2, 2, 3, 4, 5, 6 };
|
||||
assertTransformedToOriginal(transformedText, OffsetMapping.MAP_STRATEGY_CHARACTER,
|
||||
mappedCharacterOffsets);
|
||||
|
||||
// offset 2 (cursor positions before '\uFFFD') is mapped to index 2 (cursor position before
|
||||
// 'c' in the original text)
|
||||
final int[] mappedCursorOffsets = new int[] { 0, 1, 2, 2, 3, 4, 5, 6 };
|
||||
assertTransformedToOriginal(transformedText, OffsetMapping.MAP_STRATEGY_CHARACTER,
|
||||
mappedCursorOffsets);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_getHighlightStartAndEnd_insertion() {
|
||||
transformedText_getHighlightStartAndEnd_insertion(false, "\n\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_getHighlightStartAndEnd_insertion_singleLine() {
|
||||
transformedText_getHighlightStartAndEnd_insertion(true, "\uFDDD");
|
||||
}
|
||||
|
||||
public void transformedText_getHighlightStartAndEnd_insertion(boolean singleLine,
|
||||
String placeholder) {
|
||||
final SpannableStringBuilder text = new SpannableStringBuilder(TEXT);
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(3, singleLine, null);
|
||||
final InsertModeTransformationMethod.TransformedText transformedText =
|
||||
(InsertModeTransformationMethod.TransformedText) transformationMethod
|
||||
.getTransformation(text, sView);
|
||||
// TransformationMethod is set on the original text as a TextWatcher in the TextView.
|
||||
text.setSpan(transformationMethod, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
// note: the placeholder text is also highlighted.
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(3);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(3 + placeholder.length());
|
||||
|
||||
// original text is "abcxx def" after insertion.
|
||||
// the placeholder is now inserted at index 5.
|
||||
// the highlight start is still 3.
|
||||
// the highlight end now is 5 + placeholder.length(), including the newly inserted text.
|
||||
text.insert(3, "xx");
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(3);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(5 + placeholder.length());
|
||||
|
||||
// original text is "abcxxvv def" after insertion.
|
||||
// the placeholder is now inserted at index 7.
|
||||
// the highlight start is still 3.
|
||||
// the highlight end now is 7 + placeholder.length(), including the newly inserted text.
|
||||
text.insert(5, "vv");
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(3);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(7 + placeholder.length());
|
||||
|
||||
// original text is "abzzcxxvv def" after insertion.
|
||||
// the placeholder is now inserted at index 9.
|
||||
// the highlight start is 5, since the insertion happens before the highlight range.
|
||||
// the highlight end now is 9 + placeholder.length().
|
||||
text.insert(2, "zz");
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(5);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(9 + placeholder.length());
|
||||
|
||||
// original text is "abzzcxxvv iidef" after insertion.
|
||||
// the placeholder is still inserted at index 9.
|
||||
// the highlight start is still 5, since the insertion happens after the highlight range.
|
||||
// the highlight end now is 9 + placeholder.length().
|
||||
text.insert(10, "ii");
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(5);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(9 + placeholder.length());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_getHighlightStartAndEnd_deletion() {
|
||||
transformedText_getHighlightStartAndEnd_deletion(false, "\n\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_getHighlightStartAndEnd_insertion_deletion() {
|
||||
transformedText_getHighlightStartAndEnd_deletion(true, "\uFDDD");
|
||||
}
|
||||
|
||||
public void transformedText_getHighlightStartAndEnd_deletion(boolean singleLine,
|
||||
String placeholder) {
|
||||
final SpannableStringBuilder text = new SpannableStringBuilder(TEXT);
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(3, singleLine, null);
|
||||
final InsertModeTransformationMethod.TransformedText transformedText =
|
||||
(InsertModeTransformationMethod.TransformedText) transformationMethod
|
||||
.getTransformation(text, sView);
|
||||
// TransformationMethod is set on the original text as a TextWatcher in the TextView.
|
||||
text.setSpan(transformationMethod, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
// note: the placeholder text is also highlighted.
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(3);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(3 + placeholder.length());
|
||||
|
||||
// original text is "abcxxxxxx def" after insertion.
|
||||
// the placeholder is now inserted at index 9.
|
||||
// the highlight start is still 3.
|
||||
// the highlight end now is 9 + placeholder.length().
|
||||
text.insert(3, "xxxxxx");
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(3);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(9 + placeholder.length());
|
||||
|
||||
// original text is "abxxxxxx def" after deletion.
|
||||
// the placeholder is now inserted at index 6.
|
||||
// the highlight start is 2, since the deletion happens before the highlight range.
|
||||
// the highlight end now is 8 + placeholder.length().
|
||||
text.delete(2, 3);
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(2);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(8 + placeholder.length());
|
||||
|
||||
// original text is "abxxx def" after deletion.
|
||||
// the placeholder is now inserted at index 5.
|
||||
// the highlight start is still 2, since the deletion happens in the highlight range.
|
||||
// the highlight end now is 5 + placeholder.length().
|
||||
text.delete(2, 5);
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(2);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(5 + placeholder.length());
|
||||
|
||||
// original text is "abxxx d" after deletion.
|
||||
// the placeholder is now inserted at index 5.
|
||||
// the highlight start is still 2, since the deletion happens after the highlight range.
|
||||
// the highlight end now is still 5 + placeholder.length().
|
||||
text.delete(7, 9);
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(2);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(5 + placeholder.length());
|
||||
|
||||
// original text is "af" after deletion.
|
||||
// the placeholder is now inserted at index 1.
|
||||
// the highlight start is 1, since the deletion covers highlight range.
|
||||
// the highlight end is 1 + placeholder.length().
|
||||
text.delete(1, 5);
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(1);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(1 + placeholder.length());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void transformedText_getHighlightStartAndEnd_replace() {
|
||||
transformedText_getHighlightStartAndEnd_replace(false, "\n\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedText_getHighlightStartAndEnd_insertion__replace() {
|
||||
transformedText_getHighlightStartAndEnd_replace(true, "\uFDDD");
|
||||
}
|
||||
|
||||
public void transformedText_getHighlightStartAndEnd_replace(boolean singleLine,
|
||||
String placeholder) {
|
||||
final SpannableStringBuilder text = new SpannableStringBuilder(TEXT);
|
||||
final InsertModeTransformationMethod transformationMethod =
|
||||
new InsertModeTransformationMethod(3, singleLine, null);
|
||||
final InsertModeTransformationMethod.TransformedText transformedText =
|
||||
(InsertModeTransformationMethod.TransformedText) transformationMethod
|
||||
.getTransformation(text, sView);
|
||||
// TransformationMethod is set on the original text as a TextWatcher in the TextView.
|
||||
text.setSpan(transformationMethod, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
// note: the placeholder text is also highlighted.
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(3);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(3 + placeholder.length());
|
||||
|
||||
// original text is "abcxxxxxx def" after insertion.
|
||||
// the placeholder is now inserted at index 9.
|
||||
// the highlight start is still 3.
|
||||
// the highlight end now is 9 + placeholder.length().
|
||||
text.insert(3, "xxxxxx");
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(3);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(9 + placeholder.length());
|
||||
|
||||
// original text is "abvvxxxxxx def" after replace.
|
||||
// the replacement happens before the highlight range; highlight range is offset by 1
|
||||
// the placeholder is now inserted at index 10,
|
||||
// the highlight start is 4.
|
||||
// the highlight end is 10 + placeholder.length().
|
||||
text.replace(2, 3, "vv");
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(4);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(10 + placeholder.length());
|
||||
|
||||
// original text is "abvvxxx def" after replace.
|
||||
// the replacement happens in the highlight range; highlight end is offset by -3
|
||||
// the placeholder is now inserted at index 7,
|
||||
// the highlight start is still 4.
|
||||
// the highlight end is 7 + placeholder.length().
|
||||
text.replace(5, 9, "x");
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(4);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(7 + placeholder.length());
|
||||
|
||||
// original text is "abvvxxxvvv" after replace.
|
||||
// the replacement happens after the highlight range; highlight is not changed
|
||||
// the placeholder is now inserted at index 7,
|
||||
// the highlight start is still 4.
|
||||
// the highlight end is 7 + placeholder.length().
|
||||
text.replace(7, 11, "vvv");
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(4);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(7 + placeholder.length());
|
||||
|
||||
// original text is "abxxxxvvv" after replace.
|
||||
// the replacement happens covers the highlight start; highlight start extends to the
|
||||
// replacement start; highlight end is offset by -1
|
||||
// the placeholder is now inserted at index 6,
|
||||
// the highlight start is 2.
|
||||
// the highlight end is 6 + placeholder.length().
|
||||
text.replace(2, 5, "xx");
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(2);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(6 + placeholder.length());
|
||||
|
||||
// original text is "abxxxxxvv" after replace.
|
||||
// the replacement happens covers the highlight end; highlight end extends to the
|
||||
// replacement end; highlight start stays the same
|
||||
// the placeholder is now inserted at index 7,
|
||||
// the highlight start is 2.
|
||||
// the highlight end is 7 + placeholder.length().
|
||||
text.replace(5, 7, "xx");
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(2);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(7 + placeholder.length());
|
||||
|
||||
// original text is "axxv" after replace.
|
||||
// the replacement happens covers the highlight range; highlight start is set to the
|
||||
// replacement start; highlight end is set to the replacement end
|
||||
// the placeholder is now inserted at index 3,
|
||||
// the highlight start is 1.
|
||||
// the highlight end is 3 + placeholder.length().
|
||||
text.replace(1, 8, "xx");
|
||||
assertThat(transformedText.getHighlightStart()).isEqualTo(1);
|
||||
assertThat(transformedText.getHighlightEnd()).isEqualTo(3 + placeholder.length());
|
||||
}
|
||||
|
||||
private static <T> void assertNextSpanTransition(Spanned spanned, int[] transitions,
|
||||
Class<T> type) {
|
||||
int currentTransition = 0;
|
||||
for (int transition : transitions) {
|
||||
assertThat(currentTransition).isEqualTo(transition);
|
||||
currentTransition =
|
||||
spanned.nextSpanTransition(currentTransition, spanned.length(), type);
|
||||
}
|
||||
|
||||
// Make sure there is no transition after the currentTransition.
|
||||
assertThat(currentTransition).isEqualTo(spanned.length());
|
||||
}
|
||||
|
||||
private static void assertCharSequence(CharSequence actual, CharSequence expected) {
|
||||
assertThat(actual.length()).isEqualTo(expected.length());
|
||||
for (int index = 0; index < actual.length(); ++index) {
|
||||
assertThat(actual.charAt(index)).isEqualTo(expected.charAt(index));
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertOriginalToTransformed(OffsetMapping transformedText, int strategy,
|
||||
int[] expected) {
|
||||
for (int offset = 0; offset < expected.length; ++offset) {
|
||||
assertThat(transformedText.originalToTransformed(offset, strategy))
|
||||
.isEqualTo(expected[offset]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertTransformedToOriginal(OffsetMapping transformedText, int strategy,
|
||||
int[] expected) {
|
||||
for (int offset = 0; offset < expected.length; ++offset) {
|
||||
assertThat(transformedText.transformedToOriginal(offset, strategy))
|
||||
.isEqualTo(expected[offset]);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestSpan { }
|
||||
}
|
||||
Reference in New Issue
Block a user