概述
整个View树的绘图流程是在ViewRoot.java类的performTraversals()函数展开的,该函数做的执行过程可简单概况为:
- 判断是否需要重新计算视图大小(measure)
- 判断是否重新需要安置视图的位置(layout)
- 判断是否需要重绘(draw)
其整个流程图如下:
图片来自:Android 开源项目源码解析 公共技术点中的 View 绘制流程
在Android中View的整个生命周期,调用invalidate和requestLayout会触发一系列的方法,如图所示
图片来自:Android 开源项目源码解析 公共技术点中的 View 绘制流程
- 当开发者调用requestLayout方法时,只会触发measure和layout过程
- 当开发者调用invalidate方法时,会触发draw过程
Measure
- 为整个View树计算实际大小,每个View的实际大小由父控件和其本身共同决定
- measure方法调用onMeasure方法,onMeasure方法里通过setMeasuredDimension(注意padding和margin)设置View的大小
- ViewGroup子类需要重写onMeasure去遍历测量其子View的大小
- measure方法是final类型,不能被重写,需要重写的是onMeasure方法
- 整个测量过程就是对View树的递归
- 一个View一旦测量完成,即可通过getMeasuredWidth() 和 getMeasuredHeight()获得其宽度和高度
- 自定义的ViewGroup只需实现measure和layout过程
MeasureSpec
一个MeasureSpec对象由size和mode组成,MeasureSpec类通过将其封装在一个int值中以减少对象的分配。其模式有以下三种,都为int型
- UNSPECIFIED
父视图不对子视图产生任何约束,如ListView,ScrollView
- EXACTLY
父视图为子视图指定一个确切的尺寸,子视图以这个确切的值作为大小,比如match_parent或具体值20dp
- AT_MOST
父视图为子视图指定一个最大尺寸,子视图必须在这个尺寸大小内,比如wrap_content
相关函数
- makeMeasureSpec(int size, int mode) 根据size值和mode值封装成MeasureSpec
- getSize(int measureSpec) 根据MeasureSpec值返回size值
- getMode(int measureSpec) 根据MeasureSpec值返回mode值
- 以上三个函数内部实现是用位运算实现,mode使用int的最高2位,size使用其余的30位,内部关键部分代码
public static class MeasureSpec { private static final int MODE_SHIFT = 30; //移位位数为30 //int类型占32位,向右移位30位,该属性表示掩码值,用来与size和mode进行"&"运算,获取对应值。 private static final int MODE_MASK = 0x3 << MODE_SHIFT; //向右移位30位,其值为00 + (30位0) , 即 0x0000(16进制表示) public static final int UNSPECIFIED = 0 << MODE_SHIFT; //向右移位30位,其值为01 + (30位0) , 即0x1000(16进制表示) public static final int EXACTLY = 1 << MODE_SHIFT; //向右移位30位,其值为02 + (30位0) , 即0x2000(16进制表示) public static final int AT_MOST = 2 << MODE_SHIFT; //创建一个整形值,其高两位代表mode类型,其余30位代表长或宽的实际值。可以是WRAP_CONTENT、MATCH_PARENT或具体大小exactly size public static int makeMeasureSpec(int size, int mode) { return size + mode; } //获取模式,与运算 public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } //获取长或宽的实际值,与运算 public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } }
Layout
- 确定子视图在相当于父视图的位置(注意margin和padding)
- ViewGroup的onLayout是抽象的,其子类必须实现
- View的onLayout是空实现
- 此时测量已完成,可通过getMeasuredWidth() 和 getMeasuredHeight()获得其宽度和高度
- 不要在onDraw和onLayout中创建对象,因为这两个方法会被频繁调用
到这里看一张总结性的图
图片来自:Android 开源项目源码解析 公共技术点中的 View 绘制流程
LayoutParams
- 它是一个ViewGroup的内部类
- ViewGroup 的子类有其对应的 ViewGroup.LayoutParams 的子类。比如 RelativeLayout 拥有的 ViewGroup.LayoutParams 的子类 RelativeLayoutParams
- getLayoutParams() 方法得到是其所在父视图类型的 LayoutParams,比如 View 的父控件为 RelativeLayout,那么得到的 LayoutParams 类型为 RelativeLayoutParams
- 有时我们需要使用 view.getLayoutParams() 方法获取一个视图 LayoutParams ,然后进行强转,但由于不知道其具体类型,可能会导致强转错误
- 自定义View的margin等属性在LayoutParams 指定
Draw
- 自定义View绘制过程需要重写onDraw方法
- 自定义ViewGroup在dispatchDraw中发起对子视图的绘制,不应该对该函数重写
- onDraw中调用相关绘制函数进行绘制
invalidate
- 请求重绘View
- 视图大小没有变化就不会调用layout过程
- 只重新绘制那些调用了invalidate()方法的 View
- 如果要在UI线程中重绘请使用postInvalidate()方法
requestLayout
- 当布局变化的时候,比如方向变化,尺寸的变化,会调用该方法
- 它会触发measure和layout过程,但不会进行 draw过程
最佳实践
以上都是理论知识,也差不多是对多篇文章的总结性内容。下面开始实现一个自定义View和ViewGroup
自定义View
其实自定义View的大部分逻辑都是在onDraw上,onLayout基本上无需重新,onMeasure需要实现测量逻辑。
下面是一个简单的毫无任何作用的自定义View,其唯一目的就是演示onDraw和onMeasure
package cn.edu.zafu.sourcedemo;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Rect;import android.util.AttributeSet;import android.view.View;/** * Created by lizhangqu on 2015/5/3. */public class CustomView extends View { private Paint paint=null; private Rect rect=null; private int bgColor=Color.parseColor("#673AB7");//写死背景色,实际是自定义属性 private int minContentWidth=50;//最小内容宽度,不包含内边距 private int minContentHeight=50;//最小内容高度,不包含内边距 public CustomView(Context context) { this(context, null); } public CustomView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CustomView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } /*初始化*/ private void init() { paint=new Paint(); paint.setAntiAlias(true); paint.setDither(true); paint.setColor(bgColor); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //其实所有逻辑可以简单调用resolveSize函数进行测量,这里自己实现一遍,理清思路 //获得宽度和高度的mode和size int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); //最终的宽高存在这两个变量中 int width; int height; if (widthMode == MeasureSpec.EXACTLY) { // 父视图指定了大小 width = widthSize; } else { //父视图指定必须在这个大小内 //注意内边距,再加上自身需要的宽度 width=getPaddingLeft()+getPaddingRight()+minContentWidth; if (widthMode == MeasureSpec.AT_MOST) { //如果是AT_MOST,必须在父控件指定的范围内,取width和widthSize中小的那个 width = Math.min(width, widthSize); } } if (heightMode == MeasureSpec.EXACTLY) { // 父视图指定了大小 height = widthSize; } else { //父视图指定必须在这个大小内 //注意内边距,再加上自身需要的高度 height =getPaddingTop()+getPaddingBottom()+minContentHeight; if (heightMode == MeasureSpec.AT_MOST) { //如果是AT_MOST,必须在父控件指定的范围内,取width和widthSize中小的那个 height = Math.min(height, heightSize); } } setMeasuredDimension(width, height); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); rect=new Rect(getPaddingLeft(),getPaddingTop(),getMeasuredWidth()-getPaddingRight(),getMeasuredHeight()-getPaddingBottom());//绘制的时候注意内边距 canvas.drawRect(rect,paint); }}
自定义Viewgroup
实现一个纵向排布子View的ViewGroup,效果如图所示,见代码,解释看注释
package cn.edu.zafu.sourcedemo;import android.content.Context;import android.util.AttributeSet;import android.view.View;import android.view.ViewGroup;/** * Created by lizhangqu on 2015/5/3. */public class CustomViewGroup extends ViewGroup { public CustomViewGroup(Context context) { this(context, null); } public CustomViewGroup(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CustomViewGroup(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } //重写onLayout抽象方法 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int count = getChildCount(); MyLayoutParams lp = null; for (int i = 0; i < count; i++) { View child = getChildAt(i); //获得当前View lp = (MyLayoutParams) child.getLayoutParams(); //获得LayoutParams,强制转换为MyLayoutParams child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y + child.getMeasuredHeight()); //调用当前View的layout方法进行布局 } } //重写onMeasure实现测量逻辑 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = 0; int lastWidth = 0; int height = getPaddingTop(); final int count = getChildCount(); //获得子View个数 for (int i = 0; i < count; i++) { View child = getChildAt(i); //获得当前子View measureChild(child, widthMeasureSpec, heightMeasureSpec); //测量子View,必须调用 MyLayoutParams lp = (MyLayoutParams) child.getLayoutParams(); //获得LayoutParams width = Math.max(width, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); //比较当前View与之前的View宽度,取大者,注意这个宽度包含了margin lp.x = getPaddingLeft() + lp.leftMargin; //设置当前View的x左边 lp.y = height + lp.topMargin; //设置当前View的y左边 height = height + lp.topMargin + child.getMeasuredHeight() + lp.bottomMargin; //累加高度 } width=width+getPaddingLeft() + getPaddingRight(); //加上左右内边距 height = height + getPaddingBottom(); //加上下边界 setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec)); //设置宽高,resolveSize方法会根据尺寸大小和MeasureSpec计算最佳大小 } //重写生成LayoutParams的三个方法 @Override public MyLayoutParams generateLayoutParams(AttributeSet attrs) { return new MyLayoutParams(getContext(), attrs); } //重写生成LayoutParams的三个方法 @Override protected MyLayoutParams generateDefaultLayoutParams() { return new MyLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } //重写生成LayoutParams的三个方法 @Override protected MyLayoutParams generateLayoutParams(LayoutParams p) { return new MyLayoutParams(p.width, p.height); } //继承MarginLayoutParams实现自己的LayoutParams,x,y代表控件的左边和上边左边 public static class MyLayoutParams extends MarginLayoutParams { public int x;//左 public int y;//上 public MyLayoutParams(Context context, AttributeSet attrs) { super(context, attrs); } public MyLayoutParams(int w, int h) { super(w, h); } }}
布局界面如下
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <cn.edu.zafu.sourcedemo.CustomViewGroup android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="50dp" android:padding="20dp" android:background="#ffff00" > <Button android:layout_width="20dp" android:layout_height="20dp" android:layout_marginTop="5dp" android:layout_marginBottom="5dp" android:layout_marginLeft="5dp" android:layout_marginRight="5dp" android:background="#ff0000" /> <Button android:layout_width="24dp" android:layout_height="15dp" android:background="#00ff00" /> <Button android:layout_width="10dp" android:layout_height="30dp" android:background="#0000ff" /> </cn.edu.zafu.sourcedemo.CustomViewGroup></LinearLayout>
参考文章
- Android 开源项目源码解析 公共技术点中的 View 绘制流程 前面理论部分基本从这篇文章中提取来