Customize RadioGroupX to achieve multi-row and multi-column layout

Preface

Today, when we are doing new requirements, there are multiple types of activities to choose from. The design drawing provided by the UI is multi-line and multi-column layout, and a single choice. If you think about it carefully, Google does not provide us with similar controls. The preliminary idea is to use RecyclerView. Realize the multi-line and multi-column layout, and then use the code to control the logic part. It always feels unsafe, and I want to let the UI lady redesign it? It feels too unstable, so Miss UI will think that I am a dish. In order to prevent others from thinking that I am a dish, I simply customize RadioGroupX to achieve a multi-row and multi-column layout.

Thinking

In the work, facing a function, the first thing to think about is how to implement it, and then how to implement it more elegantly. As mentioned earlier, the realization of this requirement can be done in a variety of postures, such as using RecyclerView, or using ConstraintLayout with multiple TextView layouts, and using code to control the option logic. After thinking about it, it always feels too blunt. Too elegant, too much code may be prone to bugs. So I got some inspiration by reading the RadioGroup source code provided by Google. Reading the source code can often make me understand. For example, why only single-row and multi-column or multi-row and single-column layouts are supported in RadioGroup? The main reason is that RadioGroup extends LineLayout, which leads to many limitations. Seeing this, I suddenly thought that GridView supports multi-row and multi-column layout, so I imitated the RadioGroup source code to customize a container to inherit GridView.

First acquainted with OnHierarchyChangeListener interface

The OnHierarchyChangeListener interface is located in the ViewGroup java file. It is rarely used in daily work. This explanation is given in the developer's official website document: In image.png
work, we must be familiar with the two methods of addView() and RemoveView() In fact, when we operate these two methods, we will trigger the java void onChildViewAdded(View parent, View child)sum java void onChildViewRemoved(View parent, View child);two method callbacks in the OnHierarchyChangeListener interface. The source code also gives a detailed explanation. We can read the comments directly in the source code to understand.

Refer to RadioGroup source code to define internal classes

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)
        }
    }

In the above rewrite kotlin onChildViewAdded( parent: View, child: View )and kotlinonChildViewRemoved(parent: View, child: View)two methods, we focus on the onChildViewAdded method. When we add child controls to the container, how many child children the method will be triggered how many times, we dynamically set the selected event listener of the child View here.

Define CheckedStateTracker implementation

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)
        }
    }

In the onCheckedChanged method, handle the selection and cancellation events of the child View, that is, the RadioButton. Through the above two steps, it is basically completed. View selected event monitoring and event processing logic

RadioGroupX complete code

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)
        }
    }

}

Used in 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>

The activity event processing part is the same as using the RadioGroup principle, just copy it.

to sum up

Through the above short steps, we have basically completed the typesetting problem in the requirements. If I do not read the ideas in the source code, I think it is difficult for me to write it, at least I will not complete the requirement design in a short time, so I work. You should read the source code more and understand the design ideas and ideas in the source code, so that you can improve yourself.

Guess you like

Origin blog.csdn.net/zhuhuitao_struggle/article/details/114155700