/   今日科技快讯   /

华为近日发布2022年第一季度经营业绩,实现销售收入1310亿元人民币,同比下降13.9%。华为第一季度净利润率同比下降6.8个百分点,至4.3%。华为轮值董事长胡厚崑表示:“整体经营结果符合预期,消费者业务受到较大影响,ICT基础设施业务实现稳定增长。此外,公司在研发上加大投入,以保持持续创新的能力,为客户创造价值。”

/   作者简介   /

明天就是五一长假了,祝大家节日愉快,我们节后再见!

本篇文章来自coder_pig的投稿,文章主要分享了DataBinding的使用,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

coder_pig的博客地址:

https://juejin.cn/user/4142615541321928/posts

/   引言   /

放羊一个月,继续回来学穿Jetpack,带来第三个组件 DataBinding (数据绑定)。在前面的章节 《【Jetpack】学穿:ViewBinding → 视图绑定》 (https://juejin.cn/post/7067532076223823903)剥源码的时候就有看到 DataBinding 相关的代码。

ViewBinding(视图绑定) 的作用和原理一言以蔽之:

  • 作用 → 代替findViewById 的同时,还能保证 空安全 和 类型安全,且 支持Java;

  • 原理 → AGP为模块中的每个XML生成绑定类,本质上还是findViewByid,只是自动生成控件实例,并一一对应;

可以把 ViewBinding 看做 DataBinding 功能的 子集,它有的DataBinding都有,而且还多了 数据绑定。

何为数据绑定?在维基百科中的定义如下:

是将 "提供器" 的数据源与 "消费者" 绑定并使其同步的一种通用技术。通常用两种不同语言的数据/信息源完成,如XML数据绑定。在UI数据绑定中,相同语言但不同逻辑功能的数据与信息对象被绑定在一起(例如Java UI元素到Java对象)。在数据绑定过程中,每个数据更改会由绑定到数据的元素自动反射。术语"数据绑定"也指一个外部数据表示随元素更改产生变化,并且底层数据自动更新以反映此更改。

又长又臭,举个简单例子就秒懂了:

一个存储数量的变量count,一个显示数量的TextView,两者绑定,当修改count的值时,TextView自动刷新。

数据源(Model)更新,绑定视图(View) 自动更新,不用开发仔再去手动setXxx(),道理就这么简单。

这种玩法又叫 单向绑定,还有一种 双向绑定,绑定视图发生改变时,数据源也跟着改变,比如:

点击显示数量的TextView,显示的数量自增1,存储数量的变量也自增1。

互相影响,这就是双向绑定。咳...都是些浅显的概念,具体怎么做?

 观察者模式 实现,数据变量与View实例关联,数据变量有更新时,遍历回调关联View实例对应设置值的方法。

自己造轮子,可以,但Duck不必~

Jetpack库中的 DataBinding组件 已经封装好一套了,要做的就是熟读文档,然后大胆使用~

API变化日新月异,建议以官方文档为准《数据绑定库》(https://developer.android.com/topic/libraries/data-binding?hl=zh-cn),本文也是基于此文档展开的学习。

/   最简单的例子   /

通过一个超简单的例子来帮助大家了解DataBinding,先有基础认知,再往下学就容易多了。

启用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之前

未使用DataBinding之前,先写布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><TextViewandroid:id="@+id/tv_test"android:layout_width="match_parent"android:layout_height="48dp"android:gravity="center"android:text="计数器:0" /><Buttonandroid:id="@+id/bt_test"android:layout_width="100dp"android:layout_height="wrap_content"android:layout_gravity="center_horizontal"android:text="加1" /></LinearLayout>

再写Activity:

class TestActivity : AppCompatActivity() {private var mCount: Int = 0override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_test)findViewById<Button>(R.id.bt_test).setOnClickListener {findViewById<TextView>(R.id.tv_test).text = "计数器:${++mCount}"}}
}

运行效果如下(点击按钮,计数器自增1):

司空见惯的常规操作,代码中主动setText()去更新TextView的文本,接着换成DataBinding试试看。

用DataBinding之后

来到布局xml文件,鼠标点到 根布局 LinearLayout,按 Alt + Enter,点击 Convert to data binding layout,自动生成一波DataBinding所需的布局。

生成后的文件内容:

多了两个标签,接着开始改造,data标签中添加属性,修改TextView的android:text指向属性:

接着到Activity:

运行后,点击加1按钮,计数+1,效果与setText()一致,修改属性值,绑定的TextView文本跟着自动刷新。

看着 灰常简单!接着系统过一波详细用法,读者按需查阅即可~

/   详细用法   /

xml布局文件

先是必须遵守的铁律:

根结点必须为<layout>,只能存在一个<data>和一个直接子View结点。

  • variable (变量标签)

变量的 属性名name不能包含_下划线,否则再kt文件里会找不到变量,有时可能需要 指定自定义类型

<variable name="user" type="cn.coderpig.awayfornoise.entity.User"/><!-- 也可以先import,然后直接用简写类名 -->
<import type="cn.coderpig.awayfornoise.entity.User"/>
<variable name="user" type="User" /><!-- 当需要使用两个同名但不同包名的类,可以使用alias别名属性 -->
<import type="com.example.User" />
<import type="cn.coderpig.awayfornoise.entity.User" alias="CpUser" />
<variable name="user" type="CpUser" />
  • data (数据标签)

它有个属性class,可以自定义DataBinding生成的类名及路径 (一般不需要):

<!--自定义类名-->
<data class="CustomDataBinding"></data><!--自定义生成路径以及类型,自动在包名下生成包以及类-->
<data class=".CustomDataBinding"></data><!-- 一般没必要自定义路径,生成位置直接全局搜 CustomDataBindingImpl -->
  • @{}表达式

支持下述运算符和关键字:

  • 算术运算符 + - / * %

  • 字符串连接运算符 +

  • 逻辑运算符 && ||

  • 二元运算符 & | ^

  • 一元运算符 + - ! ~

  • 移位运算符 >> >>> <<

  • 比较运算符 == > < >= <=(请注意,< 需要转义为 &lt;)

  • 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转换为 驼峰式大小写,示例如下:

<EditTextandroid:id="@+id/example_text"android:layout_height="wrap_content"android:layout_width="match_parent"/><TextViewandroid:id="@+id/example_output"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@{exampleText.text}"/>

集合:可以使用[]运算符访问集合,如Array、List、Map等,示例如下:

<data><import type="android.util.SparseArray"/><import type="java.util.Map"/><import type="java.util.List"/><variable name="list" type="List&lt;String>"/><variable name="sparse" type="SparseArray&lt;String>"/><variable name="map" type="Map&lt;String, String>"/><variable name="index" type="int"/><variable name="key" type="String"/>
</data><!-- 访问集合 -->
android:text="@{list[index]}"
android:text="@{sparse[index]}"
android:text="@{map[key]}"<!-- 也可以用object.key表示法在map中引用值 -->
android:text="@{map.key}"

注:变量的元素类型type的值不能包含 '<' 字符,直接 List<String> 这样写会引起XML语法错误。需要对 '<' 做下 转义,即 &lt;。

字符串:可以用 单引号('') 包裹特征值,这样就可以在表达式中使用双引号了,示例如下:

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) { ... }
}<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"><data><variable name="handlers" type="com.example.MyHandlers"/><variable name="user" type="com.example.User"/></data><LinearLayoutandroid:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><TextView android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@{user.firstName}"// 引用的方法,参数需要和监听器方法签名一致 (参数类型、个数)android:onClick="@{handlers::onClickFriend}"/></LinearLayout>
</layout>// 监听器绑定
class Presenter {fun onSaveClick(task: Task){}
}<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"><data><variable name="task" type="com.android.example.Task" /><variable name="presenter" type="com.android.example.Presenter" /></data><LinearLayout android:layout_width="match_parent" android:layout_height="match_parent"><Button android:layout_width="wrap_content" android:layout_height="wrap_content"// lambda表达式,事件分发后会对此表达式进行求值android:onClick="@{() -> presenter.onSaveClick(task)}" /></LinearLayout>
</layout>

不难看出区别,引用方法需要和监听器的参数一致,而监听器绑定更加灵活,可在运行时动态运行lambda表达式,参数无需一致。

上述忽略了onClick(View)的View参数,如果后面的lambda表达式有用到的话,可以定义 命名参数,示例如下:

class Presenter {fun onCompletedChanged(task: Task, completed: Boolean){}
}<CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
  • 如果监听事件的返回类型不为Void,lambda表达式也要返回相同类型的值!

  • 监听器表达式这种写法功能强大,可以使代码更易阅读,但不建议写太复杂的表达式,本末倒置,反而使得布局难以阅读和维护。

变量:变量类型在编译时会进行检查,如果不同配置(如横向或纵向)有不同的布局文件,变量会合并到一起。所以这些布局文件的变量定义不要存在冲突!(如同样的变量类型不一致)

系统会根据需要生成名为 context 的特殊变量,用于绑定表达式,它的值是根视图的 getContext() 获取到的Context对象。如果另外定义了同名变量会覆盖。

包含:在include其他布局时,有时需要把变量值传递过去,可以通过 bind:变量名 进行传递,要求两个布局文件拥有同一个变量。示例如下:

<variable name="user" type="com.example.User"/>
...
<include layout="@layout/name"bind:user="@{user}"/><!-- include指向的布局 -->
<variable name="user" type="com.example.User"/>
...
android:text="@{user}"

注:不支持include作为merge元素的直接子元素,如这样的布局是编译不通过的:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:bind="http://schemas.android.com/apk/res-auto"><data><variable name="user" type="cn.coderpig.awayfornoise.entity.User"/></data><merge><!-- Doesn't work --><include layout="@layout/name"bind:user="@{user}"/><include layout="@layout/contact"bind:user="@{user}"/></merge>
</layout>

可观察的数据对象

DataBinding中可观察的数据对象有三种不同类型:字段、集合和对象,通过数据绑定,数据对象可在数据发生更改时通知其他对象,即监听器。

  • 可观察字段

class User {val firstName = ObservableField<String>()val lastName = ObservableField<String>()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<String, Any>().apply {put("firstName", "Google")put("lastName", "Inc.")put("age", 17)
}// 布局中通过字符串key找到值
<data><import type="android.databinding.ObservableMap"/><variable name="user" type="ObservableMap<String, Object>"/>
</data>
<TextView android:text="@{user.lastName}" ... />ObservableArrayList<Any>().apply {add("Google")add("Inc.")add(17)
}// 布局中通过索引访问列表
<data><import type="android.databinding.ObservableList"/><variable name="user" type="ObservableList<Object>"/>
</data>
<TextView android:text="@{user[index]}" ... />
  • 可观察对象

可以自行实现 Observable 接口,但更建议使用DataBinding提供的 BaseObservable:

实现Observable接口,线程安全,使用 PropertyChangeRegistry 来执行 OnPropertyChangedCallback

class User : BaseObservable() {@get:Bindablevar firstName: String = ""set(value) {field = valuenotifyPropertyChanged(BR.firstName)}@get:Bindablevar lastName: String = ""set(value) {field = valuenotifyPropertyChanged(BR.lastName)}
}// 附:Java中的写法
private static class User extends BaseObservable {private String firstName;private String lastName;@Bindablepublic String getFirstName() {return this.firstName;}@Bindablepublic 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()来绑定~

  • 带ID的View

DataBinding会对布局中拥有ID的每个View在绑定类中创建不可变字段。

  • 变量

DataBinding会为布局中声明的每个变量生成getter、setter方法。

  • ViewStub

占位置,惰性加载,当ViewStub被inflate或setVisible可见,它会从视图层次结构消失,如果想绑定里面的View,需要在监听 OnInflateListener,在此完成绑定

  • 即时绑定

当可变或可观察对象发生更改时,绑定会按照计划在下一帧之前发生更改。如果需要立即执行绑定,强制执行,可 executePendingBindings(),但要注意,此方法必须运行在UI线程

  • 高级绑定

动态变量,有时系统并不知道特定的绑定类,但仍需指定绑定值,如RecyclerView.Adapter,示例如下:

class BindingHolder(itemView: View): RecyclerView.ViewHolder(itemView) {lateinit var binding: ViewDataBinding
}class MyAdapter(data: List<User>) : RecyclerView.Adapter<BindingHolder>() {private var mData = dataoverride 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 = bindingreturn 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 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定义的属性名相同会冲突报错;

官方文档还贴出了更复杂一点的示例,读者感兴趣自己看吧,懒得搬运了~

  • 对象转换

自动转换对象:绑定表达式返回Object时,会选择用于设置属性值的方法,会自动转换为所选方法的参数类型。

自定义转换:某些情况下,需要在特定类型间自定义转换,如 android:background 需要 Drawable 但指定color传入的值却是整数。

<Viewandroid: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)

注:绑定表达式提供的值类型要保持一致,不能在同一个表达式中使用不同类型,如:

<Viewandroid:background="@{isError ? @drawable/error : @color/white}"android:layout_width="wrap_content"android:layout_height="wrap_content"/>

双向数据绑定

单向数据绑定时,可为属性设置值,并在事件监听器中更新属性:

<CheckBoxandroid:id="@+id/rememberMeCheckBox"android:checked="@{viewmodel.rememberMe}"android:onCheckedChanged="@{viewmodel.rememberMeChanged}"
/>

双向数据绑定为上述过程提供了一种快捷方式:

<CheckBoxandroid:id="@+id/rememberMeCheckBox"android:checked="@={viewmodel.rememberMe}"
/>

相比起普通的@{}多了个**=**,可接收属性的数据更改并同时监听用户更新,对应属性还得做下更改:

class LoginViewModel : BaseObservable {// val data = ...@Bindablefun 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组件配合)。

/   解决Drawable复用   /

Android日常开发中,有一项令我们头大的"小事" → drawable.xml文件的维护,怎么说?

  • 没有固定的设计规范,不同的设计师有不同的颜色、圆角大小倾向;

  • 祖传代码,每个接盘开发仔,都有自己一套命名规则,有些文件内容一样,就是名字不一样;

来来来,看看公司项目中drawable的这些命名:

谁看了不头皮发麻啊,还维护个XX,最好的维护就是不维护,上来就新建:

写个脚本扫描下项目中drawable.xml的文件个数 (基于Python):

import osdef search_all_drawable(path):global drawable_countos.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 += 1else:print('[!]', contact_path)passif __name__ == '__main__':drawable_sep = os.path.sep + "drawable" + os.path.sepdrawable_count = 0search_all_drawable(r"D:\Code\Android\项目路径")print("检索到drawable.xml文件共计:%d 个" % drawable_count)

运行结果如下:

817个,一个300多字节,算3个1KB好了,如果能全部干掉的话,能减少273KB的体积,APK瘦身新技能get√。

笔者已知干掉drawable.xml的两种思路

1. 自定义View

思路:将drawable.xml中的常用属性作为控件的自定义属性,在内部动态生成Drawable作为控件的背景。

实现示例:Silhouette(https://github.com/FreddyChen/Silhouette)

2. 代码自动生成Drawable赋值给控件

一种实现方法:手动构建GradientDrawable,配合扩展函数、扩展属性等语法特性,动态设置。

实现示例:《干掉shape,手动构建GradientDrwable》(https://juejin.cn/post/7082321786720747527)

另一种实现方法:为LayoutInflater添加自定义LayoutInflater.Factory,解析添加的自定义属性,并生成系统提供的GradientDrawable、RippleDrawable、StateListDrawable。

实现示例:BackgroundLibrary原理解读:《无需自定义View,彻底解放shape,selector吧》(https://juejin.cn/post/6844903676973170702)

第二种思路的实现相比第一种侵入性低多了,接着看看用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><TextViewdrawable_radius="@{10}"drawable_solidColor="@{0xffff0000}"android:layout_width="60dp"android:layout_height="60dp" />
<layout/>

原理还是非常简单的,就是把常用的属性抠出来比较麻烦,有轮子直接扒:noDrawable(https://github.com/whataa/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了。

使用过程遇到的问题及解决方案欢迎在评论区反馈,笔者自己也会记录补充下,原理篇先欠着,后续有时间再填,就酱,谢谢~

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

在微软工作365天,还你一个我眼中更加真实的微软

深度剖析Android IPC原理

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

DataBinding → 数据绑定 (使用篇)相关推荐

  1. Android端MVVM从入门到实战(第三篇) - DataBinding数据绑定

    前言 上一章内容中的代码,如果我们延迟以后重新给实体类赋值,会发现UI并没更新,在更早的内容中我们讲到过,需要用LiveData去通知观察者更新,不过这里我们要讲一下另外一个方法,也是更基本的方法 - ...

  2. 快速开发基于 HTML5 网络拓扑图应用之 DataBinding 数据绑定篇

    前言 发现大家对于我从 json 文件中直接操作节点属性来控制界面的动态变化感到比较好奇,所以这篇就针对数据绑定以及如何使用这些绑定的数据做一篇说明,我写了一个简单的例子,基于机房工控的服务器上设备的 ...

  3. python 同花顺thstrader_Python 踩坑之旅进程篇其三pgid是个什么鬼 (子进程\子孙进程无法kill 退出的解法)...

    代码示例支持 平台: Centos 6.3 Python: 2.7.14 1.1 踩坑案例 pid, ppid是大家比较常见的术语, 代表进程号,父进程号. 但pgid是个什么鬼? 了解pgid之前, ...

  4. Android 面试(Android 篇)

    Android 面试 安卓篇 一.跨进程通信的几种方式 二.多进程应用使用场景 三.Binder 机制 (进程间的通信) 四.说一说 ANR 五.什么是OOM,如何避免解决 六.内存泄露,内存溢出,内 ...

  5. 使用Android BindingAdapter与InverseBindingAdapter实现SeekBar双向(正向/反向)数据绑定...

    使用Android BindingAdapter与InverseBindingAdapter实现SeekBar双向(正向/反向)数据绑定 在我之前写的系列文章中,继续深化Android数据绑定技术的使 ...

  6. 高仿掘金App —— 基于 databinding

    0. 项目地址 MVVM-JueJin 1. 项目初衷 不同于前端 vue. react 的火热, 移动端的 databinding 好像不受待见.鉴于 vue. react 都有各自成熟的生态圈,我 ...

  7. Jetpack之DataBinding,android蓝牙开发教程

    DataBinding在xml中数据绑定支持的语法表达式也是非常丰富的,支付在布局文件中使用一下运算符.表达式和关键字: 算术运算符:+ - * / %: 字符串连接运算符:+: 逻辑运算符:& ...

  8. 创建用于 ASP.NET 的分页程序控件

    Dino Esposito Wintellect 2003 年 10 月 适用于:     Microsoft® ASP.NET 摘要:解决向任何 ASP.NET 控件添加分页功能的问题.还为开发复合 ...

  9. 9.kotlin安卓实践课程-用kotlin写第一个fragment的recyclerView的adapter具体实现

    简介 主要会通过安卓实战来讲解kotlin语法和实际应用,本教程设及知识点包括框架模式mvp+mvvm, Databinding(数据绑定框架),Dagger2(依赖注入框架),DeepLink(页面 ...

最新文章

  1. Unity The Type Matching Rule
  2. C++ Heavy Light Decomposition重轻分解的实现算法(附完整源码)
  3. python有哪些方面_Python学习中最基本的内容,看看有哪些需要我们学习的
  4. SAP Spartacus ComponentData的提前subscription
  5. java培训学费_零基础Java培训大概多少钱
  6. Linux  释放Linux 系统预留的硬盘空间
  7. spark filter过滤某个字段在几个值里面_Spark案例学习-PV的统计
  8. 关于天猫魔盒tmb100系列 开机灯亮显示器无反应的问题分析
  9. 机器学习基础算法(5)
  10. easyUI动态加载combobox
  11. Web前端——什么是web前端,什么是HTML及其常用标签
  12. 构建创新数字基础设施,中泰证券携手华为迈进数字化转型“快车道”
  13. python cv2截取不规则区域图片
  14. 如何阅读Smalltalk程序
  15. WPS、office使用入坑心得
  16. Unity 实现A* 寻路算法
  17. 微软培训和认证的建议
  18. android 音频输出手动切换到听筒播放,Android-通过听筒播放音频
  19. java calendar计算时间差_Java Calendar 计算时间差
  20. html css x y相对定位坐标,HTML与CSS之相对定位、绝对定位

热门文章

  1. 联想小新pro16 | CSGO掉帧问题解决
  2. threejs 的思维导图
  3. 对比AMD和英特尔显卡的区别
  4. 在新窗口中打开 base64 格式的图片
  5. TSN 3. Ethernet AVB 协议栈分析
  6. PLATO-2: Towards Building an Open-Domain Chatbot via Curriculum Learning
  7. 数字 IC 设计领域的书籍、工具、脚本推荐
  8. 一个全网最详细的Python教程
  9. Python:用 peewee 框架连接 SQL Server
  10. python b站 排行_用python爬虫追踪知乎/B站大V排行!