注意:本篇笔记基于官方文档生成的绑定类和绑定适配器这两部分,点击这里查看原文
另外需要注意的是,在绑定适配器部分,如设置View的新旧Padding替换,事件处理程序和对象转换,由于我没有运行出效果,所以没有写上去。
生成的绑定类
数据绑定库可以生成用于访问布局的变量和视图的绑定类。生成的绑定类将布局变量与布局中的视图关联起来。绑定类的名称和包可以自定义。所有生成的绑定类都是从ViewDataBinding类继承而来的。
系统会为每个布局文件生成一个绑定类。默认情况下,类名称基于布局文件的名称,它会转换为Pascal大小写形式并在末尾添加Binding后缀。如布局文件的名称为activity_main.xml,则生成的绑定类的名称为ActivityMainBinding。此类包含从布局属性(如User变量)到布局视图的所有绑定,并且知道如何为绑定表达式指定值。
创建绑定对象
在对布局进行扩充后,应尽快创建绑定对象,以确保视图层次结构在通过表达式与布局内的视图绑定之前不会被修改。将对象绑定到布局主要有以下四种方式:
首先创建一个layout_test.xml布局文件
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.project.databinding_moudle.data.User"
/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<TextView
android:id="@+id/tv_user_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/darker_gray"
android:padding="20dp"
android:gravity="center"
android:text="@{user.name}"
android:textColor="@android:color/black"
/>
</LinearLayout>
</layout>
- 第一种方式,通过
DataBindingUtil.inflate方式:
val binding = DataBindingUtil.inflate<LayoutTestBinding>(LayoutInflater.from(this),R.layout.layout_test,null,false)
- 第二种方式,通过
DataBindingUtil.setContentView方式(不过这种方式有一个缺陷,传递的第一个参数需要是Activity):
DataBindingUtil.setContentView<LayoutTestBinding>(this,R.layout.layout_test)
- 第三种方式,使用通过布局文件生成的绑定类中提供的方法,如上面的生成的
LayoutTestBinding.inflate:
LayoutTestBinding.inflate(LayoutInflater.from(this))
- 第四种方式,通过
LayoutTestBinding.inflate重载方法:
LayoutTestBinding.inflate(LayoutInflater.from(this),null,false)
使用以上四种方式最终都会得到一个View或者LayoutTestBinding对象,通过这个对象我们就可以对布局文件进行操作。
另外,在得到一个View对象或者布局来自于其它View的时候,也可以通过bind()方式获得DataBinding对象,LayoutTestBinding和DataBindingUtil都有提供这个方法:
val view: View = LayoutInflater.from(this).inflate(R.layout.layout_test,null,false)
DataBindingUtil.bind<LayoutTestBinding>(view)
或者
val view: View = LayoutInflater.from(this).inflate(R.layout.layout_test,null,false)
LayoutTestBinding.bind(view)
总体来说,我们就是可以通过以上几种方式得到布局文件最终生成的数据绑定类。我们在布局中定义的相关操作都会在这个类中进行实现。
带ID的视图
数据绑定库会针对布局中带有id的每个视图在绑定类中创建不可变字段,例如在上面的layout_test.xml布局文件中,就会在数据绑定类中创建出一个不可变的TextView,如:
@NonNull
public final TextView tvUserName;
可以看到,TextView通过final修饰的,同时,名字为View的简写加上id。
同时,该库一次性从视图层次结构中提取包含id的视图,相较于针对布局中的每个视图调用findViewById()方法,这种机制更快。
变量
数据绑定库为布局中声明的每个变量生成访问器方法。例如,在上面的布局中,会为变量user生成访问器方法:
@Bindable
protected User mUser;
public abstract void setUser(@Nullable User user);
@Nullable
public User getUser() {
return mUser;
}
ViewStub
和普通视图不同,ViewStub对象初始是一个不可见的视图,当它们完全显示出来或者获得明确指示进行扩充时,它们会通过扩充另一个布局在布局中完成自我替代。
由于ViewStub实际上会从视图层次中消失,因此绑定对象中的视图也必须消失,才能通过垃圾回收进行回收。由于视图是最终结果,因此ViewStubProxxy对象将取代生成的绑定类中的ViewStub,这样就能够访问ViewStub和ViewStub进行扩充后的扩充版视图层次结构。
在扩充其它布局时,必须为新布局建立绑定。因此,ViewStubProxy必须监听ViewStub.OnInflate;istener并在必要时建立绑定。由于在给定时间只能有一个监听器,因此ViewStubProxy允许设置OnInflateListener,它将在建立绑定后调用这个监听器。
比如我们在之前的activity_databinding_test2.xml中添加一个ViewStub:
<ViewStub
android:id="@+id/vs_test"
style="@style/match_width"
android:layout_marginTop="20dp"
android:layout="@layout/layout_test"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_content4" />
首先要明确的是,这个ViewStub在加载出来之后就会消失,因此在生成的数据绑定文件中保存并不是ViewStub对象,而是ViewStubProxy对象:
@NonNull
public final ViewStubProxy vsTest;
我们可以对这个对象添加OnInflateListener监听器,监听器回调的方法中包含ViewStub对象和它所引用的View对象,这样,当这个ViewStub被加载之后,我们可以通过它的这两个对象获取到它所引用的布局的DataBinding对象,比如上面引用的是layout_test这个布局文件,那么当监听器生效后我们就可以获得LayoutTestBinding这个数据绑定对象:
private lateinit var mLayoutTestBinding: LayoutTestBinding
//设置监听器
mBinding.vsTest.setOnInflateListener{
stub,view ->
mLayoutTestBinding = DataBindingUtil.bind<LayoutTestBinding>(view)!!
mLayoutTestBinding.user = User("赵六",20,"2324")
}
//在合适的时机加载ViewStub
if(!mBinding.vsTest.isInflated)
mBinding.vsTest.viewStub?.inflate()
即时绑定
当可变或可观察对象发生更改时,绑定会按照计划在下一帧之前发生更改,但有时必须要立即执行绑定,要强制执行绑定,需要调用executePendingBindings()方法。
高级绑定
有时,系统并不知道特定的绑定类。例如,针对任意布局运行的RecyclerView.Adapter不知道特定绑定类,但是在调用onBindViewHolder()时,仍必须指定绑定类。
在下面的示例中,RecyclerView绑定到的所有布局都有item变量,BindingHolder对象具有一个getBinding()方法,这个方法会返回ViewBinding的基类:
override fun onBindViewHolder(holder: BindingHolder, position: Int) {
item: T = items.get(position)
holder.binding.setVariable(BR.item, item);
holder.binding.executePendingBindings();
}
自定义绑定类名称
默认情况下,绑定类是根据布局文件的名称生成的,以大写字母开头,移除下划线"_",将后一个字母大写,最后添加后缀Binding。该类位于模块包下的databinding包中。
通过调整<data>元素的class特性,绑定类可重命名或放置在不同的包中。例如,以下布局在当前模块的dataBinding包中生成ContactItem绑定类:
<data class="ContactItem">
…
</data>
同时,可以在类名前添加句点和前缀,从而在其它文件包中生成绑定类,以下示例在模块包中生成绑定类:
<data class=".ContactItem">
…
</data>
还可以使用完整软件包名称来生成绑定类,以下示例在com.example包创建ContactItem绑定类:
<data class="com.example.ContactItem">
…
</data>
绑定适配器
绑定适配器负责发出相应的框架调用来设置值。例如,设置属性值就像调用setText()方法一样,设置事件监听器就像调用setOnClickListener()方法一样。
数据绑定库允许通过使用适配器指定为设置值而调用的方法,提供绑定逻辑和指定返回对象类型。
设置特性值
只要绑定值发生更改,生成的绑定类就必须使用绑定表达式在视图上调用setter方法。可以允许数据绑定库自动确定方法,显式声明方法或提供选择方法的自定义逻辑。
自动选择方法
对于名为example的特性,库会自动查找接受兼容类型作为参数的方法setExample(arg)。系统不会考虑特性的命名空间,搜索方法时仅使用特性名称和类型。
以android:text="@{user.name}"表达式为例,库会查找接受user.getName()所返回类型的setText(arg)方法。如果user.getName()的返回类型为String,则库会查找接受String参数的setText(String arg)方法。如果表达式返回的是int类型,则库会搜索接受int参数的setText(int arg)方法。表达式必须返回正确的类型,可以根据需要强制转换返回值的类型。
即使不存在具有给定名称的特性,数据绑定也会起作用,然后,就可以使用数据绑定为任何setter创建新特性。例如:DrawerLayout没有任何特性,但有很多setter,以下布局会自动将setScrimColor(int)和setDrawerListener(DrawerListener)方法分别用作app:scrimColor和app:drawerListener特性的setter。其实就一句话:只要所在的类中包含相应属性的setter,就可以在布局文件中指定相应的值。
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}">
指定自定义方法名称
某些特性拥有与名称不符的setter,在这些情况下,某个特性可能会使用BindingMethods注释与setter相关联。注释与类一起使用,可以包含多个BindingMethod注释,每个注释对应一个重命名的方法。绑定方法是可添加到应用中任何类的注释。在下面的例子中,android:toast特性与showTextToast()相关联,而不与setToast()方法相关联。
@BindingMethods(value = [
BindingMethod(
type = TextView::class,
attribute = "toast",
method = "showTextToast"
)
])
class ToastTextView : androidx.appcompat.widget.AppCompatTextView {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
fun showTextToast(text: String){
Toast.makeText(context, text,Toast.LENGTH_SHORT).show()
}
}
在布局文件中使用这个ToastTextView:
<com.project.databinding_moudle.widget.ToastTextView
style="@style/match_width"
app:layout_constraintTop_toBottomOf="@id/et_info"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:padding="10dp"
android:layout_marginTop="10dp"
android:text="@{student.name}"
app:toast="@{student.name}"
/>
运行程序之后,每次修改Student的name属性,都会弹出一个Toast。
不过,大多数情况下,都无需在Android框架类中重命名setter,特性已经使用命名惯例实现,可自动查找匹配的方法。
提供自定义逻辑
某些特性需要自定义绑定逻辑,例如: android:paddingLeft特性没有关联的setter,而是提供了setPadding(left,top,right,bottom)方法。使用上面的自定义方法名称虽然也可以实现这个需求,但是需要继承一个View。这个时候,使用BindingAdapter则是比较好的解决方案,它注释的静态绑定适配器方法支持自定义特性setter的调用方式。
Android框架类的特性已经创建了BindingAdapter的注释,例如,下面的例子中展示了PaddingLeft特性的绑定适配器:
@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom())
}
上面的例子是Android框架类已经设置好了的,我们也可以设置自定义的BindingAdapter,例如下面定义左边的Margin:
companion object {
@JvmStatic
@BindingAdapter(value = ["setMarginLeft"])
fun setMarginLeft(view: View, marginLeft: Int) {
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
val params = view.layoutParams as ViewGroup.MarginLayoutParams
params.leftMargin = marginLeft
view.layoutParams = params
}
}
}
注意:由于BindingAdapter注释的方法需要使用静态方法,所以在这里需要将方法定义在companion object中,同时添加@JvmStatic注释。
定义好了上面的方法后就可以在布局文件中使用:
<TextView
style="@style/match_width"
app:layout_constraintTop_toBottomOf="@id/tv_user_name"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginTop="10dp"
android:text="@{student.name}"
android:background="@android:color/black"
setMarginLeft="@{200}"
/>
这样设置完成之后运行程序就可以看到TextView左边的Margin了。
另外一个例子是,当我们对一个ImageView加载网络图片的时候,我们可能需要设置出错和加载中的图片,往常我们可能会通过在Activity/Fragment中去设置,比较麻烦,而且每一个ImageView都需要设置一边,也比较繁琐,使用自定义适配器则可以很容易解决这个问题。
- 首先我们定义一个自定义适配器方法,里面包含两个参数,一个是图片加载中显示的图片,另一个是图片加载出错之后设置的图片,如下所示:
@JvmStatic
@BindingAdapter(value = ["url","error","placeHolder"])
fun loadImage(view: ImageView,url: String,error: Drawable,placeHolder: Drawable){
Glide.with(view.context)
.asBitmap()
.load(url)
.error(error)
.placeholder(placeHolder)
.into(view)
}
- 在
ImageView中设置上面的属性:
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintTop_toBottomOf="@id/tv_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginTop="10dp"
app:url = "@{user.head}"
app:placeHolder="@{@drawable/ic_launcher}"
app:error="@{@drawable/ic_launcher}"
/>
- 在
Activity中设置User信息之后就可以加载网络图片了:
val user: User = User("姓名",10,"21233","https://tse1-mm.cn.bing.net/th?id=OIP.BX8LJipOhUSQQx8GCCplWQHaIM&w=107&h=110&c=8&rs=1&qlt=90&dpr=1.5&pid=3.1&rm=2")
mBinding.user= user
这样,我们定义了String类型的url,然后定义了Drawable类型的error和placeHolder,如果在ImageView中同时设置了这三个属性,则会调用这个适配器。如果希望在设置任意特性时都会调用适配器,则可以将适配器的可选项requireAll标记设置为false,如:
@BindingAdapter(value = ["url","error","placeHolder"],requireAll = false)
这个标记的作用在于,如果我们设置了requireAll = false,那么我们就可以不指定某些参数,比如我们并不是所有的ImageView都想指定error,那么这个时候就可以在布局文件中不指定error的参数。同时需要注意的是,在Kotlin中我们需要对相应的方法的参数类型设置为可空类型,否则会导致空指针出错。
当出现冲突的时候,绑定器会替换默认的数据绑定适配器。
绑定适配器方法可以选择性在处理程序中使用旧值,同时采用旧值和新值的方法应该先为特性声明所有旧值,然后再声明新值:
@JvmStatic
@BindingAdapter(value = ["android:paddingLeft"])
fun setPaddingLeft(view: View,oldPadding: Int,newPadding: Int){
if(oldPadding != newPadding){
view.setPadding(newPadding,
view.paddingTop,
view.paddingRight,
view.paddingBottom
)
}
}
上面的代码是我根据官方文档的代码写的,但是并不知道如何使用,因为在View中定义的android:paddingLeft并不会进入到这个方法内部处理,而且我不清楚这里有什么用。