一步步带你解析View的工作原理(一)

前言

大三了。好久没有写博客了,之前学java时,一直将博客当做学习时的零散笔记本。而最近在看《安卓开发艺术探索》一书,想结合一些博客的阅读,对view的工作原理,进行一个梳理,总结。

这个总结系列分为两方面的内容:

1.介绍View的工作原理。

2.介绍自定义View的实现方式。

 

学习View的工作原理,是为了更好的理解自定义view做准备的,我们要学的内容有:View的三大流程、常见的回调方法、一些特殊的自定义view的冲突处理等等。

在介绍View的三大流程 【测量(measure)、布局(layout)、绘制(draw)】之前,先了解一些基本概念:

1.DecorView

2.ViewRoot (对应于ViewRootImpl类)(View的三大流程都是通过其完成)

3.MeasureSpec

 

1.DecorView与ViewRoot

DecorView是什么?

是Windows中的View的最顶层的view,下面的图能帮我们很好的理解:

知识点在图中已经体现的非常直观,要说的就是一个小点:顶级View中包含一个LinearLayout,LinearLayout的下半部分即内容栏,我们在活动中通过setContentView所设置的布局文件其实就是加到此内容栏中的,因此也不能理解为Activity指定布局的方法叫setContentView。

DecorView和ViewRoot的关系?

书中原话说:“在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Windows中,同时会创建ViewRootImpl对象,并将ViewRootImpl和DecorView相关联。”

这是怎样一个过程呢?查阅源码ActivityThread的handleResumeActivity方法:

final void handleResumeActivity(IBinder token,
    boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason){
         ..........................
if (r.window == null && !a.mFinished && willBeVisible) {
                //获取当前活动的Window
                r.window = r.activity.getWindow();
                //获取DecorView对象
                View decor = r.window.getDecorView();
                //可见度设置
                decor.setVisibility(View.INVISIBLE);
                //获取windowmanager对象
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (r.mPreserveWindow) {
                    a.mWindowAdded = true;
                    r.mPreserveWindow = false;
                    // Normally the ViewRoot sets up callbacks with the Activity
                    // in addView->ViewRootImpl#setView. If we are instead reusing
                    // the decor view we have to notify the view root that the
                    // callbacks may have changed.
                    ViewRootImpl impl = decor.getViewRootImpl();
                    if (impl != null) {
                        impl.notifyChildRebuilt();
                    }
                }
                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true; 
                        //将DecorView添加到Window中
                        wm.addView(decor, l);
                    }
                ..........................
}

可以看到,这个方法:

1.获取了DecorView对象 

2.将DecorViewt添加到Window中

我们继续跟踪addView方法:

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {

        ............

        ViewRootImpl root;
        View panelParentView = null;

        ............

        //获得ViewRootImpl对象root
         root = new ViewRootImpl(view.getContext(), display);

        ...........

        // do this last because it fires off messages to start doing things
        try {
            //将传进来的参数DecorView设置到root中
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
          ...........
        }
    }

这个方法:

1.创建了VIewRootImpl对象

2.通过root的setView方法将顶层view设置到了root中。

我们通过继续的跟踪,可以发现:DecorView从ViewRootImpl的setview这个方法传入之后,会经过requestLayout()方法、scheduleTraversals()方法、doTraversal()方法,然后进入到performTraversals()方法。,而performTraversals()方法非常关键,它会依次调用performMeasure()方法,performLayout()方法,performDraw()方法对我们的DecorView进行测量、布局、绘制。这便是DecorView和ViewRootImpl之间的关系了。

介绍到这里,我们也可以清楚地了解到ViewRoot起的作用:View的绘制过程,是从ViewRoot的performTraversals方法开始的。有些同学会问:“你这只是一个DecorView,怎么就扩展到了所有的View呢?”

我们可以回头看看文章的第一个图:DecorView作为顶层视图,我们在xml内容布局中的添加的所有布局会解析成View树形结构添加到DecorView顶层视图中id为content的FrameLayout父容器上面,就是图里那个下半部分的框框。

所以,我们要理解:DecorView不是一个人光秃秃的跑进performTraversals()方法的,他是一个View树。简单说,DecorView作为老大哥,带了一大群小弟跑进performTraversals(),然后大家都通过测量、布局、绘制到我们的屏幕上了。

还有一个细节就是:既然这是一个View树,先让所有元素都经过measure、layout、draw。那就离不开递归,事实也正是如此:perforMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,这时候measure流程就从父容器传到子元素当中了,接着子元素会重复父容器的measure过程。。。如此反复,完成遍历。另一个布局流程和绘制流程亦然。

好了,DecorView和ViewRootImpl就介绍到这里。接下来我们看看MeasureSpec。

 

2.MeasureSpec

字面上看,MeasureSpec就是“测量规格”,但我更喜欢将其理解为“测量约束+规格”

我们来看我们不断提到的performTraversals()方法:

private void performTraversals() {
        // cache mView since it is used so much below...
        //mView就是DecorView根布局
        final View host = mView;
        //成员变量mAdded之前赋值为true,因此条件不成立
        if (host == null || !mAdded)
            return;
        //是否正在遍历
        mIsInTraversal = true;
        //是否马上绘制View
        mWillDrawSoon = true;

        .............
        //顶层视图DecorView所需要窗口的宽度和高度
        int desiredWindowWidth;
        int desiredWindowHeight;

        .....................
        //在构造方法中mFirst已经设置为true,表示是否是第一次绘制DecorView
        if (mFirst) {
            mFullRedrawNeeded = true;
            mLayoutRequested = true;
         //如果窗口的类型是有状态栏的,那么顶层视图DecorView所需要窗口的宽度和高度就是除了状态栏
            if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
                    || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
                // NOTE -- system code, won't try to do compat mode.
                Point size = new Point();
                mDisplay.getRealSize(size);
                desiredWindowWidth = size.x;
                desiredWindowHeight = size.y;
            } else {//否则顶层视图DecorView所需要窗口的宽度和高度就是整个屏幕的宽高
                DisplayMetrics packageMetrics =
                    mView.getContext().getResources().getDisplayMetrics();
                desiredWindowWidth = packageMetrics.widthPixels;
                desiredWindowHeight = packageMetrics.heightPixels;
            }
    }
............
//获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.widthhe和lp.height表示DecorView根布局宽和高
 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
 int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

  // Ask host how big it wants to be
  //执行测量操作
  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

........................
//执行布局操作
 performLayout(lp, desiredWindowWidth, desiredWindowHeight);

.......................
//执行绘制操作
performDraw();

}

我们可以在倒数第三四五行(不算省略号和大括号!)可以清楚的看到:MeasureSpec参与了view的测量。

那么,我们先问一个问题,为什么要用到MeasureSpec?

我们知道,我们是可以给View设置LayoutParams的,但是,我们设置的LayoutParams如果不在父容器约束之下,那岂不是乱套了吗?简单说,就是父容器的大小是50,而你给他的小弟却设置了51,这怎么行呢?是吧?

其次,我们还要知道一点:只有View的MeasureSpec确定后,onMeasure中才可以确定view的测量宽/高。

那该如何确定某个View的MeasureSpec呢?

首先,我们要知道,MeasureSpec其实是一个32位int值,高两位代表SpecMode,低30位表示SpecSize。并且,它是由View的LayoutParams和这个View的父容器的MeasureSpec协商决定的。他们协商之后,会将LayoutParams在父容器的约束之下,转换成对应的MeasureSpec。

SpecMode有三类

1.UNSPECIFIED  随便模式

2.EXACTLY          具体值模式

3.AT_MOST         最大值模式

 

先拿DecorView来作例子:DecorView已经是顶级View了,那么它的父容器是什么呢?——窗口。

正如上面的代码:(我又截取到这了)

//如果窗口的类型是有状态栏的,那么顶层视图DecorView所需要窗口的宽度和高度就是除了状态栏
            if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
                    || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
                // NOTE -- system code, won't try to do compat mode.
                Point size = new Point();
                mDisplay.getRealSize(size);
                desiredWindowWidth = size.x;
                desiredWindowHeight = size.y;
            } else {//否则顶层视图DecorView所需要窗口的宽度和高度就是整个屏幕的宽高
                DisplayMetrics packageMetrics =
                    mView.getContext().getResources().getDisplayMetrics();
                desiredWindowWidth = packageMetrics.widthPixels;
                desiredWindowHeight = packageMetrics.heightPixels;
            }
............
//获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.widthhe和lp.height表示DecorView根布局宽和高
 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
 int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

DecorView的MeasureSpec是由窗口和DecorView的宽高共同决定的。(他们都传入了getRootMeasureSpe方法)

那我们再跟踪getRootMeasureSpe方法:

 private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
      //匹配父容器时,测量模式为MeasureSpec.EXACTLY,测量大小直接为屏幕的大小,也就是充满整个屏幕
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;


        //包裹内容时,测量模式为MeasureSpec.AT_MOST,测量大小直接为屏幕大小,也就是充满整个屏幕
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;


        //其他情况时,测量模式为MeasureSpec.EXACTLY,测量大小为DecorView顶层视图布局设置的大小。
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

好了,通过以上代码,DecorView的MeasureSpec的产生过程就非常清晰了。

对于普通View来说,View的measure过程由ViewGroup传递而来,先看一下ViewGroup的measureChildWithMargins方法:


    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {

        //调用child.getLayoutParams()获得子视图的LayoutParams属性
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        
        //调用两次 getChildMeasureSpec()函数,分别计算出孩子视图的宽度和高度的 Spec
        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);

        //调用child.measure()函数,确定子视图的最终布局大小。
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

每一行代码我都加了详细的注释,很显然,子元素的MeasureSpec的创建也是由父容器的MeasureSpec和子元素本身的layoutParams决定的,此外,因为这是measureChildWithMargins方法,View的margin和padding也参与了决定。源码中还有measureChildren(),measureChild() 可以参考阅读。

很明显,getChildMeasureSpec()返回了我们子元素宽高的MeasureSpec,胜利在望了!我们继续跟踪!

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //通过父类传进的MeasureSpec确定父类的mode和size
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
           
        //因为有padding 因此子元素的可用空间是父类的size - padding
        int size = Math.max(0, specSize - padding);
        
        //子类size的初始化
        int resultSize = 0;
        //子类mode的初始化
        int resultMode = 0;
        
        //传进父类的mode
        switch (specMode) {

        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY: //父类说:我是精确值模式的,你也得是精确值模式
            if (childDimension >= 0) {//子类说: 好啊 反正我刚好设置了具体值
                resultSize = childDimension;   //将size设置为设定的值
                resultMode = MeasureSpec.EXACTLY; //将mode设置为精确值模式
            } else if (childDimension == LayoutParams.MATCH_PARENT) {//另一种情况 子类说:卧槽我没设置过具体值啊 我设置的match_parent 我想和你一样大!
                // Child wants to be our size. So be it. //这句英文看的懂吧
                resultSize = size;//父类说:你既然想和我一样大 那剩余空间都给你了 (size是父类的剩余空间)
                resultMode = MeasureSpec.EXACTLY;//这也是精确值模式 因为size是个具体值
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {//又一种情况 子类说:卧槽我没设置过具体值啊 我设置的是wrap_content 我想自己能多大就多大
                // Child wants to determine its own size. It can't be
                // bigger than us.//父类说:可以 但是你不能比我大 我最多就把剩余空间都给你
                resultSize = size;//剩余空间都给子类了
                resultMode = MeasureSpec.AT_MOST;//非具体值,所以给最大值模式。
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST://父类说:我是最大值模式的 你也得是最大值模式
            if (childDimension >= 0) {//子类:我设置了具体值
                //父类:好吧 那你就具体值模式吧
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        //父类:我对你没要求 你想怎样就怎样 这一般表示一种测量的状态
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

代码不难,也标有英文的注解,我也加了中文的注释,它的主要作用就是通过父与子的协商确定子元素的size和mode,然后将size和mode打包成MeasureSpec,我之前说了,MeasureSpec就是size和mode打包成的。

看完了注释,MeasureSpec由父子协商决定的意思也很明确了。

总结一下:这里分别将了DecorView的MeasureSpec的决定过程,还有普通view的MeasureSpec的决定过程。知道了MeasureSpec的决定之后,我们接下来就能进行View的measure和layout还有draw这三大流程啦!(下次再见)

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值