DataBinding学习与实践
搞了搞DataBinding,并在项目新模块中进行了使用,感觉还是不错的,这里记录下使用过程及基本原理,方便以后查阅。
干啥的啊
DataBinding是啥?
一个Google官方的数据绑定框架-Data Binding Library,是对MVVM在Android上的一种实现,可以直接绑定数据到xml中,并实现自动刷新。
MVVM 又是啥?
MVVM是 Model-View-ViewModel的缩写。三部分分别是:Model – 代表你的基本业务逻辑;View – 显示内容;ViewModel – 将前面两者联系在一起的对象。MVVM 在使用当中,通常还会利用双向绑定技术即: Model 变化时,ViewModel 会自动更新,而 ViewModel 变化时,View 也会自动变化。
1、准备工作
在Module的build.gradle android模块中添加
android {
…
dataBinding {
enabled = true
}
}
在布局Layout外面添加,<layout></layout>标签,示例如下:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<RelativeLayout
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tvHelloWorld"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
</RelativeLayout>
</layout>
好,准备到这里呢,我们就可以在Activity或者Fragment中使用DataBinding了。
2、在Activity、Fragment中如何初始化Binding类
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setContentView(R.layout.activity_main);
ActivityMainBinding viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
FragmentBinding inflate = DataBindingUtil.inflate(inflater, R.layout.fragment, container, false);
return inflate.getRoot();
}
分别在Activity和Fragment中分别初始化了Binding类,可以看到,我们Binding类的生成是有规则的:activity_main-->ActivityMainBinding 、fragment-->FragmentBinding(不要忘记分别给这两个布局加<layout>标签),即:第一个单词首字母大写,第二个单词首字母大写...最后都会拼上Binding就是生成的Binding类。
如何自定义生成的Binding类名呢?如下:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data class="CustomBindingName">
</data>
...
</layout>
然后我们在生成Binding类的时候就可以:
CustomBindingName inflate = DataBindingUtil.inflate(inflater, R.layout.fragment, container, false);
初始化完Binding类之后,我们就可以直接使用该layout中定义了id的View,如:
viewDataBinding.tvHelloWolrd.setText("厉害了");
为什么可以直接拿来操作呢,因为DataBinding已经帮我们初始化好了(具体怎么初始化的下一篇再说,这篇就讲基本使用啦),这个已经初始化好的View的命名和Binding类的生成命名规则相同,只是后面没有加Binding了。
3、数据绑定
3.1、基本数据绑定
修改activity_main.xml文件如下(注意:这里不用判断bean!=null,因为DataBinding会自动帮助我们进行空指针的避免,比如@{bean.name},如果bean是null的话,bean.name则会被赋默认值(null)。age的话,则是0):
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="bean"
type="com.thc.bindingdemo.BindingBean" />
</data>
<LinearLayout
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tvHelloWolrd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.name}" />
<TextView
android:id="@+id/tvAge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(bean.age)}" />
</LinearLayout>
</layout>
在java代码中:
BindingBean bindingBean = new BindingBean("thc",18);
//viewDataBinding.setVariable(com.thc.bindingdemo.BR.bean,demoBean);
viewDataBinding.setBean(demoBean);
(以上两种方式都可以)这样通过生成的Binding类给xml文件setBean就实现了View和Data的绑定。这样只能够实现给xml进行设置数据,但是View如何实现根据Data的变化实时更新呢?
就是让我们的Model extends BaseObservable,如果想更新单个属性就在该属性set方法中使用
//更新单个属性
notifyPropertyChanged(com.thc.bindingdemo.BR.school);
//更新所有的属性
notifyChange();
//定义的Bean如下:
public class BindingDemoBean extends BaseObservable {
private String name;
private String school;
private String className;
public BindingDemoBean(String name, String school,String className) {
this.name = name;
this.school = school;
this.className = className;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
//更新所有的属性
notifyChange();
}
@Bindable
public String getSchool() {
return school;
}
public void setSchool(String school) {
this.school = school;
//更新单个属性
notifyPropertyChanged(com.thc.bindingdemo.BR.school);
}
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
}
3.2数据绑定特殊的几个地方
1)集合(注意:需要判断一下集合的长度,否则有可能出现数组越界的错误)
<variable
name="beans"
type="java.util.ArrayList<com.thc.bindingdemo.BindingDemoBean>" />
<variable
name="strings"
type="java.util.ArrayList<String>" />
<variable
name="map"
type="java.util.HashMap<String,String>" />
<variable
name="str"
type="String" />
<variable
name="num"
type="int" />
<!--需要注意的是,给ArrayList<...> 的<>进行了转义 < > -->
<variable
name="beans"
type="java.util.ArrayList<com.thc.bindingdemo.BindingDemoBean>" />
<!--取集合中的数据,设置姓名-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{beans.size>0?beans.get(0).name:bean.name}" />
2)给TextView setText的时候既有动态又有写死的。注意:方式2使用单引号&&字符个数>2,如果只有一个比如"写"的话会报错(待搞)。
<!--写死的字符+动态字符 方式1-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{beans.get(0).name + @string/app_name}" />
<!--写死的字符+动态字符 方式2-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{"写死的"+beans.get(0).name}' />
4、事件绑定
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{presenter.listenBind}"
android:text="单独更新" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{()->presenter.lambda(bean)}"
android:text="全部更新" />
//事件绑定
public class Presenter{
/**
* 实现单独更新某个属性
* 方法绑定:这种方式要求,自定义的方法要和 public void onClick(View v) {} 一样,方法名可以不同,但是参数一定要有
*/
public void listenBind(View view){
demoBean.setSchool("北大");
demoBean.setClassName("二班");
}
/**
* 更新所有的属性
* lambda表达式绑定:这种就可以任意定义了,这里是把绑定到xml的bean,传回来并show出来
* 切记:如果使用lambda表达式绑定事件,在xml中调用它的方法的时候要加(),擦,吃了这里的亏
*/
public void lambda(BindingDemoBean bean){
demoBean.setName("很强势");
demoBean.setClassName("三班");
Toast.makeText(MainActivity.this,bean.toString(),Toast.LENGTH_SHORT).show();
}
public void lambda1(){
Log.e("result","切记lambda表达式调用的时候要加()");
}
}
viewDataBinding.setPresenter(new Presenter());//这一步要记得哦
5、使用include及给include中的控件设置数据、绑定变量
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="includeBean"
type="com.thc.bindingdemo.BindingDemoBean" />
<variable
name="presenter"
type="com.thc.bindingdemo.MainActivity.Presenter" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:padding="10dp">
<TextView
android:id="@+id/tvBack"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{presenter.finish}"
android:text="返回" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:text="标题" />
<TextView
android:id="@+id/tvOperate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="分享" />
</RelativeLayout>
</layout>
viewDataBinding.includeBar.setPresenter(new Presenter());
viewDataBinding.includeBar.setIncludeBean(demoBean);
使用Include还可以直接从外层布局传值到Include布局中去,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.thc.myzhihu.bindingdemo.BindingIncludeActivity">
<!--将传入外层布局的值,直接传入Include的布局中,-->
<include
layout="@layout/include_binding_test1"
bind:includeBean="@{outterLayoutBean}"
bind:presenter="@{outterPresenter}" />
</LinearLayout>
6、表达式 & 表达式链
6.1、表达式
这里只是说明下经常用的运算符:
注意:第一个三元运算符使用的时候要做如下操作
<data>
...
<import type="android.view.View"/>
</data>
<!--三元运算符-->
<ImageView
android:visibility="@{bean.age>10?View.VISIBLE:View.GONE}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"/>
<!--空合并运算符 取两个中不为null的数据-->
<TextView
android:text="@{beans.get(0).name??bean.name}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
6.2、表达式链
<!--表达式链,就比如iv1、iv2的显示隐藏都和 bean的age大小有关,那么可以简化为如下方式-->
<ImageView
android:id="@+id/iv1"
android:visibility="@{bean.age>10?View.VISIBLE:View.GONE}"
android:src="@mipmap/ic_launcher"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/iv2"
android:visibility="@{iv1.visibility}"
android:src="@mipmap/img2"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
7、自定义BindAdapter
public class BindAdapter {
/**
* 加载网络图片
*/
@BindingAdapter({"app:imageUrl","app:placeholderDraw"})
public static void setNetImg(ImageView ivNet, String imgUrl, Drawable placeHodler){
Glide.with(ivNet.getContext()).load(imgUrl).placeholder(placeHodler).into(ivNet);
}
/**
* 给将第一个字符变成红色
*/
@BindingAdapter("app:text")
public static void setSpannelText(TextView textView,String text){
SpannableString spannableString = new SpannableString(text);
ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.RED);
spannableString.setSpan(colorSpan,0,1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spannableString);
}
}
在xml中的应用:
<!--自定义BindAdapter placeholderDraw和imageUrl必须和你的定义的静态方法中的参数一样的-->
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:imageUrl="@{str}"
app:placeholderDraw="@{@drawable/img2}" />
<!--自定义BindAdapter text必须和你的定义的静态方法中的参数一样的-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:text='@{map.get("key")}' />
注意:在使用app:placeholderDraw="@{@drawable/img2}" 的时候,不能使用@mipmap/哦
8、自定Setter
针对自定义View
public class MyImageView extends ImageView {
private String imgUrl;
public MyImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setImgUrl(String imgUrl) {
this.imgUrl = imgUrl;
Glide.with(getContext()).load(imgUrl).into(this);
}
}
<com.thc.bindingdemo.MyImageView
app:imgUrl="@{myImageUrl}"
android:layout_width="100dp"
android:layout_height="100dp"/>
9、BindingConversion
用于时间转化,即我们传一个Date格式的数据给TextView,通过BindingConversion转化为你想要的时间格式,并设置给TextView,如下:
@BindingConversion
public static String convertTime(Date date) {
SimpleDateFormat time = new SimpleDateFormat("yyyy-MM-dd");
return time.format(date);
}
<!--BindingConversion-->
<TextView
android:text="@{time}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
//设置数据
viewDataBinding.setTime(new Date());
10、双向绑定
10.1 直接根据输入内容修改bean对象
如下:根据用户输入的学校,动态改变bean的学校值,同时上面tvSchool的显示值也变化了,挺好。
<!--双向绑定,动态修改学校 这里注意 @={bean.school} @后面有个=号哦 -->
<EditText
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="@={bean.school}"/>
10.2 监听输入内容(即addTextChanged效果)
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="inputText"
type="String" />
<variable
name="presenter"
type="com.thc.dialogfragmentdemo.MainActivity.MainPresenter" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.thc.dialogfragmentdemo.MainActivity">
<EditText
android:id="@+id/edtTest"
android:layout_width="match_parent"
android:layout_height="50dp"
android:afterTextChanged="@{()->presenter.afterTextChanged1(inputText)}"
android:beforeTextChanged="@{()->presenter.beforeTextChanged(inputText)}"
android:onTextChanged="@{()->presenter.onTextChanged(inputText)}"
android:text="@={inputText}" />
<!--同样-->
</LinearLayout>
</layout>
Presenter代码如下:
public class MainPresenter {
public void onTextChanged(String s) {
LogUtil.loge("MainPresenter+onTextChanged:" + s);
}
public void afterTextChanged1(String s) {
LogUtil.loge("MainPresenter+afterTextChanged:" + s);
}
public void beforeTextChanged(String s) {
LogUtil.loge("MainPresenter+beforeTextChanged:" + s);
}
}
11、RecyclerView中应用
示例如下:
customBindingName.recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
customBindingName.recyclerView.setAdapter(new MyAdapter(getActivity(),initDatas()));
//适配器代码如下
class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyHolder>{
Context mContext;
List<BindingDemoBean> mDatas;
public MyAdapter(Context conetxt,List<BindingDemoBean> list){
this.mContext = conetxt;
this.mDatas = list;
}
@Override
public MyHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflate = LayoutInflater.from(mContext);
ViewDataBinding binding = DataBindingUtil.inflate(inflate, R.layout.item_recyclerview, parent, false);
return new MyHolder(binding);
}
@Override
public void onBindViewHolder(MyHolder holder, int position) {
ViewDataBinding binding = holder.getBinding();
BindingDemoBean bindingDemoBean = mDatas.get(position);
//执行一下executePendingBindings,及时刷新
binding.executePendingBindings();
binding.setVariable(com.thc.bindingdemo.BR.item,bindingDemoBean);
}
@Override
public int getItemCount() {
return mDatas.size();
}
class MyHolder extends RecyclerView.ViewHolder{
private ViewDataBinding mBinding;
public MyHolder(ViewDataBinding mBinding) {
super(mBinding.getRoot());
this.mBinding = mBinding;
}
public ViewDataBinding getBinding(){
return mBinding;
}
}
}
ViewHolder抽取出来:
public class BindingViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
protected final T mBinding;
public BindingViewHolder(T binding) {
super(binding.getRoot());
mBinding = binding;
}
public T getBinding() {
return mBinding;
}
}
13、自定义Component
问:我们都知道可以通过自定义BindingAdapter 提供View没有的setter方法或者,在执行View自身的setter之前进行一些操作。但是,系统是如何找到我们自定义的BindingAdapter并调用它内部的static静态方法的呢?
答:在build/intermediates/classes下面,可以找到DataBindingComponent类,包名为android.databinding,全局只会有一个该类——此接口在编译时生成,包含了所有用到的实例BindingAdapters的getter方法。
当一个BindingAdapter是一个实例方法(instance method),一个实现该方法的类的实例必须被实例化。这个生成的接口会包含每个声明BindingAdapter的类/接口的get方法。命名冲突会简单地加一个数字前缀到get方法前来解决。
使用步骤如下:
(1)比如我们这里需要实现一件换肤操作需要定义两个组件:DayComponent 和 NightComponent ,在自定义组件内部进行初始化对应的BindingAdapter,如下:
/**
* 白天组件
*/
public class DayComponent implements android.databinding.DataBindingComponent {
public MyBindingAdapter myBindingAdapter = new DayBindingAdapter();
@Override
public MyBindingAdapter getMyBindingAdapter() {
return myBindingAdapter;
}
}
(2)MyBindingAdapter是DayBindingAdapter,NightBindingAdapter的父类,来实现具体的换肤操作,DayBindingAdapter如下:
/**
* 日间Adapter
*/
public class DayBindingAdapter extends MyBindingAdapter{
@Override
public void setBgColor(LinearLayout layout, int llBgColor) {
layout.setBackgroundColor(layout.getResources().getColor(R.color.pop_bgcolor));
}
@Override
public void setTvColor(TextView tv, int tvColor) {
tv.setTextColor(tv.getResources().getColor(R.color.white));
}
}
(3)首先在Application中设置默认的Component,setComponent(DayComponent),点击换肤的时候进行Day和Night的切换
Application中:
DataBindingUtil.setDefaultComponent(new DayComponent());
public void btn5(View v){
if(MyApplication.isDay){
DataBindingUtil.setDefaultComponent(new DayComponent());
}else{
DataBindingUtil.setDefaultComponent(new NightComponent());
}
MyApplication.isDay = !MyApplication.isDay;
recreate();
}
Demo地址:https://github.com/tianHouChao/DataBindingDemo
接下来总结一下,DataBinding是如何初始化View,及与数据绑定实时刷新,及BindingAdapter原理等。
嗯,可以的!