View的工作原理

初识ViewRoot和DecorView
ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联,这个过程可参看源码:
root = new ViewRootImpl(view.getContext, display);
root.setView(view, wparams, panelParentView);

View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout、draw三个过程最终才将一个View绘制出来的。其中measure用来测量View的宽和高,layout用来确定View在父容器的放置位置,而draw则负责将View绘制在屏幕上,针对perfromTraversals的大致流程,可用下列流程图来表示:


 performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw这三个流程、其中在performMeasure中会调用measure方法,measure方法中又会调用onMeasure方法,在onMeasure方法中会对所有子元素进行measure过程,完成整个View树的遍历。同理,performLayout和performDraw的流程类似,唯一不同的是,performDraw的传递过程是在draw方法中的dispatchDraw来实现的,不过并没有本质区别。
DecorView作为顶级View,一般情况下内部包含一个竖直方向的LinearLayout,在这个LinearLayout中有上下两部分,上面是标题栏,下面是内容栏。在Activity中我们通过setContentView所设置的布局就是被加到内容栏中的。可通过如下代码得到我们所设置的布局;
ViewGroup content = findViewById(R.android.id.content);
content.getChildAt(0);
DecorView其实是一个FrameLayout,View层的事件都先经过DecorView,然后传递给我们的View。

理解MeasureSpec
MeasureSpec
MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指某种测量模式下的规格大小。下面看一下MeasureSpec内部的一些常量定义:
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        public static final int AT_MOST     = 2 << MODE_SHIFT;
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

SpecMode有三类,每一类都有特殊的含义:
UNSPECIFIED
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态
EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值,它对应于layoutParams中的match_parent和具体的数值这两种模式
AT_MOST
父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同的View的具体实现,它对应于LayoutParams中的Wrap_content

MeasureSpec和LayoutParams的对应关系
对于DecorView,其MeasureSpec由窗口的尺寸和其自身的L ayoutParams共同确定的;对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定的。MeasureSpec一旦确定,onMeasure中就可以确定View的测量宽和高。

普通View的MeasureSpec的创建规则如下表:(表中的parentSize是指父容器中目前可使用的大小)

 当View采用固定宽高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且其大小遵循其LayoutParams中的大小。
当View的宽高是match_parent时,如果父容器的模式是精准模式,那么View也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么View是最大模式并且其大小不会超过父容器的剩余空间。
当View的宽高是wrap_content时,不管父容器的模式是精准还是最大,View的模式总是最大化模式,并且其大小不会超过父容器的剩余空间。
(UNSPECIFIED模式是系统内部多次Measure的情况,一般来说,我们不需要关注此模式

View的工作流程
View的工作流程主要是指measure、layout、draw这三大流程。其中measure确定View的测量宽高,layout确定View的最终宽高和四个顶点的位置,而draw则将View绘制到屏幕上。
measure过程
如果是一个View,则通过measure方法就完成了其测量过程;如果是ViewGroup,除了要完成自己的测量过程外,还要遍历调用所有子元素的measure执行测量。
1)View的measure过程
View的measure过程是由measure方法来完成的,measure方法是个final类型的方法,不能重写,measure方法调用了onMeasure方法,实现如下:
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

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

 从getDefaultSize方法的实现来看,View的宽高是由specSize决定的,所以,直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身的大小,否则在布局中使用wrap_content就相当于使用match_content。解决方法如下:
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            //自定义空间最小值
            setMeasuredDimension(200,200);
        }else if (widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(200,heightSpecSize);
        }else if (heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize,200);
        }
    }
 
2)ViewGroup的测量过程
    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);
            }
        }
    }

和View不同的是,ViewGroup是一个抽象类,因此它没有重写onMeasure方法,但是它提供了一个叫measureChildren的方法。
 measureChild的思想就是取出子元素的LayoutP arams,然后通过传递进来的父容器的MeasureSpec和子元素的LayoutParams创建资源速度恶MeasureSpec,接着将MeasureSpec直接传递给子View的measure进行测量。
ViewGroup并没有定义具体的测量过程,是因为不同的ViewGroup子类(如LinearLayout、RelativeLayout)有不同的布局特性,实现细节不一样

View的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量完毕了,如果View还没有测量完毕,那么获得的宽高就是0。
解决的方法有如下:
(1)Activity/View#onWindowFocusChanged
当Activity的窗口得到焦点和失去焦点的时候都会调用一次,且View已经初始化完毕了。
(2)view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了
(3)ViewTreeObserver
使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalalayoutListener这个接口,当View树的状态发生改变或者View树内部的View的可见性发生改变时,该接口方法将会被回调。
(4)view.measure(int widthMeasureSpec,int heightMeasureSpec)
通过手动对View进行measure来得到View的宽高......
 
layout过程
layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置。

layout方法的大致流程如下:首先会通过setFrame方法来设定View的四个顶点的位置,View的四个顶点的位置一旦确定,那么View在父容器中的位置也就确定了;接着会调用onLayout方法,即父容器确定子元素的位置,和onMeasure方法类似,通过onLayout方法去调用子元素的layout方法,子元素又通过自己的layout方法确定自己的位置,这样一层层传递下去就完成了整个View树的layout过程。onLayout的具体实现同样和具体的布局有关,所以View和ViewGroup均没有真正实现onLayout方法。

在View的默认实现中,View的测量宽高和最终宽高是相等的,只不过测量宽高是形成于View的measure过程,而最终宽高形成于View的layout过程,即两者的赋值时机不同,测量宽高稍微早些。因此,我们可以认为View的测量宽高等于最终宽高,但是的确存在某些特殊情况会导致两者不同。

draw过程
View的绘制过程遵循如下几步:
(1)绘制背景 background.draw(canvas)
(2)绘制自己 onDraw
(3)绘制children dispatchDraw
(4)绘制装饰 onDrawScrollBars

View的绘制过程的传递是通过dispatchDraw来实现,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层地传递了下去。


自定义View须知:
1、让View支持wrap_content
2、如果有必要,让你的View支持平padding
3、尽量不要在View中使用Handler,没必要
4、VIew中如果有线程或者动画,需要及时停止,参考View#onDetachedfromWindow
5、View带有滑动嵌套时,需要处理好滑动冲突

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值