美文网首页
Android自定义View基础——View的工作原理

Android自定义View基础——View的工作原理

作者: 家硕先生 | 来源:发表于2018-10-30 16:10 被阅读12次

    在了解View工作原理之前,需要先了解一些基础概念:


    Activity层次图.png
    • DecorView:顶层视图,将要显示的具体内容呈现在PhoneWindow上,是当前Activity所有View的根节点。
    • ViewRootImpl:连接WindowManager和DecorView的纽带,View的绘制流程正是从通过ViewRootImpl来完成的。

    (图片来源自:Android窗口机制

    一、View的绘制流程是从哪里开始

    这可能得从Android的启动过程说起,但是此处不深究Android的启动过程,只是抽丝剥茧的取出其中与View的绘制过程关系较为直接的来说:
    当Activity初始化Window和将布局添加到DecorView之后,ActivityThread类会调用handleResumeActivity会通过WindowManager将DecorView视图添加到窗口上,大致调用过程为:

    ActivityThread.handleResumeActivity()
    | WindowManager.addView()
    || WindowManagerGlobal.addView()
    ||| ViewRootImpl.setView() -> requestLayout()  -> scheduleTraversals()
    |||| TraversalRunnable.run() -> ViewRootImpl.doTraversal()
    ||||| ViewRootImpl.performTraversals()
    

    关键点就是ViewRootImpl.performTraversals(),展开一看究竟

    private void performTraversals(){
        ......
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ......
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        ......
        performDraw();
        ......
    }
    

    从上面的代码可以看出,performTraversals()依次调用了performMeasure、performLayout 和 performDraw方法,正是测量、布局、绘制。所以,View的绘制流程是从ViewRootImpl的performTraversals开始的。
    performMeasure()会调用顶级View的measure()方法,measure又去调用onMeasure(),onMeasure中会对所有子元素进行measure过程;接着子元素会重复父容器的measure过程,如此反复至完成整个View树的遍历。layout和draw同理。

    View绘制流程.png

    二、View绘制流程之measure

    1. 理解MeasureSpec

    在搞清楚measure过程之前,得先明白MeasureSpec的概念,因为在measure过程中,MeasureSpec将会贯穿其中。

    MeasureSpec是一个32位int值,高2位是SpecMode(测量模式),低30位代表SpecSize(某种测量模式下的规格大小)

    SpecMode的3种模式:

    • UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部。
    • EXACTLY(精确模式):父容器已测出View所需要的精确大小,View的尺寸就是父容器给的确切值。对应LyaoutParams中的match_parent或具体数值。
    • AT_MOST(最大模式):父容器给出可用大小,View的大小不能超过该值,具体要看View的具体实现。对应LayoutParams中的wrap_content。

    普通View的MeasureSpec的创建规则对照表:

    parentSpecMode / chlidLayoutParams EXACTLY AT_MOST UNSPECIFIED
    dp/px EXACTLY childSize EXACTLY childSize EXACTLY childSize
    match_parent EXACTLY parentSize AT_MOST parentSize UNSPECIFIED 0
    wrap_content AT_MOST parentSize AT_MOST parentSize UNSPECIFIED 0

    其中竖列为chlidLayoutParams,横为parentSpecMode。
    表格的推演由来,可参考:ViewGrop.getChildMeasureSpec()

    2. measure过程

    measure过程由measure完成,measure是final方法,不能被重写,所以重点看里面调用的onMeasure。容器类型的View,需要先测量子View。

    • 单个View的测量:
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // 设置宽高的测量值        
            setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
        }
    
        public static int getDefaultSize(int size, int measureSpec) {
            int result = size;
            int specMode = MeasureSpec.getMode(measureSpec);
            int specSize = MeasureSpec.getSize(measureSpec);
    
            switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            // AT_MOST & EXACTLY的返回值是一样的
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            }
            return result;
        }
    

    从getDefaultSize可以看出,默认AT_MOST和EXACTLY的返回值是一样的,所以直接继承View的控件需要重写onMeasure设置specMode=wrap_content时的大小,不然wrap_content效果=match_parent。

    • ViewGroup测量:

    ViewGroup除了测量自身,还需要去调用所有子元素的measure方法来测量子元素,子元素在递归去执行该过程。

    ViewGroup提供了measureChildren方法:

        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);
                }
            }
        }
    
        protected void measureChild(View child, int parentWidthMeasureSpec,
                int parentHeightMeasureSpec) {
            final LayoutParams lp = child.getLayoutParams();
    
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                    mPaddingLeft + mPaddingRight, lp.width);
            final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                    mPaddingTop + mPaddingBottom, lp.height);
            // 调用子View的 measure方法
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    

    在measure时,遍历所有子元素,调用子元素的measure方法。虽然ViewGroup的子类容器布局不一定使用measureChildren方法,但是主要逻辑是一样的,都是遍历子View进行测量。

    三、View绘制流程之layout

    layout的作用是用来确定容器自身以及子元素的位置,首先调用layout()确定自身位置,layout中又会调用onLayout来确定子元素位置。

    来一张图看layout的流程:

    layout过程.png

    四、View绘制流程之draw

    draw作用是将View绘制到屏幕上。

    绘制顺序如下:

    • 绘制背景:background.draw(canvas)
    • 绘制自己:onDraw(canvas)
    • 绘制children:dispatchDraw(canvas)
    • 绘制装饰:onDrawScrollBars(canvas)

    子View的绘制是通过dispatchDraw方法来往下传递的,它会遍历调用所有子元素的draw方法,所有都绘制完成后,在执行自身的onDrawScrollBars。

        protected void dispatchDraw(Canvas canvas) {
            ......
            for (int i = 0; i < childrenCount; i++) {
                while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                    final View transientChild = mTransientViews.get(transientIndex);
                    if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                            transientChild.getAnimation() != null) {
                        // 调用 child.draw方法
                        more |= drawChild(canvas, transientChild, drawingTime);
                    }
                    ......
        }
    

    相关文章

      网友评论

          本文标题:Android自定义View基础——View的工作原理

          本文链接:https://www.haomeiwen.com/subject/ghobtqtx.html