图片来自必应
Databinding是Google推出的一个支持View与ViewModel绑定的Library,可以说Databinding建立了一个UI与数据模型之间的桥梁,即UI的变化可以通知到ViewModel, ViewModel的变化同样能够通知到UI从而使UI发生改变,大大减少了之前View与Model之间的胶水代码,如findViewById, 改变及获取TextView的内容还需要调用setText()、 getText(),获取EditText编辑之后的内容需要调用getText(),而有了Databinding的双向绑定,这些重复的工作都将被省去。下面我们就来看一下如何使用Databinding来双向绑定
首先我们先定一个ViewModel,将这个ViewModel的变量content与布局文件中的TextView绑定:
class MyViewModel: ViewModel(){ var content: ObservableFiled= ObservableFiled() }复制代码
- 官方支持的双向绑定 这里的官方支持指的是Databinding库中已经定义了一些View的双向绑定,我们如果要使用的话只需要将xml文件中的"@{}"改成"@={}", 如下代码
(test_layout.xml):复制代码
上述代码中注释部分就是将单向绑定改为双向绑定的代码
官方支持的双向绑定:
- AbsListView android:selectedItemPosition
- CalendarView android:date
- CompoundButton android:checked
- DatePicker android:year, android:month, android:day
- NumberPicker android:value
- RadioGroup android:checkedButton
- RatingBar android:rating
- SeekBar android:progress
- TabHost android:currentTab
- TextView android:text
- TimePicker android:hour, android:minute
那么双向绑定是怎么实现的呢?首先来看一下Databinding的源码:
首先是我们在编译之后会生成几个相关文件如test_layout.xml, test_layout-layout.xml, TestLayoutBinding.java, BR文件等。我们主要来看一下TestLayoutBinding.java这个文件。这个文件的主要作用是声明xml内的View控件以及声明的ViewModel,如本例中声明了一个TextView:
@NonNull private final android.widget.LinearLayout mboundView0; @NonNull private final android.widget.TextView mboundView1; // variables @Nullable private com.fb.onedayimprove.viewmodel.MyModel mModel;复制代码
上述代码中mboundView0为根布局、mboundView1为声明了双向绑定的TextView,mModel为与View绑定的ViewModel。(注意:并不是所有的变量都会被声明,有三种情况:在xml布局中声明id的,将会被定义为静态变量,可以直接通过Databinding对象访问;引用了model数据的;根布局)
接下来当我们定义了双向绑定的时候,TestLayoutBinding.java会生成这样一段代码:
private android.databinding.InverseBindingListener mboundView1androidTextAttrChanged = new android.databinding.InverseBindingListener() { @Override public void onChange() { // Inverse of model.content.get() // is model.content.set((java.lang.String) callbackArg_0) //调用定义的TextViewBindingAdapter中的getTextString(TextView view)方法得到mboundView1(即布局中定义的TextView)的值 java.lang.String callbackArg_0 = android.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView1); // localize variables for thread safety // model com.fb.onedayimprove.viewmodel.MyModel model = mModel; // model.content.get() //当前View绑定的ViewModel中的content的值 java.lang.String modelContentGet = null; // model != null boolean modelJavaLangObjectNull = false; // model.content android.databinding.ObservableFieldmodelContent = null; // model.content != null boolean modelContentJavaLangObjectNull = false; modelJavaLangObjectNull = (model) != (null); if (modelJavaLangObjectNull) { modelContent = model.getContent(); modelContentJavaLangObjectNull = (modelContent) != (null); if (modelContentJavaLangObjectNull) { //将View中的值取出并复制给相应的model中绑定的数据 modelContent.set(((java.lang.String) (callbackArg_0))); } } } };复制代码
通过上述代码可以看到实现逆向绑定的关键部分,可以看到调用InverseBindingListener接口的onChange()方法就可以将view的值传回ViewModel。那么它在哪里被用到呢?看下面的代码:
@Override protected void executeBindings() { ... if ((dirtyFlags & 0x4L) != 0) { android.databinding.adapters.TextViewBindingAdapter.setTextWatcher(..., mboundView1androidTextAttrChanged); } }复制代码
可以看到在executeBindings这个方法中,将InverseBindingListener 的值传给setTextWatcher,这个方法怎么来的后面会提到。
上面的代码中提到一个TextViewBindingAdapter,通过它的名字就可以看出是绑定View与ViewModel的适配器,那么来看一下TextViewBindingAdapter做了什么 (在包android.databinding.adapters下):
@BindingAdapter("android:text") public static void setText(TextView view, CharSequence text) { final CharSequence oldText = view.getText(); //为防止双向绑定无限循环调用比较新旧内容是否相同 if (text == oldText || (text == null && oldText.length() == 0)) { return; } if (text instanceof Spanned) { if (text.equals(oldText)) { return; // No change in the spans, so don't set anything. } } else if (!haveContentsChanged(text, oldText)) { return; // No content changes, so don't set anything. } view.setText(text); }复制代码
上述代码相信大家并不陌生,当Databinding在给某一个控件的XXX属性赋值的时候,需要去找到相应的setXXX()方法,然后将model中的值给这个View。而@BindingAdapter ("xxx")正是将这个setXXX()方法与xxx属性关联起来,所以上面部分的代码作用总结起来就是做了view.setText(text)。(注:可以看到代码中有对新旧内容的比较,只有当内容不同的时候才会执行下一步,这是为了防止双向绑定循环调用)
再来看下一个方法:
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged") public static String getTextString(TextView view) { return view.getText().toString(); }复制代码
这个方法在TestLayoutBinding.java中已经说明了,是双向绑定取值时调用的方法,@InverseBindingAdapter注解和@BindingAdapter注解作用类似。
再来看下一个关键方法:
@BindingAdapter(value = { "android:beforeTextChanged", "android:onTextChanged", "android:afterTextChanged", "android:textAttrChanged"}, requireAll = false) public static void setTextWatcher(TextView view, final BeforeTextChanged before, final OnTextChanged on, final AfterTextChanged after, final InverseBindingListener textAttrChanged) { final TextWatcher newValue; //listener为空则不作处理 if (before == null && after == null && on == null && textAttrChanged == null) { newValue = null; } else { //创建一个TextWatcher监听text内容的变化 newValue = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { if (before != null) { before.beforeTextChanged(s, start, count, after); } } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (on != null) { on.onTextChanged(s, start, before, count); } //如果设定了双向绑定,则InverseBindingListener不为空,可参见之前的TestLayoutBinding.java对其的赋值。 if (textAttrChanged != null) { //调用onChange()方法(实现在TestLayoutBinding.java中),即将view的值传回给ViewModel textAttrChanged.onChange(); } } @Override public void afterTextChanged(Editable s) { if (after != null) { after.afterTextChanged(s); } } }; } final TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher); if (oldValue != null) { view.removeTextChangedListener(oldValue); } if (newValue != null) { view.addTextChangedListener(newValue); } }复制代码
从上面的代码我们可以看到@BindingAdapter绑定了几个属性,其中有一个叫做"android:textAttrChanged",那么这个属性就代表了当TextView的text属性变化,xxxAttrChanged。可以想象得到那这个方法就是在android:text属性发生变化的时候会被调用。我们在这个方法中设置了对android:text这个属性内容变化的监听(注意是内容变化,不是属性变化),以后当每次改内容变化的时候,就会调用textAttrChanged.onChange()这个方法将TextView的值传回给ViewModel。
上述的是通过官方支持的源码来看双向绑定,那么我们要自定义的View使用双向绑定应该怎么做呢?
-
自定义View双向绑定
通过对官方源码分析,我们发现要实现双向绑定关键是需要实现Adapter,实现setter,getter,以及Listene方法。下面就让我们来实现一个自定义View 的双向绑定吧。 对MyModel稍作修改:
class MyModel: ViewModel(){ var content: ObservableField= ObservableField() fun onChangeClick(view: View){ if(view is TestEditView){ Log.v("---TAG---", content.get()) view.setValue("改变后") Log.v("---TAG---", "click: ${content.get()}") } } }复制代码
对test_layout.xml修改(主要是使用自定义的View),加了一个点击事件,点击之后修改View的内容:
复制代码
这里我们引入一个自定义View:这是一个类似表单的View,左边是个小标题,右边可以填一些信息等:
class TestEditView: LinearLayout{ private lateinit var contentView: TextView private var onContentedListener: OnContentChangedListener? = null { ... } private fun initView(context: Context){ val view = View.inflate(context, R.layout.test_edit_layout, this) contentView = view.findViewById(R.id.text_content) } fun setOnContentListener( listener: OnContentChangedListener?){ this.onContentedListener = listener } public fun setValue(content: String){ if (content.isEmpty() || (!content.isEmpty() && content == contentView.text)){ return } contentView.text = content onContentedListener?.onContentChanged() } fun getContent(): String{ return contentView.text.toString() } //内容改变Listener interface OnContentChangedListener{ fun onContentChanged() } }复制代码
省略了部分代码,主要就是定义了一个内容改变的Listener。 下面是TestEditViewAdapter的代码:
//使用InverbaseBindingMethod注解,与使用@BindingAdapter效果一样其中,event、method可声明也可以不声明,不声明的话会默认设置xxxAttrChanged 与 getXXX() 方法 //@InverseBindingMethods(InverseBindingMethod(type = TestEditView::class, attribute = "content", event = "contentAttrChanged", method = "getContent")) object TestEditViewAdapter{ @JvmStatic @BindingAdapter("app:content") fun setContent(view: TestEditView, content: String){ if (content.isNotEmpty() && view.getContent() == content){ return } view.setValue(content) } @JvmStatic @InverseBindingAdapter(attribute = "app:content", event = "contentAttrChanged") fun getContent(view: TestEditView): String{ return view.getContent() } @JvmStatic @BindingAdapter(value = "app:contentAttrChanged", requireAll = false) fun setContentAttrChangedListener(view: TestEditView, bindingListener: InverseBindingListener){ if(bindingListener == null){ view.setOnContentListener(null) }else{ //如果设置了双向绑定则为其添加监听 view.setOnContentListener(object : OnContentChangedListener{ override fun onContentChanged() { bindingListener.onChange() } }) } } }复制代码
接下来我们只需要在Fragment中为content赋值,然后运行程序,点击TestEditView,就可以看到Log输出了content的初始值与改变后的值(不贴图展示了)。
val binding = TestLayoutBinding.inflate(inflater!!, container, false) val model = MyModel() model.content.set("初始值") binding.model = model return binding.root复制代码
-
总结 以上就是Databinding双向绑定的使用方法,总的来说应该注意两点:
1、修改"@{}" 为 "@={}"
2、写setXXX(), xxxAttrChanged(), getXXX()方法。