Android自定义View绘制流程小结

总之一句话,当系统控件满足不了我们的需求时,就需要自定义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);
    }
}


每个子View都要单独调用measure方法测量一次,那么ViewGroup可以嵌套多个子View,所以ViewGroup定义了3个比较快捷的方法来对子View进行测量,measureChild,measureChildren,measureChildWithMargins方法, measureChildren内部实质只是循环调用measureChild,measureChild和measureChildWithMargins的区别就是是否把margin和padding也作为子视图的大小。

 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:

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流程之后被调用才能返回有效值。


二、Layout
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的进行布局即可。


三、draw


同样的入口执行流程

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();
        }
    }

写到这里整个View的绘制流程就结束了,以上是自己通过网上资料和查看源码的小总结。









  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值