相关文章:
Jetpack:LifeCycle全面解析(包含检测app启动、进入前台、进入后台例子)。
Jetpack:ViewModel使用指南,实现原理详细解析!
Jetpack:LiveData使用指南,实现原理详细解析!
Jetpack:Room超详细使用踩坑指南!
Jetpack:Room数据库升级详解实战!
DataBinding可以让布局承担部分原本属于页面的工作,可以使得页面与布局文件之间的耦合程度降低。
DataBinding具有以下几点优势:
依赖:需要在模块的 build.gradle
文件中将 dataBinding
构建选项设置为 true
,如下所示:
android {
...
buildFeatures {
dataBinding true
}
}
//开启kapt
plugins {
...
id 'kotlin-kapt'
}
注意
可以同时使用viewBinding和dataBinding,布局里面使用layout标签,则生成ViewDataBinding类型,否则生成ViewBinding类型的绑定类。
根据简介
的描述在build.gradle
开启dataBinding
修改布局文件
在布局文件最外层添加
标签作为根节点。可以手动修改添加
标签;也可以将鼠标移动到原布局跟节点位置,单击浮现的灯泡按钮,然后选择Convert to databinding layout
选项,由编译器帮忙进行转化。如下图所示:
修改完成后的布局如下所示:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
实例化布局文件
常规的实例化布局文件方法通过Activity.this.setContentView()
方法实例布局文件,然后通过findViewById()
方法找到对应的UI控件并使用。使用DataBinding之后可以直接获取对应的Binding
类,然后将跟布局设置进布局文件即可;或者使用DataBindingUtil.setContentView()
方法直接设置布局文件,且该方法也会返回对应的Binding
类。
举个
private var _binding: ActivityDatabindingBaseBinding? = null
val binding: ActivityDatabindingBaseBinding
get() = _binding!!
//一、直接通过Binding获取Binding类,并且设置布局
private fun initBinding() {
_binding = ActivityDatabindingBaseBinding.inflate(layoutInflater)
setContentView(binding.root)
}
//二、使用DataBindingUtil直接设置布局
private fun initBinding() {
_binding = DataBindingUtil.setContentView(this, R.layout.activity_databinding_base)
}
通过对应的Binding
类可以直接方法,布局文件声明的Id类,会将原布局文件声明的id
命名,转换为驼峰命名法。
举个
//布局文件的id
<TextView
android:id="@+id/tv_video_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
//使用binding访问
binding.tvVideoName.text = "access ui controls through binding"
将数据传递到布局文件,由布局文件负责进行数据绑定
databinding
出现就希望减轻,UI曾对应数据绑定的逻辑,分层更加清晰。所以需要将数据传递到布局文件中,由布局文件负责数据的绑定。
首先声明一个实体类VideoEntity
data class VideoEntity(
val videoName: String? = "迪迦·奥特曼",
val videoIntroduction: String? = "《迪迦·奥特曼》(ウルトラマンティガ、Ultraman Tiga),是日本圆谷株式会社拍摄的特摄电视剧。是“平成系奥特曼”系列首作,平成三部曲的首作,是奥特曼系列自1980年的 《爱迪·奥特曼》后沉寂数年迎来的重生。于1996年(平成8年)9月7日至1997年(平成9年)8月30日在JNN日本新闻网播放 [1] ,共52话。",
val videoStarring: String? = "长野博;吉本多香美;高树零;增田由纪夫;影丸茂树;古屋畅一;川地民夫;石桥慧;二又一成",
val videoImageUrl: String? = "https://img1.baidu.com/it/u=2288494528,306759139&fm=253&fmt=auto&app=120&f=JPEG?w=584&h=378",
@DrawableRes val localImage: Int = R.drawable.ic_launcher_background
)
将实体类VideoEntity
传入到布局文件中,在
标签下创建标签(通过编译器直接转换databinding layout 的话,会自动生成
标签),在
标签下创建
标签,指定对象的路径以及对应的名称。
type
实体类的路径name
可自定义名称举个
<?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"
xmlns:tools="http://schemas.android.com/tools">
<data>
<!-- 实体类-->
<variable
name="videoEntity"
type="com.zxf.jetpackrelated.databinding.simpleUse.VideoEntity" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView />
<TextView />
<TextView />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
在activity中,DataBinding直接生成了对应的setter方法。所以可以直接使用setVideoEntity将对象传递给布局类。如下所示:
val videoEntity = VideoEntity()
//绑定实体类
binding.videoEntity = videoEntity
绑定布局变量
将对象传递进去直接,就可以通过@{}
直接在布局文件中将对应的变量绑定给控件了
<TextView
android:text="@{videoEntity.videoName}" />
<TextView
android:text="@{videoEntity.videoIntroduction}" />
<TextView
android:text="@{videoEntity.videoStarring}" />
在@{}
中也可以直接使用静态方法
<TextView
android:text="@{String.valueOf(1)}" />
通过Button控件,演示DataBinding如何响应onClick事件。
创建布局文件转为DataBinding布局
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<Button/>
<Button />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
声明一个EventHandleListener专门处理点击事件
class EventHandleListener(private val videoEntity:VideoEntity) {
fun onButtonClick1(view: View) {
Toast.makeText(context, "click1", Toast.LENGTH_SHORT).show()
}
fun onButtonClick2(view: View) {
Toast.makeText(context, "click2", Toast.LENGTH_SHORT).show()
}
}
布局中定义EventHandleListener变量,并将点击事件传递进button。通过双冒号语法::
进行调用
<variable
name="eventHandlerLayout"
type="com.zxf.jetpackrelated.databinding.simpleUse.SimpleDataBindingActivity.EventHandleListener"/>
<Button
android:onClick="@{eventHandlerLayout::onButtonClick1}"
/>
<Button
android:onClick="@{eventHandlerLayout::onButtonClick2}"
/>
activity中将EventHandleListener传递给布局文件
binding.eventHandler = EventHandleListener(videoEntity)
简单来说就是怎么将,数据传递到通过
标签包裹的二级页面呢?
下面就将上面的按钮响应事件通过
包裹一下,将EventHandleListener进行一个二级传递。
在一级页面定义
,将上面的按钮布局包裹进来
<include
android:id="@+id/in_bt"
layout="@layout/layout_databinding_base" />
在一级页面同样声明一下EventHandleListener变量,可以通过二级页面声明的EventHandleListener的name,将对象直接传递给二级页面
<variable
name="eventHandler"
type="com.zxf.jetpackrelated.databinding.simpleUse.SimpleDataBindingActivity.EventHandleListener" />
<include
android:id="@+id/in_bt"
layout="@layout/layout_databinding_base"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:eventHandlerLayout="@{eventHandler}"/>
当启用databinding
之后,会生成大量的类,其中包括针对UI的各种命名为XXXBindAdapter的类。
截取一下TextViewBindingAdapter类的部分源码。源码展示了DataBinding库针对android:text属性所编写的代码
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return; // No change in the spans, so don't set anything.
}
} else if (!haveContentsChanged(text, oldText)) {
return; // No content changes, so don't set anything.
}
view.setText(text);
}
在布局文件中通过@{}
绑定的text,最终通过生成的BindingImpl类,调用了对应的setText静态方法。
所以总的来说,在布局文件里面绑定的数据,可以调用到BindingAdapter修饰的静态方法。
仅官方提供的BindAdapter肯定是不够的,所以需要自定义BindAdapter,实现更多的更复杂的业务关系。
下面展示使用ImageView自定义BindAdapter。
BindAdapter注解介绍
@Target(ElementType.METHOD)
public @interface BindingAdapter {
String[] value();
boolean requireAll() default true;
}
value
即分配给布局文件中使用的名字requireAll
如果需要一下子定义多个value
,用于区别是否所有的都需要,默认为true,如果为true,则布局文件中,一旦使用了其中一个其他的也必须要声明,不然编译不通过。使用glide进行图片加载
添加依赖和网络访问权限
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
<uses-permission android:name="android.permission.INTERNET" />
定义ImageView的BindAdapter类
需要注意的是,BindingAdapter中的方法均为静态方法。第1个参数为调用者本身,即ImageView;第2个参数是布局文件在调用该方法时传递过来的参数
对于Kotlin
来说,只需要将函数声明为顶级函数,就对应java的静态方法了。
@BindingAdapter("image")
fun setImage(imageView: ImageView, imageUrl: String?) {
if (imageUrl.isNotNullOrEmpty()) {
Glide.with(imageView)
.load(imageUrl)
.into(imageView)
} else {
imageView.setBackgroundColor(Color.BLUE)
}
}
//isNotNullOrEmpty 是自定义的扩展函数
fun CharSequence?.isNotNullOrEmpty(): Boolean {
return !isNullOrEmpty()
}
布局文件中调用BindingAdapter类
方便期间,直接使用上面的VideoEntity类了,里面有对应的图片地址字段videoImageUrl
然后使用image
绑定布局
<ImageView
app:image="@{videoEntity.videoImageUrl}" />
BindAdapter方法重载
如果想网络图片加载url不存在则加载本地资源呢?多传递一个参数就好了,怎么传呢?举个
/**
* 方法的参数以value={"",""}的形式存在
* 变量requireAll用于告诉DataBinding库这些参数是否都要赋值,默认值为true,即全部需要赋值,这里写成false;
* 如果设置为true,则对应的属性必须在xml一起生命,否则编译报错
*/
@BindingAdapter(value = ["image", "defaultImageRes"], requireAll = false)
fun setImage2(imageView: ImageView, imageUrl: String?, @DrawableRes imageRes: Int) {
if (imageUrl.isNotNullOrEmpty()) {
Glide.with(imageView)
.load(imageUrl)
.into(imageView)
} else {
imageView.setImageResource(imageRes)
}
}
BindAdapter可选旧值
在某些情况下,可能希望在方法中得到该属性之前的值。假如,在修改控件的padding时,可能希望得到修改前的padding,以防止方法重复调用。
举个
/**、
* 需要注意的是,使用可选旧值时,方法中的参数顺序需要先写旧值,后写新值。即oldPadding在前,newPadding在后。
*/
@BindingAdapter(value = ["padding"])
fun setPadding(view: View, oldValue: Int, newValue: Int) {
LogUtil.d("------------paddingValueLog: oldValue:$oldValue newValue:$newValue")
if (oldValue != newValue) {
view.setPadding(newValue, newValue, newValue, newValue)
}
}
需要注意的是,使用可选旧值时,方法中的参数顺序需要先写旧值,后写新值。即oldPadding在前,newPadding在后。
绑定的代码就不写了,做了个例子,默认设置padding为30,点击时变为60,看一下控制台打印的数据
2021-10-12 11:10:06.690 12768-12768/com.zxf.jetpackrelated D/[setPadding(CustomBindingAdapterKt:74)]: ------------paddingValueLog: oldValue:0 newValue:30
2021-10-12 11:10:08.252 12768-12768/com.zxf.jetpackrelated D/[setPadding(CustomBindingAdapterKt:74)]: ------------paddingValueLog: oldValue:30 newValue:60
很明显看到,程序启动时,padding由0变为30。单击button后,程序由30变为60.
上面所有写的都只是简单的绑定,当数据变化后,目前是没有通知到UI进行刷新,同时UI刷新后,数据也没有变化。
下面通过EditText和TextView控件简单实现双向绑定。
使用BaseObservable实现双向绑定
编写实体类DisplayEntity
class DisplayEntity(
var displayStr: String
)
编写一个用于存放与实现双向绑定相关的业务逻辑的类,继承BaseObservable
class TwoBindingEntityObservable : BaseObservable() {
private val displayEntity: DisplayEntity = DisplayEntity("双向绑定测试")
/**
* 在getter 方法上面增加@Bindable 告诉编译器希望对这个字段进行双向绑定
* 在xml中直接使用displayStr 进行绑定
*/
@Bindable
fun getDisplayStr(): String = displayEntity.displayStr
/**
* setter 方法在用户编译edittext的时候被自动调用,需要在这里面对displayStr字段进行手动更新
* 调用notifyPropertyChanged方法通知观察者数据已变更
* 需要对值进行判断否则会产生循环调用的问题
*/
fun setDisplayStr(displayStr: String) {
//需要对值进行判断否则会产生循环调用的问题
if (displayStr.isNotNullOrEmpty() && displayStr == displayEntity.displayStr) {
return
}
displayEntity.displayStr = displayStr
notifyPropertyChanged(BR.displayStr)
}
}
初始化时为字段displayStr设置了默认值,接着为该字段写了Getter和Setter方法。注意,在Getter方法前加上了@Bindable标签,告诉编译器,希望对这个字段进行双向绑定。而Setter方法会在用户编辑EditText中的内容时,被自动调用,需要在该方法内对userName字段进行手动更新。
notifyPropertyChanged()是BaseObservable类中的一个方法,会通知UI进行刷新。
设置布局以及activity(注意::双向绑定使用@={}
)
<variable
name="entityObservable"
type="com.zxf.jetpackrelated.databinding.twoWayBinding.TwoBindingEntityObservable" />
<EditText
android:text="@={entityObservable.displayStr}"/>
<TextView
android:text="@={entityObservable.displayStr" />
binding.entityObservable = TwoBindingEntityObservable()
这样在编辑EditText的时候,对应的TextView也会展示对应的数据,实现了双向绑定
使用ObservabIeFieId更简单的实现双向绑定
创建ObservabIeFieId
class TwoBindingEntityObservable3 {
val displayEntityField = ObservableField("双向绑定测试-use observableField")
}
设置布局和activity
<variable
name="entityObservable3"
type="com.zxf.jetpackrelated.databinding.twoWayBinding.TwoBindingEntityObservable3" />
<EditText
android:text="@={entityObservable3.displayEntityField}"/>
<TextView
android:text="@={entityObservable3.displayEntityField" />
binding.entityObservable = TwoBindingEntityObservable3()
就可以了,是不是很简单!
databinding默认实现了一系列实现Observable接口的字段类型,可以选择使用
BaseObservable,
ObservableBoolean,
ObservableByte,
ObservableChar,
ObservableDouble,
ObservableField<T>,
ObservableFloat,
ObservableInt,
ObservableLong,
ObservableParcelable<T extends Parcelable>,
ObservableShort,
ViewDataBinding
同理可以自定义BindAdapter实现更加个性化的绑定(通知到UI),那么一样可以自定义当UI变化进行反绑(通知到数据)
这时候就用到了InverseBindingAdapter。
InverseBindingAdapter
解析
attribute
属性值(必填) 可以对应到BindingAdapter 的 value
event
非必填, 默认值 属性值 + AttrChanged
后缀简单解释一下怎么使用,InverseBindingAdapter修饰的静态方法,会在数据变更之后进行被回调,返回控件的数据,所以他对应一个@BindingAdapter
方法来实现event的属性. 用来通知数据什么时候变更。直接看实践代码吧,更加清晰一点。
创建一个SeekBar,实时显示进度到TextView
创建InverseBindingAdapter和BindingAdapter
@BindingAdapter(value = ["slideProgress"])
fun setSeekProgress(seekbar: SeekBar, slideProgressOld: Int, slideProgressNew: Int) {
if (slideProgressOld == slideProgressNew) {
return
}
seekbar.progress = slideProgressNew
}
/**
* InverseBindingAdapter 配合 BindingAdapter 一起使用
*
* event 非必填, 默认值 属性值 + AttrChanged后缀
*/
@InverseBindingAdapter(attribute = "slideProgress", event = "slideProgressAttrChanged")
fun getSeekProgress(seekbar: SeekBar) = seekbar.progress
@BindingAdapter(value = ["slideProgressAttrChanged"])
fun setSeekProgress(seekbar: SeekBar, inverseBindingListener: InverseBindingListener?) {
seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
inverseBindingListener?.onChange()
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
}
})
}
slideProgress
用于设置seekbar的进度InverseBindingAdapter
attribute 设置为 slideProgress
返回 seekbar的进度InverseBindingAdapter
的 BindingAdapter
, value 为 InverseBindingAdapter
的 event,第二个参数为 InverseBindingListener ,当数据发生之后,调用onChange()。创建可观察进度
val observableProgress = ObservableInt(50)
xml文件绑定
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="observableProgress"
type="androidx.databinding.ObservableInt" />
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<SeekBar
app:slideProgress="@={observableProgress}" />
<TextView
android:text="@{String.valueOf(observableProgress)}" />
LinearLayout>
layout>
设置数据
binding.observableProgress = observableProgress
上面的方法是不是有点蒙,简单分析一下都是怎么做的。看生成的BindingImpl文件
在布局绑定的时候会调用到executeBindings方法
@Override
protected void executeBindings() {
if ((dirtyFlags & 0x2L) != 0) {
//这里的自动绑定了 自定义的BindingAdapter 的 setSeekProgress 传进去了 mboundView1slideProgressAttrChanged
com.zxf.jetpackrelated.databinding.CustomInverseBindingAdapterKt.setSeekProgress(this.mboundView1, mboundView1slideProgressAttrChanged);
}
}
看一下 mboundView1slideProgressAttrChanged
private androidx.databinding.InverseBindingListener mboundView1slideProgressAttrChanged = new androidx.databinding.InverseBindingListener() {
@Override
public void onChange() {
// InverseBindingAdapter 的getSeekProgress
int callbackArg_0 = com.zxf.jetpackrelated.databinding.CustomInverseBindingAdapterKt.getSeekProgress(mboundView1);
boolean observableProgressJavaLangObjectNull = false;
// observableProgress
androidx.databinding.ObservableInt observableProgress = mObservableProgress;
observableProgressJavaLangObjectNull = (observableProgress) != (null);
if (observableProgressJavaLangObjectNull) {
//设置进 ObservableInt ObservableInt内部会观察 然后调用 notify 最后由进行ui绑定
observableProgress.set(((int) (callbackArg_0)));
}
}
};
所以当,onChange被调用的时候,调用了InverseBindingAdapter 修饰的 getSeekProgress。将获取的值传入进入ObservableInt,ObservableInt内部会观察 然后调用 notify 最后由进行ui绑定(还是调用到executeBindings方法)
常用的基本都在上面了,databinding不难,但是也需要手动去敲一遍,最好看一下生成的代码,为什么是这么写的,知道原理,用起来才会更加有底气!
文章定期更新,也会对已发的文章查缺补漏,有不正确的欢迎留言私信哈!