Android之TextView文字绘制流程
一:TextView的onDraw()方法:
?
1.第一句restartMarqueeIfNeeded()繪制字幕滾動。
protected void onDraw(Canvas canvas) {restartMarqueeIfNeeded();// Draw the background for this viewsuper.onDraw(canvas);...
}
首先我們看一個東西:
android.text.TextUtils.java
public enum TruncateAt {START,MIDDLE,END,MARQUEE,/*** @hide*/END_SMALL}很熟悉對不對,這就是平常在TextView的android:ellipsize屬性,當(dāng)字符顯示不下的時候省略號所在的位置,有開始/結(jié)束/中間/滾動四個枚舉值。每次onDraw的時候都檢測是否需要滾動字幕,重新滾幕的條件就是android:ellipsize屬性是MARQUEE(也就是滾動字幕)和mRestartMarquee 布爾值。
private void restartMarqueeIfNeeded() {if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) {mRestartMarquee = false;startMarquee();}}關(guān)于這部分就講這么多,知道這個是滾動字幕的就行了,若對滾幕感興趣自行研究canMarquee()/startMarquee()/stopMarquee()/startStopMarquee(boolean start)/Marquee類。
?
2.compoundDrawable的繪制,也就是drawableTop/Bottom/Left/Right屬性。
// Draw the background for this viewsuper.onDraw(canvas);final int compoundPaddingLeft = getCompoundPaddingLeft();final int compoundPaddingTop = getCompoundPaddingTop();final int compoundPaddingRight = getCompoundPaddingRight();final int compoundPaddingBottom = getCompoundPaddingBottom();....
final Drawables dr = mDrawables;if (dr != null) {/** Compound, not extended, because the icon is not clipped* if the text height is smaller.*/int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;// IMPORTANT: The coordinates computed are also used in invalidateDrawable()// Make sure to update invalidateDrawable() when changing this code.if (dr.mShowing[Drawables.LEFT] != null) {canvas.save();canvas.translate(scrollX + mPaddingLeft + leftOffset,scrollY + compoundPaddingTop +(vspace - dr.mDrawableHeightLeft) / 2);dr.mShowing[Drawables.LEFT].draw(canvas);canvas.restore();}// IMPORTANT: The coordinates computed are also used in invalidateDrawable()// Make sure to update invalidateDrawable() when changing this code.if (dr.mShowing[Drawables.RIGHT] != null) {canvas.save();canvas.translate(scrollX + right - left - mPaddingRight- dr.mDrawableSizeRight - rightOffset,scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);dr.mShowing[Drawables.RIGHT].draw(canvas);canvas.restore();}// IMPORTANT: The coordinates computed are also used in invalidateDrawable()// Make sure to update invalidateDrawable() when changing this code.if (dr.mShowing[Drawables.TOP] != null) {canvas.save();canvas.translate(scrollX + compoundPaddingLeft +(hspace - dr.mDrawableWidthTop) / 2, scrollY + mPaddingTop);dr.mShowing[Drawables.TOP].draw(canvas);canvas.restore();}// IMPORTANT: The coordinates computed are also used in invalidateDrawable()// Make sure to update invalidateDrawable() when changing this code.if (dr.mShowing[Drawables.BOTTOM] != null) {canvas.save();canvas.translate(scrollX + compoundPaddingLeft +(hspace - dr.mDrawableWidthBottom) / 2,scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);dr.mShowing[Drawables.BOTTOM].draw(canvas);canvas.restore();}} Drawables是TextView下的靜態(tài)類,持有著mShowing(drawable數(shù)組)上下左右四個drawable,這四個drawable繪制在不同的位置。
3.TextPaint和Layout,其實還有mEditor,也就是可編輯狀態(tài)下的情況(EditText)。這部分先初始化畫筆TextPaint,Cavans畫布,最重要的就是Layout,由它負(fù)責(zé)文字繪制。 Path highlight = getUpdatedHighlightPath();if (mEditor != null) {mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);} else {layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);}
?
二:Layout類
Layout是android.text下的一個抽象類,負(fù)責(zé)文字布局繪畫,它有兩個子類分別是DynamicLayout和StaticLayout,前者是可編輯狀態(tài)下的(EditText),后者是靜態(tài)的。
/*** Draw this Layout on the specified Canvas.繪制在指定的畫布*/public void draw(Canvas c) {draw(c, null, null, 0);}/*** Draw this Layout on the specified canvas, with the highlight path drawn* between the background and the text.
在背景和文字之間繪制高亮** @param canvas the canvas* @param highlight the path of the highlight or cursor; can be null* @param highlightPaint the paint for the highlight* @param cursorOffsetVertical the amount to temporarily translate the* canvas while rendering the highlight*/public void draw(Canvas canvas, Path highlight, Paint highlightPaint,int cursorOffsetVertical) {final long lineRange = getLineRangeForDraw(canvas);//獲取需要繪制的區(qū)間行int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);//第一行int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);//最后一行if (lastLine < 0) return;drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,firstLine, lastLine);drawText(canvas, firstLine, lastLine);}
1.先看畫背景:
public void drawBackground(Canvas canvas, Path highlight, Paint highlightPaint,int cursorOffsetVertical, int firstLine, int lastLine) {// First, draw LineBackgroundSpans.//首先,繪制LineBackgroundSpans(不是View的Backgrond哦)// LineBackgroundSpans know nothing about the alignment, margins, or?/它不需要自動對齊方式,間距或方向// direction of the layout or line. XXX: Should they?//xxx:需要嗎?// They are evaluated at each line.//將會應(yīng)用在每一行。if (mSpannedText) {//SpannedText才能設(shè)置Spanif (mLineBackgroundSpans == null) {mLineBackgroundSpans = new SpanSet<LineBackgroundSpan>(LineBackgroundSpan.class);}Spanned buffer = (Spanned) mText;int textLength = buffer.length();mLineBackgroundSpans.init(buffer, 0, textLength);if (mLineBackgroundSpans.numberOfSpans > 0) {//行背景span數(shù)量int previousLineBottom = getLineTop(firstLine);//記錄上一行的topint previousLineEnd = getLineStart(firstLine);//記錄上一行的endParagraphStyle[] spans = NO_PARA_SPANS;//段落樣式int spansLength = 0;TextPaint paint = mPaint;int spanEnd = 0;final int width = mWidth;for (int i = firstLine; i <= lastLine; i++) {//遍歷每行int start = previousLineEnd;int end = getLineStart(i + 1);//下一行的endpreviousLineEnd = end;int ltop = previousLineBottom;int lbottom = getLineTop(i + 1);//獲取下一行的top,也就是本行的bottompreviousLineBottom = lbottom;int lbaseline = lbottom - getLineDescent(i);if (start >= spanEnd) {// These should be infrequent, so we'll use this so that// we don't have to check as often.spanEnd = mLineBackgroundSpans.getNextTransition(start, textLength);// All LineBackgroundSpans on a line contribute to its background.spansLength = 0;// Duplication of the logic of getParagraphSpansif (start != end || start == 0) {// Equivalent to a getSpans(start, end), but filling the 'spans' local// array instead to reduce memory allocationfor (int j = 0; j < mLineBackgroundSpans.numberOfSpans; j++) {//如果設(shè)置了多個LineBackgroundSpan將一一畫上// equal test is valid since both intervals are not empty by// constructionif (mLineBackgroundSpans.spanStarts[j] >= end ||mLineBackgroundSpans.spanEnds[j] <= start) continue;spans = GrowingArrayUtils.append(spans, spansLength, mLineBackgroundSpans.spans[j]);spansLength++;}}}for (int n = 0; n < spansLength; n++) {//所有的行數(shù)和行背景(line.number*span.number)LineBackgroundSpan lineBackgroundSpan = (LineBackgroundSpan) spans[n];lineBackgroundSpan.drawBackground(canvas, paint, 0, width,ltop, lbaseline, lbottom,buffer, start, end, i);}}}mLineBackgroundSpans.recycle();//SpanSet回收}// There can be a highlight even without spans if we are drawing// a non-spanned transformation of a spanned editing buffer.if (highlight != null) {//繪制hightlight路徑(比如光標(biāo))if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical);canvas.drawPath(highlight, highlightPaint);if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical);}}至于lineBottom和linrEnd是由子類(DynamicLayout和StaticLayout)的getLineTop和getLineStart方法獲取的,很復(fù)雜很復(fù)雜。
2.畫字drawText:
public void drawText(Canvas canvas, int firstLine, int lastLine) {int previousLineBottom = getLineTop(firstLine);int previousLineEnd = getLineStart(firstLine);ParagraphStyle[] spans = NO_PARA_SPANS;int spanEnd = 0;TextPaint paint = mPaint;CharSequence buf = mText;Alignment paraAlign = mAlignment;TabStops tabStops = null;boolean tabStopsIsInitialized = false;TextLine tl = TextLine.obtain();// Draw the lines, one at a time.// The baseline is the top of the following line minus the current line's descent.for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {//遍歷每行int start = previousLineEnd;//開始previousLineEnd = getLineStart(lineNum + 1);//記錄endint end = getLineVisibleEnd(lineNum, start, previousLineEnd);//結(jié)束int ltop = previousLineBottom;//行topint lbottom = getLineTop(lineNum + 1);//行bottom,也就是下一行的toppreviousLineBottom = lbottom;//記錄行Bottomint lbaseline = lbottom - getLineDescent(lineNum);//行基線,bottom-descentint dir = getParagraphDirection(lineNum);//段亂排版方向int left = 0;int right = mWidth;//一:畫LeadingMarginif (mSpannedText) {//是spannedTextSpanned sp = (Spanned) buf;//textint textLength = buf.length();boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == '\n');//段落第一行// New batch of paragraph styles, collect into spans array.// Compute the alignment, last alignment style wins.// Reset tabStops, we'll rebuild if we encounter a line with// tabs.// We expect paragraph spans to be relatively infrequent, use// spanEnd so that we can check less frequently. Since// paragraph styles ought to apply to entire paragraphs, we can// just collect the ones present at the start of the paragraph.// If spanEnd is before the end of the paragraph, that's not// our problem.if (start >= spanEnd && (lineNum == firstLine || isFirstParaLine)) {spanEnd = sp.nextSpanTransition(start, textLength,ParagraphStyle.class);spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);//獲取段落樣式paraAlign = mAlignment;//段落對齊方式for (int n = spans.length - 1; n >= 0; n--) {if (spans[n] instanceof AlignmentSpan) {paraAlign = ((AlignmentSpan) spans[n]).getAlignment();break;}}tabStopsIsInitialized = false;}
//畫出LeadingMarginSpan// Draw all leading margin spans. Adjust left or right according// to the paragraph direction of the line.final int length = spans.length;boolean useFirstLineMargin = isFirstParaLine;for (int n = 0; n < length; n++) {if (spans[n] instanceof LeadingMarginSpan2) {int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount();int startLine = getLineForOffset(sp.getSpanStart(spans[n]));// if there is more than one LeadingMarginSpan2, use// the count that is greatestif (lineNum < startLine + count) {useFirstLineMargin = true;break;}}}for (int n = 0; n < length; n++) {if (spans[n] instanceof LeadingMarginSpan) {//LeadingMarginSpanLeadingMarginSpan margin = (LeadingMarginSpan) spans[n];if (dir == DIR_RIGHT_TO_LEFT) {//右往左margin.drawLeadingMargin(canvas, paint, right, dir, ltop,lbaseline, lbottom, buf,start, end, isFirstParaLine, this);right -= margin.getLeadingMargin(useFirstLineMargin);} else {//正常閱讀順序margin.drawLeadingMargin(canvas, paint, left, dir, ltop,lbaseline, lbottom, buf,start, end, isFirstParaLine, this);left += margin.getLeadingMargin(useFirstLineMargin);}}}}
//二:Tab或Emojiboolean hasTabOrEmoji = getLineContainsTab(lineNum);// Can't tell if we have tabs for sure, currentlyif (hasTabOrEmoji && !tabStopsIsInitialized) {if (tabStops == null) {tabStops = new TabStops(TAB_INCREMENT, spans);} else {tabStops.reset(TAB_INCREMENT, spans);}tabStopsIsInitialized = true;}// Determine whether the line aligns to normal, opposite, or center.
//三:對齊方式
Alignment align = paraAlign;if (align == Alignment.ALIGN_LEFT) {align = (dir == DIR_LEFT_TO_RIGHT) ?Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;} else if (align == Alignment.ALIGN_RIGHT) {align = (dir == DIR_LEFT_TO_RIGHT) ?Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;}
//四:獲取x軸,然后寫字。int x;if (align == Alignment.ALIGN_NORMAL) {if (dir == DIR_LEFT_TO_RIGHT) {x = left + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);} else {x = right + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);}} else {int max = (int)getLineExtent(lineNum, tabStops, false);if (align == Alignment.ALIGN_OPPOSITE) {if (dir == DIR_LEFT_TO_RIGHT) {x = right - max + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);} else {x = left - max + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);}} else { // Alignment.ALIGN_CENTERmax = max & ~1;x = ((right + left - max) >> 1) +getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);}}paint.setHyphenEdit(getHyphen(lineNum));Directions directions = getLineDirections(lineNum);
//閱讀方式從左向右的,沒有tab和emoji表情,非SpannedText,就是最原始最傳統(tǒng)最簡單畫文字cavans.drawTextif (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTabOrEmoji) {// XXX: assumes there's nothing additional to be done canvas.drawText(buf, start, end, x, lbaseline, paint);} else {//復(fù)雜的交給TextLinetl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops);tl.draw(canvas, x, ltop, lbaseline, lbottom);}paint.setHyphenEdit(0);}TextLine.recycle(tl);}
遍歷每一行,主要是由四個流程:畫LeadingMargin——>確認(rèn)tab/emoji(TextLine來畫)——>根據(jù)對齊方式確定從x軸哪個位置開始畫(比如居左x就是0咯)——>
根據(jù)條件判斷是交給cavans直接drawText還是TextLine來畫字。
?
三:Canvas&TextLine
1.先看Canvas的drawText方法,就看方法doc。
/*** Draw the specified range of text, specified by start/end, with its* origin at (x,y), in the specified Paint. The origin is interpreted* based on the Align setting in the Paint.** @param text The text to be drawn* @param start The index of the first character in text to draw* @param end (end - 1) is the index of the last character in text* to draw* @param x The x-coordinate of origin for where to draw the text* @param y The y-coordinate of origin for where to draw the text* @param paint The paint used for the text (e.g. color, size, style)*/public void drawText(@NonNull CharSequence text, int start, int end, float x, float y,@NonNull Paint paint)這個注釋說了三個點,一是畫多少個字(start-end),二是從哪開始畫,即原點(origin),這個有xy坐標(biāo)軸來確定,基于對齊方式的設(shè)定,最后就是畫筆paint。
說明一下,start和end是從text里面的所以區(qū)段,而原點的x軸跟對齊方式相關(guān),y軸一般是baseline。
2.TextLine的draw流程:
void draw(Canvas c, float x, int top, int y, int bottom) {//drawRun畫字if (!mHasTabs) {if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {drawRun(c, 0, mLen, false, x, top, y, bottom, false);return;}if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {drawRun(c, 0, mLen, true, x, top, y, bottom, false);return;}}
//根據(jù)字符轉(zhuǎn)成emoji位圖float h = 0;int[] runs = mDirections.mDirections;RectF emojiRect = null;int lastRunIndex = runs.length - 2;for (int i = 0; i < runs.length; i += 2) {...for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {int codept = 0;Bitmap bm = null;...bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept);...emojiRect.set(x + h, y + bmAscent,x + h + width, y);c.drawBitmap(bm, null, emojiRect, mPaint);...}}}}
轉(zhuǎn)入drawRun方法
/*** Draws a unidirectional (but possibly multi-styled) run of text.*** @param c the canvas to draw on* @param start the line-relative start* @param limit the line-relative limit* @param runIsRtl true if the run is right-to-left* @param x the position of the run that is closest to the leading margin* @param top the top of the line* @param y the baseline* @param bottom the bottom of the line* @param needWidth true if the width value is required.* @return the signed width of the run, based on the paragraph direction.* Only valid if needWidth is true.*/private float drawRun(Canvas c, int start,int limit, boolean runIsRtl, float x, int top, int y, int bottom,boolean needWidth)而真正的方法還得往下走:
private float handleRun(int start, int measureLimit,int limit, boolean runIsRtl, Canvas c, float x, int top, int y,int bottom, FontMetricsInt fmi, boolean needWidth) {// Case of an empty line, make sure we update fmi according to mPaint//空行,更新FontMetricsIntif (start == measureLimit) {TextPaint wp = mWorkPaint;wp.set(mPaint);if (fmi != null) {expandMetricsFromPaint(fmi, wp);}return 0f;}
//無mSpanned,直接handleTextif (mSpanned == null) {TextPaint wp = mWorkPaint;wp.set(mPaint);final int mlimit = measureLimit;return handleText(wp, start, mlimit, start, limit, runIsRtl, c, x, top,y, bottom, fmi, needWidth || mlimit < measureLimit);}
//初始化MetricAffectingSpan和CharacterStyleSpanmMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);// Shaping needs to take into account context up to metric boundaries,// but rendering needs to take into account character style boundaries.// So we iterate through metric runs to get metric bounds,// then within each metric run iterate through character style runs// for the run bounds.final float originalX = x;for (int i = start, inext; i < measureLimit; i = inext) {TextPaint wp = mWorkPaint;wp.set(mPaint);inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -mStart;int mlimit = Math.min(inext, measureLimit);ReplacementSpan replacement = null;
//遍歷MetrixAffectingSpanfor (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {// Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT// empty by construction. This special case in getSpans() explains the >= & <= testsif ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||(mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];if (span instanceof ReplacementSpan) {//ReplacementSpan特俗處理replacement = (ReplacementSpan)span;} else {// We might have a replacement that uses the draw// state, otherwise measure state would suffice. span.updateDrawState(wp);//TextPaint拋出去}}
//處理ReplacementSpanif (replacement != null) {x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,bottom, fmi, needWidth || mlimit < measureLimit);continue;}
//遍歷CharecterStyleSpanfor (int j = i, jnext; j < mlimit; j = jnext) {jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + mlimit) -mStart;wp.set(mPaint);for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {// Intentionally using >= and <= as explained aboveif ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + jnext) ||(mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;CharacterStyle span = mCharacterStyleSpanSet.spans[k];span.updateDrawState(wp);//更新draw狀態(tài)}// Only draw hyphen on last run in lineif (jnext < mLen) {wp.setHyphenEdit(0);}
//渲染文字...x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,top, y, bottom, fmi, needWidth || jnext < measureLimit);}}return x - originalX;}
看到這個TextLine主要還是處理SpannedText,遍歷出MetricAffectingSpan和CharactStyleSpan,MetricAffectingSpan下面有個ReplacementSpan,其余
的span都是更新draw狀態(tài),渲染文字最終還是在handleTextprivate float handleText(TextPaint wp, int start, int end,
int contextStart, int contextEnd, boolean runIsRtl,Canvas c, float x, int top, int y, int bottom,FontMetricsInt fmi, boolean needWidth) {...if (c != null) {if (runIsRtl) {x -= ret;}if (wp.bgColor != 0) {//畫背景色int previousColor = wp.getColor();Paint.Style previousStyle = wp.getStyle();wp.setColor(wp.bgColor);wp.setStyle(Paint.Style.FILL);c.drawRect(x, top, x + ret, bottom, wp);wp.setStyle(previousStyle);wp.setColor(previousColor);}if (wp.underlineColor != 0) {//畫下劃線// kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h//下劃線.top=文字大小的1/9+baseline+baselineShift,也就是說是從baseline空格再往下字符大小的1/9
float underlineTop = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();int previousColor = wp.getColor();Paint.Style previousStyle = wp.getStyle();boolean previousAntiAlias = wp.isAntiAlias();wp.setStyle(Paint.Style.FILL);wp.setAntiAlias(true);wp.setColor(wp.underlineColor);
//線的粗細(xì),是在TextPaint中定義的c.drawRect(x, underlineTop, x + ret, underlineTop + wp.underlineThickness, wp);wp.setStyle(previousStyle);wp.setColor(previousColor);wp.setAntiAlias(previousAntiAlias);}drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,x, y + wp.baselineShift);}return runIsRtl ? -ret : ret;}
那drawTextRun中的方法是怎么實現(xiàn)的呢?
private void drawTextRun(Canvas c, TextPaint wp, int start, int end,int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {if (mCharsValid) {int count = end - start;int contextCount = contextEnd - contextStart;c.drawTextRun(mChars, start, count, contextStart, contextCount,x, y, runIsRtl, wp);} else {int delta = mStart;c.drawTextRun(mText, delta + start, delta + end,delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);}}可以看到是調(diào)用canvas實現(xiàn)的,canvas都是通過native方法來實現(xiàn)的。
?
最后上公眾號,文章同步,手機閱讀。
總結(jié)
以上是生活随笔為你收集整理的Android之TextView文字绘制流程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 配置安全的Impala集群集成Sentr
- 下一篇: 关于 htonl 和 ntohl 的实现