一、ViewRoot与DecorView
1、ViewRoot
- ViewRoot对应于ViewRootImpl类
- 是链接WindowManager和DecorView的纽带
- View的三大流程均是通过ViewRoot来完成的
- 在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将其与DecorView建立关联
- View的绘制流程是从ViewRoot的performTraversals方法开始的
2、DecorView
- 顶级View(是一个ViewGroup)
- 是一个FrameLayout
- 一般情况下内部包含一个竖直方向上的LinearLayout,有上下两个部分
- 上面是标题栏
- 下面是内容栏(即Activity中的setContentView设置的布局文件)
- 得到Content
ViewGroup content = findViewById(ViewGroup)findViewById(android.id.content);
- 得到设置的View
content.getChildAt(0);
- 得到Content
二、理解MeasurSpec
在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽高
1、MeasureSpec
- Measure代表一个32位的int值。打包两个属性避免过多的对象内存分配,提供了打包和解打包方法
- 高2位表示SpecMode(测量模式)
- 低30位表示SpecSize(某种测量模式下的规格大小)
- SpecMode有三类
- UNSPECIFIED:父容器不对View有任何限制,要多大给多大。一般用于系统内部测量过程,不需要过多关注
- EXACTLY:父容器已经检测出View所需要的精确的大小,此时View的最终大小就是SpecSize所指定的值
- 对应于LayoutParamas中的match_parent和具体的数值这两种模式
- AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值。
- 对应于LayoutParamas中的wrap_content
2、MeasureSpec和LayoutParams的对应关系
- LayoutParamas和父容器一起决定View的MeasureSpec(源码中还体现了与padding和margin有关)
- DecorView:窗口尺寸 + 自身的LayoutParamas决定
- LayoutParamas.MATCH_PARENT:精确模式,大小就是窗口的大小
- LayoutParamas.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小
- 固定大小:精确模式,大小为LayoutParamas中指定的大小
- 普通的View:父容器的MeasureSpec + 自身的LayoutParamas决定
- 当View采用固定宽/高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且大小遵循LayoutParams的大小。
- 当View的宽/高是match_parent时,如果父容器的模式是精确模式,那么View也是最大模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么View也是最大模式并且不会超过父容器的剩余空间。
- 当View的宽和高是wrap_content时,不管父容器的模式是精准还是最大化,View也是最大模式并且不会超过父容器的剩余空间。
- DecorView:窗口尺寸 + 自身的LayoutParamas决定
三、View的工作流程
1、measure过程
- 确定View的测量宽/高(并不一定是最终宽高)
(1)对View来说
由measure方法完成。
- measure方法是一个final类型的方法,子类不能重写。
- 方法体中会调用onMeasure方法
//onMeasure protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //此方法中的最小宽高为背景大小和android:minWidth/Height属性二者的最大值,默认为0 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), HeightMeasureSpec)); }
//getDefaultSize public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; }
- 直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小。
- 否则wrap_content属性与match_parent效果一致
- 给setMeasuredDimension方法的参数设置一个默认的宽高来解决wrap_content的问题
- (2)对ViewGroup来说
- ViewGroup除了完成自己的measure过程外,还会遍历所有子元素的measure方法
- 在measureChildren方法中有体现
- measureChildren方法体内部:创建View[] children,然后循环遍历,调用measureChild方法
- measureChild方法体内部:取出子元素的LayoutParamas,然后通过getChildMeasureSpec来创建子元素的MeasureSpec,最后将MeasureSpec直接传递给View的measure方法测量
- measureChildren方法体内部:创建View[] children,然后循环遍历,调用measureChild方法
- ViewGroup是一个抽象类,所以并没有重写View的onMeasure方法
- 没有定义其具体测量过程的原因是需要ViewGroup的各个子类依据其布局特性去具体实现
- 在measureChildren方法中有体现
- ViewGroup除了完成自己的measure过程外,还会遍历所有子元素的measure方法
- Measure完成之后,通过getMeasuredWidth/Height方法可以正确的获取到View的测量宽高
- 有的时候系统需要多次measure才能确定最终的测量宽高
- 所以最好在onLayout方法中去获取View的测量宽高或最终宽高
如何在Activity已启动的时候获取View的宽高?
法1:Activity/View#onWindowFocusChanged
- 该方法的含义是:View已经初始化完毕,宽高已准备好并可获取
- 该方法会被调用多次,当Activity的窗口得到焦点和失去焦点时均会被调用一次
//典型示例 public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }
法2:view.post(runnable)
- 通过post将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,view也初始化好了
//典型示例 protected void onStart() { super.onStart(); view.post(new Runnable(){ @Override public void run() { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }); }
- 通过post将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,view也初始化好了
- 法3:ViewTreeObserver
- 通过ViewTreeObserver的众多回调接口实现
- 比如onGlobalLayout:当View树的状态发生改变或者View树内部的View的可见性发生改变时,此方法被回调
- 通过ViewTreeObserver的众多回调接口实现
- 法4:view.measure(int widthMeasureSpec, int heightMeasureSpec)
- 通过手动对View进行measure来得到View的宽高,需要根据View的LayoutParamas来分情况处理
- match_parent:直接放弃。获取不到parent
- 具体数值:Measure.makeMeasureSpec()获取数值,然后view.measure()传入该数值绘制
- wrap_content:Measure.makeMeasureSpec()
- 通过手动对View进行measure来得到View的宽高,需要根据View的LayoutParamas来分情况处理
2、layout过程
- 在ViewGroup中,layout的作用是用来确定子元素的位置。
- 首先通过setFrame方法来设定View的四个顶点的位置
- 当ViewGroup的位置被确定后,方法体中onLayout会遍历所有的子元素并调用其layout方法确定View在父容器中的位置
- 在子元素的layout方法中再调用子元素的onlayout方法。如此递推。
与onMeasure方法类似,因为与具体的布局有关,onLayout方法同样在View和ViewGroup中没有具体实现(在其具体子类中有实现)
//LinearLayout的onLayout() protected void onLayout(boolean changed, int l, int t, int r, int b) { if (mOrientation == VERTICAL) { layoutVertical(1, t, r, b); } else { layoutHorizontial(1, t, r, b); } }
- View的测量宽高与最终宽高的区别:
- 如过在layout方法中没有修改则认为相等
- getWidth/Height与getMeasuredWidth/Height的返回值相等
- 测量宽高形成于measure过程,最终宽高形成于layout过程
3、draw过程
- 步骤:
- 绘制背景 background.draw(canvas)
- 绘制自己 onDraw
- 绘制children dispatchDraw (实现View绘制的传递)
- 绘制装饰 onDrawScrollBars
- setWillNotDraw(boolean willNotDraw):如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统将会进行相应的优化
- ViewGroup默认启用此标记位。当明确知道一个ViewGroup需要onDraw来绘制内容时,需要显示地关闭WILL_NOT_DRAW这个标记位
- View默认不启用此标记位
四、自定义View
1、分类
- (1)继承View重写onDraw方法
- 主要用于实现一些不规则的效果
- 重写onDraw方法,需要自己支持wrap_content,并且padding也需要自己处理
- 主要用于实现一些不规则的效果
- (2)继承ViewGroup派生特殊的layout
- 主要用于自定义布局
- 需要合适的处理ViewGroup的测量、布局,并处理子View的测量和布局
- 主要用于自定义布局
- (3)继承特定的View
- 扩展某种已有View的功能
- (4)继承特定的ViewGroup(如LinearLayout)
- 实现某种很像几种View组合在一起的效果
- 与方法2类似,但方法2更底层
2、构造方法的使用时机
(1)一个参数:在代码中直接new一个自定义View实例的时候调用
(2)两个参数:在xml布局文件中调用自定义 View的时候调用
(3)三个参数:在xml布局文件中调用自定义View,并且自定义View标签中还有自定义属性(主题中优先级最高的属性)时
(4)四个参数:一般三个就够用了
int defStyleRes:优先级次之的内置于View的style(只有在第三个参数defStyleAttr为0,或者主题中没有找到这个defStyleAttr属性的赋值时,才可以启用)
- 这个参数不再是Attr了,而是真正的style。其实这也是一种低级别的“默认主题”,即在主题未声明属性值时,我们可以主动的给一个style,使用这个构造函数定义出的View,其主题就是这个定义的defStyleRes(是一种写死的style,因此优先级被调低)
int defStyleAttr:主题中优先级最高的属性
属性赋值优先级 Xml定义 > xml的style定义 > defStyleAttr > defStyleRes> theme直接定义
参考博客地址:https://blog.csdn.net/zhao123h/article/details/52210732
示例:使用this连续调用
/** * 第一个构造函数 */ public MyCustomView(Context context) { this(context, null); } /** * 第二个构造函数 */ public MyCustomView(Context context, AttributeSet attrs) { this(context, attrs, 0); } /** * 第三个构造函数 */ public MyCustomView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // TODO:获取自定义属性 }
3、注意
- 直接继承View或ViewGroup的控件,要让View支持wrap_content
- 直接继承View的控件在draw方法中处理padding,直接继承ViewGroup的控件,在onMeasure和onLayout中处理padding和子元素的margin
- 尽量不要在View中使用Handler,View内部提供了post方法
- View中如果有线程或动画,需要及时停止
- View#onDetachedFromWindow:包含此View的Activity退出或者当前View被remove时调用
- View#onAttachedToWindow:包含此View的Activity启动时调用
- 记得处理滑动冲突
4、过程示例
- 重写三个构造方法并在其中完成初始化工作
- 参数数量分别为1、2、3
- 三个参数的构造方法用于调用自定义属性
- 重写onDraw方法
- 布局中通过完整的包名+类名使用
- 通过getPaddingLeft/Right/Top/Bottom获得padding并处理
- wrap_content需要指定一个默认宽高
5、添加自定义属性
- (1)在values目录下创建自定义属性的XML
- (2)在View的三个参数的构造方法中解析并作相应的处理
- (3)在布局文件中自定义属性
- 需在布局中添加schemas声明
xmlns:app=http://schemas.android.com/apk/res-auto
- 需在布局中添加schemas声明