图片编辑器--重写 RadioGroup 的 onMeasure、onLayout 实现 RadioButton 多行多列排列

很久以前,我就有一个梦想:实现单选按钮 RadioButton 的多行多列排列。

I have a dream

场景:

某 PM:肖老板,之前咱们这里只有三个单选选项,现在我们内容丰富了,需要再增加两个选项。
肖老板:没问题,小事。加什么发我就行。(不就是加两个 RadioButton 嘛)

结局你们相信也猜到了,我还是太年轻啊。

尼玛,RadioGroup 居然不支持多行排列,而且还不支持多层布局,这坑爹货。

没办法,说出去的话泼出去的水,只有自己搞了。

以前的方案

写两个 RadioGroup,每个 RadioGroup 都放置一行 RadioButton,当选中第一个 RadioGroup 中的选项时,通过代码清除第二个 RadioGroup 中的选项。

也就是通过代码控制两个 RadioGroup 中的选项达到互斥效果。(不过效果的确实现了,不过有点自欺欺人的赶脚)

代码就不贴了,网上一堆。

这里要吐槽一下国内的文章,基本都是抄抄抄,搜出来的文章都是一毛一样,都不知道谁是原创者了。

更优雅的方案

通过重写 RadioGrouponMeasureonLayout 方法,修改 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 改成 layoutonMeasure 改成 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 方法)
  • 儿子:爸,我要宽高是 widthheight 的房子!(旁白:RadioButton 的测量宽高结果是 width、height)
  • 老爸:混账东西,我这里只有县城宽高是 widthMeasureSpecheightMeasureSpec 的房子,你看着办吧!(旁白:这是 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: Ethoshttps://github.com/xwdoor/Ethos),欢迎大家给颗星星,哈哈。

推荐阅读:

  1. 图片编辑器–向上弹出文字编辑框(遮罩)界面
  2. 图片编辑器–视图布局 View 悬浮在软键盘上
  3. 图片编辑器–重写 RadioGroup 的 onMeasure、onLayout 实现 RadioButton 多行多列排列

微信公众号

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值