DataBinding2

你说的曾经没有我的故事 提交于 2019-12-11 04:51:45

单项绑定与双向绑定

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的自动刷新时通过BaseObservablenotifyPropertyChanged,但是,视图->数据 是如何自动更新呢,也就是我们如何知道视图更新并通知数据改变,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)
    }
}
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!