TextUtils.java revision 54b6cfa9a9e5b861a9930af873580d6dc20f773c
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 17package android.text; 18 19import com.android.internal.R; 20 21import android.content.res.ColorStateList; 22import android.content.res.Resources; 23import android.os.Parcel; 24import android.os.Parcelable; 25import android.text.style.AbsoluteSizeSpan; 26import android.text.style.AlignmentSpan; 27import android.text.style.BackgroundColorSpan; 28import android.text.style.BulletSpan; 29import android.text.style.CharacterStyle; 30import android.text.style.ForegroundColorSpan; 31import android.text.style.LeadingMarginSpan; 32import android.text.style.MetricAffectingSpan; 33import android.text.style.QuoteSpan; 34import android.text.style.RelativeSizeSpan; 35import android.text.style.ReplacementSpan; 36import android.text.style.ScaleXSpan; 37import android.text.style.StrikethroughSpan; 38import android.text.style.StyleSpan; 39import android.text.style.SubscriptSpan; 40import android.text.style.SuperscriptSpan; 41import android.text.style.TextAppearanceSpan; 42import android.text.style.TypefaceSpan; 43import android.text.style.URLSpan; 44import android.text.style.UnderlineSpan; 45import com.android.internal.util.ArrayUtils; 46 47import java.util.regex.Pattern; 48import java.util.Iterator; 49 50public class TextUtils 51{ 52 private TextUtils() { /* cannot be instantiated */ } 53 54 private static String[] EMPTY_STRING_ARRAY = new String[]{}; 55 56 public static void getChars(CharSequence s, int start, int end, 57 char[] dest, int destoff) { 58 Class c = s.getClass(); 59 60 if (c == String.class) 61 ((String) s).getChars(start, end, dest, destoff); 62 else if (c == StringBuffer.class) 63 ((StringBuffer) s).getChars(start, end, dest, destoff); 64 else if (c == StringBuilder.class) 65 ((StringBuilder) s).getChars(start, end, dest, destoff); 66 else if (s instanceof GetChars) 67 ((GetChars) s).getChars(start, end, dest, destoff); 68 else { 69 for (int i = start; i < end; i++) 70 dest[destoff++] = s.charAt(i); 71 } 72 } 73 74 public static int indexOf(CharSequence s, char ch) { 75 return indexOf(s, ch, 0); 76 } 77 78 public static int indexOf(CharSequence s, char ch, int start) { 79 Class c = s.getClass(); 80 81 if (c == String.class) 82 return ((String) s).indexOf(ch, start); 83 84 return indexOf(s, ch, start, s.length()); 85 } 86 87 public static int indexOf(CharSequence s, char ch, int start, int end) { 88 Class c = s.getClass(); 89 90 if (s instanceof GetChars || c == StringBuffer.class || 91 c == StringBuilder.class || c == String.class) { 92 final int INDEX_INCREMENT = 500; 93 char[] temp = obtain(INDEX_INCREMENT); 94 95 while (start < end) { 96 int segend = start + INDEX_INCREMENT; 97 if (segend > end) 98 segend = end; 99 100 getChars(s, start, segend, temp, 0); 101 102 int count = segend - start; 103 for (int i = 0; i < count; i++) { 104 if (temp[i] == ch) { 105 recycle(temp); 106 return i + start; 107 } 108 } 109 110 start = segend; 111 } 112 113 recycle(temp); 114 return -1; 115 } 116 117 for (int i = start; i < end; i++) 118 if (s.charAt(i) == ch) 119 return i; 120 121 return -1; 122 } 123 124 public static int lastIndexOf(CharSequence s, char ch) { 125 return lastIndexOf(s, ch, s.length() - 1); 126 } 127 128 public static int lastIndexOf(CharSequence s, char ch, int last) { 129 Class c = s.getClass(); 130 131 if (c == String.class) 132 return ((String) s).lastIndexOf(ch, last); 133 134 return lastIndexOf(s, ch, 0, last); 135 } 136 137 public static int lastIndexOf(CharSequence s, char ch, 138 int start, int last) { 139 if (last < 0) 140 return -1; 141 if (last >= s.length()) 142 last = s.length() - 1; 143 144 int end = last + 1; 145 146 Class c = s.getClass(); 147 148 if (s instanceof GetChars || c == StringBuffer.class || 149 c == StringBuilder.class || c == String.class) { 150 final int INDEX_INCREMENT = 500; 151 char[] temp = obtain(INDEX_INCREMENT); 152 153 while (start < end) { 154 int segstart = end - INDEX_INCREMENT; 155 if (segstart < start) 156 segstart = start; 157 158 getChars(s, segstart, end, temp, 0); 159 160 int count = end - segstart; 161 for (int i = count - 1; i >= 0; i--) { 162 if (temp[i] == ch) { 163 recycle(temp); 164 return i + segstart; 165 } 166 } 167 168 end = segstart; 169 } 170 171 recycle(temp); 172 return -1; 173 } 174 175 for (int i = end - 1; i >= start; i--) 176 if (s.charAt(i) == ch) 177 return i; 178 179 return -1; 180 } 181 182 public static int indexOf(CharSequence s, CharSequence needle) { 183 return indexOf(s, needle, 0, s.length()); 184 } 185 186 public static int indexOf(CharSequence s, CharSequence needle, int start) { 187 return indexOf(s, needle, start, s.length()); 188 } 189 190 public static int indexOf(CharSequence s, CharSequence needle, 191 int start, int end) { 192 int nlen = needle.length(); 193 if (nlen == 0) 194 return start; 195 196 char c = needle.charAt(0); 197 198 for (;;) { 199 start = indexOf(s, c, start); 200 if (start > end - nlen) { 201 break; 202 } 203 204 if (start < 0) { 205 return -1; 206 } 207 208 if (regionMatches(s, start, needle, 0, nlen)) { 209 return start; 210 } 211 212 start++; 213 } 214 return -1; 215 } 216 217 public static boolean regionMatches(CharSequence one, int toffset, 218 CharSequence two, int ooffset, 219 int len) { 220 char[] temp = obtain(2 * len); 221 222 getChars(one, toffset, toffset + len, temp, 0); 223 getChars(two, ooffset, ooffset + len, temp, len); 224 225 boolean match = true; 226 for (int i = 0; i < len; i++) { 227 if (temp[i] != temp[i + len]) { 228 match = false; 229 break; 230 } 231 } 232 233 recycle(temp); 234 return match; 235 } 236 237 public static String substring(CharSequence source, int start, int end) { 238 if (source instanceof String) 239 return ((String) source).substring(start, end); 240 if (source instanceof StringBuilder) 241 return ((StringBuilder) source).substring(start, end); 242 if (source instanceof StringBuffer) 243 return ((StringBuffer) source).substring(start, end); 244 245 char[] temp = obtain(end - start); 246 getChars(source, start, end, temp, 0); 247 String ret = new String(temp, 0, end - start); 248 recycle(temp); 249 250 return ret; 251 } 252 253 /** 254 * Returns a string containing the tokens joined by delimiters. 255 * @param tokens an array objects to be joined. Strings will be formed from 256 * the objects by calling object.toString(). 257 */ 258 public static String join(CharSequence delimiter, Object[] tokens) { 259 StringBuilder sb = new StringBuilder(); 260 boolean firstTime = true; 261 for (Object token: tokens) { 262 if (firstTime) { 263 firstTime = false; 264 } else { 265 sb.append(delimiter); 266 } 267 sb.append(token); 268 } 269 return sb.toString(); 270 } 271 272 /** 273 * Returns a string containing the tokens joined by delimiters. 274 * @param tokens an array objects to be joined. Strings will be formed from 275 * the objects by calling object.toString(). 276 */ 277 public static String join(CharSequence delimiter, Iterable tokens) { 278 StringBuilder sb = new StringBuilder(); 279 boolean firstTime = true; 280 for (Object token: tokens) { 281 if (firstTime) { 282 firstTime = false; 283 } else { 284 sb.append(delimiter); 285 } 286 sb.append(token); 287 } 288 return sb.toString(); 289 } 290 291 /** 292 * String.split() returns [''] when the string to be split is empty. This returns []. This does 293 * not remove any empty strings from the result. For example split("a,", "," ) returns {"a", ""}. 294 * 295 * @param text the string to split 296 * @param expression the regular expression to match 297 * @return an array of strings. The array will be empty if text is empty 298 * 299 * @throws NullPointerException if expression or text is null 300 */ 301 public static String[] split(String text, String expression) { 302 if (text.length() == 0) { 303 return EMPTY_STRING_ARRAY; 304 } else { 305 return text.split(expression, -1); 306 } 307 } 308 309 /** 310 * Splits a string on a pattern. String.split() returns [''] when the string to be 311 * split is empty. This returns []. This does not remove any empty strings from the result. 312 * @param text the string to split 313 * @param pattern the regular expression to match 314 * @return an array of strings. The array will be empty if text is empty 315 * 316 * @throws NullPointerException if expression or text is null 317 */ 318 public static String[] split(String text, Pattern pattern) { 319 if (text.length() == 0) { 320 return EMPTY_STRING_ARRAY; 321 } else { 322 return pattern.split(text, -1); 323 } 324 } 325 326 /** 327 * An interface for splitting strings according to rules that are opaque to the user of this 328 * interface. This also has less overhead than split, which uses regular expressions and 329 * allocates an array to hold the results. 330 * 331 * <p>The most efficient way to use this class is: 332 * 333 * <pre> 334 * // Once 335 * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter); 336 * 337 * // Once per string to split 338 * splitter.setString(string); 339 * for (String s : splitter) { 340 * ... 341 * } 342 * </pre> 343 */ 344 public interface StringSplitter extends Iterable<String> { 345 public void setString(String string); 346 } 347 348 /** 349 * A simple string splitter. 350 * 351 * <p>If the final character in the string to split is the delimiter then no empty string will 352 * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on 353 * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>. 354 */ 355 public static class SimpleStringSplitter implements StringSplitter, Iterator<String> { 356 private String mString; 357 private char mDelimiter; 358 private int mPosition; 359 private int mLength; 360 361 /** 362 * Initializes the splitter. setString may be called later. 363 * @param delimiter the delimeter on which to split 364 */ 365 public SimpleStringSplitter(char delimiter) { 366 mDelimiter = delimiter; 367 } 368 369 /** 370 * Sets the string to split 371 * @param string the string to split 372 */ 373 public void setString(String string) { 374 mString = string; 375 mPosition = 0; 376 mLength = mString.length(); 377 } 378 379 public Iterator<String> iterator() { 380 return this; 381 } 382 383 public boolean hasNext() { 384 return mPosition < mLength; 385 } 386 387 public String next() { 388 int end = mString.indexOf(mDelimiter, mPosition); 389 if (end == -1) { 390 end = mLength; 391 } 392 String nextString = mString.substring(mPosition, end); 393 mPosition = end + 1; // Skip the delimiter. 394 return nextString; 395 } 396 397 public void remove() { 398 throw new UnsupportedOperationException(); 399 } 400 } 401 402 public static CharSequence stringOrSpannedString(CharSequence source) { 403 if (source == null) 404 return null; 405 if (source instanceof SpannedString) 406 return source; 407 if (source instanceof Spanned) 408 return new SpannedString(source); 409 410 return source.toString(); 411 } 412 413 /** 414 * Returns true if the string is null or 0-length. 415 * @param str the string to be examined 416 * @return true if str is null or zero length 417 */ 418 public static boolean isEmpty(CharSequence str) { 419 if (str == null || str.length() == 0) 420 return true; 421 else 422 return false; 423 } 424 425 /** 426 * Returns the length that the specified CharSequence would have if 427 * spaces and control characters were trimmed from the start and end, 428 * as by {@link String#trim}. 429 */ 430 public static int getTrimmedLength(CharSequence s) { 431 int len = s.length(); 432 433 int start = 0; 434 while (start < len && s.charAt(start) <= ' ') { 435 start++; 436 } 437 438 int end = len; 439 while (end > start && s.charAt(end - 1) <= ' ') { 440 end--; 441 } 442 443 return end - start; 444 } 445 446 /** 447 * Returns true if a and b are equal, including if they are both null. 448 * 449 * @param a first CharSequence to check 450 * @param b second CharSequence to check 451 * @return true if a and b are equal 452 */ 453 public static boolean equals(CharSequence a, CharSequence b) { 454 return a == b || (a != null && a.equals(b)); 455 } 456 457 // XXX currently this only reverses chars, not spans 458 public static CharSequence getReverse(CharSequence source, 459 int start, int end) { 460 return new Reverser(source, start, end); 461 } 462 463 private static class Reverser 464 implements CharSequence, GetChars 465 { 466 public Reverser(CharSequence source, int start, int end) { 467 mSource = source; 468 mStart = start; 469 mEnd = end; 470 } 471 472 public int length() { 473 return mEnd - mStart; 474 } 475 476 public CharSequence subSequence(int start, int end) { 477 char[] buf = new char[end - start]; 478 479 getChars(start, end, buf, 0); 480 return new String(buf); 481 } 482 483 public String toString() { 484 return subSequence(0, length()).toString(); 485 } 486 487 public char charAt(int off) { 488 return AndroidCharacter.getMirror(mSource.charAt(mEnd - 1 - off)); 489 } 490 491 public void getChars(int start, int end, char[] dest, int destoff) { 492 TextUtils.getChars(mSource, start + mStart, end + mStart, 493 dest, destoff); 494 AndroidCharacter.mirror(dest, 0, end - start); 495 496 int len = end - start; 497 int n = (end - start) / 2; 498 for (int i = 0; i < n; i++) { 499 char tmp = dest[destoff + i]; 500 501 dest[destoff + i] = dest[destoff + len - i - 1]; 502 dest[destoff + len - i - 1] = tmp; 503 } 504 } 505 506 private CharSequence mSource; 507 private int mStart; 508 private int mEnd; 509 } 510 511 private static final int ALIGNMENT_SPAN = 1; 512 private static final int FOREGROUND_COLOR_SPAN = 2; 513 private static final int RELATIVE_SIZE_SPAN = 3; 514 private static final int SCALE_X_SPAN = 4; 515 private static final int STRIKETHROUGH_SPAN = 5; 516 private static final int UNDERLINE_SPAN = 6; 517 private static final int STYLE_SPAN = 7; 518 private static final int BULLET_SPAN = 8; 519 private static final int QUOTE_SPAN = 9; 520 private static final int LEADING_MARGIN_SPAN = 10; 521 private static final int URL_SPAN = 11; 522 private static final int BACKGROUND_COLOR_SPAN = 12; 523 private static final int TYPEFACE_SPAN = 13; 524 private static final int SUPERSCRIPT_SPAN = 14; 525 private static final int SUBSCRIPT_SPAN = 15; 526 private static final int ABSOLUTE_SIZE_SPAN = 16; 527 private static final int TEXT_APPEARANCE_SPAN = 17; 528 private static final int ANNOTATION = 18; 529 530 /** 531 * Flatten a CharSequence and whatever styles can be copied across processes 532 * into the parcel. 533 */ 534 public static void writeToParcel(CharSequence cs, Parcel p, 535 int parcelableFlags) { 536 if (cs instanceof Spanned) { 537 p.writeInt(0); 538 p.writeString(cs.toString()); 539 540 Spanned sp = (Spanned) cs; 541 Object[] os = sp.getSpans(0, cs.length(), Object.class); 542 543 // note to people adding to this: check more specific types 544 // before more generic types. also notice that it uses 545 // "if" instead of "else if" where there are interfaces 546 // so one object can be several. 547 548 for (int i = 0; i < os.length; i++) { 549 Object o = os[i]; 550 Object prop = os[i]; 551 552 if (prop instanceof CharacterStyle) { 553 prop = ((CharacterStyle) prop).getUnderlying(); 554 } 555 556 if (prop instanceof AlignmentSpan) { 557 p.writeInt(ALIGNMENT_SPAN); 558 p.writeString(((AlignmentSpan) prop).getAlignment().name()); 559 writeWhere(p, sp, o); 560 } 561 562 if (prop instanceof ForegroundColorSpan) { 563 p.writeInt(FOREGROUND_COLOR_SPAN); 564 p.writeInt(((ForegroundColorSpan) prop).getForegroundColor()); 565 writeWhere(p, sp, o); 566 } 567 568 if (prop instanceof RelativeSizeSpan) { 569 p.writeInt(RELATIVE_SIZE_SPAN); 570 p.writeFloat(((RelativeSizeSpan) prop).getSizeChange()); 571 writeWhere(p, sp, o); 572 } 573 574 if (prop instanceof ScaleXSpan) { 575 p.writeInt(SCALE_X_SPAN); 576 p.writeFloat(((ScaleXSpan) prop).getScaleX()); 577 writeWhere(p, sp, o); 578 } 579 580 if (prop instanceof StrikethroughSpan) { 581 p.writeInt(STRIKETHROUGH_SPAN); 582 writeWhere(p, sp, o); 583 } 584 585 if (prop instanceof UnderlineSpan) { 586 p.writeInt(UNDERLINE_SPAN); 587 writeWhere(p, sp, o); 588 } 589 590 if (prop instanceof StyleSpan) { 591 p.writeInt(STYLE_SPAN); 592 p.writeInt(((StyleSpan) prop).getStyle()); 593 writeWhere(p, sp, o); 594 } 595 596 if (prop instanceof LeadingMarginSpan) { 597 if (prop instanceof BulletSpan) { 598 p.writeInt(BULLET_SPAN); 599 writeWhere(p, sp, o); 600 } else if (prop instanceof QuoteSpan) { 601 p.writeInt(QUOTE_SPAN); 602 p.writeInt(((QuoteSpan) prop).getColor()); 603 writeWhere(p, sp, o); 604 } else { 605 p.writeInt(LEADING_MARGIN_SPAN); 606 p.writeInt(((LeadingMarginSpan) prop). 607 getLeadingMargin(true)); 608 p.writeInt(((LeadingMarginSpan) prop). 609 getLeadingMargin(false)); 610 writeWhere(p, sp, o); 611 } 612 } 613 614 if (prop instanceof URLSpan) { 615 p.writeInt(URL_SPAN); 616 p.writeString(((URLSpan) prop).getURL()); 617 writeWhere(p, sp, o); 618 } 619 620 if (prop instanceof BackgroundColorSpan) { 621 p.writeInt(BACKGROUND_COLOR_SPAN); 622 p.writeInt(((BackgroundColorSpan) prop).getBackgroundColor()); 623 writeWhere(p, sp, o); 624 } 625 626 if (prop instanceof TypefaceSpan) { 627 p.writeInt(TYPEFACE_SPAN); 628 p.writeString(((TypefaceSpan) prop).getFamily()); 629 writeWhere(p, sp, o); 630 } 631 632 if (prop instanceof SuperscriptSpan) { 633 p.writeInt(SUPERSCRIPT_SPAN); 634 writeWhere(p, sp, o); 635 } 636 637 if (prop instanceof SubscriptSpan) { 638 p.writeInt(SUBSCRIPT_SPAN); 639 writeWhere(p, sp, o); 640 } 641 642 if (prop instanceof AbsoluteSizeSpan) { 643 p.writeInt(ABSOLUTE_SIZE_SPAN); 644 p.writeInt(((AbsoluteSizeSpan) prop).getSize()); 645 writeWhere(p, sp, o); 646 } 647 648 if (prop instanceof TextAppearanceSpan) { 649 TextAppearanceSpan tas = (TextAppearanceSpan) prop; 650 p.writeInt(TEXT_APPEARANCE_SPAN); 651 652 String tf = tas.getFamily(); 653 if (tf != null) { 654 p.writeInt(1); 655 p.writeString(tf); 656 } else { 657 p.writeInt(0); 658 } 659 660 p.writeInt(tas.getTextSize()); 661 p.writeInt(tas.getTextStyle()); 662 663 ColorStateList csl = tas.getTextColor(); 664 if (csl == null) { 665 p.writeInt(0); 666 } else { 667 p.writeInt(1); 668 csl.writeToParcel(p, parcelableFlags); 669 } 670 671 csl = tas.getLinkTextColor(); 672 if (csl == null) { 673 p.writeInt(0); 674 } else { 675 p.writeInt(1); 676 csl.writeToParcel(p, parcelableFlags); 677 } 678 679 writeWhere(p, sp, o); 680 } 681 682 if (prop instanceof Annotation) { 683 p.writeInt(ANNOTATION); 684 p.writeString(((Annotation) prop).getKey()); 685 p.writeString(((Annotation) prop).getValue()); 686 writeWhere(p, sp, o); 687 } 688 } 689 690 p.writeInt(0); 691 } else { 692 p.writeInt(1); 693 if (cs != null) { 694 p.writeString(cs.toString()); 695 } else { 696 p.writeString(null); 697 } 698 } 699 } 700 701 private static void writeWhere(Parcel p, Spanned sp, Object o) { 702 p.writeInt(sp.getSpanStart(o)); 703 p.writeInt(sp.getSpanEnd(o)); 704 p.writeInt(sp.getSpanFlags(o)); 705 } 706 707 public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR 708 = new Parcelable.Creator<CharSequence>() 709 { 710 /** 711 * Read and return a new CharSequence, possibly with styles, 712 * from the parcel. 713 */ 714 public CharSequence createFromParcel(Parcel p) { 715 int kind = p.readInt(); 716 717 if (kind == 1) 718 return p.readString(); 719 720 SpannableString sp = new SpannableString(p.readString()); 721 722 while (true) { 723 kind = p.readInt(); 724 725 if (kind == 0) 726 break; 727 728 switch (kind) { 729 case ALIGNMENT_SPAN: 730 readSpan(p, sp, new AlignmentSpan.Standard( 731 Layout.Alignment.valueOf(p.readString()))); 732 break; 733 734 case FOREGROUND_COLOR_SPAN: 735 readSpan(p, sp, new ForegroundColorSpan(p.readInt())); 736 break; 737 738 case RELATIVE_SIZE_SPAN: 739 readSpan(p, sp, new RelativeSizeSpan(p.readFloat())); 740 break; 741 742 case SCALE_X_SPAN: 743 readSpan(p, sp, new ScaleXSpan(p.readFloat())); 744 break; 745 746 case STRIKETHROUGH_SPAN: 747 readSpan(p, sp, new StrikethroughSpan()); 748 break; 749 750 case UNDERLINE_SPAN: 751 readSpan(p, sp, new UnderlineSpan()); 752 break; 753 754 case STYLE_SPAN: 755 readSpan(p, sp, new StyleSpan(p.readInt())); 756 break; 757 758 case BULLET_SPAN: 759 readSpan(p, sp, new BulletSpan()); 760 break; 761 762 case QUOTE_SPAN: 763 readSpan(p, sp, new QuoteSpan(p.readInt())); 764 break; 765 766 case LEADING_MARGIN_SPAN: 767 readSpan(p, sp, new LeadingMarginSpan.Standard(p.readInt(), 768 p.readInt())); 769 break; 770 771 case URL_SPAN: 772 readSpan(p, sp, new URLSpan(p.readString())); 773 break; 774 775 case BACKGROUND_COLOR_SPAN: 776 readSpan(p, sp, new BackgroundColorSpan(p.readInt())); 777 break; 778 779 case TYPEFACE_SPAN: 780 readSpan(p, sp, new TypefaceSpan(p.readString())); 781 break; 782 783 case SUPERSCRIPT_SPAN: 784 readSpan(p, sp, new SuperscriptSpan()); 785 break; 786 787 case SUBSCRIPT_SPAN: 788 readSpan(p, sp, new SubscriptSpan()); 789 break; 790 791 case ABSOLUTE_SIZE_SPAN: 792 readSpan(p, sp, new AbsoluteSizeSpan(p.readInt())); 793 break; 794 795 case TEXT_APPEARANCE_SPAN: 796 readSpan(p, sp, new TextAppearanceSpan( 797 p.readInt() != 0 798 ? p.readString() 799 : null, 800 p.readInt(), 801 p.readInt(), 802 p.readInt() != 0 803 ? ColorStateList.CREATOR.createFromParcel(p) 804 : null, 805 p.readInt() != 0 806 ? ColorStateList.CREATOR.createFromParcel(p) 807 : null)); 808 break; 809 810 case ANNOTATION: 811 readSpan(p, sp, 812 new Annotation(p.readString(), p.readString())); 813 break; 814 815 default: 816 throw new RuntimeException("bogus span encoding " + kind); 817 } 818 } 819 820 return sp; 821 } 822 823 public CharSequence[] newArray(int size) 824 { 825 return new CharSequence[size]; 826 } 827 }; 828 829 /** 830 * Return a new CharSequence in which each of the source strings is 831 * replaced by the corresponding element of the destinations. 832 */ 833 public static CharSequence replace(CharSequence template, 834 String[] sources, 835 CharSequence[] destinations) { 836 SpannableStringBuilder tb = new SpannableStringBuilder(template); 837 838 for (int i = 0; i < sources.length; i++) { 839 int where = indexOf(tb, sources[i]); 840 841 if (where >= 0) 842 tb.setSpan(sources[i], where, where + sources[i].length(), 843 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 844 } 845 846 for (int i = 0; i < sources.length; i++) { 847 int start = tb.getSpanStart(sources[i]); 848 int end = tb.getSpanEnd(sources[i]); 849 850 if (start >= 0) { 851 tb.replace(start, end, destinations[i]); 852 } 853 } 854 855 return tb; 856 } 857 858 /** 859 * Replace instances of "^1", "^2", etc. in the 860 * <code>template</code> CharSequence with the corresponding 861 * <code>values</code>. "^^" is used to produce a single caret in 862 * the output. Only up to 9 replacement values are supported, 863 * "^10" will be produce the first replacement value followed by a 864 * '0'. 865 * 866 * @param template the input text containing "^1"-style 867 * placeholder values. This object is not modified; a copy is 868 * returned. 869 * 870 * @param values CharSequences substituted into the template. The 871 * first is substituted for "^1", the second for "^2", and so on. 872 * 873 * @return the new CharSequence produced by doing the replacement 874 * 875 * @throws IllegalArgumentException if the template requests a 876 * value that was not provided, or if more than 9 values are 877 * provided. 878 */ 879 public static CharSequence expandTemplate(CharSequence template, 880 CharSequence... values) { 881 if (values.length > 9) { 882 throw new IllegalArgumentException("max of 9 values are supported"); 883 } 884 885 SpannableStringBuilder ssb = new SpannableStringBuilder(template); 886 887 try { 888 int i = 0; 889 while (i < ssb.length()) { 890 if (ssb.charAt(i) == '^') { 891 char next = ssb.charAt(i+1); 892 if (next == '^') { 893 ssb.delete(i+1, i+2); 894 ++i; 895 continue; 896 } else if (Character.isDigit(next)) { 897 int which = Character.getNumericValue(next) - 1; 898 if (which < 0) { 899 throw new IllegalArgumentException( 900 "template requests value ^" + (which+1)); 901 } 902 if (which >= values.length) { 903 throw new IllegalArgumentException( 904 "template requests value ^" + (which+1) + 905 "; only " + values.length + " provided"); 906 } 907 ssb.replace(i, i+2, values[which]); 908 i += values[which].length(); 909 continue; 910 } 911 } 912 ++i; 913 } 914 } catch (IndexOutOfBoundsException ignore) { 915 // happens when ^ is the last character in the string. 916 } 917 return ssb; 918 } 919 920 public static int getOffsetBefore(CharSequence text, int offset) { 921 if (offset == 0) 922 return 0; 923 if (offset == 1) 924 return 0; 925 926 char c = text.charAt(offset - 1); 927 928 if (c >= '\uDC00' && c <= '\uDFFF') { 929 char c1 = text.charAt(offset - 2); 930 931 if (c1 >= '\uD800' && c1 <= '\uDBFF') 932 offset -= 2; 933 else 934 offset -= 1; 935 } else { 936 offset -= 1; 937 } 938 939 if (text instanceof Spanned) { 940 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, 941 ReplacementSpan.class); 942 943 for (int i = 0; i < spans.length; i++) { 944 int start = ((Spanned) text).getSpanStart(spans[i]); 945 int end = ((Spanned) text).getSpanEnd(spans[i]); 946 947 if (start < offset && end > offset) 948 offset = start; 949 } 950 } 951 952 return offset; 953 } 954 955 public static int getOffsetAfter(CharSequence text, int offset) { 956 int len = text.length(); 957 958 if (offset == len) 959 return len; 960 if (offset == len - 1) 961 return len; 962 963 char c = text.charAt(offset); 964 965 if (c >= '\uD800' && c <= '\uDBFF') { 966 char c1 = text.charAt(offset + 1); 967 968 if (c1 >= '\uDC00' && c1 <= '\uDFFF') 969 offset += 2; 970 else 971 offset += 1; 972 } else { 973 offset += 1; 974 } 975 976 if (text instanceof Spanned) { 977 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, 978 ReplacementSpan.class); 979 980 for (int i = 0; i < spans.length; i++) { 981 int start = ((Spanned) text).getSpanStart(spans[i]); 982 int end = ((Spanned) text).getSpanEnd(spans[i]); 983 984 if (start < offset && end > offset) 985 offset = end; 986 } 987 } 988 989 return offset; 990 } 991 992 private static void readSpan(Parcel p, Spannable sp, Object o) { 993 sp.setSpan(o, p.readInt(), p.readInt(), p.readInt()); 994 } 995 996 public static void copySpansFrom(Spanned source, int start, int end, 997 Class kind, 998 Spannable dest, int destoff) { 999 if (kind == null) { 1000 kind = Object.class; 1001 } 1002 1003 Object[] spans = source.getSpans(start, end, kind); 1004 1005 for (int i = 0; i < spans.length; i++) { 1006 int st = source.getSpanStart(spans[i]); 1007 int en = source.getSpanEnd(spans[i]); 1008 int fl = source.getSpanFlags(spans[i]); 1009 1010 if (st < start) 1011 st = start; 1012 if (en > end) 1013 en = end; 1014 1015 dest.setSpan(spans[i], st - start + destoff, en - start + destoff, 1016 fl); 1017 } 1018 } 1019 1020 public enum TruncateAt { 1021 START, 1022 MIDDLE, 1023 END, 1024 } 1025 1026 public interface EllipsizeCallback { 1027 /** 1028 * This method is called to report that the specified region of 1029 * text was ellipsized away by a call to {@link #ellipsize}. 1030 */ 1031 public void ellipsized(int start, int end); 1032 } 1033 1034 private static String sEllipsis = null; 1035 1036 /** 1037 * Returns the original text if it fits in the specified width 1038 * given the properties of the specified Paint, 1039 * or, if it does not fit, a truncated 1040 * copy with ellipsis character added at the specified edge or center. 1041 */ 1042 public static CharSequence ellipsize(CharSequence text, 1043 TextPaint p, 1044 float avail, TruncateAt where) { 1045 return ellipsize(text, p, avail, where, false, null); 1046 } 1047 1048 /** 1049 * Returns the original text if it fits in the specified width 1050 * given the properties of the specified Paint, 1051 * or, if it does not fit, a copy with ellipsis character added 1052 * at the specified edge or center. 1053 * If <code>preserveLength</code> is specified, the returned copy 1054 * will be padded with zero-width spaces to preserve the original 1055 * length and offsets instead of truncating. 1056 * If <code>callback</code> is non-null, it will be called to 1057 * report the start and end of the ellipsized range. 1058 */ 1059 public static CharSequence ellipsize(CharSequence text, 1060 TextPaint p, 1061 float avail, TruncateAt where, 1062 boolean preserveLength, 1063 EllipsizeCallback callback) { 1064 if (sEllipsis == null) { 1065 Resources r = Resources.getSystem(); 1066 sEllipsis = r.getString(R.string.ellipsis); 1067 } 1068 1069 int len = text.length(); 1070 1071 // Use Paint.breakText() for the non-Spanned case to avoid having 1072 // to allocate memory and accumulate the character widths ourselves. 1073 1074 if (!(text instanceof Spanned)) { 1075 float wid = p.measureText(text, 0, len); 1076 1077 if (wid <= avail) { 1078 if (callback != null) { 1079 callback.ellipsized(0, 0); 1080 } 1081 1082 return text; 1083 } 1084 1085 float ellipsiswid = p.measureText(sEllipsis); 1086 1087 if (ellipsiswid > avail) { 1088 if (callback != null) { 1089 callback.ellipsized(0, len); 1090 } 1091 1092 if (preserveLength) { 1093 char[] buf = obtain(len); 1094 for (int i = 0; i < len; i++) { 1095 buf[i] = '\uFEFF'; 1096 } 1097 String ret = new String(buf, 0, len); 1098 recycle(buf); 1099 return ret; 1100 } else { 1101 return ""; 1102 } 1103 } 1104 1105 if (where == TruncateAt.START) { 1106 int fit = p.breakText(text, 0, len, false, 1107 avail - ellipsiswid, null); 1108 1109 if (callback != null) { 1110 callback.ellipsized(0, len - fit); 1111 } 1112 1113 if (preserveLength) { 1114 return blank(text, 0, len - fit); 1115 } else { 1116 return sEllipsis + text.toString().substring(len - fit, len); 1117 } 1118 } else if (where == TruncateAt.END) { 1119 int fit = p.breakText(text, 0, len, true, 1120 avail - ellipsiswid, null); 1121 1122 if (callback != null) { 1123 callback.ellipsized(fit, len); 1124 } 1125 1126 if (preserveLength) { 1127 return blank(text, fit, len); 1128 } else { 1129 return text.toString().substring(0, fit) + sEllipsis; 1130 } 1131 } else /* where == TruncateAt.MIDDLE */ { 1132 int right = p.breakText(text, 0, len, false, 1133 (avail - ellipsiswid) / 2, null); 1134 float used = p.measureText(text, len - right, len); 1135 int left = p.breakText(text, 0, len - right, true, 1136 avail - ellipsiswid - used, null); 1137 1138 if (callback != null) { 1139 callback.ellipsized(left, len - right); 1140 } 1141 1142 if (preserveLength) { 1143 return blank(text, left, len - right); 1144 } else { 1145 String s = text.toString(); 1146 return s.substring(0, left) + sEllipsis + 1147 s.substring(len - right, len); 1148 } 1149 } 1150 } 1151 1152 // But do the Spanned cases by hand, because it's such a pain 1153 // to iterate the span transitions backwards and getTextWidths() 1154 // will give us the information we need. 1155 1156 // getTextWidths() always writes into the start of the array, 1157 // so measure each span into the first half and then copy the 1158 // results into the second half to use later. 1159 1160 float[] wid = new float[len * 2]; 1161 TextPaint temppaint = new TextPaint(); 1162 Spanned sp = (Spanned) text; 1163 1164 int next; 1165 for (int i = 0; i < len; i = next) { 1166 next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class); 1167 1168 Styled.getTextWidths(p, temppaint, sp, i, next, wid, null); 1169 System.arraycopy(wid, 0, wid, len + i, next - i); 1170 } 1171 1172 float sum = 0; 1173 for (int i = 0; i < len; i++) { 1174 sum += wid[len + i]; 1175 } 1176 1177 if (sum <= avail) { 1178 if (callback != null) { 1179 callback.ellipsized(0, 0); 1180 } 1181 1182 return text; 1183 } 1184 1185 float ellipsiswid = p.measureText(sEllipsis); 1186 1187 if (ellipsiswid > avail) { 1188 if (callback != null) { 1189 callback.ellipsized(0, len); 1190 } 1191 1192 if (preserveLength) { 1193 char[] buf = obtain(len); 1194 for (int i = 0; i < len; i++) { 1195 buf[i] = '\uFEFF'; 1196 } 1197 SpannableString ss = new SpannableString(new String(buf, 0, len)); 1198 recycle(buf); 1199 copySpansFrom(sp, 0, len, Object.class, ss, 0); 1200 return ss; 1201 } else { 1202 return ""; 1203 } 1204 } 1205 1206 if (where == TruncateAt.START) { 1207 sum = 0; 1208 int i; 1209 1210 for (i = len; i >= 0; i--) { 1211 float w = wid[len + i - 1]; 1212 1213 if (w + sum + ellipsiswid > avail) { 1214 break; 1215 } 1216 1217 sum += w; 1218 } 1219 1220 if (callback != null) { 1221 callback.ellipsized(0, i); 1222 } 1223 1224 if (preserveLength) { 1225 SpannableString ss = new SpannableString(blank(text, 0, i)); 1226 copySpansFrom(sp, 0, len, Object.class, ss, 0); 1227 return ss; 1228 } else { 1229 SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis); 1230 out.insert(1, text, i, len); 1231 1232 return out; 1233 } 1234 } else if (where == TruncateAt.END) { 1235 sum = 0; 1236 int i; 1237 1238 for (i = 0; i < len; i++) { 1239 float w = wid[len + i]; 1240 1241 if (w + sum + ellipsiswid > avail) { 1242 break; 1243 } 1244 1245 sum += w; 1246 } 1247 1248 if (callback != null) { 1249 callback.ellipsized(i, len); 1250 } 1251 1252 if (preserveLength) { 1253 SpannableString ss = new SpannableString(blank(text, i, len)); 1254 copySpansFrom(sp, 0, len, Object.class, ss, 0); 1255 return ss; 1256 } else { 1257 SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis); 1258 out.insert(0, text, 0, i); 1259 1260 return out; 1261 } 1262 } else /* where = TruncateAt.MIDDLE */ { 1263 float lsum = 0, rsum = 0; 1264 int left = 0, right = len; 1265 1266 float ravail = (avail - ellipsiswid) / 2; 1267 for (right = len; right >= 0; right--) { 1268 float w = wid[len + right - 1]; 1269 1270 if (w + rsum > ravail) { 1271 break; 1272 } 1273 1274 rsum += w; 1275 } 1276 1277 float lavail = avail - ellipsiswid - rsum; 1278 for (left = 0; left < right; left++) { 1279 float w = wid[len + left]; 1280 1281 if (w + lsum > lavail) { 1282 break; 1283 } 1284 1285 lsum += w; 1286 } 1287 1288 if (callback != null) { 1289 callback.ellipsized(left, right); 1290 } 1291 1292 if (preserveLength) { 1293 SpannableString ss = new SpannableString(blank(text, left, right)); 1294 copySpansFrom(sp, 0, len, Object.class, ss, 0); 1295 return ss; 1296 } else { 1297 SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis); 1298 out.insert(0, text, 0, left); 1299 out.insert(out.length(), text, right, len); 1300 1301 return out; 1302 } 1303 } 1304 } 1305 1306 private static String blank(CharSequence source, int start, int end) { 1307 int len = source.length(); 1308 char[] buf = obtain(len); 1309 1310 if (start != 0) { 1311 getChars(source, 0, start, buf, 0); 1312 } 1313 if (end != len) { 1314 getChars(source, end, len, buf, end); 1315 } 1316 1317 if (start != end) { 1318 buf[start] = '\u2026'; 1319 1320 for (int i = start + 1; i < end; i++) { 1321 buf[i] = '\uFEFF'; 1322 } 1323 } 1324 1325 String ret = new String(buf, 0, len); 1326 recycle(buf); 1327 1328 return ret; 1329 } 1330 1331 /** 1332 * Converts a CharSequence of the comma-separated form "Andy, Bob, 1333 * Charles, David" that is too wide to fit into the specified width 1334 * into one like "Andy, Bob, 2 more". 1335 * 1336 * @param text the text to truncate 1337 * @param p the Paint with which to measure the text 1338 * @param avail the horizontal width available for the text 1339 * @param oneMore the string for "1 more" in the current locale 1340 * @param more the string for "%d more" in the current locale 1341 */ 1342 public static CharSequence commaEllipsize(CharSequence text, 1343 TextPaint p, float avail, 1344 String oneMore, 1345 String more) { 1346 int len = text.length(); 1347 char[] buf = new char[len]; 1348 TextUtils.getChars(text, 0, len, buf, 0); 1349 1350 int commaCount = 0; 1351 for (int i = 0; i < len; i++) { 1352 if (buf[i] == ',') { 1353 commaCount++; 1354 } 1355 } 1356 1357 float[] wid; 1358 1359 if (text instanceof Spanned) { 1360 Spanned sp = (Spanned) text; 1361 TextPaint temppaint = new TextPaint(); 1362 wid = new float[len * 2]; 1363 1364 int next; 1365 for (int i = 0; i < len; i = next) { 1366 next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class); 1367 1368 Styled.getTextWidths(p, temppaint, sp, i, next, wid, null); 1369 System.arraycopy(wid, 0, wid, len + i, next - i); 1370 } 1371 1372 System.arraycopy(wid, len, wid, 0, len); 1373 } else { 1374 wid = new float[len]; 1375 p.getTextWidths(text, 0, len, wid); 1376 } 1377 1378 int ok = 0; 1379 int okRemaining = commaCount + 1; 1380 String okFormat = ""; 1381 1382 int w = 0; 1383 int count = 0; 1384 1385 for (int i = 0; i < len; i++) { 1386 w += wid[i]; 1387 1388 if (buf[i] == ',') { 1389 count++; 1390 1391 int remaining = commaCount - count + 1; 1392 float moreWid; 1393 String format; 1394 1395 if (remaining == 1) { 1396 format = " " + oneMore; 1397 } else { 1398 format = " " + String.format(more, remaining); 1399 } 1400 1401 moreWid = p.measureText(format); 1402 1403 if (w + moreWid <= avail) { 1404 ok = i + 1; 1405 okRemaining = remaining; 1406 okFormat = format; 1407 } 1408 } 1409 } 1410 1411 if (w <= avail) { 1412 return text; 1413 } else { 1414 SpannableStringBuilder out = new SpannableStringBuilder(okFormat); 1415 out.insert(0, text, 0, ok); 1416 return out; 1417 } 1418 } 1419 1420 /* package */ static char[] obtain(int len) { 1421 char[] buf; 1422 1423 synchronized (sLock) { 1424 buf = sTemp; 1425 sTemp = null; 1426 } 1427 1428 if (buf == null || buf.length < len) 1429 buf = new char[ArrayUtils.idealCharArraySize(len)]; 1430 1431 return buf; 1432 } 1433 1434 /* package */ static void recycle(char[] temp) { 1435 if (temp.length > 1000) 1436 return; 1437 1438 synchronized (sLock) { 1439 sTemp = temp; 1440 } 1441 } 1442 1443 /** 1444 * Html-encode the string. 1445 * @param s the string to be encoded 1446 * @return the encoded string 1447 */ 1448 public static String htmlEncode(String s) { 1449 StringBuilder sb = new StringBuilder(); 1450 char c; 1451 for (int i = 0; i < s.length(); i++) { 1452 c = s.charAt(i); 1453 switch (c) { 1454 case '<': 1455 sb.append("<"); //$NON-NLS-1$ 1456 break; 1457 case '>': 1458 sb.append(">"); //$NON-NLS-1$ 1459 break; 1460 case '&': 1461 sb.append("&"); //$NON-NLS-1$ 1462 break; 1463 case '\\': 1464 sb.append("'"); //$NON-NLS-1$ 1465 break; 1466 case '"': 1467 sb.append("""); //$NON-NLS-1$ 1468 break; 1469 default: 1470 sb.append(c); 1471 } 1472 } 1473 return sb.toString(); 1474 } 1475 1476 /** 1477 * Returns a CharSequence concatenating the specified CharSequences, 1478 * retaining their spans if any. 1479 */ 1480 public static CharSequence concat(CharSequence... text) { 1481 if (text.length == 0) { 1482 return ""; 1483 } 1484 1485 if (text.length == 1) { 1486 return text[0]; 1487 } 1488 1489 boolean spanned = false; 1490 for (int i = 0; i < text.length; i++) { 1491 if (text[i] instanceof Spanned) { 1492 spanned = true; 1493 break; 1494 } 1495 } 1496 1497 StringBuilder sb = new StringBuilder(); 1498 for (int i = 0; i < text.length; i++) { 1499 sb.append(text[i]); 1500 } 1501 1502 if (!spanned) { 1503 return sb.toString(); 1504 } 1505 1506 SpannableString ss = new SpannableString(sb); 1507 int off = 0; 1508 for (int i = 0; i < text.length; i++) { 1509 int len = text[i].length(); 1510 1511 if (text[i] instanceof Spanned) { 1512 copySpansFrom((Spanned) text[i], 0, len, Object.class, ss, off); 1513 } 1514 1515 off += len; 1516 } 1517 1518 return new SpannedString(ss); 1519 } 1520 1521 /** 1522 * Returns whether the given CharSequence contains any printable characters. 1523 */ 1524 public static boolean isGraphic(CharSequence str) { 1525 final int len = str.length(); 1526 for (int i=0; i<len; i++) { 1527 int gc = Character.getType(str.charAt(i)); 1528 if (gc != Character.CONTROL 1529 && gc != Character.FORMAT 1530 && gc != Character.SURROGATE 1531 && gc != Character.UNASSIGNED 1532 && gc != Character.LINE_SEPARATOR 1533 && gc != Character.PARAGRAPH_SEPARATOR 1534 && gc != Character.SPACE_SEPARATOR) { 1535 return true; 1536 } 1537 } 1538 return false; 1539 } 1540 1541 /** 1542 * Returns whether this character is a printable character. 1543 */ 1544 public static boolean isGraphic(char c) { 1545 int gc = Character.getType(c); 1546 return gc != Character.CONTROL 1547 && gc != Character.FORMAT 1548 && gc != Character.SURROGATE 1549 && gc != Character.UNASSIGNED 1550 && gc != Character.LINE_SEPARATOR 1551 && gc != Character.PARAGRAPH_SEPARATOR 1552 && gc != Character.SPACE_SEPARATOR; 1553 } 1554 1555 /** 1556 * Returns whether the given CharSequence contains only digits. 1557 */ 1558 public static boolean isDigitsOnly(CharSequence str) { 1559 final int len = str.length(); 1560 for (int i = 0; i < len; i++) { 1561 if (!Character.isDigit(str.charAt(i))) { 1562 return false; 1563 } 1564 } 1565 return true; 1566 } 1567 1568 private static Object sLock = new Object(); 1569 private static char[] sTemp = null; 1570} 1571