欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android架构之DataBinding(二)

程序员文章站 2022-05-28 13:27:58
...

通过前面的学习,相信大家对DataBinding有了一个初步的认识。接下来让我们去深入的学习下DataBinding。比如说下面这行代码

<TextView
     android:id="@+id/tvAuthor"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:text="@{book.author}"
     android:padding ="@{book.padding}"
     android:textSize="25sp" />

DataBinding是如何将book类中的属性设置到text和padding上面的呢?是通过一个叫做@BindingAdapter注解,接下来要我们学习一下BindingAdapter把。

BindingAdapter

一旦我们在gradle中启用DataBinding库,它会为我们生成绑定所需要的各种类。这其中包含大量针对UI控件的、名为XXXBindingAdapter类。在这些类中包含各种静态方法,并且在这些静态方法前都有@BindingAdapter注解,标签中的别名对应于UI控件在布局文件中的属性。

接下来让我们看一看DataBinding针对android:padding属性所编写的代码:
Android架构之DataBinding(二)

在来看DataBinding针对android.text属性所编写的代码:
Android架构之DataBinding(二)
DataBinding库以静态方法的形式为UI控件的各个属性绑定了相应的代码。若我们在该UI属性中使用了布局表达式,那么当布局文件被渲染的时候,属性所绑定的静态方法会被自动调用。比如:

<TextView
     android:id="@+id/tvAuthor"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:text="@{book.author}"
     android:padding ="@{book.padding}"
     android:textSize="25sp" />

上例代码中,当TextView控件被渲染时,android.padding和android.text属性会自动调用ViewBindingAdapter.setPadding()方法TextViewBindingAdapter.setText()方法,将book属性的值设置到控件上面。现在我们知道了BindingApater的作用,那么可以自定义BindingAdapter,扩展其功能。

自定义BindingAdapter

比如有这样一个需求,我们经常会使用ImageView加载来自网络的图片,若加载失败,则让它显示一张默认图片。我们可以使用BindingAdapter来实现这个需求。

准备工作

我们采用Glide库来加载网络图片,在app的build.gradle文件中添加Glide库相关的依赖。

annotationProcessor 'androidx.annotation:annotation:1.1.0'
implementation 'com.github.bumptech.glide:glide:4.9.0'

图片加载需要访问网络,在Manifest文件中加入访问网络的权限

<uses-permission android:name="android.permission.INTERNET"/>

编写处理图片的BindingAdapter类

在该方法中通过Glide加载网络图片。

public class ImageViewBindingAdapter {
    @BindingAdapter("image")
    public static  void setImage(ImageView image,String imgUrl){
        if(!TextUtils.isEmpty(imgUrl)){
            //加载图片显示
            Glide.with(image)
                    .load(imgUrl)
                    .placeholder(R.drawable.ic_launcher_foreground)
                    .into(image);
        }
    }
}

需要注意的是: ImageViewBindingAdapter中的方法均为静态方法。第1个参数为调用者本身(也就是当前的UI控件),即ImageView; 第2个参数是布局文件在调用该方法时传递过来的参数。 我们在@BindingAdapter中添加了一个别名image,布局文件正是通过别名来调用该方法的。

在布局文件中调用BindingAdapter

1.首先,需要在布局变量中定义一个String,用于存放网络图片的地址。

	<data>
        <variable
            name="networkImage"
            type="String"/>
    </data>

2.接着,在ImageView中通过别名,即我们在ImageViewBindingAdapter文件中定义好的别名image,来调用静态方法。布局表达式@{}中的参数,则是调用方法时传入的参数

<ImageView
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     app:image="@{networkImage}"
     />

需要注意的是,我们需要在布局文件最外层包含以下命名空间,才能调用自定义@BindingAdapter标签定义的静态方法。

 xmlns:app="http://schemas.android.com/apk/res-auto"

4.在Activity中为布局变量赋值,也就是networkImage

public class DataBindingActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityDataBindingBinding activityDataBindingBinding = DataBindingUtil.setContentView(this, R.layout.activity_data_binding);
        activityDataBindingBinding.setNetworkImage("https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=3363295869,2467511306&fm=26&gp=0.jpg");
    }

结果如下:
Android架构之DataBinding(二)
图片被顺利加载出来了。

方法重载

刚才我们已经自定义BindingAdapter类,让UI控件能够通过简单的属性设置,来加载网络图片。 现在。我们可以通过方法重载,让该静态方法支持显示项目资源文件中的图片,代码如下:

@BindingAdapter("image")
    public static  void setImage(ImageView image,int imageResoure){
        image.setImageResource(imageResoure);
    }

布局文件代码:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="localImage"
            type="int"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center"
        tools:context=".DataBindingActivity">
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:image="@{localImage}"
            />
    </LinearLayout>
</layout>

Activity代码:

public class DataBindingActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityDataBindingBinding activityDataBindingBinding = DataBindingUtil.setContentView(this, R.layout.activity_data_binding);
        activityDataBindingBinding.setLocalImage(R.drawable.ic_launcher_background);
    }
}

这样,BindingAdapter便能够显示项目资源文件中的图片了。

Android架构之DataBinding(二)

多参数重载

我们可以上面的两个方法,合并成一个方法,并且将两个参数同时传入方法中。当网络图片地址为空时,则显示imageResource参数所指定的图片.代码如下:

@BindingAdapter(value = {"image","defaultImageResource"},requireAll = false)
    public static  void setImage(ImageView image,String imgUrl,int imageResoure){
        if (!TextUtils.isEmpty(imgUrl)) {
            //加载图片显示
            Glide.with(image)
                    .load(imgUrl)
                    .placeholder(R.drawable.ic_launcher_foreground)
                    .into(image);
        }else{
            image.setImageResource(imageResoure);
        }
    }

在@BindingAdapter标签中,方法的别名设置以value={"",""}的形式设置,在该方法中,我设置了两个别名,即通过这两个别名都可以调用该静态方法,变量requireAll用于告诉DataBinding库这些参数是否都要赋值,默认为true,即全部要赋值。 布局文件代码如下:

<ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:image="@{networkImage}"
        app:defaultImageResource="@{localImage}"
            />

当networkImage为空时,ImageVIew会显示locallmage所指定的图片。看到这里,大家应该都有一个疑问,方法是如何区分传入的资源是网络资源还是本地资源呢? DataBinding是根据传递参数的类型来进行区分。比如说:
networkImage是网络图片资源,那么就是String类型,方法就知道它传入的是网络资源图片,localImage是本地类型资源,那么就是int类型,方法就知道它传入的是本地资源图片。

可选旧值

BindingAdapter还有一个强大的功能,覆盖Android原先的控件属性。 比如说, 可以设置在每一个TextView的文本都加上后缀: “-小鑫”。代码如下:

 @BindingAdapter("android:text")
    public  static  void setText(TextView textView,String text){
        textView.setText(text+"-小鑫");
    }
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="networkImage"
            type="String"/>

        <variable
            name="localImage"
            type="int"/>
        <variable
            name="text"
            type="String" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center"
        tools:context=".DataBindingActivity">


        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:image="@{networkImage}"
            app:defaultImageResource="@{localImage}"
            />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="25sp"
            android:text="@{text}"/>
    </LinearLayout>
</layout>
public class DataBindingActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityDataBindingBinding activityDataBindingBinding = DataBindingUtil.setContentView(this, R.layout.activity_data_binding);
        activityDataBindingBinding.setLocalImage(R.drawable.ic_launcher_background);
        activityDataBindingBinding.setText("美女是");
    }
}

Android架构之DataBinding(二)

对象转换

自动转换对象

当绑定表达式返回Object时,库会选择用于设置属性值的方法。Object会自动转换为所选方法的参数类型。比如下面代码:

<TextView
       android:text='@{userMap["lastName"]}'
       android:layout_width="wrap_content"
       android:layout_height="wrap_content" />

注意: @{userMap[“lastName”]} 可替换为 @{userMap.lastName}。
表达式中的userMap对象会返回一个值,该值会自动转换为用于设置android.text特性值的setText(CharSequence)方法中的参数类型。如果参数类型不明确。则必须在表达式中强制转换返回类型。

自定义转换@BindingConversion

与 BindingAdapter 类似 以下方法会将布局文件中所有以@{String}方式引用到的String变量加上后缀"小鑫大美女錒"

 @BindingConversion
    public  static String convertText(String text){
        return text+"小鑫大美女錒";
    }

XML文件

<TextView
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textSize="25sp"
       android:text="@{text}"/>

Android架构之DataBinding(二)
需要注意的是,@BindingConversion会将布局文件中所有以@{String}方式引用到的String变量加上后缀"小鑫大美女錒",但是分以下两种情况:

  1. 如果属性是Android自带的属性。 比如说android:text , @BindingAdapter的优先级低于@BindingConversion,会优先调用@BindingConversion注解的方法
  2. 如果属性是自定义的。 比如我们上面列子中的图片加载,image别名,这时候@BindingAdapter的优先级会高于@BindingConversion,会优先调用@BindingAdapter注解的方法。

大家需知道@BindingAdapter和@BindingConversion的区别,特定的场景使用特定的注解。

双向绑定

在前面我们所讲的都属于单向绑定。例如,TextView的android:text与book对象的title字段之间的绑定,就是一种单向绑定。绑定后,当title字段发生变化时,TextView会更新相应的内容。
Android架构之DataBinding(二)

TextView是一个纯粹用于展示的控件,不需要与用户交互。对于其他一些能与用户产生交互的控件,例如EditText, 不仅可以实现随着字段的变化自动更新控件中的内容,还可以实现当用户修改EditText中的内容,对应的字段也会随着更新。
Android架构之DataBinding(二)

实现双向绑定

假设我们有这样一个需求: 当userName字段改变的时候,EditText会自动更新,EditText改变的时候,userName字段也会自动更新。

编写一个实现双向绑定的类

该类需要继承自BaseObservable,无论是单向绑定还是双向绑定都是观察者模式。BaseObservable是DataBinding库为了方便实现观察者模式而提供的类。

public class TwoWayBindingViewModel  extends BaseObservable {
    private  String username;

    public TwoWayBindingViewModel() {
        username="小鑫";
    }

    @Bindable
    public  String getUserName(){
        return  username;
    }

    public void  setUserName(String userName){
        if(!TextUtils.isEmpty(userName) && !userName.equals(username)){
            username = userName;
            //可以在此处理一些与业务相关的逻辑,例如保存userName字段
            notifyPropertyChanged(BR.userName);
        }
    }
}

userName字段就是我们实现双向绑定的数据流,为该字段写了Getter和Setter字段。需要注意的是:我们在Getter方法前加上了@Bindable标签,意思是告诉DataBinding,我们希望对这个字段进行双向绑定。而Setter方法会在用户编辑EditText中的内容时,会被自动调用,我们需要在该方法中对userName字段进行收到更新。

注意,在对字段更新之前,需要判断新值与旧值是否不同。因为在更新后,我们会调用notifyPropertyChanged()方法通知观察者,数据已经更新。观察者收到通知后,会对Setter方法进行调用。因此,没有对值进行判断,会引发循环调用的问题。

布局文件编写

EditText的布局表达式由@{}b变为@={}即可. @={} 表示(其中重要的是包含“=”符号)可接收属性的数据更改并同时监听用户更新。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="viewModel"
            type="com.example.jetpack.TwoWayBindingViewModel" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center"
        tools:context=".DataBindingActivity">


        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@={viewModel.userName}"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="25sp"
            android:text="@{viewModel.userName}"/>

    </LinearLayout>
</layout>

设置布局变量

public class DataBindingActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityDataBindingBinding activityDataBindingBinding = DataBindingUtil.setContentView(this, R.layout.activity_data_binding);
        activityDataBindingBinding.setViewModel(new TwoWayBindingViewModel());
    }
}

运行程序,效果就如上面所说,实现了双向绑定。

使用ObservableField优化双向绑定

实际上,上面的做法存储一些弊端,首先我们的类必须继承自BaseObservable,另外,在Getter方法前还需要加上@Bindable标签,告诉DataBinding我们要绑定该字段。最后,在Setter方法中手动调用notifyPropertyChanged()方法以通知观察者。

有一种更加简单的做法。那就是ObservableField,它能将普通对象包装成一个可观察对象。ObservableField可用于包装各种基本类型、集合数组、自定义类型。 实现代码如下:

{
    private ObservableField<String> userName;

    public TwoWayBindingViewModel() {
        userName = new ObservableField<>();
        userName.set("小鑫");
    }
    
    public  String getUserName(){
        Log.e("true",userName.get());
        return  userName.get();
    }

    public void  setUserName(String name){
        Log.e("true",name);
        userName.set(name);
    }
}

我们只通过ObservableField对象将字段给包装起来,并为字段编写了Getter和Setter方法。运行程序发现,getUserName()方法在程序启动时被自动调用,当用户修改EditText的内容时,setUserName()方法被自动调用。
Android架构之DataBinding(二)
DataBinding还提供了可观察集合 ObservableArrayMap对象和ObservableArrayList对象,这里就不再一一详解。步骤更上面使用差不多。

ObservableField与LiveData

我们发现ObservableField的使用方式和作用与LiveData很像。实际上,二者可以替换使用的,二者的区别在于,LiveData与生命周期相关,它通常在ViewModel中使用,并且需要在页面中通过observe()方法对变化进行监听。而双向绑定无须再页面中加入额外的代码,耦合度耕地。

DataBinding在RecycleView中的使用

添加依赖

在app的build.gradle文件添加RecyclerView的依赖

implementation 'androidx.recyclerview:recyclerview:1.1.0'

编写RecycleView的布局文件

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center"
        tools:context=".DataBindingActivity">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycleView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>


    </LinearLayout>
</layout>

编写Model类

Model类也就是我们的数据类,需要传递要页面的数据。

public class Book {
    public  String title;
    public  String author;
    public  String  imgUrl;

    public Book(String title, String author, String imgUrl) {
        this.title = title;
        this.author = author;
        this.imgUrl = imgUrl;
    }

    public Book() {
    }
}

定义加载图片的BindingAdapter

public class ImageViewBindingAdapter {
    @BindingAdapter("image")
    public static  void setImage(ImageView image,String imgUrl){
        if(!TextUtils.isEmpty(imgUrl)){
            //加载图片显示
            Glide.with(image)
                    .load(imgUrl)
                    .placeholder(R.drawable.ic_launcher_foreground)
                    .error(R.drawable.ic_launcher_foreground)
                    .into(image);
        }
    }
   }

编写RecycleView中item的子布局

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="book"
            type="com.example.jetpack.Book" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:context=".DataBindingActivity">

    <TextView
        android:layout_weight="1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@{book.title}"
        android:textSize="25sp"/>
    <ImageView
        android:layout_width="50dp"
        android:layout_height="50dp"
        app:image="@{book.imgUrl}"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="25sp"
        android:text="@{book.author}"/>


    </LinearLayout>
</layout>

编写RecyclerView.Adapter

public class recycleAdapter extends RecyclerView.Adapter<recycleAdapter.ViewHolder> {
    private List<Book> mList = new ArrayList<>();
    private  Context context;
    public recycleAdapter(Context context,List<Book> mList) {
        this.context = context;
        this.mList = mList;
    }

    @NonNull
    @Override
    public recycleAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        RecyItemBinding recyItemBinding = DataBindingUtil.inflate(LayoutInflater.from(context),R.layout.recy_item
        ,parent,false);
        ViewHolder viewHolder = new ViewHolder(recyItemBinding);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull recycleAdapter.ViewHolder holder, int position) {
            holder.binding.setBook(mList.get(position));
    }

    @Override
    public int getItemCount() {
        return mList.size();
    }
    class ViewHolder extends  RecyclerView.ViewHolder{
        public  RecyItemBinding binding;
        public ViewHolder(@NonNull RecyItemBinding itemView) {
            //itemView.getRoot返回的是UI最外层的布局视图
            super(itemView.getRoot());
            binding = itemView;
        }
    }
}

在Activity配置RecyclerView

public class DataBindingActivity extends AppCompatActivity {
    private List<Book> mList = new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityDataBindingBinding activityDataBindingBinding = DataBindingUtil.setContentView(this, R.layout.activity_data_binding);

        //模拟数据
        for (int i = 0; i < 10; i++) {
            Book book = new Book();
            book.author = "小鑫"+i;
            book.imgUrl = "https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=3363295869,2467511306&fm=26&gp=0.jpg";
            book.title = "小鑫大美女"+i;
            mList.add(book);
        }
        activityDataBindingBinding.recycleView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
        activityDataBindingBinding.recycleView.setLayoutManager(new LinearLayoutManager(this));
        activityDataBindingBinding.recycleView.setAdapter(new recycleAdapter(this,mList));
    }

}

添加点击事件

1.编写点击RecycleView中item的方法,点击弹出一个Toast

public class EventHandlerListener {
    private Context context;

    public EventHandlerListener(Context context) {
        this.context = context;
    }

	//将当前item中的书籍书籍传递过来了
    public  void onButtonClicked(Book book){
        Toast.makeText(context, book.author, Toast.LENGTH_SHORT).show();
    }
}

2.更改item的布局文件,引入EventHandlerListener类

<variable
        name="listener"
        type="com.example.jetpack.EventHandlerListener" />

3.为LinearLayout添加点击方法,将当前书籍内容传递过去。

<LinearLayout
        android:onClick="@{()->listener.onButtonClicked(book)}"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:context=".DataBindingActivity">

4 在adapter将EventHandlerListener类传递要布局中

@Override
    public void onBindViewHolder(@NonNull recycleAdapter.ViewHolder holder, int position) {
            holder.binding.setBook(mList.get(position));
            holder.binding.setListener(new EventHandlerListener(context));
    }

运行程序,效果如下:
Android架构之DataBinding(二)

好了,到这里DataBinding就结束了,想更深入了解的,可以去官网看看DataBinding的用法。 不足之处,欢迎大家留言,共同进步,谢谢大家。

相关标签: Android Jetpack