反思|Android View机制设计与实现:布局流程

反思 系列博客是个人一种新学习方式的尝试,该系列起源和目录请参考 这里html

概述

Android自己的View体系很是宏大,源码中值得思考和借鉴之处众多,以View自己的绘制流程为例,其通过measure测量、layout布局、draw绘制三个过程,最终才可以将其绘制出来并展现在用户面前。android

相比 测量流程布局流程 相对简单不少,若是读者不了解 测量流程 ,建议阅读这篇文章:git

反思 | Android View机制设计与实现:测量流程github

总体思路

测量流程 的目的是 测量控件宽高 ,但只获取控件的宽高其实是不够的,对于ViewGroup而言还须要一套额外的逻辑,负责对全部子控件进行对应策略的布局,这就是 布局流程(layout)。markdown

  • 1.对于叶子节点的View而言,其自己没有子控件,所以通常状况下仅须要记录本身在父控件的位置信息,并不须要处理为子控件布局的逻辑;
  • 2.对于总体的布局流程而言,子控件的位置必然交由父控件布置,和 测量流程 同样,Android中布局流程中也使用了递归思想:对于一个完整的界面而言,每一个页面都映射了一个View树,其最顶端的父控件开始布局时,会经过自身的布局策略依次计算出每一个子控件的位置——值得一提的是,为了保证控件树形结构的 内部自治性,每一个子控件的位置为 相对于父控件坐标系的相对位置 ,而不是以屏幕坐标系为准的绝对位置。位置计算完毕后,做为参数交给子控件,令子控件开始布局;如此往复一直到最底层的控件,当全部控件都布局完毕,整个布局流程结束。

对于布局流程不甚熟悉的开发者而言,上述文字彷佛晦涩难懂,但这些文字的归纳其本质倒是布局流程总体的设计思想,读者不该该将本文视为源码分析,而应该将本身代入到设计的过程当中 ,当深入理解整个流程的设计思路以后,布局流程代码地设计和编写天然行云流水一鼓作气。app

单个View的布局流程

首先思考一个问题,布局流程的本质是测量结束以后,将每一个子控件分配到对应的位置上去——既然有子控件,那说明进行布局流程的主体理应是ViewGroup,那么做为叶子节点的单个View来讲,为何也会有布局流程呢?框架

读者认真思考能够得出,布局流程其实是一个复杂的过程,整个流程主要逻辑顺序以下:ide

  • 1.决定是否须要从新进行测量流程onMeasure()
  • 2.将自身所在的位置信息进行保存;
  • 3.判断本次布局流程是否引起了布局的改变;
  • 4.若布局发生了改变,令全部子控件从新布局;
  • 5.若布局发生了改变,通知全部观察布局改变的监听发送通知。

整个布局过程当中,除了4是ViewGroup自身须要作的,其它逻辑对于ViewViewGroup而言都是公共的——这说明单个View也是有布局流程的需求的。函数

如今将整个布局过程定义三个重要的函数,分别为:oop

  • void layout(int l, int t, int r, int b):控件自身整个布局流程的函数;
  • void onLayout(boolean changed, int left, int top, int right, int bottom):ViewGroup布局逻辑的函数,开发者须要本身实现自定义布局逻辑;
  • void setFrame(int left, int top, int right, int bottom):保存最新布局位置信息的函数;

为何须要定义这样三个函数?

1.layout函数:标志布局的开始

如今咱们站在单个View的角度,首先父控件须要经过调用子控件的layout()函数,并同时将子控件的位置(left、right、top、bottom)做为参数传入,标志子控件自己布局流程的开始:

// 伪代码实现
public void layout(int l, int t, int r, int b) {
  // 1.决定是否须要从新进行测量流程(onMeasure)
  if(needMeasureBeforeLayout) {
    onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec)
  }

  // 先将以前的位置信息进行保存
  int oldL = mLeft;
  int oldT = mTop;
  int oldB = mBottom;
  int oldR = mRight;
  // 2.将自身所在的位置信息进行保存;
  // 3.判断本次布局流程是否引起了布局的改变;
  boolean changed = setFrame(l, t, r, b);

  if (changed) {
    // 4.若布局发生了改变,令全部子控件从新布局;
    onLayout(changed, l, t, r, b);
    // 5.若布局发生了改变,通知全部观察布局改变的监听发送通知
    mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
  }
}
复制代码

这里笔者经过伪代码的方式对布局流程进行了描述,实际上View自己的layout()函数内部虽然多处不一样,但核心思想是一致的——layout()函数实际上表明了控件自身布局的整个流程,setFrame()onLayout()函数都是layout()中的一个步骤。

2.setFrame函数:保存本次布局信息

为何须要保存布局信息?由于咱们老是有获取控件的宽和高的需求——好比接下来的onDraw()绘制阶段;而保存了布局信息,就能经过这些值计算控件自己的宽高:

public final int getWidth() { return mWidth; }

public final int getHeight() { return mHeight; }
复制代码

因而可知,保存控件的布局信息确实颇有必要,Android中将layout()函数的四个参数所表明的位置信息,交给了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;
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
    }
    return changed;
}
复制代码

setFrame()函数被protected修饰,这意味着开发者能够经过重写该函数来定义View自己保存布局信息的逻辑,如今将目光转到mLeft、mTop、mRight、mBottom四个变量上。

顾名思义,这四个变量对应的天然是View自身所在的位置,那么View是如何经过这四个变量描述控件的位置信息呢?

3.相对位置和绝对位置

经过一张图来看一下这四个变量所表明的意义:

这时候不可避免的会面临另一个问题,这个mLeft、mTop、mRight、mBottom的值所对应的坐标系是哪里呢?

这里须要注意的是,为了保证控件树形结构的 内部自治性,每一个子控件的位置为 相对于父控件坐标系的相对位置 ,而不是以屏幕坐标系为准的绝对位置:

反过来想,若是这些位置信息是以屏幕坐标系为准,那么就意味着每一个叶子节点的View会持有保存从根节点ViewGroup直到自身父ViewGroup每一个控件的位置信息,在计算布局时则更为繁琐,很明显是不合理的设计。

既然View自身持有了这样的位置信息,实际上前文中获取控件自身宽高的getWidth()getHeight()方法就能够从新这样定义:

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

public final int getHeight() { return mBottom - mTop; }
复制代码

这也说明了在布局流程中的setFrame()函数执行完毕后(且布局确实发生了改变),开发者才能经过getWidth()getHeight()方法获取控件正确的宽高值。

4.onLayout函数:计算子控件的位置

对于叶子节点的View而言,其并无子控件,所以通常状况下并无为子控件布局的意义(特殊状况请参考AppCompatTextView等类),所以ViewonLayout()函数被设计为一个空的实现:

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

而在ViewGroup中,不一样类型的ViewGroup有不一样的布局策略,这些布局策略的逻辑各不相同,所以该方法被设计为抽象接口,开发者必须实现这个方法以定义ViewGroup的布局策略:

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

LinearLayout为例,其布局策略为 根据排布方向,将其全部子控件按照指定方向依次排列布局

至此单个View的测量流程结束,关于ViewGrouponLayout函数细节将在下文进行描述。

完整布局流程

相比较测量流程,布局流程相对比较简单,总体思路是,对于一个完整的界面而言,每一个页面都映射了一个View树,最顶端的父控件开始布局时,会经过自身的布局策略依次计算出每一个子控件的位置。位置计算完毕后,做为参数交给子控件,令子控件开始布局;如此往复一直到最底层的控件,当全部控件都布局完毕,整个布局流程结束。

ViewGroup虽然重写了Viewlayout()函数,但实质上并未进行大的变更,咱们大抵能够认为ViewGroupViewlayout()逻辑一致:

@Override
public final void layout(int l, int t, int r, int b) {
    if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
        if (mTransition != null) {
            mTransition.layoutChange(this);
        }
        // 仍然是执行View层的layout函数
        super.layout(l, t, r, b);
    } else {
        mLayoutCalledWhileSuppressed = true;
    }
}
复制代码

惟一须要注意的是,开发者必须实现onLayout()函数以定义ViewGroup的布局策略,这里以 竖直布局LinearLayout的伪代码为例:

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

  // 遍历全部子View
  for (int i = 0; i < count; i++) {
    // 获取子View
    final View child = getVirtualChildAt(i);
    // 获取子View宽高,注意这里使用的是 getMeasuredWidth 而不是 getWidth
    final int childWidth = child.getMeasuredWidth();
    final int childHeight = child.getMeasuredHeight();

    // 令全部子控件开始布局
    setChildFrame(child, childLeft, childTop, childWidth, childHeight);   
    // 高度累加,下一个子View的 top 就等于上一个子View的 bottom ,符合竖直线性布局从上到下的布局策略 
    childTop += childHeight;      
  }
}

private void setChildFrame(View child, int left, int top, int width, int height) {
    // 这里能够看到,子控件的mRight实际上就是 mLeft + getMeasuredWidth()
    // 而在getWidth()函数中,mRight-mLeft的结果就是getMeasuredWidth()
    // 所以,getWidth() 和 getMeasuredWidth() 是一致的
    child.layout(left, top, left + width, top + height);
}
复制代码

读者须要注意到一个细节,子控件的宽度的获取,咱们并未使用getWidth(),而是使用了getMeasuredWidth(),这就引起了另一个疑问,这两个函数的区别在哪里。

getWidth 和 getMeasuredWidth 的区别

首先,从上文中咱们得知,getWidth()getHeight()函数的相关信息其实是在setFrame()函数执行完毕才准备完毕的——咱们大体能够认为是这两个函数 只有布局流程(layout)执行完毕才能调用,而在父控件的onLayout()函数中,获取子控件宽度和高度时,子控件还并未开始进行布局流程,所以此时不能调用getWidth()函数,而只能经过getMeasuredWidth()函数获取控件测量阶段结果的宽度。

那么当控件绘制流程执行完毕后,getWidth()getMeasuredWidth()函数的值有什么区别呢?从上述setChildFrame()函数中的源码能够得知,布局流程执行后,getWidth()返回值的本质其实就是getMeasuredWidth()——所以本质上,当咱们没有手动调用layout()函数强制修改控件的布局信息的话,两个函数的返回值大小是彻底一致的。

总体流程小结

在整个布局流程的设计中,设计者将流程中公共的业务逻辑(保存布局信息、通知布局发生改变的监听等)经过layout()函数进行了整合,同时,将ViewGroup额外须要的自定义布局策略经过onLayout()函数向外暴露出来,针对组件中代码的可复用性和可扩展性进行了合理的设计。

至此,布局流程总体实现完毕。借用 carson_ho 绘制的流程图对总体布局流程作一个总结:

参考


关于我

Hello,我是 却把清梅嗅 ,若是您以为文章对您有价值,欢迎 ❤️,也欢迎关注个人 博客 或者 Github

若是您以为文章还差了那么点东西,也请经过关注督促我写出更好的文章——万一哪天我进步了呢?

相关文章
相关标签/搜索