秒懂Android開發之DataBinding詳解 (part 2)

【版權申明】非商業目的註明出處可自由轉載
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/106308380
出自:shusheng007

系列:
秒懂Android開發之DataBinding詳解 (part 1)

秒懂Android開發之DataBinding詳解 (part 2)

概述

秒懂Android開發之DataBinding詳解 (part 1)一文中我們已經瞭解並上手了DataBinding,意味着你可以幹活了。 接下來就該看看其綁定規則及 BindingAdapter了,學習了DataBinding而不理解綁定原理及BindingAdapter的話,只能算是學到了皮毛。

後面行文中以DB作爲DataBinding的縮寫, BA作爲BindingAdapter的縮寫

DataBinding的綁定規則

你是否想過我們在layout文件中對TextView使用了綁定表達式後,系統是如何爲其賦值的呢?以及我們可以修改TextView的哪些特性呢?例如字體顏色可以使用表達式嗎?字體大小呢?背景可以嗎?文字超出Textview寬度後的結束方式可以嗎?等等這些問題都應該搞清楚,正是因爲這些原理性的知識將程序員分爲了庸俗和優秀兩類。不理解原理,一旦遇到特殊需求或者異常情況就會顯得束手無策。

綁定原理

DataBinding通過自動生成代碼的方式幫助程序員省去了賦值操作,整體生成的代碼比較多,最核心的是那個綁定類的具體實現類。例如我的layout文件生成綁定類爲ActivityDataBindingBinding,那麼核心類爲其子類ActivityDataBindingBindingImpl

有興趣的應該研究一下這個生成代碼的實現,由於是插件生成的,可讀性稍差,但仍然是可以閱讀的。

綁定規則

綁定規則分3種情況

  • 由類庫按規則自動選擇綁定方法

此方式系統會嘗試調用那個屬性的setter方法,例如我們爲Textview的透明度賦值 android:alpha=“@{xxx}” 那麼類庫就會尋找Textview裏面的setAlpha(arg)。如果TextView裏面有好幾個setAlpha的重載,由表達式xxx的返回類型確定調用哪一個,所以這個表達式返回值的類值型很重要。

只要是Android Framwork 提供了的屬性都可以直接使用data binding 的賦值表達式,這裏說的屬性就是你可以在layout文件中使用的那些。

data binding甚至可以綁定View中那些不存在對應屬性的setter方法。 例如DrawerLayout 中存在 setScrimColor(int) 這樣一個set方法,但是不存在可以在layout中使用的屬性,所以沒有DB的時候我們只能從代碼中調用,不能在layout文件中設置。但是使用DB就可以在layout文件中進行綁定了,如下代碼所示。

    <androidx.drawerlayout.widget.DrawerLayout
		...            
          app:scrimColor="@{xxx}"/>
  • 自定義綁定方法名稱

從第一條我們瞭解到,系統會去找屬性對應的setter方法, 那如果屬性和對應的setter方法沒有遵循傳統格式怎麼辦呢?例如 Textview的一個屬性叫 autoLink 但是其對應的設置方法叫setAutoLinkMask() 而不是叫setAutoLink() ,那系統就找不到對應的設置方法了,那怎麼辦呢?

使用@BindingMethod註解來對其重命名, 這個註解可以放在任何類上面,一般是放在你正在處理的類上面。下面的代碼將綁定方法重命名了,通過這個註解系統就知道給autoLink賦值時調用setAutoLinkMask() 而不是setAutoLink()方法

@BindingMethods({
   @BindingMethod(type = TextView.class, attribute = "android:autoLink", method = "setAutoLinkMask"),
		...
})
public class TextViewBindingAdapter {}

對於Android Framwork的屬性,即以android:開頭的屬性都不用管,DB已經幫忙做好了。但是如果你自己寫了一個擴展Textview的自定義View, 自定義屬性發生了這種問題,你就需要按照上面的方法處理了。所以你就不要找不自在了,老老實實的按照setter的語法書寫!

  • 自定義賦值方法名稱和邏輯

終於到了BindingAdapter出場的時候了。如果一個View的屬性沒有Setter方法怎麼辦呢?或者你想爲一個View新加一個自定義屬性怎麼辦呢?BindingAdapter 爲此而生。

例如 android:paddingLeft屬性就不存在相應的setter方法,那我們就可以使用BA使其可以在layout文件中綁定。Android Framework 已經爲我們實現了這個綁定適配器,如下代碼所示:

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
    view.setPadding(padding,
                view.getPaddingTop(),
                view.getPaddingRight(),
                view.getPaddingBottom())
}
   

明確了上面的綁定規則,寫起代碼來也就能做到心中有數了。 接下來就讓我們近距離觀察一下BindingAdatper吧。

BindingAdatper

通過前面的閱讀,相應你已經清楚BindingAdatper就是DataBiding庫用來爲layout中View設置值的,包括屬性和事件,的一種補充機制, 其最有魅力之處在於可以自定義名稱和邏輯。

例如下面代碼中對 android:text 值的綁定就是使用了DataBinding預先實現好的BindingAdapter。這裏需要提一下的是如果自定義BA與前的綁定規則衝突,那麼後者勝!

<TextView
    android:id="@+id/tv_girl"
	...
    android:text="@{viewModel.targetGirl.name}"
    app:wrapWithSymbol='@{viewModel.symbol}'/>

DataBinding預先爲很多View實現了各種BindingAdapter,這些你可以從你projectExternal Libaries裏面的
androidx.databinding:databinding-adapters:xxx找到,如下圖所示:
在這裏插入圖片描述
我們簡單來看一下TextViewBindingAdapter,這個是DataBinding 爲TextView實現的BindingAdapter

    @BindingAdapter("android:text")
    public static void setText(TextView view, CharSequence text) {
        ...
        ew.setText(text);
    }

    @InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
    public static String getTextString(TextView view) {
        return view.getText().toString();
    }

可以看到對應Textview的text屬性有兩個BindingAdapter,一個正向的,一個反向的,反向的用於雙向綁定,這個後面再說。layout文件中TextView 的text屬性的綁定表達式就是由上面那個正向BindingAdapter負責賦值的。

通過BindingAdapter,我們可以爲某個View使用自定義的屬性,例如前面例子中的 app:wrapWithSymbol='@{viewModel.symbol}'就是我給Textview自定義的一個屬性,這個綁定適配器的功能給人的感覺就和Kotlin裏面的擴展函數似的,只是其擴展的對象是各種View.

如何寫一個BindingAdapter

又到了躬行的時候了,不然終覺淺,讓我們動手寫一個簡單的BA吧。其實很簡單,可以分爲3步:

  1. 寫一個public static方法。
  2. 確定方法入參,第一個入參爲要綁定的View類型,例如Textview。第二個參數爲要綁定的屬性的值
  3. 使用@BindingAdapter註解標記,其參數爲自定義屬性名稱。名稱可以是任意字符串,不要帶命名空間,例如app:wrapWithSymbol 這種方式是不提倡的,因爲在查找BA的時候命名空間是會被忽略的,還一堆警告。

完成以上3步後一個BindingAdapter就寫好了,當然了,你要在方法體裏面寫上邏輯。舉個例子?sure!

先上一個Java版本吧,比較容易理解

public class MyDataBindingAdapters {
   @BindingAdapter("wrapWithSymbol")
	public static wrapWithSymbol(TextView view, String symbol) {
        view.text = symbol+view.getText()+symbol;
    }
}

對應的kotlin版本如下,稍微複雜一點,其中@JvmStatic就是爲了讓kotlin編譯器產生靜態函數的。

object MyDataBindingAdapters {
    @JvmStatic
    @BindingAdapter("wrapWithSymbol")
    fun wrapWithSymbol(view: TextView, symbol: String) {
        view.text = "$symbol${view.text}$symbol"
    }
}

BindingAdapter 裏的參數wrapWithSymbol 就是我們在layout文件中要使用的屬性名稱,可以任意定義。 方法的入參很重要,DB通過入參的類型去匹配BindingAdapter 。

多屬性BindingAdapter

自定義BindingAdapter本來就不是太常用,用到的話大多也是單屬性的,然而查看@BindingAdapter註解可以發現,它是允許傳入多個值的。

@Target(ElementType.METHOD)
public @interface BindingAdapter {
    String[] value();
    boolean requireAll() default true;
}

第一個方法類型爲數組,第二個爲bool,表示數組中傳入的屬性是否都必須賦值。

例如我們可以定義如下一個包含兩個屬性的BindingAdapter

    @JvmStatic
    @BindingAdapter(value=["imageUrl", "error"],requireAll = true)
    fun loadImage(view: ImageView, url: String, error: Drawable) {
        Picasso.get().load(url).error(error).into(view)
    }

可以在layout文件中按照如下方式調用

<ImageView 
	...
    app:imageUrl="@{viewModel.imageUrl}" 
    app:error="@{@drawable/venueError}" />

因爲我們設置了requireAll = true,所以在layout中這兩個屬性都必須賦值.

至此本文應該結束了,但是我們稍微擴展一下事件的綁定,如果沒有興趣的就可以散場了。

事件綁定

前面我們一直在談論屬性的賦值,沒有具體談論對事件的綁定,DB綁定事件時有兩種方式:

  • Method references

這種方式要求我們的業務邏輯類裏的方法簽名必須與對應的Listener裏面的方法在返回值和入參上保持簽名一致。例如View的OnClickListener源碼如下

 public interface OnClickListener {
      void onClick(View v);
  }

那麼我們的handler方法簽名必須與Listener裏的onCLick方法一致,即返回類型爲void,入參爲1個View類型

class MyHandlers {
    fun onClickFriend(view: View) { ... }
}

這樣我們就可以在layout文件中以這種方式綁定事件了,如下代碼所示

<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.MyHandlers"/>
   </data>
   <LinearLayout
       ...
       >
       <TextView 
       	   ...
           android:onClick="@{handlers::onClickFriend}"/>
   </LinearLayout>
</layout>

這種方式在事件進行綁定的時候系統已經爲我們生成了相應Listener的實現了。

  • Listener bindings

這種方式就比較靈活了,相應Listener的實現類在事件觸發的時候才生成。

只要求處理類的方法返回值類型與對應Listener一致即可,例如還是綁定View 的Click事件,我們的處理類中的方法可以傳入任意個數和類型的參數了

class MyHandlers {
     fun onClickFriend(girl: Girl) { ... }
}

我們可以使用如下綁定

 <data>
     <variable name="girl" type="com.android.example.Girl" />
     <variable name="handlers" type="com.example.MyHandlers"/>
 </data>

 <Button 
     ...
      android:onClick="@{() -> handlers.onClickFriend(girl)}" />

OnClickListener 裏onClick(View v);的參數v可以省略,也可以不省略,但是要求:要不全傳,要不全省

 <Button 
     ...
      android:onClick="@{(v) -> handlers.onClickFriend(girl)}" />

之所以講上面兩種事件綁定方式,主要是想談論一下如何綁定非函數式接口的監聽,換句話說,Listener裏面不止一個方法。例如View 中有這麼一個接口,它有兩個方法,那麼就不能直接綁定,怎麼辦呢?

    /**
     * Interface definition for a callback to be invoked when this view is attached
     * or detached from its window.
     */
    public interface OnAttachStateChangeListener {
        public void onViewAttachedToWindow(View v);
        public void onViewDetachedFromWindow(View v);
    }

答案是使用BindingAdapter,但即使使用BA也是不容易的,需要按照如下方式處理:

  1. 將原來的接口拆成多個接口,每個方法對應一個。
	@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
	public interface OnViewDetachedFromWindow {
	     void onViewDetachedFromWindow(View v);
	}
	
	@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
	public interface OnViewAttachedToWindow {
	     void onViewAttachedToWindow(View v);
	}
  1. 使用BindingAdapter多屬性方式處理
@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll=false)
public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) {
	...
}

  1. 綁定
<TextView
    ...
    android:onViewDetachedFromWindow="@{() -> handlers.onClickFriend()}"/>

總結

不知不覺嘚嘚了這麼多,我將自己認爲正確打開DataBinding的那些事都儘量說清楚了,但正所謂師父領進門修行在個人,就像俺高中時候一個老師教育俺們的:“考清華北大的學生根本就不是教出來的,而是自己學出來的”。其實他說的很對,因爲他自己只是山西臨汾師範學院畢業的。

so,加油吧,少年!

下一篇就是收宮之篇了,我們談一下雙向綁定。

本文源碼:AndroidDevMemo

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章