Loading core/java/android/text/DynamicLayout.java +45 −6 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import android.compat.annotation.UnsupportedAppUsage; import android.graphics.Paint; import android.graphics.Rect; import android.os.Build; import android.text.method.OffsetMapping; import android.text.style.ReplacementSpan; import android.text.style.UpdateLayout; import android.text.style.WrapTogetherSpan; Loading Loading @@ -1095,10 +1096,27 @@ public class DynamicLayout extends Layout { } public void beforeTextChanged(CharSequence s, int where, int before, int after) { // Intentionally empty final DynamicLayout dynamicLayout = mLayout.get(); if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) { final OffsetMapping transformedText = (OffsetMapping) dynamicLayout.mDisplay; if (mTransformedTextUpdate == null) { mTransformedTextUpdate = new OffsetMapping.TextUpdate(where, before, after); } else { mTransformedTextUpdate.where = where; mTransformedTextUpdate.before = before; mTransformedTextUpdate.after = after; } transformedText.originalToTransformed(mTransformedTextUpdate); } } 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; } reflow(s, where, before, after); } Loading @@ -1106,14 +1124,34 @@ public class DynamicLayout extends Layout { // Intentionally empty } public void onSpanAdded(Spannable s, Object o, int start, int end) { if (o instanceof UpdateLayout) /** * Reflow the {@link DynamicLayout} at the given range from {@code start} to the * {@code end}. * If the display text in this {@link DynamicLayout} is a {@link OffsetMapping} instance * (which means it's also a transformed text), it will transform the given range first and * then reflow. */ private void transformAndReflow(Spannable s, int start, int end) { final DynamicLayout dynamicLayout = mLayout.get(); if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) { final OffsetMapping transformedText = (OffsetMapping) dynamicLayout.mDisplay; start = transformedText.originalToTransformed(start, OffsetMapping.MAP_STRATEGY_CHARACTER); end = transformedText.originalToTransformed(end, OffsetMapping.MAP_STRATEGY_CHARACTER); } reflow(s, start, end - start, end - start); } public void onSpanAdded(Spannable s, Object o, int start, int end) { if (o instanceof UpdateLayout) { transformAndReflow(s, start, end); } } public void onSpanRemoved(Spannable s, Object o, int start, int end) { if (o instanceof UpdateLayout) reflow(s, start, end - start, end - start); transformAndReflow(s, start, end); } public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) { Loading @@ -1123,12 +1161,13 @@ public class DynamicLayout extends Layout { // instead of causing an exception start = 0; } reflow(s, start, end - start, end - start); reflow(s, nstart, nend - nstart, nend - nstart); transformAndReflow(s, start, end); transformAndReflow(s, nstart, nend); } } private WeakReference<DynamicLayout> mLayout; private OffsetMapping.TextUpdate mTransformedTextUpdate; } @Override Loading core/java/android/text/method/ArrowKeyMovementMethod.java +30 −0 Original line number Diff line number Diff line Loading @@ -68,6 +68,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override protected boolean left(TextView widget, Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendLeft(buffer, layout); Loading @@ -78,6 +81,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override protected boolean right(TextView widget, Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendRight(buffer, layout); Loading @@ -88,6 +94,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override protected boolean up(TextView widget, Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendUp(buffer, layout); Loading @@ -98,6 +107,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override protected boolean down(TextView widget, Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendDown(buffer, layout); Loading @@ -108,6 +120,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override protected boolean pageUp(TextView widget, Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); final boolean selecting = isSelecting(buffer); final int targetY = getCurrentLineTop(buffer, layout) - getPageHeight(widget); Loading @@ -132,6 +147,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override protected boolean pageDown(TextView widget, Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); final boolean selecting = isSelecting(buffer); final int targetY = getCurrentLineTop(buffer, layout) + getPageHeight(widget); Loading Loading @@ -176,6 +194,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override protected boolean lineStart(TextView widget, Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendToLeftEdge(buffer, layout); Loading @@ -186,6 +207,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override protected boolean lineEnd(TextView widget, Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendToRightEdge(buffer, layout); Loading Loading @@ -224,6 +248,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override public boolean previousParagraph(@NonNull TextView widget, @NonNull Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendToParagraphStart(buffer); Loading @@ -234,6 +261,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override public boolean nextParagraph(@NonNull TextView widget, @NonNull Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendToParagraphEnd(buffer); Loading core/java/android/text/method/BaseKeyListener.java +3 −2 Original line number Diff line number Diff line Loading @@ -440,8 +440,9 @@ public abstract class BaseKeyListener extends MetaKeyKeyListener private boolean deleteLine(View view, Editable content) { if (view instanceof TextView) { final Layout layout = ((TextView) view).getLayout(); if (layout != null) { final TextView textView = (TextView) view; final Layout layout = textView.getLayout(); if (layout != null && !textView.isOffsetMappingAvailable()) { final int line = layout.getLineForOffset(Selection.getSelectionStart(content)); final int start = layout.getLineStart(line); final int end = layout.getLineEnd(line); Loading core/java/android/text/method/LinkMovementMethod.java +4 −0 Original line number Diff line number Diff line Loading @@ -100,6 +100,10 @@ public class LinkMovementMethod extends ScrollingMovementMethod { private boolean action(int what, TextView widget, Spannable buffer) { Layout layout = widget.getLayout(); if (widget.isOffsetMappingAvailable()) { // The text in the layout is transformed and has OffsetMapping, don't do anything. return false; } int padding = widget.getTotalPaddingTop() + widget.getTotalPaddingBottom(); Loading core/java/android/text/method/OffsetMapping.java 0 → 100644 +170 −0 Original line number Diff line number Diff line /* * 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.IntDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * The interface for the index mapping information of a transformed text returned by * {@link TransformationMethod}. This class is mainly used to support the * {@link TransformationMethod} that alters the text length. * @hide */ public interface OffsetMapping { /** * The mapping strategy for a character offset. * * @see #originalToTransformed(int, int) * @see #transformedToOriginal(int, int) */ int MAP_STRATEGY_CHARACTER = 0; /** * The mapping strategy for a cursor position. * * @see #originalToTransformed(int, int) * @see #transformedToOriginal(int, int) */ int MAP_STRATEGY_CURSOR = 1; @IntDef(prefix = { "MAP_STRATEGY" }, value = { MAP_STRATEGY_CHARACTER, MAP_STRATEGY_CURSOR }) @Retention(RetentionPolicy.SOURCE) @interface MapStrategy {} /** * Map an offset at original text to the offset at transformed text. <br/> * * This function must be a monotonically non-decreasing function. In other words, if the offset * advances at the original text, the offset at the transformed text must advance or stay there. * <br/> * * Depending on the mapping strategy, a same offset can be mapped differently. For example, * <pre> * Original: ABCDE * Transformed: ABCXDE ('X' is introduced due to the transformation.) * </pre> * Let's check the offset 3, which is the offset of the character 'D'. * If we want to map the character offset 3, it should be mapped to index 4. * If we want to map the cursor offset 3 (the offset of the character before which the cursor is * placed), it's unclear if the mapped cursor is before 'X' or after 'X'. * This depends on how the transformed text reacts an insertion at offset 3 in the original * text. Assume character 'V' is insert at offset 3 in the original text, and the original text * become "ABCVDE". The transformed text can be: * <pre> * 1) "ABCVXDE" * or * 2) "ABCXVDE" * </pre> * In the first case, the offset 3 should be mapped to 3 (before 'X'). And in the second case, * the offset should be mapped to 4 (after 'X').<br/> * * In some cases, a character offset at the original text doesn't have a proper corresponding * offset at the transformed text. For example: * <pre> * Original: ABCDE * Transformed: ABDE ('C' is deleted due to the transformation.) * </pre> * The character offset 2 can be mapped either to offset 2 or 3, but neither is meaningful. For * convenience, it MUST map to the next offset (offset 3 in this case), or the * transformedText.length() if there is no valid character to map. * This is mandatory when the map strategy is {@link #MAP_STRATEGY_CHARACTER}, but doesn't * apply for other map strategies. * * @param offset the offset at the original text. It's normally equal to or less than the * originalText.length(). When {@link #MAP_STRATEGY_CHARACTER} is passed, it must * be less than originalText.length(). For convenience, it's also allowed to be * negative, which represents an invalid offset. When the given offset is * negative, this method should return it as it is. * @param strategy the mapping strategy. Depending on its value, the same offset can be mapped * differently. * @return the mapped offset at the transformed text, must be equal to or less than the * transformedText.length(). * * @see #transformedToOriginal(int, int) */ int originalToTransformed(int offset, @MapStrategy int strategy); /** * Map an offset at transformed text to the offset at original text. This is the reverse method * of {@link #originalToTransformed(int, int)}. <br/> * This function must be a monotonically non-decreasing function. In other words, if the offset * advances at the original text, the offset at the transformed text must advance or stay there. * <br/> * Similar to the {@link #originalToTransformed(int, int)} if a character offset doesn't have a * corresponding offset at the transformed text, it MUST return the value as the previous * offset. This is mandatory when the map strategy is {@link #MAP_STRATEGY_CHARACTER}, * but doesn't apply for other map strategies. * * @param offset the offset at the transformed text. It's normally equal to or less than the * transformedText.length(). When {@link #MAP_STRATEGY_CHARACTER} is passed, it * must be less than transformedText.length(). For convenience, it's also allowed * to be negative, which represents an invalid offset. When the given offset is * negative, this method should return it as it is. * @param strategy the mapping strategy. Depending on its value, the same offset can be mapped * differently. * @return the mapped offset at the original text, must be equal to or less than the * original.length(). * * @see #originalToTransformed(int, int) */ int transformedToOriginal(int offset, @MapStrategy int strategy); /** * Map a text update in the original text to an update the transformed text. * This method used to determine how the transformed text is updated in response to an * update in the original text. It is always called before the original text being changed. * * The main usage of this method is to update text layout incrementally. So it should report * the range where text needs to be laid out again. * * @param textUpdate the {@link TextUpdate} object containing the text update information on * the original text. The transformed text update information will also be * stored at this object. */ void originalToTransformed(TextUpdate textUpdate); /** * The class that stores the text update information that from index <code>where</code>, * <code>after</code> characters will replace the old text that has length <code>before</code>. */ class TextUpdate { /** The start index of the text update range, inclusive */ public int where; /** The length of the replaced old text. */ public int before; /** The length of the new text that replaces the old text. */ public int after; /** * Creates a {@link TextUpdate} object. * @param where the start index of the text update range. * @param before the length of the replaced old text. * @param after the length of the new text that replaces the old text. */ public TextUpdate(int where, int before, int after) { this.where = where; this.before = before; this.after = after; } } } Loading
core/java/android/text/DynamicLayout.java +45 −6 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import android.compat.annotation.UnsupportedAppUsage; import android.graphics.Paint; import android.graphics.Rect; import android.os.Build; import android.text.method.OffsetMapping; import android.text.style.ReplacementSpan; import android.text.style.UpdateLayout; import android.text.style.WrapTogetherSpan; Loading Loading @@ -1095,10 +1096,27 @@ public class DynamicLayout extends Layout { } public void beforeTextChanged(CharSequence s, int where, int before, int after) { // Intentionally empty final DynamicLayout dynamicLayout = mLayout.get(); if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) { final OffsetMapping transformedText = (OffsetMapping) dynamicLayout.mDisplay; if (mTransformedTextUpdate == null) { mTransformedTextUpdate = new OffsetMapping.TextUpdate(where, before, after); } else { mTransformedTextUpdate.where = where; mTransformedTextUpdate.before = before; mTransformedTextUpdate.after = after; } transformedText.originalToTransformed(mTransformedTextUpdate); } } 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; } reflow(s, where, before, after); } Loading @@ -1106,14 +1124,34 @@ public class DynamicLayout extends Layout { // Intentionally empty } public void onSpanAdded(Spannable s, Object o, int start, int end) { if (o instanceof UpdateLayout) /** * Reflow the {@link DynamicLayout} at the given range from {@code start} to the * {@code end}. * If the display text in this {@link DynamicLayout} is a {@link OffsetMapping} instance * (which means it's also a transformed text), it will transform the given range first and * then reflow. */ private void transformAndReflow(Spannable s, int start, int end) { final DynamicLayout dynamicLayout = mLayout.get(); if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) { final OffsetMapping transformedText = (OffsetMapping) dynamicLayout.mDisplay; start = transformedText.originalToTransformed(start, OffsetMapping.MAP_STRATEGY_CHARACTER); end = transformedText.originalToTransformed(end, OffsetMapping.MAP_STRATEGY_CHARACTER); } reflow(s, start, end - start, end - start); } public void onSpanAdded(Spannable s, Object o, int start, int end) { if (o instanceof UpdateLayout) { transformAndReflow(s, start, end); } } public void onSpanRemoved(Spannable s, Object o, int start, int end) { if (o instanceof UpdateLayout) reflow(s, start, end - start, end - start); transformAndReflow(s, start, end); } public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) { Loading @@ -1123,12 +1161,13 @@ public class DynamicLayout extends Layout { // instead of causing an exception start = 0; } reflow(s, start, end - start, end - start); reflow(s, nstart, nend - nstart, nend - nstart); transformAndReflow(s, start, end); transformAndReflow(s, nstart, nend); } } private WeakReference<DynamicLayout> mLayout; private OffsetMapping.TextUpdate mTransformedTextUpdate; } @Override Loading
core/java/android/text/method/ArrowKeyMovementMethod.java +30 −0 Original line number Diff line number Diff line Loading @@ -68,6 +68,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override protected boolean left(TextView widget, Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendLeft(buffer, layout); Loading @@ -78,6 +81,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override protected boolean right(TextView widget, Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendRight(buffer, layout); Loading @@ -88,6 +94,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override protected boolean up(TextView widget, Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendUp(buffer, layout); Loading @@ -98,6 +107,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override protected boolean down(TextView widget, Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendDown(buffer, layout); Loading @@ -108,6 +120,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override protected boolean pageUp(TextView widget, Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); final boolean selecting = isSelecting(buffer); final int targetY = getCurrentLineTop(buffer, layout) - getPageHeight(widget); Loading @@ -132,6 +147,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override protected boolean pageDown(TextView widget, Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); final boolean selecting = isSelecting(buffer); final int targetY = getCurrentLineTop(buffer, layout) + getPageHeight(widget); Loading Loading @@ -176,6 +194,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override protected boolean lineStart(TextView widget, Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendToLeftEdge(buffer, layout); Loading @@ -186,6 +207,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override protected boolean lineEnd(TextView widget, Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendToRightEdge(buffer, layout); Loading Loading @@ -224,6 +248,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override public boolean previousParagraph(@NonNull TextView widget, @NonNull Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendToParagraphStart(buffer); Loading @@ -234,6 +261,9 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override public boolean nextParagraph(@NonNull TextView widget, @NonNull Spannable buffer) { if (widget.isOffsetMappingAvailable()) { return false; } final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendToParagraphEnd(buffer); Loading
core/java/android/text/method/BaseKeyListener.java +3 −2 Original line number Diff line number Diff line Loading @@ -440,8 +440,9 @@ public abstract class BaseKeyListener extends MetaKeyKeyListener private boolean deleteLine(View view, Editable content) { if (view instanceof TextView) { final Layout layout = ((TextView) view).getLayout(); if (layout != null) { final TextView textView = (TextView) view; final Layout layout = textView.getLayout(); if (layout != null && !textView.isOffsetMappingAvailable()) { final int line = layout.getLineForOffset(Selection.getSelectionStart(content)); final int start = layout.getLineStart(line); final int end = layout.getLineEnd(line); Loading
core/java/android/text/method/LinkMovementMethod.java +4 −0 Original line number Diff line number Diff line Loading @@ -100,6 +100,10 @@ public class LinkMovementMethod extends ScrollingMovementMethod { private boolean action(int what, TextView widget, Spannable buffer) { Layout layout = widget.getLayout(); if (widget.isOffsetMappingAvailable()) { // The text in the layout is transformed and has OffsetMapping, don't do anything. return false; } int padding = widget.getTotalPaddingTop() + widget.getTotalPaddingBottom(); Loading
core/java/android/text/method/OffsetMapping.java 0 → 100644 +170 −0 Original line number Diff line number Diff line /* * 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.IntDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * The interface for the index mapping information of a transformed text returned by * {@link TransformationMethod}. This class is mainly used to support the * {@link TransformationMethod} that alters the text length. * @hide */ public interface OffsetMapping { /** * The mapping strategy for a character offset. * * @see #originalToTransformed(int, int) * @see #transformedToOriginal(int, int) */ int MAP_STRATEGY_CHARACTER = 0; /** * The mapping strategy for a cursor position. * * @see #originalToTransformed(int, int) * @see #transformedToOriginal(int, int) */ int MAP_STRATEGY_CURSOR = 1; @IntDef(prefix = { "MAP_STRATEGY" }, value = { MAP_STRATEGY_CHARACTER, MAP_STRATEGY_CURSOR }) @Retention(RetentionPolicy.SOURCE) @interface MapStrategy {} /** * Map an offset at original text to the offset at transformed text. <br/> * * This function must be a monotonically non-decreasing function. In other words, if the offset * advances at the original text, the offset at the transformed text must advance or stay there. * <br/> * * Depending on the mapping strategy, a same offset can be mapped differently. For example, * <pre> * Original: ABCDE * Transformed: ABCXDE ('X' is introduced due to the transformation.) * </pre> * Let's check the offset 3, which is the offset of the character 'D'. * If we want to map the character offset 3, it should be mapped to index 4. * If we want to map the cursor offset 3 (the offset of the character before which the cursor is * placed), it's unclear if the mapped cursor is before 'X' or after 'X'. * This depends on how the transformed text reacts an insertion at offset 3 in the original * text. Assume character 'V' is insert at offset 3 in the original text, and the original text * become "ABCVDE". The transformed text can be: * <pre> * 1) "ABCVXDE" * or * 2) "ABCXVDE" * </pre> * In the first case, the offset 3 should be mapped to 3 (before 'X'). And in the second case, * the offset should be mapped to 4 (after 'X').<br/> * * In some cases, a character offset at the original text doesn't have a proper corresponding * offset at the transformed text. For example: * <pre> * Original: ABCDE * Transformed: ABDE ('C' is deleted due to the transformation.) * </pre> * The character offset 2 can be mapped either to offset 2 or 3, but neither is meaningful. For * convenience, it MUST map to the next offset (offset 3 in this case), or the * transformedText.length() if there is no valid character to map. * This is mandatory when the map strategy is {@link #MAP_STRATEGY_CHARACTER}, but doesn't * apply for other map strategies. * * @param offset the offset at the original text. It's normally equal to or less than the * originalText.length(). When {@link #MAP_STRATEGY_CHARACTER} is passed, it must * be less than originalText.length(). For convenience, it's also allowed to be * negative, which represents an invalid offset. When the given offset is * negative, this method should return it as it is. * @param strategy the mapping strategy. Depending on its value, the same offset can be mapped * differently. * @return the mapped offset at the transformed text, must be equal to or less than the * transformedText.length(). * * @see #transformedToOriginal(int, int) */ int originalToTransformed(int offset, @MapStrategy int strategy); /** * Map an offset at transformed text to the offset at original text. This is the reverse method * of {@link #originalToTransformed(int, int)}. <br/> * This function must be a monotonically non-decreasing function. In other words, if the offset * advances at the original text, the offset at the transformed text must advance or stay there. * <br/> * Similar to the {@link #originalToTransformed(int, int)} if a character offset doesn't have a * corresponding offset at the transformed text, it MUST return the value as the previous * offset. This is mandatory when the map strategy is {@link #MAP_STRATEGY_CHARACTER}, * but doesn't apply for other map strategies. * * @param offset the offset at the transformed text. It's normally equal to or less than the * transformedText.length(). When {@link #MAP_STRATEGY_CHARACTER} is passed, it * must be less than transformedText.length(). For convenience, it's also allowed * to be negative, which represents an invalid offset. When the given offset is * negative, this method should return it as it is. * @param strategy the mapping strategy. Depending on its value, the same offset can be mapped * differently. * @return the mapped offset at the original text, must be equal to or less than the * original.length(). * * @see #originalToTransformed(int, int) */ int transformedToOriginal(int offset, @MapStrategy int strategy); /** * Map a text update in the original text to an update the transformed text. * This method used to determine how the transformed text is updated in response to an * update in the original text. It is always called before the original text being changed. * * The main usage of this method is to update text layout incrementally. So it should report * the range where text needs to be laid out again. * * @param textUpdate the {@link TextUpdate} object containing the text update information on * the original text. The transformed text update information will also be * stored at this object. */ void originalToTransformed(TextUpdate textUpdate); /** * The class that stores the text update information that from index <code>where</code>, * <code>after</code> characters will replace the old text that has length <code>before</code>. */ class TextUpdate { /** The start index of the text update range, inclusive */ public int where; /** The length of the replaced old text. */ public int before; /** The length of the new text that replaces the old text. */ public int after; /** * Creates a {@link TextUpdate} object. * @param where the start index of the text update range. * @param before the length of the replaced old text. * @param after the length of the new text that replaces the old text. */ public TextUpdate(int where, int before, int after) { this.where = where; this.before = before; this.after = after; } } }