很久以前,我就有一个梦想:实现单选按钮 RadioButton
的多行多列排列。
I have a dream
场景:
某 PM:肖老板,之前咱们这里只有三个单选选项,现在我们内容丰富了,需要再增加两个选项。
肖老板:没问题,小事。加什么发我就行。(不就是加两个RadioButton
嘛)
结局你们相信也猜到了,我还是太年轻啊。
尼玛,RadioGroup
居然不支持多行排列,而且还不支持多层布局,这坑爹货。
没办法,说出去的话泼出去的水,只有自己搞了。
以前的方案
写两个 RadioGroup
,每个 RadioGroup
都放置一行 RadioButton
,当选中第一个 RadioGroup
中的选项时,通过代码清除第二个 RadioGroup
中的选项。
也就是通过代码控制两个 RadioGroup
中的选项达到互斥效果。(不过效果的确实现了,不过有点自欺欺人的赶脚)
代码就不贴了,网上一堆。
这里要吐槽一下国内的文章,基本都是抄抄抄,搜出来的文章都是一毛一样,都不知道谁是原创者了。
更优雅的方案
通过重写 RadioGroup
的 onMeasure
和 onLayout
方法,修改 RadioGroup
中的布局方式,达到自动换行的效果。
说干就干,先简单说说「Android View 绘制流程」:measure -> layout -> draw。(哈哈,是不是简单到吓人)
- measure:测量 View 的宽高
- layout:确定 View 的摆放位置
- draw:将 View 画出来
measure
测量过程如下:
ViewGroup
的 measure 过程大概是:measure
->onMeasure
->child.measure
。
View
的 measure 过程大概是:measure
->onMeasure
。
不管是 ViewGroup
还是 View
,在 measure
方法的最后,需要调用 setMeasuredDimension
方法保存测量结果,否则会有异常。(好奇的你可以去试试哦~~)
layout
布局过程如下:
ViewGroup
的 layout 过程大概是:layout
->onlayout
->child.layout
。
View
的 layout 过程大概是:layout
->onlayout
。
机智如你,肯定发现了我的套路:我特么就是复制上边的文字,然后将 measure
改成 layout
,onMeasure
改成 onLayout
。
所以,你现在明白 View
绘制流程中的三个阶段是怎么回事了吧,流程基本一样,只是工作内容不一样而已。
代码实现
说了半天废话,现在开始撸代码了,都打起精神来。
首先出场的是 onMeasure
中的代码:
/**
* 重写测量方法,测量多行布局需要的高度和宽度
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 获取宽度模式和最大宽度(父控件给的期望宽度)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val maxWidth = MeasureSpec.getSize(widthMeasureSpec)
// 下边的逻辑主要用于测量子 View 需要排列成多少行,并测量 GroupView 需要的真实高度
// 子 View 排列的行数
var rowCount = 1
// 真实高度,由行数计算确定
var realHeight = 0
// 当前行的占用宽度,如果子 View 的宽度超过最大宽度,则换行
var rowWidth = 0
for (index in 0 until childCount) {
val child = getChildAt(index)
// 隐藏的子 View 不进行测量
if (child.visibility == View.GONE) continue
// 计算 child 的宽高
child.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
// 获取测量后的 child 的宽高,以及四边边距
val width = child.measuredWidth
val height = child.measuredHeight
val leftMargin = (child.layoutParams as LinearLayout.LayoutParams).leftMargin
val rightMargin = (child.layoutParams as LinearLayout.LayoutParams).rightMargin
val topMargin = (child.layoutParams as LinearLayout.LayoutParams).topMargin
val bottomMargin = (child.layoutParams as LinearLayout.LayoutParams).bottomMargin
rowWidth += width + leftMargin + rightMargin
// 这句话是必不可少的,如果只有一行的话,就靠这个赋值了
realHeight = rowCount * (topMargin + height + bottomMargin)
// 如果当先行的宽度超出父控件,则换行
if (rowWidth > maxWidth) {
// 第一个 child 不进行换行,即使它的宽度超出了父控件
if (index != 0) rowCount++
// 重新设置当前行的占用宽度
rowWidth = width + leftMargin + rightMargin
// 重新设置当前父控件高度
// todo: 考虑每个 RadioButton 间距或高度不一样的情况
realHeight = rowCount * (topMargin + height + bottomMargin)
}
}
// 保存测量值
setMeasuredDimension(maxWidth, realHeight)
}
简单来讲,RadioGroup
是老爸,RadioButton
是儿子,于是,在 measure
这一天,
- 老爸:儿子,你在北京要多宽多高的房子才住着舒服啊?(旁白:调用 child.measure 方法)
- 儿子:爸,我要宽高是
width
、height
的房子!(旁白:RadioButton 的测量宽高结果是 width、height) - 老爸:混账东西,我这里只有县城宽高是
widthMeasureSpec
、heightMeasureSpec
的房子,你看着办吧!(旁白:这是 measure 方法中的两个参数,里边有能给到的最大宽高)
大家明白了吧,小时候不要违逆父母,不然揍你!哼哼!
父母会尽最大的能力帮你,但你要的超出父母能给的,那就没有办法了。超出绘制范围,就不给你绘制。
接下来出场表演的就是 onLayout
了,请欣赏:
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
val maxWidth = right - left
var rowCount = 1
// 当前行宽
var rowWidth = 0
// 当前布局高度
var layoutHeight: Int
for (index in 0 until childCount) {
val child = getChildAt(index)
if (child.visibility == View.GONE) continue
// 获取测量后的 child 的宽高,以及四边边距
val width = child.measuredWidth
val height = child.measuredHeight
val leftMargin = (child.layoutParams as LinearLayout.LayoutParams).leftMargin
val rightMargin = (child.layoutParams as LinearLayout.LayoutParams).rightMargin
val topMargin = (child.layoutParams as LinearLayout.LayoutParams).topMargin
val bottomMargin = (child.layoutParams as LinearLayout.LayoutParams).bottomMargin
rowWidth += width + leftMargin + rightMargin
// 这句话是必不可少的,如果只有一行的话,就靠这个赋值了
layoutHeight = rowCount * (height + topMargin + bottomMargin)
// 如果当先行的宽度超出父控件,则换行
if (rowWidth > maxWidth) {
// 第一个 child 不进行换行,即使它的宽度超出了父控件
if (index != 0) rowCount++
// 重新设置当前行的占用宽度
rowWidth = width + leftMargin + rightMargin
layoutHeight = rowCount * (height + topMargin + bottomMargin)
}
// todo: 考虑每个 RadioButton 间距或高度不一样的情况
child.layout(
rowWidth - width - rightMargin,
layoutHeight - height - bottomMargin,
Math.min(rowWidth, maxWidth),
layoutHeight
)
}
}
这个应该没什么好说的,大部分代码跟 onMeasure
中一样,主要作用是计算 child
的定位数据:left,top,right,bottom。
效果鉴赏
(科班出身,审美惨不忍睹啊,不过后期会优化,君不见,这个界面比之前好多了嘛)
如果文章对你有帮助的话,欢迎关注公众号:XWdoor,你的关注,就是最大的支持。
文章源码:GitHub: Ethos(https://github.com/xwdoor/Ethos),欢迎大家给颗星星,哈哈。
推荐阅读: