• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.text.method;
18 
19 import android.annotation.IntRange;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.graphics.Canvas;
23 import android.graphics.Paint;
24 import android.graphics.Rect;
25 import android.text.Editable;
26 import android.text.Spannable;
27 import android.text.SpannableString;
28 import android.text.Spanned;
29 import android.text.TextUtils;
30 import android.text.TextWatcher;
31 import android.text.style.ReplacementSpan;
32 import android.util.DisplayMetrics;
33 import android.util.MathUtils;
34 import android.util.TypedValue;
35 import android.view.View;
36 
37 import com.android.internal.util.ArrayUtils;
38 import com.android.internal.util.Preconditions;
39 import com.android.text.flags.Flags;
40 
41 import java.lang.reflect.Array;
42 
43 /**
44  * The transformation method used by handwriting insert mode.
45  * This transformation will insert a placeholder string to the original text at the given
46  * offset. And it also provides a highlight range for the newly inserted text and the placeholder
47  * text.
48  *
49  * For example,
50  *   original text: "Hello world"
51  *   insert mode is started at index:  5,
52  *   placeholder text: "\n\n"
53  * The transformed text will be: "Hello\n\n world", and the highlight range will be [5, 7)
54  * including the inserted placeholder text.
55  *
56  * If " abc" is inserted to the original text at index 5,
57  *   the new original text: "Hello abc world"
58  *   the new transformed text: "hello abc\n\n world", and the highlight range will be [5, 11).
59  * @hide
60  */
61 @android.ravenwood.annotation.RavenwoodKeepWholeClass
62 public class InsertModeTransformationMethod implements TransformationMethod, TextWatcher {
63     /** The start offset of the highlight range in the original text, inclusive. */
64     private int mStart;
65     /**
66      * The end offset of the highlight range in the original text, exclusive. The placeholder text
67      * is also inserted at this index.
68      */
69     private int mEnd;
70     /** The transformation method that's already set on the {@link android.widget.TextView}. */
71     private final TransformationMethod mOldTransformationMethod;
72     /** Whether the {@link android.widget.TextView} is single-lined. */
73     private final boolean mSingleLine;
74 
75     /**
76      * @param offset the original offset to start the insert mode. It must be in the range from 0
77      *               to the length of the transformed text.
78      * @param singleLine whether the text is single line.
79      * @param oldTransformationMethod the old transformation method at the
80      * {@link android.widget.TextView}. If it's not null, this {@link TransformationMethod} will
81      * first call {@link TransformationMethod#getTransformation(CharSequence, View)} on the old one,
82      * and then do the transformation for the insert mode.
83      *
84      */
InsertModeTransformationMethod(@ntRangefrom = 0) int offset, boolean singleLine, @NonNull TransformationMethod oldTransformationMethod)85     public InsertModeTransformationMethod(@IntRange(from = 0) int offset, boolean singleLine,
86             @NonNull TransformationMethod oldTransformationMethod) {
87         this(offset, offset, singleLine, oldTransformationMethod);
88     }
89 
InsertModeTransformationMethod(int start, int end, boolean singleLine, @NonNull TransformationMethod oldTransformationMethod)90     private InsertModeTransformationMethod(int start, int end, boolean singleLine,
91             @NonNull TransformationMethod oldTransformationMethod) {
92         mStart = start;
93         mEnd = end;
94         mSingleLine = singleLine;
95         mOldTransformationMethod = oldTransformationMethod;
96     }
97 
98     /**
99      * Create a new {@code InsertModeTransformation} with the given new inner
100      * {@code oldTransformationMethod} and the {@code singleLine} value. The returned
101      * {@link InsertModeTransformationMethod} will keep the highlight range.
102      *
103      * @param oldTransformationMethod the updated inner transformation method at the
104      * {@link android.widget.TextView}.
105      * @param singleLine the updated singleLine value.
106      * @return the new {@link InsertModeTransformationMethod} with the updated
107      * {@code oldTransformationMethod} and {@code singleLine} value.
108      */
update(TransformationMethod oldTransformationMethod, boolean singleLine)109     public InsertModeTransformationMethod update(TransformationMethod oldTransformationMethod,
110             boolean singleLine) {
111         return new InsertModeTransformationMethod(mStart, mEnd, singleLine,
112                 oldTransformationMethod);
113     }
114 
getOldTransformationMethod()115     public TransformationMethod getOldTransformationMethod() {
116         return mOldTransformationMethod;
117     }
118 
getPlaceholderText(View view)119     private CharSequence getPlaceholderText(View view) {
120         if (!mSingleLine) {
121             return  "\n\n";
122         }
123         final SpannableString singleLinePlaceholder = new SpannableString("\uFFFD");
124         final DisplayMetrics displayMetrics = view.getResources().getDisplayMetrics();
125         final int widthPx = (int) Math.ceil(
126                 TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 108, displayMetrics));
127 
128         singleLinePlaceholder.setSpan(new SingleLinePlaceholderSpan(widthPx), 0, 1,
129                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
130         return singleLinePlaceholder;
131     }
132 
133     @Override
getTransformation(CharSequence source, View view)134     public CharSequence getTransformation(CharSequence source, View view) {
135         final CharSequence charSequence;
136         if (mOldTransformationMethod != null) {
137             charSequence = mOldTransformationMethod.getTransformation(source, view);
138             if (source instanceof Spannable) {
139                 final Spannable spannable = (Spannable) source;
140                 spannable.setSpan(mOldTransformationMethod, 0, spannable.length(),
141                         Spanned.SPAN_INCLUSIVE_INCLUSIVE);
142             }
143         } else {
144             charSequence = source;
145         }
146 
147         final CharSequence placeholderText = getPlaceholderText(view);
148         return new TransformedText(charSequence, placeholderText);
149     }
150 
151     @Override
onFocusChanged(View view, CharSequence sourceText, boolean focused, int direction, Rect previouslyFocusedRect)152     public void onFocusChanged(View view, CharSequence sourceText, boolean focused, int direction,
153             Rect previouslyFocusedRect) {
154         if (mOldTransformationMethod != null) {
155             mOldTransformationMethod.onFocusChanged(view, sourceText, focused, direction,
156                     previouslyFocusedRect);
157         }
158     }
159 
160     @Override
beforeTextChanged(CharSequence s, int start, int count, int after)161     public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
162 
163     @Override
onTextChanged(CharSequence s, int start, int before, int count)164     public void onTextChanged(CharSequence s, int start, int before, int count) {
165         // The text change is after the offset where placeholder is inserted, return.
166         if (start > mEnd) return;
167         final int diff = count - before;
168 
169         // Note: If start == mStart and before == 0, the change is also considered after the
170         // highlight start. It won't modify the mStart in this case.
171         if (start < mStart) {
172             if (start + before <= mStart) {
173                 // The text change is before the highlight start, move the highlight start.
174                 mStart += diff;
175             } else {
176                 if (Flags.insertModeHighlightRange()) {
177                     // The text change covers the highlight start. Don't change the start except
178                     // when it's out of range.
179                     mStart = Math.min(mStart, s.length());
180                 } else {
181                     // The text change covers the highlight start. Extend the highlight start to the
182                     // change start. This should be a rare case.
183                     mStart = start;
184                 }
185             }
186         }
187 
188         if (start + before <= mEnd) {
189             // The text change is before the highlight end, move the highlight end.
190             mEnd += diff;
191         } else if (start < mEnd) {
192             if (Flags.insertModeHighlightRange()) {
193                 // The text change covers the highlight end. Don't change the end except when it's
194                 // out of range.
195                 mEnd = Math.min(mEnd, s.length());
196             } else {
197                 // The text change covers the highlight end. Extend the highlight end to the
198                 // change end. This should be a rare case.
199                 mEnd = start + count;
200             }
201         }
202     }
203 
204     @Override
afterTextChanged(Editable s)205     public void afterTextChanged(Editable s) { }
206 
207     /**
208      * The transformed text returned by the {@link InsertModeTransformationMethod}.
209      */
210     public class TransformedText implements OffsetMapping, Spanned {
211         private final CharSequence mOriginal;
212         private final CharSequence mPlaceholder;
213         private final Spanned mSpannedOriginal;
214         private final Spanned mSpannedPlaceholder;
215 
TransformedText(CharSequence original, CharSequence placeholder)216         TransformedText(CharSequence original, CharSequence placeholder) {
217             mOriginal = original;
218             if (original instanceof Spanned) {
219                 mSpannedOriginal = (Spanned) original;
220             } else {
221                 mSpannedOriginal = null;
222             }
223             mPlaceholder = placeholder;
224             if (placeholder instanceof Spanned) {
225                 mSpannedPlaceholder = (Spanned) placeholder;
226             } else {
227                 mSpannedPlaceholder = null;
228             }
229         }
230 
231         @Override
originalToTransformed(int offset, int strategy)232         public int originalToTransformed(int offset, int strategy) {
233             if (offset < 0) return offset;
234             Preconditions.checkArgumentInRange(offset, 0, mOriginal.length(), "offset");
235             if (offset == mEnd && strategy == OffsetMapping.MAP_STRATEGY_CURSOR) {
236                 // The offset equals to mEnd. For a cursor position it's considered before the
237                 // inserted placeholder text.
238                 return offset;
239             }
240             if (offset < mEnd) {
241                 return offset;
242             }
243             return offset + mPlaceholder.length();
244         }
245 
246         @Override
transformedToOriginal(int offset, int strategy)247         public int transformedToOriginal(int offset, int strategy) {
248             if (offset < 0) return offset;
249             Preconditions.checkArgumentInRange(offset, 0, length(), "offset");
250 
251             // The placeholder text is inserted at mEnd. Because the offset is smaller than
252             // mEnd, we can directly return it.
253             if (offset < mEnd) return offset;
254             if (offset < mEnd + mPlaceholder.length()) {
255                 return mEnd;
256             }
257             return offset - mPlaceholder.length();
258         }
259 
260         @Override
originalToTransformed(TextUpdate textUpdate)261         public void originalToTransformed(TextUpdate textUpdate) {
262             if (textUpdate.where > mEnd) {
263                 textUpdate.where += mPlaceholder.length();
264             } else if (textUpdate.where + textUpdate.before > mEnd) {
265                 // The update also covers the placeholder string.
266                 textUpdate.before += mPlaceholder.length();
267                 textUpdate.after += mPlaceholder.length();
268             }
269         }
270 
271         @Override
length()272         public int length() {
273             return mOriginal.length() + mPlaceholder.length();
274         }
275 
276         @Override
charAt(int index)277         public char charAt(int index) {
278             Preconditions.checkArgumentInRange(index, 0, length() - 1, "index");
279             if (index < mEnd) {
280                 return mOriginal.charAt(index);
281             }
282             if (index < mEnd + mPlaceholder.length()) {
283                 return mPlaceholder.charAt(index - mEnd);
284             }
285             return mOriginal.charAt(index - mPlaceholder.length());
286         }
287 
288         @Override
subSequence(int start, int end)289         public CharSequence subSequence(int start, int end) {
290             if (end < start || start < 0 || end > length()) {
291                 throw new IndexOutOfBoundsException();
292             }
293             if (start == end) {
294                 return "";
295             }
296 
297             final int placeholderLength = mPlaceholder.length();
298 
299             final int seg1Start = Math.min(start, mEnd);
300             final int seg1End = Math.min(end, mEnd);
301 
302             final int seg2Start = MathUtils.constrain(start - mEnd, 0, placeholderLength);
303             final int seg2End = MathUtils.constrain(end - mEnd, 0, placeholderLength);
304 
305             final int seg3Start = Math.max(start - placeholderLength, mEnd);
306             final int seg3End = Math.max(end - placeholderLength, mEnd);
307 
308             return TextUtils.concat(
309                     mOriginal.subSequence(seg1Start, seg1End),
310                     mPlaceholder.subSequence(seg2Start, seg2End),
311                     mOriginal.subSequence(seg3Start, seg3End));
312         }
313 
314         @Override
toString()315         public String toString() {
316             return String.valueOf(mOriginal.subSequence(0, mEnd))
317                     + mPlaceholder
318                     + mOriginal.subSequence(mEnd, mOriginal.length());
319         }
320 
321         @Override
322         @SuppressWarnings("unchecked")
getSpans(int start, int end, Class<T> type)323         public <T> T[] getSpans(int start, int end, Class<T> type) {
324             if (end < start) {
325                 return ArrayUtils.emptyArray(type);
326             }
327 
328             T[] spansOriginal = null;
329             if (mSpannedOriginal != null) {
330                 final int originalStart =
331                         transformedToOriginal(start, OffsetMapping.MAP_STRATEGY_CURSOR);
332                 final int originalEnd =
333                         transformedToOriginal(end, OffsetMapping.MAP_STRATEGY_CURSOR);
334                 // We can't simply call SpannedString.getSpans(originalStart, originalEnd) here.
335                 // When start == end SpannedString.getSpans returns spans whose spanEnd == start.
336                 // For example,
337                 //   text: abcd  span: [1, 3)
338                 // getSpan(3, 3) will return the span [1, 3) but getSpan(3, 4) returns no span.
339                 //
340                 // This creates some special cases when originalStart == originalEnd.
341                 // For example:
342                 //   original text: abcd    span1: [1, 3) span2: [3, 4) span3: [3, 3)
343                 //   transformed text: abc\n\nd    span1: [1, 3) span2: [5, 6) span3: [3, 3)
344                 // Case 1:
345                 // When start = 3 and end = 4, transformedText#getSpan(3, 4) should return span3.
346                 // However, because originalStart == originalEnd == 3, originalText#getSpan(3, 3)
347                 // returns span1, span2 and span3.
348                 //
349                 // Case 2:
350                 // When start == end == 4, transformedText#getSpan(4, 4) should return nothing.
351                 // However, because originalStart == originalEnd == 3, originalText#getSpan(3, 3)
352                 // return span1, span2 and span3.
353                 //
354                 // Case 3:
355                 // When start == end == 5, transformedText#getSpan(5, 5) should return span2.
356                 // However, because originalStart == originalEnd == 3, originalText#getSpan(3, 3)
357                 // return span1,  span2 and span3.
358                 //
359                 // To handle the issue, we need to filter out the invalid spans.
360                 spansOriginal = mSpannedOriginal.getSpans(originalStart, originalEnd, type);
361                 spansOriginal = ArrayUtils.filter(spansOriginal,
362                         size -> (T[]) Array.newInstance(type, size),
363                         span -> intersect(getSpanStart(span), getSpanEnd(span), start, end));
364             }
365 
366             T[] spansPlaceholder = null;
367             if (mSpannedPlaceholder != null
368                     && intersect(start, end, mEnd, mEnd + mPlaceholder.length())) {
369                 int placeholderStart = Math.max(start - mEnd, 0);
370                 int placeholderEnd = Math.min(end - mEnd, mPlaceholder.length());
371                 spansPlaceholder =
372                         mSpannedPlaceholder.getSpans(placeholderStart, placeholderEnd, type);
373             }
374 
375             // TODO: sort the spans based on their priority.
376             return ArrayUtils.concat(type, spansOriginal, spansPlaceholder);
377         }
378 
379         @Override
getSpanStart(Object tag)380         public int getSpanStart(Object tag) {
381             if (mSpannedOriginal != null) {
382                 final int index = mSpannedOriginal.getSpanStart(tag);
383                 if (index >= 0) {
384                     // When originalSpanStart == originalSpanEnd == mEnd, the span should be
385                     // considered "before" the placeholder text. So we return the originalSpanStart.
386                     if (index < mEnd
387                             || (index == mEnd && mSpannedOriginal.getSpanEnd(tag) == index)) {
388                         return index;
389                     }
390                     return index + mPlaceholder.length();
391                 }
392             }
393 
394             // The span is not on original text, try find it on the placeholder.
395             if (mSpannedPlaceholder != null) {
396                 final int index = mSpannedPlaceholder.getSpanStart(tag);
397                 if (index >= 0) {
398                     // Find the span on placeholder, transform it and return.
399                     return index + mEnd;
400                 }
401             }
402             return -1;
403         }
404 
405         @Override
getSpanEnd(Object tag)406         public int getSpanEnd(Object tag) {
407             if (mSpannedOriginal != null) {
408                 final int index = mSpannedOriginal.getSpanEnd(tag);
409                 if (index >= 0) {
410                     if (index <= mEnd) {
411                         return index;
412                     }
413                     return index + mPlaceholder.length();
414                 }
415             }
416 
417             // The span is not on original text, try find it on the placeholder.
418             if (mSpannedPlaceholder != null) {
419                 final int index = mSpannedPlaceholder.getSpanEnd(tag);
420                 if (index >= 0) {
421                     // Find the span on placeholder, transform it and return.
422                     return index + mEnd;
423                 }
424             }
425             return -1;
426         }
427 
428         @Override
getSpanFlags(Object tag)429         public int getSpanFlags(Object tag) {
430             if (mSpannedOriginal != null) {
431                 final int flags = mSpannedOriginal.getSpanFlags(tag);
432                 if (flags != 0) {
433                     return flags;
434                 }
435             }
436             if (mSpannedPlaceholder != null) {
437                 return mSpannedPlaceholder.getSpanFlags(tag);
438             }
439             return 0;
440         }
441 
442         @Override
nextSpanTransition(int start, int limit, Class type)443         public int nextSpanTransition(int start, int limit, Class type) {
444             if (limit <= start) return limit;
445             final Object[] spans = getSpans(start, limit, type);
446             for (int i = 0; i < spans.length; ++i) {
447                 int spanStart = getSpanStart(spans[i]);
448                 int spanEnd = getSpanEnd(spans[i]);
449                 if (start < spanStart && spanStart < limit) {
450                     limit = spanStart;
451                 }
452                 if (start < spanEnd && spanEnd < limit) {
453                     limit = spanEnd;
454                 }
455             }
456             return limit;
457         }
458 
459         /**
460          * Return the start index of the highlight range for the insert mode, inclusive.
461          */
getHighlightStart()462         public int getHighlightStart() {
463             return mStart;
464         }
465 
466         /**
467          * Return the end index of the highlight range for the insert mode, exclusive.
468          */
getHighlightEnd()469         public int getHighlightEnd() {
470             return mEnd + mPlaceholder.length();
471         }
472     }
473 
474     /**
475      * The placeholder span used for single line
476      */
477     public static class SingleLinePlaceholderSpan extends ReplacementSpan {
478         private final int mWidth;
SingleLinePlaceholderSpan(int width)479         SingleLinePlaceholderSpan(int width) {
480             mWidth = width;
481         }
482         @Override
getSize(@onNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm)483         public int getSize(@NonNull Paint paint, CharSequence text, int start, int end,
484                 @Nullable Paint.FontMetricsInt fm) {
485             return mWidth;
486         }
487 
488         @Override
draw(@onNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint)489         public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x,
490                 int top, int y, int bottom, @NonNull Paint paint) { }
491     }
492 
493     /**
494      * Return true if the given two ranges intersects. This logic is the same one used in
495      * {@link Spanned} to determine whether a span range intersect with the query range.
496      */
intersect(int s1, int e1, int s2, int e2)497     private static boolean intersect(int s1, int e1, int s2, int e2) {
498         if (s1 > e2) return false;
499         if (e1 < s2) return false;
500         if (s1 != e1 && s2 != e2) {
501             if (s1 == e2) return false;
502             if (e1 == s2) return false;
503         }
504         return true;
505     }
506 }
507