
簡介
在使用DataBinding的時候我們知道數(shù)據(jù)驅(qū)動UI的顯示,這種單向的數(shù)據(jù)綁定也是我們使用它最多的地方,既然有單向的數(shù)據(jù)綁定應(yīng)該會存在雙向綁定?
不錯Android官方確實為我們提供了相應(yīng)的雙向綁定的屬性。
比如EditText和CheckBox中
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
// 重點在于這個 =
android:text="@={viewModel.etText}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="@={viewModel.checkBoxStatus}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
所謂的雙向綁定就是數(shù)據(jù)能驅(qū)動UI的顯示,UI的狀態(tài)變換也能改變綁定的屬性值;
針對上述的EditText和CheckBox,它的雙向綁定是系統(tǒng)幫助我們處理好了,雙向綁定的寫法也很簡單 “@={屬性值}”
接著就我們就自定義一個雙向綁定的屬性,在實現(xiàn)前我們看下最終的效果圖

看了效果,你想用RecylerView來實現(xiàn)?可以,不過要是使用雙向數(shù)據(jù)綁定可以很簡單的。
實現(xiàn)步驟
DataBinding為我們實現(xiàn)了雙向數(shù)據(jù)綁定提供了@InverseBindingAdapter注解;
步驟一
首先我們需要一個數(shù)據(jù)源用來設(shè)置控件上布局的數(shù)據(jù),我們使用@BindingAdapter自定義一個屬性 data用來接受數(shù)據(jù)源
/**
* 設(shè)置數(shù)據(jù)源
*/
@JvmStatic
@BindingAdapter(value = ["data"], requireAll = true)
fun setData(inverseGroupView: InverseGroupView, data: List<String>?) {
data?.let {
inverseGroupView.setData(it)
}
}
步驟二
然后我們需要定義雙向綁定的屬性 index 同樣使用 @BindingAdapter
/**
* 設(shè)置角標(biāo)
* 當(dāng)數(shù)據(jù)發(fā)生邊改的時候,會調(diào)用該方法設(shè)置數(shù)據(jù) 更新UI
*/
@JvmStatic
@BindingAdapter(value = ["index"], requireAll = true)
fun setIndex(inverseGroupView: InverseGroupView, index: Int) {
inverseGroupView.selectIndex = index
inverseGroupView.refreshSelectedIndex(index)
}
步驟三
接著我們使用@InverseBindingAdapter注解,來將我們因UI狀態(tài)改變而導(dǎo)致屬性值改變同步給自定義屬性index(也就是從View中讀取到值)
在使用@InverseBindingAdapter注解使,內(nèi)部有兩個屬性,attribute:對應(yīng)著自定義屬性,event:是一個事件名稱改屬性在下面我會詳細(xì)說明一下
@JvmStatic
@InverseBindingAdapter(attribute = "index", event = "indexChange")
fun getIndex(inverseGroupView: InverseGroupView) = inverseGroupView.selectIndex
到這里總結(jié)一下哈:當(dāng)我們的index值發(fā)生改變的情況會調(diào)用步驟二中方法,通知UI布局發(fā)生變換
當(dāng)我們的UI布局狀態(tài)發(fā)生改變的情況下我們可以調(diào)用步驟三方法通知給綁定的屬性值,讓它重新設(shè)置值。
但是有一個問題就是步驟三的方法不知道UI的狀態(tài)發(fā)生變化的時機此時就需要我們步驟4的操作了
步驟四
我們使用@BindingAdapter注解實現(xiàn)了一個View的狀態(tài)值發(fā)生變化的事件通知,
注解中的value值要和@InverseBindingAdapter中的event中的值要保持一致,這樣當(dāng)View的狀態(tài)值發(fā)生變化后會通知步驟三種的方法拿到值設(shè)置給綁定的屬性值。
/**
* 雙向數(shù)據(jù)綁定的
* InverseBindingListener 是一個監(jiān)聽器,用來處理屬性改變時的通知
* 在這里我們給View設(shè)置了點擊事件,當(dāng)屬性發(fā)生改變它會回調(diào) onChange方法告訴DataBinding 去 @InverseBindingAdapter修飾的方法中取到值 然后設(shè)置給綁定的變量
*/
@JvmStatic
@BindingAdapter("indexChange")
fun setIndexChangeListener(
inverseGroupView: InverseGroupView,
changeListener: InverseBindingListener?
) {
if (changeListener != null) {
inverseGroupView.onSelectChangeListener = {
changeListener.onChange()
}
} else {
inverseGroupView.onSelectChangeListener = null
}
}
到這里一個簡單的自定義屬性的雙向綁定就完成了,這里我貼一下當(dāng)時寫的源碼
/**
* @author : zhangqi
* @time : 12/7/20
* desc : 使用DataBinding來自定義屬性的雙向綁定
*/
class InverseGroupView : LinearLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
constructor(context: Context, attributeSet: AttributeSet, defStyle: Int) : super(
context,
attributeSet,
defStyle
)
/**
* 當(dāng)前選中的index
*/
var selectIndex: Int = 0
/**
* tag點擊事件的回調(diào)事件
*/
var onSelectChangeListener: ((Int) -> Unit)? = null
/**
* 用于收集回收可復(fù)用的View
*/
var recyclerView = ArrayList<View>()
/**
* 用于存儲拿到的數(shù)據(jù)
*/
var mData: List<Any>? = null
/**
* 設(shè)置數(shù)據(jù)
*/
fun <T : Any> setData(data: List<T>) {
updateViewData(data)
}
/**
* 更新布局的數(shù)據(jù)
* 創(chuàng)建布局,將數(shù)據(jù)設(shè)置到布局上
* data:新的數(shù)據(jù)源
*/
private fun <T : Any> updateViewData(data: List<T>) {
mData = data
// 每次執(zhí)行到這個方法時,需要回收一下,移除一下,因為接下來是要重新綁定數(shù)據(jù)的,
recyclerViewMethod()
/**
* 遍歷循環(huán)數(shù)據(jù)源,將數(shù)據(jù)綁定幫控件上
*/
data.forEachIndexed { index, any ->
val tagView = getReuseView()
val tvTagView = tagView.findViewById<TextView>(R.id.tag)
tagView.isSelected = index == selectIndex
tvTagView.text = any as String
//設(shè)置一下點擊事件
tagView.setOnClickListener {
//要更新下布局上按鈕的狀態(tài)
refreshSelectedIndex(index)
}
// 將View添加到父布局中
addView(tagView)
}
}
/**
* 刷新下選中的子View
*/
private fun refreshSelectedIndex(clickIndex: Int) {
selectIndex = clickIndex
for (i in 0 until childCount) {
getChildAt(i).isSelected = i == clickIndex
}
onSelectChangeListener?.invoke(clickIndex)
}
/**
* 獲取到布局View對象
*/
private fun newView(): View {
return LayoutInflater.from(context).inflate(R.layout.item_tag, null, false)
}
/**
* 獲取到布局文件
* 回收池中有 就拿第一個,
* 沒有的話就重新常見一個View對象
*/
private fun getReuseView(): View {
return if (recyclerView.isNotEmpty() && recyclerView.size > 0) {
recyclerView.removeAt(0)
} else {
newView()
}
}
/**
* 首先將目前布局上已經(jīng)有的子View存儲到復(fù)用池中
* 然后將這些子View從布局上移除
*/
private fun recyclerViewMethod() {
for (i in 0 until childCount) {
recyclerView.add(getChildAt(i))
}
removeAllViews()
}
companion object {
/**
* 設(shè)置數(shù)據(jù)源
*/
@JvmStatic
@BindingAdapter(value = ["data"], requireAll = true)
fun setData(inverseGroupView: InverseGroupView, data: List<String>?) {
data?.let {
inverseGroupView.setData(it)
}
}
/**
* 設(shè)置角標(biāo)
* 當(dāng)數(shù)據(jù)發(fā)生邊改的時候,會調(diào)用該方法設(shè)置數(shù)據(jù) 更新UI
*/
@JvmStatic
@BindingAdapter(value = ["index"], requireAll = true)
fun setIndex(inverseGroupView: InverseGroupView, index: Int) {
if (inverseGroupView.selectIndex == index) return
inverseGroupView.selectIndex = index
inverseGroupView.refreshSelectedIndex(index)
}
/**
* 獲取到當(dāng)前選中的角標(biāo)
* event:數(shù)據(jù)改變的事件
*
* 當(dāng)View的狀態(tài)發(fā)生改變的時候(包括數(shù)據(jù)的填充,bg的改變),會調(diào)用該方法來獲取到值
*/
@JvmStatic
@InverseBindingAdapter(attribute = "index", event = "indexChange")
fun getIndex(inverseGroupView: InverseGroupView) = inverseGroupView.selectIndex
/**
* 雙向數(shù)據(jù)綁定的
* InverseBindingListener 是一個監(jiān)聽器,用來處理屬性改變時的通知
* 在這里我們給View設(shè)置了點擊事件,當(dāng)屬性發(fā)生改變它會回調(diào) onChange方法告訴DataBinding 去 @InverseBindingAdapter修飾的方法中取到值 然后設(shè)置給綁定的變量
*/
@JvmStatic
@BindingAdapter("indexChange")
fun setIndexChangeListener(
inverseGroupView: InverseGroupView,
changeListener: InverseBindingListener?
) {
if (changeListener != null) {
inverseGroupView.onSelectChangeListener = {
changeListener.onChange()
}
} else {
inverseGroupView.onSelectChangeListener = null
}
}
}
}
注意點
由于當(dāng)時我綁定的屬性值使用的是LiveData,當(dāng)我改變了View的狀態(tài)值是會通知到@InverseBindingAdapter注解修飾的方法讓它拿到值設(shè)置給綁定的屬性值。
由于LiveData天生就有可觀察性,當(dāng)觀察到數(shù)據(jù)源發(fā)生變化又會驅(qū)動UI狀態(tài)值發(fā)生變化,這樣UI發(fā)生變化又會被監(jiān)聽到 又去通知@InverseBindingAdapter注解修飾的方法讓它拿到值設(shè)置給綁定的屬性值。
這樣就會陷入到無限的循環(huán)中,所以我當(dāng)時的做法就是在綁定的屬性值的 setter方法中做了新舊值的判斷,如果值一致就不觸發(fā)UI狀態(tài)值的更新了
@JvmStatic
@BindingAdapter(value = ["index"], requireAll = true)
fun setIndex(inverseGroupView: InverseGroupView, index: Int) {
if (inverseGroupView.selectIndex == index) return
inverseGroupView.selectIndex = index
inverseGroupView.refreshSelectedIndex(index)
}
其實這個地方可以從綁定的屬性值入手解決這個問題,比如我們在設(shè)置值之前檢查當(dāng)前的值和將要的值一致的話就不進(jìn)行 set或者post,比如我這個 DiffLiveData
/**
* @author : zhangqi
* @time : 12/6/20
* desc : 如果當(dāng)前LiveData中攜帶的值和將要設(shè)置的值是一致的,就不進(jìn)行設(shè)置值的操作了
*/
class DiffLiveData<T>(value:T) : MutableLiveData<T>(value) {
override fun setValue(value: T?) {
if (Objects.equals(value, getValue())) return
super.setValue(value)
}
override fun postValue(value: T?) {
if (Objects.equals(value, getValue())) return
super.postValue(value)
}
}
雖然DataBinding在報錯的時候,錯誤查找起來不是很友好,但是作為AAC架構(gòu)的基礎(chǔ),給我們帶來很多方便之處,比如利用這種思想的開源庫ItemBinding
就給我在日常開發(fā)中有很大的效率提高;這些好用的地方完全勝過它的一些小缺點。