Android Data Binding实战-高级篇
来源:互联网 发布:网络文件夹中 编辑:程序博客网 时间:2024/06/01 08:01
在掌握了Data Binding的基础使用方法之后,来尝试一下相对高级的一点的使用方法。
慕客网对应的课程视频:Android Data Binding实战-高级篇
声明:博客只是个人写的,用于学习与交流,与慕课网平台和授课老师没有其他任何关系。如涉及版权问题,请联系本人,将马上改正。
1、在RecyclerView中进行绑定
首先,先看看主界面的布局:
<?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://schemas.android.com/apk/res/ android" xmlns:tools="http://schemas.android.com/tools"> <data class="RvActivityBinding"> </data> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.hut.example.Main2Activity"> <android.support.v7.widget.RecyclerView android:id="@+id/rv" //设置id方便直接引用 android:layout_width="match_parent" android:layout_height="match_parent"/> </RelativeLayout></layout>
在上述布局中,使用了一个技巧,就是通过class
属性,自定义了Binding类的类名。
默认情况下,binding 类的名称取决于布局文件的命名,以大写字母开头,移除下划线,后续字母大写并追加 “Binding” 结尾。这个类会被放置在 databinding 包中。举个例子,布局文件 contact_item.xml 会生成 ContactItemBinding 类。如果 module 包名为 com.example.my.app ,binding 类会被放在 com.example.my.app.databinding 中。通过修改 data 标签中的 class 属性,可以修改 Binding 类的命名与位置。举个例子:<data class="CustomBinding"> ...</data>以上会在 databinding 包中生成名为 CustomBinding 的 binding 类。如果需要放置在不同的包下,可以在前面加 “.”:<data class=".CustomBinding"> ...</data>这样的话, CustomBinding 会直接生成在 module 包下。如果提供完整的包名,binding 类可以放置在任何包名中:<data class="com.example.CustomBinding"> ...</data>
接着是RecyclerView的item的布局:
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data class="TestBinding"> <variable name="item_user" type="com.hut.example.User"/> </data> <LinearLayout android:id="@+id/layout" android:layout_margin="5dp" android:background="#76f7e4" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/test" android:layout_width="match_parent" android:layout_height="20dp" android:text="Name:" /> <TextView android:layout_width="match_parent" android:layout_height="40dp" android:gravity="center" android:text="@{item_user.name}" android:textSize="20sp" /> <TextView android:layout_width="match_parent" android:layout_height="20dp" android:layout_weight="2" android:text="Age:" /> <TextView android:layout_width="match_parent" android:layout_height="40dp" android:gravity="center" android:text='@{""+item_user.age}' android:textSize="20sp" /> </LinearLayout></layout>
就是一个简单线性布局,显示User对象的name与age。
public class User { private ObservableField<String> name = new ObservableField<>(); private ObservableInt age=new ObservableInt(); public User(String name, int age) { this.name.set(name); this.age.set(age); } public void setName(String name) { this.name.set(name); } public void setAge(int age) { this.age .set(age); } public ObservableInt getAge() { return age; } public ObservableField<String> getName() { return name; }}
之后还需要实现自定义的适配器:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.BindingHolder> { private List<User> mData=new ArrayList<>(); public MyAdapter() { Random random=new Random(); for (int i=0;i<30;i++) { User user=new User("User "+i,random.nextInt(100)); mData.add(user); } } @Override public BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) {// TestBinding binding=// DataBindingUtil.inflate(// LayoutInflater.from(parent.getContext()),// R.layout.item1_user,parent,false);//这里也可以直接使用item1_user布局对应的TestBinding(自定义命名了的),其父类就是ViewDataBindng ViewDataBinding binding= DataBindingUtil.inflate( LayoutInflater.from(parent.getContext()), R.layout.item1_user,parent,false); return new BindingHolder (binding); } @Override public void onBindViewHolder(BindingHolder holder, int position) { final User user=mData.get(position); holder.getBinding().setVariable(com.hut.example.BR.item_user,user); holder.getBinding().executePendingBindings(); } @Override public int getItemCount() { return mData.size(); } public static class BindingHolder extends RecyclerView.ViewHolder { private final ViewDataBinding binding; //如果声明的binding的类型为ViewDataBinding,并非根据某一布局而生成的特定的XxxBinding类型 //就算其对应的布局里面的控件设置了id,也无法通过binding.xxx来直接使用 public BindingHolder (ViewDataBinding binding) { super(binding.getRoot()); this.binding=binding; } public ViewDataBinding getBinding() { return binding; } }}
最后只要在主界面中设置RecyclerView的LayoutManager以及适配器即可。
binding.rv.setLayoutManager(new LinearLayoutManager(this));binding.rv.setAdapter(new MyAdapter());
2、有关自定义属性
我在这一节的视频在照着敲了之后,编译时会出现一个错误:
Error:(16, 24) 警告: Application namespace for attribute app:imageUrl will be ignored.Error:(16, 24) 警告: Application namespace for attribute app:placeholder will be ignored.
而原因就出在下面代码上
@BindingAdapter({"app:imageUrl","app:placeholder"}) public static void loadImageForUrl(ImageView view, String url, Drawable drawable) { Glide.with(view.getContext()).load(url).placeholder(drawable).into(view); }
其中app
是命名空间,其名字是可以自定义的, 所以不一定非得叫app
,换成testapp
也照样可以在布局文件中使用,因此,需要在上面的代码中去掉app:
,变成@BindingAdapter({"imageUrl","placeholder"})
即可(在匹配时自定义命名空间会被忽略)。但是如果命名空间是系统的,如android:xxx
,那么可以写上完整的,如@BindingAdapter({"android:xxx})"
。(当然我也没试过不加android:
的 +_+)
而且关于这一节的视频内容,推荐去 Android Data Binding 系列(一) – 详细介绍与使用 的第8、9点去看看,那里会更直观。
尤其需要注意里面的那个例子(如下):
Binding adapter 在其他自定义类型上也很好用。举个例子,一个 loader 可以在非主线程加载图片。
// 无需手动调用此函数@BindingAdapter({"imageUrl", "error"})public static void loadImage(ImageView view, String url, Drawable error) { Glide.with(view.getContext()).load(url).error(error).into(view);}
<ImageView app:imageUrl=“@{url}” app:error=“@{@drawable/ic_launcher}”/>
就是一个很好的示例,当 imageUrl 与 error 存在时这个 adapter 会被调用。imageUrl 是一个 String,error 是一个 Drawable。(注意app:error
中一定要用@{}
,而非直接@drawable/xxxx
,因为这里需要的是一个包含@{}
的表达式,否则是无法得到预期效果的)
3、双向绑定
有关双向绑定的数据需要实现Observable。
也许会疑惑什么是双向绑定,但是在看了接下来的例子之后,就能直观的体会到了。
首先创建一个实体类TestBean,且需要实现Observable:
public class TestBean extends BaseObservable { private String test; @Bindable public String getTest() { return test; } public void setTest(String test) { this.test = test; notifyPropertyChanged(BR.test);//注意BR别import错了,否则是不会有test的 }}
然后是布局文件:
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" tools:context="com.zero.myapplication.MainActivity"> <data> <variable name="mTest" type="com.zero.myapplication.TestBean"/> </data> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <EditText android:layout_marginTop="10dp" android:hint="This is a test!" android:text="@={mTest.test}" android:background="#dedede" android:layout_width="match_parent" android:layout_height="50dp" /> <TextView android:text='@{"------->"+mTest.test}' android:textColor="#ff0000" android:layout_marginTop="10dp" android:layout_width="match_parent" android:layout_height="50dp" /> </LinearLayout></layout>
需要注意的是,设置EditText的android:text
属性时,使用的@={}
,而不是以前的@{}
。
最后就是在java代码中实现设置mTest变量了。
TestBean bean=new TestBean();bean.setTest("Zero");binding.setMTest(bean);
之后运行代码,在EditText中输入字符串时,TextView中的内容也在做着相应的变化,其根本原因就是mTest的test也在做着相应的变化。这就是所谓的双向绑定了,不仅仅是布局中的view绑定了实例对象的内容,实例对象的内容也和view绑定在一起,其中一者发生了变化,另外一者也相对变化,反之亦然。这样,在一些实际场景中可以节省代码,减轻工作量。
但是,在实现双向绑定时需要注意实体类实现Observable时只能通过继承BaseObservable(也就是通过注释@Bindable
)来实现,而不能直接将其成员变量设置为ObservableFields类型来实现。
回到上面的@={}
,这就是双向绑定得以实现的关键之一,不然可以试试,去掉=
号是什么样的反应?那样的话编辑EditText的内容,TextView中的内容是不会跟随其变化的,根本原=原因就是mTest的test没有发生改变。而有了=
之后,实际上在编辑EditText的时候,因为EditText与mTest的test做了绑定,就会调用test的set方法该同步test的内容(可以在set方法中打印log看看),而test又实现了Observable,所以TextView中的内容也会随之改变。这就是双向绑定实现的大概原因,而具体的实现原理,就不在这里赘述了。(需要注意,@={}
中只能是单一变量引用,不能使表达式或者常量等,如@={“——->”+mTest.test}是不行的,这些会在编译时导致错误)
说到这里,双向绑定的大致内容讲完了,不过不知道会不会有一个疑惑,就是双向绑定导致死循环?
假设有两个EditText,其android:text
都实现的@={mTest.test}
,那么在改变第一个EditText的内容时,会通过set方法设置test的内容,从而导致第二个EditText的text同步变化,但是由于该text也实现了@={mTest.test}
,又会去通过set方法设置test的内容,从而导致第一个EditText的text同步变化,最后进入一个死循环呢?当然,答案是否定的,其原因就在下图中:
在设置android:text
的内容时会做判断,如果与上次的内容一样,就会return,所以在第二个EditText的text通过set方法同步test的内容后,第一个EditText的text并不会因此而重新设置,从而阻止了死循环的发生。
还有,需要指出的是,双向绑定并不是支持所有属性的,暂时只支持那些带有额外事件的属性,比如text会带有TextChanged事件,checked会带有CheckedChange事件等。
~~~~~~~~~~~~~~~~~~~
最后,再指出一点,就是怎么实现监听属性的变更?比如在EditText的text发生变化时,实现一些额外的逻辑要求,但是这里实现了双向绑定之后,再去实现一个android:onTextChanged
就有点缀余了,那么该怎么变通呢?幸好在BaseObservable中有一个addOnPropertyChangedCallback()
方法,该方法的参数为一个Observable.OnPropertyChangedCallback,当属性发生变化时,就会回调该抽象类的onPropertyChanged(Observable observable, int i)
方法,因此可以在该方法中实现额外的逻辑。其中第一个参数observable
就是实现addOnPropertyChangedCallback()
方法的Observable对象,第二个参数i
则是对应的BR里面的int值。如果还是不理解我会在下面的例子中说明。
TestBean bean=new TestBean(); bean.setTest("Zero"); Log.d("测试1",""+bean.hashCode()); bean.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() { @Override public void onPropertyChanged(Observable observable, int i) { Log.d("测试2",""+observable.hashCode()); Log.d("测试3",""+(i==com.android.databinding.library.baseAdapters.BR.test)); Toast.makeText(MainActivity.this, String.valueOf(i), Toast.LENGTH_SHORT).show(); } }); //addOnPropertyChangedCallback需要在set Variable之前才能生效 binding.setMTest(bean);
上述代码是基于前面的来的。
其中打印的两个测试log1、2是为了验证bean对象其实就是onPropertyChanged中的observable对象(注意:TestBean继承自BaseObservable,而BaseObservable又继承自Observable);而i
则是bean对象的setTest方法中notifyPropertyChanged(BR.test);
的BR.test,通过测试log3打印的结果就可以验证了。另外还需要注意addOnPropertyChangedCallback需要在set Variable之前才能生效。
突然忍不住多想了一点:
TestBean bean=new TestBean(); bean.setTest("Zero"); bean.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() { @Override public void onPropertyChanged(Observable observable, int i) { Log.d("测试1",observable.hashCode()+" i="+i); } }); binding.setMTest(bean); TestBean bean2=new TestBean(); bean2.setTest("Zero2"); bean2.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() { @Override public void onPropertyChanged(Observable observable, int i) { Log.d("测试2",observable.hashCode()+" i="+i); } }); binding.setMTest2(bean2);
假设有两个TestBean对象做如上处理,会怎么样呢?
根据打印的日志得出结果:打印的observable.hashCode()
是不一样的,这时因为是两个不同的实例对象,而i
是一样的,因为都是BR.test
。
3、表达式链
(1)简化表达式
在开发中,有可能会出现如下图那样重复的表达式:
敲代码的时候可能会比较麻烦,相信大部分人都是直接copy的,但是有了Data Binding之后,就可以将上述表达式简化了:
在上图中,TextView和CkeckBox的visibility
属性都是跟随IamgeView的属性来动态变化的,而ImageView的visibility
属性则是根据user.isAdult实现动态绑定的,因此实现了跟第一张图中重复表达式的效果。而实现的关键,就是给ImageView设置id(avatar),然后在另外两个控件中利用表达式与ImageView的visibility
属性绑定在一起(@{avatar.visibility}
)。当然,也可以实现更加复杂的逻辑,如:
android:visibility="@{avatar.visibility==View.VISIBLE?View.INVISIBLE:View.VISIBLE}"
(2)隐式更新
隐式更新实现起来跟简化表达式差不多。上图就是一个实例,ImageView的visibility
属性是跟随CheckBox的checked
属性绑定在一起的,当CheckBox的checked
属性变化时,ImageView的visibility
属性也会随之变化,而实现的前提也是献给CheckBox设置id。
最后,需要注意一点的是,在给控件设置id时,可能会加入下划线,如:my_avatar
,但是在引用的时候却不能直接使用my_avatar
了,否则不会通过编译,这时因为Data Binding会根据id名自动变成驼峰变量名,即myAvatar
,从而在表达式中引用。但是在findViewById的时候,还是R.id.my_avatar
。
4、Lambda表达式
在这一节的视频中,需要注意的就是可以在xml中可以直接使用Activity对应的context变量,从而在监听器绑定中将该context回传等。
看来这一节之后,突然很好奇,在RecyclerView的item的布局中能不能回传context?为了验证这一想法,进行了如下测试。
首先创建RecyclerView的item的布局如下:
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="item" type="com.zero.myapplication.TestBean"/> <variable name="presenter" type="com.zero.myapplication. MyAdapter.Presenter"/> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="50dp" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" android:text="@{item.test}" /> <Button android:onClick="@{()-> presenter.onItemClick(context)}" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text='@{"BUTTON->"+item.test}'/> </LinearLayout></layout>
其中TestBean类为前文中的使用过的。
然后是自定义适配器:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.BindingHolder> { private Context mContext; private List<TestBean> mData=new ArrayList<>(); public MyAdapter(Context mContext) { this.mContext = mContext; for (int i=0;i<30;i++) { TestBean bean=new TestBean(); bean.setTest("BEAN "+i); mData.add(bean); } } @Override public BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) { ViewDataBinding binding= DataBindingUtil.inflate( LayoutInflater.from(mContext),R.layout.item_rv,parent,false ); Log.d("测试1", "parent.getContext()="+parent.getContext()); return new BindingHolder(binding); } @Override public void onBindViewHolder(BindingHolder holder, int position) { final TestBean bean=mData.get(position); holder.binding.setVariable(BR.item,bean); holder.binding.setVariable(BR.presenter,new Presenter()); holder.binding.executePendingBindings(); } @Override public int getItemCount() { return mData.size(); } public static class BindingHolder extends RecyclerView.ViewHolder { private final ViewDataBinding binding; public BindingHolder(ViewDataBinding binding) { super(binding.getRoot()); this.binding=binding; } } public class Presenter { public void onItemClick(Context contextFromXML) { Log.d("测试2","contextFromXML="+contextFromXML); Log.d("测试3","mContext="+mContext); } }}
在上述代码中,有三条log输出,目的就是为了检验初始化适配器时从Activity传入的context
,与从item布局回传的context
以及onCreateViewHolder时通过参数parent.getContext()获得的context
是否都一致。
经过实践,答案是肯定的。以下就是打印的部分log:
D/测试1: parent.getContext()=com.zero.myapplication.Main3Activity@4890e94 D/测试2: contextFromXML=com.zero.myapplication.Main3Activity@4890e94 D/测试3: mContext=com.zero.myapplication.Main3Activity@4890e94
其实,初始化适配器时从Activity传入的context
和onCreateViewHolder时通过参数parent.getContext()获得的context
肯定会是一致的,因为参数parent就是初始化一个新的item布局时item对应的RecyclerView所在的Activity布局的根ViewGroup,而为什么是所在的Activity布局的根ViewGroup,以及从item布局回传的context
为什么也与上述两个context一致,就要探究其实现原理了,在下能力有限,暂时就不深入了。
5、动画
在Data Binding中,可以使用 Transition (适用 API >= 19,系统 >= 4.4)来实现某些动画效果,但是这种动画只是简单的,实现一个简单的过渡效果,为了使某些场景不至于那么突兀。
演示效果如下:
在上述演示中,一个ImageView是跟随CheckBox状态的改变而VISIBLE或者GONE,而另外一个则是VISIBLE或者INVISIBLE。
涉及的代码如下:
//布局的代码<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <import type="android.view.View"/> <variable name="showImage1" type="boolean"/> <variable name="showImage2" type="boolean"/> <variable name="presenter" type="com.zero.myapplication.Main2Activity.Presenter"/> </data> <LinearLayout android:id="@+id/mLayout" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/iv1" android:visibility="@{showImage1?View.VISIBLE:View.GONE}" android:background="#f9acac" android:layout_marginTop="10dp" android:layout_gravity="center_horizontal" android:src="@drawable/ic_launcher" android:layout_width="150dp" android:layout_height="150dp" /> <CheckBox android:checked="true" android:layout_marginTop="20dp" android:layout_gravity="center_horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onCheckedChanged="@{(cb,isChecked) ->presenter.onCheckedChanged(1,isChecked)}"/> <ImageView android:id="@+id/iv2" android:visibility="@{showImage2?View.VISIBLE:View.INVISIBLE}" android:background="#f9acac" android:layout_marginTop="10dp" android:layout_gravity="center_horizontal" android:src="@drawable/ic_launcher" android:layout_width="150dp" android:layout_height="150dp" /> <CheckBox android:checked="true" android:layout_marginTop="20dp" android:layout_gravity="center_horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onCheckedChanged="@{(cb,isChecked) ->presenter.onCheckedChanged(2,isChecked)}"/> </LinearLayout></layout>
//java代码public class MainActivity extends AppCompatActivity { private ActivityMainBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.activity_main); binding.setShowImage1(true); binding.setShowImage2(true); binding.addOnRebindCallback(new OnRebindCallback() { @Override public boolean onPreBind(ViewDataBinding binding) { ViewGroup viewGroup = (ViewGroup) binding.getRoot(); //作用于整个布局,因为这里得到的viewGroup就是根布局 TransitionManager.beginDelayedTransition(viewGroup); return true; //如果返回false,发生绑定的反应则不会发生(ImageView不会INVISIBLE或者GONE), //除非显示调用ViewDataBinding.executePendingBindings() /** * Return true to allow the reevaluation to happen or false * if the reevaluation should be stopped. * If false is returned, it is the responsibility of * the OnRebindListener implementer to explicitly * call ViewDataBinding.executePendingBindings(). */ } }); binding.setPresenter(new Presenter()); } public class Presenter { public void onCheckedChanged(int which, boolean isChecked) { switch (which) { case 1:binding.setShowImage1(isChecked);break; case 2:binding.setShowImage2(isChecked);break; default:break; } } }}
下面这一段引用自:安卓 Data Binding 使用方法总结(妹妹篇)
但是这种方法对某些情况是失效的,如随着滚轮的滑动 TextView 的内容发生改变:
更具普遍性的方法是在 @BindingAdapter 修饰的方法中进行设置:
@BindingAdapter("adText")public static animateTextChanges(TextView textView, String oldText, String newText) { if (oldText == null || oldText.equals(newText)) { return; } animateTextChange(textView, oldText, newText);}
- Android Data Binding实战-高级篇
- Android Data Binding高级
- Android Data Binding实战
- Android Data Binding实战-入门篇
- Android Data Binding 高级用法
- Android Data Binding代码实战
- 从零开始的Android新项目8 - Data Binding高级篇
- Android Data Binding实战-入门篇(补充)
- Android Data Binding实战(一)
- Android Data Binding代码实战,mvvm
- 【MVVM】Android Data Binding实战(一)
- Android Data Binding高级用法-Observable、动态生成Binding Class(三)
- Android Data Binding 技术
- Android Data Binding学习
- Android Data Binding
- Android Data Binding
- android data binding
- Android Data Binding 技术
- myql更改权限后无法本地登录解决
- root和普通用户切换
- Kettle 6.1定时
- 【送给Git初学者】
- java异常处理-初步学习
- Android Data Binding实战-高级篇
- Centos7 配置登录服务器
- html+css那些必须在一起的属性
- 160. Intersection of Two Linked Lists
- 【NOIP2013】Day1T3 玛雅游戏
- node事件监听当中的on和addListener的区别
- cs231n课程课件、作业以及课程笔记
- Python学习笔记1
- yarn ha