package com.glview.text; import com.glview.graphics.Rect; import com.glview.hwui.GLCanvas; import com.glview.hwui.GLPaint; public abstract class Layout { /** * Return how wide a layout must be in order to display the * specified text with one line per paragraph. */ public static float getDesiredWidth(CharSequence source, GLPaint paint) { return getDesiredWidth(source, 0, source.length(), paint); } /** * Return how wide a layout must be in order to display the * specified text slice with one line per paragraph. */ public static float getDesiredWidth(CharSequence source, int start, int end, GLPaint paint) { float need = 0; int next; for (int i = start; i <= end; i = next) { next = TextUtils.indexOf(source, '\n', i, end); if (next < 0) next = end; // note, omits trailing paragraph char float w = measurePara(paint, source, i, next); if (w > need) need = w; next++; } return need; } /** * Subclasses of Layout use this constructor to set the display text, * width, and other standard properties. * @param text the text to render * @param paint the default paint for the layout. Styles can override * various attributes of the paint. * @param width the wrapping width for the text. * @param align whether to left, right, or center the text. Styles can * override the alignment. * @param spacingMult factor by which to scale the font size to get the * default line spacing * @param spacingAdd amount to add to the default line spacing */ protected Layout(CharSequence text, GLPaint paint, int width, Alignment align, float spacingMult, float spacingAdd, boolean drawDefer) { this(text, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR, spacingMult, spacingAdd, drawDefer); } /** * Subclasses of Layout use this constructor to set the display text, * width, and other standard properties. * @param text the text to render * @param paint the default paint for the layout. Styles can override * various attributes of the paint. * @param width the wrapping width for the text. * @param align whether to left, right, or center the text. Styles can * override the alignment. * @param spacingMult factor by which to scale the font size to get the * default line spacing * @param spacingAdd amount to add to the default line spacing */ protected Layout(CharSequence text, GLPaint paint, int width, Alignment align, TextDirectionHeuristic textDir, float spacingMult, float spacingAdd, boolean drawDefer) { if (width < 0) throw new IllegalArgumentException("Layout: " + width + " < 0"); mText = text; mPaint = paint; mWidth = width; mAlignment = align; mSpacingMult = spacingMult; mSpacingAdd = spacingAdd; mDrawDefer = drawDefer; } /** * Replace constructor properties of this Layout with new ones. Be careful. */ /* package */ void replaceWith(CharSequence text, GLPaint paint, int width, Alignment align, float spacingmult, float spacingadd) { if (width < 0) { throw new IllegalArgumentException("Layout: " + width + " < 0"); } mText = text; mPaint = paint; mWidth = width; mAlignment = align; mSpacingMult = spacingmult; mSpacingAdd = spacingadd; } public void draw(GLCanvas canvas) { int firstLine = 0; int lastLine = getLineCount() - 1; if (lastLine < 0) return; drawText(canvas, firstLine, lastLine); } /** * @hide */ public void drawText(GLCanvas canvas, int firstLine, int lastLine) { int previousLineBottom = getLineTop(firstLine); int previousLineEnd = getLineStart(firstLine); GLPaint paint = mPaint; CharSequence buf = mText; Alignment paraAlign = mAlignment; // Draw the lines, one at a time. // The baseline is the top of the following line minus the current line's descent. for (int i = firstLine; i <= lastLine; i++) { int start = previousLineEnd; previousLineEnd = getLineStart(i + 1); int end = getLineVisibleEnd(i, start, previousLineEnd); int ltop = previousLineBottom; int lbottom = getLineTop(i+1); previousLineBottom = lbottom; int lbaseline = lbottom - getLineDescent(i); int left = 0; int right = mWidth; // Determine whether the line aligns to normal, opposite, or center. Alignment align = paraAlign; if (align == Alignment.ALIGN_LEFT) { align = Alignment.ALIGN_NORMAL; } else if (align == Alignment.ALIGN_RIGHT) { align = Alignment.ALIGN_OPPOSITE; } int x; if (align == Alignment.ALIGN_NORMAL) { x = left; } else { int max = (int)getLineExtent(i, false); if (align == Alignment.ALIGN_OPPOSITE) { x = right - max; } else { // Alignment.ALIGN_CENTER max = max & ~1; x = (right + left - max) >> 1; } } boolean drawDefer = i == lastLine ? mDrawDefer : true; // FIXME what to do if (i != firstLine && TextUtils.isSpace(buf.charAt(start)) && buf.charAt(start - 1) != CHAR_NEW_LINE) { start ++; } canvas.drawText(buf, start, end, x, lbaseline, paint, drawDefer); } } private static final char CHAR_NEW_LINE = '\n'; /** * Return the text that is displayed by this Layout. */ public final CharSequence getText() { return mText; } /** * Return the base Paint properties for this layout. * Do NOT change the paint, which may result in funny * drawing for this layout. */ public final GLPaint getPaint() { return mPaint; } /** * Return the width of this layout. */ public final int getWidth() { return mWidth; } /** * Return the width to which this Layout is ellipsizing, or * {@link #getWidth} if it is not doing anything special. */ public int getEllipsizedWidth() { return mWidth; } /** * Increase the width of this layout to the specified width. * Be careful to use this only when you know it is appropriate— * it does not cause the text to reflow to use the full new width. */ public final void increaseWidthTo(int wid) { if (wid < mWidth) { throw new RuntimeException("attempted to reduce Layout width"); } mWidth = wid; } /** * Return the total height of this layout. */ public int getHeight() { return getLineTop(getLineCount()); } /** * Return the base alignment of this layout. */ public final Alignment getAlignment() { return mAlignment; } /** * Return what the text height is multiplied by to get the line height. */ public final float getSpacingMultiplier() { return mSpacingMult; } /** * Return the number of units of leading that are added to each line. */ public final float getSpacingAdd() { return mSpacingAdd; } /** * Return the heuristic used to determine paragraph text direction. * @hide */ public final TextDirectionHeuristic getTextDirectionHeuristic() { return mTextDir; } /** * Return the number of lines of text in this layout. */ public abstract int getLineCount(); /** * Return the baseline for the specified line (0…getLineCount() - 1) * If bounds is not null, return the top, left, right, bottom extents * of the specified line in it. * @param line which line to examine (0..getLineCount() - 1) * @param bounds Optional. If not null, it returns the extent of the line * @return the Y-coordinate of the baseline */ public int getLineBounds(int line, Rect bounds) { if (bounds != null) { bounds.left = 0; // ??? bounds.top = getLineTop(line); bounds.right = mWidth; // ??? bounds.bottom = getLineTop(line + 1); } return getLineBaseline(line); } /** * Get the leftmost position that should be exposed for horizontal * scrolling on the specified line. */ public float getLineLeft(int line) { int dir = getParagraphDirection(line); Alignment align = getParagraphAlignment(line); if (align == Alignment.ALIGN_LEFT) { return 0; } else if (align == Alignment.ALIGN_NORMAL) { if (dir == DIR_RIGHT_TO_LEFT) return getParagraphRight(line) - getLineMax(line); else return 0; } else if (align == Alignment.ALIGN_RIGHT) { return mWidth - getLineMax(line); } else if (align == Alignment.ALIGN_OPPOSITE) { if (dir == DIR_RIGHT_TO_LEFT) return 0; else return mWidth - getLineMax(line); } else { /* align == Alignment.ALIGN_CENTER */ int left = getParagraphLeft(line); int right = getParagraphRight(line); int max = ((int) getLineMax(line)) & ~1; return left + ((right - left) - max) / 2; } } /** * Get the rightmost position that should be exposed for horizontal * scrolling on the specified line. */ public float getLineRight(int line) { int dir = getParagraphDirection(line); Alignment align = getParagraphAlignment(line); if (align == Alignment.ALIGN_LEFT) { return getParagraphLeft(line) + getLineMax(line); } else if (align == Alignment.ALIGN_NORMAL) { if (dir == DIR_RIGHT_TO_LEFT) return mWidth; else return getParagraphLeft(line) + getLineMax(line); } else if (align == Alignment.ALIGN_RIGHT) { return mWidth; } else if (align == Alignment.ALIGN_OPPOSITE) { if (dir == DIR_RIGHT_TO_LEFT) return getLineMax(line); else return mWidth; } else { /* align == Alignment.ALIGN_CENTER */ int left = getParagraphLeft(line); int right = getParagraphRight(line); int max = ((int) getLineMax(line)) & ~1; return right - ((right - left) - max) / 2; } } /** * Gets the unsigned horizontal extent of the specified line, including * leading margin indent, but excluding trailing whitespace. */ public float getLineMax(int line) { float margin = 0f;//getParagraphLeadingMargin(line); float signedExtent = getLineExtent(line, false); return margin + (signedExtent >= 0 ? signedExtent : -signedExtent); } /** * Gets the unsigned horizontal extent of the specified line, including * leading margin indent and trailing whitespace. */ public float getLineWidth(int line) { float margin = 0f;//getParagraphLeadingMargin(line); float signedExtent = getLineExtent(line, true); return margin + (signedExtent >= 0 ? signedExtent : -signedExtent); } /** * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the * tab stops instead of using the ones passed in. * @param line the index of the line * @param full whether to include trailing whitespace * @return the extent of the line */ private float getLineExtent(int line, boolean full) { int start = getLineStart(line); int end = full ? getLineEnd(line) : getLineVisibleEnd(line); // TextLine tl = TextLine.obtain(); // tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops); // float width = tl.metrics(null); // TextLine.recycle(tl); return mPaint.measureText(mText, start, end); } /** * Return the vertical position of the top of the specified line * (0…getLineCount()). * If the specified line is equal to the line count, returns the * bottom of the last line. */ public abstract int getLineTop(int line); /** * Return the descent of the specified line(0…getLineCount() - 1). */ public abstract int getLineDescent(int line); /** * Returns the primary directionality of the paragraph containing the * specified line, either 1 for left-to-right lines, or -1 for right-to-left * lines (see {@link #DIR_LEFT_TO_RIGHT}, {@link #DIR_RIGHT_TO_LEFT}). */ public abstract int getParagraphDirection(int line); /** * Returns whether the specified line contains one or more * characters that need to be handled specially, like tabs * or emoji. */ public abstract boolean getLineContainsTab(int line); /** * Returns the directional run information for the specified line. * The array alternates counts of characters in left-to-right * and right-to-left segments of the line. * * <p>NOTE: this is inadequate to support bidirectional text, and will change. */ public abstract Directions getLineDirections(int line); /** * Return the text offset of the beginning of the specified line ( * 0…getLineCount()). If the specified line is equal to the line * count, returns the length of the text. */ public abstract int getLineStart(int line); /** * Returns the (negative) number of extra pixels of ascent padding in the * top line of the Layout. */ public abstract int getTopPadding(); /** * Returns the number of extra pixels of descent padding in the * bottom line of the Layout. */ public abstract int getBottomPadding(); /** * Get the line number corresponding to the specified vertical position. * If you ask for a position above 0, you get 0; if you ask for a position * below the bottom of the text, you get the last line. */ // FIXME: It may be faster to do a linear search for layouts without many lines. public int getLineForVertical(int vertical) { int high = getLineCount(), low = -1, guess; while (high - low > 1) { guess = (high + low) / 2; if (getLineTop(guess) > vertical) high = guess; else low = guess; } if (low < 0) return 0; else return low; } /** * Get the line number on which the specified text offset appears. * If you ask for a position before 0, you get 0; if you ask for a position * beyond the end of the text, you get the last line. */ public int getLineForOffset(int offset) { int high = getLineCount(), low = -1, guess; while (high - low > 1) { guess = (high + low) / 2; if (getLineStart(guess) > offset) high = guess; else low = guess; } if (low < 0) return 0; else return low; } /** * Return the text offset after the last character on the specified line. */ public final int getLineEnd(int line) { return getLineStart(line + 1); } /** * Return the text offset after the last visible character (so whitespace * is not counted) on the specified line. */ public int getLineVisibleEnd(int line) { return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); } private int getLineVisibleEnd(int line, int start, int end) { CharSequence text = mText; char ch; if (line == getLineCount() - 1) { return end; } for (; end > start; end--) { ch = text.charAt(end - 1); if (ch == '\n') { return end - 1; } if (ch != ' ' && ch != '\t') { break; } } return end; } /** * Return the vertical position of the bottom of the specified line. */ public final int getLineBottom(int line) { return getLineTop(line + 1); } /** * Return the vertical position of the baseline of the specified line. */ public final int getLineBaseline(int line) { // getLineTop(line+1) == getLineTop(line) return getLineTop(line+1) - getLineDescent(line); } /** * Get the ascent of the text on the specified line. * The return value is negative to match the Paint.ascent() convention. */ public final int getLineAscent(int line) { // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line) return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line)); } /** * Get the alignment of the specified paragraph, taking into account * markup attached to it. */ public final Alignment getParagraphAlignment(int line) { Alignment align = mAlignment; return align; } /** * Get the left edge of the specified paragraph, inset by left margins. */ public final int getParagraphLeft(int line) { int left = 0; int dir = getParagraphDirection(line); if (dir == DIR_RIGHT_TO_LEFT) { return left; // leading margin has no impact, or no styles } return getParagraphLeadingMargin(line); } /** * Get the right edge of the specified paragraph, inset by right margins. */ public final int getParagraphRight(int line) { int right = mWidth; int dir = getParagraphDirection(line); if (dir == DIR_LEFT_TO_RIGHT) { return right; // leading margin has no impact, or no styles } return right - getParagraphLeadingMargin(line); } /** * Returns the effective leading margin (unsigned) for this line, * taking into account LeadingMarginSpan and LeadingMarginSpan2. * @param line the line index * @return the leading margin of this line */ private int getParagraphLeadingMargin(int line) { return 0; } /* package */ static float measurePara(GLPaint paint, CharSequence text, int start, int end) { return paint.measureText(text, start, end); } /** * Stores information about bidirectional (left-to-right or right-to-left) * text within the layout of a line. */ public static class Directions { // Directions represents directional runs within a line of text. // Runs are pairs of ints listed in visual order, starting from the // leading margin. The first int of each pair is the offset from // the first character of the line to the start of the run. The // second int represents both the length and level of the run. // The length is in the lower bits, accessed by masking with // DIR_LENGTH_MASK. The level is in the higher bits, accessed // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK. // To simply test for an RTL direction, test the bit using // DIR_RTL_FLAG, if set then the direction is rtl. /* package */ int[] mDirections; /* package */ Directions(int[] dirs) { mDirections = dirs; } } /** * Return the offset of the first character to be ellipsized away, * relative to the start of the line. (So 0 if the beginning of the * line is ellipsized, not getLineStart().) */ public abstract int getEllipsisStart(int line); /** * Returns the number of characters to be ellipsized away, or 0 if * no ellipsis is to take place. */ public abstract int getEllipsisCount(int line); private char getEllipsisChar(TextUtils.TruncateAt method) { return (method == TextUtils.TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS[0] : ELLIPSIS_NORMAL[0]; } private void ellipsize(int start, int end, int line, char[] dest, int destoff, TextUtils.TruncateAt method) { int ellipsisCount = getEllipsisCount(line); if (ellipsisCount == 0) { return; } int ellipsisStart = getEllipsisStart(line); int linestart = getLineStart(line); for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) { char c; if (i == ellipsisStart) { c = getEllipsisChar(method); // ellipsis } else { c = '\uFEFF'; // 0-width space } int a = i + linestart; if (a >= start && a < end) { dest[destoff + a - start] = c; } } } /* package */ static class Ellipsizer implements CharSequence, GetChars { /* package */ CharSequence mText; /* package */ Layout mLayout; /* package */ int mWidth; /* package */ TextUtils.TruncateAt mMethod; public Ellipsizer(CharSequence s) { mText = s; } public char charAt(int off) { char[] buf = TextUtils.obtain(1); getChars(off, off + 1, buf, 0); char ret = buf[0]; TextUtils.recycle(buf); return ret; } public void getChars(int start, int end, char[] dest, int destoff) { int line1 = mLayout.getLineForOffset(start); int line2 = mLayout.getLineForOffset(end); TextUtils.getChars(mText, start, end, dest, destoff); for (int i = line1; i <= line2; i++) { mLayout.ellipsize(start, end, i, dest, destoff, mMethod); } } public int length() { return mText.length(); } public CharSequence subSequence(int start, int end) { char[] s = new char[end - start]; getChars(start, end, s, 0); return new String(s); } @Override public String toString() { char[] s = new char[length()]; getChars(0, length(), s, 0); return new String(s); } } public void setDrawDefer(boolean drawDefer) { mDrawDefer = drawDefer; } private CharSequence mText; private GLPaint mPaint; private int mWidth; private Alignment mAlignment = Alignment.ALIGN_NORMAL; private float mSpacingMult; private float mSpacingAdd; private TextDirectionHeuristic mTextDir; protected boolean mDrawDefer; public static final int DIR_LEFT_TO_RIGHT = 1; public static final int DIR_RIGHT_TO_LEFT = -1; /* package */ static final int DIR_REQUEST_LTR = 1; /* package */ static final int DIR_REQUEST_RTL = -1; /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2; /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2; /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff; /* package */ static final int RUN_LEVEL_SHIFT = 26; /* package */ static final int RUN_LEVEL_MASK = 0x3f; /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT; public enum Alignment { ALIGN_NORMAL, ALIGN_OPPOSITE, ALIGN_CENTER, /** @hide */ ALIGN_LEFT, /** @hide */ ALIGN_RIGHT, } /* package */ static final char[] ELLIPSIS_NORMAL = { '\u2026' }; // this is "..." /* package */ static final char[] ELLIPSIS_TWO_DOTS = { '\u2025' }; // this is ".." /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT = new Directions(new int[] { 0, RUN_LENGTH_MASK }); /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT = new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG }); }