Android 页面 Layout 布局

发布于 2024-06-26 05:51:44 字数 10159 浏览 19 评论 0

布局过程,就是把界面中的所有控件,用他们的要求的大小,摆放在正确的位置。更通俗的将,就是程序在运行时利用布局文件的代码来计算出实际尺寸的过程。

安卓的布局过程

包括两个阶段:测量阶段和布局阶段。

  1. 测量阶段:从上到下递归地测量每一级,每一个子 View 的尺寸并计算它们的位置;
  2. 布局阶段:依然是从最顶级的 View 向下递归地把测得的它们的 尺寸位置 赋值给它们。

这样每个 View 的尺寸和位置就确定了,布局过程就算完成了。接下来就是绘制规程了,每个 View 会根据这些得到的尺寸和位置来进行自我绘制。

测量过程的调用

布局过程

  1. 当测量过程到来时,一个 View 的 measure() 方法会被它的父节点调用(根节点的 View 不是一个 View 对象)。 通知该 View 进行自我测量,而真正进行测量不是 measure() 方法,而是调用自己的 onMeasure() 方法。 measure 是一个调度方法,它会做一些测量的预处理工作,然后调用 onMeasure() 来进行真正的测量。

     

  2. onMeasure() 的自我测量包含两部分内容,要看这个 View 是 简单的 View 还是 ViewGroup

     

    • 如果它是一个简单的 View, 它做的事情只有一件,测量出自己的尺寸。
    • 如果它是一个 ViewGroup,它会先调用所有子 View 的 measure() 方法。让他们都进行自我测量。然后根据这些子 View 自我测量出的尺寸来计算出他们的位置,并且把他们的尺寸和位置保存下来。同时,根据这些子 View 的尺寸和位置,最终计算出自己的尺寸。 也就是说,ViewGroup 的尺寸主要有子 View 的尺寸和位置确定的。

布局截断的调用

一个 View 的 layout(...) 方法会被它的父节点所调用,用于对 View 进行内部布局。和 measure() 方法一样, layout(...) 也只是一个调度方法,实际进行布局的是 onLayout 方法。 onLayout 内部会做两件事。 首先 layout 方法是有参数的,它的父几点在调用它的时候回把之前测量截断保存下来的这个 View 的尺寸和位置通过参数给传进来,而 layout 做的第一件事就是把这个尺寸和位置保存下来。测量阶段是父 View 统一保存所有子 View 的尺寸和位置,而到了布局阶段就是 View 保存自己的尺寸和位置了。 第二件事是它会调用自己的 onLayout 方法,这 onLayout 对自己进行真正的内部布局。 内部布局的意思是,它会调用每一个子 View 的 layout 方法,并且把他们的尺寸和位置作为参数传递给它们。对于简单的 View 来处,它的 onLayout 什么也不用做,就是一个空方法,之所以也要调用,是逻辑的统一。

布局过程的自定义

就是重写 View 的测量过程和布局过程的相关方法,以此来定制自己想要的尺寸和排放效果,

注意: 重写的是 onMeasureonLayout 方法,因为 measurelayout 是用来调度的,而真实进行测量的布局的是 onMeasureonLayout 方法。

具体过程可以分为三类

  1. 重写 onMeasure 来修改已有的 View 的尺寸。
  2. 重写 onMeasure 来全新计算自定义 View 的尺寸。
  3. 重写 onMeasureonLayout 来全新计算自定 ViewGroup 的内部布局。

    1. 重写 onMeasure 来修改已有的 View 的尺寸

这一类是对一些已有的 View 进行修改尺寸。例如,修改 ImageView 的尺寸就属于这一类。他已经有自己的尺寸计算算法了,它的 onMeasure 已经正确计算出它的尺寸和位置,你不需要从新进行计算一遍。 只需要根据自己的需要进行相应的调整。做法

  1. 在重写的 onMeasure 方法中先调用 super.onMeasure 方法让它进行一次原有的测量。
  2. 然后增加代码,计算得到想要的尺寸。通过 getMeasuredWidthgetMeasuredHeight 获得测得的尺寸,然后重新计算尺寸。
  3. 保存新尺寸。计算尺寸并不是通过返回值返回给父 View 的,而是通过 setMeasuredDimension 方法把它存在自己内部。所有取的时候也是要用相应的方法来取: getMeasuredWidthgetMeasuredHeight

注意,setMeasuredDimension 保存的是一个测得的尺寸,它会之后通过 layout 方法传进来的那个尺寸未必是相等的。这个 setMeasuredDimension 保存的尺寸是一个 View 对自己尺寸的期望值。然后父 View 会根据这个期望值再去判断,至于最终它是否会同意你这么大,还是要求你再去测量一次,或者是给你指派一个新尺寸,这个由父 View 来决定。最终它会通过 layout 的方法的参数来传给你。


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 先执行原测量算法
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // 获得原先的测量结果
    int measuredWidth = getMeasuredWidth();
    int measuredHeight = getMeasuredHeight();

    // 利用原先的测量结果计算出新尺寸。
    if (measuredWidth > measuredHeight) {
        measuredWidth = measuredHeight;
    } else {
        measuredHeight = measuredWidth;
    }
    // 保存计算后的尺寸。
    setMeasuredDimension(measuredWidth, measuredHeight);
}

2. 重写 onMeasure 来全新计算自定义 View 的尺寸

主要用于自定义 View, 这些 View 的绘制完全自己写的,这时,在尺寸上就没有现有的可作为基准的值修改使用。需要完全自己去计算它的尺寸。这种测量和第一种是有点不一样的:

  1. 不需要调用父类的 onMeasure 方法,而是完全自己计算尺寸。计算所有内部绘制的他们的间距,边距,得到最后的尺寸即可。
  2. 全新计算 View 尺寸,需要保证自己计算出的尺寸满足父布局的限制: 就是 onMeasure 方法的参数,父布局在调用子 view 的 measure 方法时,会将父布局对子 View 的限制作为参数传递过来, measure 方法在调用 onMeasure 方法时,又会原封不动地将其传递给 onMeasure 方法。

     

    1. 限制是怎么来的?父 View 为什么会对子 View 进行限制? 父 View 把开发者对子 View 的尺寸要求(就是开发者在 xml 文件中这个 View 写的以 layout_ 开头的属性限制,它们使用来设置这个 View 的位置和尺寸的。)进行处理计算之后所得到的更精确的要求。 layout_ 开头的属性不是给 View 自己看的,而是给它的父 View 看的。也就是说在程序运行显示界面的时候,没一个 ViewGroup 会读取它的子 View 的 layout_ 开头属性,然后用他们进行处理和计算,得出一个限制。这个限制分为三种:不限制,设置上限,固定值(为了流程的通一,要求子 View 计算一遍)。如果不遵循这个限制,就会产生 bug,例如,这个现实是 500,偏要使用 400,这时使用者写了 match_parent 属性,却发现并没有填充满父布局的可用尺寸,从而属性无效,达不到开发者的要求。
    2. 子 View 的 onMeasure 方法里面应该怎么做,才能满足父布局的限制。

    很简单,在计算完尺寸后,调用一个 resolveSize 来调整计算的数值,返回值就是修正之后的尺寸。然后把修正之后尺寸用 setMeasuredDimension 保存起来就行了。

setMeasuredDimension 方法的处理过程

public static int resolveSize(int size, int measureSpec, int childMeasuredState) {
    // 父 View 传过来的宽度和高度限制都是一个压缩数据,包括限制类型和尺寸两部分。可以通过 `MeasureSpec.getMode` 来获取限制类型,`MeasureSpec.getSize` 获得限制尺寸值。
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        // 对于限制上限,取较小的值。
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        // 对于指定值,返回指定尺寸。
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        // 对与不限制的, 直接返回计算的值,
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

3. 重写 onMeasureonLayout 来全新计算自定 ViewGroup 的内部布局

用于重写 ViewGroup 类型的 View.

  1. 重写 onMeasure 来计算内部元素的尺寸和位置,以及自己的尺寸。分三步:
    1. 调用每个子 View 的 measure 方法,来让你子 View 自我测量。
    2. 根据子 View 计算出的尺寸,得出子 View 的位置,并保存它们的尺寸和位置。
    3. 根据子 View 的尺寸和位置计算出自己的尺寸,并用 setMeasuredDimension 方法保存下来。
  2. 重写 onLayout 来摆放内部元素。

1. 重写 onMeasure 来计算内部元素的尺寸和位置,以及自己的尺寸

第一步:调用每个子 View 的 measure 方法,来让你子 View 自我测量

使用 for 循环调用子 view 的 measure 方法,而这个方法有两个尺寸限制参数,而这个限制并不是现成的,需要根据子 View 的属性设置(xml 文件)和自己的可用空间(剩余)限制,计算出来。开发者的要求(属性设置)在地位上要绝对高于可用空间。例如,开发者写了 layout_width="48dp" 那就不用管剩余空间有没有 48dp 了,直接设置子 View 的尺寸是固定 48dp 就好了。即,mode 是 EXACTLY ,width 是 48dp 对应的像素值。

获取子 View 的 layout_ 开头的属性,可以使用子 View 的 getLayoutParams() 方法,获得一个 LayoutParams 对象,它包含了 xml 文件里 layout_ 开头的参数的对应值。 LayoutParams 对象的 widthheight 属性值,就对应了 layout_widthlayout_height 值。它的值可以是 WRAP_CONTENTMATCH_PARENT 这两个常量,或者准换后的具体像素值。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int usedWidth, usedHeight;
    for (int i = 0; i < getChildCount(); i++) {
        View childView = getChildAt(i);
        ViewGroup.LayoutParams lp = childView.getLayoutParams();
        int childWidthSpec;
        int selfWidthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int selfWidthSpacSize = MeasureSpec.getSize(widthMeasureSpec);
        switch (lp.width) {
            // 如果是填满父布局,则也使用固定尺寸,尺寸是自己的可用宽度/高度。这时候自己的尺寸还没有计算出来。
            // 但是有父布局传过来的 `widthMeasureSpec` 和 `heightMeasureSpec` 尺寸限制。这个尺寸限制,
            // 虽然不是自己的最终实际尺寸,但依据这个限制,自己可以得到一个可用空间。得出自己最多有多啊到地方给
            // 子 View 用。
            case MATCH_PARENT:
                if (selfWidthSpecMode == EXACTLY || selfWidthSpecMode == MeasureSpec.AT_MOST) {
                    childWidthSpec = MeasureSpec.makeMeasureSpec(selfWidthSpacSize - usedWidth, EXACTLY);
                } else {
                    // 较新版本的 Android, UNSPECIFIED 的 size 也有自己的作用,后期研究。
                    childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
                }
                // 如果开发者在子 view 写的是固定尺寸值,直接设置为 `EXACTLY`。压缩方法使用 MeasureSpec.makeMeasureSpec
                break;
            // WRAP_CONTENT 虽然没有说父布局没有限制,但包含一个隐含条件,就是不能草果不父布局的可用宽度。
            case WRAP_CONTENT:
                if (selfWidthSpecMode == EXACTLY || selfWidthSpecMode == MeasureSpec.AT_MOST) {
                    childWidthSpec = MeasureSpec.makeMeasureSpec(selfWidthSpacSize - usedWidth, MeasureSpec.AT_MOST);
                } else {
                    // 较新版本的 Android, UNSPECIFIED 的 size 也有自己的作用,后期研究。
                    childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
                }
            default:
                childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, EXACTLY);
                break;
        }

        ... // height 同样计算



    }
}
第二步:根据子 View 计算出的尺寸,得出子 View 的位置,并保存它们的尺寸和位置。

调用子 View 的 测量方法,就能得出子 View 的尺寸,绝大多数情况,每一个 View,它测得的尺寸就是它的最终尺寸,要用到的时候,调用它的 getMeasuredWidthgetMeasuredHeight 就行。 为什么要保存这些尺寸? 因为现在是测量阶段,到布局阶段,这些尺寸和位置才会最终传给子 view。有两点注意:

  1. 并不是所有的 Layout 都要保存子 View 的位置。比如 LinearLayout,它的内容都是横向或纵向一字排开的。那么子 View 的位置就可以在布局阶段通过一个一个的把尺寸累加起来得到。这种就不需要保存位置了。
  2. 在某些时候,对子 View 进行一次测量是不够的。可能进行多次测量才能得到正确的尺寸和位置。根据自己想要达到的目的,可以增加测量的次数,以及选择性的测量某些 View。
第三步:根据子 View 的尺寸和位置计算出自己的尺寸,并用 setMeasuredDimension 方法保存下来

根据子 View 的尺寸和排布,计算出边界,保存即可。

2. 重写 onLayout 来摆放内部元素

重写 onLayout 很简单,只需要调用每一个子 view 的 layout 方法,把 onMeasure 保存的位置和尺寸传进去即可。

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            View viewChild = getChildAt(i);
            viewChild.layout(childLeft[i], childTop[i], childRight[i], childBottom[i]);
        }
    }

子 View 的 layout 参数是它在父 View 中的相对坐标。需要把位置和尺寸转化一下。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

0 文章
0 评论
23 人气
更多

推荐作者

月光色

文章 0 评论 0

咆哮

文章 0 评论 0

痞味浪人

文章 0 评论 0

宁涵

文章 0 评论 0

军弟

文章 0 评论 0

临走之时

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文