故事:View 树大厦装修

89 阅读4分钟

故事背景:View 树大厦装修记

想象一下,你是一位装修设计师,负责一栋名为 "View 树大厦" 的建筑装修工作。这栋大厦有一个独特的结构:它由许多房间(View)组成,每个房间可能包含更小的房间(子 View),形成一个树形结构。你的任务是确保每个房间都有合适的大小和位置。

装修的两个关键阶段

在装修过程中,有两个关键阶段:

  1. 测量阶段(onMeasure) :确定每个房间需要多大的空间
  2. 布局阶段(onLayout) :确定每个房间的具体位置

代码实现与故事结合

让我们通过一个简单的代码示例来理解这个过程。假设我们有一个自定义的 LivingRoom 类,它包含一个 TV 和一个 Sofa

java

public class LivingRoom extends ViewGroup {
    private TV tv;
    private Sofa sofa;

    public LivingRoom(Context context) {
        super(context);
        init();
    }

    private void init() {
        // 创建并添加子 View
        tv = new TV(getContext());
        sofa = new Sofa(getContext());
        addView(tv);
        addView(sofa);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 故事场景:设计师测量客厅大小
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        // 1. 首先测量子 View(TV 和 Sofa)
        measureChild(tv, widthMeasureSpec, heightMeasureSpec);
        measureChild(sofa, widthMeasureSpec, heightMeasureSpec);

        // 2. 根据子 View 的大小和布局规则,计算客厅的最终大小
        int finalWidth = calculateFinalWidth(widthMode, widthSize);
        int finalHeight = calculateFinalHeight(heightMode, heightSize);

        // 3. 设置客厅的测量大小
        setMeasuredDimension(finalWidth, finalHeight);
    }

    private int calculateFinalWidth(int widthMode, int widthSize) {
        // 计算客厅宽度的逻辑
        // 例如:如果是 EXACTLY 模式,直接使用父容器指定的大小
        // 否则,根据子 View 的宽度和内边距计算
        return widthSize;
    }

    private int calculateFinalHeight(int heightMode, int heightSize) {
        // 计算客厅高度的逻辑
        return heightSize;
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 故事场景:设计师确定家具摆放位置
        
        // 获取客厅的尺寸
        int width = r - l;
        int height = b - t;

        // 1. 确定 TV 的位置(例如:放在客厅顶部中央)
        int tvWidth = tv.getMeasuredWidth();
        int tvHeight = tv.getMeasuredHeight();
        int tvLeft = (width - tvWidth) / 2;
        int tvTop = 0;
        tv.layout(tvLeft, tvTop, tvLeft + tvWidth, tvTop + tvHeight);

        // 2. 确定 Sofa 的位置(例如:放在客厅底部中央)
        int sofaWidth = sofa.getMeasuredWidth();
        int sofaHeight = sofa.getMeasuredHeight();
        int sofaLeft = (width - sofaWidth) / 2;
        int sofaTop = height - sofaHeight;
        sofa.layout(sofaLeft, sofaTop, sofaLeft + sofaWidth, sofaTop + sofaHeight);
    }
}

详细解释装修过程

1. 测量阶段(onMeasure)

这个阶段就像是设计师为每个房间和家具测量尺寸的过程:

  • 父容器的限制:当设计师开始测量客厅时,建筑图纸(父容器)可能会对客厅的大小有一些限制。例如:

    • EXACTLY:客厅必须是指定的大小(例如:5 米 ×4 米)
    • AT_MOST:客厅最大可以是指定的大小(例如:最大 5 米 ×4 米)
    • UNSPECIFIED:客厅可以是任意大小(例如:设计师可以自由发挥)
  • 子 View 的测量:设计师首先需要测量 TV 和 Sofa 的大小。这就像是让家具制造商提供家具的尺寸信息。

  • 计算最终大小:根据子 View 的大小和布局规则,设计师计算出客厅的最终大小。例如,如果 TV 和 Sofa 的总高度是 2.5 米,设计师可能会决定客厅的高度至少需要 3 米。

2. 布局阶段(onLayout)

这个阶段就像是设计师根据测量结果摆放家具的过程:

  • 确定位置:设计师根据客厅的尺寸和家具的大小,确定每个家具的具体位置。例如:

    • TV 放在客厅顶部中央
    • Sofa 放在客厅底部中央
  • 递归布局:如果 TV 或 Sofa 本身也是容器(例如:Sofa 上有抱枕),那么它们也会重复这个测量和布局的过程,直到所有的子 View 都被布局好。

整个 View 树的装修过程

当 Android 系统需要显示界面时,它会从根 View 开始,递归地执行测量和布局过程:

  1. 根 View(大厦)  首先进行测量,确定自己的大小

  2. 根 View 然后测量它的子 View(例如:客厅、卧室、厨房)

  3. 每个子 View 继续测量自己的子 View,直到所有 View 都被测量

  4. 接着从根 View 开始,递归地进行布局,确定每个 View 的位置

  5. 最终,所有 View 都有了自己的大小和位置,界面就可以被绘制出来了

通过这个装修故事,你可以理解 Android View 树的布局过程其实就像设计师装修一栋大楼一样,需要先测量每个房间和家具的大小,然后再确定它们的位置。这个过程是递归进行的,从根 View 开始,直到所有的子 View 都被处理完毕。