Android架构之DataBinding(二)
通过前面的学习,相信大家对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属性所编写的代码:
在来看DataBinding针对android.text属性所编写的代码:
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");
}
结果如下:
图片被顺利加载出来了。
方法重载
刚才我们已经自定义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便能够显示项目资源文件中的图片了。
多参数重载
我们可以上面的两个方法,合并成一个方法,并且将两个参数同时传入方法中。当网络图片地址为空时,则显示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("美女是");
}
}
对象转换
自动转换对象
当绑定表达式返回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}"/>
需要注意的是,@BindingConversion会将布局文件中所有以@{String}方式引用到的String变量加上后缀"小鑫大美女錒",但是分以下两种情况:
- 如果属性是Android自带的属性。 比如说android:text , @BindingAdapter的优先级低于@BindingConversion,会优先调用@BindingConversion注解的方法
- 如果属性是自定义的。 比如我们上面列子中的图片加载,image别名,这时候@BindingAdapter的优先级会高于@BindingConversion,会优先调用@BindingAdapter注解的方法。
大家需知道@BindingAdapter和@BindingConversion的区别,特定的场景使用特定的注解。
双向绑定
在前面我们所讲的都属于单向绑定。例如,TextView的android:text与book对象的title字段之间的绑定,就是一种单向绑定。绑定后,当title字段发生变化时,TextView会更新相应的内容。
TextView是一个纯粹用于展示的控件,不需要与用户交互。对于其他一些能与用户产生交互的控件,例如EditText, 不仅可以实现随着字段的变化自动更新控件中的内容,还可以实现当用户修改EditText中的内容,对应的字段也会随着更新。
实现双向绑定
假设我们有这样一个需求: 当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()方法被自动调用。
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));
}
运行程序,效果如下:
好了,到这里DataBinding就结束了,想更深入了解的,可以去官网看看DataBinding的用法。 不足之处,欢迎大家留言,共同进步,谢谢大家。