本章从Graphic的角度来分析Android系统中一个基础的view是如何被绘制出来的(只讨论硬件加速打开的场景):
下面我们以TextView这个类的onDraw函数为例看下,这个类是很多view的父类。
- protected void onDraw(Canvas canvas) {
- // 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 int scrollX = mScrollX;
- final int scrollY = mScrollY;
- final int right = mRight;
- final int left = mLeft;
- final int bottom = mBottom;
- final int top = mTop;
- final boolean isLayoutRtl = isLayoutRtl();
- final int offset = getHorizontalOffsetForDrawables();
- final int leftOffset = isLayoutRtl ? 0 : offset;
- final int rightOffset = isLayoutRtl ? offset : 0 ;
- final Drawables dr = mDrawables;
- if (dr != null) {
- 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.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 int dx = (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", "<GATE-M>LCDSTR:" + mText + "/LCDSTR</GATE-M>");
- //NAGSM_ANDROID_HQ_FRWK Gate -
- canvas.restore();
- if (mTextStrikeThroughEnabled) {
- drawTextStrikethrough(canvas);
- }
- }
这个函数很长,所以我们还是按照老办法,分段来分析这个函数。
1 位置的计算
- final int compoundPaddingLeft = getCompoundPaddingLeft();
- final int compoundPaddingTop = getCompoundPaddingTop();
- final int compoundPaddingRight = getCompoundPaddingRight();
- final int compoundPaddingBottom = getCompoundPaddingBottom();
- final int scrollX = mScrollX;
- final int scrollY = mScrollY;
- final int right = mRight;
- final int left = mLeft;
- final int bottom = mBottom;
- final int top = mTop;
- final boolean isLayoutRtl = isLayoutRtl();
- final int offset = getHorizontalOffsetForDrawables();
- final int leftOffset = isLayoutRtl ? 0 : offset;
- final int rightOffset = isLayoutRtl ? offset : 0 ;
首先是一系列位置的计算,这些在app编码设计时都可以修改赋值,应该都是一些屏幕上绝对坐标,后面的绘制会使用到这些坐标。
2 Drawable的绘制
- 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.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函数,
- public int save() {
- return nSave(mRenderer, Canvas.CLIP_SAVE_FLAG | Canvas.MATRIX_SAVE_FLAG);
- }
- static jint android_view_GLES20Canvas_save(JNIEnv* env, jobject clazz, OpenGLRenderer* renderer,
- jint flags) {
- return renderer->save(flags);
- }
我们看到,最终实现还是通过renderer来实现,基本上Canvas的命令都是这么一个过程。
- int DisplayListRenderer::save(int flags) {
- addStateOp(new (alloc()) SaveOp(flags));
- return OpenGLRenderer::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是执行时是做了什么操作。
- virtual void applyState(OpenGLRenderer& renderer, int saveCount) const {
- renderer.save(mFlags);
- }
这里无非就是执行了OpenGLRenderer的save方法,我们可以这样认为,OpenGLRenderer就可以实现renderer的所有功能,一旦我们调用OpenGLRenderer类的某个函数,那就意味着这条命令会被立刻执行(而不是先保存起来)。
2.1.2 save命令的执行
我们来继续看下save命令在OpenGLRenderer里面的执行(顺便提一句,从DisplayListRenderer::save代码我们看到,其实保存完Op后,save命令其实立刻被执行了。事实上,应该是所有的状态类Op在保存完之后都会立刻执行,而绘制类Op并不会执行,猜测原因应该是状态类Op其实是不需要GPU运算的,因此可以立刻执行):
- int OpenGLRenderer::save(int flags) {
- return saveSnapshot(flags);
- }
- int OpenGLRenderer::saveSnapshot(int flags) {
- mSnapshot = new Snapshot(mSnapshot, flags);
- return mSaveCount++;
- }
我们发现,这里其实就是将当前的状态保存在了一个快照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的函数:
- void DisplayListRenderer::translate(float dx, float dy) {
- mHasTranslate = true;
- mTranslateX += dx;
- mTranslateY += dy;
- insertRestoreToCount();
- OpenGLRenderer::translate(dx, dy);
- }
- void DisplayListRenderer::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过程:
- public void draw(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(new BitmapShader(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);
- }
- }
由于我们重点分析Graphic层面的逻辑,因此这里和上层有关的一些概念我们不再讲解,Shader有关的知识可以参考这篇博客:http://blog.csdn.net/ldj299/article/details/6166071,BitmapShader可以参考这篇:http://blog.csdn.net/sjf0115/article/details/7267532。
我们这里重点分析下canvas的两个draw函数:drawBitmap和drawRect。依然是直接看DisplayListRenderer的对应函数:
- status_t DisplayListRenderer::drawBitmap(SkBitmap* bitmap, float left, float top, SkPaint* paint) {
- bitmap = refBitmap(bitmap);
- paint = refPaint(paint);
- addDrawOp(new (alloc()) DrawBitmapOp(bitmap, left, top, paint));
- return DrawGlInfo::kStatusDone;
- }
这里添加了一个新的DrawBitmapOp,它在reply时会调用OpenGlRenderer的drawBitmap函数。绘制的执行过程,我们会在DisplayList的回放过程中一起分析。
3 layout.draw的绘制
onDraw函数的中段很长一部分是一些参数的设置,我们这里不详细研究上层的逻辑,我们接下来重点看下一个绘制函数,layout.draw。
- 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);
- }
我们直接看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(const char* text, int bytesCount, int count,
- float x, float y, const float* positions, SkPaint* paint,
- float totalAdvance, const Rect& bounds, DrawOpMode drawOpMode) {
- text = refText(text, bytesCount);
- positions = refBuffer<float>(positions, count * 2);
- paint = refPaint(paint);
- DrawOp* op = new (alloc()) DrawTextOp(text, bytesCount, count,
- x, y, positions, paint, totalAdvance, bounds);
- addDrawOp(op);
- return DrawGlInfo::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命令的真正执行过程