package com.glview.text; import com.glview.R; import com.glview.content.GLContext; import com.glview.hwui.GLPaint; public class TextUtils { private TextUtils() { /* cannot be instantiated */ } public static void getChars(CharSequence s, int start, int end, char[] dest, int destoff) { Class<? extends CharSequence> 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 { for (int i = start; i < end; i++) dest[destoff++] = s.charAt(i); } } /** * 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(CharSequence str) { if (str == null || str.length() == 0) return true; else return false; } public static int indexOf(CharSequence s, char ch) { return indexOf(s, ch, 0); } public static int indexOf(CharSequence s, char ch, int start) { Class<? extends CharSequence> 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<? extends CharSequence> 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<? extends CharSequence> 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<? extends CharSequence> 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; } public enum TruncateAt { START, MIDDLE, END, MARQUEE, /** * @hide */ 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, GLPaint 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 <code>preserveLength</code> is specified, the returned copy * will be padded with zero-width spaces to preserve the original * length and offsets instead of truncating. * If <code>callback</code> 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, GLPaint paint, float avail, TruncateAt where, boolean preserveLength, EllipsizeCallback callback) { final String ellipsis = (where == TruncateAt.END_SMALL) ? GLContext.get().getApplicationContext().getString(R.string.ellipsis_two_dots) : GLContext.get().getApplicationContext().getString(R.string.ellipsis); return ellipsize(text, paint, avail, where, preserveLength, callback, TextDirectionHeuristics.FIRSTSTRONG_LTR, ellipsis); } /** * 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 <code>preserveLength</code> is specified, the returned copy * will be padded with zero-width spaces to preserve the original * length and offsets instead of truncating. * If <code>callback</code> is non-null, it will be called to * report the start and end of the ellipsized range. * * @hide */ public static CharSequence ellipsize(CharSequence text, GLPaint paint, float avail, TruncateAt where, boolean preserveLength, EllipsizeCallback callback, TextDirectionHeuristic textDir, String ellipsis) { int len = text.length(); MeasuredText mt = MeasuredText.obtain(); try { mt.setPara(text, 0, len, textDir); float width = mt.addStyleRun(paint, len, null); 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); } char[] buf = mt.mChars; int remaining = len - (right - left); if (preserveLength) { if (remaining > 0) { // else eliminate the ellipsis too buf[left++] = ellipsis.charAt(0); } for (int i = left; i < right; i++) { buf[i] = ZWNBS_CHAR; } String s = new String(buf, 0, len); return s; } if (remaining == 0) { return ""; } StringBuilder sb = new StringBuilder(remaining + ellipsis.length()); sb.append(buf, 0, left); sb.append(ellipsis); sb.append(buf, right, len - right); return sb.toString(); } finally { MeasuredText.recycle(mt); } } /* package */ static char[] obtain(int len) { char[] buf; synchronized (sLock) { buf = sTemp; sTemp = null; } if (buf == null || buf.length < len) buf = new char[len]; return buf; } /* package */ static void recycle(char[] temp) { if (temp.length > 1000) return; synchronized (sLock) { sTemp = temp; } } private static Object sLock = new Object(); private static char[] sTemp = null; private static final char ZWNBS_CHAR = '\uFEFF'; /** * For the purpose of layout, a word break is a boundary with no * kerning or complex script processing. This is necessarily a * heuristic, but should be accurate most of the time. */ public static boolean isWordBreak(char c) { if (isSpace(c)) { // spaces return true; } if ((c >= 0x3400 && c <= 0x9fff)) { // CJK ideographs (and yijing hexagram symbols) return true; } // Note: kana is not included, as sophisticated fonts may kern kana return false; } public static boolean isSpace(char c) { if (c == ' ' || (c >= 0x2000 && c <= 0x200a) || c == 0x3000) { // spaces return true; } return false; } }