单项绑定与双向绑定
DataBinding的核心是数据驱动View 即是:数据变化,视图自动变化,DataBinding同时也实现了双向驱动(双向绑定),即是当View的属性变化时,其对应的绑定的数据也会发生变化
1.单项绑定
单项绑定是 当数据改变时和数据绑定的View也自动更改
实现方式有两种:方式一
继承BaseObservable 在get方法上添加注解@Bindable,在set方法上 添加notifyPropertyChanged(BR.属性名称),来通知视图更新,实例如下:
public class Data extends BaseObservable {
public Data(String name){
this.name = name;
}
private String name;
@Bindable
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
notifyPropertyChanged(com.wkkun.jetpack.BR.name);
}
}
BR类似于R文件 内部存储的是变量的ID 注解@Bindable 是在BR中声明其注解的属性
上述代码实现了 每一次调用setName()方法 与该属性name绑定的所有视图都换跟着更新
方式二
如果我们需要绑定的变量比较少,那么我们可以使用DataBinding提供的类型包装类:如
ObservableBoolean
ObservableByte
ObservableChar
ObservableShort
ObservableInt
ObservableLong
ObservableFloat
ObservableDouble
ObservableParcelable
ObservableArrayMap //Map的包装类
ObservableArrayList //ArryList的包装类
ObservableField<T> //这是个变量的包装类 T可以是一切类型
上述的包装类其内部 都实现了BaseObservable 而且自动实现了 方式1中的注解 以及通知View 所以我们只要关注业务本身就行,比如上述代码我们只需要写成:
public class Data extends BaseObservable {
ObservableField<String> name = new ObservableField<String>();
}
当我们 调用name.set("hah");
时 ObservableField内部自动为我们做好了通知的逻辑
2,双向绑定
双向绑定是指,数据与View进行绑定:当数据变化时,View会自动更改,当VIew变化时,与其绑定的数据也随之绑定
不过先说双向绑定之前,我们需要了解几个注解,
@BindingMethod
有时View的属性名和其设置该属性的方法并不一致,比如ImageView的"android:tint"属性 如果我们不做任何处理的话 DataBinding在设置属性的时候 会查找setTint方法进行属性设置 但是实际上并没有这个方法,而是使用setImageTintList()方法进行设置,为了将属性和设置属性的方法关联起来 Databinding为我们提供了@BindingMethod注解 使用方式:
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
BindingMethods注解是专门且只能用来存放@BindingMethod注解的
上面BindingMethod注解 将 android.widget.ImageView的属性android:tint绑定到其内部的方法setImageTintList
比如:
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tint="@{color}"/>
android:tint 在赋值的时候 会调用 ImageView.setImageTintList()方法
说明@BindingMethod是作用于类的 而且是任何一个类都可以 比如:
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
class A{
}
类A是和ImageView以及需要用到该注解的 DataBinding一点关系都没有的类
说明:
type 指定要进行绑定的类
attribute 指定要进行绑定的属性
method 指定于属性进行绑定的方法,该方法是 type类中的方法 而且method可以省略 省略的话 则默认绑定 set+属性名的 方法
@BindingMethods
是专门用来存放注解@BindingMethod的注解容器类,比如
@BindingMethods({@BindingMethod(type = SeekBar.class, attribute = "seekProgress", method = "setProgress")
, @BindingMethod(type = TestView.class, attribute = "num", method = "setCount")})
多个@BindingMethod 用逗号隔开
示例:看注解@BindingMethod的示例
@BindingAdapter
该注解是 是将View的某个属性绑定到另一个方法上,比如
class A{
....
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
...
}
上面例子实现了 对VIew单独设置一个paddingLeft,
其中BindingAdapter内部填入属性 `android:paddingLeft` 代表是要绑定的属性
而被注解的是方法就是与属性绑定的方法,
该方法有两个参数 第一个参数 是指要作用的View 后一个参数是要填入的属性值.
上例中传入的属性值可以带命名空间比如:`android:paddingLeft` 也可以不带命名空间`paddingLeft`
带命名空间表示属性的命名空间(前缀) 必须和声明的相同,
上例声明的是`android:paddingLeft` 则我们在实际声明属性时也必须和声明的相同即是:`android:paddingLeft`,
如果声明的不带命名空间`paddingLeft`,则在声明该属性时,其前缀可以是任意的比如 ,我们可以声明为`app:paddingLeft` 或者是`abb:paddingLeft` 等等
@BindingAdapter
也可以声明多个属性,比如
@BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)
public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {
if (url == null) {
imageView.setImageDrawable(placeholder);
} else {
MyImageLoader.loadInto(imageView, url, placeholder);
}
}
该例中 声明两个属性value={"imageUrl", "placeholder"} requireAll说明是否需要填入所有的属性,true代表声明的属性必须都写入才会调用方法.
@BindingAdapter
在声明属性的时候,也可以说明填入旧值,比如说:
class A{
....
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding,int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
...
}
该实例中就说明要在绑定的方法中输入旧值 需要注意的是 旧值是在新值的前面,一定是先声明完旧只以后再新值,比如声明多个属性的,则必须先将所有属性的旧值参数写完,才可以写其后的新值参数
比如:
//该BindingAdapter作用于 TextView 且两个属性"abc:text","abc:textColor" 都必选填写
@BindingAdapter(value = {"abc:text","abc:textColor"},requireAll = true)
public static void setText(TextView text,String contentOld,String colorOld,String content,String color) {
Log.d("=====contentOld==", "" + contentOld);
Log.d("======colorOld=","" +colorOld);
Log.d("======content=","" +content);
Log.d("======color=","" +color);
}
布局文件
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:abc="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="data"
type="com.wkkun.jetpack.bean.Data" />
<variable
name="activity"
type="com.wkkun.jetpack.TestActivity" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="viewClick"
android:text="点击}" />
<TextView
android:id="@+id/tv"
abc:text="@{data.value1}"
abc:textColor="@{data.value2}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</layout>
activity
val data = Data()
var count = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val activityTestBinding =
DataBindingUtil.setContentView<ActivityTestBinding>(this, R.layout.activity_test)
activityTestBinding.activity = this
data.value1.set("${count}value1")
data.value2.set("${count}value2")
activityTestBinding.data = data
}
fun viewClick(view: View) {
count++
data.value1.set("${count}value1")
data.value2.set("${count}value2")
}
bean类
public class Data extends BaseObservable {
public ObservableField<String> value1 = new ObservableField<String>();
public ObservableField<String> value2 = new ObservableField<String>();
}
上述代码 实现每次点击AppCompatButton 都会执行viewClick方法 其内改变data的值 进而触发与其绑定的textView的属性
abc:text="@{data.value1}"
abc:textColor="@{data.value2}"
的变化,因为其属性使用@BindingAdapter绑定了setText方法 连续点击button 打印结果如下:
//初始化
=====contentOld==: null
======colorOld=: null
======content=: 1value1
======color=: 1value2
//点击第一次
=====contentOld==: 1value1
======colorOld=: 1value2
======content=: 2value1
======color=: 2value2
//点击第二次
=====contentOld==: 2value1
======colorOld=: 2value2
======content=: 3value1
======color=: 3value2
总结:@BindingAdapter
可以绑定方法 而且可以接受所有参数的旧值和新值
绑定事件
上面说了 @BindingAdapter
可以绑定属性 但是上例中绑定的属性都是值 而没有事件比如 android:onClick="viewClick"
,而实际上 是可以绑定事件的 比如:
@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
View.OnLayoutChangeListener newValue) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (oldValue != null) {
view.removeOnLayoutChangeListener(oldValue);
}
if (newValue != null) {
view.addOnLayoutChangeListener(newValue);
}
}
}
//定义变量接口
<variable
name="listener"
type="android.view.View.OnLayoutChangeListener" />
//数据绑定View
<View
android:id="@+id/tv"
android:onLayoutChange="@{listener}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
但是需要注意的是,上述listener只能是有一个方法的接口或者抽象类不能含有多个方法
上述是使用 @BindingAdapter(“android:onLayoutChange”) 绑定属性 该属性绑定事件OnLayoutChangeListener,上述方式是直接填入listener 还有一种方式是写入方法 如下:
<View
android:id="@+id/tv"
android:onLayoutChange="@{()->activity.onLayoutChange()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
类似于DataBinding1中的监听器绑定
上述绑定的事件 只有一个方法,假如我们要绑定的事件有多个方法 比如: View.OnAttachStateChangeListener
有两个方法 onViewAttachedToWindow(View)
onViewDetachedFromWindow(View)
这时我们不能绑定一个含有两个方法的接口,我们必须将该事件的两个方法 拆分成两个属性,分别设置监听器:比如:
设置监听View.OnAttachStateChangeListener事件
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}
@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll=false)
public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
OnAttachStateChangeListener newListener;
if (detach == null && attach == null) {
newListener = null;
} else {
newListener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (attach != null) {
attach.onViewAttachedToWindow(v);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
if (detach != null) {
detach.onViewDetachedFromWindow(v);
}
}
};
}
//使用ListenerUtil可以获取旧的监听器 并移除
OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, newListener,
R.id.onAttachStateChangeListener);
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener);
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener);
}
}
}
@InverseBindingMethods
该注解是用来存放注解@InverseBindingMethod的 作用和 @BindingMethods一样
@InverseBindingMethod
当我们在数据月视图单向绑定时,如果View的属性和其内部的方法不统一,我们则需要使用@BindingMethod
将属性和View内部的方法绑定起来,
但是假设我们需要双向绑定即是:从View->数据时,view的属性名与获取该属性的方法可能不统一 这时我们就需要明确获取属性的方法究竟是那个
@InverseBindingMethods(@InverseBindingMethod(type = SeekBar.class, attribute = "seekProgress", method = "getProgress"))
class A{
}
上述@InverseBindingMethod
指定类SeekBar.class,当获取去属性 seekProgress 时使用getProgress方法获取,因为按照默认的方法,获取seekProgress属性的方法应该是getSeekProgress,但是显然SeekBar.class没有这个方法,正确的是getProgress
其中:
type 指定类 如 type = SeekBar.class
attribute 指定要绑定的属性 attribute = "seekProgress"
method 指定获取属性时应该采用的方法 method = "getProgress" 其中该属性可以省略 那么databinding会默认查询 get + 属性名的
的方法
@InverseBindingAdapter
该注解是和@BindingAdapter
注解对应, 是指定获取属性时应该调用的方法,比如:
@InverseBindingAdapter("time")
public static Time getTime(MyView view) {
return view.getTime();
@BindingConversion
参数转换注解: 比如 view的android:background属性可以设置ColorDrawable 但是我们在数据绑定中 数据只有color的int值 比如 @color/red,这个运行错误,但是我们又不能直接new 一个ColorDrawable对象,怎么办呢 ,这时可以用到 @BindingConversion
注解 比如
//下面注解是在View属性需要ColorDrawable值,但是传递进来的是int时执行的操作
//但是需要注意的是 该注解是使用于全部的databinding的 而且在任何一个地方注解均可
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
//int 转 String
@BindingConversion
public static String convertIntToString(int value) {
return String.value(value);
}
注意:该注解是应用于方法,而且作用于所有的Databinding,容易引起一些错误bug而且无法察觉,比如在:
//错误示例
//在一个地方进行特殊的转换 对int 进行加值在转换 ,那么在另外一个地方另外一个人不知道有这个转换的时候 在给TextView赋值时 直接写入了int 这时得到的竟然是+3后的string值 那么就很懵逼了
@BindingConversion
public static String convertIntToString(int value) {
return String.value(value+3);
}
@InverseMethod
在数据绑定视图的时候 有时我们需要对数据进行特殊的处理,但是我们在双向绑定时 那如何将属性的值再反转为数据的形式呢,这时就要用到 @InverseMethod
假如我们需要双向绑定比如
<EditText
android:id="@+id/birth_date"
android:text="@={Converter.dateToString(viewmodel.birthDate)}"
/>
viewmodel.birthDate 是long类型 要特殊处理才能转换成string类型,这里我们使用Converter.dateToString()方法进行转换
但是反转回来呢:我们需要
public class Converter {
//注解InverseMethod 标注与dateToString相对应的反转方法为stringToDate 那么在从 视图->数据时
便会调用Converter.stringToDate()来进行反转
@InverseMethod("stringToDate")
public static String dateToString(EditText view, long oldValue,
long value) {
// Converts long to String.
}
public static long stringToDate(EditText view, String oldValue,
String value) {
// Converts String to long.
}
}
实现双向绑定
之前单向绑定时:我们在布局中的赋值方式是
属性="@{表达式}"
双向绑定的赋值方式是
属性="@={表达式}"
我们现在已经知道双向绑定中 数据->View的自动刷新时通过BaseObservable
的 notifyPropertyChanged
,但是,视图->数据 是如何自动更新呢,也就是我们如何知道视图更新并通知数据改变,android 中一般都是通过设置listener来监听View变化,这里也是通过同样的方式来监听
事实上,Databinding为每一个双向绑定(@=),都生成一个合成事件 事件名为 "属性+AttrChanged" 拼接,该事件变量继承与InverseBindingListener类
并在其内部方法onChange()中获取View的属性并设置到数据中:
并且它会寻找 该合成事件的设置方法并传达一个 InverseBindingListener 参数,如果没有这个参数则异常报错
所以我们需要声明一个绑定合成属性的方法 比如:设置我们给MyView的time的属性设置双向绑定 则我们必须要绑定设置属性 timeAttrChanged 方法
这里 属性的值的类型为InverseBindingListener 比如:
@BindingAdapter("app:timeAttrChanged")
public static void setListeners(MyView view, final InverseBindingListener attrChange) {
// Set a listener for click, focus, touch, etc.
//我们在此处监听MyView与time属性相关的事件变化,当触发该触发该变化时 调用attrChange.onchange() 方法 比如下面的例子
}
//RatingBar 监听rating变化
@BindingAdapter(value = "android:ratingAttrChanged")
public static void setListeners(RatingBar view,final InverseBindingListener ratingChange) {
if(ratingChange==null){
view.setOnRatingBarChangeListener(null)
}else {
view.setOnRatingBarChangeListener(new OnRatingBarChangeListener() {
@Override
public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser) {
if (listener != null) {
listener.onRatingChanged(ratingBar, rating, fromUser);
}
ratingChange.onChange();
}
});
}
这是我们应该也能想到 数据->视图 视图->数据 这是双向绑定,但是也会造成循环调用,代码停不下来 所以我们需要再设置属性值或者设置数据源的时候,
先判断设置的值和当前值是否相同,相同则不进行设置,这样也就中断了循环,
双向绑定举例:
自定义一个View 其包含2个Button和一个TextView 2个button增减 TextView中的数字 以此模拟用户交互引起的View属性变化
自定义VIew TestView
class TestView(context: Context, attributeSet: AttributeSet) : FrameLayout(context, attributeSet) {
private var count: Int = 0
private var tvShow: TextView? = null
var listener: OnNumChangeListener? = null
init {
addView(LayoutInflater.from(context).inflate(R.layout.item_test, this, false))
tvShow = findViewById(R.id.tvShow)
findViewById<Button>(R.id.btDes).setOnClickListener {
count--
tvShow?.text = count.toString()
listener?.numChange(count)
}
findViewById<Button>(R.id.btIns).setOnClickListener {
count++
tvShow?.text = count.toString()
listener?.numChange(count)
}
}
fun getCount(): Int {
Log.d("===getCount=", count.toString())
return count
}
fun setCount(num: Int) {
Log.d("===setCount=", count.toString())
count = num
tvShow?.text = count.toString()
listener?.numChange(count)
}
interface OnNumChangeListener {
fun numChange(num: Int)
}
}
item_test.xml
<?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="50dp"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btDes"
android:layout_width="50dp"
android:text="-"
android:layout_height="50dp" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tvShow"
android:layout_width="50dp"
android:gravity="center"
android:textSize="16sp"
android:layout_height="match_parent"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btIns"
android:layout_width="50dp"
android:text="+"
android:layout_height="50dp" />
</LinearLayout>
显示如下
现在模拟双向绑定: 在布局中显示一个TestView以及一个用来展示绑定数据值的TextView
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="bean"
type="com.wkkun.jetpack.bean.TwoWayBean" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<com.wkkun.jetpack.TestView
android:id="@+id/testView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:count="@={bean.progress}" />
<TextView
android:layout_marginTop="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:text="@{String.valueOf(bean.progress)}"
/>
</LinearLayout>
</layout>
显示如下
数据类
public class TwoWayBean extends BaseObservable {
public TwoWayBean(int progress) {
this.progress = progress;
}
private int progress;
@Bindable
public int getProgress() {
Log.d("==getProgress=",String.valueOf(progress));
return progress;
}
public void setProgress(int progress) {
Log.d("==setProgress=",String.valueOf(progress));
this.progress = progress;
notifyPropertyChanged(com.wkkun.jetpack.BR.progress);
}
}
绑定类
@BindingMethods(@BindingMethod(type = TestView.class, attribute = "num", method = "setCount"))
@InverseBindingMethods( @InverseBindingMethod(type = TestView.class, attribute = "count"))
public class TwoWayAdapter {
@BindingAdapter(value = {"countAttrChanged"})
public static void setCountChangeListener(TestView testView, final InverseBindingListener listener) {
if (listener == null) {
testView.setListener(null);
} else {
testView.setListener(new TestView.OnNumChangeListener() {
@Override
public void numChange(int num) {
listener.onChange();
}
});
}
}
@BindingAdapter(value = {"count"})
public static void setCountNum(TestView testView, int num) {
Log.d("===setCountNum=", String.valueOf(num));
if (num != testView.getCount()) {
testView.setCount(num);
}
}
}
fragment类:
class TwoWayFragment : Fragment() {
private var twoWayBinding: FragmentTwoWayBinding? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
twoWayBinding = FragmentTwoWayBinding.inflate(inflater, container, false)
return twoWayBinding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
twoWayBinding?.bean = TwoWayBean(50)
}
}
来源:CSDN
作者:wkk_ly
链接:https://blog.csdn.net/wkk_ly/article/details/103472985