View的工作原理
1、初识ViewRoot和DecorView
为了更好的自定义View,还需要掌握View的底层工作原理,比如View的测量流程、布局流程和绘制流程,掌握这几个基本流程之后,我们就对View的底层更加了解,这样我们就可以做出一个比较完善的自定义View。
自定义View的实现看起来很复杂,实际上说简单也简单。
ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成。
在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。
View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个流程才能最终将一个View绘制出来,其中:
①、measure用来测量View的宽和高
②、layout用来确定View在父容器中的放置位置
③、draw用于将View绘制到屏幕上
measure过程决定了View的宽高,Measure完成以后,可以通过getMeasuredWidth和getMeasureHeight方法获取到VIew测量后的宽高,几乎所有情况下它都等同于View最终的宽高;
layout过程决定了View的四个顶点的坐标和实际的View的宽高,完成以后,可以通过getTop、getBottom、getLeft、getRight来拿到View的四个顶点的位置,并可以通过getWidth和getHeight方法来拿到VIew的最终宽高;
draw过程这决定了View的显示,只有draw方法完成后View的内容才呈现在屏幕上。
DecorView作为顶级View,一般情况下它内部会包含一个竖直方法的LinearLayout,在这个LinearLayout里面有上下两个部分,上面是标题栏titlebar,下面是内容栏。
通过源码我们知道,DecorView其实是一个FrameLayout,View层的事件都先经过DecorView,然后才传递给我们的View。
2、理解View的MeasureSpec
MeasureSpec:测量规格,测量说明书。不管怎么翻译,它看起来都好像是或多或少地决定了View的测量过程。
MeasureSpec在很大程度上决定了一个View的尺寸规格,之所以说是很大程度上是因为这个过程还受父容器的影响,因为父容器影响View的MeasureSpec的创建过程。
在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成为对应的MeasureSpec,然后再根据这个MeasureSpec来测量出View的宽高。
1)、MeasureSpec
MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。
SpecMode有三类,每一类都表示特殊的含义:
UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。
EXACTLY:父容器已经检测出View所需的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数字这两种模式。
AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。
②、MeasureSpec和LayoutParams的对应关系
在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽高。
需要注意的是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽高。
对于普通的View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽高。
①、当View采用固定宽高是,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式,并且大小是LayoutParams中的大小;
②、当View的宽高是match_parent时,如果父容器的模式是精确模式,那么View也是精确模式,并且大小是父容器的剩余空间;如果父容器是最大模式,并且大小不会超过父容器的剩余空间;
③、当View的宽高是wrap_content时,不管父容器的模式是精确模式还是最大模式,View的模式总是最大模式,并且大小不超过父容器的剩余空间。
④、我们的分析漏掉了UNSPECIFIED模式,那是因为这个模式主要用于系统内部多次Measure的情形,一般来说,我们不需要关注此模式。
3、View的工作流程
View的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制,其中measure确定View的测量宽高,layout确定view的最终宽高和四个顶点的位置,而draw则将view绘制到屏幕上。
1)、measure过程
measure过程要分情况来看,如果是一个原始的View,那么通过measure方法就完成了其测量过程,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历调用所有子元素的measure方法,各个子元素再递归去执行这个流程。
①、View的measure过程:
结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,直接在布局中使用wrap_content就相当于使用match_parent。
②、ViewGroup的measure过程:
对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但是它提供了一个叫measureChildren的方法。
View的measure过程是三大流程中最复杂的一个,measure完成以后,通过getMeasureWidth和getMeasureHeight方法就可以正确地获取到View的测量宽高。需要注意的是,在某些极端情况下,系统可能需要多次measure才能确定最终的测量宽高,在这种情况下,在这种情形下,在onMeasure方法中拿到的测量宽、高很可能是不准确的。一个较好的习惯是在onLayout方法中获取View的测量宽高或最终宽高。
获取View宽高的方法:
①、Activity/View---->onWindowFocusChanged
onWindowFocusChanged这个方法的含义是:View已经初始化完毕了,宽高已经准备好了,这时候获取宽高是没问题的
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus){
int width = iv_splash_bg.getMeasuredWidth();
int height = iv_splash_bg.getMeasuredHeight();
}
}
②、View.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。
iv_splash_bg.post(new Runnable() {
@Override
public void run() {
int width = iv_splash_bg.getMeasuredWidth();
int height = iv_splash_bg.getMeasuredHeight();
}
});
③、ViewTreeObserver
使用ViewTreeObserver的众多回调可以完成这个功能,例如使用OnGlobalLayoutListener这个接口,当View树的状态发生改变或者View树内部的View的可见性发生改变时,onGlobalLayout方法将被回调,因此这是获取View的宽高一个很好的时机,需要注意的是,伴随着View树的状态改变等,onGlobalLayout会被调用多次。
ViewTreeObserver treeObserver =iv_splash_bg.getViewTreeObserver();
treeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
iv_splash_bg.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = iv_splash_bg.getMeasuredWidth();
int height = iv_splash_bg.getMeasuredHeight();
}
});
2)layout过程
Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。Layout过程和measure过程相比就简单多了,layout方法确定View本身的位置,onLayout方法则会确定所有子元素的位置。
3)draw过程
Draw过程叫比较简单了,它的作用是将View绘制到屏幕上面。View的绘制过程遵循如下几步:
①、绘制背景background.draw(canvas)
②、绘制自己(onDraw)
③、绘制children(dispatchDraw)
④、绘制装饰(onDrawScrollBars)
4、自定义View
自定义View是一个综合的技术体系,它涉及View的层次结构,事件分发机制和View的工作原理等技术细节。而这些技术细节每一项又都是初学者难以掌握的。
1)自定义View的分类
①、继承View重写onDraw方法
这种方式主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态地显示一些不规则图形。这需要通过绘制的方式来实现,即重写onDraw方法。采用这种方式需要自己支持wrap_content、并且padding也需要自己处理。
②、继承ViewGroup派生特殊的Layout
这种方式主要用于实现自定义的布局,即除了LinearLayout、RelativeLayout、FrameLayout这几种系统的布局以外,我们需要重新定义一种新布局,当某种效果看起来很像几个View组合在一起时,可以采用这种方式来实现。采用这种方式稍微复杂一些,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。
③继承特定的View(如TextView)
这种方式比较常见,一般用于扩展某种已有的View的功能,比如:TextView、这种方式比较容易实现,这种方式不需要自己支持wrap_content和padding等。
④、继承特定的ViewGroup(比如LinearLayout)
这种方式比较常见,当效果看起来很像几种View组合在一起时,可以采用这种方式实现。采用这种方法不需要自己处理ViewGroup的测量和布局这两个过程。一般来说方法2实现的效果方法4也都能实现,两者的区别是方法2更加接近View底层。
2)、自定义View须知
这些问题处理不好,有些会影响View的正常使用,而有些则会导致内存泄露
①、让View支持wrap_content
这是因为直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,那么当外界在布局中使用wrap_content时就无法达到预期的效果。
②、如果有必要,让View支持padding
这是因为直接继承View的控件,如果不在draw方法中处理padding,那么padding属性无法其作用。另外,直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。
③尽量不要在View中使用Handler,没必要
这是因为View内部本身就提供了post系列的方法,完全可以替代Handler的作用,除非很明确地要使用Handler发送消息。
④View中如果有线程或者动画,需要及时停止,参看View---onDetachedFromWindow
如果有线程或者动画需要停止时,那么onDetachedFromWindow是一个好时机。当包含此View的Activity退出或者当前View被remove时,view的onDetachedFromWindowf方法被调用,同时,当View变得不可见时我们需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄露。
⑤、View带有滑动嵌套情形时,需要处理好滑动冲突
如果有滑动冲突的话,那么要合适地处理滑动冲突,否则将会严重影响View的效果。
3)、自定义View示例
①、继承View重写onDraw方法
这种方法主要用于实现一些不规则的效果,一般需要重写onDraw方法,采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。margin属性有父容器控制。对于直接继承自View的控件,如果不对wrap_content做特殊处理,那么使用wrap_content就相当于match_parent,其次,针对padding的问题,也很简单,只要在绘制时考虑一下padding即可。
最后,为了让我们的View更加容易使用,很多情况下我们还需要提供自定义属性,像android:layout_width和android:layout_padding这种以android开头的属性是系统自带的属性。
①第一步、在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>
②、第二步,在View的构造方法中解析自定义属性的值并作响应处理
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = typedArray.getColor(R.styleable.CircleView_circle_color, Color.RED);
typedArray.recycle();
init();
}
③、在布局文件中使用自定义属性
@创建者 :yqlee
* @时间 :2016/3/25 14:30
* @描述 :圆形
*/
public class CircleView extends View {
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = typedArray.getColor(R.styleable.CircleView_circle_color, Color.RED);
typedArray.recycle();
init();
}
private void init() {
mPaint.setColor(mColor);
}
@Override
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);
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);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//获取padding
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
//处理padding
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(width, height) / 2;
canvas.drawCircle(width / 2, height / 2, radius, mPaint);
}
}
②、继承ViewGroup派生特殊的Layout
这种方法主要用于实现自定义的布局,采用这种方式稍微复杂一些,需要合适地处理ViewGroup的测量、布局这两个过程。
4、自定义View的思想
自定义View是一个综合的技术体系,很多情况下需要灵活地分析从而找出最高效的方法。核心思想:首先要掌握基本功,比如View的弹性滑动、滑动冲突、绘制原理等,这些东西都是自定义View所必须的。