本章从Graphic的角度来分析Android系统中一个基础的view是如何被绘制出来的(只讨论硬件加速打开的场景):
下面我们以TextView这个类的onDraw函数为例看下,这个类是很多view的父类。
protectedvoidonDraw(Canvas canvas) {
// Draw the background for this view
super.onDraw(canvas);
final intcompoundPaddingLeft = getCompoundPaddingLeft();
final intcompoundPaddingTop = getCompoundPaddingTop();
final intcompoundPaddingRight = getCompoundPaddingRight();
final intcompoundPaddingBottom = getCompoundPaddingBottom();
final intscrollX = mScrollX;
final intscrollY = mScrollY;
final intright = mRight;
final intleft = mLeft;
final intbottom = mBottom;
final inttop = mTop;
final boolean isLayoutRtl = isLayoutRtl();
final intoffset = getHorizontalOffsetForDrawables();
final intleftOffset = isLayoutRtl ? 0 : offset;
final intrightOffset = isLayoutRtl ? offset : 0 ;
final Drawables dr = mDrawables;
if(dr != null) {
intvspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
inthspace = right - left - compoundPaddingRight - compoundPaddingLeft;
// IMPORTANT: The coordinates computed are also used in invalidateDrawable()
// Make sure to update invalidateDrawable() when changing this code.
if(dr.mDrawableLeft != null) {
canvas.save();
canvas.translate(scrollX + mPaddingLeft + leftOffset,
scrollY + compoundPaddingTop +
(vspace - dr.mDrawableHeightLeft) / 2);
dr.mDrawableLeft.draw(canvas);
canvas.restore();
}
mTextPaint.setColor(color);
mTextPaint.drawableState = getDrawableState();
canvas.save();
canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
if(mEditor != null) {
mEditor.onDraw(canvas, layout, highlight, highlightPaint, cursorOffsetVertical);
} else{
layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
}
if(mMarquee != null && mMarquee.shouldDrawGhost()) {
final intdx = (int) mMarquee.getGhostOffset();
mBidiFormat = BidiFormatter.getInstance();
if(!mBidiFormat.isRtl(mText.toString())) {
canvas.translate(+dx, 0.0f);
} else{
canvas.translate(-dx, 0.0f);
}
layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
}
//NAGSM_ANDROID_HQ_FRWK Gate +
if(GateConfig.isGateEnabled() && GateConfig.isGateLcdtextEnabled())
Log.i("GATE","LCDSTR:"+ mText +"/LCDSTR");
//NAGSM_ANDROID_HQ_FRWK Gate -
canvas.restore();
if(mTextStrikeThroughEnabled) {
drawTextStrikethrough(canvas);
}
}
这个函数很长,所以我们还是按照老办法,分段来分析这个函数。
1 位置的计算
finalintcompoundPaddingLeft = getCompoundPaddingLeft();
final intcompoundPaddingTop = getCompoundPaddingTop();
final intcompoundPaddingRight = getCompoundPaddingRight();
final intcompoundPaddingBottom = getCompoundPaddingBottom();
final intscrollX = mScrollX;
final intscrollY = mScrollY;
final intright = mRight;
final intleft = mLeft;
final intbottom = mBottom;
final inttop = mTop;
final boolean isLayoutRtl = isLayoutRtl();
final intoffset = getHorizontalOffsetForDrawables();
final intleftOffset = isLayoutRtl ? 0 : offset;
final intrightOffset = isLayoutRtl ? offset : 0 ;
首先是一系列位置的计算,这些在app编码设计时都可以修改赋值,应该都是一些屏幕上绝对坐标,后面的绘制会使用到这些坐标。
2 Drawable的绘制
intvspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
inthspace = right - left - compoundPaddingRight - compoundPaddingLeft;
// IMPORTANT: The coordinates computed are also used in invalidateDrawable()
// Make sure to update invalidateDrawable() when changing this code.
if(dr.mDrawableLeft != null) {
canvas.save();
canvas.translate(scrollX + mPaddingLeft + leftOffset,
scrollY + compoundPaddingTop +
(vspace - dr.mDrawableHeightLeft) / 2);
dr.mDrawableLeft.draw(canvas);
canvas.restore();
}
首先是view上下左右四个方向上的drawable的绘制(代码只贴了左边的drawable的绘制),我们是可以再view上设置这么多drawable的,如下图:
下面我们一个个来看下调用的函数。
2.1 Canvas.save 状态的保存
看代码之前,我们来看下api中对这个函数的说明:
/**
* Based on saveFlags, can save the current matrix and clip onto a private
* stack. Subsequent calls to translate,scale,rotate,skew,concat or
* clipRect,clipPath will all operate as usual, but when the balancing
* call to restore() is made, those calls will be forgotten, and the
* settings that existed before the save() will be reinstated.
*
* @param saveFlags flag bits that specify which parts of the Canvas state
* to save/restore
* @return The value to pass to restoreToCount() to balance this save()
*/
从api里我们可以看到这个函数的作用,它是为了保存当前的一些状态,比如矩阵,坐标等等。后面函数可以随意根据自己的需要进行一些translate,scale,rotate,skew,concat之类的操作,然后在restore调用后,这些变化将恢复到save时的状态。类似与游戏存档读档的功能。
在硬件加速打开的场景下,这里实际上是调用了GLES20Canvas的save函数,
publicintsave() {
returnnSave(mRenderer, Canvas.CLIP_SAVE_FLAG | Canvas.MATRIX_SAVE_FLAG);
}
staticjint android_view_GLES20Canvas_save(JNIEnv* env, jobject clazz, OpenGLRenderer* renderer,
jint flags) {
returnrenderer->save(flags);
}
我们看到,最终实现还是通过renderer来实现,基本上Canvas的命令都是这么一个过程。
intDisplayListRenderer::save(intflags) {
addStateOp(new(alloc()) SaveOp(flags));
returnOpenGLRenderer::save(flags);
}
来看下save函数的具体实现,上面这个函数分为两部分,首先是通过addStateOp将具体操作对应的Op保存在了一个容器里面;接着通过调用OpenGLRenderer的函数,使调用立刻生效。我们来分别看下。
2.1.1 addStateOp操作命令的保存
我们首先要来理解一些Op的概念。在displaylist生效的状态下,硬件加速不会针对每一条命令都发送给GPU执行,而是先录制在displaylist里面,在最后通过回放的方式一起执行(这个详细的过程我们会在HWUI一章中讲到)。
目前我们可以这样理解Op,就是HWUI里面为了所有类型的操作都实现了对应的Op类,存储时只要存储一个类的对象即可。所有的Op都是DisplayListOp的子类,又分为两大类,一类是状态类的StateOp,一类是绘制类的DrawOp。Op在被回放时,会调用它们的replay函数,一般来说,StateOp会继续调用applyState来实现真正的逻辑,DrawOp则是调用applyDraw。
我们来看下这次保存的这个SaveOp是执行时是做了什么操作。
virtualvoidapplyState(OpenGLRenderer& renderer,intsaveCount)const{
renderer.save(mFlags);
}
这里无非就是执行了OpenGLRenderer的save方法,我们可以这样认为,OpenGLRenderer就可以实现renderer的所有功能,一旦我们调用OpenGLRenderer类的某个函数,那就意味着这条命令会被立刻执行(而不是先保存起来)。
2.1.2 save命令的执行
我们来继续看下save命令在OpenGLRenderer里面的执行(顺便提一句,从DisplayListRenderer::save代码我们看到,其实保存完Op后,save命令其实立刻被执行了。事实上,应该是所有的状态类Op在保存完之后都会立刻执行,而绘制类Op并不会执行,猜测原因应该是状态类Op其实是不需要GPU运算的,因此可以立刻执行):
intOpenGLRenderer::save(intflags) {
returnsaveSnapshot(flags);
}
intOpenGLRenderer::saveSnapshot(intflags) {
mSnapshot = newSnapshot(mSnapshot, flags);
returnmSaveCount++;
}
我们发现,这里其实就是将当前的状态保存在了一个快照Snapshot里面。所以我们下面来看下快照的概念。
2.1.3 快照Snapshot
本小节参考(http://blog.csdn.net/wind_hzx/article/details/20315471)
Snapshot可以称为快照,hwui就是通过Snapshot的方式来组织整个渲染状态的,Snapshot保存了当前渲染所需的视口,变换矩阵,裁剪区域,fbo信息等。同时还保存了当前Snapshot的上一个Snapshot,也就是通过栈的方式来组织Snapshot之间的关系的。
如图所示,在OpenGLRenderer类创建的时候,将会创建一个空的Snapshot,也就是FirstSnapshot,也可以称之为根节点,接着每次创建新的节点,都将会保存上一次的节点指针,新节点都用指针mSnapshot,来指向当年的节点,其实这就是用栈的形式来存储hwui的绘制状态,通过设置好Snapshot后,后续的绘图操作都会在mSnapshot(当前快照)上进行操作,当当前的绘制操作完成后,可以返回到上一次的Snapshot中,下一次的绘制操作不会影响上一次的绘制操作,但是上一次的绘制操作,可以通过设置来确定是否可以影响下一次Snapshot的渲染状态。
其实Snapshot很简单,本质上就是一个保存状态的快照,类似于网页快照的概念。唯一一点稍微复杂的逻辑,就是会根据flag的不同,对之前状态进行一些变换,进而生成当前的状态。
2.2 translate坐标变换
保存完状态后,函数调用translate开始进行绘制前的坐标变换。后面这种调用我们不再贴中途调用的流程,因为流程基本都是GLES20Canvas→JNI→DisplayListRenderer的顺序,我们直接看DisplayListRenderer的函数:
voidDisplayListRenderer::translate(floatdx,floatdy) {
mHasTranslate = true;
mTranslateX += dx;
mTranslateY += dy;
insertRestoreToCount();
OpenGLRenderer::translate(dx, dy);
}
voidDisplayListRenderer::insertRestoreToCount() {
if(mRestoreSaveCount >= 0) {
DisplayListOp* op = new(alloc()) RestoreToCountOp(mRestoreSaveCount);
mDisplayListData->displayListOps.add(op);
mRestoreSaveCount = -1;
}
}
和前面save函数一样,这里同样是保存了一个Op,然后立刻进行了坐标的变换。但是有趣的是,这里保存的一个Op的类型是一个RestoreToCountOp类型,看起来在执行时也压根没有做坐标变换(这个还可以理解,毕竟在录制过程中已经先做好了坐标变化,那么录制到的绘制命令坐标已经是更改过的,因此,实际上直接拿来执行即可),而且做了一些奇怪的操作(这部分代码相当奇怪,需要详细的展开研究)。
我们暂且跳过。
2.3 Drawable的绘制
接下来就是将当前的Drawable进行绘制,这里根据Drawable具体类型的不同,函数也不同,我们这里分析一个最常见的BitmapDrawable的draw过程:
publicvoiddraw(Canvas canvas) {
final BitmapState state = mBitmapState;
if(state.mRebuildShader) {
Shader.TileMode tmx = state.mTileModeX;
Shader.TileMode tmy = state.mTileModeY;
if(tmx == null && tmy == null) {
state.mPaint.setShader(null);
} else{
state.mPaint.setShader(newBitmapShader(bitmap,
tmx == null ? Shader.TileMode.CLAMP : tmx,
tmy == null ? Shader.TileMode.CLAMP : tmy));
}
state.mRebuildShader = false;
copyBounds(mDstRect);
}
Shader shader = state.mPaint.getShader();
if(shader == null) {
canvas.drawBitmap(bitmap, null, mDstRect, state.mPaint);
} else{
canvas.drawRect(mDstRect, state.mPaint);
}
}
我们这里重点分析下canvas的两个draw函数:drawBitmap和drawRect。依然是直接看DisplayListRenderer的对应函数:
status_t DisplayListRenderer::drawBitmap(SkBitmap* bitmap,floatleft,floattop, SkPaint* paint) {
bitmap = refBitmap(bitmap);
paint = refPaint(paint);
addDrawOp(new(alloc()) DrawBitmapOp(bitmap, left, top, paint));
returnDrawGlInfo::kStatusDone;
}
这里添加了一个新的DrawBitmapOp,它在reply时会调用OpenGlRenderer的drawBitmap函数。绘制的执行过程,我们会在DisplayList的回放过程中一起分析。
3 layout.draw的绘制
onDraw函数的中段很长一部分是一些参数的设置,我们这里不详细研究上层的逻辑,我们接下来重点看下一个绘制函数,layout.draw。
publicvoiddraw(Canvas canvas, Path highlight, Paint highlightPaint,
intcursorOffsetVertical) {
final longlineRange = getLineRangeForDraw(canvas);
intfirstLine = TextUtils.unpackRangeStartFromLong(lineRange);
intlastLine = TextUtils.unpackRangeEndFromLong(lineRange);
if(lastLine
drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
firstLine, lastLine);
drawText(canvas, firstLine, lastLine);
}
我们直接看drawText,这个函数同样很长,但是大多数是view层面的一些参数设置,我们依然只关心Graphic层面的绘制:
其实重点只是这一点:
{
...
if(directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTabOrEmoji) {
canvas.drawText(buf, start, end, x, lbaseline, paint);
} else{
tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops);
tl.draw(canvas, x, ltop, lbaseline, lbottom);
}
...
}
看到了canvas的drawText,在打开硬件加速的情况下,我们可以按照老规矩直接看DisplayListRenderer的对应函数:
status_t DisplayListRenderer::drawText(constchar* text,intbytesCount,intcount,
floatx,floaty,constfloat* positions, SkPaint* paint,
floattotalAdvance,constRect& bounds, DrawOpMode drawOpMode) {
text = refText(text, bytesCount);
positions = refBuffer(positions, count * 2);
paint = refPaint(paint);
DrawOp* op = new(alloc()) DrawTextOp(text, bytesCount, count,
x, y, positions, paint, totalAdvance, bounds);
addDrawOp(op);
returnDrawGlInfo::kStatusDone;
}
这里依然是op的创建,然后添加,目前并不会真正执行,我们会在DisplayList的回放过程中一起分析。
小节
上面我们分析了从view怎么调用到Graphic层面,来将view分解到Graphic的Op层面。不管是什么view,整个流程都是一致的,可能只在最后调用时是创建了bitmap的op还是text的op等等的区别。因为本文主要从Graphic的层面分析绘制问题,因为我们这里没有详细展开view层面全部完整的设置和调用过程。但是通过上面的方法,我想我们可以分析任何一个View的创建Op的过程。
其实我们发现view层面生成Op的过程还是很简单的,无非只是根据参数的不同进行一些设置,然后根据要绘制内容的不同调用canvas不同的方法,本质上,Framework层面的调用和我们在app中使用canvas也并无明显不同。
Canvas调用成功后,通过GLES20Canvas→JNI→DisplayListRenderer的顺序,调用了DisplayListRenderer的方法,DisplayListRenderer一般只是直接生成对应Op并保存,等待后面一起调用OpenGLRenderer的方法真正执行。需要指出的是,对应state类的Op,并不会等待,而是在DisplayListRenderer中被调用时就会直接执行。
对于绘制类的Op,在保存后,会等待执行,至于执行的时机,以及具体如何执行,我们将在后文HWUI讲解完成之后再次进行分析Op命令的真正执行过程