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