• 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.style;
18 
19 import android.annotation.IntRange;
20 import android.annotation.NonNull;
21 import android.graphics.Typeface;
22 import android.text.Spannable;
23 import android.text.Spanned;
24 import android.util.LongArray;
25 
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.List;
29 
30 /**
31  * @hide
32  */
33 @android.ravenwood.annotation.RavenwoodKeepWholeClass
34 public class SpanUtils {
SpanUtils()35     private SpanUtils() {}  // Do not instantiate
36 
37     /**
38      * Toggle the bold state of the given range.
39      *
40      * If there is at least one character is not bold in the given range, make the entire region to
41      * be bold. If all characters of the given range is already bolded, this method removes bold
42      * style from the given selection.
43      *
44      * @param spannable a spannable string
45      * @param min minimum inclusive index of the selection.
46      * @param max maximum exclusive index of the selection.
47      * @return true if the selected region is toggled.
48      */
toggleBold(@onNull Spannable spannable, @IntRange(from = 0) int min, @IntRange(from = 0) int max)49     public static boolean toggleBold(@NonNull Spannable spannable,
50             @IntRange(from = 0) int min, @IntRange(from = 0) int max) {
51 
52         if (min == max) {
53             return false;
54         }
55 
56         final StyleSpan[] boldSpans = spannable.getSpans(min, max, StyleSpan.class);
57         final ArrayList<StyleSpan> filteredBoldSpans = new ArrayList<>();
58         for (StyleSpan span : boldSpans) {
59             if ((span.getStyle() & Typeface.BOLD) == Typeface.BOLD) {
60                 filteredBoldSpans.add(span);
61             }
62         }
63 
64         if (!isCovered(spannable, filteredBoldSpans, min, max)) {
65             // At least one character doesn't have bold style. Making given region bold.
66             spannable.setSpan(
67                     new StyleSpan(Typeface.BOLD), min, max, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
68             return true;
69         }
70 
71         // Span covers the entire selection. Removing spans from tha region.
72         for (int si = 0; si < filteredBoldSpans.size(); ++si) {
73             final StyleSpan span = filteredBoldSpans.get(si);
74             final int start = spannable.getSpanStart(span);
75             final int end = spannable.getSpanEnd(span);
76             final int flag = spannable.getSpanFlags(span);
77 
78             // If BOLD_ITALIC style is attached, need to set ITALIC span to the subtracted range.
79             final boolean needItalicSpan = (span.getStyle() & Typeface.ITALIC) == Typeface.ITALIC;
80 
81             if (start < min) {
82                 if (end > max) {
83                     // selection: ------------|===================|----------------
84                     //      span:     <-------------------------------->
85                     //    result:     <------->                   <---->
86                     spannable.setSpan(span, start, min, flag);
87                     spannable.setSpan(new StyleSpan(span.getStyle()), max, end, flag);
88                     if (needItalicSpan) {
89                         spannable.setSpan(new StyleSpan(Typeface.ITALIC), min, max, flag);
90                     }
91                 } else {
92                     // selection: ------------|===================|----------------
93                     //      span:     <----------->
94                     //    result:     <------->
95                     spannable.setSpan(span, start, min, flag);
96                     if (needItalicSpan) {
97                         spannable.setSpan(new StyleSpan(Typeface.ITALIC), min, end, flag);
98                     }
99                 }
100             } else {
101                 if (end > max) {
102                     // selection: ------------|===================|----------------
103                     //      span:                     <------------------------>
104                     //    result:                                 <------------>
105                     spannable.setSpan(span, max, end, flag);
106                     if (needItalicSpan) {
107                         spannable.setSpan(new StyleSpan(Typeface.ITALIC), max, end, flag);
108                     }
109                 } else {
110                     // selection: ------------|===================|----------------
111                     //      span:                 <----------->
112                     //    result:
113                     spannable.removeSpan(span);
114                     if (needItalicSpan) {
115                         spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, flag);
116                     }
117                 }
118             }
119         }
120         return true;
121     }
122 
123     /**
124      * Toggle the italic state of the given range.
125      *
126      * If there is at least one character is not italic in the given range, make the entire region
127      * to be italic. If all characters of the given range is already italic, this method removes
128      * italic style from the given selection.
129      *
130      * @param spannable a spannable string
131      * @param min minimum inclusive index of the selection.
132      * @param max maximum exclusive index of the selection.
133      * @return true if the selected region is toggled.
134      */
toggleItalic(@onNull Spannable spannable, @IntRange(from = 0) int min, @IntRange(from = 0) int max)135     public static boolean toggleItalic(@NonNull Spannable spannable,
136             @IntRange(from = 0) int min, @IntRange(from = 0) int max) {
137 
138         if (min == max) {
139             return false;
140         }
141 
142         final StyleSpan[] boldSpans = spannable.getSpans(min, max, StyleSpan.class);
143         final ArrayList<StyleSpan> filteredBoldSpans = new ArrayList<>();
144         for (StyleSpan span : boldSpans) {
145             if ((span.getStyle() & Typeface.ITALIC) == Typeface.ITALIC) {
146                 filteredBoldSpans.add(span);
147             }
148         }
149 
150         if (!isCovered(spannable, filteredBoldSpans, min, max)) {
151             // At least one character doesn't have italic style. Making given region italic.
152             spannable.setSpan(
153                     new StyleSpan(Typeface.ITALIC), min, max, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
154             return true;
155         }
156 
157         // Span covers the entire selection. Removing spans from tha region.
158         for (int si = 0; si < filteredBoldSpans.size(); ++si) {
159             final StyleSpan span = filteredBoldSpans.get(si);
160             final int start = spannable.getSpanStart(span);
161             final int end = spannable.getSpanEnd(span);
162             final int flag = spannable.getSpanFlags(span);
163 
164             // If BOLD_ITALIC style is attached, need to set BOLD span to the subtracted range.
165             final boolean needBoldSpan = (span.getStyle() & Typeface.BOLD) == Typeface.BOLD;
166 
167             if (start < min) {
168                 if (end > max) {
169                     // selection: ------------|===================|----------------
170                     //      span:     <-------------------------------->
171                     //    result:     <------->                   <---->
172                     spannable.setSpan(span, start, min, flag);
173                     spannable.setSpan(new StyleSpan(span.getStyle()), max, end, flag);
174                     if (needBoldSpan) {
175                         spannable.setSpan(new StyleSpan(Typeface.BOLD), min, max, flag);
176                     }
177                 } else {
178                     // selection: ------------|===================|----------------
179                     //      span:     <----------->
180                     //    result:     <------->
181                     spannable.setSpan(span, start, min, flag);
182                     if (needBoldSpan) {
183                         spannable.setSpan(new StyleSpan(Typeface.BOLD), min, end, flag);
184                     }
185                 }
186             } else {
187                 if (end > max) {
188                     // selection: ------------|===================|----------------
189                     //      span:                     <------------------------>
190                     //    result:                                 <------------>
191                     spannable.setSpan(span, max, end, flag);
192                     if (needBoldSpan) {
193                         spannable.setSpan(new StyleSpan(Typeface.BOLD), max, end, flag);
194                     }
195                 } else {
196                     // selection: ------------|===================|----------------
197                     //      span:                 <----------->
198                     //    result:
199                     spannable.removeSpan(span);
200                     if (needBoldSpan) {
201                         spannable.setSpan(new StyleSpan(Typeface.BOLD), start, end, flag);
202                     }
203                 }
204             }
205         }
206         return true;
207     }
208 
209     /**
210      * Toggle the underline state of the given range.
211      *
212      * If there is at least one character is not underlined in the given range, make the entire
213      * region to underlined. If all characters of the given range is already underlined, this
214      * method removes underline from the given selection.
215      *
216      * @param spannable a spannable string
217      * @param min minimum inclusive index of the selection.
218      * @param max maximum exclusive index of the selection.
219      * @return true if the selected region is toggled.
220      */
toggleUnderline(@onNull Spannable spannable, @IntRange(from = 0) int min, @IntRange(from = 0) int max)221     public static boolean toggleUnderline(@NonNull Spannable spannable,
222             @IntRange(from = 0) int min, @IntRange(from = 0) int max) {
223 
224         if (min == max) {
225             return false;
226         }
227 
228         final List<UnderlineSpan> spans =
229                 Arrays.asList(spannable.getSpans(min, max, UnderlineSpan.class));
230 
231         if (!isCovered(spannable, spans, min, max)) {
232             // At least one character doesn't have underline style. Making given region underline.
233             spannable.setSpan(new UnderlineSpan(), min, max, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
234             return true;
235         }
236         // Span covers the entire selection. Removing spans from tha region.
237         for (int si = 0; si < spans.size(); ++si) {
238             final UnderlineSpan span = spans.get(si);
239             final int start = spannable.getSpanStart(span);
240             final int end = spannable.getSpanEnd(span);
241             final int flag = spannable.getSpanFlags(span);
242 
243             if (start < min) {
244                 if (end > max) {
245                     // selection: ------------|===================|----------------
246                     //      span:     <-------------------------------->
247                     //    result:     <------->                   <---->
248                     spannable.setSpan(span, start, min, flag);
249                     spannable.setSpan(new UnderlineSpan(), max, end, flag);
250                 } else {
251                     // selection: ------------|===================|----------------
252                     //      span:     <----------->
253                     //    result:     <------->
254                     spannable.setSpan(span, start, min, flag);
255                 }
256             } else {
257                 if (end > max) {
258                     // selection: ------------|===================|----------------
259                     //      span:                     <------------------------>
260                     //    result:                                 <------------>
261                     spannable.setSpan(span, max, end, flag);
262                 } else {
263                     // selection: ------------|===================|----------------
264                     //      span:                 <----------->
265                     //    result:
266                     spannable.removeSpan(span);
267                 }
268             }
269         }
270         return true;
271     }
272 
pack(int from, int to)273     private static long pack(int from, int to) {
274         return ((long) from) << 32 | (long) to;
275     }
276 
min(long packed)277     private static int min(long packed) {
278         return (int) (packed >> 32);
279     }
280 
max(long packed)281     private static int max(long packed) {
282         return (int) (packed & 0xFFFFFFFFL);
283     }
284 
hasIntersection(int aMin, int aMax, int bMin, int bMax)285     private static boolean hasIntersection(int aMin, int aMax, int bMin, int bMax) {
286         return aMin < bMax && bMin < aMax;
287     }
288 
intersection(int aMin, int aMax, int bMin, int bMax)289     private static long intersection(int aMin, int aMax, int bMin, int bMax) {
290         return pack(Math.max(aMin, bMin), Math.min(aMax, bMax));
291     }
292 
isCovered(@onNull Spannable spannable, @NonNull List<T> spans, @IntRange(from = 0) int min, @IntRange(from = 0) int max)293     private static <T> boolean isCovered(@NonNull Spannable spannable, @NonNull List<T> spans,
294             @IntRange(from = 0) int min, @IntRange(from = 0) int max) {
295 
296         if (min == max) {
297             return false;
298         }
299 
300         LongArray uncoveredRanges = new LongArray();
301         LongArray nextUncoveredRanges = new LongArray();
302 
303         uncoveredRanges.add(pack(min, max));
304 
305         for (int si = 0; si < spans.size(); ++si) {
306             final T span = spans.get(si);
307             final int start = spannable.getSpanStart(span);
308             final int end = spannable.getSpanEnd(span);
309 
310             for (int i = 0; i < uncoveredRanges.size(); ++i) {
311                 final long packed = uncoveredRanges.get(i);
312                 final int uncoveredStart = min(packed);
313                 final int uncoveredEnd = max(packed);
314 
315                 if (!hasIntersection(start, end, uncoveredStart, uncoveredEnd)) {
316                     // This span doesn't affect this uncovered range. Try next span.
317                     nextUncoveredRanges.add(packed);
318                 } else {
319                     // This span has an intersection with uncovered range. Update the uncovered
320                     // range.
321                     long intersectionPack = intersection(start, end, uncoveredStart, uncoveredEnd);
322                     int intersectStart = min(intersectionPack);
323                     int intersectEnd = max(intersectionPack);
324 
325                     // Uncovered Range           : ----------|=======================|-------------
326                     //    Intersection           :                 <---------->
327                     // Remaining uncovered ranges: ----------|=====|----------|======|-------------
328                     if (uncoveredStart != intersectStart) {
329                         // There is still uncovered area on the left.
330                         nextUncoveredRanges.add(pack(uncoveredStart, intersectStart));
331                     }
332                     if (intersectEnd != uncoveredEnd) {
333                         // There is still uncovered area on the right.
334                         nextUncoveredRanges.add(pack(intersectEnd, uncoveredEnd));
335                     }
336                 }
337             }
338 
339             if (nextUncoveredRanges.size() == 0) {
340                 return true;
341             }
342 
343             // Swap the uncoveredRanges and nextUncoveredRanges and clear the next one.
344             final LongArray tmp = nextUncoveredRanges;
345             nextUncoveredRanges = uncoveredRanges;
346             uncoveredRanges = tmp;
347             nextUncoveredRanges.clear();
348         }
349 
350         return false;
351     }
352 }
353