[Android基础系列] 第四章 自定义组件、动画(一)

263 阅读21分钟

第四章 自定义组件、动画

(一)View体系

(1)View简介

View是Android所有控件的基类,同时ViewGroup也是继承自View。我们常用的这些控件都是继承于View。 在这里插入图片描述

(2)Android坐标系

Android中有两种坐标系,分别为Android坐标系和视图坐标系,首先我们先来看看Android坐标系。 在Android中,将屏幕的左上角的顶点作为Android坐标系的原点,这个原点向右是X轴正方向,原点向下是Y轴正方向。 MotionEvent提供的getRawX()和getRawY()获取的坐标都是Android坐标系的坐标。 在这里插入图片描述

(3)视图坐标系

在这里插入图片描述 View获取自身宽高 getHeight():获取View自身高度 getWidth():获取View自身宽度 View自身坐标 通过如下方法可以获得View到其父控件(ViewGroup)的距离:

getTop():获取View自身顶边到其父布局顶边的距离 getLeft():获取View自身左边到其父布局左边的距离 getRight():获取View自身右边到其父布局左边的距离 getBottom():获取View自身底边到其父布局顶边的距离 MotionEvent提供的方法 我们看上图那个深蓝色的点,假设就是我们触摸的点,我们知道无论是View还是ViewGroup,最终的点击事件都会由onTouchEvent(MotionEvent event)方法来处理,MotionEvent也提供了各种获取焦点坐标的方法:

getX():获取点击事件距离控件左边的距离,即视图坐标 getY():获取点击事件距离控件顶边的距离,即视图坐标 getRawX():获取点击事件距离整个屏幕左边距离,即绝对坐标 getRawY():获取点击事件距离整个屏幕顶边的的距离,即绝对坐标

(二)自定义View

(1)onMeasure:对当前View的尺寸进行测量

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 

widthMeasureSpec和heightMeasureSpec包含测量模式和尺寸大小

int widthMode = MeasureSpec.getMode(widthMeasureSpec); 
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

尺寸大小:wrap_content、match_parent以及指定固定尺寸 测量模式:UNSPECIFIED,EXACTLY,AT_MOST 在这里插入图片描述

(2)重写onMeasure

private int getMySize(int defaultSize, int measureSpec) {
    int mySize = defaultSize;

    int mode = MeasureSpec.getMode(measureSpec);
    int size = MeasureSpec.getSize(measureSpec);

    switch (mode) {
        case MeasureSpec.UNSPECIFIED: {//如果没有指定大小,就设置为默认大小
            mySize = defaultSize;
            break;
        }
        case MeasureSpec.AT_MOST: {//如果测量模式是最大取值为size,则大小取最大值
            mySize = size;
            break;
        }
        case MeasureSpec.EXACTLY: {//如果是固定的大小,那就不要去改变它
            mySize = size;
            break;
        }
    }
    return mySize;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);//获取组件宽、高信息
    int width = getMySize(defalutSize,widthMeasureSpec);
    int height = getMySize(defalutSize,heightMeasureSpec);//自定义默认宽高情况

    if(width<height)height=width;
    else width=height;

    setMeasuredDimension(width,height);//自定义组件,用来决定组件大小
}

(3)重写onDraw:绘制当前View

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);//调用父View的onDraw函数,因为View这个类帮我们实现了一些基本的而绘制功能,比如绘制背景颜色、背景图片等
    int r = getMeasuredHeight() / 2;//半径
    int centerX = getLeft() + r;//圆心的横坐标为当前的View的左边起始位置+半径
    int centerY = getTop() + r; //圆心的纵坐标为当前的View的顶部起始位置+半径
    Paint paint = new Paint();
    paint.setColor(Color.BLUE);
    //开始绘制
    canvas.drawCircle(centerX,centerY,r,paint);
}

(4)自定义布局属性

res/attrs.xml

<!--属性集合名,一般与View名称相同-->
<declare-styleable name="MyCircleView">
    <!--属性名为default_size,取值类型为尺寸类型(dp,px等)-->
    <attr name="default_size" format="dimension"/>
</declare-styleable>
xmlns:app="http://schemas.android.com/apk/res-auto"
<com.sdu.chy.chytest.myView.myViewUtils.MyCircleView
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:background="@color/Orange"
    app:default_size="200dp"/>
public class MyCircleView extends View {
    int defalutSize = 0;
public MyCircleView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    //第二个参数就是我们在styles.xml文件中的<declare-styleable>标签,即属性集合的标签,在R文件中名称为R.styleable+name
    TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.MyCircleView);
    //第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称
    //第二个参数为,如果没有设置这个属性,则设置的默认的值
    defalutSize = a.getDimensionPixelSize(R.styleable.MyCircleView_default_size, 0);

    //最后记得将TypedArray对象回收
    a.recycle();
}

(三)自定义ViewGroup

具体实例:将子View按从上到下垂直顺序一个挨着一个摆放,模仿实现LinearLayout垂直布局

1、知道各个子View大小并根据子View大小得到ViewGroup大小

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //1.将所有的子View进行测量,这会触发每个子View的onMeasure函数,与measureChild区分,measureChild是对单个view进行测量
    //调用这个函数后,能获得后面每个子View的测量值(必加方法)
    measureChildren(widthMeasureSpec,heightMeasureSpec);

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int childCount = getChildCount();

    //2.根据子View的大小,及ViewGroup的大小,决定当前ViewGroup大小
    if(childCount==0){
        setMeasuredDimension(0,0);
    }else{
        if(widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
            //如果高度和宽度均为wrap_content,则宽度为子View中最大宽度,高度为所有子View高度和
            setMeasuredDimension(getMaxChildWidth(),getTotalHeight());
        }else if (widthMode == MeasureSpec.AT_MOST){
            //宽度是wrap_content,则设置宽度为子View最大宽度,高度为测量值
            setMeasuredDimension(getMaxChildWidth(),heightSize);
        }else if (heightMode == MeasureSpec.AT_MOST){
            //高度是wrap_content,则设置高度为所有子View高度,宽度为测量值
            setMeasuredDimension(widthSize,getTotalHeight());
        }else{
            setMeasuredDimension(widthSize,heightSize);
        }
    }
}

private int getMaxChildWidth(){
    //经过measureChildren(widthMeasureSpec,heightMeasureSpec);已经得到子View的测量值,可设置子View尺寸
    int childCount = getChildCount();
    int maxWidth = 0;
    for(int i=0;i<childCount;i++){
        View childView = getChildAt(i);
        if(childView.getMeasuredWidth() > maxWidth){
            maxWidth = childView.getMeasuredWidth();
        }
    }
    return maxWidth;
}

private int getTotalHeight(){
    int childCount = getChildCount();
    int totalHeight = 0;
    for(int i=0;i<childCount;i++){
        View childView = getChildAt(i);
        totalHeight += childView.getMeasuredHeight();
    }
    return totalHeight;
}

2、根据View与ViewGroup大小进行布局

//@param changed 该参数指出当前ViewGroup的尺寸或者位置是否发生了改变
//@param left top right bottom 当前ViewGroup相对于其父控件的坐标位置
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    //对子View在ViewGroup的布局进行管理(如何摆放?)
    int childCount = getChildCount();
    //记录当前高度
    int currentHeight = 0;
    //将子View逐个摆放
    for(int i=0;i<childCount;i++){
        View childView = getChildAt(i);
        int height = childView.getMeasuredHeight();
        int width = childView.getMeasuredWidth();
        //摆放子View,参数分别是子View矩形区域的左、上、右、下
        childView.layout(left,currentHeight,left+width,currentHeight+height);
        currentHeight += height;
    }
}

3、布局

<com.sdu.chy.chytest.myView.myViewUtils.MyViewGroup
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@color/Yellow">
    <Button
    android:layout_width="100dp"
    android:layout_height="200dp"
    android:background="@color/Orange"/>
    <Button
        android:layout_width="20dp"
        android:layout_height="150dp"
        android:background="@color/Orange"/>
    <Button
        android:layout_width="150dp"
        android:layout_height="20dp"
        android:background="@color/Orange"/>
</com.sdu.chy.chytest.myView.myViewUtils.MyViewGroup>

(四)Activity页面加载流程

在这里插入图片描述

(1) Window

Window即窗口,这个概念在Android Framework中的实现为android.view.Window这个抽象类,这个抽象类是对Android系统中的窗口的抽象。窗口是一个宏观的思想,它是屏幕上用于绘制各种UI元素及响应用户输入事件的一个矩形区域。通常具备以下两个特点: (1)独立绘制,不与其它界面相互影响; (2)不会触发其它界面的输入事件; 在Android系统中,窗口是独占一个Surface实例的显示区域,每个窗口的Surface由WindowManagerService分配。我们可以把Surface看作一块画布,应用可以通过Canvas或OpenGL在其上面作画。画好之后,通过SurfaceFlinger将多块Surface按照特定的顺序(即Z-order)进行混合,而后输出到FrameBuffer中,这样用户界面就得以显示。 android.view.Window这个抽象类可以看做Android中对窗口这一宏观概念所做的约定,而PhoneWindow这个类是Framework为我们提供的Android窗口概念的具体实现。接下来我们先来介绍一下android.view.Window这个抽象类。 这个抽象类包含了三个核心组件:

WindowManager.LayoutParams: 窗口的布局参数; Callback: 窗口的回调接口,通常由Activity实现; ViewTree: 窗口所承载的控件树。

下面我们来看一下Android中Window的具体实现(也是唯一实现)——PhoneWindow。 PhoneWindow这个类是Framework为我们提供的Android窗口的具体实现。我们平时调用setContentView()方法设置Activity的用户界面时,实际上就完成了对所关联的PhoneWindow的ViewTree的设置。我们还可以通过Activity类的requestWindowFeature()方法来定制Activity关联PhoneWindow的外观,这个方法实际上做的是把我们所请求的窗口外观特性存储到了PhoneWindow的mFeatures成员中,在窗口绘制阶段生成外观模板时,会根据mFeatures的值绘制特定外观。

(2) setContentView()

这个方法只是完成了Activity的ContentView的创建,而并没有执行View的绘制流程。 调用的setContentView()方法是Activity类的,源码如下:

  public void setContentView(@LayoutRes int layoutResID) {
      getWindow().setContentView(layoutResID);    
. . .
  }

getWindow()方法会返回Activity所关联的PhoneWindow,也就是说,实际上调用到了PhoneWindow的setContentView()方法,源码如下:

  @Override
  public void setContentView(int layoutResID) {
      if (mContentParent == null) {
          // mContentParent即为上面提到的ContentView的父容器,若为空则调用installDecor()生成
          installDecor();
      } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
          // 具有FEATURE_CONTENT_TRANSITIONS特性表示开启了Transition
          // mContentParent不为null,则移除decorView的所有子View
          mContentParent.removeAllViews();
      }
      if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
          // 开启了Transition,做相应的处理,我们不讨论这种情况
          // 感兴趣的同学可以参考源码
  . . .
      } else {
          // 一般情况会来到这里,调用mLayoutInflater.inflate()方法来填充布局
          // 填充布局也就是把我们设置的ContentView加入到mContentParent中
          mLayoutInflater.inflate(layoutResID, mContentParent);
      }
. . .
      // cb即为该Window所关联的Activity
      final Callback cb = getCallback();
      if (cb != null && !isDestroyed()) {
          // 调用onContentChanged()回调方法通知Activity窗口内容发生了改变
          cb.onContentChanged();
      }

. . .
  }

(3) LayoutInflater.inflate()

PhoneWindow的setContentView()方法中调用了LayoutInflater的inflate()方法来填充布局,这个方法的源码如下:

  public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
      return inflate(resource, root, root != null);
  }

  public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
      final Resources res = getContext().getResources();
. . .
      final XmlResourceParser parser = res.getLayout(resource);
      try {
          return inflate(parser, root, attachToRoot);
      } finally {
          parser.close();
      }
  }

在PhoneWindow的setContentView()方法中传入了decorView作为LayoutInflater.inflate()的root参数,我们可以看到,通过层层调用,最终调用的是inflate(XmlPullParser, ViewGroup, boolean)方法来填充布局。这个方法的源码如下:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
. . .
        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        View result = root;
        try {
            // Look for the root node.
            int type;
            // 一直读取xml文件,直到遇到开始标记
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }
            // 最先遇到的不是开始标记,报错
            if (type != XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription()+ ": No start tag found!");
            }
            final String name = parser.getName();
  . . .
// 单独处理<merge>标签,不熟悉的同学请参考官方文档的说明
            if (TAG_MERGE.equals(name)) {
// 若包含<merge>标签,父容器(即root参数)不可为空且attachRoot须为true,否则报错
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }
                // 递归地填充布局
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // temp为xml布局文件的根View
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                ViewGroup.LayoutParams params = null;
                if (root != null) {
      . . .
                    // 获取父容器的布局参数(LayoutParams)
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
// 若attachToRoot参数为false,则我们只会将父容器的布局参数设置给根View
                        temp.setLayoutParams(params);
                    }
                }
// 递归加载根View的所有子View
                rInflateChildren(parser, temp, attrs, true);
    . . .
                if (root != null && attachToRoot) {
// 若父容器不为空且attachToRoot为true,则将父容器作为根View的父View包裹上来
                    root.addView(temp, params);
                }
                // 若root为空或是attachToRoot为false,则以根View作为返回值
                if (root == null || !attachToRoot) {
                    result = temp;
                }
        return result;
    }
}

在上面的源码中,首先对于布局文件中的标签进行单独处理,调用rInflate()方法来递归填充布局。这个方法的源码如下:

void rInflate(XmlPullParser parser, View parent, Context context,
              AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    // 获取当前标记的深度,根标记的深度为0
    final int depth = parser.getDepth();
    int type;
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
        // 不是开始标记则继续下一次迭代
        if (type != XmlPullParser.START_TAG) {
            continue;
        }
        final String name = parser.getName();
        // 对一些特殊标记做单独处理
        if (TAG_REQUEST_FOCUS.equals(name)) {
            parseRequestFocus(parser, parent);
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            // 对<include>做处理
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            throw new InflateException("<merge /> must be the root element");
        } else {
            // 对一般标记的处理
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params=viewGroup.generateLayoutParams(attrs);
            // 递归地加载子View
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }

    if (finishInflate) {
        parent.onFinishInflate();
    }
}

我们可以看到,上面的inflate()和rInflate()方法中都调用了rInflateChildren()方法,这个方法的源码如下:

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

到这里,setContentView()的整体执行流程我们就分析完了,至此我们已经完成了Activity的ContentView的创建与设置工作。接下来开始View的绘制。 ViewRoot 在介绍View的绘制前,首先我们需要知道是谁负责执行View绘制的整个流程。实际上,View的绘制是由ViewRoot来负责的。每个应用程序窗口的decorView都有一个与之关联的ViewRoot对象,这种关联关系是由WindowManager来维护的。 那么decorView与ViewRoot的关联关系是在什么时候建立的呢?答案是Activity启动时,ActivityThread.handleResumeActivity()方法中建立了它们两者的关联关系。这里我们不具体分析它们建立关联的时机与方式,感兴趣的同学可以参考相关源码。下面我们直入主题,分析一下ViewRoot是如何完成View的绘制的。

View绘制的起点

当建立好了decorView与ViewRoot的关联后,ViewRoot类的requestLayout()方法会被调用,以完成应用程序用户界面的初次布局。实际被调用的是ViewRootImpl类的requestLayout()方法,这个方法的源码如下: @Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { // 检查发起布局请求的线程是否为主线程
checkThread(); mLayoutRequested = true; scheduleTraversals(); } }

上面的方法中调用了scheduleTraversals()方法来调度一次完成的绘制流程,该方法会向主线程发送一个“遍历”消息,最终会导致ViewRootImpl的performTraversals()方法被调用。下面,我们以performTraversals()为起点,来分析View的整个绘制流程。 接下来对遍历的每个View进行三个阶段的绘制:measure、draw、layout

(五)Android View 绘制流程

在这里插入图片描述 View绘制大致可以分成三个流程,分别是measure(测量),layout(布局),draw(绘制),这三者的顺序就是measure(测量)->layout(布局)->draw(绘制)。

measure: 判断是否需要重新计算View的大小,需要的话则计算; layout: 判断是否需要重新计算View的位置,需要的话则计算; draw: 判断是否需要重新绘制View,需要的话则重绘制。

在这里插入图片描述

1、Measure

Measure的目的就是测量View的宽和高

(1)MeasureSpec理解——父容器传递给子容器的布局要求

MeasureSpec(View的内部类) 由父View的MeasureSpec和子View的LayoutParams通过简单的计算得出一个针对子View的测量要求(测量模式+测量参数)。对于一个ViewGroup或者View的宽高而言,都一一对应一个MeasureSpec。 测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成,其中SpecMode只有三种值:

  • UPSPECIFIED : 父容器对于子容器没有任何限制,子容器想要多大就多大
  • EXACTLY: 父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间。
  • AT_MOST:子容器可以是声明大小内的任意大小(测量子View大小child.measure(width,height),但子View大小不能超过声明大小)

组合下的子View和父View之间宽高的关系,将LayoutParams和MeasureSpec组合起来分析最终子View的宽高。LayoutParams指的是子View的宽高设置参数,而MeasureSpec是父View传递给子View的,因为LayoutParams有三种情况(不讨论fill _ parent,因为已经过时),而MeasureSpec也有三种,最终会有3*3 = 9种情况: 在这里插入图片描述 ViewGroup.LayoutParams 我们常见的ViewGroup是各种布局等控件,像线性布局(LinearLayout),相对布局(RelativeLayout),约束布局(ConstraintLayout),网格布局(GridLayout)等等,而LayoutParams类就是指定View宽高等布局参数而被使用的。其实很简单,就对应着我们在布局文件中对应的为View设置各种宽高,如下所示:

  • 具体值:以px或者dp为单位
  • fill _ parent:这个已经过时,强制性使子视图的大小扩展至与父视图大小相等(不含 padding )
  • match _ parent:特性和fill_parent相似,Android版本大于2.3使用
  • wrap _ content:自适应大小,强制性地使视图扩展以便显示其全部内容(含 padding )

(2)View的Measure过程(默认)

在这里插入图片描述 measure():基本测量逻辑的判断。 onMeasure():根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()&存储测量后的View宽 / 高:setMeasuredDimension() setMeasuredDimension():存储测量后的宽和高。 getDefaultSize():根据View宽/高的测量规格计算View的宽/高。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//设置mMeasuredWidth和mMeasuredHeight,View的测量结束
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); 
} 

//建议高度/宽度是android:minHeight属性的值或者View背景图片的大小值
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
//@param size参数一般表示设置了android:minHeight属性或者该View背景图片的大小值(getSuggestedMinimumWidth)
//@param measureSpec参数是父View传给自己的MeasureSpec(是由父View的measureSpec和子View的LayoutParams共同确定的)
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://表示该View的大小视图未定,设置为默认值
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
    }
    return result;
}

对于View默认是测量很简单,大部分情况就是拿计算出来的MeasureSpec的size 当做最终测量的大小。 而对于其他的一些View的派生类,如TextView、Button、ImageView等,它们的onMeasure方法系统了都做了重写,一般先去测量字符或者图片的高度等,然后拿到View本身content这个高度(字符高度等),如果MeasureSpec是AT_MOST,而且View本身content的高度不超出MeasureSpec的size,那么可以直接用View本身content的高度(字符高度等),而不是像View.java 直接用MeasureSpec的size做为View的大小。

(3)ViewGroup的Measure过程

在这里插入图片描述 measure():基本测量逻辑的判断。 onMeasure():遍历所有的子View进行测量,如何遍历子View进行测量呢,就是调用measureChildren()方法,当所有的子View测量完成后,将会合并所有子View的尺寸最终计算出ViewGroup的尺寸。 measureChildren():遍历子View并对子View进行测量,后续会调用measureChild()方法。 measureChild():计算出单个子View的MeasureSpec,通过调用getChildMeasureSpce()方法实现,调用每个子View的measure()方法进行测量。 getChildMeasureSpec():计算出子View的MeasureSpec。 setMeasuredDimension():存储测量后的宽和高。

    //FrameLayout 的测量
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
....
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                // 遍历自己的子View,只要不是GONE的都会参与测量,measureChildWithMargins方法基本思想就是父View把自己的MeasureSpec 
// 传给子View结合子View自己的LayoutParams 算出子View 的MeasureSpec,然后继续往下传,
// 传递叶子节点,叶子节点没有子View,根据传下来的这个MeasureSpec测量自己就好了。
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth, child.getMeasuredWidth() +  lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);  
     .... 
            }
        }
..... 
//所有的孩子测量之后,经过一系类的计算之后通过setMeasuredDimension设置自己的宽高,/对于FrameLayout 可能用最大的子View的大小,
// 对于LinearLayout,可能是高度的累加,具体测量的原理去看看源码。总的来说,父View是等所有的子View测量结束之后,再来测量自己。
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
....
    }  

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

// 子View的LayoutParams,你在xml的layout_width和layout_height,
// layout_xxx的值最后都会封装到这个个LayoutParams。
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

//根据父View的测量规格和父View自己的Padding,
//还有子View的Margin和已经用掉的空间大小(widthUsed),就能算出子View的MeasureSpec,具体计算过程看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的MeasureSpec和子View的自己LayoutParams的计算,算出子View的MeasureSpec,然后父容器传递给子容器的
// 然后让子View用这个MeasureSpec(一个测量要求,比如不能超过多大)去测量自己,如果子View是ViewGroup 那还会递归往下测量。
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

    }

// spec参数   表示父View的MeasureSpec
// padding参数    父View的Padding+子View的Margin,父View的大小减去这些边距,才能精确算出
//               子View的MeasureSpec的size
// childDimension参数  表示该子View内部LayoutParams属性的值(lp.width或者lp.height)
//                    可以是wrap_content、match_parent、一个精确指(an exactly size),
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);  //获得父View的mode
        int specSize = MeasureSpec.getSize(spec);  //获得父View的大小

        //父View的大小-自己的Padding+子View的Margin,得到值才是子View的大小。
        int size = Math.max(0, specSize - padding);
        //初始化值,最后通过这个两个值生成子View的MeasureSpec
        int resultSize = 0;
        int resultMode = 0;
//对应理解MeasureSpec机制
        switch (specMode) {
            //1、父View是EXACTLY(Parent has imposed an exact size on us)
            case MeasureSpec.EXACTLY:
                //1.1、子View的width或height是个精确值 (an exactly size)
                if (childDimension >= 0) {
                    resultSize = childDimension;         //size为精确值
                    resultMode = MeasureSpec.EXACTLY;    //mode为 EXACTLY 。
                }
                //1.2、子View的width或height为 MATCH_PARENT
                else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size. So be it.
                    resultSize = size;                   //size为父视图大小
                    resultMode = MeasureSpec.EXACTLY;    //mode为 EXACTLY 。
                }
                //1.3、子View的width或height为 WRAP_CONTENT
                else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be bigger than us.
                    resultSize = size;                   //size为父视图大小
                    resultMode = MeasureSpec.AT_MOST;    //mode为AT_MOST 。
                }
                break;

            //2、父View是AT_MOST的(Parent has imposed a maximum size on us)
            case MeasureSpec.AT_MOST:
                //2.1、子View的width或height是个精确值 (an exactly size)
                if (childDimension >= 0) {
                    // Child wants a specific size.so be it
                    resultSize = childDimension;        //size为精确值
                    resultMode = MeasureSpec.EXACTLY;   //mode为 EXACTLY 。
                }
                //2.2、子View的width或height为 MATCH_PARENT/FILL_PARENT
                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;                  //size为父视图大小
                    resultMode = MeasureSpec.AT_MOST;   //mode为AT_MOST
                }
                //2.3、子View的width或height为 WRAP_CONTENT
                else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be bigger than us.
                    resultSize = size;                  //size为父视图大小
                    resultMode = MeasureSpec.AT_MOST;   //mode为AT_MOST
                }
                break;

            //3、父View是UNSPECIFIED的(Parent asked to see how big we want to be)
            case MeasureSpec.UNSPECIFIED:
                //3.1、子View的width或height是个精确值 (an exactly size)
                if (childDimension >= 0) {
                    // Child wants a specific size.let him have it
                    resultSize = childDimension;        //size为精确值
                    resultMode = MeasureSpec.EXACTLY;   //mode为 EXACTLY
                }
                //3.2、子View的width或height为 MATCH_PARENT/FILL_PARENT
                else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size... find out how big it should be
                    resultSize = 0;                        //size为0! ,其值未定
                    resultMode = MeasureSpec.UNSPECIFIED;  //mode为 UNSPECIFIED
                }
                //3.3、子View的width或height为 WRAP_CONTENT
                else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size.... find out how big it should be
                    resultSize = 0;                        //size为0! ,其值未定
                    resultMode = MeasureSpec.UNSPECIFIED;  //mode为 UNSPECIFIED
                }
                break;
        }
        //根据上面逻辑条件获取的mode和size构建MeasureSpec对象。(这个值由父View的MeasureSpec和子View的lp(childDimension)决定)
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

(4)具体案例

1、布局代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/linear"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="50dp"
    android:background="@android:color/holo_blue_dark"
    android:paddingBottom="70dp"
    android:orientation="vertical">
    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/material_blue_grey_800"
        android:text="TextView"
        android:textColor="@android:color/white"
        android:textSize="20sp" />
    <View
        android:id="@+id/view"
        android:layout_width="match_parent"
        android:layout_height="150dp"
        android:background="@android:color/holo_green_dark" />
</LinearLayout>
2、布局结果

在这里插入图片描述

3、View树

在这里插入图片描述

4、布局流程

View绘制的起点 View的绘制是由ViewRoot来负责的。每个应用程序窗口的decorView都有一个与之关联的ViewRoot对象,这种关联关系是由WindowManager来维护的(Activity启动时,ActivityThread.handleResumeActivity()方法中建立了它们两者的关联关系。) 当建立好了decorView与ViewRoot的关联后,ViewRoot类的requestLayout()方法会被调用,以完成应用程序用户界面的初次布局。实际被调用的是ViewRootImpl类的requestLayout()方法,这个方法的源码如下:

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        // 检查发起布局请求的线程是否为主线程  
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

上面的方法中调用了scheduleTraversals()方法来调度一次完成的绘制流程,该方法会向主线程发送一个“遍历”消息,最终会导致ViewRootImpl的performTraversals()方法被调用。下面,我们以performTraversals()为起点,来分析View的整个绘制流程。

step1.DecorView(FragmentLayout)——整个View的ROOT

绘制入口是由ViewRootImpl的perform Traversals()发起Measure,Layout,Draw等流程

    private void performTraversals() {
......
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);//mWidth为屏幕宽度,lp.width=match_parent
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);//mHeight为屏幕高度,lp.height=match_parent
......
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);//mView其实就是DecorView,DecorView本质是Fragment,进入Fragment的OnMeasure()
......
        mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
        mView.draw(canvas);
......
    }

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
            case ViewGroup.LayoutParams.MATCH_PARENT:
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.EXACTLY);
                break;
   ......
        }
        return measureSpec;
    }

在这里插入图片描述 那么接下来在FrameLayout 的onMeasure()方法DecorView开始for循环测量自己的子View,测量完所有的子View再来测量自己,根据View树知接下来要测量ViewRoot的大小

step2.ViewRoot(LinearLayout)

ViewRoot 的MeasureSpec mode应该等于EXACTLY(DecorView MeasureSpec 的mode是EXACTLY,ViewRoot的layoutparams 是match_parent),size 等于DecorView的size 在这里插入图片描述 ViewRoot是一个LinearLayout ,ViewRoot.measure最终会执行的LinearLayout 的onMeasure 方法,LinearLayout 的onMeasure 方法又开始逐个测量它的子View(measureChildWithMargins),那么根据View的层级图,接下来测量的是header(ViewStub),由于header的Gone,所以直接跳过不做测量工作,所以接下来轮到ViewRoot的第二个child content(android.R.id.content)

step3.Content(LinearLayout)

由于ViewRoot 的mPaddingBottom=100px(id/statusBarBackground的View的高度刚好等于100px,所以计算出来Content(android.R.id.content) 的MeasureSpec 的高度少了100px )它的宽高的mode 根据算出来也是EXACTLY(ViewRoot 是EXACTLY和android.R.id.content 是match_parent)。 在这里插入图片描述 Content(android.R.id.content) 是FrameLayout,递归调用开始准备计算id/linear的MeasureSpec

step4.linear(LinearLayout)

id/linear的heightMeasureSpec 的mode=AT_MOST,因为id/linear 的LayoutParams 的layout_height="wrap_content",由于id/linear 的 android:layout_marginTop="50dp" 使得lp.topMargin=200px (本设备的density=4,px=4*pd),在计算后id/linear的heightMeasureSpec 的size 少了200px。 在这里插入图片描述

step5.text(TextView)

算出id/text 的MeasureSpec 后TextView 拿着刚才计算出来的MeasureSpec(mode=AT_MOST,size=1980),这个就是对TextView的高度和宽度的约束,进到TextView 的onMeasure(widthMeasureSpec,heightMeasureSpec) 方法 在这里插入图片描述 TextView字符的高度(也就是TextView的content高度[wrap_content])测出来=107px,107px 并没有超过1980px(允许的最大高度),所以实际测量出来TextView的高度是107px。 最终算出id/text 的mMeasureWidth=1440px,mMeasureHeight=107px。

step6 view(View)

在这里插入图片描述 id/linear 的子View的高度都计算完毕了,接下来id/linear就通过所有子View的测量结果计算自己的高宽,id/linear是LinearLayout,所有它的高度计算简单理解就是子View的高度的累积+自己的Padding. 在这里插入图片描述 最终算出id/linear出来后,id/content 就要根据它唯一的子View id/linear 的测量结果和自己的之前算出的MeasureSpec一起来测量自己的结果,具体计算的逻辑去看FrameLayout onMeasure 函数的计算过程。以此类推,接下来测量ViewRoot,然后再测量id/statusBarBackground,最后测量DecorView 的高宽,最终整个测量过程结束。

2、Layout

layout的主要作用 :根据子视图的大小以及布局参数将View树放到合适的位置上。确认View&ViewGroup的四个顶点的位置(从而确定位置),left,top,right,bottom

1.Android屏幕坐标系

View&ViewGroup位置与Android屏幕坐标系相关。

2.入口DecorView

mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight()); 

3.ViewGroup的layout函数

在这里插入图片描述 layout():调用layout()方法计算ViewGroup自身的位置,在此方法调用路径中有一个方法特别重要,这个方法就是setFrame(),它的作用就是根据传入的4个位置值,设置View本身的四个顶点位置,也就是用来确定最终View的位置的。接下来就是回调onLayout()方法。 onLayout():对于ViewGroup而言,它不仅仅要确认自身的位置,它还要计算它的子View的位置,因此onLayout的作用就是遍历并计算每个子View的位置。

public final void layout(int l, int t, int r, int b) {
    if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
        //LayoutTransition是用于处理ViewGroup增加和删除子视图的动画效果
        if (mTransition != null) {
            mTransition.layoutChange(this);
        }
        super.layout(l, t, r, b);
    } else {
        mLayoutCalledWhileSuppressed = true;
    }
}

int childCount = getChildCount() ;
//安排其children在父视图的具体位置
for(int i=0 ;i<childCount ;i++){
    View child = getChildAt(i) ;
    //整个layout()过程就是个递归过程
    child.layout(l, t, r, b) ;
}

遍历自己的孩子,然后调用child.layout(l, t, r, b) ,给子view 通过setFrame(l, t, r, b) 确定位置, 而重点是(l, t, r, b) 怎么计算出来的呢。是通过之前measure测量出来的MeasuredWidth和MeasuredHeight、在xml 设置的Gravity、RelativeLayout 的其他参数等等一起来确定子View在父视图的具体位置的。 具体的计算过程不同的ViewGroup 的实现都不一样(FragmentLayout\RelativeLayout\LinearLayout)

4.View的layout函数

在这里插入图片描述 layout():调用layout()方法主要为了计算View自身的位置,在此方法调用路径中有一个方法特别重要,这个方法就是setFrame(),它的作用就是根据传入的4个位置值,设置View本身的四个顶点位置,也就是用来确定最终View的位置的。接下来就是回调onLayout()方法。 onLayout():对于View的onLayout()方法来说,它是一个空实现。为什么View的onLayout()方法是空实现呢?因为onLayout()方法作用是计算此VIew的子View的位置,对于单一的View而言,它并不存在子View,因此它肯定是空实现啦!

public final void layout(int l, int t, int r, int b) {
   .....
    //设置View位于父视图的坐标轴
    //setFrame(l, t, r, b) 可以理解为给mLeft 、mTop、mRight、mBottom赋值,确定该View在父View的相对位置
    boolean changed = setFrame(l, t, r, b);
    //判断View的位置是否发生过变化,看有必要进行重新layout吗
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
        }
        //调用onLayout(changed, l, t, r, b); 函数
        onLayout(changed, l, t, r, b);
        mPrivateFlags &= ~LAYOUT_REQUIRED;
    }
    mPrivateFlags &= ~FORCE_LAYOUT;
   .....
}

3、Draw

Draw过程的目的绘制View&ViewGroup的视图。

1、背景绘制

2、对ViewGroup绘制

在这里插入图片描述 draw():绘制ViewGroup自身。 drawBackground():绘制ViewGroup自身的背景。 onDraw():绘制View自身的内容。 dispatchDraw():对于ViewGroup而言,它是存在子View的,因此此方法就是用来遍历子View,然后让每个子View进入Draw过程从而完成绘制过程。 onDrawScrollBars():ViewGroup的装饰绘制。

3、对View绘制

在这里插入图片描述 draw():绘制View自身。 drawBackground():绘制View自身的背景。 onDraw():绘制View自身的内容。 dispatchDraw():对于View而言,它是空实现,因为它的作用是绘制子View的,因为单一的View没有子View,因此它是空实现。 onDrawScrollBars():从名字可以看出,它是绘制滑动条等装饰的,比如ListView的滑动条。

onDraw(canvas) 方法是view用来draw 自己的,具体如何绘制,颜色线条什么样式就需要子View自己去实现,View.java 的onDraw(canvas) 是空实现,ViewGroup 也没有实现,每个View的内容是各不相同的,所以需要由子类去实现具体逻辑。

4、对当前父View的所有子View绘制

就是遍历子View然后drawChild(),drawChild()方法实际调用的是子View.draw()方法,ViewGroup类已经为我们实现绘制子View的默认过程

5、对View滚动条绘制

在这里插入图片描述