自定义控件(四) 源码分析 layout 和 draw 流程

系列文章传送门 (持续更新中..) :android

自定义控件(一) Activity的构成(PhoneWindow、DecorView)canvas

自定义控件(二) 从源码分析事件分发机制bash

自定义控件(三) 源码分析measure流程ide


在上一篇中咱们详细分析了 View 工做三大流程中最复杂的 measure 流程, 掌握了 measure 流程后, layout 和 draw 流程就相对比较简单些了。源码分析

  • 在布局流程中, 当 ViewGroup 的 layout 方法被父容器调用后它的位置将被肯定下来, 而后它在 onLayout 中遍历全部的子元素并调用它的 layout 方法对子元素进行摆放, 而在 layout 中 onLayout 方法又被调用, 如此反复直到布局完成。

简单讲:layout 方法肯定 View 自己的位置, onLayout 肯定全部子元素的位置布局

layout 过程 :

在看源码以前,先提出一个问题, View 的 getWidth()getMeasuredWidth() 有什么区别?post

public void layout(int l, int t, int r, int b) {
	...
	boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame (l, t, r, b) : setFrame(l, t, r, b);
	if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
	        onLayout(changed, l, t, r, b);
	...
}
复制代码

layout 方法中, 先调用 setFrame() 给本身的四个顶点赋值, 就肯定了本身的位置。而后调用 onLayout 方法对子元素进行摆放优化

protected boolean setFrame(int left, int top, int right, int bottom) {
	...
	mLeft = left;
	mTop = top;
	mRight = right;
	mBottom = bottom;
	...
}
复制代码

看到没,mLeft、mTop 、mRight 、mBottom 就是这个 View 的四个顶点,当四个顶点的值呗肯定,View 的位置就摆放完了。 因为在 View 中 onLayout() 方法是空实现, ViewGroup 的 onLayout() 是抽象方法, 因此就挑一个 ViewGroup 经常使用的子类 FrameLayout 看一下 (其它都相似, 本身能够去看下):ui

#FrameLayout - onLayoutthis

protected void onLayout(boolean changed, int left, int top, int right, int bottom){
    layoutChildren(left, top, right, bottom, false);
}
复制代码

onLayout 的参数直接传给 layoutChildren,继续走:

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    ...
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();
            ...
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
            ...
复制代码

很明显, 在 layoutChildren 遍历全部的子元素, 并调用其 layout 方法来摆放子元素,这样父容器在 layout 方法中完成本身的摆放后,经过 onLayout 方法去遍历调用子元素的 layout 方法,子元素又会经过 layout 方法肯定本身的位置,这样一层一层传递下去从而完成整个 View 树的 layout 过程。

  • 注意看调用 layout 传的参数,这里传入的 widthheight, 其实就是这个 view 的测量宽/高。前面分析了 layout 中传入的参数会对 View 的四个顶点(mLeft、mTop 、mRight 、mBottom)赋值来肯定位置,这里咱们来看一下 getWidth() 的返回值:
public final int getWidth() {
    return mRight - mLeft;
}

public final int getHeight() {
    return mBottom - mTop;
}
复制代码
  • 结合咱们刚刚的源码分析不难看出来,mRight - mLeft 和 mBottom - mTop 的返回值不就分别是 view 的测量宽高么, 因此在系统 View 的默认实现中,以及开发中咱们能够直接认为 getWidth() = getMeasuredWidth(),只是赋值时间不一样, getHeight 和 getMeasuredHeight 同理getMeasuredWidth() 是在 onMeasure() 方法中执行完成测量流程后并保存尺寸的时候被赋值,getWidth() 是在 layout 方法中肯定本身位置的时候被赋值。

  • 固然也存在两种状况会出现不相等:一种是某些极端状况系统须要屡次执行measure流程,这时则除了最后一次measure,前几回的measure结果就可能存在不相等。另外一种则是在 onLayout() 中调用 layout 时, 对传入的四个顶点值作了一些运算处理, 则这两个值也是不相等的,以下

protected void onLayout(boolean changed, int left, int top, int right, int bottom){	
	...
    child.layout(childLeft, childTop, childLeft + width + 100, childTop + height + 100);
    ...
}
// 或重写 layout 方法
public void layout(int l, int t, int r, int b) {
    super.layout(l, t, r + 100, b + 100);
}
复制代码

draw 过程 :

绘制过程就是将 View 绘制到屏幕上, 它分为4步:

  • (1) 绘制背景(私有方法不能重写)
  • (2) 主体绘制(通常重写此方法)
  • (3) 绘制子元素
  • (4) 绘制前景和滑动相关(绘制前景的支持是在 Android 6.0 以后)

具体从 draw 方法中能够明了的看出来:

public void draw(Canvas canvas) {
	...
	drawBackground(canvas);
	...
	onDraw(canvas);
	...
	dispatchDraw(canvas);
	...
	onDrawForeground(canvas);
	...
}
复制代码

绘制过程的传递是经过 dispatchDraw 来实现,dispatchDraw 中会遍历全部子元素的 draw 方法,如此反复下去直到绘制完成。

setWillNotDraw :

这是 View 的一个特殊方法,具体看源码:

/**
 * If this view doesn't do any drawing on its own, set this flag to * allow further optimizations. By default, this flag is not set on * View, but could be set on some View subclasses such as ViewGroup. * * Typically, if you override {@link #onDraw(android.graphics.Canvas)} * you should clear this flag. * * @param willNotDraw whether or not this View draw on its own */ public void setWillNotDraw(boolean willNotDraw) { setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK); } 复制代码

从注释大体能看出来,若是一个 View 不须要对本身进行任何绘制,设置这个标志位为 true 后,系统会进行相应的优化,即绕过 draw() 方法,换而直接执行 dispatchDraw(),以此来简化绘制流程。默认状况下 View 没有设置这个标志位, 而 ViewGroup 默认会启动这个标志位。

  • 在实际开发中, 若是自定义控件继承于 ViewGroup 而且自己不具有绘制功能时,就能够开启这个标志让系统进行绘制优化。 可是当明确知道一个 ViewGroup 须要在它的除 dispatchDraw() 之外的任何一个绘制方法内绘制内容,你显示的关闭这个 WILL_NOT_DRAW 这个标志位:View.setWillNotDraw(false)

以为有用的话,点个赞再走呗~

相关文章
相关标签/搜索