继承ViewGroup
重写三个构造方法、定义存储数据结构
// 每一行的View
private lateinit var lineViews: MutableList<View>
// 所有的行 一行一行的存
private lateinit var views: MutableList<MutableList<View>>
// 每一行的高度
private lateinit var heights: MutableList<Int>    
constructor(context: Context?) : this(context, null)
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
    context,
    attrs,
    defStyleAttr
)
自定义参数和使用
在 attrs.xml 文件定义
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="FlowLayout">
        <attr name="android:gravity" />
        <attr name="horizonSpacing" format="dimension|reference" />
    </declare-styleable>
    <declare-styleable name="FlowLayout_Layout">
        <attr name="android:layout_gravity" />
    </declare-styleable>
</resources>
方法一:
直接在自定义 ViewGroup 里面调用 getXXXX 方法
try {
    mProgressDrawable = getDrawable(R.styleable.QingheSeekBarPreference_seekBarProgressDrawable)
    mThumbDrawable = getDrawable(R.styleable.QingheSeekBarPreference_seekBarThumbDrawable)
    mProgressColor = getColor(R.styleable.QingheSeekBarPreference_progressColor, Color.BLUE)
} finally {
    a.recycle()
}
方法二:
在自定义的 View 里定义一个继承 LayoutParams 内部类
class MyLayoutParams : MarginLayoutParams {
        private var gravity = -1
        constructor(c: Context?, attrs: AttributeSet?) : super(c, attrs) {
            // 取出属性集合
            val a = c!!.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout)
            try {
                // 拿到自定义的属性
                gravity = a.getInt(R.styleable.FlowLayout_android_gravity, -1)
            } finally {
                a.recycle()
            }
        }
        constructor(width: Int, height: Int) : super(width, height)
        constructor(source: LayoutParams?) : super(source)
    }
自定义 ViewGroup 要使用自定义属性类 得重写几个方法
    // 生成布局参数
    override fun generateLayoutParams(p: LayoutParams?): MyLayoutParams {
        return MyLayoutParams(p)
    }
    override fun generateLayoutParams(attrs: AttributeSet?): MyLayoutParams {
        return MyLayoutParams(context, attrs)
    }
    override fun generateDefaultLayoutParams(): MyLayoutParams {
        return MyLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
    }
    // 检查布局参数
    override fun checkLayoutParams(p: LayoutParams?): Boolean {
        return super.checkLayoutParams(p) && p is LayoutParams
    }
使用,取出来,转换,然后从 lp 里面获取
// 自定义参数在这里可以用
val lp = child.layoutParams as MyLayoutParams
onMeasure 测量所占的宽高
遍历所有子view,对子view进行测量,分配到具体的行。
- 看当前的行的剩余的宽度是否可以容纳下一个子View, 如果放不下,换行 保存当前行的所有子View,累加行高,当前行的宽度、高度置零
- 每一次都把childView信息添加到当前行上
- 过滤掉 match_parent 的情况
- 处理最后不够一行的情况
遍历结束后,重新测量一次,处理 lp.height = match_parent 的情况
onLayout 布局子 view
外层循环针对 行布局,内层循环针对 每一行的 view 布局
- 取出所有这一行的 view 还有高度
- 对每一个 view 设置 left、top、right、bottom child.layout(left, top, right, bottom)
- 设置下一个 view 布局所要的参数
滑动实现
事件拦截
关键方法 onInterceptTouchEvent
ACTION_DOWN 时记录点击的位置
ACTION_MOVE 时跟 DOWN 时记录的信息比较,y 的变化大时,认为上下滑动,记录位置,返回 true
处理事件
ACTION_DOWN 记录当前 y 的位置
ACTION_MOVE 时比较得出 dy,处理滑动,记录位置
ACTION_UP 处理边界回弹,根据滑动速度继续滑动列表
使用 OverScroller
fling 根据手指抬起时的速度继续滑动列表
startScroll 把滑动分段,触发 ViewGroup 的 computeScroll 真正滑动
springBack 处理边界回弹
ACTION_MOVE 拦截不到
点击子view 外的区域,发现没有响应,move事件的日志也没有打印。
可能是 onTouchEvent 返回  return super.onTouchEvent(event)
原因是拦截到后 onTouchEvent 没有返回 true,也没有子 View 返回 true,后续事件就不发送到该 ViewGroup 了
 
                    