Android自定义RadioGroupX实现多行多列布局
作者:辉涛
前言
今天在做新需求的时候,活动有多个类型可以选择,UI给的设计图为多行多列排版,且单项选择,细细想来,谷歌并没有为我们提供类似的控件,初步设想使用RecyclerView实现多行多列布局,然后再用代码控制逻辑部分,总感觉不太稳妥,又想到让UI小姐姐重新设计一番?感觉也不太稳妥,这样UI小姐姐就会认为我菜,为了不让別人觉得我菜,干脆自定义RadioGroupX实现多行多列布局。
思考
在工作中,面对一个功能,首先想到的是应该怎样实现完成它,然后再考虑究竟怎样实现才更优雅。正如前面提到,实现这种需求是可以用多种姿势完成,比如使用RecyclerView,或者使用ConstraintLayout装有多个TextView的布局,用代码控制选项逻辑,在思考一番后,总感觉太生硬,不太优雅,代码量多也许容易出bug。于是通过阅读谷歌为我们提供的RadioGroup源码得出一些灵感,阅读源码往往能使自己大彻大悟。比如在RadioGroup中为什么只支持单行多列或者多行单列布局,主要原因是因为RadioGroup extends LineLayout,所以導致了很多局限性。看到这里突然联想到GridView支持多行多列布局,于是乎,模仿RadioGroup源码自定义一个容器继承GridView。
初识OnHierarchyChangeListener接口
OnHierarchyChangeListener接口位于ViewGroup java文件中,在日常工作中,几乎不会用到,在developer官网文档中给出了这样的解释:
工作中,我们对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原理一样,照搬即可。
总结
通过上面短短几步,我们基本完成了需求中的排版问题,如果不阅读借鉴源码中的思路,我想我是很难写出来,至少不会在很短时间就完成需求设计,所以工作我应该做到更多的阅读源码,了解源码中的设计思路和思想,这样自己才能有所提高。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。