一.DataBinding的意义和优势
我们知道,布局文件通常只负责UI控件的布局工作。页面通过setContentView()方法关联布局文件,再通过UI控件的id找到控件,接着在页面中通过代码对控件进行操作。可以说,页面承担了绝大部分的工作量,为了减轻页面的工作量,Google提出了DataBinding。DataBinding的出现让布局文件承担了部分原本属于页面的工作,也使页面和布局文件之间的耦合度进一步降低。DataBinding具有以下优势:
*项目更简洁,可读性更高。部分和UI控件相关的代码可以直接在布局文件中完成
*不再需要findViewById()方法了
*布局文件可以包含简单的业务逻辑,UI控件能够直接与数据模型中的字段绑定,甚至能响应用户的交互
二.DataBinding的简单绑定
假设有这样一个需求,在Activity中通过3个TextView控件,分别展示Book对象的三个字段,书名,作者和评分。下面采用DataBinding来实现:
1.在app/build.gradle中启用数据绑定:
android { dataBinding{ enabled=true } }
2.修改布局文件:
在布局文件的外层加入<layout>标签,并将命名空间移动到<layout>标签中,然后rebuild该项目,DataBinding会自动生成绑定该布局文件所需要的类,代码如下:
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <LinearLayout android:layout_height="match_parent" android:layout_width="match_parent" android:orientation="vertical"> <TextView android:id="@+id/tv_bookname" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{book.name}"/> <TextView android:id="@+id/tv_bookauthor" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{book.author}"/> <TextView android:id="@+id/tv_bookrate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{BookRating.ratingString(book.rate)}"/> </LinearLayout> </layout>
3.实例化布局文件
在没有DataBinding时,通常需要通过Activity.setContentView()方法实例化布局文件,接着通过findViewById()方法找到布局文件中对应的UI控件。有了DataBinding组件后,就可以告别findViewById()方法了,我们可以通过DataBindingUtil.setContentView()方法来实例化布局文件,该方法返回实例化后的布局文件对象,名字是布局文件的名字首字母大写然后在最后加上Binding。代码如下:
public class MainActivity extends AppCompatActivity { private ActivityMainBinding activityMainBinding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); activityMainBinding= DataBindingUtil.setContentView(this,R.layout.activity_main); activityMainBinding.setBook(new Book("西游记","罗贯中",5)); } }
4.将数据传递到布局文件
首先,在布局文件中定义布局变量<variable>,然后指定对象的类型和名字,名字可以自定义,需要注意的是,布局变量需要定义在<data>标签中,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.example.databinding.BookRating"/>
<variable name="book" type="com.example.databinding.Book"/> </data> <LinearLayout android:layout_height="match_parent" android:layout_width="match_parent" android:orientation="vertical"> <TextView android:id="@+id/tv_bookname" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{book.name}"/> <TextView android:id="@+id/tv_bookauthor" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{book.author}"/> <TextView android:id="@+id/tv_bookrate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{BookRating.ratingString(book.rate)}"/> </LinearLayout> </layout>
public class Book { private String name; private String author; private Integer rate; public Book(String name, String author, Integer rate) { this.name = name; this.author = author; this.rate = rate; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public Integer getRate() { return rate; } public void setRate(Integer rate) { this.rate = rate; } }
DataBinding为了方便我们使用,给布局变量提供了Setter方法,我们可以使用setBook()方法将Book对象传递给布局文件中对应的布局变量。
<data>标签用于存放布局文件中各个UI控件所需要的所有数据,这些数据类型可以是自定义类型,也可以是基本类型。
5.绑定UI控件和布局变量
android:text="@{book.name}"
6.在布局文件中引入静态类
有时,我们需要在布局文件中引入一些java工具类,帮助我们处理一些简单的逻辑,例如将阿拉伯数字转化为对应的中文分数。我们可以在布局文件中通过<import>标签导入静态工具类。
public class BookRating { public static String ratingString(int rate){ switch(rate){ case 0: return "零星"; case 1: return "一星"; case 2: return "二星"; case 3: return "三星"; case 4: return "四星"; case 5: return "五星"; } return "error"; } }
三.DataBinding响应事件
我们通过Button控件来演示DataBinding如何响应onClick事件。
1.编写一个名为EventHandleListener的类,用于接收和响应Button的onClick事件。代码如下:
public class EventHandleListener { private Context context; public EventHandleListener(Context context){ this.context=context; } public void onClick(View view){ Toast.makeText(context, "Button1 Clicked.", Toast.LENGTH_SHORT).show(); } }
2.在布局文件中的<data>标签中定义布局变量:
<variable name="EventHandleListener" type="com.example.databinding.EventHandleListener"/>
3.实例化EventHandleListener类,并传入布局文件:
activityMainBinding.setEventHandleListener(new EventHandleListener(this));
4.通过布局表达式,调用EventHandleListener中的方法:
android:onClick="@{EventHandleListener::onClick}"
那如果想要将布局变量book也传入onClick()方法,该怎么做呢?答案是使用lambda表达式,代码如下:
<Button android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="按钮2" android:onClick="@{()->EventHandleListener.onClick(book)}"/>
public void onClick(Book book){ Toast.makeText(context,book.toString(),Toast.LENGTH_SHORT).show(); }
实现按钮的点击事件还有一种方式,因为比较简单,下面直接看代码:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); activityMainBinding= DataBindingUtil.setContentView(this,R.layout.activity_main); activityMainBinding.button3.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Toast.makeText(MainActivity.this, "button3 clicked.", Toast.LENGTH_SHORT).show(); } }); }
四.二级页面的绑定
在这里,我们将Activity/Fragment直接对应的页面定义为一级页面,将在一级页面通过<include>标签进行引用的页面定义为二级页面。在一级页面中设置好布局变量book后,便可以直接接收来自页面的数据了,然后和UI控件进行绑定;不仅如此,布局变量book同时也是命名空间xmlns:app的一个属性。一级页面正是通过命名空间xmlns:app引用布局变量book,将数据对象传递给二级页面的,代码如下:
<?xml version="1.0" encoding="utf-8" ?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="book" type="com.example.databinding.Book" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/tv_bookname" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{book.name}"/> </LinearLayout> </layout>
这个是二级页面的代码,下面给出一级页面的代码:
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="book" type="com.example.databinding.Book"/> </data> <LinearLayout android:layout_height="match_parent" android:layout_width="match_parent" android:orientation="vertical"> <include
layout="@layout/second" app:book="@{book}"/> </LinearLayout> </layout>
需要注意的是,在二级页面中同样需要定义布局变量book。
五.自定义BindingAdapter
为了让布局文件能够承担更多的工作,处理更复杂的业务,DataBinding允许我们自定义BindingAdapter,下面我们以ImageView加载网络图片为例来进行说明。
1.导入依赖:implementation 'com.squareup.picasso:picasso:2.71828'
2.添加网络权限
3.编写处理图片的BindingAdapter类,需要注意的是,BindingAdapter中的方法都是静态方法,第一个参数是调用者本身,即ImageView,第二第三个参数是布局文件在调用该方法时传入的参数,代码如下:
public class ImageViewBindingAdapter { @BindingAdapter(value = {"image","defaultImage"},requireAll = false) public static void setImage(ImageView imageView,String imageUrl,int defaultImageResource){ if(!TextUtils.isEmpty(imageUrl)){ Picasso.get() .load(imageUrl) .placeholder(R.drawable.ic_launcher_background) .error(R.drawable.ic_launcher_foreground) .into(imageView); } else{ imageView.setImageResource(defaultImageResource); } } }
requireAll用于告诉DataBinding库这些参数是否都要赋值,默认值为true。
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="imageUrl" type="String"/> <variable name="defaultImageResource" type="int" /> </data> <LinearLayout android:layout_height="match_parent" android:layout_width="match_parent" android:orientation="vertical"> <ImageView android:id="@+id/iv_url" android:layout_width="wrap_content" android:layout_height="wrap_content" app:image="@{imageUrl}" app:defaultImage="@{defaultImageResource}"/> </LinearLayout> </layout>
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); activityMainBinding= DataBindingUtil.setContentView(this,R.layout.activity_main); activityMainBinding.setImageUrl("https://img.win3000.com/m00/d7/1a/d685398ab14eb827da7a883931cf38af.jpg"); activityMainBinding.setDefaultImageResource(R.drawable.accept); }
实现的效果就是当网络url为空的时候,就显示本地图片,否则就显示网络图片。
BindingAdapter中的方法有一个有趣的功能——可选旧值,什么意思呢?就是在某些情况下,我们可能想要得到某个属性的旧值,比如我们在修改padding的时候,想要得到修改前的padding值,以防止方法重复调用。下面给出代码:
@BindingAdapter("padding") public static void setPadding(ImageView imageView,int oldPadding,int newPadding){ Log.i("Padding",""+oldPadding+","+newPadding); if(oldPadding!=newPadding){ imageView.setPadding(newPadding,newPadding,newPadding,newPadding); } }
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="padding" type="int"/> </data> <LinearLayout android:layout_height="match_parent" android:layout_width="match_parent" android:orientation="vertical"> <ImageView android:id="@+id/iv_url" android:layout_width="wrap_content" android:layout_height="wrap_content" app:padding="@{padding}"/> </LinearLayout> </layout>
需要注意的是,在使用这个功能的时候,方法的参数顺序需要先写旧值,后写新值。