前言
今天在做新需求的時(shí)候弃理,活動(dòng)有多個(gè)類型可以選擇屿良,UI給的設(shè)計(jì)圖為多行多列排版,且單項(xiàng)選擇霉涨,細(xì)細(xì)想來按价,谷歌并沒有為我們提供類似的控件,初步設(shè)想使用RecyclerView實(shí)現(xiàn)多行多列布局笙瑟,然后再用代碼控制邏輯部分楼镐,總感覺不太穩(wěn)妥,又想到讓UI小姐姐重新設(shè)計(jì)一番往枷?感覺也不太穩(wěn)妥框产,這樣UI小姐姐就會(huì)認(rèn)為我菜,為了不讓別人覺得我菜错洁,干脆自定義RadioGroupX實(shí)現(xiàn)多行多列布局秉宿。
思考
在工作中,面對(duì)一個(gè)功能屯碴,首先想到的是應(yīng)該怎樣實(shí)現(xiàn)完成它描睦,然后再考慮究竟怎樣實(shí)現(xiàn)才更優(yōu)雅。正如前面提到导而,實(shí)現(xiàn)這種需求是可以用多種姿勢(shì)完成忱叭,比如使用RecyclerView,或者使用ConstraintLayout裝有多個(gè)TextView的布局今艺,用代碼控制選項(xiàng)邏輯窑多,在思考一番后,總感覺太生硬洼滚,不太優(yōu)雅埂息,代碼量多也許容易出bug。于是通過閱讀谷歌為我們提供的RadioGroup源碼得出一些靈感,閱讀源碼往往能使自己大徹大悟千康。比如在RadioGroup中為什么只支持單行多列或者多行單列布局享幽,主要原因是因?yàn)镽adioGroup extends LineLayout,所以導(dǎo)致了很多局限性∈捌看到這里突然聯(lián)想到GridView支持多行多列布局值桩,于是乎,模仿RadioGroup源碼自定義一個(gè)容器繼承GridView豪椿。
初識(shí)OnHierarchyChangeListener接口
OnHierarchyChangeListener接口位于ViewGroup java文件中奔坟,在日常工作中,幾乎不會(huì)用到搭盾,在developer官網(wǎng)文檔中給出了這樣的解釋:
工作中咳秉,我們對(duì)addView()和RemoveView()這兩個(gè)方法一定不陌生,其實(shí)我們?cè)诓僮鬟@兩個(gè)方法的時(shí)候就會(huì)觸發(fā)OnHierarchyChangeListener接口中的
java void onChildViewAdded(View parent, View child)
和java void onChildViewRemoved(View parent, View child);
兩個(gè)方法回調(diào)鸯隅,源碼中也給了詳細(xì)解釋澜建。我們可以直接在源碼中閱讀注釋加以理解。
參照RadioGroup源碼定義內(nèi)部類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)
兩個(gè)方法蝌以,我們著重關(guān)注onChildViewAdded方法炕舵,當(dāng)我們?cè)谌萜髦刑砑幼涌丶r(shí),有多少個(gè)子孩子該方法就會(huì)觸發(fā)多少次跟畅,我們?cè)诖藙?dòng)態(tài)設(shè)置子View的選中事件監(jiān)聽咽筋。
定義CheckedStateTracker實(shí)現(xiàn)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的選中與取消事件,通過以上兩個(gè)步驟徊件,基本完成了奸攻,View選中事件監(jiān)聽和事件處理邏輯
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="數(shù)學(xué)" />
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="語(yǔ)文" />
<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="計(jì)算機(jī)" />
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="化學(xué)" />
</com.example.multilineradiogroupdemo.RadioGroupX>
activity事件處理部分和使用RadioGroup原理一樣,照搬即可庇忌。
總結(jié)
通過上面短短幾步,我們基本完成了需求中的排版問題舰褪,如果不閱讀借鑒源碼中的思路皆疹,我想我是很難寫出來,至少不會(huì)在很短時(shí)間就完成需求設(shè)計(jì)占拍,所以工作我應(yīng)該做到更多的閱讀源碼略就,了解源碼中的設(shè)計(jì)思路和思想,這樣自己才能有所提高晃酒。