View的工作原理
ViewRoot和DecorView
ViewRoot对应于ViewRootImpl,连接WindowManager和DecorView的纽带。
View的绘制流程从ViewRoot的performTraversals方法开始,经过以下三个过程:
- measure
- layout
- draw
理解MeasureSpec
MeasureSpec是一个会影响到View测量过程的参数。在测量View的宽高的过程中,系统会将View的LayoutParams根据父View的规则转换成对应的MeasureSpec,在进行宽高测量。
MeasureSpec
MeasureSpec是一个32位的int值,高两位代表SpecMode,低30位代表SpecSize。即模式+尺寸。Android里将这两个参数打包成了一个int值来避免过都的内存分配,可以通过get方法解包得到mode和size的分别值。源码里主要是一些“位操作”,类似:
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
如源码里给出的,SpecMode有三类:
- UNSPECIFIED:父容器不对View有任何限制,一般系统内部使用。
- EXACTLY:父容器已经检测出View的精确大小,由SpecSize指定。
- AT_MOST:父容器指定一个可用大小SpecSize,子View的大小不能超过这个值。
子View Mode会被父View的specMode所影响,在getChildMeasureSpec方法中,给出了这种影响的具体过程,其流程图如下:
子View会根据父View的Spec不同模式,得到不同的结果。
从流程图和表格可以总结出:
- View的MesureSpec由父View的MesureSpec和自身的LayoutParams共同决定;
- 若View指定了大小,则不管父View的MeasureSpec如何,其Spec将总是ECACTLY,而大小为其指定的大小;
- 子View的LayoutParams为Wrap_content时,无论父类为何种模式,子View总是AT_MOST。因此,对于自定义控件来说,当指定view为wrap_content时,需要指定自身的大小,否则子View会在AT_MOST的模式下,最大程度的利用父View的空间。
- getMeasureSpec方法返回的是一个打包后的MesureSpec,子View的Mode将由其前2位确定,而后30位事实上代表了父View的可用大小,子View将参考这一值,但并不是最终子View的大小(事实上,View的最终大小是在layout阶段被确定的,但是一般情况下,View的测量大小和最终大小相等)。
View的工作流程
View的工作流程主要有:measure、layout、draw,即测量,布局和绘制。
View的measure过程
对于View来说,measure过程就是测量自身尺寸的过程;对于ViewGroup来说,measure过程除了测量自身尺寸外,还要递归的去测量所有children的尺寸。
View的measure过程比较简单:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
基本上,所有的onMeasure都要干一件事:计算好自己的宽高,然后调用setMeasuredDimension方法保存。对于自定义的View,我们要自己计算width和height数值。这里就不贴getDefaultSize的代码了,也比较简单,就是根据SpecMode的值,来判断应该使用什么样的size。
ViewGroup的measure过程
ViewGroup的measure过程除了绘制自身外,还要绘制其children。ViewGroup本身是个抽象类,并没有去实现View的onMeasure方法,其通过一个measureChildren的方法对所有的Children进行测量。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
其中调用了measureChild对每个child进行测量:
测量过程本质上和View是一致的,外部传入了需要测量的child视图和父View的MeasureSpec,在调用View中getChildMeasureSpec方法创建MeasureSpec,而测量结果传递到View的measure方法中进行测量。接下去就是一个递归遍历的过程。
由于ViewGroup本身是抽象类,没有实现onMeasure方法,因此需要其具体的实现类,来完成这个方法。典型如LinearLayout、RelativeLayout等。事实上,每个ViewGroup的onMeasure方法考虑的东西很多,Android里LinearLayout源码还比较长,值得一看,可以了解下具体的测量过程。
View 的Measure过程和Activity的生命周期方法并不同步,往往在onCreate方法中去获取View的尺寸,得到的值并不是最终View的尺寸大小,为了在Activity启动时获取一个View的尺寸,有四种方法。
(1) Activity/View#onWindowsFocusChanged
当Activity窗口获得焦点时会被调用,并且这个方法表示View已经初始化完毕。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
(2) view.post(Runnable)
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
}
});
}
(3) ViewTreeObserver
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@SuppressLint("NewApi")
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int widt = view.getMeasuredHeight();
}
});
}
layout过程
layout是在Measure结束后的步骤,将用来确定子View的位置。对于ViewGroup来说,layout方法确定本身的位置,然后调用onlayout方法确定所有子view的位置。对于View而言,其layout过程如下:
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList listenersCopy =
(ArrayList)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
layout方法会先使用setFrame来设定View本身的四个顶点位置,在调用onLayout方法去测量子View的位置,而onlayout是一个抽象方法,对于一个view而言,将不会有什么作用,对于一个ViewGroup而言,将会去确定其中所有子view的位置;同样的,在子view内,也会再调用layout方法确定自身和onlayout方法确定子子view,因此通过一层一层的传递,完成整个view树的layout过程。
ViewGroup的一个实现类是LinearLayout,在LinearLayout中,会重写onlayout方法,来完成自身和子View的布局位置确定:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
LinearLayout布局可以选择横向排列或者纵向排列内部的子View,两者实现逻辑类似,看看layoutVertical(l,t,r,b)的一些代码:
void layoutVertical(int left, int top, int right, int bottom) {
......
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
......
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
在layoutVertical中,通过Gravity的属性,来判断child的left、right、top等参数如何计算,这里省略贴代码了。在对一个子view计算好四个坐标后,通过setChildFrame函数记录。注意到在setChildFrame后,childTop会加上这个chil自身的高度,这就意味着下一个child的视图位置一定会在当前child下面,实现垂直排列的效果。而在setChildFrame中,实际上也是调用了view的layout方法:
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
draw过程
在Measure和layout之后,意味着每一个view在屏幕上的最终大小和位置都被确定了,这时候就通过draw过程将其绘制到屏幕上,其步骤:
- 绘制背景background.draw(canvas)
- 绘制自己(onDraw)
- 绘制Children(dispatchDraw)
- 绘制装饰(onDrawScrollBars)
/* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */
View有一个特殊的方法setWillNotDraw,它表示如果一个View不需要绘制本身,可以把这个标志位设为true,以便于系统对其进行优化。显然,一个普通的view一般不会去设置这个标志位,但是在某些ViewGroup中,可能本身并不需要经行绘制,那么可以通过这个方法设置从而优化性能。
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
自定义View
按照Android开发艺术探索里的分类,有如下四种情况:
- 继承View重写onDraw 方法
- 继承ViewGroup派生
- 继承已有的实体View,如TextView
- 继承已有的实体ViewGroup,如LinearLayout
总结一下就是:自定义View,毫无疑问都需要直接或者间接继承于View,然后根据具体需要实现的功能,来决定是利用现有的View来扩展,还是从底层开始重写。继承的层次越少,自定义空间就越大,同时难度也越高。因此,自定义View时,需要我们找到一种cost最小的方法去实现我们需要的功能。
自定义View时,一些注意事项:
- View需要去支持wrap_content $$ wrap_content对应的MeasureSpec是AT_MOST,如果View不对wrap_content进行处理,会最大限度的利用父view的空间
- View需要去处理padding 和margin $$ 从之前的三大过程来看,padding和margin是参与到了view的绘制计算中的,如果不处理,则这些属性会无效
- View中尽量不使用Handler $$ 因为View本身提供了post方法来发送消息
- View中如果有线程或者动画,需要及时停止 $$ 一般在onDetachedFromWindow中处理,否则可能造成内存泄露
- View如果带有滑动嵌套,需要处理滑动冲突 $$ 有外部拦截发和内部拦截法
重写onDraw方法
public class CircleView extends View {
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int width = getWidth()-paddingLeft-paddingRight;
int height = getHeight()-paddingTop-paddingBottom;
int radus = Math.min(width,height)/2;
canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,radus,mPaint);
}
}
通过自定义了一个CircleView,重写其onDraw方法,来实现画圆。可以看到,onDraw方法中,我对padding属性经行了处理,使得自定义View才能够对xml文件里的padding属性经行支持;另外,在onMeasure方法里,也对wrap_content的默认属性进行了设置。为了使一个自定义View支持我们需要的自定义属性,需要在values目录下创建一个自定义属性是xml文件:
之后,在CircleView的构造函数里对属性进行解析:
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = array.getColor(R.styleable.CircleView_circle_color, Color.RED);
array.recycle();
init();
}
最后,在xml布局文件中,正常使用即可。需要注意的是,要对命名空间进行声明,类似:
xmlns:app="http://schemas.android.com/apk/res-auto"