ViewBinding(视图绑定) 的作用和原理一言以蔽之:
可以把 ViewBinding 看做 DataBinding 功能的 子集,它有的DataBinding都有,而且还多了 数据绑定。
何为数据绑定? 在维基百科中的定义如下:
是将 “提供器” 的数据源与 “消费者” 绑定并使其同步的一种通用技术。通常用两种不同语言的数据/信息源完成,如XML数据绑定。在UI数据绑定中,相同语言但不同逻辑功能的数据与信息对象被绑定在一起(例如Java UI元素到Java对象)。在数据绑定过程中,每个数据更改会由绑定到数据的元素自动反射。术语"数据绑定"也指一个外部数据表示随元素更改产生变化,并且底层数据自动更新以反映此更改。
又长又臭,举个简单例子就秒懂了:
一个存储数量的变量count,一个显示数量的TextView,两者绑定,当修改count的值时,TextView自动刷新。
数据源(Model)更新,绑定视图(View) 自动更新
,不用开发仔再去手动setXxx(),道理就这么简单。
这种玩法又叫 单向绑定
,还有一种 双向绑定
,绑定视图发生改变时,数据源也跟着改变,比如:
点击显示数量的TextView,显示的数量自增1,存储数量的变量也自增1。
互相影响,这就是双向绑定。咳…都是些浅显的概念,具体怎么做?
用 观察者模式 实现,数据变量与View实例关联,数据变量有更新时,遍历回调关联View实例对应设置值的方法。
自己造轮子,可以,但Duck不必~
Jetpack库中的 DataBinding组件
已经封装好一套了,要做的就是熟读文档,然后大胆使用~
API变化日新月异,建议以官方文档为准《数据绑定库》,本文也是基于此文档展开的学习。
通过一个超简单的例子来帮助大家了解DataBinding,先有基础认知,再往下学就容易多了。
DataBinding与AGP捆绑,无需声明这个库的依赖,在模块级别的 build.gradle
添加下述配置启用即可 ( 区分AS版本 )。
apply plugin: 'kotlin-kapt'
android {
...
// AS 4.0以下
dataBinding{
enabled true
}
// AS 4.0及以上
buildFeatures {
dataBinding true
}
// 还可以这样写
buildFeatures.dataBinding = true
}
未使用DataBinding之前,先写布局:
再写Activity:
class TestActivity : AppCompatActivity() {
private var mCount: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
findViewById
运行效果如下(点击按钮,计数器自增1):
司空见惯的常规操作,代码中主动setText()去更新TextView的文本,接着换成DataBinding试试看。
来到布局xml文件,鼠标点到 根布局
LinearLayout,按 Alt + Enter
,点击 Convert to data binding layout
,自动生成一波DataBinding所需的布局。
生成后的文件内容:
多了两个标签,接着开始改造,data标签中添加属性,修改TextView的android:text指向属性:
接着到Activity:
运行后,点击加1按钮,计数+1,效果与setText()一致,修改属性值,绑定的TextView文本跟着自动刷新。
看着 灰常简单!接着系统过一波详细用法,读者按需查阅即可~
先是必须遵守的铁律:
根结点必须为,只能存在一个和一个直接子View结点。
变量的 属性名name不能包含_下划线,否则再kt文件里会找不到变量,有时可能需要 指定自定义类型:
它有个属性class,可以自定义DataBinding生成的类名及路径 (一般不需要):
支持下述运算符和关键字:
+ - / * %
+
&& ||
& | ^
+ - ! ~
>> >>> <<
== > < >= <=
(请注意,< 需要转义为 <
;)instanceof
()
null
[]
?:
使用示例如下:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
不支持关键字及操作:this、super、new、显式泛型调用。
null合并运算符(??):如果左边不为Null,取左边,否则取右边,示例如下:
android:text="@{user.displayName ?? user.lastName}"
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
属性引用:表达式中可以引用类的属性,如:
android:text="@{user.lastName}"
空安全:DataBinding生成的代码会 自动检查null值并避免出现空指针异常。
如user为null,会为user.lastName分配默认null值,如果引用user.age,age为int,分配默认值0。
View引用:可以通过ID引用布局中其他的View,会将ID转换为 驼峰式大小写,示例如下:
集合:可以使用[]
运算符访问集合,如Array、List、Map等,示例如下:
android:text="@{list[index]}"
android:text="@{sparse[index]}"
android:text="@{map[key]}"
android:text="@{map.key}"
注:变量的元素类型type的值不能包含 ‘<’ 字符,直接 List
这样写会引起XML语法错误。需要对 ‘<’ 做下 转义,即 <
。
字符串
可以用 单引号(‘’) 包裹特征值,这样就可以在表达式中使用双引号了,示例如下:
android:text='@{map["firstName"]}'
可以用双引号扩住特征值,然后用 反单引号(``) 将字符串括起来,示例如下:
android:text="@{map[`firstName`]}"
还支持用 +
号拼接字符串哦~
资源
表达式中引用应用资源,示例如下:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
还支持格式化字符串及复数的参数传入,示例如下:
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
还可以把属性引用和View引用作为资源参数进行传递,示例如下:
android:text="@{@string/example_resource(user.lastName, exampleText.text)}"
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
某些资源需要显式类型求值,如下表所示:
事件属性名一般由 监听器方法名称确定,如:View.OnClickListener → onClick() → android:onClick。但存在特例,如下表:
另外,可以使用 方法引用
或 监听器绑定
来进行事件处理,两者的代码示例如下:
// 方法引用
class MyHandlers {
fun onClickFriend(view: View) { ... }
}
// 监听器绑定
class Presenter {
fun onSaveClick(task: Task){}
}
不难看出区别,引用方法需要和监听器的参数一致,而监听器绑定更加灵活,可在运行时动态运行lambda表达式,参数无需一致。
上述忽略了onClick(View)的View参数,如果后面的lambda表达式有用到的话,可以定义 命名参数,示例如下:
class Presenter {
fun onCompletedChanged(task: Task, completed: Boolean){}
}
注:
- 如果监听事件的返回类型不为Void,lambda表达式也要返回相同类型的值!
- 监听器表达式这种写法功能强大,可以使代码更易阅读,但不建议写太复杂的表达式,本末倒置,反而使得布局难以阅读和维护。
变量
变量类型在编译时会进行检查,如果不同配置(如横向或纵向)有不同的布局文件,变量会合并到一起。所以这些布局文件的变量定义不要存在冲突!(如同样的变量类型不一致)
系统会根据需要生成名为 context
的特殊变量,用于绑定表达式,它的值是根视图的 getContext() 获取到的Context对象。如果另外定义了同名变量会覆盖。
包含
在include其他布局时,有时需要把变量值传递过去,可以通过 bind:变量名
进行传递,要求两个布局文件拥有同一个变量。示例如下:
...
...
android:text="@{user}"
注:不支持include作为merge元素的直接子元素,如这样的布局是编译不通过的:
DataBinding中可观察的数据对象有三种不同类型:字段、集合和对象,通过数据绑定,数据对象可在数据发生更改时通知其他对象,即监听器。
示例如下:
class User {
val firstName = ObservableField()
val lastName = ObservableField()
val age = ObservableInt()
}
// 访问字段值,使用set()、get() 访问器方法
user.firstName = "Google"
val age = user.age
除了ObservableInt类外还有这些基本类型:
ObservableBoolean、ObservableByte、ObservableChar、ObservableShort、ObservableInt、ObservableLong、ObservableFloat、ObservableDouble、ObservableParcelable
注:AS 3.1及更高版本允许使用LiveData对象替换可观察字段。
示例如下:
ObservableArrayMap().apply {
put("firstName", "Google")
put("lastName", "Inc.")
put("age", 17)
}
// 布局中通过字符串key找到值
ObservableArrayList().apply {
add("Google")
add("Inc.")
add(17)
}
// 布局中通过索引访问列表
可以自行实现 Observable 接口,但更建议使用DataBinding提供的 BaseObservable
:
实现Observable接口,线程安全,使用 PropertyChangeRegistry 来执行 OnPropertyChangedCallback。
使用代码示例如下:
class User : BaseObservable() {
@get:Bindable
var firstName: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.firstName)
}
@get:Bindable
var lastName: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.lastName)
}
}
// 附:Java中的写法
private static class User extends BaseObservable {
private String firstName;
private String lastName;
@Bindable
public String getFirstName() {
return this.firstName;
}
@Bindable
public String getLastName() {
return this.lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}
流程:getter设置Bindable注解
+ setter中调用notifyPropertyChanged()
。
问:上面的BR哪来的?
DataBinding会在模块包中生成名为
BR
的类,该类包含数据绑定的资源ID。在编译期,Bindable 注释会在BR类文件中生成一个条目。如果数据类的父类没办法更改,Observable接口可以使用PropertyChangeRegistry
对象实现。
生成的绑定类都是继承的 ViewDataBinding
,类名基于布局名称,采用 Pascal命名法 进行转换并添加Binding 后缀,如 activity_main.xml → ActivityMainBinding。
直接点开 DataBindingUtil
类,可以看到里面提供的多种绑定相关的方法:
如果可以 预知绑定类型,如ActivityMainBinding,也可以直接用ActivityMainBinding.bind()来绑定~
DataBinding会对布局中拥有ID的每个View在绑定类中创建不可变字段。
DataBinding会为布局中声明的每个变量生成getter、setter方法。
占位置,惰性加载,当ViewStub被inflate或setVisible可见,它会从视图层次结构消失,如果想绑定里面的View,需要在监听 OnInflateListener,在此完成绑定
当可变或可观察对象发生更改时,绑定会按照计划在下一帧之前发生更改。如果需要立即执行绑定,强制执行,可 executePendingBindings()
,但要注意,此方法必须运行在UI线程。
动态变量,有时系统并不知道特定的绑定类,但仍需指定绑定值,如RecyclerView.Adapter,示例如下:
class BindingHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
lateinit var binding: ViewDataBinding
}
class MyAdapter(data: List) : RecyclerView.Adapter() {
private var mData = data
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder {
// 核心代码
val binding: ViewDataBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.item_layout,
parent,
false
)
val holder = BindingHolder(binding.root)
holder.binding = binding
return holder
}
override fun onBindViewHolder(holder: BindingHolder, position: Int) {
val user = mData[position]
holder.binding.setVariable(BR.mUser, user)
}
override fun getItemCount() = mData.size
}
属性搜索对应方法,不会考虑命名空间,只考虑 属性名称
和 **类型*
*,如:
android:text="@{user.name}
如果user.getName()的返回值为String,查找接受String参数的setText()方法,所以表达式返回正确的类型很重要,必要时你还可以根据需要进行类型转换。
使用 @BindingMethods
注解一个类 (接口也可以),相当于一个容器,内部参数是一个 @BindingMethod
数组。一般用不到它,绝大部分的属性DataBinding都已经使用命名惯例实现了。用法示例如下:
@BindingMethods(value = [
BindingMethod(type = ImageView::class, attribute = "android:tint", method = "setImageTintList"),
BindingMethod(type = ImageView::class, attribute = "android:xxx", method = "setAaaXxx")
])
class ImageBindingAdapter
有些属性需要自定义逻辑,可以使用 @BindingAdapter
注解来自定义setter的,如:android:paddingLeft没有关联的setter,而是提供了setPadding(left, top, right, bottom) 。示例如下:
@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom())
}
注意参数类型:与属性关联的View类型 + 与属性绑定表达式中接受的类型。
还可以定义接收多个属性的适配器,示例如下:
@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
Picasso.get().load(url).error(error).into(view)
}
在布局中使用适配器,示例如下:
如果ImageView同时使用了imageUrl、error,且前者是String,后者是Drawable,就会调用适配器。
如果你希望设置了任意属性就调用适配器,可以将适配器的 requireAll
设置为 false,示例如下:
@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
if (url == null) {
imageView.setImageDrawable(placeholder);
} else {
MyImageLoader.loadInto(imageView, url, placeholder);
}
}
注:
官方文档还贴出了更复杂一点的示例,读者感兴趣自己看吧,懒得搬运了~
自动转换对象:绑定表达式返回Object时,会选择用于设置属性值的方法,会自动转换为所选方法的参数类型。
自定义转换:某些情况下,需要在特定类型间自定义转换,如 android:background 需要 Drawable 但指定color传入的值却是整数。
每当需要Drawable且返回整数时,int都应转换为ColorDrawable,可以使用 @BindingConversion
注解静态方法来完成这个转换。
@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)
注:绑定表达式提供的值类型要保持一致,不能在同一个表达式中使用不同类型,如:
单向数据绑定时,可为属性设置值,并在事件监听器中更新属性:
双向数据绑定为上述过程提供了一种快捷方式:
相比起普通的@{}多了个**=
**,可接收属性的数据更改并同时监听用户更新,对应属性还得做下更改:
class LoginViewModel : BaseObservable {
// val data = ...
@Bindable
fun getRememberMe(): Boolean {
return data.rememberMe
}
fun setRememberMe(value: Boolean) {
// 避免死循环
if (data.rememberMe != value) {
data.rememberMe = value
// 对变化做出反应
saveData()
// 更新观察者
notifyPropertyChanged(BR.remember_me)
}
}
}
由于可绑定属性的 getter 方法称为 getRememberMe()
,因此属性的相应 setter 方法会自动使用名称 setRememberMe()
。
双向绑定 存在一个很大的问题 死循环,数据变化触发视图变化,视图变化又会触发数据变化,一直循环,所以 需要对变化前后的数据进行判断,有变动才更新。
DataBinding中内置支持双向绑定的类如下图所示:
表中没有的属性,想用双向绑定,就得自己实现 @BindingAdapter
注解了。
官方例子:对名为MyView的自定义View中,对其"time"属性启用双向绑定,流程如下:
// 1、使用@BindingAdapter修饰setter方法
@BindingAdapter("time")
@JvmStatic fun setTime(view: MyView, newValue: Time) {
// 新旧值对比,避免死循环
if (view.time != newValue) {
view.time = newValue
}
}
// 2、使用@InverseBindingAdapter修饰getter方法
@InverseBindingAdapter("time")
@JvmStatic fun getTime(view: MyView) : Time {
return view.getTime()
}
DataBinding知道 数据更改时要执行的操作(@BindingAdapter注解修饰的方法),还知道 View属性发生改变时要调用的内容(InverseBindingListener),但不知道属性何时被修改,所以还要给View设置监听器,将@BindingAdapter注解也加到监听器方法上:
// 3、View上设置监听器,可以是自定义的,也可以是通用事件,如焦点丢失或文本修改
@BindingAdapter("app:timeAttrChanged")
@JvmStatic fun setListeners(
view: MyView,
attrChange: InverseBindingListener
) {
// Set a listener for click, focus, touch, etc.
attrChange.onChange() // 通知数据更新
}
监听器中包含一个 InverseBindingListener
可用它告知DataBinding,属性已更改,可以开始调用 @InverseBindingAdapter
修饰的方法。
绑定到View的变量需要设置格式、转换或更改后才能显示,可以定义转换器对象来设置格式。
如果使用到双向表达式,还得使用反向转换器,以告知DataBinding如何将用户提供的字符串转换回后备数据类型。示例如下:
object Converter {
// 添加注解修饰反向转换器。
@InverseMethod("stringToDate")
@JvmStatic fun dateToString(
view: EditText, oldValue: Long,
value: Long
): String {
// Converts long to String.
}
@JvmStatic fun stringToDate(
view: EditText, oldValue: String,
value: String
): Long {
// Converts String to long.
}
}
纸上得来终觉浅,绝知此事要躬行,大概的用法就过到这里,后续实践过程遇到问题再来补充 (如和其他Jetpack组件配合)。
Android日常开发中,有一项令我们头大的"小事" → drawable.xml文件的维护,怎么说?
来来来,看看公司项目中drawable的这些命名:
谁看了不头皮发麻啊,还维护个XX,最好的维护就是不维护,上来就新建:
写个脚本扫描下项目中drawable.xml的文件个数 (基于Python):
import os
def search_all_drawable(path):
global drawable_count
os.chdir(path)
items = os.listdir(os.curdir)
for item in items:
contact_path = os.path.join(path, item)
// 判断文件夹、路径包含\drawable\的文件、不满足条件的文件
if os.path.isdir(contact_path):
print("[-]", contact_path)
search_all_drawable(contact_path)
elif contact_path.find(drawable_sep) != -1:
if contact_path.endswith(".xml"):
print("[+]", contact_path)
drawable_count += 1
else:
print('[!]', contact_path)
pass
if __name__ == '__main__':
drawable_sep = os.path.sep + "drawable" + os.path.sep
drawable_count = 0
search_all_drawable(r"D:\Code\Android\项目路径")
print("检索到drawable.xml文件共计:%d 个" % drawable_count)
运行结果如下:
817个,一个300多字节,算3个1KB好了,如果能全部干掉的话,能减少273KB的体积,APK瘦身新技能get√。
思路:将drawable.xml中的常用属性作为控件的自定义属性,在内部动态生成Drawable作为控件的背景。
实现示例:Silhouette
就是对常用到drawable的控件进行自定义封装,可以,但侵入式太强了,不好应用到其他任意控件。有些第三方控件可能还得走drawable.xml的老路。
一种实现方法:手动构建GradientDrawable,配合扩展函数、扩展属性等语法特性,动态设置。
通过下面这样的代码动态设置
mBinding.goMeetingBtn.shape = corner(17) +
stroke(5, "#ff0000") +
gradient(GradientDrawable.Orientation.RIGHT_LEFT, "#00ff00", "#0000ff")
还可以再折腾下,弄成更简洁的DSL形式,感兴趣可以参考 drawable.dsl 自行实现。
另一种实现方法:为LayoutInflater添加自定义LayoutInflater.Factory,解析添加的自定义属性,并生成系统提供的GradientDrawable、RippleDrawable、StateListDrawable。
实现示例:BackgroundLibrary
第二种思路的实现相比第一种侵入性低多了,接着看看用DataBinding怎么做~
上面说过,可以通过 @BindingAdapter
注解为属性提供自定义逻辑。
我们要做的就是抽取drawable.xml中的常用属性,定下属性命名规则,如:drawable_solidColor,然后编写Drawable创建及设置的逻辑,示例如下:
@BindingAdapter(value = {
"drawable_solidColor",
"drawable_radius",
}, requireAll = false)
public static void setViewBackground(View v, int color, int radius) {
GradientDrawable drawable = new GradientDrawable();
drawable.setColor(color);
drawable.setCornerRadius(radius);
view.setBackground(drawable);
}
接着就可以在符合DataBinding规则的xml中使用了:
原理还是非常简单的,就是把常用的属性抠出来比较麻烦,有轮子直接扒:noDrawable
不想另外依赖库的话,直接Copy这两个文件就好:
还可以根据自己的需求添加属性,或进行其他扩展,这种方案也比较简单。不过也有局限性,需要对应的 页面用上DataBinding,否则不会生效。
这些方案对于新项目还好,旧项目的话,想一上来就干掉所有drawable.xml,不太现实,重复的工作量太大了。可以先保证新开发的页面不再使用drawable.xml,后续改动到的页面逐步去掉drawable.xml,当剩余drawable.xml量级比较少时再批量修改。没有最好的方案,只有最适合的方案。
2333,想批量干掉也不是不可以,写脚本就好,毕竟都是重复操作,BackgroundLibrary那个方案比较好入手,文件文本替换。大概的思路:定义一个类存每个Drawable对应属性的值,然后遍历所有drawable.xml,查找用到@drawable/xxx的xml文件,定位到对应标签,删掉原来的语句,补上BackgroundLibrary里定义的属性和值。另外,如果Java/Kotlin代码中动态用到了drawable.xml还得单独处理下。
本节系统地过了一下DataBinding的用法,还送了一个DataBinding解决Drawable复用的案例,相信读者看完应该能够放心大胆地用上DataBinding了。
使用过程遇到的问题及解决方案欢迎在评论区反馈,笔者自己也会记录补充下,原理篇先欠着,后续有时间再填,就酱,谢谢~
我总结了一些Android核心知识点,以及一些最新的大厂面试题、知识脑图和视频资料解析。
需要的小伙伴私信【学习】我免费分享给你,以后的路也希望我们能一起走下去。(谢谢大家一直以来的支持,需要的自己领取)
Android学习PDF+架构视频+面试文档+源码笔记
部分资料一览:
领取地址:
点击下方卡片免费领取