1 簡介
之前已經講過TextView的基礎知識,現在在這進一步進行講解,這篇文字主要講解如何給TextView設置段落級別的Span。如果一個Span想要影響段落層次的文本格式,則需要實現ParagraphStyle。
2 ParagraphStyle
ParagraphStyle是一個接口,通過查看Android源碼,我們發現這個接口里面什么方法也沒有定義,因此,我們可以認為,這個接口無非是標識實現這個接口的Span為段落級別的Span。
在Android源碼中又繼續定義了幾個接口實現了ParagraphStyle接口。
- LeadingMarginSpan:用來處理像word中項目符號一樣的接口;
- AlignmentSpan:用來處理整個段落對其的接口;
- LineBackgroundSpan:用來處理一行的背景的接口;
- LineHeightSpan:用來處理一行高度的接口;
- TabStopSpan:用來將字符串中的"\t"替換成相應的空行;
3 LeadingMarginSpan
LeadingMarginSpan用來控制整個段落左邊或者右邊顯示某些特定效果,里面有兩個接口方法。
/**
* Returns the amount by which to adjust the leading margin. Positive values
* move away from the leading edge of the paragraph, negative values move
* towards it.
*
* @param first true if the request is for the first line of a paragraph,
* false for subsequent lines
* @return the offset for the margin.
*/
public int getLeadingMargin(boolean first);
/**
* Renders the leading margin. This is called before the margin has been
* adjusted by the value returned by {@link #getLeadingMargin(boolean)}.
*
* @param c the canvas
* @param p the paint. The this should be left unchanged on exit.
* @param x the current position of the margin
* @param dir the base direction of the paragraph; if negative, the margin
* is to the right of the text, otherwise it is to the left.
* @param top the top of the line
* @param baseline the baseline of the line
* @param bottom the bottom of the line
* @param text the text
* @param start the start of the line
* @param end the end of the line
* @param first true if this is the first line of its paragraph
* @param layout the layout containing this line
*/
public void drawLeadingMargin(Canvas c, Paint p,
int x, int dir,
int top, int baseline, int bottom,
CharSequence text, int start, int end,
boolean first, Layout layout);
LeadingMarginSpan2還多規定了一個方法。
/**
* Returns the number of lines of the paragraph to which this object is
* attached that the "first line" margin will apply to.
*/
public int getLeadingMarginLineCount();
第一個方法first為是否為第一行,返回值為整個段落偏移的距離。
第二個方法可以在偏移的位置里面進行各種效果繪制。
第三個方法可以控制影響的行數。
下面通過三個LeadingMarginSpan的實現來具體說明。
3.1 BulletSpan
先來看BulletSpan實現的效果,效果如下圖所示:
通過上面的圖片可以看見整個段落右移了一段距離,然后在移動留下的空間處繪制了一個小圓點。
具體來看代碼,BulletSpan代碼如下所示:
public int getLeadingMargin(boolean first) {
return 2 * BULLET_RADIUS + mGapWidth;
}
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
int top, int baseline, int bottom,
CharSequence text, int start, int end,
boolean first, Layout l) {
if (((Spanned) text).getSpanStart(this) == start) {
Paint.Style style = p.getStyle();
int oldcolor = 0;
if (mWantColor) {
oldcolor = p.getColor();
p.setColor(mColor);
}
p.setStyle(Paint.Style.FILL);
if (c.isHardwareAccelerated()) {
if (sBulletPath == null) {
sBulletPath = new Path();
// Bullet is slightly better to avoid aliasing artifacts on mdpi devices.
sBulletPath.addCircle(0.0f, 0.0f, 1.2f * BULLET_RADIUS, Direction.CW);
}
c.save();
c.translate(x + dir * BULLET_RADIUS, (top + bottom) / 2.0f);
c.drawPath(sBulletPath, p);
c.restore();
} else {
c.drawCircle(x + dir * BULLET_RADIUS, (top + bottom) / 2.0f, BULLET_RADIUS, p);
}
if (mWantColor) {
p.setColor(oldcolor);
}
p.setStyle(style);
}
}
第一個方法無論是否是第一行都返回了偏移距離為2 * BULLET_RADIUS + mGapWidth,因此整個段落都移動了相應的距離。
第二個方法繪制了一個圓形,((Spanned) text).getSpanStart(this) == start判斷了這一行的起始位置是否是整個Span的起始位置,如果是則繪制圓形,如果把這個判斷去掉,那么每一行都將繪制小圓形。
3.2 QuoteSpan
先看實現的效果,實現的效果如下所示:
QuoteSpan代碼如下所示:
public int getLeadingMargin(boolean first) {
return STRIPE_WIDTH + GAP_WIDTH;
}
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
int top, int baseline, int bottom,
CharSequence text, int start, int end,
boolean first, Layout layout) {
Paint.Style style = p.getStyle();
int color = p.getColor();
p.setStyle(Paint.Style.FILL);
p.setColor(mColor);
c.drawRect(x, top, x + dir * STRIPE_WIDTH, bottom, p);
p.setStyle(style);
p.setColor(color);
}
上面的代碼就十分清晰了,每行都偏移相應距離,然后每行都繪制矩形,就連成了一條豎線。
3.3 TextRoundSpan
如果希望做到兩端文字環繞圖片的效果,其實可以考慮編寫Span實現LeadingMarginSpan2。具體做法其實比較簡單,相對布局中放置ImageView和TextView,然后根據ImageView的大小計算TextView需要偏移的距離和行數,整個效果就可以實現,實現的效果如下所示:
float fontSpacing=mTextView.getPaint().getFontSpacing();
lines = (int) (finalHeight/fontSpacing);
/**
* Build the layout with LeadingMarginSpan2
*/
TextRoundSpan span = new TextRoundSpan(lines, finalWidth +10 );
class TextRoundSpan implements LeadingMarginSpan.LeadingMarginSpan2 {
private int margin;
private int lines;
TextRoundSpan(int lines, int margin) {
this.margin = margin;
this.lines = lines;
}
/**
* Apply the margin
*
* @param first
* @return
*/
@Override
public int getLeadingMargin(boolean first) {
if (first) {
return margin;
} else {
return 0;
}
}
@Override
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
int top, int baseline, int bottom, CharSequence text,
int start, int end, boolean first, Layout layout) {}
@Override
public int getLeadingMarginLineCount() {
return lines;
}
};
其實分析上面可以得出當當前行數小于等于getLeadingMarginLineCount(),getLeadingMargin(boolean first)中first的值為true。
4 AlignmentSpan
AlignmentSpan處理整個段落文字排列,當設置不同的排列方式,顯示的效果不同。
AlignmentSpan接口中定義了一個接口方法,里面還有個Standard實現。
Layout.Alignment getAlignment();
AlignmentSpan比較簡單,不多做講述。
5 LineBackgroundSpan
LineBackgroundSpan用來設置每一行的背景顏色,這個和對字體設置顏色不同,具體區別如下:
可以看見下面圖片中背景顏色是整行的。
具體代碼如下:
public class MainActivity extends Activity {
private static class MySpan implements LineBackgroundSpan {
private final int color;
public MySpan(int color) {
this.color = color;
}
@Override
public void drawBackground(Canvas c, Paint p, int left, int right, int top, int baseline, int bottom, CharSequence text, int start, int end, int lnum) {
final int paintColor = p.getColor();
p.setColor(color);
c.drawRect(new Rect(left, top, right, bottom), p);
p.setColor(paintColor);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final TextView tv = new TextView(this);
setContentView(tv);
tv.setText("Lines:\n", TextView.BufferType.EDITABLE);
appendLine(tv.getEditableText(), "123456 123 12345678\n", Color.BLACK);
appendLine(tv.getEditableText(), "123456 123 12345678\n", Color.RED);
appendLine(tv.getEditableText(), "123456 123 12345678\n", Color.BLACK);
}
private void appendLine(Editable text, String string, int color) {
final int start = text.length();
text.append(string);
final int end = text.length();
text.setSpan(new MySpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
6 LineHeightSpan
要想熟練使用這個Span,需要對字體的高度設置有著較好的理解。
Top和Ascent之間存在的距離是考慮到了類似讀音符號。Android依然會在繪制文本的時候在文本外層留出一定的邊距,這就是為什么top和bottom總會比ascent和descent大一點的原因。而在TextView中我們可以通過xml設置其屬性android:includeFontPadding="false"去掉一定的邊距值但是不能完全去掉。
上面圖片的一行文字打印FontMetrics相應的值,如下所示:
- ascent:-46.38672
- top:-52.807617
- leading:0.0
- descent:12.207031
- bottom:13.549805
下面我們來看一下Android提供的DrawableMarginSpan的源碼。
public class DrawableMarginSpan
implements LeadingMarginSpan, LineHeightSpan
{
public DrawableMarginSpan(Drawable b) {
mDrawable = b;
}
public DrawableMarginSpan(Drawable b, int pad) {
mDrawable = b;
mPad = pad;
}
public int getLeadingMargin(boolean first) {
return mDrawable.getIntrinsicWidth() + mPad;
}
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
int top, int baseline, int bottom,
CharSequence text, int start, int end,
boolean first, Layout layout) {
int st = ((Spanned) text).getSpanStart(this);
int ix = (int)x;
int itop = (int)layout.getLineTop(layout.getLineForOffset(st));
int dw = mDrawable.getIntrinsicWidth();
int dh = mDrawable.getIntrinsicHeight();
// XXX What to do about Paint?
mDrawable.setBounds(ix, itop, ix+dw, itop+dh);
mDrawable.draw(c);
}
public void chooseHeight(CharSequence text, int start, int end,
int istartv, int v,
Paint.FontMetricsInt fm) {
if (end == ((Spanned) text).getSpanEnd(this)) {
int ht = mDrawable.getIntrinsicHeight();
int need = ht - (v + fm.descent - fm.ascent - istartv);
if (need > 0)
fm.descent += need;
need = ht - (v + fm.bottom - fm.top - istartv);
if (need > 0)
fm.bottom += need;
}
}
private Drawable mDrawable;
private int mPad;
}
這個Span實現了LeadingMarginSpan和LineHeightSpan接口,實現了LeadingMarginSpan接口是為了實現段落便宜的效果,不過這里的代碼存在一定的問題,因為會多次調用Drawable的繪制。實現LineHeightSpan是為了解決TextView高度的問題,設置最后一行的高度從而來保證整個TextView的高度大于或者等于Drawable的高度。
int need = ht - (v + fm.descent - fm.ascent - istartv);
上面v為這一行的起始垂直坐標,descent為正數,ascent為負數,istartv為整個Span的起始垂直坐標,上面表達式減去的就是整個TextView到這一行的高度,然后將這個高度和Drawable的高度進行對比,從而進行相應設置。
7 TabStopSpan
TabStopSpan用來將字符串中的"\t"替換成相應的空行,普通情況下"\t"不會進行顯示,當使用TabStopSpan可以將"\t"替換成相應長度的空白區域。
/**
* Returns the offset of the tab stop from the leading margin of the
* line.
* @return the offset
*/
public int getTabStop();
這個接口方法返回空白的長度。