欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android 自定义控件的 measure, layout

程序员文章站 2022-06-07 13:45:30
...

Android 自定义控件的 measure, layout

Android 自定义 View 一般都要写 测量, 摆放

在 onMeasure 里面测量出自己的宽高, 然后父控件会根据自己测量出来的宽高来进行摆放(layout)

如果不按照父容器的约束来, 就会出问题

重写了 layout 方法, 摆放的很大, 但是父容器那里的尺寸并没有改, 所以其他的 view 就会有重叠

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <com.example.lsn13_layout.views.OneHundredView
        android:layout_width="10dp"
        android:layout_height="10dp"
        android:background="#FF0000" />

    <View
        android:layout_width="10dp"
        android:layout_height="10dp"
        android:background="#00FF00" />
</LinearLayout>


class OneHundredView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

    override fun layout(l: Int, t: Int, r: Int, b: Int) {
        super.layout(l, t, r + dp2px(100F).toInt(), b + dp2px(100F).toInt())
    }
}

Android 自定义控件的 measure, layout

流程

  • 从整体看:

    • 测量流程: 从根 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() 里调用每个子 Viewlayout() 把它们的尺寸位置传给它们

自定义控件一般有三种情况

1、集成已有的 View , 简单改写它们的尺寸: 重写 onMeasure()

  • 重写 onMeasure()

  • getMeasuredWidth()getMeasuredHeight() 获取到测量出的尺寸

  • 计算出最终要的尺寸

  • setMeasureDimension(width, height) 把结果保存

2、对自定义 View 完全进行自定义尺寸计算: 重写 onMeasure()

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val size = (PADDING + RADIUS) * 2
    val measuredWidth = resolveSizeAndState(size.toInt(), widthMeasureSpec, 0)
    val measuredHeight = resolveSizeAndState(size.toInt(), heightMeasureSpec, 0)
    setMeasuredDimension(measuredWidth, measuredHeight)
}
  • 1、重写 onMeasure

  • 2、计算出自己的尺寸

  • 3、用 resolveSize() 或者 resolveSizeAndState() 修正结果

    • resolveSize() 内部实现

      • 首先用 MeasureSpec.getMode(measureSpec)MeasureSpec.getSize(measureSpec) 取出父容器对自己的尺寸限制类型和具体限制尺寸;

      • 如果 measureSpecmodeMeasureSpec.EXACTLY , 表示父 View 对子 View 的尺寸做出了精确限制, 所以就放弃计算出的 size , 直接选用 measureSpecsize;

      • 如果 measureSpecmodeMeasureSpec.AT_MOST , 表示父 View 对子 View 的尺寸只限制了上限, 需要看情况:

        • 如果计算出的 size 不大于 measureSpec 中显示的 size , 表示尺寸没有超出限制, 所以选用计算出的 size;

        • 而如果计算出的 size 大于 measureSpec 中限制的 size , 表示尺寸超限了, 所以选用 measureSpecsize , 并且在 resolveSizeAndState() 中会添加标志 MEASURED_STATE_TOO_SMALL (这个表示可以辅助父View做测量和布局的计算);

      • 如果 measureSpecmodeMeasureSpec.UNSPECIFIED , 表示父 View 对子 View 没有任何尺寸限制, 所以直接选用计算出的 size , 忽略 measureSpec 中的 size.

  • 4、用 setMeaduredDimension(width, height) 来保存结果

3、自定义 Layout: 重写 onMeasure()onLayout()

  • 首先用 measureChildWithMargins(child, widthMeasureSpec, paddingStart + paddingEnd, heightMeasureSpec, heightUsed + paddingTop + paddingBottom) 测量子控件的宽高

  • 如果已经用掉的宽度加上子控件的宽度超过了父容器给的宽度, 就要换行, 然后重新测量

  • 最后用 resolveSizeAndState() 来纠正宽高

  • 1、重写 onMeasure()

    • 1、遍历每个子 View , 用 measureChildWithMargins() 测量

      • 需要重写 generateLayoutParams() 并返回 MarginLayoutParams

      • 换行处的 View 需要重新测量

      • 测量完成后, 得出子 View 的尺寸和位置, 并保存

    • 2、测量出所有的子 View 的尺寸后, 计算出自己的尺寸, 最后用 setMeasuredDimension(measuredWidth, meaduredHeight) 保存

  • 2、重写 onLayout()

    • 遍历每个子 View , 调用它们的 layout() 方法将它们的尺寸和位置传进去
相关标签: 自定义控件