1 /* 2 * Copyright (C) 2006 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; 18 19 import android.annotation.FloatRange; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.PluralsRes; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.icu.lang.UCharacter; 26 import android.icu.text.CaseMap; 27 import android.icu.text.Edits; 28 import android.icu.util.ULocale; 29 import android.os.Parcel; 30 import android.os.Parcelable; 31 import android.os.SystemProperties; 32 import android.provider.Settings; 33 import android.text.style.AbsoluteSizeSpan; 34 import android.text.style.AccessibilityClickableSpan; 35 import android.text.style.AccessibilityURLSpan; 36 import android.text.style.AlignmentSpan; 37 import android.text.style.BackgroundColorSpan; 38 import android.text.style.BulletSpan; 39 import android.text.style.CharacterStyle; 40 import android.text.style.EasyEditSpan; 41 import android.text.style.ForegroundColorSpan; 42 import android.text.style.LeadingMarginSpan; 43 import android.text.style.LocaleSpan; 44 import android.text.style.MetricAffectingSpan; 45 import android.text.style.ParagraphStyle; 46 import android.text.style.QuoteSpan; 47 import android.text.style.RelativeSizeSpan; 48 import android.text.style.ReplacementSpan; 49 import android.text.style.ScaleXSpan; 50 import android.text.style.SpellCheckSpan; 51 import android.text.style.StrikethroughSpan; 52 import android.text.style.StyleSpan; 53 import android.text.style.SubscriptSpan; 54 import android.text.style.SuggestionRangeSpan; 55 import android.text.style.SuggestionSpan; 56 import android.text.style.SuperscriptSpan; 57 import android.text.style.TextAppearanceSpan; 58 import android.text.style.TtsSpan; 59 import android.text.style.TypefaceSpan; 60 import android.text.style.URLSpan; 61 import android.text.style.UnderlineSpan; 62 import android.text.style.UpdateAppearance; 63 import android.util.Log; 64 import android.util.Printer; 65 import android.view.View; 66 67 import com.android.internal.R; 68 import com.android.internal.util.ArrayUtils; 69 import com.android.internal.util.Preconditions; 70 71 import java.lang.reflect.Array; 72 import java.util.Iterator; 73 import java.util.List; 74 import java.util.Locale; 75 import java.util.regex.Pattern; 76 77 public class TextUtils { 78 private static final String TAG = "TextUtils"; 79 80 /* package */ static final char[] ELLIPSIS_NORMAL = { '\u2026' }; // this is "..." 81 /** {@hide} */ 82 public static final String ELLIPSIS_STRING = new String(ELLIPSIS_NORMAL); 83 84 /* package */ static final char[] ELLIPSIS_TWO_DOTS = { '\u2025' }; // this is ".." 85 private static final String ELLIPSIS_TWO_DOTS_STRING = new String(ELLIPSIS_TWO_DOTS); 86 TextUtils()87 private TextUtils() { /* cannot be instantiated */ } 88 getChars(CharSequence s, int start, int end, char[] dest, int destoff)89 public static void getChars(CharSequence s, int start, int end, 90 char[] dest, int destoff) { 91 Class<? extends CharSequence> c = s.getClass(); 92 93 if (c == String.class) 94 ((String) s).getChars(start, end, dest, destoff); 95 else if (c == StringBuffer.class) 96 ((StringBuffer) s).getChars(start, end, dest, destoff); 97 else if (c == StringBuilder.class) 98 ((StringBuilder) s).getChars(start, end, dest, destoff); 99 else if (s instanceof GetChars) 100 ((GetChars) s).getChars(start, end, dest, destoff); 101 else { 102 for (int i = start; i < end; i++) 103 dest[destoff++] = s.charAt(i); 104 } 105 } 106 indexOf(CharSequence s, char ch)107 public static int indexOf(CharSequence s, char ch) { 108 return indexOf(s, ch, 0); 109 } 110 indexOf(CharSequence s, char ch, int start)111 public static int indexOf(CharSequence s, char ch, int start) { 112 Class<? extends CharSequence> c = s.getClass(); 113 114 if (c == String.class) 115 return ((String) s).indexOf(ch, start); 116 117 return indexOf(s, ch, start, s.length()); 118 } 119 indexOf(CharSequence s, char ch, int start, int end)120 public static int indexOf(CharSequence s, char ch, int start, int end) { 121 Class<? extends CharSequence> c = s.getClass(); 122 123 if (s instanceof GetChars || c == StringBuffer.class || 124 c == StringBuilder.class || c == String.class) { 125 final int INDEX_INCREMENT = 500; 126 char[] temp = obtain(INDEX_INCREMENT); 127 128 while (start < end) { 129 int segend = start + INDEX_INCREMENT; 130 if (segend > end) 131 segend = end; 132 133 getChars(s, start, segend, temp, 0); 134 135 int count = segend - start; 136 for (int i = 0; i < count; i++) { 137 if (temp[i] == ch) { 138 recycle(temp); 139 return i + start; 140 } 141 } 142 143 start = segend; 144 } 145 146 recycle(temp); 147 return -1; 148 } 149 150 for (int i = start; i < end; i++) 151 if (s.charAt(i) == ch) 152 return i; 153 154 return -1; 155 } 156 lastIndexOf(CharSequence s, char ch)157 public static int lastIndexOf(CharSequence s, char ch) { 158 return lastIndexOf(s, ch, s.length() - 1); 159 } 160 lastIndexOf(CharSequence s, char ch, int last)161 public static int lastIndexOf(CharSequence s, char ch, int last) { 162 Class<? extends CharSequence> c = s.getClass(); 163 164 if (c == String.class) 165 return ((String) s).lastIndexOf(ch, last); 166 167 return lastIndexOf(s, ch, 0, last); 168 } 169 lastIndexOf(CharSequence s, char ch, int start, int last)170 public static int lastIndexOf(CharSequence s, char ch, 171 int start, int last) { 172 if (last < 0) 173 return -1; 174 if (last >= s.length()) 175 last = s.length() - 1; 176 177 int end = last + 1; 178 179 Class<? extends CharSequence> c = s.getClass(); 180 181 if (s instanceof GetChars || c == StringBuffer.class || 182 c == StringBuilder.class || c == String.class) { 183 final int INDEX_INCREMENT = 500; 184 char[] temp = obtain(INDEX_INCREMENT); 185 186 while (start < end) { 187 int segstart = end - INDEX_INCREMENT; 188 if (segstart < start) 189 segstart = start; 190 191 getChars(s, segstart, end, temp, 0); 192 193 int count = end - segstart; 194 for (int i = count - 1; i >= 0; i--) { 195 if (temp[i] == ch) { 196 recycle(temp); 197 return i + segstart; 198 } 199 } 200 201 end = segstart; 202 } 203 204 recycle(temp); 205 return -1; 206 } 207 208 for (int i = end - 1; i >= start; i--) 209 if (s.charAt(i) == ch) 210 return i; 211 212 return -1; 213 } 214 indexOf(CharSequence s, CharSequence needle)215 public static int indexOf(CharSequence s, CharSequence needle) { 216 return indexOf(s, needle, 0, s.length()); 217 } 218 indexOf(CharSequence s, CharSequence needle, int start)219 public static int indexOf(CharSequence s, CharSequence needle, int start) { 220 return indexOf(s, needle, start, s.length()); 221 } 222 indexOf(CharSequence s, CharSequence needle, int start, int end)223 public static int indexOf(CharSequence s, CharSequence needle, 224 int start, int end) { 225 int nlen = needle.length(); 226 if (nlen == 0) 227 return start; 228 229 char c = needle.charAt(0); 230 231 for (;;) { 232 start = indexOf(s, c, start); 233 if (start > end - nlen) { 234 break; 235 } 236 237 if (start < 0) { 238 return -1; 239 } 240 241 if (regionMatches(s, start, needle, 0, nlen)) { 242 return start; 243 } 244 245 start++; 246 } 247 return -1; 248 } 249 regionMatches(CharSequence one, int toffset, CharSequence two, int ooffset, int len)250 public static boolean regionMatches(CharSequence one, int toffset, 251 CharSequence two, int ooffset, 252 int len) { 253 int tempLen = 2 * len; 254 if (tempLen < len) { 255 // Integer overflow; len is unreasonably large 256 throw new IndexOutOfBoundsException(); 257 } 258 char[] temp = obtain(tempLen); 259 260 getChars(one, toffset, toffset + len, temp, 0); 261 getChars(two, ooffset, ooffset + len, temp, len); 262 263 boolean match = true; 264 for (int i = 0; i < len; i++) { 265 if (temp[i] != temp[i + len]) { 266 match = false; 267 break; 268 } 269 } 270 271 recycle(temp); 272 return match; 273 } 274 275 /** 276 * Create a new String object containing the given range of characters 277 * from the source string. This is different than simply calling 278 * {@link CharSequence#subSequence(int, int) CharSequence.subSequence} 279 * in that it does not preserve any style runs in the source sequence, 280 * allowing a more efficient implementation. 281 */ substring(CharSequence source, int start, int end)282 public static String substring(CharSequence source, int start, int end) { 283 if (source instanceof String) 284 return ((String) source).substring(start, end); 285 if (source instanceof StringBuilder) 286 return ((StringBuilder) source).substring(start, end); 287 if (source instanceof StringBuffer) 288 return ((StringBuffer) source).substring(start, end); 289 290 char[] temp = obtain(end - start); 291 getChars(source, start, end, temp, 0); 292 String ret = new String(temp, 0, end - start); 293 recycle(temp); 294 295 return ret; 296 } 297 298 /** 299 * Returns a string containing the tokens joined by delimiters. 300 * @param tokens an array objects to be joined. Strings will be formed from 301 * the objects by calling object.toString(). 302 */ join(CharSequence delimiter, Object[] tokens)303 public static String join(CharSequence delimiter, Object[] tokens) { 304 StringBuilder sb = new StringBuilder(); 305 boolean firstTime = true; 306 for (Object token: tokens) { 307 if (firstTime) { 308 firstTime = false; 309 } else { 310 sb.append(delimiter); 311 } 312 sb.append(token); 313 } 314 return sb.toString(); 315 } 316 317 /** 318 * Returns a string containing the tokens joined by delimiters. 319 * @param tokens an array objects to be joined. Strings will be formed from 320 * the objects by calling object.toString(). 321 */ join(CharSequence delimiter, Iterable tokens)322 public static String join(CharSequence delimiter, Iterable tokens) { 323 StringBuilder sb = new StringBuilder(); 324 Iterator<?> it = tokens.iterator(); 325 if (it.hasNext()) { 326 sb.append(it.next()); 327 while (it.hasNext()) { 328 sb.append(delimiter); 329 sb.append(it.next()); 330 } 331 } 332 return sb.toString(); 333 } 334 335 /** 336 * String.split() returns [''] when the string to be split is empty. This returns []. This does 337 * not remove any empty strings from the result. For example split("a,", "," ) returns {"a", ""}. 338 * 339 * @param text the string to split 340 * @param expression the regular expression to match 341 * @return an array of strings. The array will be empty if text is empty 342 * 343 * @throws NullPointerException if expression or text is null 344 */ split(String text, String expression)345 public static String[] split(String text, String expression) { 346 if (text.length() == 0) { 347 return EMPTY_STRING_ARRAY; 348 } else { 349 return text.split(expression, -1); 350 } 351 } 352 353 /** 354 * Splits a string on a pattern. String.split() returns [''] when the string to be 355 * split is empty. This returns []. This does not remove any empty strings from the result. 356 * @param text the string to split 357 * @param pattern the regular expression to match 358 * @return an array of strings. The array will be empty if text is empty 359 * 360 * @throws NullPointerException if expression or text is null 361 */ split(String text, Pattern pattern)362 public static String[] split(String text, Pattern pattern) { 363 if (text.length() == 0) { 364 return EMPTY_STRING_ARRAY; 365 } else { 366 return pattern.split(text, -1); 367 } 368 } 369 370 /** 371 * An interface for splitting strings according to rules that are opaque to the user of this 372 * interface. This also has less overhead than split, which uses regular expressions and 373 * allocates an array to hold the results. 374 * 375 * <p>The most efficient way to use this class is: 376 * 377 * <pre> 378 * // Once 379 * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter); 380 * 381 * // Once per string to split 382 * splitter.setString(string); 383 * for (String s : splitter) { 384 * ... 385 * } 386 * </pre> 387 */ 388 public interface StringSplitter extends Iterable<String> { setString(String string)389 public void setString(String string); 390 } 391 392 /** 393 * A simple string splitter. 394 * 395 * <p>If the final character in the string to split is the delimiter then no empty string will 396 * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on 397 * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>. 398 */ 399 public static class SimpleStringSplitter implements StringSplitter, Iterator<String> { 400 private String mString; 401 private char mDelimiter; 402 private int mPosition; 403 private int mLength; 404 405 /** 406 * Initializes the splitter. setString may be called later. 407 * @param delimiter the delimeter on which to split 408 */ SimpleStringSplitter(char delimiter)409 public SimpleStringSplitter(char delimiter) { 410 mDelimiter = delimiter; 411 } 412 413 /** 414 * Sets the string to split 415 * @param string the string to split 416 */ setString(String string)417 public void setString(String string) { 418 mString = string; 419 mPosition = 0; 420 mLength = mString.length(); 421 } 422 iterator()423 public Iterator<String> iterator() { 424 return this; 425 } 426 hasNext()427 public boolean hasNext() { 428 return mPosition < mLength; 429 } 430 next()431 public String next() { 432 int end = mString.indexOf(mDelimiter, mPosition); 433 if (end == -1) { 434 end = mLength; 435 } 436 String nextString = mString.substring(mPosition, end); 437 mPosition = end + 1; // Skip the delimiter. 438 return nextString; 439 } 440 remove()441 public void remove() { 442 throw new UnsupportedOperationException(); 443 } 444 } 445 stringOrSpannedString(CharSequence source)446 public static CharSequence stringOrSpannedString(CharSequence source) { 447 if (source == null) 448 return null; 449 if (source instanceof SpannedString) 450 return source; 451 if (source instanceof Spanned) 452 return new SpannedString(source); 453 454 return source.toString(); 455 } 456 457 /** 458 * Returns true if the string is null or 0-length. 459 * @param str the string to be examined 460 * @return true if str is null or zero length 461 */ isEmpty(@ullable CharSequence str)462 public static boolean isEmpty(@Nullable CharSequence str) { 463 return str == null || str.length() == 0; 464 } 465 466 /** {@hide} */ nullIfEmpty(@ullable String str)467 public static String nullIfEmpty(@Nullable String str) { 468 return isEmpty(str) ? null : str; 469 } 470 471 /** {@hide} */ emptyIfNull(@ullable String str)472 public static String emptyIfNull(@Nullable String str) { 473 return str == null ? "" : str; 474 } 475 476 /** {@hide} */ firstNotEmpty(@ullable String a, @NonNull String b)477 public static String firstNotEmpty(@Nullable String a, @NonNull String b) { 478 return !isEmpty(a) ? a : Preconditions.checkStringNotEmpty(b); 479 } 480 481 /** {@hide} */ length(@ullable String s)482 public static int length(@Nullable String s) { 483 return isEmpty(s) ? 0 : s.length(); 484 } 485 486 /** 487 * @return interned string if it's null. 488 * @hide 489 */ safeIntern(String s)490 public static String safeIntern(String s) { 491 return (s != null) ? s.intern() : null; 492 } 493 494 /** 495 * Returns the length that the specified CharSequence would have if 496 * spaces and ASCII control characters were trimmed from the start and end, 497 * as by {@link String#trim}. 498 */ getTrimmedLength(CharSequence s)499 public static int getTrimmedLength(CharSequence s) { 500 int len = s.length(); 501 502 int start = 0; 503 while (start < len && s.charAt(start) <= ' ') { 504 start++; 505 } 506 507 int end = len; 508 while (end > start && s.charAt(end - 1) <= ' ') { 509 end--; 510 } 511 512 return end - start; 513 } 514 515 /** 516 * Returns true if a and b are equal, including if they are both null. 517 * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if 518 * both the arguments were instances of String.</i></p> 519 * @param a first CharSequence to check 520 * @param b second CharSequence to check 521 * @return true if a and b are equal 522 */ equals(CharSequence a, CharSequence b)523 public static boolean equals(CharSequence a, CharSequence b) { 524 if (a == b) return true; 525 int length; 526 if (a != null && b != null && (length = a.length()) == b.length()) { 527 if (a instanceof String && b instanceof String) { 528 return a.equals(b); 529 } else { 530 for (int i = 0; i < length; i++) { 531 if (a.charAt(i) != b.charAt(i)) return false; 532 } 533 return true; 534 } 535 } 536 return false; 537 } 538 539 /** 540 * This function only reverses individual {@code char}s and not their associated 541 * spans. It doesn't support surrogate pairs (that correspond to non-BMP code points), combining 542 * sequences or conjuncts either. 543 * @deprecated Do not use. 544 */ 545 @Deprecated getReverse(CharSequence source, int start, int end)546 public static CharSequence getReverse(CharSequence source, int start, int end) { 547 return new Reverser(source, start, end); 548 } 549 550 private static class Reverser 551 implements CharSequence, GetChars 552 { Reverser(CharSequence source, int start, int end)553 public Reverser(CharSequence source, int start, int end) { 554 mSource = source; 555 mStart = start; 556 mEnd = end; 557 } 558 length()559 public int length() { 560 return mEnd - mStart; 561 } 562 subSequence(int start, int end)563 public CharSequence subSequence(int start, int end) { 564 char[] buf = new char[end - start]; 565 566 getChars(start, end, buf, 0); 567 return new String(buf); 568 } 569 570 @Override toString()571 public String toString() { 572 return subSequence(0, length()).toString(); 573 } 574 charAt(int off)575 public char charAt(int off) { 576 return (char) UCharacter.getMirror(mSource.charAt(mEnd - 1 - off)); 577 } 578 579 @SuppressWarnings("deprecation") getChars(int start, int end, char[] dest, int destoff)580 public void getChars(int start, int end, char[] dest, int destoff) { 581 TextUtils.getChars(mSource, start + mStart, end + mStart, 582 dest, destoff); 583 AndroidCharacter.mirror(dest, 0, end - start); 584 585 int len = end - start; 586 int n = (end - start) / 2; 587 for (int i = 0; i < n; i++) { 588 char tmp = dest[destoff + i]; 589 590 dest[destoff + i] = dest[destoff + len - i - 1]; 591 dest[destoff + len - i - 1] = tmp; 592 } 593 } 594 595 private CharSequence mSource; 596 private int mStart; 597 private int mEnd; 598 } 599 600 /** @hide */ 601 public static final int ALIGNMENT_SPAN = 1; 602 /** @hide */ 603 public static final int FIRST_SPAN = ALIGNMENT_SPAN; 604 /** @hide */ 605 public static final int FOREGROUND_COLOR_SPAN = 2; 606 /** @hide */ 607 public static final int RELATIVE_SIZE_SPAN = 3; 608 /** @hide */ 609 public static final int SCALE_X_SPAN = 4; 610 /** @hide */ 611 public static final int STRIKETHROUGH_SPAN = 5; 612 /** @hide */ 613 public static final int UNDERLINE_SPAN = 6; 614 /** @hide */ 615 public static final int STYLE_SPAN = 7; 616 /** @hide */ 617 public static final int BULLET_SPAN = 8; 618 /** @hide */ 619 public static final int QUOTE_SPAN = 9; 620 /** @hide */ 621 public static final int LEADING_MARGIN_SPAN = 10; 622 /** @hide */ 623 public static final int URL_SPAN = 11; 624 /** @hide */ 625 public static final int BACKGROUND_COLOR_SPAN = 12; 626 /** @hide */ 627 public static final int TYPEFACE_SPAN = 13; 628 /** @hide */ 629 public static final int SUPERSCRIPT_SPAN = 14; 630 /** @hide */ 631 public static final int SUBSCRIPT_SPAN = 15; 632 /** @hide */ 633 public static final int ABSOLUTE_SIZE_SPAN = 16; 634 /** @hide */ 635 public static final int TEXT_APPEARANCE_SPAN = 17; 636 /** @hide */ 637 public static final int ANNOTATION = 18; 638 /** @hide */ 639 public static final int SUGGESTION_SPAN = 19; 640 /** @hide */ 641 public static final int SPELL_CHECK_SPAN = 20; 642 /** @hide */ 643 public static final int SUGGESTION_RANGE_SPAN = 21; 644 /** @hide */ 645 public static final int EASY_EDIT_SPAN = 22; 646 /** @hide */ 647 public static final int LOCALE_SPAN = 23; 648 /** @hide */ 649 public static final int TTS_SPAN = 24; 650 /** @hide */ 651 public static final int ACCESSIBILITY_CLICKABLE_SPAN = 25; 652 /** @hide */ 653 public static final int ACCESSIBILITY_URL_SPAN = 26; 654 /** @hide */ 655 public static final int LAST_SPAN = ACCESSIBILITY_URL_SPAN; 656 657 /** 658 * Flatten a CharSequence and whatever styles can be copied across processes 659 * into the parcel. 660 */ writeToParcel(CharSequence cs, Parcel p, int parcelableFlags)661 public static void writeToParcel(CharSequence cs, Parcel p, int parcelableFlags) { 662 if (cs instanceof Spanned) { 663 p.writeInt(0); 664 p.writeString(cs.toString()); 665 666 Spanned sp = (Spanned) cs; 667 Object[] os = sp.getSpans(0, cs.length(), Object.class); 668 669 // note to people adding to this: check more specific types 670 // before more generic types. also notice that it uses 671 // "if" instead of "else if" where there are interfaces 672 // so one object can be several. 673 674 for (int i = 0; i < os.length; i++) { 675 Object o = os[i]; 676 Object prop = os[i]; 677 678 if (prop instanceof CharacterStyle) { 679 prop = ((CharacterStyle) prop).getUnderlying(); 680 } 681 682 if (prop instanceof ParcelableSpan) { 683 final ParcelableSpan ps = (ParcelableSpan) prop; 684 final int spanTypeId = ps.getSpanTypeIdInternal(); 685 if (spanTypeId < FIRST_SPAN || spanTypeId > LAST_SPAN) { 686 Log.e(TAG, "External class \"" + ps.getClass().getSimpleName() 687 + "\" is attempting to use the frameworks-only ParcelableSpan" 688 + " interface"); 689 } else { 690 p.writeInt(spanTypeId); 691 ps.writeToParcelInternal(p, parcelableFlags); 692 writeWhere(p, sp, o); 693 } 694 } 695 } 696 697 p.writeInt(0); 698 } else { 699 p.writeInt(1); 700 if (cs != null) { 701 p.writeString(cs.toString()); 702 } else { 703 p.writeString(null); 704 } 705 } 706 } 707 writeWhere(Parcel p, Spanned sp, Object o)708 private static void writeWhere(Parcel p, Spanned sp, Object o) { 709 p.writeInt(sp.getSpanStart(o)); 710 p.writeInt(sp.getSpanEnd(o)); 711 p.writeInt(sp.getSpanFlags(o)); 712 } 713 714 public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR 715 = new Parcelable.Creator<CharSequence>() { 716 /** 717 * Read and return a new CharSequence, possibly with styles, 718 * from the parcel. 719 */ 720 public CharSequence createFromParcel(Parcel p) { 721 int kind = p.readInt(); 722 723 String string = p.readString(); 724 if (string == null) { 725 return null; 726 } 727 728 if (kind == 1) { 729 return string; 730 } 731 732 SpannableString sp = new SpannableString(string); 733 734 while (true) { 735 kind = p.readInt(); 736 737 if (kind == 0) 738 break; 739 740 switch (kind) { 741 case ALIGNMENT_SPAN: 742 readSpan(p, sp, new AlignmentSpan.Standard(p)); 743 break; 744 745 case FOREGROUND_COLOR_SPAN: 746 readSpan(p, sp, new ForegroundColorSpan(p)); 747 break; 748 749 case RELATIVE_SIZE_SPAN: 750 readSpan(p, sp, new RelativeSizeSpan(p)); 751 break; 752 753 case SCALE_X_SPAN: 754 readSpan(p, sp, new ScaleXSpan(p)); 755 break; 756 757 case STRIKETHROUGH_SPAN: 758 readSpan(p, sp, new StrikethroughSpan(p)); 759 break; 760 761 case UNDERLINE_SPAN: 762 readSpan(p, sp, new UnderlineSpan(p)); 763 break; 764 765 case STYLE_SPAN: 766 readSpan(p, sp, new StyleSpan(p)); 767 break; 768 769 case BULLET_SPAN: 770 readSpan(p, sp, new BulletSpan(p)); 771 break; 772 773 case QUOTE_SPAN: 774 readSpan(p, sp, new QuoteSpan(p)); 775 break; 776 777 case LEADING_MARGIN_SPAN: 778 readSpan(p, sp, new LeadingMarginSpan.Standard(p)); 779 break; 780 781 case URL_SPAN: 782 readSpan(p, sp, new URLSpan(p)); 783 break; 784 785 case BACKGROUND_COLOR_SPAN: 786 readSpan(p, sp, new BackgroundColorSpan(p)); 787 break; 788 789 case TYPEFACE_SPAN: 790 readSpan(p, sp, new TypefaceSpan(p)); 791 break; 792 793 case SUPERSCRIPT_SPAN: 794 readSpan(p, sp, new SuperscriptSpan(p)); 795 break; 796 797 case SUBSCRIPT_SPAN: 798 readSpan(p, sp, new SubscriptSpan(p)); 799 break; 800 801 case ABSOLUTE_SIZE_SPAN: 802 readSpan(p, sp, new AbsoluteSizeSpan(p)); 803 break; 804 805 case TEXT_APPEARANCE_SPAN: 806 readSpan(p, sp, new TextAppearanceSpan(p)); 807 break; 808 809 case ANNOTATION: 810 readSpan(p, sp, new Annotation(p)); 811 break; 812 813 case SUGGESTION_SPAN: 814 readSpan(p, sp, new SuggestionSpan(p)); 815 break; 816 817 case SPELL_CHECK_SPAN: 818 readSpan(p, sp, new SpellCheckSpan(p)); 819 break; 820 821 case SUGGESTION_RANGE_SPAN: 822 readSpan(p, sp, new SuggestionRangeSpan(p)); 823 break; 824 825 case EASY_EDIT_SPAN: 826 readSpan(p, sp, new EasyEditSpan(p)); 827 break; 828 829 case LOCALE_SPAN: 830 readSpan(p, sp, new LocaleSpan(p)); 831 break; 832 833 case TTS_SPAN: 834 readSpan(p, sp, new TtsSpan(p)); 835 break; 836 837 case ACCESSIBILITY_CLICKABLE_SPAN: 838 readSpan(p, sp, new AccessibilityClickableSpan(p)); 839 break; 840 841 case ACCESSIBILITY_URL_SPAN: 842 readSpan(p, sp, new AccessibilityURLSpan(p)); 843 break; 844 845 default: 846 throw new RuntimeException("bogus span encoding " + kind); 847 } 848 } 849 850 return sp; 851 } 852 853 public CharSequence[] newArray(int size) 854 { 855 return new CharSequence[size]; 856 } 857 }; 858 859 /** 860 * Debugging tool to print the spans in a CharSequence. The output will 861 * be printed one span per line. If the CharSequence is not a Spanned, 862 * then the entire string will be printed on a single line. 863 */ dumpSpans(CharSequence cs, Printer printer, String prefix)864 public static void dumpSpans(CharSequence cs, Printer printer, String prefix) { 865 if (cs instanceof Spanned) { 866 Spanned sp = (Spanned) cs; 867 Object[] os = sp.getSpans(0, cs.length(), Object.class); 868 869 for (int i = 0; i < os.length; i++) { 870 Object o = os[i]; 871 printer.println(prefix + cs.subSequence(sp.getSpanStart(o), 872 sp.getSpanEnd(o)) + ": " 873 + Integer.toHexString(System.identityHashCode(o)) 874 + " " + o.getClass().getCanonicalName() 875 + " (" + sp.getSpanStart(o) + "-" + sp.getSpanEnd(o) 876 + ") fl=#" + sp.getSpanFlags(o)); 877 } 878 } else { 879 printer.println(prefix + cs + ": (no spans)"); 880 } 881 } 882 883 /** 884 * Return a new CharSequence in which each of the source strings is 885 * replaced by the corresponding element of the destinations. 886 */ replace(CharSequence template, String[] sources, CharSequence[] destinations)887 public static CharSequence replace(CharSequence template, 888 String[] sources, 889 CharSequence[] destinations) { 890 SpannableStringBuilder tb = new SpannableStringBuilder(template); 891 892 for (int i = 0; i < sources.length; i++) { 893 int where = indexOf(tb, sources[i]); 894 895 if (where >= 0) 896 tb.setSpan(sources[i], where, where + sources[i].length(), 897 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 898 } 899 900 for (int i = 0; i < sources.length; i++) { 901 int start = tb.getSpanStart(sources[i]); 902 int end = tb.getSpanEnd(sources[i]); 903 904 if (start >= 0) { 905 tb.replace(start, end, destinations[i]); 906 } 907 } 908 909 return tb; 910 } 911 912 /** 913 * Replace instances of "^1", "^2", etc. in the 914 * <code>template</code> CharSequence with the corresponding 915 * <code>values</code>. "^^" is used to produce a single caret in 916 * the output. Only up to 9 replacement values are supported, 917 * "^10" will be produce the first replacement value followed by a 918 * '0'. 919 * 920 * @param template the input text containing "^1"-style 921 * placeholder values. This object is not modified; a copy is 922 * returned. 923 * 924 * @param values CharSequences substituted into the template. The 925 * first is substituted for "^1", the second for "^2", and so on. 926 * 927 * @return the new CharSequence produced by doing the replacement 928 * 929 * @throws IllegalArgumentException if the template requests a 930 * value that was not provided, or if more than 9 values are 931 * provided. 932 */ expandTemplate(CharSequence template, CharSequence... values)933 public static CharSequence expandTemplate(CharSequence template, 934 CharSequence... values) { 935 if (values.length > 9) { 936 throw new IllegalArgumentException("max of 9 values are supported"); 937 } 938 939 SpannableStringBuilder ssb = new SpannableStringBuilder(template); 940 941 try { 942 int i = 0; 943 while (i < ssb.length()) { 944 if (ssb.charAt(i) == '^') { 945 char next = ssb.charAt(i+1); 946 if (next == '^') { 947 ssb.delete(i+1, i+2); 948 ++i; 949 continue; 950 } else if (Character.isDigit(next)) { 951 int which = Character.getNumericValue(next) - 1; 952 if (which < 0) { 953 throw new IllegalArgumentException( 954 "template requests value ^" + (which+1)); 955 } 956 if (which >= values.length) { 957 throw new IllegalArgumentException( 958 "template requests value ^" + (which+1) + 959 "; only " + values.length + " provided"); 960 } 961 ssb.replace(i, i+2, values[which]); 962 i += values[which].length(); 963 continue; 964 } 965 } 966 ++i; 967 } 968 } catch (IndexOutOfBoundsException ignore) { 969 // happens when ^ is the last character in the string. 970 } 971 return ssb; 972 } 973 getOffsetBefore(CharSequence text, int offset)974 public static int getOffsetBefore(CharSequence text, int offset) { 975 if (offset == 0) 976 return 0; 977 if (offset == 1) 978 return 0; 979 980 char c = text.charAt(offset - 1); 981 982 if (c >= '\uDC00' && c <= '\uDFFF') { 983 char c1 = text.charAt(offset - 2); 984 985 if (c1 >= '\uD800' && c1 <= '\uDBFF') 986 offset -= 2; 987 else 988 offset -= 1; 989 } else { 990 offset -= 1; 991 } 992 993 if (text instanceof Spanned) { 994 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, 995 ReplacementSpan.class); 996 997 for (int i = 0; i < spans.length; i++) { 998 int start = ((Spanned) text).getSpanStart(spans[i]); 999 int end = ((Spanned) text).getSpanEnd(spans[i]); 1000 1001 if (start < offset && end > offset) 1002 offset = start; 1003 } 1004 } 1005 1006 return offset; 1007 } 1008 getOffsetAfter(CharSequence text, int offset)1009 public static int getOffsetAfter(CharSequence text, int offset) { 1010 int len = text.length(); 1011 1012 if (offset == len) 1013 return len; 1014 if (offset == len - 1) 1015 return len; 1016 1017 char c = text.charAt(offset); 1018 1019 if (c >= '\uD800' && c <= '\uDBFF') { 1020 char c1 = text.charAt(offset + 1); 1021 1022 if (c1 >= '\uDC00' && c1 <= '\uDFFF') 1023 offset += 2; 1024 else 1025 offset += 1; 1026 } else { 1027 offset += 1; 1028 } 1029 1030 if (text instanceof Spanned) { 1031 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, 1032 ReplacementSpan.class); 1033 1034 for (int i = 0; i < spans.length; i++) { 1035 int start = ((Spanned) text).getSpanStart(spans[i]); 1036 int end = ((Spanned) text).getSpanEnd(spans[i]); 1037 1038 if (start < offset && end > offset) 1039 offset = end; 1040 } 1041 } 1042 1043 return offset; 1044 } 1045 readSpan(Parcel p, Spannable sp, Object o)1046 private static void readSpan(Parcel p, Spannable sp, Object o) { 1047 sp.setSpan(o, p.readInt(), p.readInt(), p.readInt()); 1048 } 1049 1050 /** 1051 * Copies the spans from the region <code>start...end</code> in 1052 * <code>source</code> to the region 1053 * <code>destoff...destoff+end-start</code> in <code>dest</code>. 1054 * Spans in <code>source</code> that begin before <code>start</code> 1055 * or end after <code>end</code> but overlap this range are trimmed 1056 * as if they began at <code>start</code> or ended at <code>end</code>. 1057 * 1058 * @throws IndexOutOfBoundsException if any of the copied spans 1059 * are out of range in <code>dest</code>. 1060 */ copySpansFrom(Spanned source, int start, int end, Class kind, Spannable dest, int destoff)1061 public static void copySpansFrom(Spanned source, int start, int end, 1062 Class kind, 1063 Spannable dest, int destoff) { 1064 if (kind == null) { 1065 kind = Object.class; 1066 } 1067 1068 Object[] spans = source.getSpans(start, end, kind); 1069 1070 for (int i = 0; i < spans.length; i++) { 1071 int st = source.getSpanStart(spans[i]); 1072 int en = source.getSpanEnd(spans[i]); 1073 int fl = source.getSpanFlags(spans[i]); 1074 1075 if (st < start) 1076 st = start; 1077 if (en > end) 1078 en = end; 1079 1080 dest.setSpan(spans[i], st - start + destoff, en - start + destoff, 1081 fl); 1082 } 1083 } 1084 1085 /** 1086 * Transforms a CharSequences to uppercase, copying the sources spans and keeping them spans as 1087 * much as possible close to their relative original places. In the case the the uppercase 1088 * string is identical to the sources, the source itself is returned instead of being copied. 1089 * 1090 * If copySpans is set, source must be an instance of Spanned. 1091 * 1092 * {@hide} 1093 */ 1094 @NonNull toUpperCase(@ullable Locale locale, @NonNull CharSequence source, boolean copySpans)1095 public static CharSequence toUpperCase(@Nullable Locale locale, @NonNull CharSequence source, 1096 boolean copySpans) { 1097 final Edits edits = new Edits(); 1098 if (!copySpans) { // No spans. Just uppercase the characters. 1099 final StringBuilder result = CaseMap.toUpper().apply( 1100 locale, source, new StringBuilder(), edits); 1101 return edits.hasChanges() ? result : source; 1102 } 1103 1104 final SpannableStringBuilder result = CaseMap.toUpper().apply( 1105 locale, source, new SpannableStringBuilder(), edits); 1106 if (!edits.hasChanges()) { 1107 // No changes happened while capitalizing. We can return the source as it was. 1108 return source; 1109 } 1110 1111 final Edits.Iterator iterator = edits.getFineIterator(); 1112 final int sourceLength = source.length(); 1113 final Spanned spanned = (Spanned) source; 1114 final Object[] spans = spanned.getSpans(0, sourceLength, Object.class); 1115 for (Object span : spans) { 1116 final int sourceStart = spanned.getSpanStart(span); 1117 final int sourceEnd = spanned.getSpanEnd(span); 1118 final int flags = spanned.getSpanFlags(span); 1119 // Make sure the indices are not at the end of the string, since in that case 1120 // iterator.findSourceIndex() would fail. 1121 final int destStart = sourceStart == sourceLength ? result.length() : 1122 toUpperMapToDest(iterator, sourceStart); 1123 final int destEnd = sourceEnd == sourceLength ? result.length() : 1124 toUpperMapToDest(iterator, sourceEnd); 1125 result.setSpan(span, destStart, destEnd, flags); 1126 } 1127 return result; 1128 } 1129 1130 // helper method for toUpperCase() toUpperMapToDest(Edits.Iterator iterator, int sourceIndex)1131 private static int toUpperMapToDest(Edits.Iterator iterator, int sourceIndex) { 1132 // Guaranteed to succeed if sourceIndex < source.length(). 1133 iterator.findSourceIndex(sourceIndex); 1134 if (sourceIndex == iterator.sourceIndex()) { 1135 return iterator.destinationIndex(); 1136 } 1137 // We handle the situation differently depending on if we are in the changed slice or an 1138 // unchanged one: In an unchanged slice, we can find the exact location the span 1139 // boundary was before and map there. 1140 // 1141 // But in a changed slice, we need to treat the whole destination slice as an atomic unit. 1142 // We adjust the span boundary to the end of that slice to reduce of the chance of adjacent 1143 // spans in the source overlapping in the result. (The choice for the end vs the beginning 1144 // is somewhat arbitrary, but was taken because we except to see slightly more spans only 1145 // affecting a base character compared to spans only affecting a combining character.) 1146 if (iterator.hasChange()) { 1147 return iterator.destinationIndex() + iterator.newLength(); 1148 } else { 1149 // Move the index 1:1 along with this unchanged piece of text. 1150 return iterator.destinationIndex() + (sourceIndex - iterator.sourceIndex()); 1151 } 1152 } 1153 1154 public enum TruncateAt { 1155 START, 1156 MIDDLE, 1157 END, 1158 MARQUEE, 1159 /** 1160 * @hide 1161 */ 1162 END_SMALL 1163 } 1164 1165 public interface EllipsizeCallback { 1166 /** 1167 * This method is called to report that the specified region of 1168 * text was ellipsized away by a call to {@link #ellipsize}. 1169 */ ellipsized(int start, int end)1170 public void ellipsized(int start, int end); 1171 } 1172 1173 /** 1174 * Returns the original text if it fits in the specified width 1175 * given the properties of the specified Paint, 1176 * or, if it does not fit, a truncated 1177 * copy with ellipsis character added at the specified edge or center. 1178 */ ellipsize(CharSequence text, TextPaint p, float avail, TruncateAt where)1179 public static CharSequence ellipsize(CharSequence text, 1180 TextPaint p, 1181 float avail, TruncateAt where) { 1182 return ellipsize(text, p, avail, where, false, null); 1183 } 1184 1185 /** 1186 * Returns the original text if it fits in the specified width 1187 * given the properties of the specified Paint, 1188 * or, if it does not fit, a copy with ellipsis character added 1189 * at the specified edge or center. 1190 * If <code>preserveLength</code> is specified, the returned copy 1191 * will be padded with zero-width spaces to preserve the original 1192 * length and offsets instead of truncating. 1193 * If <code>callback</code> is non-null, it will be called to 1194 * report the start and end of the ellipsized range. TextDirection 1195 * is determined by the first strong directional character. 1196 */ ellipsize(CharSequence text, TextPaint paint, float avail, TruncateAt where, boolean preserveLength, EllipsizeCallback callback)1197 public static CharSequence ellipsize(CharSequence text, 1198 TextPaint paint, 1199 float avail, TruncateAt where, 1200 boolean preserveLength, 1201 EllipsizeCallback callback) { 1202 return ellipsize(text, paint, avail, where, preserveLength, callback, 1203 TextDirectionHeuristics.FIRSTSTRONG_LTR, 1204 (where == TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS_STRING : ELLIPSIS_STRING); 1205 } 1206 1207 /** 1208 * Returns the original text if it fits in the specified width 1209 * given the properties of the specified Paint, 1210 * or, if it does not fit, a copy with ellipsis character added 1211 * at the specified edge or center. 1212 * If <code>preserveLength</code> is specified, the returned copy 1213 * will be padded with zero-width spaces to preserve the original 1214 * length and offsets instead of truncating. 1215 * If <code>callback</code> is non-null, it will be called to 1216 * report the start and end of the ellipsized range. 1217 * 1218 * @hide 1219 */ ellipsize(CharSequence text, TextPaint paint, float avail, TruncateAt where, boolean preserveLength, EllipsizeCallback callback, TextDirectionHeuristic textDir, String ellipsis)1220 public static CharSequence ellipsize(CharSequence text, 1221 TextPaint paint, 1222 float avail, TruncateAt where, 1223 boolean preserveLength, 1224 EllipsizeCallback callback, 1225 TextDirectionHeuristic textDir, String ellipsis) { 1226 1227 int len = text.length(); 1228 1229 MeasuredText mt = MeasuredText.obtain(); 1230 try { 1231 float width = setPara(mt, paint, text, 0, text.length(), textDir); 1232 1233 if (width <= avail) { 1234 if (callback != null) { 1235 callback.ellipsized(0, 0); 1236 } 1237 1238 return text; 1239 } 1240 1241 // XXX assumes ellipsis string does not require shaping and 1242 // is unaffected by style 1243 float ellipsiswid = paint.measureText(ellipsis); 1244 avail -= ellipsiswid; 1245 1246 int left = 0; 1247 int right = len; 1248 if (avail < 0) { 1249 // it all goes 1250 } else if (where == TruncateAt.START) { 1251 right = len - mt.breakText(len, false, avail); 1252 } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) { 1253 left = mt.breakText(len, true, avail); 1254 } else { 1255 right = len - mt.breakText(len, false, avail / 2); 1256 avail -= mt.measure(right, len); 1257 left = mt.breakText(right, true, avail); 1258 } 1259 1260 if (callback != null) { 1261 callback.ellipsized(left, right); 1262 } 1263 1264 char[] buf = mt.mChars; 1265 Spanned sp = text instanceof Spanned ? (Spanned) text : null; 1266 1267 int remaining = len - (right - left); 1268 if (preserveLength) { 1269 if (remaining > 0) { // else eliminate the ellipsis too 1270 buf[left++] = ellipsis.charAt(0); 1271 } 1272 for (int i = left; i < right; i++) { 1273 buf[i] = ZWNBS_CHAR; 1274 } 1275 String s = new String(buf, 0, len); 1276 if (sp == null) { 1277 return s; 1278 } 1279 SpannableString ss = new SpannableString(s); 1280 copySpansFrom(sp, 0, len, Object.class, ss, 0); 1281 return ss; 1282 } 1283 1284 if (remaining == 0) { 1285 return ""; 1286 } 1287 1288 if (sp == null) { 1289 StringBuilder sb = new StringBuilder(remaining + ellipsis.length()); 1290 sb.append(buf, 0, left); 1291 sb.append(ellipsis); 1292 sb.append(buf, right, len - right); 1293 return sb.toString(); 1294 } 1295 1296 SpannableStringBuilder ssb = new SpannableStringBuilder(); 1297 ssb.append(text, 0, left); 1298 ssb.append(ellipsis); 1299 ssb.append(text, right, len); 1300 return ssb; 1301 } finally { 1302 MeasuredText.recycle(mt); 1303 } 1304 } 1305 1306 /** 1307 * Formats a list of CharSequences by repeatedly inserting the separator between them, 1308 * but stopping when the resulting sequence is too wide for the specified width. 1309 * 1310 * This method actually tries to fit the maximum number of elements. So if {@code "A, 11 more" 1311 * fits}, {@code "A, B, 10 more"} doesn't fit, but {@code "A, B, C, 9 more"} fits again (due to 1312 * the glyphs for the digits being very wide, for example), it returns 1313 * {@code "A, B, C, 9 more"}. Because of this, this method may be inefficient for very long 1314 * lists. 1315 * 1316 * Note that the elements of the returned value, as well as the string for {@code moreId}, will 1317 * be bidi-wrapped using {@link BidiFormatter#unicodeWrap} based on the locale of the input 1318 * Context. If the input {@code Context} is null, the default BidiFormatter from 1319 * {@link BidiFormatter#getInstance()} will be used. 1320 * 1321 * @param context the {@code Context} to get the {@code moreId} resource from. If {@code null}, 1322 * an ellipsis (U+2026) would be used for {@code moreId}. 1323 * @param elements the list to format 1324 * @param separator a separator, such as {@code ", "} 1325 * @param paint the Paint with which to measure the text 1326 * @param avail the horizontal width available for the text (in pixels) 1327 * @param moreId the resource ID for the pluralized string to insert at the end of sequence when 1328 * some of the elements don't fit. 1329 * 1330 * @return the formatted CharSequence. If even the shortest sequence (e.g. {@code "A, 11 more"}) 1331 * doesn't fit, it will return an empty string. 1332 */ 1333 listEllipsize(@ullable Context context, @Nullable List<CharSequence> elements, @NonNull String separator, @NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail, @PluralsRes int moreId)1334 public static CharSequence listEllipsize(@Nullable Context context, 1335 @Nullable List<CharSequence> elements, @NonNull String separator, 1336 @NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail, 1337 @PluralsRes int moreId) { 1338 if (elements == null) { 1339 return ""; 1340 } 1341 final int totalLen = elements.size(); 1342 if (totalLen == 0) { 1343 return ""; 1344 } 1345 1346 final Resources res; 1347 final BidiFormatter bidiFormatter; 1348 if (context == null) { 1349 res = null; 1350 bidiFormatter = BidiFormatter.getInstance(); 1351 } else { 1352 res = context.getResources(); 1353 bidiFormatter = BidiFormatter.getInstance(res.getConfiguration().getLocales().get(0)); 1354 } 1355 1356 final SpannableStringBuilder output = new SpannableStringBuilder(); 1357 final int[] endIndexes = new int[totalLen]; 1358 for (int i = 0; i < totalLen; i++) { 1359 output.append(bidiFormatter.unicodeWrap(elements.get(i))); 1360 if (i != totalLen - 1) { // Insert a separator, except at the very end. 1361 output.append(separator); 1362 } 1363 endIndexes[i] = output.length(); 1364 } 1365 1366 for (int i = totalLen - 1; i >= 0; i--) { 1367 // Delete the tail of the string, cutting back to one less element. 1368 output.delete(endIndexes[i], output.length()); 1369 1370 final int remainingElements = totalLen - i - 1; 1371 if (remainingElements > 0) { 1372 CharSequence morePiece = (res == null) ? 1373 ELLIPSIS_STRING : 1374 res.getQuantityString(moreId, remainingElements, remainingElements); 1375 morePiece = bidiFormatter.unicodeWrap(morePiece); 1376 output.append(morePiece); 1377 } 1378 1379 final float width = paint.measureText(output, 0, output.length()); 1380 if (width <= avail) { // The string fits. 1381 return output; 1382 } 1383 } 1384 return ""; // Nothing fits. 1385 } 1386 1387 /** 1388 * Converts a CharSequence of the comma-separated form "Andy, Bob, 1389 * Charles, David" that is too wide to fit into the specified width 1390 * into one like "Andy, Bob, 2 more". 1391 * 1392 * @param text the text to truncate 1393 * @param p the Paint with which to measure the text 1394 * @param avail the horizontal width available for the text (in pixels) 1395 * @param oneMore the string for "1 more" in the current locale 1396 * @param more the string for "%d more" in the current locale 1397 * 1398 * @deprecated Do not use. This is not internationalized, and has known issues 1399 * with right-to-left text, languages that have more than one plural form, languages 1400 * that use a different character as a comma-like separator, etc. 1401 * Use {@link #listEllipsize} instead. 1402 */ 1403 @Deprecated commaEllipsize(CharSequence text, TextPaint p, float avail, String oneMore, String more)1404 public static CharSequence commaEllipsize(CharSequence text, 1405 TextPaint p, float avail, 1406 String oneMore, 1407 String more) { 1408 return commaEllipsize(text, p, avail, oneMore, more, 1409 TextDirectionHeuristics.FIRSTSTRONG_LTR); 1410 } 1411 1412 /** 1413 * @hide 1414 */ 1415 @Deprecated commaEllipsize(CharSequence text, TextPaint p, float avail, String oneMore, String more, TextDirectionHeuristic textDir)1416 public static CharSequence commaEllipsize(CharSequence text, TextPaint p, 1417 float avail, String oneMore, String more, TextDirectionHeuristic textDir) { 1418 1419 MeasuredText mt = MeasuredText.obtain(); 1420 try { 1421 int len = text.length(); 1422 float width = setPara(mt, p, text, 0, len, textDir); 1423 if (width <= avail) { 1424 return text; 1425 } 1426 1427 char[] buf = mt.mChars; 1428 1429 int commaCount = 0; 1430 for (int i = 0; i < len; i++) { 1431 if (buf[i] == ',') { 1432 commaCount++; 1433 } 1434 } 1435 1436 int remaining = commaCount + 1; 1437 1438 int ok = 0; 1439 String okFormat = ""; 1440 1441 int w = 0; 1442 int count = 0; 1443 float[] widths = mt.mWidths; 1444 1445 MeasuredText tempMt = MeasuredText.obtain(); 1446 for (int i = 0; i < len; i++) { 1447 w += widths[i]; 1448 1449 if (buf[i] == ',') { 1450 count++; 1451 1452 String format; 1453 // XXX should not insert spaces, should be part of string 1454 // XXX should use plural rules and not assume English plurals 1455 if (--remaining == 1) { 1456 format = " " + oneMore; 1457 } else { 1458 format = " " + String.format(more, remaining); 1459 } 1460 1461 // XXX this is probably ok, but need to look at it more 1462 tempMt.setPara(format, 0, format.length(), textDir, null); 1463 float moreWid = tempMt.addStyleRun(p, tempMt.mLen, null); 1464 1465 if (w + moreWid <= avail) { 1466 ok = i + 1; 1467 okFormat = format; 1468 } 1469 } 1470 } 1471 MeasuredText.recycle(tempMt); 1472 1473 SpannableStringBuilder out = new SpannableStringBuilder(okFormat); 1474 out.insert(0, text, 0, ok); 1475 return out; 1476 } finally { 1477 MeasuredText.recycle(mt); 1478 } 1479 } 1480 setPara(MeasuredText mt, TextPaint paint, CharSequence text, int start, int end, TextDirectionHeuristic textDir)1481 private static float setPara(MeasuredText mt, TextPaint paint, 1482 CharSequence text, int start, int end, TextDirectionHeuristic textDir) { 1483 1484 mt.setPara(text, start, end, textDir, null); 1485 1486 float width; 1487 Spanned sp = text instanceof Spanned ? (Spanned) text : null; 1488 int len = end - start; 1489 if (sp == null) { 1490 width = mt.addStyleRun(paint, len, null); 1491 } else { 1492 width = 0; 1493 int spanEnd; 1494 for (int spanStart = 0; spanStart < len; spanStart = spanEnd) { 1495 spanEnd = sp.nextSpanTransition(spanStart, len, 1496 MetricAffectingSpan.class); 1497 MetricAffectingSpan[] spans = sp.getSpans( 1498 spanStart, spanEnd, MetricAffectingSpan.class); 1499 spans = TextUtils.removeEmptySpans(spans, sp, MetricAffectingSpan.class); 1500 width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null); 1501 } 1502 } 1503 1504 return width; 1505 } 1506 1507 // Returns true if the character's presence could affect RTL layout. 1508 // 1509 // In order to be fast, the code is intentionally rough and quite conservative in its 1510 // considering inclusion of any non-BMP or surrogate characters or anything in the bidi 1511 // blocks or any bidi formatting characters with a potential to affect RTL layout. 1512 /* package */ couldAffectRtl(char c)1513 static boolean couldAffectRtl(char c) { 1514 return (0x0590 <= c && c <= 0x08FF) || // RTL scripts 1515 c == 0x200E || // Bidi format character 1516 c == 0x200F || // Bidi format character 1517 (0x202A <= c && c <= 0x202E) || // Bidi format characters 1518 (0x2066 <= c && c <= 0x2069) || // Bidi format characters 1519 (0xD800 <= c && c <= 0xDFFF) || // Surrogate pairs 1520 (0xFB1D <= c && c <= 0xFDFF) || // Hebrew and Arabic presentation forms 1521 (0xFE70 <= c && c <= 0xFEFE); // Arabic presentation forms 1522 } 1523 1524 // Returns true if there is no character present that may potentially affect RTL layout. 1525 // Since this calls couldAffectRtl() above, it's also quite conservative, in the way that 1526 // it may return 'false' (needs bidi) although careful consideration may tell us it should 1527 // return 'true' (does not need bidi). 1528 /* package */ doesNotNeedBidi(char[] text, int start, int len)1529 static boolean doesNotNeedBidi(char[] text, int start, int len) { 1530 final int end = start + len; 1531 for (int i = start; i < end; i++) { 1532 if (couldAffectRtl(text[i])) { 1533 return false; 1534 } 1535 } 1536 return true; 1537 } 1538 obtain(int len)1539 /* package */ static char[] obtain(int len) { 1540 char[] buf; 1541 1542 synchronized (sLock) { 1543 buf = sTemp; 1544 sTemp = null; 1545 } 1546 1547 if (buf == null || buf.length < len) 1548 buf = ArrayUtils.newUnpaddedCharArray(len); 1549 1550 return buf; 1551 } 1552 recycle(char[] temp)1553 /* package */ static void recycle(char[] temp) { 1554 if (temp.length > 1000) 1555 return; 1556 1557 synchronized (sLock) { 1558 sTemp = temp; 1559 } 1560 } 1561 1562 /** 1563 * Html-encode the string. 1564 * @param s the string to be encoded 1565 * @return the encoded string 1566 */ htmlEncode(String s)1567 public static String htmlEncode(String s) { 1568 StringBuilder sb = new StringBuilder(); 1569 char c; 1570 for (int i = 0; i < s.length(); i++) { 1571 c = s.charAt(i); 1572 switch (c) { 1573 case '<': 1574 sb.append("<"); //$NON-NLS-1$ 1575 break; 1576 case '>': 1577 sb.append(">"); //$NON-NLS-1$ 1578 break; 1579 case '&': 1580 sb.append("&"); //$NON-NLS-1$ 1581 break; 1582 case '\'': 1583 //http://www.w3.org/TR/xhtml1 1584 // The named character reference ' (the apostrophe, U+0027) was introduced in 1585 // XML 1.0 but does not appear in HTML. Authors should therefore use ' instead 1586 // of ' to work as expected in HTML 4 user agents. 1587 sb.append("'"); //$NON-NLS-1$ 1588 break; 1589 case '"': 1590 sb.append("""); //$NON-NLS-1$ 1591 break; 1592 default: 1593 sb.append(c); 1594 } 1595 } 1596 return sb.toString(); 1597 } 1598 1599 /** 1600 * Returns a CharSequence concatenating the specified CharSequences, 1601 * retaining their spans if any. 1602 * 1603 * If there are no parameters, an empty string will be returned. 1604 * 1605 * If the number of parameters is exactly one, that parameter is returned as output, even if it 1606 * is null. 1607 * 1608 * If the number of parameters is at least two, any null CharSequence among the parameters is 1609 * treated as if it was the string <code>"null"</code>. 1610 * 1611 * If there are paragraph spans in the source CharSequences that satisfy paragraph boundary 1612 * requirements in the sources but would no longer satisfy them in the concatenated 1613 * CharSequence, they may get extended in the resulting CharSequence or not retained. 1614 */ concat(CharSequence... text)1615 public static CharSequence concat(CharSequence... text) { 1616 if (text.length == 0) { 1617 return ""; 1618 } 1619 1620 if (text.length == 1) { 1621 return text[0]; 1622 } 1623 1624 boolean spanned = false; 1625 for (CharSequence piece : text) { 1626 if (piece instanceof Spanned) { 1627 spanned = true; 1628 break; 1629 } 1630 } 1631 1632 if (spanned) { 1633 final SpannableStringBuilder ssb = new SpannableStringBuilder(); 1634 for (CharSequence piece : text) { 1635 // If a piece is null, we append the string "null" for compatibility with the 1636 // behavior of StringBuilder and the behavior of the concat() method in earlier 1637 // versions of Android. 1638 ssb.append(piece == null ? "null" : piece); 1639 } 1640 return new SpannedString(ssb); 1641 } else { 1642 final StringBuilder sb = new StringBuilder(); 1643 for (CharSequence piece : text) { 1644 sb.append(piece); 1645 } 1646 return sb.toString(); 1647 } 1648 } 1649 1650 /** 1651 * Returns whether the given CharSequence contains any printable characters. 1652 */ isGraphic(CharSequence str)1653 public static boolean isGraphic(CharSequence str) { 1654 final int len = str.length(); 1655 for (int cp, i=0; i<len; i+=Character.charCount(cp)) { 1656 cp = Character.codePointAt(str, i); 1657 int gc = Character.getType(cp); 1658 if (gc != Character.CONTROL 1659 && gc != Character.FORMAT 1660 && gc != Character.SURROGATE 1661 && gc != Character.UNASSIGNED 1662 && gc != Character.LINE_SEPARATOR 1663 && gc != Character.PARAGRAPH_SEPARATOR 1664 && gc != Character.SPACE_SEPARATOR) { 1665 return true; 1666 } 1667 } 1668 return false; 1669 } 1670 1671 /** 1672 * Returns whether this character is a printable character. 1673 * 1674 * This does not support non-BMP characters and should not be used. 1675 * 1676 * @deprecated Use {@link #isGraphic(CharSequence)} instead. 1677 */ 1678 @Deprecated isGraphic(char c)1679 public static boolean isGraphic(char c) { 1680 int gc = Character.getType(c); 1681 return gc != Character.CONTROL 1682 && gc != Character.FORMAT 1683 && gc != Character.SURROGATE 1684 && gc != Character.UNASSIGNED 1685 && gc != Character.LINE_SEPARATOR 1686 && gc != Character.PARAGRAPH_SEPARATOR 1687 && gc != Character.SPACE_SEPARATOR; 1688 } 1689 1690 /** 1691 * Returns whether the given CharSequence contains only digits. 1692 */ isDigitsOnly(CharSequence str)1693 public static boolean isDigitsOnly(CharSequence str) { 1694 final int len = str.length(); 1695 for (int cp, i = 0; i < len; i += Character.charCount(cp)) { 1696 cp = Character.codePointAt(str, i); 1697 if (!Character.isDigit(cp)) { 1698 return false; 1699 } 1700 } 1701 return true; 1702 } 1703 1704 /** 1705 * @hide 1706 */ isPrintableAscii(final char c)1707 public static boolean isPrintableAscii(final char c) { 1708 final int asciiFirst = 0x20; 1709 final int asciiLast = 0x7E; // included 1710 return (asciiFirst <= c && c <= asciiLast) || c == '\r' || c == '\n'; 1711 } 1712 1713 /** 1714 * @hide 1715 */ isPrintableAsciiOnly(final CharSequence str)1716 public static boolean isPrintableAsciiOnly(final CharSequence str) { 1717 final int len = str.length(); 1718 for (int i = 0; i < len; i++) { 1719 if (!isPrintableAscii(str.charAt(i))) { 1720 return false; 1721 } 1722 } 1723 return true; 1724 } 1725 1726 /** 1727 * Capitalization mode for {@link #getCapsMode}: capitalize all 1728 * characters. This value is explicitly defined to be the same as 1729 * {@link InputType#TYPE_TEXT_FLAG_CAP_CHARACTERS}. 1730 */ 1731 public static final int CAP_MODE_CHARACTERS 1732 = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; 1733 1734 /** 1735 * Capitalization mode for {@link #getCapsMode}: capitalize the first 1736 * character of all words. This value is explicitly defined to be the same as 1737 * {@link InputType#TYPE_TEXT_FLAG_CAP_WORDS}. 1738 */ 1739 public static final int CAP_MODE_WORDS 1740 = InputType.TYPE_TEXT_FLAG_CAP_WORDS; 1741 1742 /** 1743 * Capitalization mode for {@link #getCapsMode}: capitalize the first 1744 * character of each sentence. This value is explicitly defined to be the same as 1745 * {@link InputType#TYPE_TEXT_FLAG_CAP_SENTENCES}. 1746 */ 1747 public static final int CAP_MODE_SENTENCES 1748 = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; 1749 1750 /** 1751 * Determine what caps mode should be in effect at the current offset in 1752 * the text. Only the mode bits set in <var>reqModes</var> will be 1753 * checked. Note that the caps mode flags here are explicitly defined 1754 * to match those in {@link InputType}. 1755 * 1756 * @param cs The text that should be checked for caps modes. 1757 * @param off Location in the text at which to check. 1758 * @param reqModes The modes to be checked: may be any combination of 1759 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and 1760 * {@link #CAP_MODE_SENTENCES}. 1761 * 1762 * @return Returns the actual capitalization modes that can be in effect 1763 * at the current position, which is any combination of 1764 * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and 1765 * {@link #CAP_MODE_SENTENCES}. 1766 */ getCapsMode(CharSequence cs, int off, int reqModes)1767 public static int getCapsMode(CharSequence cs, int off, int reqModes) { 1768 if (off < 0) { 1769 return 0; 1770 } 1771 1772 int i; 1773 char c; 1774 int mode = 0; 1775 1776 if ((reqModes&CAP_MODE_CHARACTERS) != 0) { 1777 mode |= CAP_MODE_CHARACTERS; 1778 } 1779 if ((reqModes&(CAP_MODE_WORDS|CAP_MODE_SENTENCES)) == 0) { 1780 return mode; 1781 } 1782 1783 // Back over allowed opening punctuation. 1784 1785 for (i = off; i > 0; i--) { 1786 c = cs.charAt(i - 1); 1787 1788 if (c != '"' && c != '\'' && 1789 Character.getType(c) != Character.START_PUNCTUATION) { 1790 break; 1791 } 1792 } 1793 1794 // Start of paragraph, with optional whitespace. 1795 1796 int j = i; 1797 while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) { 1798 j--; 1799 } 1800 if (j == 0 || cs.charAt(j - 1) == '\n') { 1801 return mode | CAP_MODE_WORDS; 1802 } 1803 1804 // Or start of word if we are that style. 1805 1806 if ((reqModes&CAP_MODE_SENTENCES) == 0) { 1807 if (i != j) mode |= CAP_MODE_WORDS; 1808 return mode; 1809 } 1810 1811 // There must be a space if not the start of paragraph. 1812 1813 if (i == j) { 1814 return mode; 1815 } 1816 1817 // Back over allowed closing punctuation. 1818 1819 for (; j > 0; j--) { 1820 c = cs.charAt(j - 1); 1821 1822 if (c != '"' && c != '\'' && 1823 Character.getType(c) != Character.END_PUNCTUATION) { 1824 break; 1825 } 1826 } 1827 1828 if (j > 0) { 1829 c = cs.charAt(j - 1); 1830 1831 if (c == '.' || c == '?' || c == '!') { 1832 // Do not capitalize if the word ends with a period but 1833 // also contains a period, in which case it is an abbreviation. 1834 1835 if (c == '.') { 1836 for (int k = j - 2; k >= 0; k--) { 1837 c = cs.charAt(k); 1838 1839 if (c == '.') { 1840 return mode; 1841 } 1842 1843 if (!Character.isLetter(c)) { 1844 break; 1845 } 1846 } 1847 } 1848 1849 return mode | CAP_MODE_SENTENCES; 1850 } 1851 } 1852 1853 return mode; 1854 } 1855 1856 /** 1857 * Does a comma-delimited list 'delimitedString' contain a certain item? 1858 * (without allocating memory) 1859 * 1860 * @hide 1861 */ delimitedStringContains( String delimitedString, char delimiter, String item)1862 public static boolean delimitedStringContains( 1863 String delimitedString, char delimiter, String item) { 1864 if (isEmpty(delimitedString) || isEmpty(item)) { 1865 return false; 1866 } 1867 int pos = -1; 1868 int length = delimitedString.length(); 1869 while ((pos = delimitedString.indexOf(item, pos + 1)) != -1) { 1870 if (pos > 0 && delimitedString.charAt(pos - 1) != delimiter) { 1871 continue; 1872 } 1873 int expectedDelimiterPos = pos + item.length(); 1874 if (expectedDelimiterPos == length) { 1875 // Match at end of string. 1876 return true; 1877 } 1878 if (delimitedString.charAt(expectedDelimiterPos) == delimiter) { 1879 return true; 1880 } 1881 } 1882 return false; 1883 } 1884 1885 /** 1886 * Removes empty spans from the <code>spans</code> array. 1887 * 1888 * When parsing a Spanned using {@link Spanned#nextSpanTransition(int, int, Class)}, empty spans 1889 * will (correctly) create span transitions, and calling getSpans on a slice of text bounded by 1890 * one of these transitions will (correctly) include the empty overlapping span. 1891 * 1892 * However, these empty spans should not be taken into account when layouting or rendering the 1893 * string and this method provides a way to filter getSpans' results accordingly. 1894 * 1895 * @param spans A list of spans retrieved using {@link Spanned#getSpans(int, int, Class)} from 1896 * the <code>spanned</code> 1897 * @param spanned The Spanned from which spans were extracted 1898 * @return A subset of spans where empty spans ({@link Spanned#getSpanStart(Object)} == 1899 * {@link Spanned#getSpanEnd(Object)} have been removed. The initial order is preserved 1900 * @hide 1901 */ 1902 @SuppressWarnings("unchecked") removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass)1903 public static <T> T[] removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass) { 1904 T[] copy = null; 1905 int count = 0; 1906 1907 for (int i = 0; i < spans.length; i++) { 1908 final T span = spans[i]; 1909 final int start = spanned.getSpanStart(span); 1910 final int end = spanned.getSpanEnd(span); 1911 1912 if (start == end) { 1913 if (copy == null) { 1914 copy = (T[]) Array.newInstance(klass, spans.length - 1); 1915 System.arraycopy(spans, 0, copy, 0, i); 1916 count = i; 1917 } 1918 } else { 1919 if (copy != null) { 1920 copy[count] = span; 1921 count++; 1922 } 1923 } 1924 } 1925 1926 if (copy != null) { 1927 T[] result = (T[]) Array.newInstance(klass, count); 1928 System.arraycopy(copy, 0, result, 0, count); 1929 return result; 1930 } else { 1931 return spans; 1932 } 1933 } 1934 1935 /** 1936 * Pack 2 int values into a long, useful as a return value for a range 1937 * @see #unpackRangeStartFromLong(long) 1938 * @see #unpackRangeEndFromLong(long) 1939 * @hide 1940 */ packRangeInLong(int start, int end)1941 public static long packRangeInLong(int start, int end) { 1942 return (((long) start) << 32) | end; 1943 } 1944 1945 /** 1946 * Get the start value from a range packed in a long by {@link #packRangeInLong(int, int)} 1947 * @see #unpackRangeEndFromLong(long) 1948 * @see #packRangeInLong(int, int) 1949 * @hide 1950 */ unpackRangeStartFromLong(long range)1951 public static int unpackRangeStartFromLong(long range) { 1952 return (int) (range >>> 32); 1953 } 1954 1955 /** 1956 * Get the end value from a range packed in a long by {@link #packRangeInLong(int, int)} 1957 * @see #unpackRangeStartFromLong(long) 1958 * @see #packRangeInLong(int, int) 1959 * @hide 1960 */ unpackRangeEndFromLong(long range)1961 public static int unpackRangeEndFromLong(long range) { 1962 return (int) (range & 0x00000000FFFFFFFFL); 1963 } 1964 1965 /** 1966 * Return the layout direction for a given Locale 1967 * 1968 * @param locale the Locale for which we want the layout direction. Can be null. 1969 * @return the layout direction. This may be one of: 1970 * {@link android.view.View#LAYOUT_DIRECTION_LTR} or 1971 * {@link android.view.View#LAYOUT_DIRECTION_RTL}. 1972 * 1973 * Be careful: this code will need to be updated when vertical scripts will be supported 1974 */ getLayoutDirectionFromLocale(Locale locale)1975 public static int getLayoutDirectionFromLocale(Locale locale) { 1976 return ((locale != null && !locale.equals(Locale.ROOT) 1977 && ULocale.forLocale(locale).isRightToLeft()) 1978 // If forcing into RTL layout mode, return RTL as default 1979 || SystemProperties.getBoolean(Settings.Global.DEVELOPMENT_FORCE_RTL, false)) 1980 ? View.LAYOUT_DIRECTION_RTL 1981 : View.LAYOUT_DIRECTION_LTR; 1982 } 1983 1984 /** 1985 * Return localized string representing the given number of selected items. 1986 * 1987 * @hide 1988 */ formatSelectedCount(int count)1989 public static CharSequence formatSelectedCount(int count) { 1990 return Resources.getSystem().getQuantityString(R.plurals.selected_count, count, count); 1991 } 1992 1993 /** 1994 * Returns whether or not the specified spanned text has a style span. 1995 * @hide 1996 */ hasStyleSpan(@onNull Spanned spanned)1997 public static boolean hasStyleSpan(@NonNull Spanned spanned) { 1998 Preconditions.checkArgument(spanned != null); 1999 final Class<?>[] styleClasses = { 2000 CharacterStyle.class, ParagraphStyle.class, UpdateAppearance.class}; 2001 for (Class<?> clazz : styleClasses) { 2002 if (spanned.nextSpanTransition(-1, spanned.length(), clazz) < spanned.length()) { 2003 return true; 2004 } 2005 } 2006 return false; 2007 } 2008 2009 /** 2010 * If the {@code charSequence} is instance of {@link Spanned}, creates a new copy and 2011 * {@link NoCopySpan}'s are removed from the copy. Otherwise the given {@code charSequence} is 2012 * returned as it is. 2013 * 2014 * @hide 2015 */ 2016 @Nullable trimNoCopySpans(@ullable CharSequence charSequence)2017 public static CharSequence trimNoCopySpans(@Nullable CharSequence charSequence) { 2018 if (charSequence != null && charSequence instanceof Spanned) { 2019 // SpannableStringBuilder copy constructor trims NoCopySpans. 2020 return new SpannableStringBuilder(charSequence); 2021 } 2022 return charSequence; 2023 } 2024 2025 /** 2026 * Prepends {@code start} and appends {@code end} to a given {@link StringBuilder} 2027 * 2028 * @hide 2029 */ wrap(StringBuilder builder, String start, String end)2030 public static void wrap(StringBuilder builder, String start, String end) { 2031 builder.insert(0, start); 2032 builder.append(end); 2033 } 2034 2035 private static Object sLock = new Object(); 2036 2037 private static char[] sTemp = null; 2038 2039 private static String[] EMPTY_STRING_ARRAY = new String[]{}; 2040 2041 private static final char ZWNBS_CHAR = '\uFEFF'; 2042 } 2043