https://github.com/LaxusJie/DataBindingSimple
Data binding 入坑笔记二进阶篇之双向绑定
https://www.cnblogs.com/longjunhao/p/5860353.html
https://academy.realm.io/cn/posts/data-binding-android-boyar-mount/ 英文翻译(较全)
https://www.colabug.com/1197309.html 使用@InverseBindingMethods实现反向绑定
https://blog.csdn.net/zhangphil/article/details/77839555 使用@InverseBindingAdapter反向绑定
DataBinding框架使用的的环境要求:
1、保证接入的项目gradle插件版本不低于1.5.0-alpha1:
classpath'com.android.tools.build:gradle:1.5.0'
2、在对应模块(Module)的build.gradle文件添加如下配置
dataBinding {
enabledtrue
}
Databinding接入
一、布局文件
要使用Data Binding首先xml文件有了以下变化,他的根节点变成了layout,里面包含一个data节点和原根节点LinearLayout,其中这个data是用来绑定数据的。
在data内可以利用variable声明变量,里面有两个属性name和type,name是变量的名字,type是变量的类型。(这里的User是一个实体类,下面说)
也可以把类型提出来用import导入 ,基础类型(java.lang包下的,比如String)可以直接使用,无需import。
配置好布局文件后,IDE会根据xml文件的名称自动生成一个DataBinding类用于数据绑定,命名规则如下
activity_main.xml-> ActivityMainBinding
如果不喜欢自动生成的Data Binding名,我们可以自己来定义
.........
class对应的就是生成的Data Binding名
二、(viewModle)POJO对象
POJO类对象User,这种类型的对象具有从不改变的数据。在应用程序中,数据是一次读取,此后从不更改,这是很常见的。
public class User{
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
也可以使用JavaBean形式
public class User{
private final String firstName;
private final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
returnthis.firstName;
}
public String getLastName() {
returnthis.lastName;
}
}
修改Activity中的onCreate方法,
用DataBindingUtil.setContentView替代原来的setContentView方法从而实现数据绑定,创建user对象,
通过binding.setUser(user)从而实现数据填充。(此时还是自动填充,需要手动调用binding对象的setUser方法通binding对象更新view视图)
private User user;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
user =newUser("Laxus","J");
binding.setUser(user);
}
此处可以多说一句,binding的setUser()方法是根据布局文件中标签在编译时生成了一个方法,binding也是编译时生成的一个类,这个方法就声明在binding这个类中,并且还有对应的一个getUser方法。除此之外,binging里还有int age这个属性的set,get方法;
前面说了,写到这个一步已经可以将user对象的数据刷新到布局视图中去了,但是需要手动调用setUser方法,那么怎样才能在数据更新后自动更新到布局中而不用调用setUser方法呢?
Databing提供了一个Observable接口,实现此接口的的pojo类可以被Databing框架识别和管理;为了方便,Databing框架已经为我们封装了一个BaseObservable类,里面帮我们已经封装了好了基础功能;
让一个类继承BaseObservable:
public class Student extends BaseObservable{
private String name;
privateString className;
@Bindable
public String getName() {
return name;
}
@Bindable
public String getClassName() {
return className;
}
public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name);
}
public void setClassName(String className) {
this.className = className;
notifyPropertyChanged(BR.className);
}
}
BR是编译阶段生成的一个类,功能与 R.java 类似,用 @Bindable标记过 getter方法会在BR中生成一个静态常量池,所有Pojo类的属性在这个BR都有唯一一个对应的BR.XX常量。
public class BR{
public static final int_all =0;
public static final int className =1
}
在setter方法里我们需要用notifyPropertyChanged方法来更新对象。写了这个notifyPropertyChanged方法,此时就可以不用之后再改变一个对象中的某一个属性,就可以不用再调用binding对象的整个set方法;
还有另一种适用于POJO的写法,这种写法不需要继承BaseObservable,不过每个成员变量都要new一个ObservableField对象。
public class Teacher{
public final ObservableField name =newObservableField<>();
public final ObservableField className = newObservableField<>();
public final ObservableInt age =newObservableInt();
}
注意,这个类里面没有set、get方法,不用写,Databing已经做了处理,使用时在代码中如下写:
mTeacher.name.set("wang");
mTeacher.className.set("Java");
而在布局文件中的使用方法与普通属性写法一样;
DataBinding 还准备了ObservableBoolean,ObservableByte,ObservableChar,ObservableShort,ObservableInt,ObservableLong,ObservableFloat,ObservableDouble, 以及ObservableParcelableObservableArrayMap和ObservableArrayList。
逆向绑定
上边已经讲完了,数据改变引起视图改变的单向绑定,下面说一下页面中空间的内容改变如何引起数据改变;
如果想在用户输入的时候给一个pojo类的属性赋值,则需要用到@={}这个操作符
只比单向绑定时,在@和{}中间多了一个=,就变成了双向绑定!
引申
我们引申两点,一是逆向绑定的原理和另一个用法,二,整个Databing的工作原理。
逆向绑定:
Databing在对页面生成一个bing对象时,会根据这个布局页面里的view生成一些代码,其中就包括了如下代码:
// values
// listeners
// Inverse Binding Event Handlers
private android.databinding.InverseBindingListener mboundView1androidTextAttrChanged =
new android.databinding.InverseBindingListener() {
@Override
public void onChange() {
// Inverse of student.name
// is student.setName((java.lang.String) callbackArg_0)
java.lang.String callbackArg_0 = android.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView1); // localize variables for thread safety
// student com.jie.databindingsimple.entity.Student student = mStudent;
// student.name java.lang.String studentName = null;
// student != null
if (student)!= null)) {
student.setName(((java.lang.String) (callbackArg_0)));
}
}
};
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
return view.getText().toString();
}
@BindingAdapter(value = {"android:textAttrChanged"}, requireAll = false)
public static void setAndroidTextAttrChanged(Textview textview, InverseBindingListener inverseBindingListener) {
if (inverseBindingListener == null) {
Log.e("错误!", "InverseBindingListener为空!");
} else { // 触发更新
inverseBindingListener =inverseBindingListener ;
}
}
先是Databing在编译时根据布局文件在生成如上代码,运行时当用户改变了文本,触发textview的text属性的改变,然后Databing根据text属性找到了getTextString方法和上边的的注解中的event,然后根据event中的android:textAttrChanged找到setAndroidTextAttrChanged方法,并将mboundView1androidTextAttrChanged属性设置进去,而textview的相关监听中判断InverseBindingListener!=null了,就调用InverseBindingListener.onChange(); onChange方法找到getTextString方法获取文本并设置给Pojo对象;以上的的代码Databing已经帮我们写好了,不过知道了这几部原理,我们也可以定义自己view逻辑。具体参考:
https://www.colabug.com/1197309.html 使用@InverseBindingMethods实现反向绑定
https://blog.csdn.net/zhangphil/article/details/77839555使用@InverseBindingAdapter反向绑定。
我回头看一下Observable接口:
package android.databinding;
public interface Observable {
void addOnPropertyChangedCallback(Observable.OnPropertyChangedCallback var1);
void removeOnPropertyChangedCallback(Observable.OnPropertyChangedCallback var1);
public abstract static class OnPropertyChangedCallback { public OnPropertyChangedCallback() { }
public abstract void onPropertyChanged(Observable var1, int var2); }
}
Observable接口中有一个addOnPropertyChangedCallback方法用来添加一个接收属性改变的callback,我可以在activity中这样写,
来监听到某个属性的改变;
ActivityTwowayBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_twoway);
Student student = new Student();
binding.setStudent(student);
student.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable observable, int i) {
switch (i) {
case BR.name:
Toast.makeText(TwowayActivity.this, "name", Toast.LENGTH_SHORT).show();
break;
case BR.className:
Toast.makeText(TwowayActivity.this, "classname", Toast.LENGTH_SHORT).show();
break;
}
}
});
}
目前双向绑定仅支持如text,checked,year,month,hour,rating,progress等绑定。其它的需要我们自定义;
Databing原理
https://www.jianshu.com/p/c41e7a597ac8
自定义用法
1、转换器
类型转换对view赋值时有些类型需要转换,例如:color 为int值 ,
view.setBackground 需要的是drawable参数,这个时候,需要定义类型转换,
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
注意这个方法必须是public static的,该方法可以定义在任何地方。
2、适配器 @BindAdapter
适配器开发中一个比较常见的场景就是使用ImageLoader进行加载图片,在任何地方添加适配器代码:
@BindingAdapter("android:src",requireAll = false)
public static void setImageUrl(ImageView view, String url) {
Picasso.with(view.getContext()).load(url).into(view);
}
这个时候可以使用下面代码进行数据绑定:当进行bind的时候,会自动调用适配器定义的函数。适配器也可以用来添加view的属性。比如我想在Textview中绑定long型的时间戳,并且按照一个格式显示。
@android.databinding.BindingAdapter(value = {"app:date", "app:format"}, requireAll = false)
public static void bindFormat(TextView view, String date, String format) {
if (date == null) {
return;
}
SimpleDateFormat formatter;
if (format != null) {
formatter = new SimpleDateFormat(format);
}else {
formatter = new SimpleDateFormat("yyyy-MM-dd");
}
Long timeStamp = Long.parseLong(date);
view.setText(formatter.format(new Date(timeStamp)));
}
其中 requireAll = false表示当参数不全时,也会调用适配器函数,补全的参数会传入空值,所以要自己判断。
关于ImageView的另一种用法:
@BindingAdapter(value={"android:src","placeHolder"},requireAll=false)
public static void setImageUrl(ImageViewview,Stringurl,intplaceHolder){
RequestCreator requestCreator=Picasso.with(view.getContext()).load(url);
if(placeHolder!=0){
requestCreator.placeholder(placeHolder);
}
requestCreator.into(view);
}
资源(Resources):
可以使用正常语法的表达式访问资源:
[html]view plaincopy
android:padding=“@{large? @dimen/largePadding : @dimen/smallPadding}”
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
格式字符串和复数可通过提供参数判断:
[html]view plaincopy
android:text=“@{@string/nameFormat(firstName, lastName)}”
android:text=“@{@plurals/banana(bananaCount)}”
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
当复数需要多个参数时,所有参数都应通过:
[html]view plaincopy
Have an orange
Have %d oranges
android:text=“@{@plurals/orange(orangeCount, orangeCount)}”
Have an orange
Have %d oranges
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
一些资源需要显示类型判断:
自动安装(Automatic Setters)
对于一个属性,Data Binding 试图找到 setAttribute 方法,与该属性的 name space 并没有什么关系,仅仅与属性本身名称有关
例如,有关 TextView 的 android:text 属性的表达式会寻找一个 setText(String) 的方法,如果表达式返回一个 int,Data Binding会搜索的 setText(int) 方法,这里要注意:表达式要返回正确的类型,如果需要的话使用 casting,Data Binding 任然会工作,即使没有给定的名称属性存在,然后,你可以通过 Data Binding 轻松地为任何 setter “创造”属性,例如,DrawerLayout 没有任何属性,但大量的 setters,你可以使用自动 setters 来使用其中的一个:
[html]view plaincopy
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
app:scrimColor=“@{@color/scrim}”
app:drawerListener=“@{fragment.drawerListener}”/>
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}"/>
重命名 Setters(Renamed Setters)
一些有 Setters 的属性按名称并不匹配,对于这些方法,属性可以通过 BindingMethods 注解相关联,还必须与一个包含BindingMethod 注解类相关联,每一个用于重命名的方法,例如 andorid:tin 属性与 setImageTintList 相关联,而不与 setTint相关
[java]view plaincopy
@BindingMethods({
@BindingMethod(type = “android.widget.ImageView”,
attribute =”android:tint”,
method =”setImageTintList”),
})
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
上面的例子,开发者不太可能重命名编译程序,Android 框架属性已经实现了
泛型支持
">
">// 左尖括号需要转义,
自定义 Setters(Custom Setters)
有些属性需要自定义绑定逻辑,例如,对于 android:paddingLeft 属性并没有相关的setter,相反,setPadding (left、top、right、bottom)是存在,一个带有 BindingAdapter 注解的静态绑定适配器方法允许开发者自定义 setter 如何对于一个属性的调用
Android 的属性已经创造了 BindingAdapters,举例来说,对于 paddingLeft:
[java]view plaincopy
@BindingAdapter(“android:paddingLeft”)
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
Binding 适配对其他定制类型非常有用,例如,自定义 loader 可以用异步载入图像
当有冲突时,开发人员创建的 Binding 适配器将覆盖 Data Binding 默认适配器
你也可以创建可以接收多个参数的适配器:
[java]view plaincopy
@BindingAdapter({“bind:imageUrl”, “bind:error”})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.with(view.getContext()).load(url).error(error).into(view);
}
@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.with(view.getContext()).load(url).error(error).into(view);
}
[html]view plaincopy
app:error=“@{@drawable/venueError}”/>
app:error="@{@drawable/venueError}"/>
如果对于一个 ImageView imageUrl 和 error 都被使用,并且 imageUrl 是一个 String 类型以及 error 是一个 drawable 时,该适配器被调用
匹配的过程中自定义 name spaces 将被忽略
你也可以为 Android name spaces 写适配器
绑定适配器方法可以在其处理程序中选择旧值。取旧值和新值的方法应具有所有属性的旧值,其次是新值:
[java]view plaincopy
@BindingAdapter(“android:paddingLeft”)
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
if (oldPadding != newPadding) {
view.setPadding(newPadding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
}
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
if (oldPadding != newPadding) {
view.setPadding(newPadding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
}
事件处理程序只能用一个抽象方法与接口或抽象类一起使用,例如:
[java]view plaincopy
@BindingAdapter(“android:onLayoutChange”)
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
View.OnLayoutChangeListener newValue) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (oldValue != null) {
view.removeOnLayoutChangeListener(oldValue);
}
if (newValue != null) {
view.addOnLayoutChangeListener(newValue);
}
}
}
@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
View.OnLayoutChangeListener newValue) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (oldValue != null) {
view.removeOnLayoutChangeListener(oldValue);
}
if (newValue != null) {
view.addOnLayoutChangeListener(newValue);
}
}
}
当监听器有多个方法时,它必须被分割成多个监听器,例如,View.OnAttachStateChangeListener 的方法有两种:onViewAttachedToWindow() 和 onViewDetachedFromWindow()。然后,我们必须创建两个接口来区分它们的属性和处理程序:
[java]view plaincopy
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}
因为改变一个监听器也会影响另一个,我们必须有三个不同的绑定适配器,一个为每个属性和一个为两者,他们应该被设置:
[java]view plaincopy
@BindingAdapter(“android:onViewAttachedToWindow”)
public static void setListener(View view, OnViewAttachedToWindow attached) {
setListener(view,null, attached);
}
@BindingAdapter(“android:onViewDetachedFromWindow”)
public static void setListener(View view, OnViewDetachedFromWindow detached) {
setListener(view, detached,null);
}
@BindingAdapter({“android:onViewDetachedFromWindow”, “android:onViewAttachedToWindow”})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
final OnViewAttachedToWindow attach) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
final OnAttachStateChangeListener newListener;
if (detach == null && attach == null) {
newListener =null;
}else {
newListener =new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (attach != null) {
attach.onViewAttachedToWindow(v);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
if (detach != null) {
detach.onViewDetachedFromWindow(v);
}
}
};
}
final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
newListener,R.id.onAttachStateChangeListener);
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener);
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener);
}
}
}
@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
setListener(view, null, attached);
}
@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
setListener(view, detached, null);
}
@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
final OnViewAttachedToWindow attach) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
final OnAttachStateChangeListener newListener;
if (detach == null && attach == null) {
newListener = null;
} else {
newListener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (attach != null) {
attach.onViewAttachedToWindow(v);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
if (detach != null) {
detach.onViewDetachedFromWindow(v);
}
}
};
}
final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
newListener, R.id.onAttachStateChangeListener);
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener);
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener);
}
}
}
上面的例子是比正常的稍微复杂,因为 View 使用添加和删除的监听者而不是为 View.OnAttachStateChangeListener 设置方法,android.databinding.adapters.listenerutil 类有助于保持跟踪,他们可能会在绑定适配器删除以前的 listener
https://blog.csdn.net/willba/article/details/71552525时间监听
事件绑定方式
监听方法调用控件常用的监听都可以用事件绑定来搞定,比如以下这几种
android:onClick
android:onLongClick
android:onTextChanged
其他事件同理
Lambda表达式
在xml中我们还可以应用lambda表达式来书写
android:onClick="@{(view) -> activity.onTextClick(view)}"
统计一下事件监听表达的用法
由此可见事件监听绑定有很多种用法,我们在项目中最好统一规范应用其中一种,避免个性化
android:onClick="onTextClick"
android:onClick="@{(view) -> activity.onTextClick(view)}"
android:onClick="@{activity::onTextClick}"
android:onClick="@{activity.onTextClick}"
问题汇总:
直接 Binding
当一个 Variable 或 Observable 变化时,binding 会在下一帧前被计划要求改变,但是有很多次在 Binding 时必需立即执行,要强制执行,使用 executePendingBindings() 方法
后台线程
只要它不是一个集合,你可以在后台中改变数据模型,在判断是否要避免任何并发问题时,Data Binding 会对每个 Variable/field本地化
线程问题
无论在子线程还是主线程中更新ViewModle数据,Binding框架在更新Ui时都是将交给主线更关心uI,通常来说不会需要我们再做同步的事情,但是如果是RecycleView,则需要再UI线程中更新数据,因为列表在滑动过程中不断binging新Item,UI线程会不断读取viewModle中的数据(get方法),如果此时再子线程更新了uI线程正在读取的数据容易出现线程问题。
网友评论