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