DataBinding2

單項綁定與雙向綁定

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)
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章