总之一句话,当系统控件满足不了我们的需求时,就需要自定义View来实现,足以表达自定义有多么强大!
通过网上资料和结合自己实践,这篇文章主要用来理解绘制流程的一个具体过程的,绘制流程的起始都是在ViewRootlmpl类的performTraversals方法里开始。
private void performTraversals() {
......
//最外层的根视图的widthMeasureSpec和heightMeasureSpec由来
//lp.width和lp.height在创建ViewGroup实例时等于MATCH_PARENT
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
mView.draw(canvas);
......
}
一、measure
首先搞明白,ViewGroup是View的一个扩展类,所以我们最开始先从View着手,上面代码可以看到依次调用了View的3个方法,我们查看View的measure方法,发现是flinal修饰,不让子类重载,看到最终调了自己的onMeasure方法。
//final方法,子类不可重写
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
//回调onMeasure()方法
onMeasure(widthMeasureSpec, heightMeasureSpec);
......
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
可以看到View默认调用了setMeasuredDimension方法进行了测量,然后通过getSuggestedMinimumWidth和getSuggestedMinimumHeight方法获取到View默认的背景尺寸然后通过getDefaultSize方法将父类传来的宽高和子View的背景宽高进行判断,拿到最终的测量值,这也是我们为什么自定义View的时候,没有重写onMeasure方法,但是View也是有大小的,因为有一个View的背景最小值。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//通过MeasureSpec解析获取mode与size
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
回过头继续看上面onMeasure方法,其中getDefaultSize参数的widthMeasureSpec和heightMeasureSpec都是由父View传递进来的。getSuggestedMinimumWidth与getSuggestedMinimumHeight都是View的方法,具体如下:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
看见没有,建议的最小宽度和高度都是由View的Background尺寸与通过设置View的miniXXX属性共同决定的,到了这里,View的测量也就结束了,其实看测量的源码只是想弄明白为什么这三个方法会被调用?为什么我们自定义View不设置尺寸也会有默认大小?默认大小是从哪里来的?是如何进行判断并且测量进去的?
看了以上代码这些问题也都明白了,自定义View重载onMeasure方法测量也就这么简单,无非就是对widthMeasureSpec, heightMeasureSpec两个参数进行测量获取模式和大小,代码如下:
public class Views extends View {
private Paint mPaint;
private Rect mRect;
private String mContent;
public Views(Context context) {
this(context,null);
}
public Views(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public Views(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//初始化画笔
mPaint = new Paint();
mContent = "被绘制的文字";
mRect = new Rect();
//设置大小
mPaint.setTextSize(50);
//计算出界限对象
mPaint.getTextBounds(mContent,0, mContent.length(),mRect);
}
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.YELLOW);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
//开始使用Canvas画
mPaint.setColor(getResources().getColor(R.color.colorAccent));
canvas.drawText(mContent,getWidth() / 2 - mRect.width() / 2, getHeight() / 2 + mRect.height() / 2, mPaint);
}
/*当我们设置View明确宽高时,系统帮我们测量的结果就是我们设置的结果,比如100,此时的模式EXACTLY
* 当设置wrap_content,表示不确定的,系统测量结果就是match_parent,这个时候就需要对View进行测试 AT_MOST
*
*
* */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//widthMeasureSpec和heightMeasureSpec是父类提供给我们用来测试当前这个View的宽高
//第一步获取宽和高的模式和大小,系统提供了MeasureSpec
int widthMode = MeasureSpec.getMode(widthMeasureSpec); //宽尺寸模式
int widthSize = MeasureSpec.getSize(widthMeasureSpec); //宽大小
int heightMode = MeasureSpec.getMode(heightMeasureSpec);//高尺寸模式
int heightSize = MeasureSpec.getSize(heightMeasureSpec);//高大小
//第二步判断模式
int width,height;
// 如果等于确定模式,就把实际大小给View
if(widthMode == MeasureSpec.EXACTLY){
width = widthSize;
}else{ //如果不确定模式,获取View的内容大小+边距
width = mRect.width()+getPaddingLeft()+getPaddingRight();
}
if(heightMode == MeasureSpec.EXACTLY){
height = heightSize;
}else{
height = mRect.height()+getPaddingBottom()+getPaddingTop();
}
//第三步,得到具体的大小,手动测试
setMeasuredDimension(width,height);
}
}
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);
}
}
}
看看measureChildWithMargins方法
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//获取子视图的LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//调整MeasureSpec
//通过这两个参数以及子视图本身的LayoutParams来共同决定子视图的测量规格
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
//调运子View的measure方法,子View的measure中会回调子View的onMeasure方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
measure过程主要就是从顶层父View向子View递归调用view.measure方法(measure中又回调onMeasure方法)的过程。具体measure核心主要有如下几点:
MeasureSpec(View的内部类)测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成。其中specMode只有三种值。
MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;
MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定;
View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。
View的布局大小由父View和子View共同决定。
使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。
private void performTraversals() {
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
mView.draw(canvas);
......
}
我们看到ViewGorup的layout实际是调用了View的layout
@Override
public final void layout(int l, int t, int r, int b) {
......
super.layout(l, t, r, b);
......
}
所以我猜想View的layout肯定回调了onLayout方法,就像View的measure方法回调了onMeasure方法一样,果不其然看看View的layout方法
public void layout(int l, int t, int r, int b) {
......
//实质都是调用setFrame方法把参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量
//判断View的位置是否发生过变化,以确定有没有必要对当前的View进行重新layout
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//需要重新layout
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//回调onLayout
onLayout(changed, l, t, r, b);
......
}
......
}
对于layout方法,其实我们只需要实现onLayout方法,将自己的子View的进行布局即可。
同样的入口执行流程
private void performTraversals() {
......
final Rect dirty = mDirty;
......
canvas = mSurface.lockCanvas(dirty);
......
mView.draw(canvas);
......
}
ViewGorup没有draw方法,直接看View
public void draw(Canvas canvas) {
......
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
1:对View的背景进行绘制。
drawBackground(canvas);方法实现了背景绘制。我们来看下这个方法源码,如下:
private void drawBackground(Canvas canvas) {
//获取xml中通过android:background属性或者代码中setBackgroundColor()、setBackgroundResource()等方法进行赋值的背景Drawable
final Drawable background = mBackground;
......
//根据layout过程确定的View位置来设置背景的绘制区域
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
rebuildOutline();
}
......
//调用Drawable的draw()方法来完成背景的绘制工作
background.draw(canvas);
......
}
2:对View的内容进行绘制。
调用了onDraw(Canvas canvas),实际是一个空方法,子View自己实现。
protected void onDraw(Canvas canvas) {
}
3:对View和所有子View进行绘制。
dispatchDraw方法,也是一个空方法。
protected void dispatchDraw(Canvas canvas) {
}
既然我们自定义View,不管是View还是ViewGroup都只需要重写onDraw方法,那这个方法干嘛的?我们查看
ViewGorup,发现它实现了该方法,里面循环调用drawChild方法,最终调用View的draw方法,所以说ViewGroup类已经为我们重写了dispatchDraw()的功能实现,我们一般不需要重写该方法,但可以重载父类函数实现具体的功能。
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
4:对View的滚动条进行绘制。
其实任何一个View都是有(水平垂直)滚动条的,只是一般情况下没让它显示而已。
四、讲完绘制流程,分析一下最常用的invalidete和postInvalidete方法。
invalidete:内部实际调用了invalidateInternal方法,
public void invalidate(Rect dirty) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
//实质还是调运invalidateInternal方法
invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,
dirty.right - scrollX, dirty.bottom - scrollY, true, false);
}
invalidateInternal方法,p就是父类,一层一层往上调。
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
......
// Propagate the damage rectangle to the parent view.
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
//设置刷新区域
damage.set(l, t, r, b);
//传递调运Parent ViewGroup的invalidateChild方法
p.invalidateChild(this, damage);
}
......
}
最后传到
ViewRootImpl最顶层类里面的某一个方法,在这里面Handler发送异步消息,然后调用了ViewRootImpl的performTraversals方法,performTraversals就是整个View数开始绘制的起始调运地方,所以说View调运invalidate方法的实质是层层上传到父级,直到传递到ViewRootImpl后触发了scheduleTraversals方法,然后整个View树开始重新按照上面分析的View绘制流程进行重绘任务。
postInvalidete:调用此方法实际就是通过ViewRootImpl的Handler发送了一个延时消息,然后接收处理的时候,在UI线程使用了invalidete方法而已。
public void postInvalidateDelayed(long delayMilliseconds) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
//核心,实质就是调运了ViewRootImpl.dispatchInvalidateDelayed方法
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
我们继续看他调运的ViewRootImpl类的dispatchInvalidateDelayed方法
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
看见没有,通过ViewRootImpl类的Handler发送了一条MSG_INVALIDATE消息,继续追踪这条消息的处理可以发现实质就是又在UI Thread中调运了View的invalidate();方法,那接下来View的invalidate();方法我们就不说了,上面已经分析过了。
public void handleMessage(Message msg) {
......
switch (msg.what) {
case MSG_INVALIDATE:
((View) msg.obj).invalidate();
break;
......
}
......
}
回到最开始ViewRootImpl的performTraversals方法,这个方法最开始是在哪里调用的?
当我们写一个Activity时,我们一定会通过setContentView方法将我们要展示的界面传入该方法,该方法会讲我们界面通过addView追加到id为content的一个FrameLayout(ViewGroup)中,然后addView方法中通过调运invalidate(true)去通知触发ViewRootImpl类的performTraversals()方法,至此递归绘制我们自定义的所有布局。
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
......
//如果mContentParent为空进行一些初始化,实质mContentParent是通过findViewById(ID_ANDROID_CONTENT);获取的id为content的FrameLayout的布局(不清楚的请先看《Android应用setContentView与LayoutInflater加载解析机制源码分析》文章)
if (mContentParent == null) {
installDecor();
}
......
//把我们的view追加到mContentParent
mContentParent.addView(view, params);
......
}
public void addView(View child) {
addView(child, -1);
}
public void addView(View child, int index) {
......
addView(child, index, params);
}
public void addView(View child, int index, LayoutParams params) {
......
requestLayout();
//重点关注!!!
invalidate(true);
......
}
requestLayout方法:调用requestLayout方法,也是一层层往上调用父类的requestLayout方法。
public void requestLayout() {
......
if (mParent != null && !mParent.isLayoutRequested()) {
//由此向ViewParent请求布局
//从这个View开始向上一直requestLayout,最终到达ViewRootImpl的requestLayout
mParent.requestLayout();
}
......
}
直到ViewRootImpl为止,然后触发ViewRootImpl的requestLayout方法,如下就是ViewRootImpl的requestLayout方法:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
//View调运requestLayout最终层层上传到ViewRootImpl后最终触发了该方法
scheduleTraversals();
}
}