[android 自定义控件](4)View的绘制流程

这几天研究了自定义View,因为东西太多了,所以开一片博客当作笔记,也共大家学习。

准备

稍微学习过android的人都知道,View是一个神奇的东西,所有的可见物都是View。当我们开始接触View时我们就知道了LinearLayout等布局,TextView等组件都是直接或者见解继承View实现的。那么问题来了如果官方给的View不够酷炫时。那么怎么办呢?当然时自定义View了。但是自定义View是非常复杂的。你必须对Application Framework层深入了解才可以。这样会深入anroid 的方方面面。下面是大神们的学习心得,本人感觉不错推荐给大家
https://www.zhihu.com/question/46486807

下面是是阅读源码是可能遇到的问题的解决
http://kaedea.com/2016/02/09/android-about-source-code-how-to-read/

什么是View

This class represents the basic building block for user interface components. A View occupies a rectangular area on the screen and is responsible for drawing and event handling
从官方的定义中我们知道

  1. View是和用户接口的基本单位(写给用户看的)
  2. 它是一个矩形空间
  3. 它负责画和处理事件

View都有些什么

点开VIew你可能大吃一惊,2万多行,你可能到现在都没有写过怎么多代码。但是没关系。我们慢慢来,总结总结就没什么了

View中的内部类

View.OnXXXX
//以On开头都是监听类,里面有各种各样的时候进行回调

View.MeasureSpec
//这个类知道点View绘制过程的人应该都知道这个东西,不说了

View.DragShadowBuilder
//DragShadowBuilder 允许你指定拖拽操作的效果。

View.BaseSavedState
// onSaveInstanceState()中希望保持的状态的积累

View.AccessibilityDelegate
//这个是android 无障碍设计使用的类,所谓无障碍就是当视觉、肢残和老龄化用户激活设备中的无障碍服务和特性。这个本人不了解不多言了

View中回掉函数

onDetachedFromWindow
//Activity退出,或者View被remove时被调用

onAttachedToWindow
//和上面对应,当Activity载入,或View被载入Activity中时调用

View绘制流程

View树的绘制一切源头都在ViewRootImpl中的performTraversals()方法里面 。 这个类实现ViewParent接口,意味着它和ViewGroup一样管理者View的一切。根View DecorView 就添加在其中。且自己被WindowManager管理,WindowManager和WMS(window Manger Sever)交互.看过我写的android系统结构你会发现WMS是android 框架的一部分,WMS负责处着窗口服务。这是一个天坑(新世界?,有机会是一定要深入的)。所以大家知道ViewRootImpl管理着根View就行了。正常情况下ViewRootImpl在ide中是不对你开放的,有两种方法可以看,一是在SDK中找到。但是这些源代码是没有缩进的,还有一个方法就是在AOSP上看。下面是是地址,有兴趣的朋友可以参考一下
https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewRootImpl.java

这个源码真的不要太长啊!

private void performTraversals() {
        ......
        //最外层的根视图的widthMeasureSpec和heightMeasureSpec由来
        //lp.width和lp.height在创建ViewGroup实例时等于MATCH_PARENT
        // mWidth和mHeight 是phonewindows对象传递过来的。更具平台和应用的类别传递窗口的大小。一般为手机屏幕的大小
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        //测量
           performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ......
       // 布局
        performLayout(lp, mWidth, mHeight);
        ......
        //画
        performDraw();
        ......
    }

上面的三个方法将会最终分别调用下面这些方法(说惭愧,因为sourcce insight不会用加上学校网太慢了没法下源码,所以结果是在网上搜的,请见谅 )

//mView就是我们的DecorView(View树的根)
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
//
mView.layout(0, 0, mView.getMeasuredWidth();
mView.layout(0, 0, mView.getMeasuredHeight();
//
mView.getMeasuredHeight());
mView.getMeasuredWidth());
//
mView.draw(canvas);

从上面的measure的参数你可以看见它传入了两个参数childWidthMeasureSpec和childHeightMeasureSpec。字面意思为子类宽,高测量标准。这个是什么呢?它是一个值,这个值主要用在ViewGroup中,你还记得xml中写的 android:layout_width和android:layout_height吗,你可能不理解android 是怎么实现控件的状态依赖于父容器的。而这个值就是完成这个工作的。下面我们就来解释这个。

childHeightMeasureSpec这个值是一个View.MeasureSpec类的makeMeasureSpec方法返回来的,其实这个类也就这一个关键的函数

  public static class MeasureSpec {
  
   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);
            }
        }
        //getSize()
        //getMode()

}

从上面你可以看见如果用户不破环制作测量标准的话(当然不会了),makeMeasureSpec返回了一个int类型的值,这个值我们一般也称它为MeasureSpec(记住不要和MeasureSpec类搞混了),而MeasureSpec值是一个32位的值,高2位我们叫specMode,低30位是specSize
那么我们来看看Mode都有哪些值吧

  1. EXACTLY
    父容器已经测量出子View所需要的精确大小了,这个时候View的大小就是spectSize了,对应与LayoutParams的指定值和match_parent
  2. AT_MOST
    父容器制定一个可用的大小spectSize,View的大小不能大于这个值。具体大小有View自己决定,对应与LayoutParams的wrap_content
  3. UNSPECIFIED
    父容器不对子View有任何限制,想要多大给多大,一般系统内部使用该测量状态

看了上面你可能更加懵了,每个字的意思我都懂,可是拼在一起之后就不知道什么意思了呢。现在我来看看这个值是怎么用的呢。

先从根View看:
performTraversals()中getRootMeasureSpec方法会获得根视图的MeasureSpec值,源码如下

   private static int getRootMeasureSpec(int windowSize, int rootDimension) {
   //进来函数之后 你要知道windowSize等于手机窗口的大小,rootDimension等于MATCH_PARENT
        int measureSpec;
        switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            //自然而然我们就走到这了
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            //从上面我们可以知道measureSpec 是一个MeasureSpec.EXACTLY+windowSize(窗口大小)的组合值
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

根View的measure最后接受的measureSpec 值是一个MeasureSpec.EXACTLY+windowSize(窗口大小)的组合值(这句话很重要)。接着我们看看根View的measure方法。那么问题来了根View的measure该在哪看呢,还记得我之前说根View是一个FrameLayout吗。那么我们去FrameLayout中找measure看看,可是没有啊,那么我们去它的父类ViewGroup看看,可是还是找不到,没办法接着去View中找,这时候终于找到了。可以看见该方法位final ,所有子类是不能继承的,所以我们一直找不到。

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
			 //防止size<0
        ......
        //回调onMeasure()方法
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        //记录一些状态
        ......
    }
    

onMeasure

这个onMeasure就是我们自定义控件时需要操作的类。下面onMeasure的源码是FrameLayout中的。其中出现的函数都是我们需要掌握的。等掌握了这些最基本的函数,我们就能游刃有余创建我们自己的布局了,请仔细理解。

    //如果对于根View,MeasureSpec值为MeasureSpec.EXACTLY和窗口大小的组合值
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	    //获得ViewGroup中有多少个子View(不包含嵌套)
        int count = getChildCount();
		  //根View时measureMatchParentChildren 一定为false
		  
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
      //这个mMatchParentChildren是一个 arrylist,他存储这那些子View可以被FrameLayout显示。我们先清空它,为存储做准备。
        mMatchParentChildren.clear();
  

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;
  	//找到最大的子View
        for (int i = 0; i < count; i++) {
     
            final View child = getChildAt(i);
            // mMeasureAllChildren是否测量子View的标志,为了优化性能所以默认为false,
            //只要是子View是GONE状态的(隐藏控件,即不保留控件所占有的空间)就会跳过测量工作。
            //而visible状态(显示控件,控件可点击)和invisible状态(不显示控件,但保留控件所占有的空间,控件不可点击) 就会进入测量状态
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                        //核心代码1
                        //
                        //
                        //是一个核心函数,也是一个大坑,它会遍历child下的所有组件,而且是考虑Margins(外边距的),来完成们每一个View的测量工作,
                        //这样就方便FramLayout评估自己应该多大了。我们之后详细说
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                // 下面就要计算FrameLayout本身的大小了,
                //因为FrameLayout本身时一堆组件堆叠在一起,所以下面的maxWidth和,maxHeight会反复被最大值覆盖、
                //最终的值就是最大child的大小了(实际大小+Margin)
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                
                //FrameLayout不希望自己有固定的大小,而是根据其子控件的大小自由变换,它自己可以变得比父容器还大。
                if (measureMatchParentChildren) {
               //这个if块解释了,如果添加的子View如果高和宽都不是MATCH_PARENT时不会在FrameLayout中显示的
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                             //想要FrameLayout能显示view 至少满足两点
                             //1 父元素不能时有确切大小,下面会讲什么是确切大小
                             //2添加进去的子控件宽和高都MATCH_PARENT
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }
		//下面的方法会考虑很多事情,以最终确定FramLayout的大小
        // Account for padding too
        //最大子View的大小+最大子View外边距大小+自身内边距大小
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width
        //xml设置的最大大小和自己算出的最大大小选最大值
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width
        //在和绘制的前景大小比较,选最大
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }
//setMeasuredDimension这个方法非常非常的重要。
//所有的measure工作,最后都是调用这个方法为每一个View中的mMeasuredWidth和mMeasuredHeight变量赋值,这个变量就是我们的测量的大小存储的地方
//只有赋值之后才能呢更使用getMeasuredWidth,getMeasuredHeight获得该值
//而下面的resolveSizeAndState方法也是我们要分析的方法,之后会详细说*/
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
//进过上面的一系列操作之后,我们的onMeasure最终完成了对FrameLayout的大小的测量,且个个子View也有了自己的测量大小了
	//下面的代码又会去测量子View的。你可能会疑问,之前不是已经测量每一个View了吗,为什么还要测量。
	//看完draw你就懂了
	//下面的代码会重新测量那些需要在FrameLayout上显示的View的大小
        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//测量宽
                final int childWidthMeasureSpec;
                if (lp.width == LayoutParams.MATCH_PARENT) {
                //如果子View是LayoutParams.MATCH_PARENT,那么子类会填充大小就包括前景的padding和FrameLayout的padding和margin
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                //反之不包括上面的区域
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }
//同宽
                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) {
                    final int height = Math.max(0, getMeasuredHeight()
                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }

//可以显示的子类传递MeasureSpec方便他们做测量,调用View的measure方法,measure像之前介绍的一样调用View的onMeasure
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

一般人看完上面一定是比较晕的的,因为上面很多操作是一个递归的操作,想想那些算法题你就知道递归是一个多神奇的东西了。上面的内容你不需要全部了解,但是你必须知道下面的知识点(如果不知道请回去仔细阅读)

从官方给根View测量的源码中我们可以看出,其中的View绘制流程是一个递归的过程,父级VIew调用measure(),measure()调用onMeasure()。而所有测量逻辑都实现在这个这个onMeasure()中,它干两件事。

  1. 是测量出自己有多大,那么怎么测量出自己大小呢,各个View都有自己的逻辑。不过最终一定会调用setMeasuredDimension()方法来设置测量大小。
  2. 告诉子级View该怎么测量。那么子View该怎么测量呢,通过传递给子ViewmeasureSpec 值(这个很关键,后面详细说)。

子View的measure函数调用onMeasure()。这样又进入子View的onMeasure(),同样干两件事一是测量出自己有多大,而是告诉孙View该怎么测量……循环往复最后进入View树最上层的View了,它只干一件事:我有多大(调用setMeasuredDimension设置)。就这样就完成了整个View树的测量过程了。

说了半天你可能想打我了,我还是不知道MeasureSpec 值怎么解决递归测量的,不要担心我现在就说。我们回到FrameLayout中填上我留下的坑

核心代码1:measureChildWithMargins

measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

注意上面的这个方法实现在ViewGroup中,ViewGroup给我实现了很多用来测量所有子View的方法,这个只是其中的一个

 protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
            //parentWidthMeasureSpec就是MeasureSpec值他是Mode+size的组合值
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//获得子类的LayoutParams类,其中保存者xml中layout_xxxxx属性
//核心函数1.1
//
//
//看见了重要的函数了。理解getChildMeasureSpec我们就能理解控件如何依赖于父容器来设定自身大小
        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,如果chile是一个非VIewGroup的话,那么一般就会运行默认的onMeasure测量自己结束
//反之会onMeasure还要告诉其子View怎么测量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

getChildMeasureSpec函数是整个测量的核心了

   public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
   //spec为父类传来的MeasureSpec值,padding内外边距和+withUsed大小,
   //childDimension为MATCH_PARENT,WRAP_CONTENT,或者具体值
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
		//size为减去内外边距和之后的值
        int size = Math.max(0, specSize - padding);
		
        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        //
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } 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 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);
    }

方法虽然长但是都很简单,上面的if变成表就是下面这个样子

childLayoutParams\parentSpecModeEXACTLYAT_MOSTUNSPECIFIED
dp/dxEXACTLY,ChaildsizeEXACTLY,ChaildsizeEXACTLY,Chaildsize
match_parentEXACTLY,ParentsizeAT_MOST,ParentsizeUNSPECIFIED,0
warp_contentAT_MOST,ParentsizeAT_MOST,ParentsizeUNSPECIFIED,0

现在我们来说一说如何通过MeasureSpec值和LayouParams来确定View的大小呢。
LayouParams的参数干什么的呢?它告诉android系统 我希望填充 父元素中的全部的区域(match_parent),或者按照我本身的大小填充父元素(warp_content),按照制定大小填充父元素(设定值)。那么我们怎么理解MeasureSpec呢?想要理解这个我要知道android 为什么要创造这个类。首先我们假设MeasureSpec只有specSize这个值,再假有下面这个场景

mView(根VIew)->VIewGroup->View
现在假设如果VIewGroup的LayouParams是dx/dp,warp_content,match_parent其中的一种,View是一个笃定胆小的控件。我们要怎么实现绘制呢?mView传递过来了specSize(假设MeasureSpec中只有specSize),这个时候可以写个3个if,当是dx/dp是可以把这个值代替到specSize传给VIew。View根据这个值来测量大小,当是warp_content,我们可以不管specSize的大小,直接返回View的默认大小,当是match_parent我们可以把specSize交给子View。这样好像也可以实现绘制?你可能会心一笑,有怎么简单嘛?其实就是怎么简单,怎么然View知道我们的需求。我们可以传递一负值来表示这个状态但是。当然我们可以学习官方再MeasureSpec加入一个状态值specMode 来完成这件事呢,用EXACTLY来表示 dp/dx和 match_parent,用AT_MOST表示 warp_content,UNSPECIFIED保留给系统使用

所以MeasureSpec的作用: 当有ViewGroup嵌套时,specMode传递前一个ViewGroup的LayouParams状态,specSize传递前面一个ViewGroup分配的大小

而上面的那张表表示的是多个ViewGroup嵌套时,MeasureSpec的变化,对应这下面的这种情况

mView(根VIew)->VIewGroup1->VIewGroup2->……->View

总结:

子控件如果是warp_content 就把specMode设置成AT_MOST,如果是dp/dx时 就把specMode设置成 EXACTLY,其他情况下不变

通过上面的学习我们知道在View树递归测量中最后的VIew只干一件事,就是测量自身大小。那么我们来看看默认情况下,最后一个View是怎么测量自身大小的吧,如果子View不重写onMeasure()的话,默认的测量实现就在其中

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

和我们之前分析FrameLayout说的一样,onMeasure最终调用了setMeasuredDimension方法,在说明我们还要弄清楚getDefaultSize方法和getSuggestedMinimumWidth方法。从嵌套层次一步一步来。

getSuggestedMinimumWidth

 protected int getSuggestedMinimumWidth() {
 //这个mMinWidth就是我们在xml中使用minWidth设置的大小
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

现在我们就知道他是选择背景和mMinWidth的最大值

getDefaultSize

   public static int getDefaultSize(int size, int measureSpec) {
   //measureSpec是父级传下来的MeasureSpec,里面包含只SpecMode,和父级给予的大小值,size就是getSuggestedMinimumWidth返回的值
        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;
    }

从上面你可以看出父类传递过来的SpecMode==UNSPECIFIED的话(这一般是系统内部使用的标记,从mView的specMode为EXACTLY可以看出来)就是getSuggestedMinimumWidth返回值的的大小(背景色下和xml中mmMinWidth两者的的最大值),否则就是SpecSize的值(也就是父类给的大小),


protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
//.....
}

这个就不多说了,设置 mMeasuredWidth 和mMeasuredHeight 这是一个宽度/高度值,不是一个MeasureSpec,它的大小是getDefaultSize返回值(一般情况就是父级传递下来MeasureSpec中的给予size)

这样我们的View树的测量就完成了,之后就可通过getMeasuredWidth,getMeasuredHeight获得SpecSize了
getMeasuredWidthAndState,getMeasuredHeightAndState获得MeasureSpec

总结:

  1. measure的过程是h递归的过程,**如果我们像创建ViewGroup的话那么一定要重载OnMeasure方法,**不然会调用View中的默认OnMeasure。可以看到它只是单纯的设置了一个给ViewGroup设置了测量大小,而没有为ViewGroup的子类设置测量大小。对于这个有两种方法。
    第一种:
    getChildMeasureSpec()方法child.measure()方法的配套使用最大化定制测量
    第二种:
    调用相关的测量函数
    measureChildren 为所有子View一次性赋值
    measureChild 为单个View赋值
    measureChildWithMargins 考虑内外边距的赋值
  2. 从分析FrameLayout我们可以发现 在如果在OnMeasure中获得的获得MeasureSpec是不准确的,因为OnMeasure可能被多次调用,说一最好在OnLayout中调用测量值
  3. Activity的生命周期和View的measure不同步的。意思是说你不能在Activity的onCreate或者onResume中准确使用用测量值
    1)解决办法重载Activity中实现了Window.Callback接口的onWindowFocusChanged()方法,这个方法在失去或者获得焦点之后被调用,这时候 View初始化已经完毕,但是注意这个函数可能会被平凡调用
    2)view.port会在像消息队尾部放一个runnable,等Looper调用时,View已经初始化完毕了
    3)ViewTreeObserver的众多回掉接口可以完成这些事情
    4)手动调用onMeasure,这个方法使用起来好坑,除极端情况下不然建议不要使用

Layout

写到这其实已经想吐了,但是没办法,自己开的博客,死了也要写完。当测量完,之后接着就是Layout了。首先当然是mView(根View)的Layout,但是我们在FrameLayout中找不到layout方法,接着去ViewGroup中寻找,果然和measure一样在View中

    public void layout(int l, int t, int r, int b) {
    //传进来的是左上角和右下角的坐标,和measure一样这个函数一会被多次递归
  
    //对于根View来说就是就是窗口的大小坐标
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
//之前的的位置值先存起来,监听回掉需要用
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
//setFrame非常重要的函数,它会判断布局是否改变,从而优化性能
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        //和测量过程一样,关键还是会调用onLayout函数
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
//如果布局改变了会调用监听,注意这个监听是在布局改变之后被调用
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                //回掉监听
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

上面说了setFrame是关键代码所以我们要看看它的源码

    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
		……
        //下面是关键代码,判断之前的位置是否和现在的一样
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

	        ……
//这块就很关键了,这样此View的布局位置就被存储下面的4个值中了,而我们的getwidth和getheight就是其中值的相减所得
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
         ……
        }
        return changed;
    }

接着我们来看看FramLayout中的onLayout

   protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

接着看吧

    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();
//下面会获得FrameLayout的中的可用大小
        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
            //获得子类的LayoutParams 
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;
//获得子类LayoutParams 的gravity值
                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }
//下面就是FrameLayout的主要逻辑,大概的意思就是根据布局方向和子View的gravity值来设置具体的布局信息
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }

                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }
//看见核心的东西了,下面的代码会把上面生成的布局信息传递给子View的layout方法
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

所有的ViewGroup都实现了自己的onLayout方法,当我们想发明自己的布局时候,其实也就是对onLayout方法进行重载,来完成你想要的布局,现在我们来看看onLayout递归到View树的最底下时,默认的onLayout长什么样子吧

  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

是空的,也是。都是最底下的View了也不需要分配这些位置信息给谁了,自己的布局位置已经在Layout中被存起来了。
我们来看看ViewGroup的onLayout

    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

可见这个方法必须要实现的(不然自定义个毛布局)。这也是唯一一个必须要实现的ViewGroup方法,我们布局分配的逻辑就在这里面写。
总结:

  1. 我在layout中存储布局位置信息,在onLayout中分配布局位置信息
  2. layout方法不是final的(你可能没发现),这样子类就有可能重写它,来改变传入的onlayout中的参数值,如下
public final void layout(int l, int t, int r, int b){
	super.layout( l+100,  t+100, r+100,  b+100)
}

虽然不知道干什么,但是完全有可能

  1. 下面的这些函数会获得想要的位置值
    getTop,getLeft,getBottom,getRight
  2. OnLayoutChangeListeners监听类的onLayoutChange方法会在布局改变之后回掉

不知不觉Layout的部分已经完了,我们赶紧趁热打铁到draw部分吧

draw

经过我的寻找发现只有View中才有draw(canvas)方法

  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
        ......
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        ......

        // Step 2, save the canvas' layers
        ......
            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }
        ......

        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers
        ......
        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, right, top + length, p);
        }
        ......

        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
        ......
    }

接下来我们按照官方的步骤来看源代码吧
1. Draw the background
经过寻找我们发现drawBackground是定义在View中

   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. If necessary, save the canvas’ layers to prepare for fading
这个我们不关心所以跳过了

3. Draw view’s content
经过寻找我们发现onDraw是定义在View中

  protected void onDraw(Canvas canvas) {
    }

看见没有是一个空方法,那么它的意思就是交给继承View的组件自己实现了
4. Draw children
经过寻找我们发现dispatchDraw是定义在在ViewGroup中进行了定义

  @Override
    protected void dispatchDraw(Canvas canvas) {
        ......
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        ......
        for (int i = 0; i < childrenCount; i++) {
            ......
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ......
        // Draw any disappearing views that have animations
        if (mDisappearingChildren != null) {
            ......
            for (int i = disappearingCount; i >= 0; i--) {
                ......
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ......
    }

可以看见该方法内部会遍历每个子View,然后调用drawChild()方法,我们可以看下ViewGroup的drawChild方法,如下:

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

好了如果到了最低层View,这时候我们就不需要传递draw信息了,所以我们看看View中的dispatchDraw

  protected void dispatchDraw(Canvas canvas) {

    }

果然是一个空方法,不需要干任何事情
5. If necessary, draw the fading edges and restore layers
这块我们也不关心,所以跳过
6. Draw decorations (scrollbars for instance)
这块我们也不关心。所以跳过

好了到现在为止我们终于说完View的绘制过程了。

invalidate绘制

invalidate 是UI线程使用的,如果在非UI线程中想要绘制请使用postInvalidate

invalidate的方法重载由很多,但是最终都会进入下面的函数

public void invalidate(int l, int t, int r, int b) {
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
    }

紧接着来看invalidateInternal

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
        if (mGhostView != null) {
            mGhostView.invalidate(true);
            return;
        }
//kipInvalidate()方法来判断当前View是否需要重绘,判断的逻辑也比较简单,如果View是不可见的且没有执行任何动画,就认为不需要重绘了
        if (skipInvalidate()) {
            return;
        }
//进行透明度的判断,并给View添加一些标记位
        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
                || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
                || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
                || (fullInvalidate && isOpaque() != mLastIsOpaque)) {
            if (fullInvalidate) {
                mLastIsOpaque = isOpaque();
                mPrivateFlags &= ~PFLAG_DRAWN;
            }

            mPrivateFlags |= PFLAG_DIRTY;

            if (invalidateCache) {
                mPrivateFlags |= PFLAG_INVALIDATED;
                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            }

            // 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);
                
            //核心代码1
            //
            //
            //把重绘的区域传递给父级view
                p.invalidateChild(this, damage);
            }

            // Damage the entire projection receiver, if necessary.
            if (mBackground != null && mBackground.isProjected()) {
                final View receiver = getProjectionReceiver();
                if (receiver != null) {
                    receiver.damageInParent();
                }
            }
        }
    }

核心代码1除的对象要满足ViewParent接口,我们发现ViewGroup是满足条件的


public abstract class ViewGroup extends View implements ViewParent, ViewManager {}

接着看ViewGroup中的invalidateChild。(尽然被遗弃了?)

 /**
     * Don't call or override this method. It is used for the implementation of
     * the view hierarchy.
     *
     * @deprecated Use {@link #onDescendantInvalidated(View, View)} instead to observe updates to
     * draw state in descendants.
     */
    @Deprecated
    @Override
    public final void invalidateChild(View child, final Rect dirty) {
      //..

        ViewParent parent = this;
     //..
            do {
          		//..

                parent = parent.invalidateChildInParent(location, dirty);
               //...
            } while (parent != null);
        
    }

看这个方法会不停的调用父View的invalidateChildInParent,直到invalidateChildInParentf返回null才退出。所以最后一站就在ViewRootImpl中。

    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
   /。。。。
        invalidateRectOnScreen(dirty);

        return null;
    }

接着看

 private void invalidateRectOnScreen(Rect dirty) {
      //。。。。
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            scheduleTraversals();
        }
    }
        void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            //mTraversalBarrier确保绘制懂唯一性,优化性能
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //android 4.1引入的新机制,把绘制事件mTraversalRunnable打包成绘制消息给mChoreographer处理
            //它会根据一系列的复杂逻辑判断是否调用相应的绘制事件。
            //这样可以防止绘制事件太多了,导致卡死的问题。这里的坑太大了,我也没有搞明白。就不多赘述了
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

看见这里调运的scheduleTraversals这个方法吗?scheduleTraversals最终通过调用mTraversal的doTraversal方法来绘制。

   void doTraversal() {
//。。。

            performTraversals();

//。。。

最后回到performTraversals()执行重绘。

invalidate总结

调用视图的setVisibility()、setEnabled()、setSelected()等方法时都会导致视图重绘,而如果我们想要手动地强制让视图进行重绘,可以调用invalidate()方法来实现。当然了,setVisibility()、setEnabled()、setSelected()等方法的内部其实也是通过调用invalidate()方法来实现的。

View调用invalidate方法的实质是层层上传到父级,直到传递到ViewRootImpl后触发了scheduleTraversals方法,每向上传递一次就重新包装血药绘制的区域参数。然后整个View树开始重新绘制(测量,布局,绘制)

postInvalidate手动触发绘制

非UI线程想要重新绘制控件就需要postInvalidate

 /**
     * <p>Cause an invalidate to happen on a subsequent cycle through the event loop.
     * Use this to invalidate the View from a non-UI thread.</p>
     *
     * <p>This method can be invoked from outside of the UI thread
     * only when this View is attached to a window.</p>
     *
     * @see #invalidate()
     * @see #postInvalidateDelayed(long)
     */
    public void postInvalidate() {
        postInvalidateDelayed(0);
    }

再来看postInvalidateDelayed函数

    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;
        if (attachInfo != null) {
        核心,实质就是调运了ViewRootImpl.dispatchInvalidateDelayed方法
            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消息,再来看看处理歇息的类

  final class ViewRootHandler extends Handler {
   @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MSG_INVALIDATE:
                ((View) msg.obj).invalidate();
                break;
                //。。。
               

最终还是调用iViewRootImpl的nvalidate方法,这样就回到了nvalidate方法中去了。

postInvalidate总结

postInvalidate 通过android message Handle来处理异步消息,最终还是调用ViewRootImpl的nvalidate方法

setContentView是如何绘制View的

看过[android 自定义控件](1) ContentView和inflater的应该知道

 //上面会调用下面的函数
public void setContentView(View view, ViewGroup.LayoutParams params) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            view.setLayoutParams(params);
            final Scene newScene = new Scene(mContentParent, view);
            transitionTo(newScene);
        } else {
        //直接根据位置参数添加View
            mContentParent.addView(view, params);
        }
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

mContentParent使用addView函数添加主题,查看addView的重载函数最后汇入下面的addview

   public void addView(View child, int index, LayoutParams params) {
        if (DBG) {
            System.out.println(this + " addView");
        }

        if (child == null) {
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        }

        // addViewInner() will call child.requestLayout() when setting the new LayoutParams
        // therefore, we call requestLayout() on ourselves before, so that the child's request
        // will be blocked at our level
        requestLayout();
        invalidate(true);
        addViewInner(child, index, params, false);
    }

看见了最终还是会调用invalidate

requestLayout 重新布局

和invalidate一样

参考:http://blog.csdn.net/yanbober/article/details/46128379
http://blog.csdn.net/guolin_blog/article/details/16330267

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值