Android Data Binding实战-高级篇

来源:互联网 发布:网络文件夹中 编辑:程序博客网 时间:2024/06/01 08:01

在掌握了Data Binding的基础使用方法之后,来尝试一下相对高级的一点的使用方法。

慕客网对应的课程视频:Android Data Binding实战-高级篇

声明:博客只是个人写的,用于学习与交流,与慕课网平台和授课老师没有其他任何关系。如涉及版权问题,请联系本人,将马上改正。


1、在RecyclerView中进行绑定

首先,先看看主界面的布局:

<?xml version="1.0" encoding="utf-8"?><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 类的命名与位置。举个例子:<data class="CustomBinding">    ...</data>以上会在 databinding 包中生成名为 CustomBinding 的 binding 类。如果需要放置在不同的包下,可以在前面加 “.”:<data class=".CustomBinding">    ...</data>这样的话, CustomBinding 会直接生成在 module 包下。如果提供完整的包名,binding 类可以放置在任何包名中:<data class="com.example.CustomBinding">    ...</data>

接着是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<String> 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<String> getName() {        return name;    }}

之后还需要实现自定义的适配器:
这里写图片描述

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.BindingHolder> {    private List<User> 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());

2、有关自定义属性

我在这一节的视频在照着敲了之后,编译时会出现一个错误:

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,因为这里需要的是一个包含@{}的表达式,否则是无法得到预期效果的)


3、双向绑定

有关双向绑定的数据需要实现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


3、表达式链

(1)简化表达式

在开发中,有可能会出现如下图那样重复的表达式:
这里写图片描述
敲代码的时候可能会比较麻烦,相信大部分人都是直接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}"

(2)隐式更新

这里写图片描述
隐式更新实现起来跟简化表达式差不多。上图就是一个实例,ImageView的visibility属性是跟随CheckBox的checked属性绑定在一起的,当CheckBox的checked属性变化时,ImageView的visibility属性也会随之变化,而实现的前提也是献给CheckBox设置id。

最后,需要注意一点的是,在给控件设置id时,可能会加入下划线,如:my_avatar,但是在引用的时候却不能直接使用my_avatar了,否则不会通过编译,这时因为Data Binding会根据id名自动变成驼峰变量名,即myAvatar,从而在表达式中引用。但是在findViewById的时候,还是R.id.my_avatar


4、Lambda表达式

在这一节的视频中,需要注意的就是可以在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<TestBean> 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传入的contextonCreateViewHolder时通过参数parent.getContext()获得的context肯定会是一致的,因为参数parent就是初始化一个新的item布局时item对应的RecyclerView所在的Activity布局的根ViewGroup,而为什么是所在的Activity布局的根ViewGroup,以及从item布局回传的context为什么也与上述两个context一致,就要探究其实现原理了,在下能力有限,暂时就不深入了。


5、动画

在Data Binding中,可以使用 Transition (适用 API >= 19,系统 >= 4.4)来实现某些动画效果,但是这种动画只是简单的,实现一个简单的过渡效果,为了使某些场景不至于那么突兀。
演示效果如下:
这里写图片描述
在上述演示中,一个ImageView是跟随CheckBox状态的改变而VISIBLE或者GONE,而另外一个则是VISIBLE或者INVISIBLE。

涉及的代码如下:

//布局的代码<layout xmlns:android="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);}
1 0