Android Data Binding实战-高级篇

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

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

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


1、在RecyclerView中进行绑定

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


<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;
    }
}

之后还需要实现自定义的适配器:
Android Data Binding实战-高级篇_第1张图片

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());

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 Data Binding实战-高级篇_第2张图片
在设置android:text的内容时会做判断,如果与上次的内容一样,就会return,所以在第二个EditText的text通过set方法同步test的内容后,第一个EditText的text并不会因此而重新设置,从而阻止了死循环的发生。

还有,需要指出的是,双向绑定并不是支持所有属性的,暂时只支持那些带有额外事件的属性,比如text会带有TextChanged事件,checked会带有CheckedChange事件等。
Android Data Binding实战-高级篇_第3张图片

~~~~~~~~~~~~~~~~~~~

最后,再指出一点,就是怎么实现监听属性的变更?比如在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)简化表达式

在开发中,有可能会出现如下图那样重复的表达式:
Android Data Binding实战-高级篇_第4张图片
敲代码的时候可能会比较麻烦,相信大部分人都是直接copy的,但是有了Data Binding之后,就可以将上述表达式简化了:
Android Data Binding实战-高级篇_第5张图片
在上图中,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)隐式更新

Android Data Binding实战-高级篇_第6张图片
隐式更新实现起来跟简化表达式差不多。上图就是一个实例,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 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。

涉及的代码如下:

//布局的代码
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 的内容发生改变:
Android Data Binding实战-高级篇_第7张图片
更具普遍性的方法是在 @BindingAdapter 修饰的方法中进行设置:

@BindingAdapter("adText")
public static animateTextChanges(TextView textView, String oldText, String newText) {
    if (oldText == null || oldText.equals(newText)) {
       return;
    }

    animateTextChange(textView, oldText, newText);
}

你可能感兴趣的:(Android Data Binding实战-高级篇)