protected void onDraw(Canvas canvas) { restartMarqueeIfNeeded(); // Draw the background for this view super.onDraw(canvas);
...
}
首先咱們看一個東西:java
android.text.TextUtils.javaandroid
public enum TruncateAt { START, MIDDLE, END, MARQUEE, /** * @hide */ END_SMALL }
很熟悉對不對,這就是日常在TextView的android:ellipsize屬性,當字符顯示不下的時候省略號所在的位置,有開始/結束/中間/滾動四個枚舉值。每次onDraw的時候都檢測是否須要滾動字幕,從新滾幕的條件就是android:ellipsize屬性是MARQUEE(也就是滾動字幕)和mRestartMarquee 布爾值。canvas
private void restartMarqueeIfNeeded() { if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) { mRestartMarquee = false; startMarquee(); } }
關於這部分就講這麼多,知道這個是滾動字幕的就好了,若對滾幕感興趣自行研究canMarquee()/startMarquee()/stopMarquee()/startStopMarquee(boolean start)/Marquee類。api
// Draw the background for this view super.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下的靜態類,持有着mShowing(drawable數組)上下左右四個drawable,這四個drawable繪製在不一樣的位置。
3.TextPaint和Layout,其實還有mEditor,也就是可編輯狀態下的狀況(EditText)。這部分先初始化畫筆TextPaint,Cavans畫布,最重要的就是Layout,由它負責文字繪製。
Path highlight = getUpdatedHighlightPath(); if (mEditor != null) { mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical); } else { layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical); }
Layout是android.text下的一個抽象類,負責文字佈局繪畫,它有兩個子類分別是DynamicLayout和StaticLayout,前者是可編輯狀態下的(EditText),後者是靜態的。數組
/** * 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);//獲取須要繪製的區間行 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); }
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.//將會應用在每一行。 if (mSpannedText) {//SpannedText才能設置Span if (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數量 int previousLineBottom = getLineTop(firstLine);//記錄上一行的top int previousLineEnd = getLineStart(firstLine);//記錄上一行的end ParagraphStyle[] 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);//下一行的end previousLineEnd = end; int ltop = previousLineBottom; int lbottom = getLineTop(i + 1);//獲取下一行的top,也就是本行的bottom previousLineBottom = 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 getParagraphSpans if (start != end || start == 0) { // Equivalent to a getSpans(start, end), but filling the 'spans' local // array instead to reduce memory allocation for (int j = 0; j < mLineBackgroundSpans.numberOfSpans; j++) {//若是設置了多個LineBackgroundSpan將一一畫上 // equal test is valid since both intervals are not empty by // construction if (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++) {//全部的行數和行背景(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路徑(好比光標) if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical); canvas.drawPath(highlight, highlightPaint); if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical); } }
至於lineBottom和linrEnd是由子類(DynamicLayout和StaticLayout)的getLineTop和getLineStart方法獲取的,很複雜很複雜。app
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);//記錄end int end = getLineVisibleEnd(lineNum, start, previousLineEnd);//結束 int ltop = previousLineBottom;//行top int lbottom = getLineTop(lineNum + 1);//行bottom,也就是下一行的top previousLineBottom = lbottom;//記錄行Bottom int lbaseline = lbottom - getLineDescent(lineNum);//行基線,bottom-descent int dir = getParagraphDirection(lineNum);//段亂排版方向 int left = 0; int right = mWidth;
//一:畫LeadingMargin if (mSpannedText) {//是spannedText Spanned sp = (Spanned) buf;//text int 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 greatest if (lineNum < startLine + count) { useFirstLineMargin = true; break; } } } for (int n = 0; n < length; n++) { if (spans[n] instanceof LeadingMarginSpan) {//LeadingMarginSpan LeadingMarginSpan 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或Emoji boolean hasTabOrEmoji = getLineContainsTab(lineNum); // Can't tell if we have tabs for sure, currently if (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_CENTER max = max & ~1; x = ((right + left - max) >> 1) + getIndentAdjust(lineNum, Alignment.ALIGN_CENTER); } } paint.setHyphenEdit(getHyphen(lineNum)); Directions directions = getLineDirections(lineNum);
//閱讀方式從左向右的,沒有tab和emoji表情,非SpannedText,就是最原始最傳統最簡單畫文字cavans.drawText if (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 {//複雜的交給TextLine tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops); tl.draw(canvas, x, ltop, lbaseline, lbottom); } paint.setHyphenEdit(0); } TextLine.recycle(tl); }
遍歷每一行,主要是由四個流程:畫LeadingMargin——>確認tab/emoji(TextLine來畫)——>根據對齊方式肯定從x軸哪一個位置開始畫(好比居左x就是0咯)——>less
根據條件判斷是交給cavans直接drawText仍是TextLine來畫字。ide
/** * 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座標軸來肯定,基於對齊方式的設定,最後就是畫筆paint。佈局
說明一下,start和end是從text裏面的因此區段,而原點的x軸跟對齊方式相關,y軸通常是baseline。ui
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; } }
//根據字符轉成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); ... } } } }
轉入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
//空行,更新FontMetricsInt if (start == measureLimit) { TextPaint wp = mWorkPaint; wp.set(mPaint); if (fmi != null) { expandMetricsFromPaint(fmi, wp); } return 0f; }
//無mSpanned,直接handleText if (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和CharacterStyleSpan mMetricAffectingSpanSpanSet.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;
//遍歷MetrixAffectingSpan for (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 >= & <= tests if ((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拋出去 } }
//處理ReplacementSpan if (replacement != null) { x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y, bottom, fmi, needWidth || mlimit < measureLimit); continue; }
//遍歷CharecterStyleSpan for (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 above if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + jnext) || (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue; CharacterStyle span = mCharacterStyleSpanSet.spans[k]; span.updateDrawState(wp);//更新draw狀態 } // Only draw hyphen on last run in line if (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狀態,渲染文字最終仍是在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);
//線的粗細,是在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中的方法是怎麼實現的呢?
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); } }
能夠看到是調用canvas實現的,canvas都是經過native方法來實現的。
最後上公衆號,文章同步,手機閱讀。