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、自勉,多研究多使用,深入其源码往往会有意想不到的收获,同时也更容易消化相关的只是与内容。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章