單項綁定與雙向綁定
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)
}
}