Android 基本功-View 的工作流程(四)
这是 View 工作流程的最后一部分了,依然从 ViewRootImpl 说起,这次是 performDraw() 方法,方法里代码比较多,我就挑重点的 draw() 方法继续看了,不过再继续之前,有个概念要先做个了解,
Surface
Surface 对象用来将图像渲染到设备屏幕上。(这里仅做简单了解,深了我也不会)还有个相关的接口 SurfaceHolder,通过它可以实现对 Surface 的配置,类似于 Config 操作。
绘制流程
在 View 绘制的时候,Canvas 对象是不可或缺的,但 Canvas 对象的来源又是哪里?
//为了找 Canvas 对象来源,先看下 ViewRootImpl 的 draw
private boolean draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
if (!surface.isValid()) {
return false;
}
//这个对象没太理解作用,虽然出现的频率比较高
final Rect dirty = mDirty;
boolean accessibilityFocusDirty = false;
//这中间也省略了部分代码
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
//重点看这块判断
//如果走 if,那将用 GPU 进行渲染(也就是硬件渲染),
//简单看了下会涉及到 native 层,这里做大致了解
if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this, callback);
} else {
// else 这里分两种情况,一种是因为硬件渲染条件不足,所以在这里做了初始化后重新执行 View 的工作流程去了
//另外一种就是用软件渲染,也就是 CPU 渲染
if (mAttachInfo.mThreadedRenderer != null &&
!mAttachInfo.mThreadedRenderer.isEnabled() &&
mAttachInfo.mThreadedRenderer.isRequested() &&
mSurface.isValid()) {
mAttachInfo.mThreadedRenderer.initializeIfNeeded(
mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);
mFullRedrawNeeded = true;
//这里开始重走 View 的工作流程
scheduleTraversals();
return false;
}
//这里去进行软件渲染
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
}
}
//ViewRootImpl
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
//省略了一些代码,挑重点的看
//这个 Canvas 就是我们平常接触到的 Canvas
final Canvas canvas;
try {
//在这里通过 Surface 对象赋值,
canvas = mSurface.lockCanvas(dirty);
canvas.setDensity(mDensity);
}
try {
dirty.setEmpty();
try {
//mView 很熟悉了,就是 DecorView,调了它的 draw 方法,并传入 canvas 对象
//这样看,这个 canvas 可能就是我们平常接触的那个
mView.draw(canvas);
}
} finally {
try {
//这个方法和前面的 lockCanvas 是匹配的
surface.unlockCanvasAndPost(canvas);
}
}
return true;
}
官方文档对 lockCanvas() 和 unlockCanvasAndPost() 的说明是为了资源不被同时占用,就要做一个锁的操作,这类似线程共享的资源。
往下再继续看 View 的 draw() 方法,就会发现入参的 canvas 对象正是平常我们接触到的那个 canvas,因此它的来源就是通过 Surface 对象创建的,可见绘制的背后是 Surface 对象以及一些 native 方法在主导。解决了 Canvas 对象来源问题,再继续看绘制过程的剩余部分,
//View 的 draw 方法
public void draw(Canvas canvas) {
/*
* 绘制需要有个合理的步骤
* 1.画背景
* 2.如果需要,就暂存画布准备进行渐变处理
* 3.画 View 的内容
* 4.画子 View
* 5.如果需要,再画渐变的边界,并恢复画布
* 6.画一些附加件,例如滚动条
*/
//源码里的注释是这样的步骤,所以 draw 方法的整体思路估计就是这样了
if (!dirtyOpaque) {
//不透明的话就画背景
drawBackground(canvas);
}
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
//这是不考虑第二,第五步的情况
if (!verticalEdges && !horizontalEdges) {
//不透明的话继续画 View 的内容
if (!dirtyOpaque) onDraw(canvas);
//画子 View 们
dispatchDraw(canvas);
//画一些附加件,也就是画前景
onDrawForeground(canvas);
//还有第七步,应该是画光标
drawDefaultFocusHighlight(canvas);
return;
}
//下面代码是考虑第二,第五步的情况
//简单看了下绘制渐变区域其实用到的,和我们平时绘制用到的差不多,也是涉及到区域,旋转,缩放,平移,画笔,矩阵等这些东西
//除了多出的第二,第五步骤,其他和上面一样,代码就不放了。
}
Canvas
这里简单说下 Canvas 的 save 和 restore 相关的概念。画布有个图层的概念,如果不想后续的操作影响到当前图层的效果,就要保存图层,然后在新的图层上画,但是图层与图层之间是叠加的,所以看起来就像在一层上操作,这个概念可以参考下 Photoshop 里图层的概念。restore 的话就是恢复某图层状态(我在想这么一来原先的操作效果不就没了么,这块还有待实践验证)。除了不同图层,画布还有针对状态的一个保存恢复操作,这个也有待实践验证。建议大家可以再看下这个 Canvas类的最全面详解 - 自定义View应用系列 说不定更好理解,之前也看过这个作者写的关于事件分发的分析,也很到位。这里他也分析了 View 的工作流程,值得推荐。
绘制的通用步骤
再说回来,依次看下通用的绘制步骤,
//绘制背景
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
//这里还是尝试看看能否用硬件渲染,如果可以,下面的 draw 就不会执行了
if (canvas.isHardwareAccelerated() && mAttachInfo != null && mAttachInfo.mThreadedRenderer != null) {
mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);
final RenderNode renderNode = mBackgroundRenderNode;
if (renderNode != null && renderNode.isValid()) {
setBackgroundRenderNodeProperties(renderNode);
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
return;
}
}
final int scrollX = mScrollX;
final int scrollY = mScrollY;
//不考虑滚动值的话接下去就是 Drawable 去执行绘制的操作了
//Drawable 是个抽象类,子类有 BitmapDrawable,ColorDrawable, NinePatchDrawable 等等
//对应到的也就是我们常用的一些资源的绘制转换,例如 NinePatchDrawable 应该就是与 .9 图对应
//这些子类都有各自的 draw 方法实现,最终才能展示到屏幕上
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
View 的 onDraw() 方法是空实现,去看看 DecorView 有没有重写该方法,
//DecorView 的 onDraw
public void onDraw(Canvas c) {
super.onDraw(c);
//这个东西简单看了下, BackgroundFallback 类的解释是专门给 DecorView 绘制 fallback background 的,我理解的是就像是我们弹起一个对话框,周围暗一点的透明层,那个估计就叫 fallback background.
mBackgroundFallback.draw(this, mContentRoot, c, mWindow.mContentParent,
mStatusColorViewState.view, mNavigationColorViewState.view);
}
照这么看,DecorView 就画了个 fallback 背景,FrameLayout 也没做啥相关的。我想,容器类控件估计也不会做什么,更多的是具体控件会做一些相关绘制。
//步骤 dispatchDraw 的实现要追溯到 DecorView 的父类的父类 ViewGroup
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
//mChildren 明确说了是在这个 ViewGroup 下的子 View
final View[] children = mChildren;
//接下去的一段代码是和动画相关的,这里省略
final ArrayList<View> preorderedList = usingRenderNodeProperties ? null : buildOrderedChildList();
//对子View 的绘制分发肯定会涉及到遍历,所以我的思路是找循环
for (int i = 0; i < childrenCount; i++) {
//这里面这个循环好像也是和动画相关的,不做深入整理了,不过其核心部分
//和普通绘制调的方法一样
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE || transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
//这里会去找到具体的子View,这个方法的入参有 3 个,前面 2 个,一个是列表,一个是数组
//列表表示用硬件渲染,数组就是软件渲染了,
//方法的逻辑其实就是如果列表存在就到列表里找,不然就到数组里找
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
//可见只有 Visible 的 view 才会绘制
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
//这个 drawChild 我是通过网页看源码找的,Android Studio 貌似找不到
//里面还是有点工作的,大致就是是否能用硬件渲染,再进行画布的实际绘制操作还有保存恢复等
more |= drawChild(canvas, child, drawingTime);
}
}
//后面又做了一些断后处理,这里省略
}
最后的绘制前景过程和绘制背景差不多,最终也会落到 Drawable 的绘制。
以上就是整个的绘制流程了,我们实际项目中可能接触的比较多的在于偏视觉展示类的自定义 View,或者动画,当然更多的是对一些绘制工具的掌握,对动画本质(数学函数等)的理解,还有绘制上对性能的考究。