DataBinding-自定義屬性雙向數(shù)據(jù)綁定

Android_Banner.jpg

簡介

在使用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)前我們看下最終的效果圖

inverse.gif

看了效果,你想用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ā)中有很大的效率提高;這些好用的地方完全勝過它的一些小缺點。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容