美文网首页前端开发那些事儿
DataBinding-自定义属性双向数据绑定

DataBinding-自定义属性双向数据绑定

作者: dashingqi | 来源:发表于2020-12-07 22:28 被阅读0次
Android_Banner.jpg

简介

在使用DataBinding的时候我们知道数据驱动UI的显示,这种单向的数据绑定也是我们使用它最多的地方,既然有单向的数据绑定应该会存在双向绑定?

不错Android官方确实为我们提供了相应的双向绑定的属性。

比如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" />

所谓的双向绑定就是数据能驱动UI的显示,UI的状态变换也能改变绑定的属性值;

针对上述的EditText和CheckBox,它的双向绑定是系统帮助我们处理好了,双向绑定的写法也很简单 “@={属性值}”

接着就我们就自定义一个双向绑定的属性,在实现前我们看下最终的效果图

inverse.gif

看了效果,你想用RecylerView来实现?可以,不过要是使用双向数据绑定可以很简单的。

实现步骤

DataBinding为我们实现了双向数据绑定提供了@InverseBindingAdapter注解;

步骤一

首先我们需要一个数据源用来设置控件上布局的数据,我们使用@BindingAdapter自定义一个属性 data用来接受数据源

 /**
 * 设置数据源
 */
 @JvmStatic
 @BindingAdapter(value = ["data"], requireAll = true)
 fun setData(inverseGroupView: InverseGroupView, data: List<String>?) {
   data?.let {
   inverseGroupView.setData(it)
   }
 }
步骤二

然后我们需要定义双向绑定的属性 index 同样使用 @BindingAdapter

/**
* 设置角标
* 当数据发生边改的时候,会调用该方法设置数据 更新UI
*/
@JvmStatic
@BindingAdapter(value = ["index"], requireAll = true)
fun setIndex(inverseGroupView: InverseGroupView, index: Int) {
    inverseGroupView.selectIndex = index
    inverseGroupView.refreshSelectedIndex(index)
}
步骤三

接着我们使用@InverseBindingAdapter注解,来将我们因UI状态改变而导致属性值改变同步给自定义属性index(也就是从View中读取到值)

在使用@InverseBindingAdapter注解使,内部有两个属性,attribute:对应着自定义属性,event:是一个事件名称改属性在下面我会详细说明一下

@JvmStatic
@InverseBindingAdapter(attribute = "index", event = "indexChange")
fun getIndex(inverseGroupView: InverseGroupView) = inverseGroupView.selectIndex

到这里总结一下哈:当我们的index值发生改变的情况会调用步骤二中方法,通知UI布局发生变换

当我们的UI布局状态发生改变的情况下我们可以调用步骤三方法通知给绑定的属性值,让它重新设置值。

但是有一个问题就是步骤三的方法不知道UI的状态发生变化的时机此时就需要我们步骤4的操作了

步骤四

我们使用@BindingAdapter注解实现了一个View的状态值发生变化的事件通知,

注解中的value值要和@InverseBindingAdapter中的event中的值要保持一致,这样当View的状态值发生变化后会通知步骤三种的方法拿到值设置给绑定的属性值。

/**
* 双向数据绑定的
* InverseBindingListener 是一个监听器,用来处理属性改变时的通知
* 在这里我们给View设置了点击事件,当属性发生改变它会回调 onChange方法告诉DataBinding 去 @InverseBindingAdapter修饰的方法中取到值 然后设置给绑定的变量
*/
@JvmStatic
@BindingAdapter("indexChange")
fun setIndexChangeListener(
  inverseGroupView: InverseGroupView,
  changeListener: InverseBindingListener?
) {
      if (changeListener != null) {
        inverseGroupView.onSelectChangeListener = {
          changeListener.onChange()
        }
      } else {
        inverseGroupView.onSelectChangeListener = null
      }
}

到这里一个简单的自定义属性的双向绑定就完成了,这里我贴一下当时写的源码

/**
 * @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
    )
    /**
     * 当前选中的index
     */
    var selectIndex: Int = 0
    /**
     * tag点击事件的回调事件
     */
    var onSelectChangeListener: ((Int) -> Unit)? = null
    /**
     * 用于收集回收可复用的View
     */
    var recyclerView = ArrayList<View>()
    /**
     * 用于存储拿到的数据
     */
    var mData: List<Any>? = null
    /**
     * 设置数据
     */
    fun <T : Any> setData(data: List<T>) {
        updateViewData(data)
    }
    /**
     * 更新布局的数据
     * 创建布局,将数据设置到布局上
     * data:新的数据源
     */
    private fun <T : Any> updateViewData(data: List<T>) {
        mData = data
        // 每次执行到这个方法时,需要回收一下,移除一下,因为接下来是要重新绑定数据的,
        recyclerViewMethod()
        /**
         * 遍历循环数据源,将数据绑定帮控件上
         */
        data.forEachIndexed { index, any ->
            val tagView = getReuseView()
            val tvTagView = tagView.findViewById<TextView>(R.id.tag)
            tagView.isSelected = index == selectIndex
            tvTagView.text = any as String
            //设置一下点击事件
            tagView.setOnClickListener {
                //要更新下布局上按钮的状态
                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()
        }
    }
    /**
     * 首先将目前布局上已经有的子View存储到复用池中
     * 然后将这些子View从布局上移除
     */
    private fun recyclerViewMethod() {
        for (i in 0 until childCount) {
            recyclerView.add(getChildAt(i))
        }
        removeAllViews()
    }
    companion object {
        /**
         * 设置数据源
         */
        @JvmStatic
        @BindingAdapter(value = ["data"], requireAll = true)
        fun setData(inverseGroupView: InverseGroupView, data: List<String>?) {
            data?.let {
                inverseGroupView.setData(it)
            }
        }
        /**
         * 设置角标
         * 当数据发生边改的时候,会调用该方法设置数据 更新UI
         */
        @JvmStatic
        @BindingAdapter(value = ["index"], requireAll = true)
        fun setIndex(inverseGroupView: InverseGroupView, index: Int) {
            if (inverseGroupView.selectIndex == index) return
            inverseGroupView.selectIndex = index
            inverseGroupView.refreshSelectedIndex(index)
        }
        /**
         * 获取到当前选中的角标
         * event:数据改变的事件
         *
         * 当View的状态发生改变的时候(包括数据的填充,bg的改变),会调用该方法来获取到值
         */
        @JvmStatic
        @InverseBindingAdapter(attribute = "index", event = "indexChange")
        fun getIndex(inverseGroupView: InverseGroupView) = inverseGroupView.selectIndex
        /**
         * 双向数据绑定的
         * InverseBindingListener 是一个监听器,用来处理属性改变时的通知
         * 在这里我们给View设置了点击事件,当属性发生改变它会回调 onChange方法告诉DataBinding 去 @InverseBindingAdapter修饰的方法中取到值 然后设置给绑定的变量
         */
        @JvmStatic
        @BindingAdapter("indexChange")
        fun setIndexChangeListener(
            inverseGroupView: InverseGroupView,
            changeListener: InverseBindingListener?
        ) {
            if (changeListener != null) {
                inverseGroupView.onSelectChangeListener = {
                    changeListener.onChange()
                }
            } else {
                inverseGroupView.onSelectChangeListener = null
            }
        }
    }

}

注意点

由于当时我绑定的属性值使用的是LiveData,当我改变了View的状态值是会通知到@InverseBindingAdapter注解修饰的方法让它拿到值设置给绑定的属性值。

由于LiveData天生就有可观察性,当观察到数据源发生变化又会驱动UI状态值发生变化,这样UI发生变化又会被监听到 又去通知@InverseBindingAdapter注解修饰的方法让它拿到值设置给绑定的属性值。

这样就会陷入到无限的循环中,所以我当时的做法就是在绑定的属性值的 setter方法中做了新旧值的判断,如果值一致就不触发UI状态值的更新了

@JvmStatic
@BindingAdapter(value = ["index"], requireAll = true)
fun setIndex(inverseGroupView: InverseGroupView, index: Int) {
    if (inverseGroupView.selectIndex == index) return
    inverseGroupView.selectIndex = index
    inverseGroupView.refreshSelectedIndex(index)
}

其实这个地方可以从绑定的属性值入手解决这个问题,比如我们在设置值之前检查当前的值和将要的值一致的话就不进行 set或者post,比如我这个 DiffLiveData

/**
 * @author : zhangqi
 * @time : 12/6/20
 * desc : 如果当前LiveData中携带的值和将要设置的值是一致的,就不进行设置值的操作了
 */
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架构的基础,给我们带来很多方便之处,比如利用这种思想的开源库ItemBinding

就给我在日常开发中有很大的效率提高;这些好用的地方完全胜过它的一些小缺点。

相关文章

  • DataBinding-自定义属性双向数据绑定

    简介 在使用DataBinding的时候我们知道数据驱动UI的显示,这种单向的数据绑定也是我们使用它最多的地方,既...

  • Angular基本指令与语法

    循环指令 选择指令 注意事项 多重分支判断 事件绑定 属性绑定 双向数据绑定 自定义指令 创建组件 使用组件 过滤...

  • Vue组件中的数据传递

    Vue组件中严禁数据双向绑定。避免数据源的污染。 1.自定义属性(props) 2.自定义事件(event冒泡原理...

  • JS双向数据绑定

    双向数据绑定简述 双向数据绑定,可以将JS对象的属性绑定到DOM节点上,实现JS对象跟DOM节点的同名属性的关联,...

  • Vue - v-model原理

    v-model相当于一个自定义指令,通过事件监听和属性绑定实现双向数据绑定。 使用:value绑定msg到inpu...

  • Angular框架中的模板语法和常用指令

    模板表达式(属性绑定): 输入属性的值为实例属性: 事件绑定: 双向数据绑定: *与template: 1、*ng...

  • angular 学习记录(四)

    数据绑定: 一、数据双向绑定( [(...)] ): 你经常需要显示数据属性,并在用户作出更改时更新该属性。在元素...

  • vue 自定义组件双向绑定v-model

    'vue的双向绑定是针对表单元素,当然可以自定义组件实现双向绑定。(外部data的字段值赋值给自定义组件的属性,自...

  • angular的双向数据绑定与属性绑定

    1.双向数据绑定语法为{{}}, 2.而属性绑定为[ ] =" " 如果想在input框中使用双向绑定[(ngMo...

  • vue基础

    v-bind:v-bind:(属性名) 单向数据绑定 v-model:v-model:(属性名) 双向数据绑定,一...

网友评论

    本文标题:DataBinding-自定义属性双向数据绑定

    本文链接:https://www.haomeiwen.com/subject/bbjxgktx.html