6) 高级绑定
动态变量,有时系统并不知道特定的绑定类,但仍需指定绑定值,如RecyclerView.Adapter,示例如下:
class BindingHolder(itemView: View): RecyclerView.ViewHolder(itemView) { lateinit var binding: ViewDataBinding } class MyAdapter(data: List<User>) : RecyclerView.Adapter<BindingHolder>() { 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 }
④ 绑定适配器
1) 自动选择方法
属性搜索对应方法,不会考虑命名空间,只考虑 属性名称
和 **类型*
*,如:
android:text="@{user.name}
如果user.getName()的返回值为String,查找接受String参数的setText()方法,所以表达式返回正确的类型很重要,必要时你还可以根据需要进行类型转换。
2) 指定自定义方法名
使用 @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
3) 提供自定义逻辑
有些属性需要自定义逻辑,可以使用 @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 app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />
如果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); } }
注:
- requireAll设置为false,没填写的属性将为null,需要做好非空判断!
- 上述写法命名空间可以随意,写xxx:imageUrl也是可以的,但如果定义了如android:imageUrl就只能用这个命名空间;
- 自定义的绑定适配器和默认数据绑定适配器冲突,会使用自定义的绑定适配器;
- @BindingMethod属性名和@BindingAdapter定义的属性名相同会冲突报错;
官方文档还贴出了更复杂一点的示例,读者感兴趣自己看吧,懒得搬运了~
4) 对象转换
自动转换对象:绑定表达式返回Object时,会选择用于设置属性值的方法,会自动转换为所选方法的参数类型。
自定义转换:某些情况下,需要在特定类型间自定义转换,如 android:background 需要 Drawable 但指定color传入的值却是整数。
<View android:background="@{isError ? @color/red : @color/white}" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
每当需要Drawable且返回整数时,int都应转换为ColorDrawable,可以使用 @BindingConversion
注解静态方法来完成这个转换。
@BindingConversion fun convertColorToDrawable(color: Int) = ColorDrawable(color)
注:绑定表达式提供的值类型要保持一致,不能在同一个表达式中使用不同类型,如:
<View android:background="@{isError ? @drawable/error : @color/white}" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
⑤ 双向数据绑定
单向数据绑定时,可为属性设置值,并在事件监听器中更新属性:
<CheckBox android:id="@+id/rememberMeCheckBox" android:checked="@{viewmodel.rememberMe}" android:onCheckedChanged="@{viewmodel.rememberMeChanged}" />
双向数据绑定为上述过程提供了一种快捷方式:
<CheckBox android:id="@+id/rememberMeCheckBox" android:checked="@={viewmodel.rememberMe}" />
相比起普通的@{}多了个**=
**,可接收属性的数据更改并同时监听用户更新,对应属性还得做下更改:
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
注解了。
1) 自定义属性的双向绑定
官方例子:对名为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
修饰的方法。
2) 转换器
绑定到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组件配合)。
0x4、妙用DataBinding——解决Drawable复用
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的两种思路
自定义View
思路:将drawable.xml中的常用属性作为控件的自定义属性,在内部动态生成Drawable作为控件的背景。
实现示例:Silhouette
就是对常用到drawable的控件进行自定义封装,可以,但侵入式太强了,不好应用到其他任意控件。有些第三方控件可能还得走drawable.xml的老路。
代码自动生成Drawable赋值给控件
一种实现方法:手动构建GradientDrawable,配合扩展函数、扩展属性等语法特性,动态设置。
实现示例:《干掉shape,手动构建GradientDrwable》
通过下面这样的代码动态设置
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原理解读:《无需自定义View,彻底解放shape,selector吧》
第二种思路的实现相比第一种侵入性低多了,接着看看用DataBinding怎么做~
② 用DataBinding干掉drawable.xml的思路
上面说过,可以通过 @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中使用了:
<layout> <TextView drawable_radius="@{10}" drawable_solidColor="@{0xffff0000}" android:layout_width="60dp" android:layout_height="60dp" /> <layout/>
原理还是非常简单的,就是把常用的属性抠出来比较麻烦,有轮子直接扒:noDrawable
不想另外依赖库的话,直接Copy这两个文件就好:
还可以根据自己的需求添加属性,或进行其他扩展,这种方案也比较简单。不过也有局限性,需要对应的 页面用上DataBinding,否则不会生效。
这些方案对于新项目还好,旧项目的话,想一上来就干掉所有drawable.xml,不太现实,重复的工作量太大了。可以先保证新开发的页面不再使用drawable.xml,后续改动到的页面逐步去掉drawable.xml,当剩余drawable.xml量级比较少时再批量修改。没有最好的方案,只有最适合的方案。
2333,想批量干掉也不是不可以,写脚本就好,毕竟都是重复操作,BackgroundLibrary那个方案比较好入手,文件文本替换。大概的思路:定义一个类存每个Drawable对应属性的值,然后遍历所有drawable.xml,查找用到@drawable/xxx的xml文件,定位到对应标签,删掉原来的语句,补上BackgroundLibrary里定义的属性和值。另外,如果Java/Kotlin代码中动态用到了drawable.xml还得单独处理下。
0x5、小结
本节系统地过了一下DataBinding的用法,还送了一个DataBinding解决Drawable复用的案例,相信读者看完应该能够放心大胆地用上DataBinding了。
使用过程遇到的问题及解决方案欢迎在评论区反馈,笔者自己也会记录补充下,原理篇先欠着,后续有时间再填,就酱,谢谢~