UI-自定义布局

自定义布局

布局过程

  • 确定每个 View的位置和尺⼨
  • 作用:为绘制和触摸范围做⽀持
    • 绘制:知道往哪⾥绘制
    • 触摸反馈:知道用户点的是哪⾥

流程

  • 从整体看:
    • 测量流程:从根 View递归调用每⼀级子 Viewmeasure() ⽅法,对它们进⾏测量
    • 布局流程:从根 View递归调用每⼀级子 Viewlayout() ⽅法,把测量过程得出的子 View 的位置和尺⼨传给子 View,子 View保存
    • 为什么要分两个流程?
  • 从个体看,对于每个 View
    1. 运⾏前,开发者在 xml ⽂件⾥写⼊对 View的布局要求 layout_xxx
    2. View在自己的 onMeasure() 中,根据开发者在 xml 中写的对子 View的要求,和自己的可用空间,得出对子 View的具体尺⼨要求
    3. View在自己的 onMeasure() 中,根据自己的特性算出自己的期望尺⼨如果是 ViewGroup,还会在这⾥调用每个子 Viewmeasure() 进⾏测量
    4. View在子 View计算出期望尺⼨后,得出子 View的实际尺⼨和位置
    5. View在自己的 layout() ⽅法中,将⽗ View传进来的自己的实际尺⼨和位置保存如果是 ViewGroup,还会在 onLayout() ⾥调用每个字 View的 layout() 把它们的尺⼨位置传给它们

具体开发

  • 继承已有的 View,简单改写它们的尺⼨:重写 onMeasure()SquareImageView

    1. 重写 onMeasure()

    2. getMeasuredWidth()getMeasuredsize() 获取到测量出的尺⼨

    3. 计算出最终要的尺⼨

    4. setMeasuredDimension(width, height) 把结果保存

      1
      2
      3
      4
      5
      6
      7
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      int width = getMeasuredWidth();
      int height = getMeasuredHeight();
      int `size` = Math.min(width, height);
      setMeasuredDimension(`size`, `size`);
      }
  • 对自定义 View完全进⾏自定义尺⼨计算:重写 onMeasure()CircleView

    1. 重写 onMeasure()

    2. 计算出自己的尺⼨

    3. resolvesize() 或者 resolvesizeAndState() 修正结果

      • resolvesize() / resolvesizeAndState() 内部实现(⼀定读⼀下代码,这个极少需要自己写,但⾯试时很多时候会考):
        • ⾸先用 MeasureSpec.getMode(measureSpec)MeasureSpec.getsize(measureSpec) 取出⽗ 对自己的尺⼨限制类型和具体限制尺⼨;
        • 如果 measure specmodeEXACTLY,表示⽗ View对子 View的尺⼨做出了精确限制,所以就放弃计算出的 size,直接选用 measure specsize
        • 如果 measure specmodeAT_MOST,表示⽗ View对子 View的尺⼨只限制了上限,需要看情况:
          • 如果计算出的 size 不⼤于 spec 中限制的 size,表示尺⼨没有超出限制,所以选用计算出的 size
          • ⽽如果计算出的 size ⼤于 spec 中限制的 size,表示尺⼨超限了,所以选用spec 的 size,并且在 resolvesizeAndState() 中会添加标志 MEASURED_STATE_TOO_SMALL(这个标志可以辅助⽗ View做测量和布局的计算;
        • 如果 measure specmodeUNSPECIFIED,表示⽗ View对子 View没有任何尺⼨限制,所以直接选用计算出的 size,忽略 spec 中的 size
    4. 使用 setMeasuredDimension(width, height) 保存结果

      1
      2
      3
      4
      5
      6
      7
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      int width = (int) ((PADDING + RADIUS) * 2);
      int height = (int) ((PADDING + RADIUS) * 2);
      setMeasuredDimension(resolve`size`AndState(width, widthMeasureSpec,
      0),
      resolve`size`AndState(height, heightMeasureSpec, 0));
      }
  • 自定义 Layout:重写 onMeasure()onLayout()TagLayout

    1. 重写 onMeasure()

      1. 遍历每个子 View,用 measureChildWidthMargins() 测量子 View

        • 需要重写 generateLayoutParams() 并返回 MarginLayoutParams 才能使用measureChildWithMargins() ⽅法

        • 有些子 View可能需要重新测量(⽐如换⾏处)

        • 测量完成后,得出子 View的实际位置和尺⼨,并暂时保存

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          protected void onMeasure(int widthMeasureSpec, int
          heightMeasureSpec) {
          for (int i = 0; i < getChildCount(); i++) {
          `View`child = getChildAt(i);
          Rect childBounds = childrenBounds[i];
          // 测量子 View
          measureChildWithMargins(child, widthMeasureSpec,
          widthUsed,
          heightMeasureSpec, heightUsed);
          // 保存子 `View`的位置和尺⼨
          childBounds.set(childlLeft, childTop, childLeft
          + child.getMeasuredWidth(), chiltTop
          + child.getMeasuredHeight());
          ......
          }
          // 计算自己的尺⼨,并保存
          int width = ...;
          int height = ...;
          setMeasuredDimension(resolve`size`AndState(width,
          widthMeasureSpec, 0),
          resolve`size`AndState(height, heightMeasureSpec,
          0));
          }
        • measureChildWidthMargins() 的内部实现(最好读⼀下代码,这个极少需要自己写,但⾯试时很多时候会考):

          • 通过 getChildMeasureSpec(int spec, int padding, int childDimension) ⽅法计算出子 ViewwidthMeasureSpecheightMeasureSpec,然后调用 child.measure() ⽅法来让子 View自我测量;

            1
            2
            3
            4
            5
            6
            7
            8
            9
            10
            11
            12
            13
            14
            15
            16
            17
            // ViewGroup.measureChildWithMargins() 源码
            protected void measureChildWithMargins(`View`child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
            final MarginLayoutParams lp =
            (MarginLayoutParams) child.getLayoutParams();
            final int childWidthMeasureSpec =
            getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin
            + lp.rightMargin + widthUsed, lp.width);
            final int childHeightMeasureSpec =
            getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin
            + lp.bottomMargin + heightUsed, lp.height);
            child.measure(childWidthMeasureSpec,
            childHeightMeasureSpec);
            }

            getChildMeasureSpec(int spec, int padding, int childDimension) ⽅法的内部实现是,结合开发者设置的 LayoutParams 中的 widthheight 与⽗ View自己的剩余可用空间,综合得出子 View的尺⼨限制,并使用 MeasureSpec.makeMeasureSpec(size, mode) 来求得结果:

            1
            2
            3
            4
            5
            6
            7
            8
            9
            10
            11
            12
            13
            14
            15
            16
            17
            18
            19
            20
            21
            22
            23
            24
            25
            26
            27
            28
            29
            30
            31
            32
            33
            34
            35
            36
            37
            38
            39
            40
            41
            42
            43
            44
            45
            46
            47
            48
            49
            50
            51
            52
            53
            54
            55
            56
            57
            58
            59
            60
            61
            62
            63
            64
            65
            66
            67
            68
            69
            70
            71
            72
            73
            74
            75
            76
            77
            78
            // ViewGroup.getChildMeasureSpec() 源码
            public static int getChildMeasureSpec(int spec, int
            padding,
            int childDimension) {
            int specMode = MeasureSpec.getMode(spec);
            int spec`size` = MeasureSpec.get`size`(spec);
            int `size` = Math.max(0, spec`size` - padding);
            int result`size` = 0;
            int resultMode = 0;
            switch (specMode) {
            // Parent has imposed an exact `size` on us
            case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
            result`size` = childDimension;
            resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension ==
            LayoutParams.MATCH_PARENT) {
            // Child wants to be our `size`. So be it.
            result`size` = `size`;
            resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension ==
            LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own `size`. It
            can't be
            // bigger than us.
            result`size` = `size`;
            resultMode = MeasureSpec.AT_MOST; }
            break;
            // Parent has imposed a maximum `size` on us
            case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
            // Child wants a specific `size`... so be it
            result`size` = childDimension;
            resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension ==
            LayoutParams.MATCH_PARENT) {
            // Child wants to be our `size`, but our `size` is
            not fixed.
            // Constrain child to not be bigger than us.
            result`size` = `size`;
            resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension ==
            LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own `size`. It
            can't be
            // bigger than us.
            result`size` = `size`;
            resultMode = MeasureSpec.AT_MOST; }
            break;
            // Parent asked to see how big we want to be
            case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
            // Child wants a specific `size`... let him have
            it
            result`size` = childDimension;
            resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension ==
            LayoutParams.MATCH_PARENT) {
            // Child wants to be our `size`... find out how
            big it should
            // be
            result`size` =
            View.sUseZeroUnspecifiedMeasureSpec ? 0 : `size`;
            resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension ==
            LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own `size`....
            find out how
            // big it should be
            result`size` =
            View.sUseZeroUnspecifiedMeasureSpec ? 0 : `size`;
            resultMode = MeasureSpec.UNSPECIFIED; }
            break;
            }
            //noinspection ResourceType
            return MeasureSpec.makeMeasureSpec(result`size`,
            resultMode);
            }

            注意:源码中的分类⽅式是先⽐较自己的 MeasureSpec 中的 mode,再

            ⽐较开发者设置的 layout_widthlayout_height,⽽我给出的判断⽅式

            (下⾯的这⼏段内容)是先⽐较 layout_widthlayout_height,再⽐较

            自己 MeasureSpec 中的 mode。两种分类⽅法都能得出正确的结果,但

            源码中的分类⽅法在逻辑上可能不够直观,如果你读源码理解困难,可以

            尝试用我上⾯的这种⽅法来理解。

            1. 如果开发者写了具体值(例如 layout_width="24dp"),就不用再考虑⽗View的剩余空间了,直接用 LayoutParams.width / height 来作为子 View的限制 size,⽽限制 modeEXACTLY(为什么?课堂上说过,因为软件的直接开发者——即 xml 布局⽂件的编写者——的意⻅最重要,发⽣冲突的时候应该以开发者的意⻅为准。换个⻆度说,如果真的由于冲突导致界⾯不正确,开发者可以通过修改 xml ⽂件来解决啊,所以开发者的意⻅是第⼀位,但你如果设计成冲突时开发者的意⻅不在第⼀位,就会导致软件的可配置性严重降低);
            2. 如果开发者写的是 MATCH_PARENT,即要求填满⽗控件的可用空间,那么由于自己的可用空间和自己的两个 MeasureSpec 有关,所以需要根据自己的 widthMeasureSpecheightMeasureSpec 中的 mode 来分情况判断:
            3. 如果自己的 spec 中的 modeEXACTLY 或者 AT_MOST,说明自己的尺⼨有上限,那么把 spec 中的 size 减去自己的已用宽度或⾼度,就是自己可以给子 Viewsize;⾄于 mode,就用 EXACTLY(注意:就算自己的 modeAT_MOST,传给子 View的也是EXACTLY,想不通的话好好琢磨⼀下);
            4. 如果自己的 spec 中的 modeUNSPECIFIED,说明自己的尺⼨没有上限,那么让子 View填满自己的可用空间就⽆从说起,因此选用退让⽅案:给子 View限制的 mode 就设置为 UNSPECIFIEDsize 写 0 就好;
            5. 如果开发者写的是 WRAP_CONTENT,即要求子 View在不超限制的前提下自我测量,那么同样由于自己的可用空间和自己的两个 MeasureSpec 有关,所以也需要根据自己的 widthMeasureSpecheightMeasureSpec 中的 mode 来分情况判断:
            6. 如果自己的 spec 中的 modeEXACTLY 或者 AT_MOST,说明自己的尺⼨有上限,那么把 spec 中的 size 减去自己的已用宽度或⾼度,就是自己可以给子 View的尺⼨上限;⾄于 mode,就用 AT_MOST(注意,就算自己的 modeEXACTLY,传给子 View的也是 AT_MOST,想不通的话好好琢磨⼀下;
            7. 如果自己的 spec 中的 modeUNSPECIFIED,说明自己的尺⼨没有上限,那么也就不必限制子 View的上限,因此给子 View限制的mode 就设置为 UNSPECIFIEDsize 写 0 就好。
      2. 测量出所有子 View的位置和尺⼨后,计算出自己的尺⼨,并用 setMeasuredDimension(width, height) 保存

    2. 重写 onLayout()

      • 遍历每个子 View,调用它们的 layout() ⽅法来将位置和尺⼨传给它们

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        protected void onLayout(boolean changed, int l, int t, int r, 
        int b) {
        for (int i = 0; i < getChildCount(); i++) {
        `View`child = getChildAt(i);
        Rect childBounds = childrenBounds[i];
        // 将每个子 `View`的位置和尺⼨传递给它
        child.layout(childBounds.left, childBounds.top,
        childBounds.right, childBounds.bottom);
        }
        }