第4章 View的工作原理
##1、初识ViewRoot和DecorView
ViewRoot的实现是 ViewRootImpl 类,是连接WindowManager和DecorView的纽带,View的三大流程( mearsure、layout、draw) 均是通过ViewRoot来完成。当Activity对象被创建完毕后,会将DecorView添加到Window中,同时创建 ViewRootImpl 对象,并将ViewRootImpl 对象和DecorView建立连接,源码如下:
root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams, panelParentView);
View的绘制流程是从ViewRoot的performTraversals开始的
performTraversals会依次调用 performMeasure 、 performLayout 和performDraw 三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程。其中 performMeasure 中会调用 measure 方法,在 measure 方法中又会调用 onMeasure 方法,在 onMeasure 方法中则会对所有子元素进行measure过程,这样就完成了一次measure过程;子元素会重复父容器的measure过程,如此反复完成了整个View数的遍历。另外两个过程同理。
Measure完成后, 可以通过getMeasuredWidth 、getMeasureHeight 方法来获取View测量后的宽/高。特殊情况下,测量的宽高不等于最终的宽高,详见后面。
Layout过程决定了View的四个顶点的坐标和实际View的宽高,完成后可通过 getTop 、 getBotton 、 getLeft 和 getRight 拿到View的四个定点坐标。
##2、理解MeasureSpec
MeasureSpec决定了一个View的尺寸规格。但是父容器会影响View的MeasureSpec的创建过程。系统将View的 LayoutParams 根据父容器所施加的规则转换成对应的MeasureSpec,然后根据这个MeasureSpec来测量出View的宽高。 MeasureSpec代表一个32位int值,高2位代表SpecMode( 测量模式) ,低30位代表SpecSize( 在某个测量模式下的规格大小)
SpecMode有三种:
UNSPECIFIED :父容器不对View进行任何限制,要多大给多大,一般用于系统内部
EXACTLY:父容器检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,对应LayoutParams中的 match_parent 和具体数值这两种模式
AT_MOST :对应View的默认大小,不同View实现不同,View的大小不能大于父容器的SpecSize,对应 LayoutParams 中的 wrap_content
##3、View的工作流程
(1) measure过程
View的measure过程
直接继承View的自定义控件需要重写 onMeasure 方法并设置 wrap_content ( 即specMode是 AT_MOST 模式) 时的自身大小,否则在布局中使用 wrap_content 相当于使用 match_parent 。对于非 wrap_content 的情形,我们沿用系统的测量值即可。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
// 在 MeasureSpec.AT_MOST 模式下,给定一个默认值mWidth,mHeight。默认宽高灵活指定
//参考TextView、ImageView的处理方式
//其他情况下沿用系统测量规则即可
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
ViewGroup的measure过程
ViewGroup是一个抽象类,没有重写View的 onMeasure 方法,但是它提供了一个 measureChildren 方法。这是因为不同的ViewGroup子类有不同的布局特性,导致他们的测量细节各不相同,比如 LinearLayout 和 RelativeLayout ,因此ViewGroup没办法同一实现 onMeasure方法。
measureChildren方法的流程:
1、 取出子View的 LayoutParams
2、 通过 getChildMeasureSpec 方法来创建子元素的 MeasureSpec
3、 将 MeasureSpec 直接传递给View的measure方法来进行测量
通过LinearLayout的onMeasure方法里来分析ViewGroup的measure过程:
1、 LinearLayout在布局中如果使用match_parent或者具体数值,测量过程就和View一致,即高度为specSize
2、 LinearLayout在布局中如果使用wrap_content,那么它的高度就是所有子元素所占用的高度总和,但不超过它的父容器的剩余空间
3、 LinearLayout的的最终高度同时也把竖直方向的padding考虑在内
View的measure过程是三大流程中最复杂的一个,measure完成以后,通过 getMeasuredWidth/Height 方法就可以正确获取到View的测量后宽/高。在某些情况下,系统可能需要多次measure才能确定最终的测量宽/高,所以在onMeasure中拿到的宽/高很可能不是准确的。如果我们想要在Activity启动的时候就获取一个View的宽高,怎么操作呢?因为View的measure过程和Activity的生命周期并不是同步执行,无法保证在Activity的 onCreate、onStart、onResume 时某个View就已经测量完毕。所以有以下四种方式来获取View的宽高:
1、 Activity/View#onWindowFocusChanged
onWindowFocusChanged这个方法的含义是:VieW已经初始化完毕了,宽高已经准备好了,需要注意:它会被调用多次,当Activity的窗口得到焦点和失去焦点均会被调用。
2、 view.post(runnable)
通过post将一个runnable投递到消息队列的尾部,当Looper调用此runnable的时候,View也初始化好了。
3、 ViewTreeObserver
使用 ViewTreeObserver 的众多回调可以完成这个功能,比如OnGlobalLayoutListener 这个接口,当View树的状态发送改变或View树内部的View的可见性发生改变时,onGlobalLayout 方法会被回调,这是获取View宽高的好时机。需要注意的是,伴随着View树状态的改变, onGlobalLayout 会被回调多次。
4、 view.measure(int widthMeasureSpec,int heightMeasureSpec)
手动对view进行measure。需要根据View的layoutParams分情况处理:
1、 match_parent:
无法measure出具体的宽高,因为不知道父容器的剩余空间,无法测量出View的大小
2、 具体的数值( dp/px):
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
3、 wrap_content:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
// View的尺寸使用30位二进制表示,最大值30个1,在AT_MOST模式下,我们用View理论上能支持的最大值去构造MeasureSpec是合理的
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec,heightMeasureSpec);
(2)、layout过程
Layout的作用是ViewGroup用来确定子View的位置,当ViewGroup的位置被确定后,它会在onLayout中遍历所有的子View并调用其layout方法,在 layout 方法中, onLayout 方法又会被调用。
View的 layout 方法确定本身的位置,源码流程如下:
1、 setFrame 确定View的四个顶点位置,即确定了View在父容器中的位置
2、 调用 onLayout 方法,确定所有子View的位置,和onMeasure一样,onLayout的实现和布局有关,因此View和ViewGroup均没有真正实现 onLayout 方法。
View的测量宽高和最终宽高的区别:
View的测量宽高和最终宽高相等,只不过测量宽高形成于measure过程,最终宽高形成于layout过程。但重写view的layout方法可以使他们不相等。
(3)、draw过程
View的绘制过程遵循如下几步:
1、 绘制背景 drawBackground(canvas)
2、 绘制自己 onDraw
3、绘制children dispatchDraw 遍历所有子View的 draw 方法
4、绘制装饰 onDrawScrollBars
ViewGroup会默认启用 setWillNotDraw 为ture,导致系统不会去执行 onDraw ,所以自定义ViewGroup需要通过onDraw来绘制内容时,必须显式的关闭 WILL_NOT_DRAW 这个优化标记位,即调用 setWillNotDraw(false);
##4、自定义View
(1)自定义View的分类
1、继承View 重写onDraw方法
通过 onDraw 方法来实现一些不规则的效果,这种效果不方便通过布局的组合方式来达到。这种方式需要自己支持 wrap_content 并且padding也要去进行处理。
2、继承ViewGroup派生特殊的layout
实现自定义的布局方式,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子View的测量和布局过程。
3、继承特定的View子类( 如TextView、Button)
扩展某种已有的控件的功能,比较简单,不需要自己去管理 wrap_content 和padding。
4、继承特定的ViewGroup子类( 如LinearLayout)
比较常见,实现几种view组合一起的效果。与方法二的差别是方法二更接近底层实现。
(2)自定义View的须知
1、直接继承View或ViewGroup的控件, 需要在onmeasure中对wrap_content做特殊处理。指定wrap_content下的默认宽/高。
2、直接继承View的控件,如果不在draw方法中处理padding,那么padding属性就无法起作用。直接继承ViewGroup的控件也需要在onMeasure和onLayout中考虑padding和子元素margin的影响,不然padding和子元素的margin无效。
3、尽量不要用在View中使用Handler,因为没必要。View内部提供了post系列的方法,完全可以替代Handler的作用。
4、View中有线程和动画,需要在View的onDetachedFromWindow中停止。当View不可见时,也需要停止线程和动画,否则可能造成内存泄漏。
5、View带有滑动嵌套情形时,需要处理好滑动冲突
(3)自定义View实例
1、继承View重写onDraw方法:采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。
CircleView 遵循以下步骤:
1、在values目录下创建自定义属性的XML,如attrs.xml。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color" />
</declare-styleable>
</resources>
2、在View的构造方法中解析自定义属性的值并做相应处理,这里我们解析circle_color。 然后在onDraw中绘制圆。
public class CicleView extends View {
private int mColor = Color.RED;
Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CicleView(Context context) {
super(context);
init();
}
public CicleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public CicleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
typedArray.getColor(R.styleable.CircleView_circle_color,Color.RED);
typedArray.recycle();
init();
}
private void init() {
mPaint.setColor(mColor);
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int radius = Math.min(width,height) / 2;
canvas.drawCircle( width/2,height/2,radius,mPaint);
}
}
3、在Activity布局文件中使用自定义属性
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:orientation="vertical" >
<com.ryg.chapter_4.ui.CircleView
android:id="@+id/circleView1"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="#000000"
android:padding="20dp"
app:circle_color="@color/light_green" />
</LinearLayout>
实现的效果为:
虽然设置了 android:padding="20dp",但是并没有效果。使用android:layout_width="wrap_content" 或者设置为match_parent都不行,前面章节提到过,对于直接继承自View的控件,如果不对wrap_content做处理,那么就相当于使用match_parent;
在这里只需要指定一个wrap_content模式的默认宽高即可,比如选择200dp作为默认的宽高。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200,200);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200,heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize,200);
}
}
实现效果为
发现 android:padding="20dp" 没有达到效果,对此我们要对onDraw()做一下修改
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int width = getWidth() -paddingLeft-paddingRight;
int height = getHeight() -paddingBottom-paddingTop;
int radius = Math.min(width,height) / 2;
canvas.drawCircle(paddingLeft + width/2,paddingTop + height/2,radius,mPaint);
}
就是在绘制的时候考虑下view四周的空白即可,其中圆心和半径都会考虑到view四周的padding,就达到了要求。