All Downloads are FREE. Search and download functionalities are using the official Maven repository.

android.text.TextUtils Maven / Gradle / Ivy

/*
 * Copyright (C) 2006 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.text;

import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.icu.lang.UCharacter;
import android.icu.text.CaseMap;
import android.icu.text.Edits;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.style.*;
import android.util.Log;
import android.util.Printer;
import android.view.View;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;

import java.lang.reflect.Array;
import java.util.Iterator;
import java.util.Locale;
import java.util.regex.Pattern;

public class TextUtils {
    private static final String TAG = "TextUtils";

    // Zero-width character used to fill ellipsized strings when codepoint lenght must be preserved.
    /* package */ static final char ELLIPSIS_FILLER = '\uFEFF'; // ZERO WIDTH NO-BREAK SPACE

    // TODO: Based on CLDR data, these need to be localized for Dzongkha (dz) and perhaps
    // Hong Kong Traditional Chinese (zh-Hant-HK), but that may need to depend on the actual word
    // being ellipsized and not the locale.
    private static final String ELLIPSIS_NORMAL = "\u2026"; // HORIZONTAL ELLIPSIS (…)
    private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // TWO DOT LEADER (‥)

    /** {} */
    @NonNull
    public static String getEllipsisString(@NonNull TruncateAt method) {
        return (method == TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL;
    }


    private TextUtils() { /* cannot be instantiated */ }

    public static void getChars(CharSequence s, int start, int end,
                                char[] dest, int destoff) {
        Class c = s.getClass();

        if (c == String.class)
            ((String) s).getChars(start, end, dest, destoff);
        else if (c == StringBuffer.class)
            ((StringBuffer) s).getChars(start, end, dest, destoff);
        else if (c == StringBuilder.class)
            ((StringBuilder) s).getChars(start, end, dest, destoff);
        else if (s instanceof GetChars)
            ((GetChars) s).getChars(start, end, dest, destoff);
        else {
            for (int i = start; i < end; i++)
                dest[destoff++] = s.charAt(i);
        }
    }

    public static int indexOf(CharSequence s, char ch) {
        return indexOf(s, ch, 0);
    }

    public static int indexOf(CharSequence s, char ch, int start) {
        Class c = s.getClass();

        if (c == String.class)
            return ((String) s).indexOf(ch, start);

        return indexOf(s, ch, start, s.length());
    }

    public static int indexOf(CharSequence s, char ch, int start, int end) {
        Class c = s.getClass();

        if (s instanceof GetChars || c == StringBuffer.class ||
            c == StringBuilder.class || c == String.class) {
            final int INDEX_INCREMENT = 500;
            char[] temp = obtain(INDEX_INCREMENT);

            while (start < end) {
                int segend = start + INDEX_INCREMENT;
                if (segend > end)
                    segend = end;

                getChars(s, start, segend, temp, 0);

                int count = segend - start;
                for (int i = 0; i < count; i++) {
                    if (temp[i] == ch) {
                        recycle(temp);
                        return i + start;
                    }
                }

                start = segend;
            }

            recycle(temp);
            return -1;
        }

        for (int i = start; i < end; i++)
            if (s.charAt(i) == ch)
                return i;

        return -1;
    }

    public static int lastIndexOf(CharSequence s, char ch) {
        return lastIndexOf(s, ch, s.length() - 1);
    }

    public static int lastIndexOf(CharSequence s, char ch, int last) {
        Class c = s.getClass();

        if (c == String.class)
            return ((String) s).lastIndexOf(ch, last);

        return lastIndexOf(s, ch, 0, last);
    }

    public static int lastIndexOf(CharSequence s, char ch,
                                  int start, int last) {
        if (last < 0)
            return -1;
        if (last >= s.length())
            last = s.length() - 1;

        int end = last + 1;

        Class c = s.getClass();

        if (s instanceof GetChars || c == StringBuffer.class ||
            c == StringBuilder.class || c == String.class) {
            final int INDEX_INCREMENT = 500;
            char[] temp = obtain(INDEX_INCREMENT);

            while (start < end) {
                int segstart = end - INDEX_INCREMENT;
                if (segstart < start)
                    segstart = start;

                getChars(s, segstart, end, temp, 0);

                int count = end - segstart;
                for (int i = count - 1; i >= 0; i--) {
                    if (temp[i] == ch) {
                        recycle(temp);
                        return i + segstart;
                    }
                }

                end = segstart;
            }

            recycle(temp);
            return -1;
        }

        for (int i = end - 1; i >= start; i--)
            if (s.charAt(i) == ch)
                return i;

        return -1;
    }

    public static int indexOf(CharSequence s, CharSequence needle) {
        return indexOf(s, needle, 0, s.length());
    }

    public static int indexOf(CharSequence s, CharSequence needle, int start) {
        return indexOf(s, needle, start, s.length());
    }

    public static int indexOf(CharSequence s, CharSequence needle,
                              int start, int end) {
        int nlen = needle.length();
        if (nlen == 0)
            return start;

        char c = needle.charAt(0);

        for (;;) {
            start = indexOf(s, c, start);
            if (start > end - nlen) {
                break;
            }

            if (start < 0) {
                return -1;
            }

            if (regionMatches(s, start, needle, 0, nlen)) {
                return start;
            }

            start++;
        }
        return -1;
    }

    public static boolean regionMatches(CharSequence one, int toffset,
                                        CharSequence two, int ooffset,
                                        int len) {
        int tempLen = 2 * len;
        if (tempLen < len) {
            // Integer overflow; len is unreasonably large
            throw new IndexOutOfBoundsException();
        }
        char[] temp = obtain(tempLen);

        getChars(one, toffset, toffset + len, temp, 0);
        getChars(two, ooffset, ooffset + len, temp, len);

        boolean match = true;
        for (int i = 0; i < len; i++) {
            if (temp[i] != temp[i + len]) {
                match = false;
                break;
            }
        }

        recycle(temp);
        return match;
    }

    /**
     * Create a new String object containing the given range of characters
     * from the source string.  This is different than simply calling
     * {@link CharSequence#subSequence(int, int) CharSequence.subSequence}
     * in that it does not preserve any style runs in the source sequence,
     * allowing a more efficient implementation.
     */
    public static String substring(CharSequence source, int start, int end) {
        if (source instanceof String)
            return ((String) source).substring(start, end);
        if (source instanceof StringBuilder)
            return ((StringBuilder) source).substring(start, end);
        if (source instanceof StringBuffer)
            return ((StringBuffer) source).substring(start, end);

        char[] temp = obtain(end - start);
        getChars(source, start, end, temp, 0);
        String ret = new String(temp, 0, end - start);
        recycle(temp);

        return ret;
    }

    /**
     * Returns a string containing the tokens joined by delimiters.
     *
     * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string
     *     "null" will be used as the delimiter.
     * @param tokens an array objects to be joined. Strings will be formed from the objects by
     *     calling object.toString(). If tokens is null, a NullPointerException will be thrown. If
     *     tokens is an empty array, an empty string will be returned.
     */
    public static String join(@NonNull CharSequence delimiter, @NonNull Object[] tokens) {
        final int length = tokens.length;
        if (length == 0) {
            return "";
        }
        final StringBuilder sb = new StringBuilder();
        sb.append(tokens[0]);
        for (int i = 1; i < length; i++) {
            sb.append(delimiter);
            sb.append(tokens[i]);
        }
        return sb.toString();
    }

    /**
     * Returns a string containing the tokens joined by delimiters.
     *
     * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string
     *     "null" will be used as the delimiter.
     * @param tokens an array objects to be joined. Strings will be formed from the objects by
     *     calling object.toString(). If tokens is null, a NullPointerException will be thrown. If
     *     tokens is empty, an empty string will be returned.
     */
    public static String join(@NonNull CharSequence delimiter, @NonNull Iterable tokens) {
        final Iterator it = tokens.iterator();
        if (!it.hasNext()) {
            return "";
        }
        final StringBuilder sb = new StringBuilder();
        sb.append(it.next());
        while (it.hasNext()) {
            sb.append(delimiter);
            sb.append(it.next());
        }
        return sb.toString();
    }

    /**
     * String.split() returns [''] when the string to be split is empty. This returns []. This does
     * not remove any empty strings from the result. For example split("a,", ","  ) returns {"a", ""}.
     *
     * @param text the string to split
     * @param expression the regular expression to match
     * @return an array of strings. The array will be empty if text is empty
     *
     * @throws NullPointerException if expression or text is null
     */
    public static String[] split(String text, String expression) {
        if (text.length() == 0) {
            return EMPTY_STRING_ARRAY;
        } else {
            return text.split(expression, -1);
        }
    }

    /**
     * Splits a string on a pattern. String.split() returns [''] when the string to be
     * split is empty. This returns []. This does not remove any empty strings from the result.
     * @param text the string to split
     * @param pattern the regular expression to match
     * @return an array of strings. The array will be empty if text is empty
     *
     * @throws NullPointerException if expression or text is null
     */
    public static String[] split(String text, Pattern pattern) {
        if (text.length() == 0) {
            return EMPTY_STRING_ARRAY;
        } else {
            return pattern.split(text, -1);
        }
    }

    /**
     * An interface for splitting strings according to rules that are opaque to the user of this
     * interface. This also has less overhead than split, which uses regular expressions and
     * allocates an array to hold the results.
     *
     * 

The most efficient way to use this class is: * *

     * // Once
     * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
     *
     * // Once per string to split
     * splitter.setString(string);
     * for (String s : splitter) {
     *     ...
     * }
     * 
*/ public interface StringSplitter extends Iterable { public void setString(String string); } /** * A simple string splitter. * *

If the final character in the string to split is the delimiter then no empty string will * be returned for the empty string after that delimeter. That is, splitting "a,b," on * comma will return "a", "b", not "a", "b", "". */ public static class SimpleStringSplitter implements StringSplitter, Iterator { private String mString; private char mDelimiter; private int mPosition; private int mLength; /** * Initializes the splitter. setString may be called later. * @param delimiter the delimeter on which to split */ public SimpleStringSplitter(char delimiter) { mDelimiter = delimiter; } /** * Sets the string to split * @param string the string to split */ public void setString(String string) { mString = string; mPosition = 0; mLength = mString.length(); } public Iterator iterator() { return this; } public boolean hasNext() { return mPosition < mLength; } public String next() { int end = mString.indexOf(mDelimiter, mPosition); if (end == -1) { end = mLength; } String nextString = mString.substring(mPosition, end); mPosition = end + 1; // Skip the delimiter. return nextString; } public void remove() { throw new UnsupportedOperationException(); } } public static CharSequence stringOrSpannedString(CharSequence source) { if (source == null) return null; if (source instanceof SpannedString) return source; if (source instanceof Spanned) return new SpannedString(source); return source.toString(); } /** * Returns true if the string is null or 0-length. * @param str the string to be examined * @return true if str is null or zero length */ public static boolean isEmpty(@Nullable CharSequence str) { return str == null || str.length() == 0; } /** {} */ public static String nullIfEmpty(@Nullable String str) { return isEmpty(str) ? null : str; } /** {} */ public static String emptyIfNull(@Nullable String str) { return str == null ? "" : str; } /** {} */ public static String firstNotEmpty(@Nullable String a, @NonNull String b) { return !isEmpty(a) ? a : Preconditions.checkStringNotEmpty(b); } /** {} */ public static int length(@Nullable String s) { return isEmpty(s) ? 0 : s.length(); } /** * @return interned string if it's null. * */ public static String safeIntern(String s) { return (s != null) ? s.intern() : null; } /** * Returns the length that the specified CharSequence would have if * spaces and ASCII control characters were trimmed from the start and end, * as by {@link String#trim}. */ public static int getTrimmedLength(CharSequence s) { int len = s.length(); int start = 0; while (start < len && s.charAt(start) <= ' ') { start++; } int end = len; while (end > start && s.charAt(end - 1) <= ' ') { end--; } return end - start; } /** * Returns true if a and b are equal, including if they are both null. *

Note: In platform versions 1.1 and earlier, this method only worked well if * both the arguments were instances of String.

* @param a first CharSequence to check * @param b second CharSequence to check * @return true if a and b are equal */ public static boolean equals(CharSequence a, CharSequence b) { if (a == b) return true; int length; if (a != null && b != null && (length = a.length()) == b.length()) { if (a instanceof String && b instanceof String) { return a.equals(b); } else { for (int i = 0; i < length; i++) { if (a.charAt(i) != b.charAt(i)) return false; } return true; } } return false; } /** * This function only reverses individual {@code char}s and not their associated * spans. It doesn't support surrogate pairs (that correspond to non-BMP code points), combining * sequences or conjuncts either. * @deprecated Do not use. */ @Deprecated public static CharSequence getReverse(CharSequence source, int start, int end) { return new Reverser(source, start, end); } private static class Reverser implements CharSequence, GetChars { public Reverser(CharSequence source, int start, int end) { mSource = source; mStart = start; mEnd = end; } public int length() { return mEnd - mStart; } public CharSequence subSequence(int start, int end) { char[] buf = new char[end - start]; getChars(start, end, buf, 0); return new String(buf); } @Override public String toString() { return subSequence(0, length()).toString(); } public char charAt(int off) { return (char) UCharacter.getMirror(mSource.charAt(mEnd - 1 - off)); } @SuppressWarnings("deprecation") public void getChars(int start, int end, char[] dest, int destoff) { TextUtils.getChars(mSource, start + mStart, end + mStart, dest, destoff); AndroidCharacter.mirror(dest, 0, end - start); int len = end - start; int n = (end - start) / 2; for (int i = 0; i < n; i++) { char tmp = dest[destoff + i]; dest[destoff + i] = dest[destoff + len - i - 1]; dest[destoff + len - i - 1] = tmp; } } private CharSequence mSource; private int mStart; private int mEnd; } /** */ public static final int ALIGNMENT_SPAN = 1; /** */ public static final int FIRST_SPAN = ALIGNMENT_SPAN; /** */ public static final int FOREGROUND_COLOR_SPAN = 2; /** */ public static final int RELATIVE_SIZE_SPAN = 3; /** */ public static final int SCALE_X_SPAN = 4; /** */ public static final int STRIKETHROUGH_SPAN = 5; /** */ public static final int UNDERLINE_SPAN = 6; /** */ public static final int STYLE_SPAN = 7; /** */ public static final int BULLET_SPAN = 8; /** */ public static final int QUOTE_SPAN = 9; /** */ public static final int LEADING_MARGIN_SPAN = 10; /** */ public static final int URL_SPAN = 11; /** */ public static final int BACKGROUND_COLOR_SPAN = 12; /** */ public static final int TYPEFACE_SPAN = 13; /** */ public static final int SUPERSCRIPT_SPAN = 14; /** */ public static final int SUBSCRIPT_SPAN = 15; /** */ public static final int ABSOLUTE_SIZE_SPAN = 16; /** */ public static final int TEXT_APPEARANCE_SPAN = 17; /** */ public static final int ANNOTATION = 18; /** */ public static final int SUGGESTION_SPAN = 19; /** */ public static final int SPELL_CHECK_SPAN = 20; /** */ public static final int SUGGESTION_RANGE_SPAN = 21; /** */ public static final int EASY_EDIT_SPAN = 22; /** */ public static final int LOCALE_SPAN = 23; /** */ public static final int TTS_SPAN = 24; /** */ public static final int ACCESSIBILITY_CLICKABLE_SPAN = 25; /** */ public static final int ACCESSIBILITY_URL_SPAN = 26; /** */ public static final int LAST_SPAN = ACCESSIBILITY_URL_SPAN; /** * Flatten a CharSequence and whatever styles can be copied across processes * into the parcel. */ public static void writeToParcel(CharSequence cs, Parcel p, int parcelableFlags) { if (cs instanceof Spanned) { p.writeInt(0); p.writeString(cs.toString()); Spanned sp = (Spanned) cs; Object[] os = sp.getSpans(0, cs.length(), Object.class); // note to people adding to this: check more specific types // before more generic types. also notice that it uses // "if" instead of "else if" where there are interfaces // so one object can be several. for (int i = 0; i < os.length; i++) { Object o = os[i]; Object prop = os[i]; if (prop instanceof CharacterStyle) { prop = ((CharacterStyle) prop).getUnderlying(); } if (prop instanceof ParcelableSpan) { final ParcelableSpan ps = (ParcelableSpan) prop; final int spanTypeId = ps.getSpanTypeIdInternal(); if (spanTypeId < FIRST_SPAN || spanTypeId > LAST_SPAN) { Log.e(TAG, "External class \"" + ps.getClass().getSimpleName() + "\" is attempting to use the frameworks-only ParcelableSpan" + " interface"); } else { p.writeInt(spanTypeId); ps.writeToParcelInternal(p, parcelableFlags); writeWhere(p, sp, o); } } } p.writeInt(0); } else { p.writeInt(1); if (cs != null) { p.writeString(cs.toString()); } else { p.writeString(null); } } } private static void writeWhere(Parcel p, Spanned sp, Object o) { p.writeInt(sp.getSpanStart(o)); p.writeInt(sp.getSpanEnd(o)); p.writeInt(sp.getSpanFlags(o)); } public static final Parcelable.Creator CHAR_SEQUENCE_CREATOR = new Parcelable.Creator() { /** * Read and return a new CharSequence, possibly with styles, * from the parcel. */ public CharSequence createFromParcel(Parcel p) { int kind = p.readInt(); String string = p.readString(); if (string == null) { return null; } if (kind == 1) { return string; } SpannableString sp = new SpannableString(string); while (true) { kind = p.readInt(); if (kind == 0) break; switch (kind) { case ALIGNMENT_SPAN: readSpan(p, sp, new AlignmentSpan.Standard(p)); break; case FOREGROUND_COLOR_SPAN: readSpan(p, sp, new ForegroundColorSpan(p)); break; case RELATIVE_SIZE_SPAN: readSpan(p, sp, new RelativeSizeSpan(p)); break; case SCALE_X_SPAN: readSpan(p, sp, new ScaleXSpan(p)); break; case STRIKETHROUGH_SPAN: readSpan(p, sp, new StrikethroughSpan(p)); break; case UNDERLINE_SPAN: readSpan(p, sp, new UnderlineSpan(p)); break; case STYLE_SPAN: readSpan(p, sp, new StyleSpan(p)); break; case BULLET_SPAN: readSpan(p, sp, new BulletSpan(p)); break; case QUOTE_SPAN: readSpan(p, sp, new QuoteSpan(p)); break; case LEADING_MARGIN_SPAN: readSpan(p, sp, new LeadingMarginSpan.Standard(p)); break; case URL_SPAN: readSpan(p, sp, new URLSpan(p)); break; case BACKGROUND_COLOR_SPAN: readSpan(p, sp, new BackgroundColorSpan(p)); break; case TYPEFACE_SPAN: readSpan(p, sp, new TypefaceSpan(p)); break; case SUPERSCRIPT_SPAN: readSpan(p, sp, new SuperscriptSpan(p)); break; case SUBSCRIPT_SPAN: readSpan(p, sp, new SubscriptSpan(p)); break; case ABSOLUTE_SIZE_SPAN: readSpan(p, sp, new AbsoluteSizeSpan(p)); break; case TEXT_APPEARANCE_SPAN: readSpan(p, sp, new TextAppearanceSpan(p)); break; case ANNOTATION: readSpan(p, sp, new Annotation(p)); break; case SUGGESTION_SPAN: readSpan(p, sp, new SuggestionSpan(p)); break; case SPELL_CHECK_SPAN: readSpan(p, sp, new SpellCheckSpan(p)); break; case SUGGESTION_RANGE_SPAN: readSpan(p, sp, new SuggestionRangeSpan(p)); break; case EASY_EDIT_SPAN: readSpan(p, sp, new EasyEditSpan(p)); break; case LOCALE_SPAN: readSpan(p, sp, new LocaleSpan(p)); break; case TTS_SPAN: readSpan(p, sp, new TtsSpan(p)); break; case ACCESSIBILITY_CLICKABLE_SPAN: readSpan(p, sp, new AccessibilityClickableSpan(p)); break; case ACCESSIBILITY_URL_SPAN: readSpan(p, sp, new AccessibilityURLSpan(p)); break; default: throw new RuntimeException("bogus span encoding " + kind); } } return sp; } public CharSequence[] newArray(int size) { return new CharSequence[size]; } }; /** * Debugging tool to print the spans in a CharSequence. The output will * be printed one span per line. If the CharSequence is not a Spanned, * then the entire string will be printed on a single line. */ public static void dumpSpans(CharSequence cs, Printer printer, String prefix) { if (cs instanceof Spanned) { Spanned sp = (Spanned) cs; Object[] os = sp.getSpans(0, cs.length(), Object.class); for (int i = 0; i < os.length; i++) { Object o = os[i]; printer.println(prefix + cs.subSequence(sp.getSpanStart(o), sp.getSpanEnd(o)) + ": " + Integer.toHexString(System.identityHashCode(o)) + " " + o.getClass().getCanonicalName() + " (" + sp.getSpanStart(o) + "-" + sp.getSpanEnd(o) + ") fl=#" + sp.getSpanFlags(o)); } } else { printer.println(prefix + cs + ": (no spans)"); } } /** * Return a new CharSequence in which each of the source strings is * replaced by the corresponding element of the destinations. */ public static CharSequence replace(CharSequence template, String[] sources, CharSequence[] destinations) { SpannableStringBuilder tb = new SpannableStringBuilder(template); for (int i = 0; i < sources.length; i++) { int where = indexOf(tb, sources[i]); if (where >= 0) tb.setSpan(sources[i], where, where + sources[i].length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } for (int i = 0; i < sources.length; i++) { int start = tb.getSpanStart(sources[i]); int end = tb.getSpanEnd(sources[i]); if (start >= 0) { tb.replace(start, end, destinations[i]); } } return tb; } /** * Replace instances of "^1", "^2", etc. in the * template CharSequence with the corresponding * values. "^^" is used to produce a single caret in * the output. Only up to 9 replacement values are supported, * "^10" will be produce the first replacement value followed by a * '0'. * * @param template the input text containing "^1"-style * placeholder values. This object is not modified; a copy is * returned. * * @param values CharSequences substituted into the template. The * first is substituted for "^1", the second for "^2", and so on. * * @return the new CharSequence produced by doing the replacement * * @throws IllegalArgumentException if the template requests a * value that was not provided, or if more than 9 values are * provided. */ public static CharSequence expandTemplate(CharSequence template, CharSequence... values) { if (values.length > 9) { throw new IllegalArgumentException("max of 9 values are supported"); } SpannableStringBuilder ssb = new SpannableStringBuilder(template); try { int i = 0; while (i < ssb.length()) { if (ssb.charAt(i) == '^') { char next = ssb.charAt(i+1); if (next == '^') { ssb.delete(i+1, i+2); ++i; continue; } else if (Character.isDigit(next)) { int which = Character.getNumericValue(next) - 1; if (which < 0) { throw new IllegalArgumentException( "template requests value ^" + (which+1)); } if (which >= values.length) { throw new IllegalArgumentException( "template requests value ^" + (which+1) + "; only " + values.length + " provided"); } ssb.replace(i, i+2, values[which]); i += values[which].length(); continue; } } ++i; } } catch (IndexOutOfBoundsException ignore) { // happens when ^ is the last character in the string. } return ssb; } public static int getOffsetBefore(CharSequence text, int offset) { if (offset == 0) return 0; if (offset == 1) return 0; char c = text.charAt(offset - 1); if (c >= '\uDC00' && c <= '\uDFFF') { char c1 = text.charAt(offset - 2); if (c1 >= '\uD800' && c1 <= '\uDBFF') offset -= 2; else offset -= 1; } else { offset -= 1; } if (text instanceof Spanned) { ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, ReplacementSpan.class); for (int i = 0; i < spans.length; i++) { int start = ((Spanned) text).getSpanStart(spans[i]); int end = ((Spanned) text).getSpanEnd(spans[i]); if (start < offset && end > offset) offset = start; } } return offset; } public static int getOffsetAfter(CharSequence text, int offset) { int len = text.length(); if (offset == len) return len; if (offset == len - 1) return len; char c = text.charAt(offset); if (c >= '\uD800' && c <= '\uDBFF') { char c1 = text.charAt(offset + 1); if (c1 >= '\uDC00' && c1 <= '\uDFFF') offset += 2; else offset += 1; } else { offset += 1; } if (text instanceof Spanned) { ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, ReplacementSpan.class); for (int i = 0; i < spans.length; i++) { int start = ((Spanned) text).getSpanStart(spans[i]); int end = ((Spanned) text).getSpanEnd(spans[i]); if (start < offset && end > offset) offset = end; } } return offset; } private static void readSpan(Parcel p, Spannable sp, Object o) { sp.setSpan(o, p.readInt(), p.readInt(), p.readInt()); } /** * Copies the spans from the region start...end in * source to the region * destoff...destoff+end-start in dest. * Spans in source that begin before start * or end after end but overlap this range are trimmed * as if they began at start or ended at end. * * @throws IndexOutOfBoundsException if any of the copied spans * are out of range in dest. */ public static void copySpansFrom(Spanned source, int start, int end, Class kind, Spannable dest, int destoff) { if (kind == null) { kind = Object.class; } Object[] spans = source.getSpans(start, end, kind); for (int i = 0; i < spans.length; i++) { int st = source.getSpanStart(spans[i]); int en = source.getSpanEnd(spans[i]); int fl = source.getSpanFlags(spans[i]); if (st < start) st = start; if (en > end) en = end; dest.setSpan(spans[i], st - start + destoff, en - start + destoff, fl); } } /** * Transforms a CharSequences to uppercase, copying the sources spans and keeping them spans as * much as possible close to their relative original places. In the case the the uppercase * string is identical to the sources, the source itself is returned instead of being copied. * * If copySpans is set, source must be an instance of Spanned. * * {} */ @NonNull public static CharSequence toUpperCase(@Nullable Locale locale, @NonNull CharSequence source, boolean copySpans) { final Edits edits = new Edits(); if (!copySpans) { // No spans. Just uppercase the characters. final StringBuilder result = CaseMap.toUpper().apply( locale, source, new StringBuilder(), edits); return edits.hasChanges() ? result : source; } final SpannableStringBuilder result = CaseMap.toUpper().apply( locale, source, new SpannableStringBuilder(), edits); if (!edits.hasChanges()) { // No changes happened while capitalizing. We can return the source as it was. return source; } final Edits.Iterator iterator = edits.getFineIterator(); final int sourceLength = source.length(); final Spanned spanned = (Spanned) source; final Object[] spans = spanned.getSpans(0, sourceLength, Object.class); for (Object span : spans) { final int sourceStart = spanned.getSpanStart(span); final int sourceEnd = spanned.getSpanEnd(span); final int flags = spanned.getSpanFlags(span); // Make sure the indices are not at the end of the string, since in that case // iterator.findSourceIndex() would fail. final int destStart = sourceStart == sourceLength ? result.length() : toUpperMapToDest(iterator, sourceStart); final int destEnd = sourceEnd == sourceLength ? result.length() : toUpperMapToDest(iterator, sourceEnd); result.setSpan(span, destStart, destEnd, flags); } return result; } // helper method for toUpperCase() private static int toUpperMapToDest(Edits.Iterator iterator, int sourceIndex) { // Guaranteed to succeed if sourceIndex < source.length(). iterator.findSourceIndex(sourceIndex); if (sourceIndex == iterator.sourceIndex()) { return iterator.destinationIndex(); } // We handle the situation differently depending on if we are in the changed slice or an // unchanged one: In an unchanged slice, we can find the exact location the span // boundary was before and map there. // // But in a changed slice, we need to treat the whole destination slice as an atomic unit. // We adjust the span boundary to the end of that slice to reduce of the chance of adjacent // spans in the source overlapping in the result. (The choice for the end vs the beginning // is somewhat arbitrary, but was taken because we except to see slightly more spans only // affecting a base character compared to spans only affecting a combining character.) if (iterator.hasChange()) { return iterator.destinationIndex() + iterator.newLength(); } else { // Move the index 1:1 along with this unchanged piece of text. return iterator.destinationIndex() + (sourceIndex - iterator.sourceIndex()); } } public enum TruncateAt { START, MIDDLE, END, MARQUEE, /** * */ END_SMALL } public interface EllipsizeCallback { /** * This method is called to report that the specified region of * text was ellipsized away by a call to {@link #ellipsize}. */ public void ellipsized(int start, int end); } /** * Returns the original text if it fits in the specified width * given the properties of the specified Paint, * or, if it does not fit, a truncated * copy with ellipsis character added at the specified edge or center. */ public static CharSequence ellipsize(CharSequence text, TextPaint p, float avail, TruncateAt where) { return ellipsize(text, p, avail, where, false, null); } /** * Returns the original text if it fits in the specified width * given the properties of the specified Paint, * or, if it does not fit, a copy with ellipsis character added * at the specified edge or center. * If preserveLength is specified, the returned copy * will be padded with zero-width spaces to preserve the original * length and offsets instead of truncating. * If callback is non-null, it will be called to * report the start and end of the ellipsized range. TextDirection * is determined by the first strong directional character. */ public static CharSequence ellipsize(CharSequence text, TextPaint paint, float avail, TruncateAt where, boolean preserveLength, @Nullable EllipsizeCallback callback) { return ellipsize(text, paint, avail, where, preserveLength, callback, TextDirectionHeuristics.FIRSTSTRONG_LTR, getEllipsisString(where)); } /** * Returns the original text if it fits in the specified width * given the properties of the specified Paint, * or, if it does not fit, a copy with ellipsis character added * at the specified edge or center. * If preserveLength is specified, the returned copy * will be padded with zero-width spaces to preserve the original * length and offsets instead of truncating. * If callback is non-null, it will be called to * report the start and end of the ellipsized range. * * */ public static CharSequence ellipsize(CharSequence text, TextPaint paint, float avail, TruncateAt where, boolean preserveLength, @Nullable EllipsizeCallback callback, TextDirectionHeuristic textDir, String ellipsis) { int len = text.length(); MeasuredParagraph mt = null; try { mt = MeasuredParagraph.buildForMeasurement(paint, text, 0, text.length(), textDir, mt); float width = mt.getWholeWidth(); if (width <= avail) { if (callback != null) { callback.ellipsized(0, 0); } return text; } // XXX assumes ellipsis string does not require shaping and // is unaffected by style float ellipsiswid = paint.measureText(ellipsis); avail -= ellipsiswid; int left = 0; int right = len; if (avail < 0) { // it all goes } else if (where == TruncateAt.START) { right = len - mt.breakText(len, false, avail); } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) { left = mt.breakText(len, true, avail); } else { right = len - mt.breakText(len, false, avail / 2); avail -= mt.measure(right, len); left = mt.breakText(right, true, avail); } if (callback != null) { callback.ellipsized(left, right); } final char[] buf = mt.getChars(); Spanned sp = text instanceof Spanned ? (Spanned) text : null; final int removed = right - left; final int remaining = len - removed; if (preserveLength) { if (remaining > 0 && removed >= ellipsis.length()) { ellipsis.getChars(0, ellipsis.length(), buf, left); left += ellipsis.length(); } // else skip the ellipsis for (int i = left; i < right; i++) { buf[i] = ELLIPSIS_FILLER; } String s = new String(buf, 0, len); if (sp == null) { return s; } SpannableString ss = new SpannableString(s); copySpansFrom(sp, 0, len, Object.class, ss, 0); return ss; } if (remaining == 0) { return ""; } if (sp == null) { StringBuilder sb = new StringBuilder(remaining + ellipsis.length()); sb.append(buf, 0, left); sb.append(ellipsis); sb.append(buf, right, len - right); return sb.toString(); } SpannableStringBuilder ssb = new SpannableStringBuilder(); ssb.append(text, 0, left); ssb.append(ellipsis); ssb.append(text, right, len); return ssb; } finally { if (mt != null) { mt.recycle(); } } } /** * Formats a list of CharSequences by repeatedly inserting the separator between them, * but stopping when the resulting sequence is too wide for the specified width. * * This method actually tries to fit the maximum number of elements. So if {@code "A, 11 more" * fits}, {@code "A, B, 10 more"} doesn't fit, but {@code "A, B, C, 9 more"} fits again (due to * the glyphs for the digits being very wide, for example), it returns * {@code "A, B, C, 9 more"}. Because of this, this method may be inefficient for very long * lists. * * Note that the elements of the returned value, as well as the string for {@code moreId}, will * be bidi-wrapped using {@link BidiFormatter#unicodeWrap} based on the locale of the input * Context. If the input {@code Context} is null, the default BidiFormatter from * {@link BidiFormatter#getInstance()} will be used. * * @param context the {@code Context} to get the {@code moreId} resource from. If {@code null}, * an ellipsis (U+2026) would be used for {@code moreId}. * @param elements the list to format * @param separator a separator, such as {@code ", "} * @param paint the Paint with which to measure the text * @param avail the horizontal width available for the text (in pixels) * @param moreId the resource ID for the pluralized string to insert at the end of sequence when * some of the elements don't fit. * * @return the formatted CharSequence. If even the shortest sequence (e.g. {@code "A, 11 more"}) * doesn't fit, it will return an empty string. */ // public static CharSequence listEllipsize(@Nullable Context context, // @Nullable List elements, @NonNull String separator, // @NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail, // @PluralsRes int moreId) { // if (elements == null) { // return ""; // } // final int totalLen = elements.size(); // if (totalLen == 0) { // return ""; // } // // final Resources res; // final BidiFormatter bidiFormatter; // if (context == null) { // res = null; // bidiFormatter = BidiFormatter.getInstance(); // } else { // res = context.getResources(); // bidiFormatter = BidiFormatter.getInstance(res.getConfiguration().getLocales().get(0)); // } // // final SpannableStringBuilder output = new SpannableStringBuilder(); // final int[] endIndexes = new int[totalLen]; // for (int i = 0; i < totalLen; i++) { // output.append(bidiFormatter.unicodeWrap(elements.get(i))); // if (i != totalLen - 1) { // Insert a separator, except at the very end. // output.append(separator); // } // endIndexes[i] = output.length(); // } // // for (int i = totalLen - 1; i >= 0; i--) { // // Delete the tail of the string, cutting back to one less element. // output.delete(endIndexes[i], output.length()); // // final int remainingElements = totalLen - i - 1; // if (remainingElements > 0) { // CharSequence morePiece = (res == null) ? // ELLIPSIS_NORMAL : // res.getQuantityString(moreId, remainingElements, remainingElements); // morePiece = bidiFormatter.unicodeWrap(morePiece); // output.append(morePiece); // } // // final float width = paint.measureText(output, 0, output.length()); // if (width <= avail) { // The string fits. // return output; // } // } // return ""; // Nothing fits. // } /** * Converts a CharSequence of the comma-separated form "Andy, Bob, * Charles, David" that is too wide to fit into the specified width * into one like "Andy, Bob, 2 more". * * @param text the text to truncate * @param p the Paint with which to measure the text * @param avail the horizontal width available for the text (in pixels) * @param oneMore the string for "1 more" in the current locale * @param more the string for "%d more" in the current locale * * @deprecated Do not use. This is not internationalized, and has known issues * with right-to-left text, languages that have more than one plural form, languages * that use a different character as a comma-like separator, etc. * Use listEllipsize instead. */ @Deprecated public static CharSequence commaEllipsize(CharSequence text, TextPaint p, float avail, String oneMore, String more) { return commaEllipsize(text, p, avail, oneMore, more, TextDirectionHeuristics.FIRSTSTRONG_LTR); } /** * */ @Deprecated public static CharSequence commaEllipsize(CharSequence text, TextPaint p, float avail, String oneMore, String more, TextDirectionHeuristic textDir) { MeasuredParagraph mt = null; MeasuredParagraph tempMt = null; try { int len = text.length(); mt = MeasuredParagraph.buildForMeasurement(p, text, 0, len, textDir, mt); final float width = mt.getWholeWidth(); if (width <= avail) { return text; } char[] buf = mt.getChars(); int commaCount = 0; for (int i = 0; i < len; i++) { if (buf[i] == ',') { commaCount++; } } int remaining = commaCount + 1; int ok = 0; String okFormat = ""; int w = 0; int count = 0; float[] widths = mt.getWidths().getRawArray(); for (int i = 0; i < len; i++) { w += widths[i]; if (buf[i] == ',') { count++; String format; // XXX should not insert spaces, should be part of string // XXX should use plural rules and not assume English plurals if (--remaining == 1) { format = " " + oneMore; } else { format = " " + String.format(more, remaining); } // XXX this is probably ok, but need to look at it more tempMt = MeasuredParagraph.buildForMeasurement( p, format, 0, format.length(), textDir, tempMt); float moreWid = tempMt.getWholeWidth(); if (w + moreWid <= avail) { ok = i + 1; okFormat = format; } } } SpannableStringBuilder out = new SpannableStringBuilder(okFormat); out.insert(0, text, 0, ok); return out; } finally { if (mt != null) { mt.recycle(); } if (tempMt != null) { tempMt.recycle(); } } } // Returns true if the character's presence could affect RTL layout. // // In order to be fast, the code is intentionally rough and quite conservative in its // considering inclusion of any non-BMP or surrogate characters or anything in the bidi // blocks or any bidi formatting characters with a potential to affect RTL layout. /* package */ static boolean couldAffectRtl(char c) { return (0x0590 <= c && c <= 0x08FF) || // RTL scripts c == 0x200E || // Bidi format character c == 0x200F || // Bidi format character (0x202A <= c && c <= 0x202E) || // Bidi format characters (0x2066 <= c && c <= 0x2069) || // Bidi format characters (0xD800 <= c && c <= 0xDFFF) || // Surrogate pairs (0xFB1D <= c && c <= 0xFDFF) || // Hebrew and Arabic presentation forms (0xFE70 <= c && c <= 0xFEFE); // Arabic presentation forms } // Returns true if there is no character present that may potentially affect RTL layout. // Since this calls couldAffectRtl() above, it's also quite conservative, in the way that // it may return 'false' (needs bidi) although careful consideration may tell us it should // return 'true' (does not need bidi). /* package */ static boolean doesNotNeedBidi(char[] text, int start, int len) { final int end = start + len; for (int i = start; i < end; i++) { if (couldAffectRtl(text[i])) { return false; } } return true; } /* package */ static char[] obtain(int len) { char[] buf; synchronized (sLock) { buf = sTemp; sTemp = null; } if (buf == null || buf.length < len) buf = ArrayUtils.newUnpaddedCharArray(len); return buf; } /* package */ static void recycle(char[] temp) { if (temp.length > 1000) return; synchronized (sLock) { sTemp = temp; } } /** * Html-encode the string. * @param s the string to be encoded * @return the encoded string */ public static String htmlEncode(String s) { StringBuilder sb = new StringBuilder(); char c; for (int i = 0; i < s.length(); i++) { c = s.charAt(i); switch (c) { case '<': sb.append("<"); //$NON-NLS-1$ break; case '>': sb.append(">"); //$NON-NLS-1$ break; case '&': sb.append("&"); //$NON-NLS-1$ break; case '\'': //http://www.w3.org/TR/xhtml1 // The named character reference ' (the apostrophe, U+0027) was introduced in // XML 1.0 but does not appear in HTML. Authors should therefore use ' instead // of ' to work as expected in HTML 4 user agents. sb.append("'"); //$NON-NLS-1$ break; case '"': sb.append("""); //$NON-NLS-1$ break; default: sb.append(c); } } return sb.toString(); } /** * Returns a CharSequence concatenating the specified CharSequences, * retaining their spans if any. * * If there are no parameters, an empty string will be returned. * * If the number of parameters is exactly one, that parameter is returned as output, even if it * is null. * * If the number of parameters is at least two, any null CharSequence among the parameters is * treated as if it was the string "null". * * If there are paragraph spans in the source CharSequences that satisfy paragraph boundary * requirements in the sources but would no longer satisfy them in the concatenated * CharSequence, they may get extended in the resulting CharSequence or not retained. */ public static CharSequence concat(CharSequence... text) { if (text.length == 0) { return ""; } if (text.length == 1) { return text[0]; } boolean spanned = false; for (CharSequence piece : text) { if (piece instanceof Spanned) { spanned = true; break; } } if (spanned) { final SpannableStringBuilder ssb = new SpannableStringBuilder(); for (CharSequence piece : text) { // If a piece is null, we append the string "null" for compatibility with the // behavior of StringBuilder and the behavior of the concat() method in earlier // versions of Android. ssb.append(piece == null ? "null" : piece); } return new SpannedString(ssb); } else { final StringBuilder sb = new StringBuilder(); for (CharSequence piece : text) { sb.append(piece); } return sb.toString(); } } /** * Returns whether the given CharSequence contains any printable characters. */ public static boolean isGraphic(CharSequence str) { final int len = str.length(); for (int cp, i=0; ireqModes will be * checked. Note that the caps mode flags here are explicitly defined * to match those in {@link InputType}. * * @param cs The text that should be checked for caps modes. * @param off Location in the text at which to check. * @param reqModes The modes to be checked: may be any combination of * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and * {@link #CAP_MODE_SENTENCES}. * * @return Returns the actual capitalization modes that can be in effect * at the current position, which is any combination of * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and * {@link #CAP_MODE_SENTENCES}. */ public static int getCapsMode(CharSequence cs, int off, int reqModes) { if (off < 0) { return 0; } int i; char c; int mode = 0; if ((reqModes&CAP_MODE_CHARACTERS) != 0) { mode |= CAP_MODE_CHARACTERS; } if ((reqModes&(CAP_MODE_WORDS|CAP_MODE_SENTENCES)) == 0) { return mode; } // Back over allowed opening punctuation. for (i = off; i > 0; i--) { c = cs.charAt(i - 1); if (c != '"' && c != '\'' && Character.getType(c) != Character.START_PUNCTUATION) { break; } } // Start of paragraph, with optional whitespace. int j = i; while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) { j--; } if (j == 0 || cs.charAt(j - 1) == '\n') { return mode | CAP_MODE_WORDS; } // Or start of word if we are that style. if ((reqModes&CAP_MODE_SENTENCES) == 0) { if (i != j) mode |= CAP_MODE_WORDS; return mode; } // There must be a space if not the start of paragraph. if (i == j) { return mode; } // Back over allowed closing punctuation. for (; j > 0; j--) { c = cs.charAt(j - 1); if (c != '"' && c != '\'' && Character.getType(c) != Character.END_PUNCTUATION) { break; } } if (j > 0) { c = cs.charAt(j - 1); if (c == '.' || c == '?' || c == '!') { // Do not capitalize if the word ends with a period but // also contains a period, in which case it is an abbreviation. if (c == '.') { for (int k = j - 2; k >= 0; k--) { c = cs.charAt(k); if (c == '.') { return mode; } if (!Character.isLetter(c)) { break; } } } return mode | CAP_MODE_SENTENCES; } } return mode; } /** * Does a comma-delimited list 'delimitedString' contain a certain item? * (without allocating memory) * * */ public static boolean delimitedStringContains( String delimitedString, char delimiter, String item) { if (isEmpty(delimitedString) || isEmpty(item)) { return false; } int pos = -1; int length = delimitedString.length(); while ((pos = delimitedString.indexOf(item, pos + 1)) != -1) { if (pos > 0 && delimitedString.charAt(pos - 1) != delimiter) { continue; } int expectedDelimiterPos = pos + item.length(); if (expectedDelimiterPos == length) { // Match at end of string. return true; } if (delimitedString.charAt(expectedDelimiterPos) == delimiter) { return true; } } return false; } /** * Removes empty spans from the spans array. * * When parsing a Spanned using {@link Spanned#nextSpanTransition(int, int, Class)}, empty spans * will (correctly) create span transitions, and calling getSpans on a slice of text bounded by * one of these transitions will (correctly) include the empty overlapping span. * * However, these empty spans should not be taken into account when layouting or rendering the * string and this method provides a way to filter getSpans' results accordingly. * * @param spans A list of spans retrieved using {@link Spanned#getSpans(int, int, Class)} from * the spanned * @param spanned The Spanned from which spans were extracted * @return A subset of spans where empty spans ({@link Spanned#getSpanStart(Object)} == * {@link Spanned#getSpanEnd(Object)} have been removed. The initial order is preserved * */ @SuppressWarnings("unchecked") public static T[] removeEmptySpans(T[] spans, Spanned spanned, Class klass) { T[] copy = null; int count = 0; for (int i = 0; i < spans.length; i++) { final T span = spans[i]; final int start = spanned.getSpanStart(span); final int end = spanned.getSpanEnd(span); if (start == end) { if (copy == null) { copy = (T[]) Array.newInstance(klass, spans.length - 1); System.arraycopy(spans, 0, copy, 0, i); count = i; } } else { if (copy != null) { copy[count] = span; count++; } } } if (copy != null) { T[] result = (T[]) Array.newInstance(klass, count); System.arraycopy(copy, 0, result, 0, count); return result; } else { return spans; } } /** * Pack 2 int values into a long, useful as a return value for a range * @see #unpackRangeStartFromLong(long) * @see #unpackRangeEndFromLong(long) * */ public static long packRangeInLong(int start, int end) { return (((long) start) << 32) | end; } /** * Get the start value from a range packed in a long by {@link #packRangeInLong(int, int)} * @see #unpackRangeEndFromLong(long) * @see #packRangeInLong(int, int) * */ public static int unpackRangeStartFromLong(long range) { return (int) (range >>> 32); } /** * Get the end value from a range packed in a long by {@link #packRangeInLong(int, int)} * @see #unpackRangeStartFromLong(long) * @see #packRangeInLong(int, int) * */ public static int unpackRangeEndFromLong(long range) { return (int) (range & 0x00000000FFFFFFFFL); } /** * Return the layout direction for a given Locale * * @param locale the Locale for which we want the layout direction. Can be null. * @return the layout direction. This may be one of: * {@link View#LAYOUT_DIRECTION_LTR} or * {@link View#LAYOUT_DIRECTION_RTL}. * * Be careful: this code will need to be updated when vertical scripts will be supported */ // public static int getLayoutDirectionFromLocale(Locale locale) { // return ((locale != null && !locale.equals(Locale.ROOT) // && ULocale.forLocale(locale).isRightToLeft()) // // If forcing into RTL layout mode, return RTL as default // || SystemProperties.getBoolean(Settings.Global.DEVELOPMENT_FORCE_RTL, false)) // ? View.LAYOUT_DIRECTION_RTL // : View.LAYOUT_DIRECTION_LTR; // } // /** // * Return localized string representing the given number of selected items. // * // * // */ // public static CharSequence formatSelectedCount(int count) { // return Resources.getSystem().getQuantityString(R.plurals.selected_count, count, count); // } /** * Returns whether or not the specified spanned text has a style span. * */ public static boolean hasStyleSpan(@NonNull Spanned spanned) { Preconditions.checkArgument(spanned != null); final Class[] styleClasses = { CharacterStyle.class, ParagraphStyle.class, UpdateAppearance.class}; for (Class clazz : styleClasses) { if (spanned.nextSpanTransition(-1, spanned.length(), clazz) < spanned.length()) { return true; } } return false; } /** * If the {@code charSequence} is instance of {@link Spanned}, creates a new copy and * {@link NoCopySpan}'s are removed from the copy. Otherwise the given {@code charSequence} is * returned as it is. * * */ @Nullable public static CharSequence trimNoCopySpans(@Nullable CharSequence charSequence) { if (charSequence != null && charSequence instanceof Spanned) { // SpannableStringBuilder copy constructor trims NoCopySpans. return new SpannableStringBuilder(charSequence); } return charSequence; } /** * Prepends {@code start} and appends {@code end} to a given {@link StringBuilder} * * */ public static void wrap(StringBuilder builder, String start, String end) { builder.insert(0, start); builder.append(end); } /** * Intent size limitations prevent sending over a megabyte of data. Limit * text length to 100K characters - 200KB. */ private static final int PARCEL_SAFE_TEXT_LENGTH = 100000; /** * Trims the text to {@link #PARCEL_SAFE_TEXT_LENGTH} length. Returns the string as it is if * the length() is smaller than {@link #PARCEL_SAFE_TEXT_LENGTH}. Used for text that is parceled * into a {@link Parcelable}. * * */ @Nullable public static T trimToParcelableSize(@Nullable T text) { return trimToSize(text, PARCEL_SAFE_TEXT_LENGTH); } /** * Trims the text to {@code size} length. Returns the string as it is if the length() is * smaller than {@code size}. If chars at {@code size-1} and {@code size} is a surrogate * pair, returns a CharSequence of length {@code size-1}. * * @param size length of the result, should be greater than 0 * * */ @Nullable public static T trimToSize(@Nullable T text, @IntRange(from = 1) int size) { Preconditions.checkArgument(size > 0); if (TextUtils.isEmpty(text) || text.length() <= size) return text; if (Character.isHighSurrogate(text.charAt(size - 1)) && Character.isLowSurrogate(text.charAt(size))) { size = size - 1; } return (T) text.subSequence(0, size); } private static Object sLock = new Object(); private static char[] sTemp = null; private static String[] EMPTY_STRING_ARRAY = new String[]{}; }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy