在掌握了Data Binding的基础使用方法之后,来尝试一下相对高级的一点的使用方法。
慕客网对应的课程视频:Android Data Binding实战-高级篇
声明:博客只是个人写的,用于学习与交流,与慕课网平台和授课老师没有其他任何关系。如涉及版权问题,请联系本人,将马上改正。
首先,先看看主界面的布局:
<layout
xmlns:android="http://schemas.android.com/apk/res/
android"
xmlns:tools="http://schemas.android.com/tools">
<data class="RvActivityBinding">
data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.hut.example.Main2Activity">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv" //设置id方便直接引用
android:layout_width="match_parent"
android:layout_height="match_parent"/>
RelativeLayout>
layout>
在上述布局中,使用了一个技巧,就是通过class
属性,自定义了Binding类的类名。
默认情况下,binding 类的名称取决于布局文件的命名,以大写字母开头,移除下划线,后续字母大写并追加 “Binding” 结尾。这个类会被放置在 databinding 包中。举个例子,布局文件 contact_item.xml 会生成 ContactItemBinding 类。如果 module 包名为 com.example.my.app ,binding 类会被放在 com.example.my.app.databinding 中。
通过修改 data 标签中的 class 属性,可以修改 Binding 类的命名与位置。举个例子:
"CustomBinding">
...
以上会在 databinding 包中生成名为 CustomBinding 的 binding 类。如果需要放置在不同的包下,可以在前面加 “.”:
".CustomBinding">
...
这样的话, CustomBinding 会直接生成在 module 包下。如果提供完整的包名,binding 类可以放置在任何包名中:
"com.example.CustomBinding">
...
接着是RecyclerView的item的布局:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data class="TestBinding">
<variable
name="item_user"
type="com.hut.example.User"/>
data>
<LinearLayout
android:id="@+id/layout"
android:layout_margin="5dp"
android:background="#76f7e4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/test"
android:layout_width="match_parent"
android:layout_height="20dp"
android:text="Name:" />
<TextView
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center"
android:text="@{item_user.name}"
android:textSize="20sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_weight="2"
android:text="Age:" />
<TextView
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center"
android:text='@{""+item_user.age}'
android:textSize="20sp" />
LinearLayout>
layout>
就是一个简单线性布局,显示User对象的name与age。
public class User {
private ObservableField name = new ObservableField<>();
private ObservableInt age=new ObservableInt();
public User(String name, int age) {
this.name.set(name);
this.age.set(age);
}
public void setName(String name) {
this.name.set(name);
}
public void setAge(int age) {
this.age .set(age);
}
public ObservableInt getAge() {
return age;
}
public ObservableField getName() {
return name;
}
}
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.BindingHolder> {
private List mData=new ArrayList<>();
public MyAdapter() {
Random random=new Random();
for (int i=0;i<30;i++) {
User user=new User("User "+i,random.nextInt(100));
mData.add(user);
}
}
@Override
public BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// TestBinding binding=
// DataBindingUtil.inflate(
// LayoutInflater.from(parent.getContext()),
// R.layout.item1_user,parent,false);
//这里也可以直接使用item1_user布局对应的TestBinding(自定义命名了的),其父类就是ViewDataBindng
ViewDataBinding binding= DataBindingUtil.inflate(
LayoutInflater.from(parent.getContext()),
R.layout.item1_user,parent,false);
return new BindingHolder (binding);
}
@Override
public void onBindViewHolder(BindingHolder holder, int position) {
final User user=mData.get(position);
holder.getBinding().setVariable(com.hut.example.BR.item_user,user);
holder.getBinding().executePendingBindings();
}
@Override
public int getItemCount() {
return mData.size();
}
public static class BindingHolder extends RecyclerView.ViewHolder {
private final ViewDataBinding binding;
//如果声明的binding的类型为ViewDataBinding,并非根据某一布局而生成的特定的XxxBinding类型
//就算其对应的布局里面的控件设置了id,也无法通过binding.xxx来直接使用
public BindingHolder (ViewDataBinding binding) {
super(binding.getRoot());
this.binding=binding;
}
public ViewDataBinding getBinding() {
return binding;
}
}
}
最后只要在主界面中设置RecyclerView的LayoutManager以及适配器即可。
binding.rv.setLayoutManager(new LinearLayoutManager(this));
binding.rv.setAdapter(new MyAdapter());
我在这一节的视频在照着敲了之后,编译时会出现一个错误:
Error:(16, 24) 警告: Application namespace for attribute app:imageUrl will be ignored.
Error:(16, 24) 警告: Application namespace for attribute app:placeholder will be ignored.
而原因就出在下面代码上
@BindingAdapter({"app:imageUrl","app:placeholder"})
public static void loadImageForUrl(ImageView view, String url, Drawable drawable) {
Glide.with(view.getContext()).load(url).placeholder(drawable).into(view);
}
其中app
是命名空间,其名字是可以自定义的, 所以不一定非得叫app
,换成testapp
也照样可以在布局文件中使用,因此,需要在上面的代码中去掉app:
,变成@BindingAdapter({"imageUrl","placeholder"})
即可(在匹配时自定义命名空间会被忽略)。但是如果命名空间是系统的,如android:xxx
,那么可以写上完整的,如@BindingAdapter({"android:xxx})"
。(当然我也没试过不加android:
的 +_+)
而且关于这一节的视频内容,推荐去 Android Data Binding 系列(一) – 详细介绍与使用 的第8、9点去看看,那里会更直观。
尤其需要注意里面的那个例子(如下):
Binding adapter 在其他自定义类型上也很好用。举个例子,一个 loader 可以在非主线程加载图片。
// 无需手动调用此函数
@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Glide.with(view.getContext()).load(url).error(error).into(view);
}
<ImageView
app:imageUrl=“@{url}”
app:error=“@{@drawable/ic_launcher}”/>
就是一个很好的示例,当 imageUrl 与 error 存在时这个 adapter 会被调用。imageUrl 是一个 String,error 是一个 Drawable。(注意app:error
中一定要用@{}
,而非直接@drawable/xxxx
,因为这里需要的是一个包含@{}
的表达式,否则是无法得到预期效果的)
有关双向绑定的数据需要实现Observable。
也许会疑惑什么是双向绑定,但是在看了接下来的例子之后,就能直观的体会到了。
首先创建一个实体类TestBean,且需要实现Observable:
public class TestBean extends BaseObservable {
private String test;
@Bindable
public String getTest() {
return test;
}
public void setTest(String test) {
this.test = test;
notifyPropertyChanged(BR.test);//注意BR别import错了,否则是不会有test的
}
}
然后是布局文件:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.zero.myapplication.MainActivity">
<data>
<variable
name="mTest"
type="com.zero.myapplication.TestBean"/>
data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:layout_marginTop="10dp"
android:hint="This is a test!"
android:text="@={mTest.test}"
android:background="#dedede"
android:layout_width="match_parent"
android:layout_height="50dp" />
<TextView
android:text='@{"------->"+mTest.test}'
android:textColor="#ff0000"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="50dp" />
LinearLayout>
layout>
需要注意的是,设置EditText的android:text
属性时,使用的@={}
,而不是以前的@{}
。
最后就是在java代码中实现设置mTest变量了。
TestBean bean=new TestBean();
bean.setTest("Zero");
binding.setMTest(bean);
之后运行代码,在EditText中输入字符串时,TextView中的内容也在做着相应的变化,其根本原因就是mTest的test也在做着相应的变化。这就是所谓的双向绑定了,不仅仅是布局中的view绑定了实例对象的内容,实例对象的内容也和view绑定在一起,其中一者发生了变化,另外一者也相对变化,反之亦然。这样,在一些实际场景中可以节省代码,减轻工作量。
但是,在实现双向绑定时需要注意实体类实现Observable时只能通过继承BaseObservable(也就是通过注释@Bindable
)来实现,而不能直接将其成员变量设置为ObservableFields类型来实现。
回到上面的@={}
,这就是双向绑定得以实现的关键之一,不然可以试试,去掉=
号是什么样的反应?那样的话编辑EditText的内容,TextView中的内容是不会跟随其变化的,根本原=原因就是mTest的test没有发生改变。而有了=
之后,实际上在编辑EditText的时候,因为EditText与mTest的test做了绑定,就会调用test的set方法该同步test的内容(可以在set方法中打印log看看),而test又实现了Observable,所以TextView中的内容也会随之改变。这就是双向绑定实现的大概原因,而具体的实现原理,就不在这里赘述了。(需要注意,@={}
中只能是单一变量引用,不能使表达式或者常量等,如@={“——->”+mTest.test}是不行的,这些会在编译时导致错误)
说到这里,双向绑定的大致内容讲完了,不过不知道会不会有一个疑惑,就是双向绑定导致死循环?
假设有两个EditText,其android:text
都实现的@={mTest.test}
,那么在改变第一个EditText的内容时,会通过set方法设置test的内容,从而导致第二个EditText的text同步变化,但是由于该text也实现了@={mTest.test}
,又会去通过set方法设置test的内容,从而导致第一个EditText的text同步变化,最后进入一个死循环呢?当然,答案是否定的,其原因就在下图中:
在设置android:text
的内容时会做判断,如果与上次的内容一样,就会return,所以在第二个EditText的text通过set方法同步test的内容后,第一个EditText的text并不会因此而重新设置,从而阻止了死循环的发生。
还有,需要指出的是,双向绑定并不是支持所有属性的,暂时只支持那些带有额外事件的属性,比如text会带有TextChanged事件,checked会带有CheckedChange事件等。
~~~~~~~~~~~~~~~~~~~
最后,再指出一点,就是怎么实现监听属性的变更?比如在EditText的text发生变化时,实现一些额外的逻辑要求,但是这里实现了双向绑定之后,再去实现一个android:onTextChanged
就有点缀余了,那么该怎么变通呢?幸好在BaseObservable中有一个addOnPropertyChangedCallback()
方法,该方法的参数为一个Observable.OnPropertyChangedCallback,当属性发生变化时,就会回调该抽象类的onPropertyChanged(Observable observable, int i)
方法,因此可以在该方法中实现额外的逻辑。其中第一个参数observable
就是实现addOnPropertyChangedCallback()
方法的Observable对象,第二个参数i
则是对应的BR里面的int值。如果还是不理解我会在下面的例子中说明。
TestBean bean=new TestBean();
bean.setTest("Zero");
Log.d("测试1",""+bean.hashCode());
bean.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable observable, int i) {
Log.d("测试2",""+observable.hashCode());
Log.d("测试3",""+(i==com.android.databinding.library.baseAdapters.BR.test));
Toast.makeText(MainActivity.this, String.valueOf(i), Toast.LENGTH_SHORT).show();
}
});
//addOnPropertyChangedCallback需要在set Variable之前才能生效
binding.setMTest(bean);
上述代码是基于前面的来的。
其中打印的两个测试log1、2是为了验证bean对象其实就是onPropertyChanged中的observable对象(注意:TestBean继承自BaseObservable,而BaseObservable又继承自Observable);而i
则是bean对象的setTest方法中notifyPropertyChanged(BR.test);
的BR.test,通过测试log3打印的结果就可以验证了。另外还需要注意addOnPropertyChangedCallback需要在set Variable之前才能生效。
突然忍不住多想了一点:
TestBean bean=new TestBean();
bean.setTest("Zero");
bean.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable observable, int i) {
Log.d("测试1",observable.hashCode()+" i="+i); }
});
binding.setMTest(bean);
TestBean bean2=new TestBean();
bean2.setTest("Zero2");
bean2.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable observable, int i) {
Log.d("测试2",observable.hashCode()+" i="+i);
}
});
binding.setMTest2(bean2);
假设有两个TestBean对象做如上处理,会怎么样呢?
根据打印的日志得出结果:打印的observable.hashCode()
是不一样的,这时因为是两个不同的实例对象,而i
是一样的,因为都是BR.test
。
在开发中,有可能会出现如下图那样重复的表达式:
敲代码的时候可能会比较麻烦,相信大部分人都是直接copy的,但是有了Data Binding之后,就可以将上述表达式简化了:
在上图中,TextView和CkeckBox的visibility
属性都是跟随IamgeView的属性来动态变化的,而ImageView的visibility
属性则是根据user.isAdult实现动态绑定的,因此实现了跟第一张图中重复表达式的效果。而实现的关键,就是给ImageView设置id(avatar),然后在另外两个控件中利用表达式与ImageView的visibility
属性绑定在一起(@{avatar.visibility}
)。当然,也可以实现更加复杂的逻辑,如:
android:visibility="@{avatar.visibility==View.VISIBLE?View.INVISIBLE:View.VISIBLE}"
隐式更新实现起来跟简化表达式差不多。上图就是一个实例,ImageView的visibility
属性是跟随CheckBox的checked
属性绑定在一起的,当CheckBox的checked
属性变化时,ImageView的visibility
属性也会随之变化,而实现的前提也是献给CheckBox设置id。
最后,需要注意一点的是,在给控件设置id时,可能会加入下划线,如:my_avatar
,但是在引用的时候却不能直接使用my_avatar
了,否则不会通过编译,这时因为Data Binding会根据id名自动变成驼峰变量名,即myAvatar
,从而在表达式中引用。但是在findViewById的时候,还是R.id.my_avatar
。
在这一节的视频中,需要注意的就是可以在xml中可以直接使用Activity对应的context变量,从而在监听器绑定中将该context回传等。
看来这一节之后,突然很好奇,在RecyclerView的item的布局中能不能回传context?为了验证这一想法,进行了如下测试。
首先创建RecyclerView的item的布局如下:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="item"
type="com.zero.myapplication.TestBean"/>
<variable
name="presenter"
type="com.zero.myapplication.
MyAdapter.Presenter"/>
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="@{item.test}" />
<Button
android:onClick="@{()->
presenter.onItemClick(context)}"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text='@{"BUTTON->"+item.test}'/>
LinearLayout>
layout>
其中TestBean类为前文中的使用过的。
然后是自定义适配器:
public class MyAdapter
extends RecyclerView.Adapter<MyAdapter.BindingHolder> {
private Context mContext;
private List mData=new ArrayList<>();
public MyAdapter(Context mContext) {
this.mContext = mContext;
for (int i=0;i<30;i++) {
TestBean bean=new TestBean();
bean.setTest("BEAN "+i);
mData.add(bean);
}
}
@Override
public BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewDataBinding binding= DataBindingUtil.inflate(
LayoutInflater.from(mContext),R.layout.item_rv,parent,false
);
Log.d("测试1",
"parent.getContext()="+parent.getContext());
return new BindingHolder(binding);
}
@Override
public void onBindViewHolder(BindingHolder holder, int position) {
final TestBean bean=mData.get(position);
holder.binding.setVariable(BR.item,bean);
holder.binding.setVariable(BR.presenter,new Presenter());
holder.binding.executePendingBindings();
}
@Override
public int getItemCount() {
return mData.size();
}
public static class BindingHolder
extends RecyclerView.ViewHolder {
private final ViewDataBinding binding;
public BindingHolder(ViewDataBinding binding) {
super(binding.getRoot());
this.binding=binding;
}
}
public class Presenter {
public void onItemClick(Context contextFromXML) {
Log.d("测试2","contextFromXML="+contextFromXML);
Log.d("测试3","mContext="+mContext);
}
}
}
在上述代码中,有三条log输出,目的就是为了检验初始化适配器时从Activity传入的context
,与从item布局回传的context
以及onCreateViewHolder时通过参数parent.getContext()获得的context
是否都一致。
经过实践,答案是肯定的。以下就是打印的部分log:
D/测试1: parent.getContext()=com.zero.myapplication.Main3Activity@4890e94
D/测试2: contextFromXML=com.zero.myapplication.Main3Activity@4890e94
D/测试3: mContext=com.zero.myapplication.Main3Activity@4890e94
其实,初始化适配器时从Activity传入的context
和onCreateViewHolder时通过参数parent.getContext()获得的context
肯定会是一致的,因为参数parent就是初始化一个新的item布局时item对应的RecyclerView所在的Activity布局的根ViewGroup,而为什么是所在的Activity布局的根ViewGroup,以及从item布局回传的context
为什么也与上述两个context一致,就要探究其实现原理了,在下能力有限,暂时就不深入了。
在Data Binding中,可以使用 Transition (适用 API >= 19,系统 >= 4.4)来实现某些动画效果,但是这种动画只是简单的,实现一个简单的过渡效果,为了使某些场景不至于那么突兀。
演示效果如下:
在上述演示中,一个ImageView是跟随CheckBox状态的改变而VISIBLE或者GONE,而另外一个则是VISIBLE或者INVISIBLE。
涉及的代码如下:
//布局的代码
http: //schemas.android.com/apk/res/android">
<data>
<import type="android.view.View"/>
<variable
name="showImage1"
type="boolean"/>
<variable
name="showImage2"
type="boolean"/>
<variable
name="presenter"
type="com.zero.myapplication.Main2Activity.Presenter"/>
data>
<LinearLayout
android:id="@+id/mLayout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv1"
android:visibility="@{showImage1?View.VISIBLE:View.GONE}"
android:background="#f9acac"
android:layout_marginTop="10dp"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_launcher"
android:layout_width="150dp"
android:layout_height="150dp" />
<CheckBox
android:checked="true"
android:layout_marginTop="20dp"
android:layout_gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb,isChecked)
->presenter.onCheckedChanged(1,isChecked)}"/>
<ImageView
android:id="@+id/iv2"
android:visibility="@{showImage2?View.VISIBLE:View.INVISIBLE}"
android:background="#f9acac"
android:layout_marginTop="10dp"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_launcher"
android:layout_width="150dp"
android:layout_height="150dp" />
<CheckBox
android:checked="true"
android:layout_marginTop="20dp"
android:layout_gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb,isChecked)
->presenter.onCheckedChanged(2,isChecked)}"/>
LinearLayout>
layout>
//java代码
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setShowImage1(true);
binding.setShowImage2(true);
binding.addOnRebindCallback(new OnRebindCallback() {
@Override
public boolean onPreBind(ViewDataBinding binding) {
ViewGroup viewGroup = (ViewGroup) binding.getRoot();
//作用于整个布局,因为这里得到的viewGroup就是根布局
TransitionManager.beginDelayedTransition(viewGroup);
return true;
//如果返回false,发生绑定的反应则不会发生(ImageView不会INVISIBLE或者GONE),
//除非显示调用ViewDataBinding.executePendingBindings()
/**
* Return true to allow the reevaluation to happen or false
* if the reevaluation should be stopped.
* If false is returned, it is the responsibility of
* the OnRebindListener implementer to explicitly
* call ViewDataBinding.executePendingBindings().
*/
}
});
binding.setPresenter(new Presenter());
}
public class Presenter {
public void onCheckedChanged(int which, boolean isChecked) {
switch (which) {
case 1:binding.setShowImage1(isChecked);break;
case 2:binding.setShowImage2(isChecked);break;
default:break;
}
}
}
}
下面这一段引用自:安卓 Data Binding 使用方法总结(妹妹篇)
但是这种方法对某些情况是失效的,如随着滚轮的滑动 TextView 的内容发生改变:
更具普遍性的方法是在 @BindingAdapter 修饰的方法中进行设置:
@BindingAdapter("adText")
public static animateTextChanges(TextView textView, String oldText, String newText) {
if (oldText == null || oldText.equals(newText)) {
return;
}
animateTextChange(textView, oldText, newText);
}