先顺带提一下LayoutInflater:
layoutInflater.inflate(resourceId, root,attachToRoot):
resourceId传入要加载的xml布局;root如果不为空,就相当于给这个布局外部嵌套一层root父布局;attachToRoot在两个参数的时候默认为true,如果设为false,则会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效。
inflate 三个参数说明见: 三个案例带你看懂LayoutInflater中inflate方法两个参数和三个参数的区别__江南一点雨的博客-CSDN博客
在inflate方法中,采用了PULL解析方法来解析XML,首先会创建XmlPullParser对象并通过next()方法不断地遍历节点直到遍历完全部节点,根据解析出来的节点名来创建View对象(通过createViewFromTag这个api);
Q1:invalidate和postInvalidate方法区别:
都是去刷新view,其中invalidate只能在UI线程中使用(如果子线程操作可用handler处理),而postInvalidate可在非UI线程中使用。
绘制流程:
在onResume执行后,会创建一个ViewRootImpl然后将Decorview交给它进行管理,DecorView底下的view逻辑事件都是ViewRootImpl来管理;ViewRootImpl实现了ViewParent接口,包含了requestLayout方法(内部包含了两步,检查是否是UI线程和调用performTraversals方法),所以当View的ViewParent都是同一个,可以理解为这些View都在View链上,invalidate的时候会将整个Viewtree刷新一遍; View的绘制流程是从ViewRootImpl的performTraversals方法开始,依次调用measure。layout,draw,将整个Viewtree进行遍历执行这三个方法;
关于View执行onMeasure,onLayout的次数>
分析ViewRootImpl的源码,scheduleTraversales()内部会执行postCallBack触发mTraversalRunnable重新走一遍performTraversals(),第二次执行performTraversals()就会触发performDraw()。所以performTraversals()走了两次,那么肯定会走2次measure方法,但不一定走2次onMeasure(),读过View measure方法源码的都应知道measure方法做了2级测量优化:
- 1.如果flag不为forceLayout或者与上次测量规格(MeasureSpec)相比未改变,那么将不会进行重新测量(执行onMeasure方法),直接使用上次的测量值;
- 2.如果满足非强制测量的条件,即前后二次测量规格不一致,会先根据目前测量规格生成的key索引缓存数据,索引到就无需进行重新测量;如果targetSDK小于API 20则二级测量优化无效,依旧会重新测量,不会采用缓存测量值
第二次走performTraversals逻辑:
即view可见时再重绘一次,同理layout、draw走两次,但各种限制条件导致onLayout和onDraw不一定也走两次;
1.OnMeasure:测量view的宽/高
2.OnLayout:决定view的四个顶点位置,以及拿到View的实际宽/高
3.OnDraw:绘制view, 绘制内容的步骤: ①、绘制视图的背景(drawBackground);②、保存画布的图层(Layer);③、绘制View的内容(onDraw);④、绘制View子视图,如果没有就不用(dispatchDraw);⑤、还原图层(Layer);⑥、绘制滚动条。
OnMeasure:
其中最重要的参数MeasureSpec,是一个32位int型,高2位是specMode,低30位是specSize;
通过父类的measureSpec,由specSize和specMode共同组成的,specSize来获取子类的size;
三种specMode:
EXACTLY:精准大小,表示父视图希望子视图大小由specSize决定或者由开发人员自己设置任意某个数值;
AT_MOST:不超过父类;表示子视图最大只能小于specSize指定的数值,系统默认按照这个规则设置,开发人员也可以自己设置任意小于specSize数值;
UNSPECIFIED:可将试图按照自己意愿设置成任意大小;
内部是通过判断传入xml中的参数来产生specMode:
Match_Parent或者具体数值 ---> EXACTLY
WRAP_CONTENT ---> AT_MOST
onMeasure方法最后会在getDefaultSize中通过判断specMode来拿到具体size:
但是注意一点的是:AT_MOST它是和EXACTLY一样,都会返回父View规定的size;如果开发者不想在使用WRAP_CONTENT时展现出和MATCH_PARENT的样子,那么就可以在onMeasure中自己拿到specMode进行判断和设置,比如设一个具体的数值(如下);
注意:上述wrap_content表现为Match_parent只是在view或继承view的自定义view才表现这样,如果是TextView、Button或其他控件,内部已经进行过处理:
因此TextView等控件并不会像View直接展现为match_parent,而是持有一个suggestionWidth来限定宽高
在拿到实际要设置的size大小后,如下图调用setMeasuredDimension(width,height),这样一次measure过程就结束了;
常见用法:
当然上面只是view的onMeasure流程,如果存在一个父容器ViewGroup包含了多个子view,ViewGroup中定义了measureChildren()方法遍历测量包含的子view,每个子view的measure流程和上述view的流程相同:
onLayout:
layout()方法中接受了左上右下四个坐标位置,并用setFrame和onLayout()方法来确定view的位置;
然而View中onLayout是一个空方法,因为onLayout()是为了确定view在布局中所在的位置,所以应该由父容器来决定;所以只有ViewGroup像LinearLayout等中才实现了onLayout方法用来约束子view的位置;
用法举例:
1.先去measure判断是否包含子视图,有的话就量出它的大小;
2.layout方法中传入的四个参数依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),其中,调用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中测量出的宽和高
问题Q1:getMeasureWidth和getWidth区别?
getMeasureWidth拿到的是onMeasure时setMeasuredDimension方法进行设置的值, 在measure()过程结束后获取到;
getWidth是在onLayout时传入的右边界减去左边界的值, 要在layout()过程结束后获取到;
所以如果处理了setMeasuredDimension和onLayout传左右边界的值,就可能会让getMeasureWidth和getWidth不同;
Q2:什么时候能拿到view的高度?如果是动态变的view如何拿到最终的view高度?
如上是自定义view类中执行的方法顺序,可以看到view的宽高确定是在onMeasure后得到的,如果是动态的view会在onSizeChanged中getMeasuredHeight()拿到最终确定的宽高;
此外,如何在onCreate中拿到View的宽度和高度
- View.post(runnable)
view.post(new Runnable() {
@Override
public void run() {
int width = view.getWidth();
int measuredWidth = view.getMeasuredWidth();
Log.i(TAG, "width: " + width);
Log.i(TAG, "measuredWidth: " + measuredWidth);
}
});
利用Handler通信机制,发送一个Runnable到MessageQueue中,当View布局处理完成时,自动发送消息,通知UI进程。借此机制,巧妙获取View的高宽属性,代码简洁,相比ViewTreeObserver监听处理,还不需要手动移除观察者监听事件。
XML中,设置padding值失效,因为默认是不生效的,只能自己在onDraw中实现