美文网首页
自定义RadioGroupX实现多行多列布局

自定义RadioGroupX实现多行多列布局

作者: 辉涛 | 来源:发表于2021-02-25 23:17 被阅读0次

    前言

    今天在做新需求的时候,活动有多个类型可以选择,UI给的设计图为多行多列排版,且单项选择,细细想来,谷歌并没有为我们提供类似的控件,初步设想使用RecyclerView实现多行多列布局,然后再用代码控制逻辑部分,总感觉不太稳妥,又想到让UI小姐姐重新设计一番?感觉也不太稳妥,这样UI小姐姐就会认为我菜,为了不让別人觉得我菜,干脆自定义RadioGroupX实现多行多列布局。

    思考

    在工作中,面对一个功能,首先想到的是应该怎样实现完成它,然后再考虑究竟怎样实现才更优雅。正如前面提到,实现这种需求是可以用多种姿势完成,比如使用RecyclerView,或者使用ConstraintLayout装有多个TextView的布局,用代码控制选项逻辑,在思考一番后,总感觉太生硬,不太优雅,代码量多也许容易出bug。于是通过阅读谷歌为我们提供的RadioGroup源码得出一些灵感,阅读源码往往能使自己大彻大悟。比如在RadioGroup中为什么只支持单行多列或者多行单列布局,主要原因是因为RadioGroup extends LineLayout,所以導致了很多局限性。看到这里突然联想到GridView支持多行多列布局,于是乎,模仿RadioGroup源码自定义一个容器继承GridView。

    初识OnHierarchyChangeListener接口

    OnHierarchyChangeListener接口位于ViewGroup java文件中,在日常工作中,几乎不会用到,在developer官网文档中给出了这样的解释:

    image.png
    工作中,我们对addView()和RemoveView()这两个方法一定不陌生,其实我们在操作这两个方法的时候就会触发OnHierarchyChangeListener接口中的java void onChildViewAdded(View parent, View child)java void onChildViewRemoved(View parent, View child);两个方法回调,源码中也给了详细解释。我们可以直接在源码中阅读注释加以理解。

    参照RadioGroup源码定义内部类PassThroughHierarchyChangeListener

       private inner class PassThroughHierarchyChangeListener :
            OnHierarchyChangeListener {
            private val mOnHierarchyChangeListener: OnHierarchyChangeListener? = null
            @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
            override fun onChildViewAdded(
                parent: View,
                child: View
            ) {
                if (parent == this@MultiLineRadioGroup && child is RadioButton) {
                    var id = child.getId()
                    // generates an id if it's missing
                    if (id == View.NO_ID) {
                        id = View.generateViewId()
                        child.setId(id)
                    }
                    child.setOnCheckedChangeListener(
                        mChildOnCheckedChangeListener
                    )
                }
                mOnHierarchyChangeListener?.onChildViewAdded(parent, child)
            }
    
            /**
             * {@inheritDoc}
             */
            override fun onChildViewRemoved(parent: View, child: View) {
                if (parent == this@MultiLineRadioGroup && child is RadioButton) {
                    child.setOnCheckedChangeListener(null)
                }
                mOnHierarchyChangeListener?.onChildViewRemoved(parent, child)
            }
        }
    

    在上面重写kotlin onChildViewAdded( parent: View, child: View )kotlinonChildViewRemoved(parent: View, child: View)两个方法,我们着重关注onChildViewAdded方法,当我们在容器中添加子控件时,有多少个子孩子该方法就会触发多少次,我们在此动态设置子View的选中事件监听。

    定义CheckedStateTracker实现CompoundButton.OnCheckedChangeListener接口

        private inner  class CheckedStateTracker : CompoundButton.OnCheckedChangeListener {
            override fun onCheckedChanged(
                buttonView: CompoundButton,
                isChecked: Boolean
            ) { // prevents from infinite recursion
                if (mProtectFromCheckedChange) {
                    return
                }
                mProtectFromCheckedChange = true
                if (mCheckedId != -1) {
                    setCheckedStateForView(mCheckedId, false)
                }
                mProtectFromCheckedChange = false
                val id = buttonView.id
                setCheckedId(id)
            }
        }
    

    在onCheckedChanged方法中处理子View也就是RadioButton的选中与取消事件,通过以上两个步骤,基本完成了,View选中事件监听和事件处理逻辑

    RadioGroupX完整代码

    class RadioGroupX: GridLayout {
    
        private var mProtectFromCheckedChange = false
        var mCheckedId = -1
    
        private val mChildOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener = CheckedStateTracker()
        private val mPassThroughListener: PassThroughHierarchyChangeListener = PassThroughHierarchyChangeListener()
        private var mOnCheckedChangeListener: OnCheckedChangeListener? = null
    
        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)
    
        init {
            super.setOnHierarchyChangeListener(mPassThroughListener)
        }
    
        override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
            if (child is RadioButton) {
                if (child.isChecked) {
                    mProtectFromCheckedChange = true
                    if (mCheckedId != -1) {
                        setCheckedStateForView(mCheckedId, false)
                    }
                    mProtectFromCheckedChange = false
                    setCheckedId(child.id)
                }
            }
            super.addView(child, index, params)
        }
    
        fun check(@IdRes id: Int) { // don't even bother
            if (id != -1 && id == mCheckedId) {
                return
            }
            if (mCheckedId != -1) {
                setCheckedStateForView(mCheckedId, false)
            }
            if (id != -1) {
                setCheckedStateForView(id, true)
            }
            setCheckedId(id)
        }
    
        private fun setCheckedId(@IdRes id: Int) {
            val changed = id != mCheckedId
            mCheckedId = id
            mOnCheckedChangeListener?.onCheckedChanged(this, mCheckedId)
    //        if (changed) {
    //            val afm: AutofillManager = mContext.getSystemService(
    //                AutofillManager::class.java
    //            )
    //            afm?.notifyValueChanged(this)
    //        }
        }
    
        private fun setCheckedStateForView(viewId: Int, checked: Boolean) {
            val checkedView = findViewById<View>(viewId)
            if (checkedView != null && checkedView is RadioButton) {
                checkedView.isChecked = checked
            }
        }
    
        private inner  class CheckedStateTracker : CompoundButton.OnCheckedChangeListener {
            override fun onCheckedChanged(
                buttonView: CompoundButton,
                isChecked: Boolean
            ) { // prevents from infinite recursion
                if (mProtectFromCheckedChange) {
                    return
                }
                mProtectFromCheckedChange = true
                if (mCheckedId != -1) {
                    setCheckedStateForView(mCheckedId, false)
                }
                mProtectFromCheckedChange = false
                val id = buttonView.id
                setCheckedId(id)
            }
        }
    
        fun setOnCheckedChangeListener(listener: OnCheckedChangeListener) {
            mOnCheckedChangeListener = listener
        }
    
        interface OnCheckedChangeListener {
            fun onCheckedChanged(group: RadioGroupX?, @IdRes checkedId: Int)
        }
    
        private inner class PassThroughHierarchyChangeListener :
            OnHierarchyChangeListener {
            private val mOnHierarchyChangeListener: OnHierarchyChangeListener? = null
            @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
            override fun onChildViewAdded(
                parent: View,
                child: View
            ) {
                if (parent == this@RadioGroupX && child is RadioButton) {
                    var id = child.getId()
                    // generates an id if it's missing
                    if (id == View.NO_ID) {
                        id = View.generateViewId()
                        child.setId(id)
                    }
                    child.setOnCheckedChangeListener(
                        mChildOnCheckedChangeListener
                    )
                }
                mOnHierarchyChangeListener?.onChildViewAdded(parent, child)
            }
    
            /**
             * {@inheritDoc}
             */
            override fun onChildViewRemoved(parent: View, child: View) {
                if (parent == this@RadioGroupX && child is RadioButton) {
                    child.setOnCheckedChangeListener(null)
                }
                mOnHierarchyChangeListener?.onChildViewRemoved(parent, child)
            }
        }
    
    }
    

    xml中使用

            <com.example.multilineradiogroupdemo.RadioGroupX
                android:layout_width="match_parent"
                android:columnCount="3"
                android:layout_height="wrap_content"
                app:layout_constraintTop_toBottomOf="@id/line">
    
                <RadioButton
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="数学" />
    
                <RadioButton
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="语文" />
    
                <RadioButton
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="地理" />
    
                <RadioButton
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="生物" />
    
                <RadioButton
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="计算机" />
    
                <RadioButton
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="化学" />
    
    
            </com.example.multilineradiogroupdemo.RadioGroupX>
    

    activity事件处理部分和使用RadioGroup原理一样,照搬即可。

    总结

    通过上面短短几步,我们基本完成了需求中的排版问题,如果不阅读借鉴源码中的思路,我想我是很难写出来,至少不会在很短时间就完成需求设计,所以工作我应该做到更多的阅读源码,了解源码中的设计思路和思想,这样自己才能有所提高。

    相关文章

      网友评论

          本文标题:自定义RadioGroupX实现多行多列布局

          本文链接:https://www.haomeiwen.com/subject/btmefltx.html