Android基础之View的绘制原理
1.测量
简单流程就是:
- 确定view是树结构
- 递归遍历每个子节点,即子view,进行测量。
下面具体说一下:
首先view是树结构。
也就是说子view是父view的孩子节点。
根节点是就是DecorView。
了解树的都知道,树的遍历都是递归遍历。
那么测量view的过程,其实就是遍历树的过程。
测量什么呢,怎么做一个记录呢。
就有了MesureSpec。是一个封装的int类型。
高2位表示mode,其余表示size。
mode即类型有哪些呢。
通常我们在定义一个view的宽高时有三种写法。
一是直接写多少多少dp
一个是match_parent
一个是wrap_content
所以mode也有三种,分别是:
EXACTLY:对应固定数值的写法,有确切的size。
AT_MOST:父view给定一个size,子view不超过这个size即可。
UNSPECIFIED:似乎没有用到。
所以不同类型的view它的测量方式是不同的。
比如LinearLayout和FrameLayout具体的测量方式是不同的。
测量的逻辑(可以先不要看代码,看一下具体逻辑,思考一下怎么实现)
-
测量一个view需要知道父view的MeasureSpec。因为需要父view的mode和size来确定子view的mode和size
-
根节点没有父view,那么MeasureSpec从哪里获取
最外层的根节点DecorView的MeasureSpec只由自己的LayoutParams决定
如果是match_parent和固定数值对应的就是EXACTLY
如果是wrap_content对应的就是AT_MOST
对应的方法是
private static int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { //如果是MATCH_PARENT,那么就是EXACTLY case ViewGroup.LayoutParams.MATCH_PARENT: // Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; //如果是WRAP_CONTENT,就是AT_MOST case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: //如果是固定的值,也是EXACTLY // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; }
- 从根节点开始,遍历子view
整体流程是其实就是层次遍历。先计算子view的count,然后for循环,获取每个子view,计算其宽高的MeasureSpce,然后子view也是view吧。再去递归调用view的measure方法实现递归调用。
这里子view其实分为view和ViewGroup。
view的话后面不要递归遍历,直接计算就行了。
如果是ViewGroup还是要层次遍历,确保ViewGroup的子view都被调用到。
FrameLayout对应的 onMeasure 方法如下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (mMeasureAllChildren || child.getVisibility() != GONE) { //分析1 : 遍历所有子控件,测量每个子控件的大小 //参数1:View控件 //参数2:宽MeasureSpec //参数3:父容器在宽度上已经用了多少了,因为FrameLayout的规则是:前面已经放置的View并不会影响后面放置View的宽高,是直接覆盖到上一个View上的.所以这里传0 //参数4:高MeasureSpec //参数5:父容器在高度上已经用了多少了 measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); } } ...... //分析2 : 测量完所有的子控件的大小之后,才知道自己的大小 这很符合FrameLayout的规则嘛 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)); ...... }
ViewGroup类中 measureChildWithMargins 方法实现:
/** * Ask one of the children of this view to measure itself, taking into * account both the MeasureSpec requirements for this view and its padding * and margins. The child must have MarginLayoutParams The heavy lifting is * done in getChildMeasureSpec. * * @param child The child to measure * @param parentWidthMeasureSpec The width requirements for this view * @param widthUsed Extra space that has been used up by the parent * horizontally (possibly by other children of the parent) * @param parentHeightMeasureSpec The height requirements for this view * @param heightUsed Extra space that has been used up by the parent * vertically (possibly by other children of the parent) */ protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
View类中的setMeasuredDimension方法实现:
/** * <p>This method must be called by {@link #onMeasure(int, int)} to store the * measured width and measured height. Failing to do so will trigger an * exception at measurement time.</p> * * @param measuredWidth The measured width of this view. May be a complex * bit mask as defined by {@link #MEASURED_SIZE_MASK} and * {@link #MEASURED_STATE_TOO_SMALL}. * @param measuredHeight The measured height of this view. May be a complex * bit mask as defined by {@link #MEASURED_SIZE_MASK} and * {@link #MEASURED_STATE_TOO_SMALL}. */ protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int opticalWidth = insets.left + insets.right; int opticalHeight = insets.top + insets.bottom; measuredWidth += optical ? opticalWidth : -opticalWidth; measuredHeight += optical ? opticalHeight : -opticalHeight; } setMeasuredDimensionRaw(measuredWidth, measuredHeight); }
4.具体计算子view的宽高。
就是根据父view的MesureMode和子view写的是固定值还是wrap_content或者match_parent来确定子view的mode和size。
具体方法是:
//这里来自ViewGroup的getChildMeasureSpec方法,无删减 public static int getChildMeasureSpec(int spec, int padding, int childDimension) { //根据父容器的MeasureSpec获取父容器的SpecMode和SpecSize int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); //剩下的size int size = Math.max(0, specSize - padding); //最终的size和mode int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us //父容器有一个确定的大小 case MeasureSpec.EXACTLY: if (childDimension >= 0) { //子控件也是确定的大小,那么最终的大小就是子控件设置的大小,SpecMode为EXACTLY resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. // 子控件想要占满剩余的空间,那么就给它吧. resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. //子控件想要自己定义大小,但是不能超过剩余空间 size resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
MeasureSpec
一个view的MeasureMode怎么确定
- 如果该view的宽高是固定值,那么其mode是EXACTLY
- 如果view的宽高是MATCH_PARENT,那就继承父view的mode。
即父view的mode是EXCETLY,该view就是EXACTLY;
父View是AT_MOST,该view就是AT_MOST;
父View是UNSPECIFIED,该view也是 UNSPECIFIED。 - 如果view的宽高是WRAP_CONTENT。那就不论父view是什么mode,该view都是AT_MOST除了UNSPECIFIED。
Measure流程图
2.布局
思考一个问题,影响布局有哪些因素:
- 首先能想到的是上个步骤中测量的宽高
- 其次就是Gravity,不同的ViewGroup不一样。比如RelativeLayout和LinearLayout差别就很大。需要具体分析。layout的时候也要考虑。
- Gravity.BOTTOM
- Gravity.TOP
- Gravity.CENTER_VERTICAL
- Gravity.CENTER_HORIZONTAL
- Gravity.RIGHT
- Gravity.LEFT
- 就是layout时自定义的几个参数left,top,right,bottom
布局首先是确定四个参数:
left,top,right,bottom
什么意思呢:
这四个参数位置都是相对于父容器而言的
那么具体是怎么操作呢:
- 首先是从根节点也就是DecorView开始。
根节点的四个参数如何确定,很简单,如下所示。
//这里的host其实是根视图(DecorView) //参数:left,top,right,bottom 这些位置都是相对于父容器而言的 host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
- 根节点之后就开始层次遍历。
这里子view其实分为view和ViewGroup。view的话后面不要递归遍历,直接计算就行了。如果是ViewGroup还是要层次遍历,确保ViewGroup的子view都被调用到。
拿到当前view的子view的count,for循环拿到每个子view。根据子view的测量出来的宽高,以及Gravity等参数。去计算子view的四个参数。
就是该子view相对于父view的布局是什么。
然后再去递归调用子view的布局方法即layout方法。实现整个递归遍历。
3.绘制
绘制又分为两种:
如果开启并支持硬件绘制加速(从 Android 4.X 开始谷歌已经默认开启硬件加速),则走 GPU 硬件绘制的流程,否则走 CPU 软件绘制的流程。
先看一下官方给的注释:
/* 注意了这是官方给的注释,谷歌工程师还真是贴心,把draw步骤写的详详细细,给力,点赞 * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */
翻译一下就是:
- 绘制背景
- 绘制控件自己本身的内容
- 绘制子控件
- 绘制装饰(比如滚动条)和前景
整体流程比较简单:
还是从ViewRootImpl的performTraversals方法开始分析
private void performTraversals() { //开始绘画流程 performDraw(); } private void performDraw() { ...... draw(fullRedrawNeeded); ...... } private void draw(boolean fullRedrawNeeded){ ..... drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty); ..... } private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) { ...... mView.draw(canvas); ...... }
随着方法的调用深入,发现来到了View的draw方法
public void draw(Canvas canvas) { ..... /* 注意了这是官方给的注释,谷歌工程师还真是贴心,把draw步骤写的详详细细,给力,点赞 * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */ // Step 1, draw the background, if needed //1. 绘制背景 if (!dirtyOpaque) { drawBackground(canvas); } // skip step 2 & 5 if possible (common case) final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content //3. 绘制自己的内容 if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children //4. 绘制子控件 如果是View的话这个方法是空实现,如果是ViewGroup则绘制子控件 dispatchDraw(canvas); drawAutofilledHighlight(canvas); // Overlay is part of the content and draws beneath Foreground if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } // Step 6, draw decorations (foreground, scrollbars) //6. 绘制装饰和前景 onDrawForeground(canvas); // Step 7, draw the default focus highlight //7. 绘制默认焦点高亮显示 drawDefaultFocusHighlight(canvas); if (debugDraw()) { debugDrawFocus(canvas); } // we're done... return; } ..... }
两个比较容易混淆的方法
invalidate方法:
如果这时候视图大小没有发生变化,不会调用layout放置过程
requestLayout 方法:
当布局发生变化的时候,比如方向、尺寸变化;
比如在某些情况下需要重新测量大小,需要手动调用这个方法,而调用完这个方法后,就会去触发它的mesure和layout过程,但是不会调用draw方法。
总结
绘制完毕之后干嘛呢。会交个RenderThread处理,然后交给SurfaceFlinger,最后显示到屏幕上。
这部分后面继续分析。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)