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