Android Jetpack之DataBinding(三)

前言

本文是在前文的基礎上繼續深入DataBinding的使用這一塊,如果有不懂的地方,請移步上一篇
Android Jetpack之DataBinding(一)
Android Jetpack之DataBinding(二)

DataBinding使用之綁定適配器

綁定適配器負責發出相應的框架調用來設置值。如setText()、setOnClickListener()等,同時適配器綁定庫允許自定義方法、邏輯、和
返回值類型等操作。
1. 設置特性值、自動選擇方法:
只要綁定的值放生改變,生成的綁定類就會調用對應的set方法實現相應的改變,看個最簡單的例子如下,其實是在調用了響應的setText方法:
xml代碼:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="student" type="com.zgj.demojetpackapp.bean.Student"/>
    </data>
    <LinearLayout
            android:gravity="center_horizontal"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <TextView
                android:layout_marginTop="@dimen/dimen_10"
                android:id="@+id/tv_nick"
                android:text="@{student.name}"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>

        <TextView
                android:layout_marginTop="@dimen/dimen_10"
                android:id="@+id/tv_age"
                android:text="@{student.age+``}"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>

    </LinearLayout>
</layout>
class TestActivity7 : AppCompatActivity() {
    var binding: ActivityTestDataBinding7Binding? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(
            this,
            R.layout.activity_test_data_binding7
        )
        binding?.student = Student("小美女", 5)
    }

}

效果如下:
在這裏插入圖片描述

綁定的過程分析:
  1. 起始點:起始的出發點肯定是在student變化的代碼,即:
 binding?.student = Student("小美女", 5)
  1. 一路追進去最終會找到綁定實現類ActivityTestDataBinding7BindingImpl的方法executeBindings的方法如下,重點在下面
    protected void executeBindings() {
    		.... 
    		 //此處自動選擇get方法取值
       		  // read student.name
             studentName = student.getName();
              // read student.age
            studentAge = student.getAge();
       		...此處進行值的設置
            androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvAge, studentAgeJavaLangString);
            androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvNick, studentName);
       		...
    }

最終調用到了TextViewBindingAdapter的setText方法,再進入可以看到具體的設置過程,可以看到在進行了一些列的判斷之後最終執行的方法是view的setText方法,和我們平時設置的過程是一樣的。
@BindingAdapter(“android:text”)
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return; // No change in the spans, so don’t set anything.
}
} else if (!haveContentsChanged(text, oldText)) {
return; // No content changes, so don’t set anything.
}
// 這裏
view.setText(text);
}

2. 綁定listener
...
   <variable name="listener"
                  type="com.zgj.demojetpackapp.databinding.TestActivity7.EventListener"/>

        <TextView
                android:padding="10dp"
                android:text="點擊事件"
                android:onClick="@{listener::onClick}"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>
...
...
 inner class EventListener {
        fun onClick(view: View) {
            Toast.makeText(this@TestActivity7, "點擊按鈕", Toast.LENGTH_SHORT).show()
        }
    }
...
  binding?.listener = EventListener()
...

效果如下:
效果2

綁定的過程分析:
  1. 起始點:起始點依然是設置監聽器的位置
  binding?.listener = EventListener()
  1. 和上述的綁定過程類似,最終到了ActivityTestDataBinding7BindingImpl的方法executeBindings的方法如下:
    最終設置的是一個持有傳入listener的原生監聽器子類OnClickListenerImpl
  protected void executeBindings() {
    		.... 
    		//聲明瞭一個View本身的listener
       		android.view.View.OnClickListener listenerOnClickAndroidViewViewOnClickListener = null;
    		//這塊是取到設置的listener
    		 com.zgj.demojetpackapp.databinding.TestActivity7.EventListener listener = mListener;
    		 ...
    		   //對上面聲明的變量原生listener進行賦值,三目運算符操作,是一個新的實現類OnClickListenerImpl
    		   // read listener::onClick
             listenerOnClickAndroidViewViewOnClickListener = (((mListenerOnClickAndroidViewViewOnClickListener == null) ? (mListenerOnClickAndroidViewViewOnClickListener = new OnClickListenerImpl()) : mListenerOnClickAndroidViewViewOnClickListener).setValue(listener));
             ...
             //最終將聲明的原生監聽器設置給setOnClickListener方法
             this.mboundView3.setOnClickListener(listenerOnClickAndroidViewViewOnClickListener);
       		...
    }

OnClickListenerImpl代碼如下:

    // Listener Stub Implementations
    public static class OnClickListenerImpl implements android.view.View.OnClickListener{
    	//持有傳入的listener
        private com.zgj.demojetpackapp.databinding.TestActivity7.EventListener value;
        public OnClickListenerImpl setValue(com.zgj.demojetpackapp.databinding.TestActivity7.EventListener value) {
            this.value = value;
            return value == null ? null : this;
        }
        @Override
        public void onClick(android.view.View arg0) {
        	//此處調用傳入的listener的方法實現回調		
            this.value.onClick(arg0); 
        }
    }
結果
此處可以看出listener具體綁定的實現,當然也可以發現前面說過的問題原因,
listener的監聽器的方法名和參數必須和原生的一樣(value.onClick(arg0))。
3. 提供自定義綁定方法:
此處不做過多的說明,一般來說系統的方法都能自動選擇實現綁定,可以看下TextViewBindingAdapter的源碼,
此處進行了自定義方法的綁定,如下:
@BindingMethods({
        @BindingMethod(type = TextView.class, attribute = "android:autoLink", method = "setAutoLinkMask"),
        @BindingMethod(type = TextView.class, attribute = "android:drawablePadding", method = "setCompoundDrawablePadding"),
        @BindingMethod(type = TextView.class, attribute = "android:editorExtras", method = "setInputExtras"),
        @BindingMethod(type = TextView.class, attribute = "android:inputType", method = "setRawInputType"),
        @BindingMethod(type = TextView.class, attribute = "android:scrollHorizontally", method = "setHorizontallyScrolling"),
        @BindingMethod(type = TextView.class, attribute = "android:textAllCaps", method = "setAllCaps"),
        @BindingMethod(type = TextView.class, attribute = "android:textColorHighlight", method = "setHighlightColor"),
        @BindingMethod(type = TextView.class, attribute = "android:textColorHint", method = "setHintTextColor"),
        @BindingMethod(type = TextView.class, attribute = "android:textColorLink", method = "setLinkTextColor"),
        @BindingMethod(type = TextView.class, attribute = "android:onEditorAction", method = "setOnEditorActionListener"),
})
4. 提供自定義邏輯:
此處以加載圖片爲例,ImageView只有一個src設置圖片,沒有辦法根據特殊情況,如路徑問題、網絡問題導致的圖片加載不成功進行相關的
加載過程,一般來說項目中加載圖片會有網絡路徑、佔位圖和錯誤圖,這裏通過自定義邏輯實現這個過程。
<?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">
    <data>

        <variable name="url" type="String"/>
    </data>
    <LinearLayout
            android:gravity="center_horizontal"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

 
        <ImageView
                android:id="@+id/iv_img"
                app:url="@{url}"
                android:layout_width="match_parent"
                android:layout_height="200dp"/>
    </LinearLayout>


</layout>
class TestActivity7 : AppCompatActivity() {
    var binding: ActivityTestDataBinding7Binding? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(
            this,
            R.layout.activity_test_data_binding7
        )
        binding?.student = Student("小美女", 5)
        binding?.listener = EventListener()
        val url =
            "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1589877841442&di=55353d36cb62454230a0b86e90bd0c36&imgtype=0&src=http%3A%2F%2Fpic1.win4000.com%2Fwallpaper%2F2018-11-06%2F5be15b7635e5d.jpg"
        binding?.imgBean = ImgBean(url, R.mipmap.error, R.mipmap.placeholder)
    }

    companion object {
        @BindingAdapter(value = ["url", "error", "placeholder"], requireAll = false)
        @JvmStatic
        fun loadImg(view: ImageView, url: String, err: Int?, placeholder: Int?) {
            Glide.with(view.context).load(url).also {
                placeholder?.let { it1 -> it.placeholder(it1) }
                err?.let { it1 -> it.error(it1) }
            }.into(view)
        }
    }


}

效果如下(第一個網絡圖加載成功,第二個只有佔位圖(展示佔位圖),第三個只有錯誤圖(或者錯誤圖佔位圖都有,這兩種都是展示錯誤圖)):
效果1效果2效果3

注意問題:
  1. kotlin中加載方法要寫在 伴生對象companion object {}內,同時加上@JvmStatic註解纔可以正常運行,如果不加註解則會報錯如下(提示不是靜態的,類似於java的靜態方法):
    在這裏插入圖片描述
  2. xml中使用的參數名爲 @BindingAdapter(value = [“url”, “error”, “placeholder”], requireAll = false)中生命的參數名,不是方法的形參名,形參順序和其對應就可以,requireAll表示參數是否必須。
  3. 可選參數在方法形參必須聲明可空,即?。
5. 自動對象轉換:
當聲明或者返回的對象爲Object類型時候,會選擇設置值的方法進行設置,Object會轉換爲所選方法的參數類型,例如:
...
 <variable name="info" type="Object"/>
...
        <TextView
                android:text="@{info}"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>
...
//注意此處類型,不能直接寫個1   那樣會報類型轉換錯誤
binding?.info ="123456789"

效果如下:
效果7
轉換的位置是在ActivityTestDataBinding7BindingImpl的executeBindings方法中,代碼如下:

protected void executeBindings() {
...
 androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView5, (java.lang.CharSequence) info);
...
}
5. 自定義轉換:
自定義轉換這塊,這塊提供兩個示例,一個是boolean轉換drawable,一個是Date轉換String,代碼如下:
...
    <variable name="isException" type="boolean"/>
        <variable name="date" type="java.util.Date"/>
...
 <TextView
                android:layout_margin="@dimen/dimen_10"
                android:padding="@dimen/dimen_10"
                android:text="展示內容"
                android:textColor="@android:color/white"
                android:background="@{isException? @android:color/holo_red_dark: @android:color/darker_gray}"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>


        <TextView
                android:layout_margin="@dimen/dimen_10"
                android:padding="@dimen/dimen_10"
                android:text="展示內容1"
                android:textColor="@android:color/white"
                android:background="@{isException}"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>


        <TextView
                android:text="@{date}"
                android:layout_margin="@dimen/dimen_10"
                android:padding="@dimen/dimen_10"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>

...
//轉換器單獨定義類 @JvmStatic表示生成靜態方法
object BindingConverts {

    @BindingConversion
    @JvmStatic
    fun convertDateToString(date: Date): String {
        return SimpleDateFormat.getDateInstance(0).format(date)
    }

    @BindingConversion
    @JvmStatic
    fun convertBooleanToDrawable(back: Boolean): Drawable {
        return if (back) {
            ColorDrawable(Color.RED)
        } else {
            ColorDrawable(Color.DKGRAY)
        }
    }

}

展示效果如下(三個TextView對應三個效果,剛開始前兩個背景爲灰色,2s之後會變色,因爲延時2s修改了isException的值):
效果

過程分析
  1. isException? @android:color/holo_red_dark: @android:color/darker_gray此處雖然傳入的是資源id,但是其實最終傳入的還是Drawable對象,只是這一步是系統的轉換器完成的,在ActivityTestDataBinding7BindingImpl的executeBindings方法中有如下代碼:
  protected void executeBindings() {
  ...
  androidx.databinding.adapters.ViewBindingAdapter.setBackground(this.mboundView6, androidx.databinding.adapters.Converters.convertColorToDrawable(isExceptionMboundView6AndroidColorHoloRedDarkMboundView6AndroidColorDarkerGray))
  ...
  }

最終調用了Converters的convertColorToDrawable的方法,找到該方法如下,很清楚很明白,還是轉換器,只是系統已經定義好了

public class Converters {
    /**
     * Converts {@code int} color into a {@link ColorDrawable}.
     *
     * @param color The integer representation of the color.
     *
     * @return ColorDrawable matching the color
     */
    @BindingConversion
    public static ColorDrawable convertColorToDrawable(int color) {
        return new ColorDrawable(color);
    }

    /**
     * Converts {@code int} color into a {@link ColorStateList}.
     *
     * @param color The integer representation of the color.
     *
     * @return ColorStateList from the single color
     */
    @BindingConversion
    public static ColorStateList convertColorToColorStateList(int color) {
        return ColorStateList.valueOf(color);
    }
}
  1. 其餘兩個的調用也是在ActivityTestDataBinding7BindingImpl的executeBindings方法中,如下:
  protected void executeBindings() {
  ...
  //boolean轉換Drawable
   androidx.databinding.adapters.ViewBindingAdapter.setBackground(this.mboundView7, com.zgj.demojetpackapp.databinding.BindingConverts.convertBooleanToDrawable(isException));
   ...
   //date轉換String
   androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView8, com.zgj.demojetpackapp.databinding.BindingConverts.convertDateToString(date));
  ...
  }
注意問題:
  1. 報警告,其實報的很明白,這塊聲明isException爲Boolean類型爲裝箱類型,需要拆箱後進行操作,當然此處不處理運行也是正常的,將Boolean改成boolean就不再包警告了

    警告: isException is a boxed field but needs to be un-boxed to execute isException ? @android:color/holo_red_dark :
    @android:color/darker_gray. This may cause NPE so Data Binding will safely unbox it. You can change the expression
    and explicitly wrap isException with safeUnbox() to prevent the warning

  2. 自定義轉換器的時候注意將轉換器單獨寫在一個object聲明的類中,如BindingConverts ,不要寫在companion object 伴生對象中,如果寫進去會報錯如下:
    錯誤
    提示你BindingConversion註解只能用在public static 方法上,但是其實此時也生成public static方法,我們通過查看對應的java代碼如下:

  ...
   @JvmStatic
   public static final void loadImg(@NotNull ImageView view, @NotNull String url, @Nullable Integer err, @Nullable Integer placeholder) {
      Companion.loadImg(view, url, err, placeholder);
   }
   ...

也生成了靜態方法,但是系統仍然報錯,報錯在伴生類的方法上

  @BindingConversion
      @JvmStatic
      @NotNull
      public final String convertDateToString(@NotNull Date date) {
         Intrinsics.checkParameterIsNotNull(date, "date");
         String var10000 = SimpleDateFormat.getDateInstance(0).format(date);
         Intrinsics.checkExpressionValueIsNotNull(var10000, "SimpleDateFormat.getDateInstance(0).format(date)");
         return var10000;
      }

至於這個的具體原因目前還沒有探究清楚。

源碼

源碼鏈接:源碼

總結

1、databinding綁定適配器的使用及分析先到此處。
2、本以爲這塊適配器就是個小內容,準備分析完分析雙向綁定相關的內容,但是這塊的使用及坑還不少,限於篇幅,此篇先到此處結束,
後續再去分析雙向綁定相關的內容。
3、以上的代碼都是經過測試正常的,但是難免在粘貼或者修改的時候會有粗心或者錯誤,歡迎指出討論。
4、以上的注意問題都是自己踩過的坑,當然也可能由於各方面的侷限性會存在偏見,歡迎指出討論。
5、自勉,多研究多使用,深入其源碼往往會有意想不到的收穫,同時也更容易消化相關的只是與內容。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章