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

系列文章传送门 (持续更新中..) :
自定义控件(一) Activity的构成(PhoneWindow、DecorView)
自定义控件(二) 从源码分析事件分发机制
自定义控件(三) 源码分析measure流程


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

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

简单讲:layout 方法确定 View 本身的位置, onLayout 确定所有子元素的位置

layout 过程 :

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

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 看一下 (其它都类似, 自己可以去看下):

#FrameLayout - onLayout

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)

觉得有用的话,点个再走呗~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值