美文网首页
View的工作原理

View的工作原理

作者: 阿泽Leo | 来源:发表于2018-09-25 17:31 被阅读0次

    本文不再讨论自定义View,推荐阅读扔物线的HenCoder的自定义View系列文章。

    View的流程简述

    View的绘制流程是从ViewRoot的performTraversals方法开始的,主要是:measure、layout、draw。大概流程如下图:

    View的绘制流程.jpg
    • measure方法确定View的测量宽、高
    • layout方法确定View的最终宽高四个顶点的位置。
    • draw方法将View绘制到屏幕上。
    • ViewRoot的实现类是ViewRootImpl,是连接WindowManager和DecorView的纽带。
    • 从ViewRoot的performTraversals开始,经过performMeasure()performLayout()performDraw()完成顶级View(DecorView)的绘制。其中performMeasure()会调用measure和onMeasure,在onMeasure中对所有子元素调用measure和onMeasure,如此循环则完成measure过程,layout、draw同理。
    • 顶级View,即DecorView是一个FrameLayout。内部有一个竖直方向的LinearLayout,包含了titleBar和contentView。如下图:
    DecorView.png
    • 我们的setContentView就是将自己的View放进了content里面。获取自己的View可以这样做:
    ViewGroup viewGroup = findViewById(android.R.id.content);
    View view = viewGroup.getChildAt(0);
    

    View的具体工作流程

    1. measure过程

    作用:确定View的测量宽高

    • MeasureSpec:一个32位的int值。高2位代表SpecMode(测量模式),低30位代表SpecSize(规格大小)。

    SpecMode分为UNSPECIFIED、AT_MOST、EXACTLY。

    • UNSPECIFIED 父容器不对View做任何限制,常用于系统内部。
    • EXACTLY 父容器指定View的大小为SpecSize指定的值。对应match_parent具体数值
    • AT_MOST 父容器指定最大尺寸SpecSize。对应wrap_content。
    • 最终View的MeasureSpec由View的LayoutParams和父容器的MeasureSpec决定,一旦确定后,在onMeasure就可以确定View的测量宽高。

    引用一张网上的图片帮助理解:


    MeasureSpec.png

    其中parentLeftSize表示父容器剩下的可用大小,childSize为子View的指定大小。

    1.1 View的measure过程

    在View的measure方法中会去调用View的onMeasure方法:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(
                getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    

    setMeasuredDimension方法会设置View的宽高测量值。getDefaultSize方法:

    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;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
    

    可以看出,如果直接继承View的自定义控件没有重写onMeasure,那么参数match_parent和wrap_content将为同一结果。处理这个问题的常用代码:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int withSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int withSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        //处理warp_content为自己指定默认值,其余为本身测量值
        if (withSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, mHeight);
        } else if (withSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(withSpecSize, mHeight);
        }
    
    }
    
    1.2 ViewGroup的measure过程

    ViewGroup是一个抽象类,其中没有重写View的onMeasure方法但是提供了measureChildren的方法来测量子类,具体的测量过程需要子类去实现。

    在实际开发中,需要在Activity启动时获取某个控件的宽高,如果在onCreate、onStart中获取这个View的宽高,有可能不是正确的数值。在View测量完成前,获取到的数值都是0。

    解决方法有4种:

    • Activity/View#onWindowFocusChanged
      当Activity获得焦点时,View已经初始化完毕了。
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    }
    
    • view.post(runnable)
      通过post将runnable投递到消息队列尾部,等待Looper调用此runnable时View也已经初始化好了。
    view.post(new Runnable() {
        @Override
        public void run() {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
    
    • ViewTreeObserver
      这是View树的回调接口,当View树状态改变或者View可见性改变时,onGlobalLayout会被回调。
    • view.measure(widthMeasureSpec, heightMeasureSpec)
      手动去测量view的宽高。

    2. layout过程

    作用:确定View的四个顶点的位置和最终宽高。
    layout方法首先确定ViewGroup的位置,然后在onLayout中对子View遍历来确定子View的位置。
    具体流程:


    ViewGroup的layout过程.jpg

    3. draw过程

    作用:将View绘制到屏幕上。
    View的过程如下几步:

    • 绘制背景:drawBackground()
    • 绘制自己:onDraw()
    • 绘制children:dispatchDraw()
    • 绘制装饰:onDrawForeground()

    通过查看View的源码可以清晰看到:

    public void draw(Canvas canvas) {
        ...
    
        /*
         * 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)
         */
    
        // 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;
        }
        ...
    }
    

    相关文章

      网友评论

          本文标题:View的工作原理

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