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 });
}