任何的View想要显示到屏幕上,都要经过3个流程:
- measure 测量宽和高
- layout 确定左、上、右、底的位置。
- draw 绘制
而这一章将总结这3个环节的机制,从而可以真正自如地去定义一个自己的View或ViewGroup。
View的测量、布局、绘制原理机制
这一系列的过程首先是从ViewRootImpl的一个方法performTraversals
开始进行的,这个方法代码比较多,简单说会按顺序依次调用performMeasure
,performLayout
,performDraw
。performMeasure
源码如下:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
很简单,就是调用mView的measure,这里传的参数都是屏幕的最大宽和高。浏览整个ViewRootImpl源码文件,唯一对mView做有效赋值的是方法setView
,毫无疑问这里的mView一定就是DecorView。所以测量的入口就是DecorView的measure方法了。
一般来讲,DecorView由两部分组成,上面的titlebar和下面的contentView,而我们在acitivity中setContentView
方法所设定的便是下面这一部分。另外,DecorView继承自FrameLayout,同时重写了onMeasure方法,在这个方法中又调用了super.onMeasure
所以FrameLayout的onMeasure也被调用,在这个onMeasure中会对所有子元素进行measure,我们的contentView当然也不例外。
上面说了内部进入measure的过程,读者可能脑袋有点大了,不过我先带来一些总结过的知识:
- 在View中会默认提供这些方法:measure/onMeasure, layout/onLayout, draw/onDraw/dispatchDraw,这些成对的方法,后者都会被前者所调用。即measure里会调用onMeasure,layout里会调用onLayout,draw里会调用onDraw,再调用dispatchDraw。其中measure方法是不可重写的。onDraw/dispatchDraw默认都是空的。
- 在具体的View和ViewGroup实现类(子类)中通过重写onMeasure,onLayout,onDraw/dispatchDraw完成自己的视图业务。
- 在onMeasure中最后一定要用setMeasuredDimension来设置最终的宽和高。如果要实现的是ViewGroup,则有义务去调用子View的measure,这个过程一般可以通过ViewGroup提供的measureChild系列方法来间接调用。如果不调用ChildView的onMeasure,结果就是ChildView均无法显示。如果不去特别对待MeasureSpec的AT_MOST和EXACTLY模式的话,最终结果很可能使得此View的wrap_content和match_parent效果一样。
- 在onLayout中,如果要实现的是ViewGroup则有义务去调用ChildView的layout方法,传递的参数是此ViewGroup想让孩子所处的左坐标,上坐标,右坐标,下坐标。在使用的过程也可以放心地获取childView的measuredWidth和measuredHeight等。这个方法的重写对于ViewGroup比较重要,用来真正设置childView的位置。
- 在onDraw/dispatchDraw中,如果要实现的是ViewGroup,一般不加以干涉,ViewGroup会重写dispatchDraw方法,对于大部分ViewGroup的绘制过程基本通用。比如RelativeLayout就没有任何draw方面的方法,全部使用ViewGroup的。而LinearLayout重写了onDraw方法用来绘制dividers,而在dispatchDraw时仍然使用ViewGroup的。如果要实现的是View,则有必要重写onDraw方法,来绘制出自己真正想显示的东西,绘制的东西如果超出了layout的过程的限制,则不会显示。
由此,我们结合上面所说的整个显示过程是从ViewRootImpl的performTravelsals开始,以decorView为根开始进入的,我们就掌握了整个View显示过程的来龙去脉。下面,我简单自定义了一个ViewGroup作为练习。其业务很简单,将每个childView依次向右下方排列,支持wrap_content,支持childView的可见性为gone(这一点需要自己手动实现)。贴代码:
public class TestViewGroup extends ViewGroup {
final int marginLeftDelta = 20;
final int marginTopDelta = 40;
public TestViewGroup(Context context) {
super(context);
}
public TestViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TestViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int maxWidth = 0, maxHeight = 0;
int visibleCount = 0;
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
//needs to check visiablity
if (child.getVisibility() == GONE) continue;
measureChild(child, widthMeasureSpec, heightMeasureSpec);
maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + visibleCount * marginLeftDelta);
maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + visibleCount * marginTopDelta);
visibleCount++;
}
if (widthMode == MeasureSpec.AT_MOST) widthSize = maxWidth;
if (heightMode == MeasureSpec.AT_MOST) heightSize = maxHeight;
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int visibleCount = 0;
for (int i = 0; i < getChildCount(); i++) {
//needs to check visiablity
final View child = getChildAt(i);
if (child.getVisibility() == GONE) continue;
child.layout(marginLeftDelta * visibleCount, marginTopDelta * visibleCount, marginLeftDelta * visibleCount + child.getMeasuredWidth(), marginTopDelta * visibleCount + child.getMeasuredHeight());
visibleCount++;
}
}
}
测试的布局文件:
<com.zerastudio.viewtest.TestViewGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<View
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@color/colorAccent" />
<View
android:layout_width="50dp"
android:layout_height="50dp"
android:background="#ffffaa" />
<View
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@color/colorAccent" />
<View
android:layout_width="50dp"
android:layout_height="50dp"
android:background="#ffffaa" />
<View
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@color/colorAccent" />
</com.zerastudio.viewtest.TestViewGroup>
在AS的preview就能看到此时的效果了:
如果把一个childView的可见性设为gone则:
看到右边,确实没有再对visibility为gone的childView进行measure和layout了,draw部分完全使用了ViewGroup提供的。
至此,View的测、布、绘的原理就基本清楚了,但这也只是一个开始,接下来我将继续探索如下的方面:
- LinearLayout和RelativeLayout甚至其他Layout的测、布、绘的区别,真正分析其效率差异原因。
- RecyclerView和ListView的测、布、绘的区别,以及两者的效率差异原因。
- BitmapShader和xfermode,这两者将有助于绘制出更复杂美丽的View。