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