View绘制那些事儿

目录

  • 初识ViewRoot和DecorView
  • View绘制流程
    • measure
      • View
        • MeasureSpec
          • SpecMode
          • 实践看一看
          • 注意点
      • ViewGroup
        • measureChildren
        • measureChild
        • measureChildWithMargins
        • getChildMeasureSpec
        • 例子
    • layout
      • View
      • 例子
    • draw
    • 补充
      • view.post
      • activity/view#onWindowFocusChanged
      • getMeasuredWidth和getWidth的区别
        • getWidth
        • getMeasuredWidth
  • 下篇预告

初识ViewRoot和DecorView

这里咱的重点是View的绘制,但是想让大家有一个整体的认识,我觉得还是从顶层开始说起,要对应的标题初识,咱这会大致讲解,后面Window机制中回去好好说。

其实每一个页面的根布局是一个DecorView,而我们在Activity中调用setContentView去设置布局,其实只不过是Decorview中的一个子View。DecorView这个所有页面的根视图,其实是一个FrameLayout,里面有一个LinearLayout的子View,该LinearLayout是垂直布局,又有两个子View一个是Title,还有一个是Content也就是我们调用Activity的setContentView所设置的View。
View绘制那些事儿_第1张图片
而这个DecorView不是依附在Activity上的而是依附在Window上的,也就是说真正的视图是有window来显示的。window中视图的增删对应的操作类是WindowManager,ViewRoot则是负责连接DecorView和WindowManager的枢纽,通过ViewRoot才开始去绘制DecorView,呈树形结构模型,依次ViewGroup的measure–>layout—>draw去绘制,接着再去调用子View的measure—>layout---->draw完成整个视图的绘制流程。

View绘制流程

虽然说绘制流程是呈现树状的形式,从树顶依次往下绘制,也就是说先从ViewGroup开始绘制,依次往子View下绘制。由于ViewGroup是继承View,所以绘制流程的开始方法measure,layout,draw方法都在View中,故我们按照方法去讲解。

  • measure:测量,来测量自身View的高宽
  • layout:位置,表示位于父View的什么位置
  • draw:绘制,就是将View绘制到屏幕上

measure

咱们先来看看测量的入口,measure方法

  public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            onMeasure(widthMeasureSpec, heightMeasureSpec);
        } else {
            ...
    }

这么说,不管你是View还是ViewGroup,测量的入口都是View#measure方法,因为ViewGroup继承View。但是ViewGroup和View的测量流程一定不一样,为了区分故ViewGroup和View的视图需要重写onMeasure方法,并且该方法还返回了宽高对应的参数,可以去实现对应的测量逻辑。

那么ViewGroup的测量流程和View的测量流程有什么先后顺序的关联呢?关联如下
View绘制那些事儿_第2张图片
由于绘制是从顶部开始,而顶部是DecorView一定是一个ViewGroup,所以一定是ViewGroup开始先绘制。

  1. 首先ViewGroup调用measure,进入测量入口也就是View#measure,为了自定义ViewGroup的测量流程,会去重写onMeasure()。
  2. 在onMeasure方法里会去获取所有的Child,然后去调用Child#measure方法,每一个Child也需要去自定义测量,从而间接的调用了Child#onMeasure方法。

View

正如上诉流程描述,我们先来讲解一下View的onMeasure(),可以帮助我们做什么。

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //下面为自己添加部分
        MeasureSpec.getSize(widthMeasureSpec);
        MeasureSpec.getMode(widthMeasureSpec);
        MeasureSpec.getSize(heightMeasureSpec);
        MeasureSpec.getMode(heightMeasureSpec);
    }

其实widthMeasureSpec和heightMeasureSpec所表示的确实和宽高有关,但是并不能拿来直接用,它所表达的是父View结合子View自身的宽高再施加规则所返回的值,需要通过MeasureSpec去解析用法如上。

MeasureSpec

MeasureSpec则代表着一个32位的int值,高两位表示SpecMode,低30位表示SpecSize。SpecMode表示测量模式,SpecSize则表示某种测量模式下的规格大小。

  • 可以通过MeasureSpec.getSize(xx)去获取到SpecSize
  • 可以通过MeasureSpec.getMode(xx)去获取到SpecMode
SpecMode

测量模式对应的值有三种如下图
View绘制那些事儿_第3张图片
注意:因为SpecMode所表示的模式是有父View的宽高和子View的宽高共同约束的,下面结论仅针对父亲宽高都是match_parent的时候

模式 约束意义 对应的值
EXACTLY 父控件决定给子View一个精确的尺寸 1073741824
AL_MOST 父控件会给子View一个尽可能大的尺寸 -2147483648
UNSPECIFIED 父控件不强加任何约束,它可以是它想要的任何大小 0

可以这么理解

  • EXACTLY:既然父控件可以给予子View精确的尺寸,那么子View自身的宽/高一定是确定的,xml中对应的就是match_parent/准确的尺寸
  • AL_MOST:父空间会给子View一个尽可能大的尺寸,但不能超过自己,也就说明View自身的宽/度不确定,xml中对应的就是wrap_content

UNSPECIFIED
一般用不到,一般会出现在系统View绘制中。由于一般博客都没怎么讲解到,在此为了弥补这一空缺,咱们要好好的解释一波。

就拿系统的RecycleView为例子,在Item进行measure也就是测量的时候,如果可以滑动且Item宽高是wrap_content的话,那么接下来Item的onMeasure方法就会收到MeasureSpec.UNSPECIFIED。

**为什么此时是不是AL_MOST?**我们来看看RecycleView去生成Child的约束的方法getChildMeasureSpec

  public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
                int childDimension, boolean canScroll) {
            int size = Math.max(0, parentSize - padding);
            int resultSize = 0;
            int resultMode = 0;
            if (canScroll) {
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    switch (parentMode) {
                        case MeasureSpec.AT_MOST:
                        case MeasureSpec.EXACTLY:
                            resultSize = size;
                            resultMode = parentMode;
                            break;
                                                // MATCH_PARENT can't be applied since we can scroll in this dimension, wrap
                    // instead using UNSPECIFIED.
                        case MeasureSpec.UNSPECIFIED:
                            resultSize = 0;
                            resultMode = MeasureSpec.UNSPECIFIED;
                            break;
                    }
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    resultSize = 0;
                    resultMode = MeasureSpec.UNSPECIFIED;
                }
            } else {
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    resultSize = size;
                    resultMode = parentMode;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    resultSize = size;
                    if (parentMode == MeasureSpec.AT_MOST || parentMode == MeasureSpec.EXACTLY) {
                        resultMode = MeasureSpec.AT_MOST;
                    } else {
                        resultMode = MeasureSpec.UNSPECIFIED;
                    }

                }
            }
            //noinspection WrongConstant
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
        }

可以看到当可滑动的时候,child为wrap_content的时候,child的约束的UNSPECIFIED;当不能滚动的时候,若父控件的约束是UNSPECIFIED且child是wrap_content则是UNSPECIFIED。

上面还有一段官方的注解,它表达的意思大致就是能滚动的时候,不应该去限制child的大小。

因为本身RecycleView就是可以滚动的,哪怕是child的的宽高超出了屏幕的范围,也还是可以通过滚动去查看显示,若此时约束为AL_MOST,那么child最大最大的宽高只能是父控件的宽高,这样显然是不合理的。

所以这时候需要UNSPECIFIED的约束,因为父控件不强加任何约束,那么子View想要多少就有多少,想放哪里就放哪里,这才形成了超出屏幕的范围,最终才呈现出滑动的效果。


实践看一看

接下来我们来看看子View的宽高对应的SpecSize和SpecMode,首先我们自定义一个View然后日志输出。

public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }
    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        System.out.println("width mode " + MeasureSpec.getMode(widthMeasureSpec));
        System.out.println("width size " + MeasureSpec.getSize(widthMeasureSpec));
        System.out.println("height mode " + MeasureSpec.getMode(heightMeasureSpec));
        System.out.println("height size " + MeasureSpec.getSize(heightMeasureSpec));
    }
}

创建了两个View一个是宽:match_parent,高:50dp;另一个是宽:wrap_content,宽:wrap_content.

  <com.sinosun.csdnnote.views.MyView
  		android:id="@+id/view_1"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        tools:ignore="MissingConstraints"></com.sinosun.csdnnote.views.MyView>

    <com.sinosun.csdnnote.views.MyView
      	android:id="@+id/view_2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="MissingConstraints"></com.sinosun.csdnnote.views.MyView>

那么最后的日志如下图,可以根据值去查看SpecMode的值,

View绘制那些事儿_第4张图片

  1. 前面四个是view_1的输出,由于是match_parent和指定的尺寸,所返回的约束是EXCTLY,看结果可以知道屏幕的宽度是1080px,高度的50dp根据对应的dpi转化成150px。
  2. 后面四个是view_2的输出,由于是wrap_content,所以生成的约束是AL_MOST,但是最后显示的宽高是整个屏幕的宽高,按照道理AL_MOST约束下,父亲会给子View一个尽可能大的尺寸。什么意思?就是说给的尺寸刚刚好,与结果相违背。这恰恰是自定义View的一个注意点,咱们来好好分析一下
注意点

先找到View#onMeasure方法

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

一共有三个方法

  1. getSuggestedMinimumWidth/getSuggestedMinimumHeight
  2. getDefaultSize
  3. setMeasuredDimension
  • getSuggestedMinimumWidth/getSuggestedMinimumHeight
    先来分析getSuggestedMinimumWidth/getSuggestedMinimumHeight,就拿其中的getSuggestedMinimumWidth来说事儿吧,就看这名字的意思就是获取建议的最小宽度
   protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

代码里也很明白,如果没有设置背景,那么就View内容的宽度;如果有背景,就比较背景和内容的大小选取最大。

  • getDefaultSize
    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

这一段代码似乎解决了,为什么AT_MOST所显示的尺寸和EXACTLY,也就是上面view_2的输入日志问题所在。在View的源码里,默认把AT_MOST和EXACTLY类型同一处理,所以也就为什么wrap_content获取到的值是match_parent,所以在自定义VIew的时候,需要对约束进行判断,若EXACTLY则返回对应的SpecSize的值,若是AT_MOST则要返回实际的尺寸

  • setMeasuredDimension
    这方法很简单,最后就是去设置View的宽高
   protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {//判断是不是ViewGroup
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

这一段源码告诉我们

  1. 系统不会去区分AL_MOST和EXACTLY约束,我们在自定义时我们需要去区分。
  2. 在最后确认View的宽高之后,需要调用setMeasuredDimension()方法去设置View的宽高。

ViewGroup

根据measure的流程图,ViewGroup到底是如何调用child#measure方法呢?我们来看看内部提供的三个方法。

  • measureChildren
  • measureChild
  • measureChildWithMargins
    我们来看看具体是怎么样实现的。

measureChildren

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

代码其实很简单,就是后去所有的Child,调用measureChild方法,传入Child和ViewGroup自身的约束。所以该方法主要是遍历Child。

measureChild

 protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

这一段代码也很好理解,通过父控件的约束、child自身的宽高和padding去生成最终的测量约束MeasureSpec,然后在调用Child#measure,从而可以在重写child#onMeasure方法中去回调到最终的约束值。也就是说padding会参与到约束条件中去。

measureChildWithMargins

 protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        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(childWidthMeasureSpec, childHeightMeasureSpec);
    }

其实measureChildWithMargins和measureChild类似,只不过measureChildWithMargins会把margin参与到约束的计算中去。

getChildMeasureSpec

似乎约束是有该方法生成的,那么我们不妨去看看约束条件是如何生成的

  public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

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

看到switch,就可以那些约束最终会生成什么SpecMode了

父约束 子View宽高 子约束
EXACTLY match_parent/准确的尺寸 EXCTLY
EXACTLY wrap_content AL_MOST
AL_MOST 准确的尺寸 EXCTLY
AL_MOST match_parent/wrap_content AL_MOST
UNSPECIFIED 准确的尺寸 EXCTLY
UNSPECIFIED match_parent/wrap_content UNSPECIFIED

例子

可能有些人还是觉得很抽象,那么我来看一下ScrollView的onMeasure看看做了什么?

      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		...
        if (getChildCount() > 0) {
            final View child = getChildAt(0);
            final int widthPadding;
            final int heightPadding;
            final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
            final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (targetSdkVersion >= VERSION_CODES.M) {
                widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
                heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
            } else {
                widthPadding = mPaddingLeft + mPaddingRight;
                heightPadding = mPaddingTop + mPaddingBottom;
            }
			//获取ScrollView的测量高度-padding部分
            final int desiredHeight = getMeasuredHeight() - heightPadding;
            //若ScrollView的可用空间大于Child需要的空间
            if (child.getMeasuredHeight() < desiredHeight) {
                final int childWidthMeasureSpec = getChildMeasureSpec(
                        widthMeasureSpec, widthPadding, lp.width);
                final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                        desiredHeight, MeasureSpec.EXACTLY);
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

上诉是ScrollView#onMeasure方法,重在思路。由于ScrollView只能拥有一个Child需我们只能看到一个Child调用measure方法。

就拿ScrollView为例,ScrollView的测量先调用到了measure,由于系统把测量权交给了我们,所以ScrollView需要重写onMeasure,然后计算出自身的约束,再去调用Child#measure方法,从而间接的触发Child#onMeasure方法。

layout

layout的分析流程其实和measure一样的,虽然会最先设置ViewGroup的layout(位置),但是layout入口的实现其实是其父类也就是View的layout。整一个ViewGroup与View的关系如下图
View绘制那些事儿_第5张图片

View

正如上图流程,那么我们先来看看View#layout的入口源码

 public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
		//分析1
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
		//分析2 setOpticalFrame和setFrame
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
		//分析3
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
     	...
        }
	...
  • a.分析1处:
    该layout的目的就是确定及设置View的位置,该处的oldL分别表示当前位置,也为了缓存,因为分析2处,就会根据新传入的参数重新设置位置。
  • b.分析2处
    该处主要的作用就是通过setOpticalFrame/setFrame来确定新的位置,判断是否需要发送位置的改变。那么先来看看setFrame方法
  protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (DBG) {
            Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }
		//分析点1
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            invalidate(sizeChanged);
			//分析点2
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
		...
   
        return changed;
    }
  1. 分析点1:
    其实首先就是去判断,新的位置与就位置是否一样,若一样着不需要改变;若不一样这需要去重新渲染。invalidate就是去渲染新的位置。
  2. 分析点2
    然后并记录下新的位置

setOpticalFrame该方法内部实则还是调用setFrame方法

  private boolean setOpticalFrame(int left, int top, int right, int bottom) {
        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;
        Insets childInsets = getOpticalInsets();
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }

  • c.分析点3
    该处会去调用onLayout,此时需要分类讨论。若当前是一个ViewGroup需要去重写一个onLayout方法,作用同onMeasure一样,我们需要去遍历所有的Child,再调用Child#layout方法,赋予Child具体的位置;若当前是一个View则是一个空实现,因为View没有子View。

可以看看ViewGroup#onLayout,该方法还是一个抽象方法,如果继承ViewGroup必定要实现onLayout

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

例子

那就结合LinearLayout#onLayout来看看,毕竟理论要结合实际嘛

    void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;

        int childTop;
        int childLeft;

        // Where right end of child should go
        final int width = right - left;
        int childRight = width - mPaddingRight;

        // Space available for child
        int childSpace = width - paddingLeft - mPaddingRight;

        final int count = getVirtualChildCount();

        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
	//先计算出第一个Child的top
        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;

               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;

           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }

        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();

                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                //设置left
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }
				//添加分隔符
                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }
				
                childTop += lp.topMargin;
                //调用child.layout
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }
    
   private void setChildFrame(View child, int left, int top, int width, int height) {
        child.layout(left, top, left + width, top + height);
    }

其实大体思路很清晰,先计算出第一个Child的Top,然后其余的Child在此基础上依次累加。遍历所有的Child,根据padding和margin计算出left,最后在通过setChildFrame方法,去设置Child#layout方法。

draw

draw表示绘制流程,大体流程也是同measure,layout是一个性质,其实有时候源码的注解可以给我们很多阅读源码的方向。
View绘制那些事儿_第6张图片
那么对应的中文就是
绘制遍历执行几个绘制步骤,这些步骤必须按适当的顺序执行

  1. 绘制背景
  2. 如果需要,保存图层
  3. 绘制内容
  4. 绘制子View
  5. 恢复保存的图层
  6. 绘制装饰(例如滚动条)

那么我们重点看看3 4点
View绘制那些事儿_第7张图片我们可以看到若要实现绘制内容,则需要去重写onDraw方法即可。那么dispatchDraw则表示绘子View,此时还是需要分类讨论,既然是绘制子View,那么View没有Child所以是空实现;如果是ViewGroup则会去重写dispatchDraw方法。

那么我们来看看LinearLayout的dispatchDraw,来看看是如何实现的

    protected void dispatchDraw(Canvas canvas) {
        boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        int flags = mGroupFlags;
	
        if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
            final boolean buildCache = !isHardwareAccelerated();
            //遍历所有Child去设置动画
            for (int i = 0; i < childrenCount; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                    final LayoutParams params = child.getLayoutParams();
                    attachLayoutAnimationParameters(child, params, i, childrenCount);
                    bindLayoutAnimation(child);
                }
            }

            final LayoutAnimationController controller = mLayoutAnimationController;
            if (controller.willOverlap()) {
                mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
            }
			//开始动画
            controller.start();

            mGroupFlags &= ~FLAG_RUN_ANIMATION;
            mGroupFlags &= ~FLAG_ANIMATION_DONE;
		//设置动画监听
            if (mAnimationListener != null) {
                mAnimationListener.onAnimationStart(controller.getAnimation());
            }
        }
		...
        // We will draw our child's animation, let's reset the flag
        mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
        mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;

        boolean more = false;
        final long drawingTime = getDrawingTime();

        if (usingRenderNodeProperties) canvas.insertReorderBarrier();
        final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
        int transientIndex = transientCount != 0 ? 0 : -1;
        // Only use the preordered list if not HW accelerated, since the HW pipeline will do the
        // draw reordering internally
        final ArrayList<View> preorderedList = usingRenderNodeProperties
                ? null : buildOrderedChildList();
        final boolean customOrder = preorderedList == null
                && isChildrenDrawingOrderEnabled();
                //遍历所有Child
        for (int i = 0; i < childrenCount; i++) {
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                      //调用drawChild,内部实则调用child.draw
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }
		...
}

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

所以我们可看到LinearLayout先调用draw方法,通过重写dispatchDraw,去遍历所有Child,接着调用Child#draw,从而触发到我们重写的onDraw去绘制内容。

补充

对于View的宽高获取需要特别注意,不能随随便便的getHeight/Width,因为你无法确定此时的View是否已经绘制完成,所以需要额外注意。

view.post

最常用的方式使用很简单,通过post可以将一个runnable投递到消息队列的尾部,等待Looper调用runnable的时候,view已经初始化了

view.post(new Runnable() {
    @Override
    public void run() {
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
    }
});

activity/view#onWindowFocusChanged

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if (hasFocus) {
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
    }
}

重写onWindowFocusChanged方法即可,不过该方法会被多次调用,onResume和onPause都会被调用

getMeasuredWidth和getWidth的区别

getWidth

  public final int getWidth() {
        return mRight - mLeft;
    }

从源码来看,getWidth所返回的值是mRight和mLeft的差值,而mRight,mLeft表示该View的位置,在layout中会被设置,也就是说当layout调用完毕,确定好View的位置,那么getWidth的值就有了

getMeasuredWidth

    public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }
    
  protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
    
 private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

可以看到getMeasuredWidth的值与mMeasuredWidth有关,而mMeasuredWidth是在onMeasure中得到测量后的宽高通过setMessuredDimension方法去设置保存的,所以getMeasuredWidth的调用最好是在setMessuredDimension方法之后。若在setMessuredDimension之前调用getMeasuredWidth则会返回0。

下篇预告

那么 measure layout draw 三个流程大致讲完,下一篇就是View的事件机制,以及冲突的解决方案。

你可能感兴趣的:(Android那些事儿)