Android Data Binding

前提

之前講過 Android 中的框架:傳送門,裏面有一個 MVVM 框架,在 MVVM 框架中就用到了 Data Binding,在這裏我們詳細說一下,Data Binding 的優勢有什麼呢?有下面幾點:

  • 去掉了 Activity 和 Fragment 中的 UI 相關代碼
  • 讓 XML 變成 UI 的唯一真實來源
  • 不再需要 findViewById
  • 性能超過了手寫代碼,並且更加安全,不會因爲 id 錯而導致 crash
  • 所有的 UI 修改代碼保證執行在主線程

Data Binding 使用

Data Binding 聲明

我們要去使用 Data Binding 的話,就需要在 Android app 的 build.gradle 中聲明使用 Data Binding。
build.gradle( app )

android {
    dataBinding {
        enabled = true
    }
}

如果使用的是 Kotlin 的話還需要添加下面代碼

apply plugin: 'kotlin-kapt'

kapt {
    generateStubs = true
}
Layout 文件改寫

其實也很簡單,就是在原有的 Layout 文件外再套上一層 <layout></layout> 標籤

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <EditText
            android:id="@+id/userName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:hint="請輸入用戶名"
            android:textSize="14sp" />

        <Button
            android:id="@+id/submit"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="30dp"
            android:text="提交" />

        <TextView
            android:id="@+id/result"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:textSize="14sp" />
    </LinearLayout>
</layout>

然後在 Activity 中修改導入 Layout 文件的部分代碼,最後我們就可以直接通過 binding 去訪問控件。
Activity

ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.result.setText("hello, world");
UI / 事件綁定

這裏就要說到 xml 文件中的 <data></data> 標籤了。我們需要在 xml 文件中定義變量,然後在 Activity 中綁定這個數據,最後直接在控件中使用該變量,就可以達到 Activity 中數據發生改變,控件中的數據也會隨之改變的效果。然後因爲我們不需要在外部通過 ID 去修改控件中的值了,所以爲了防止這種情況的發生,我們也可以將 xml 中控件的 ID 刪去。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="viewModel"
            type="com.xjh.queryuser.mvvm.MVVMViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:hint="請輸入用戶名"
            android:textSize="14sp" />

        <Button
            android:id="@+id/submit"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="30dp"
            android:text="提交" />

        <TextView
            android:id="@+id/result"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:text="@{viewModel.result}"
            android:textSize="14sp" />
    </LinearLayout>
</layout>

然後方法我們是不是也可以這樣做呢,當然是可以的,總共有兩種綁定方式:

  1. 方法引用
  2. 監聽器綁定
方法引用

我們只要傳遞進一個類,類中有我們需要調用的方法,我們通過控件的事件屬性去調用這個方法就可以實現了,這樣我們就可以使得外部更加乾淨。
事件屬性如:

  • android:onClick
  • android:onLongClick
  • android:onTextChanged

下面就拿 android:onClick 爲例吧:

<Button
     android:id="@+id/submit"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_margin="30dp"
     android:onClick="@{viewModel.getData}"
     android:text="提交" />

然後我們在外部調用 setXXX() 方法,將我們需要的綁定的值傳遞進來就可以了。
Activity

ViewModel viewModel = new ViewModel();
binging.setViewModel(viewModel);
監聽器綁定

監聽器綁定其實也是在類中聲明一個方法,但是這個方法是需要參數傳遞的,我們也接着用 android:onClick 爲例:

<Button
    android:id="@+id/submit"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="30dp"
    android:onClick="@{() -> viewModel.getData(viewModel.userInput)}"
    android:text="提交" />

Data Binding 源碼分析

其實我們從之前的使用中可以發現,和 DataBinding 有關的主要是有下面三種:

  • android.binding 包下生成的相關代碼
  • BR 類似與 R 文件,通過這個文件可以訪問 XML 裏面的控件,類似與 R 文件
  • XXXBinding XML 對應的自動生成的 Binding 類

實現我們可以看到我們的入口函數,我們是通過 DataBindingUtil 的 setContentView 方法去獲取我們的 Binding 對象。

public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
        int layoutId, @Nullable DataBindingComponent bindingComponent) {
    activity.setContentView(layoutId);
    View decorView = activity.getWindow().getDecorView();
    ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
    return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}

進入這個方法我們可以發現,他首先做的就是 Activity 的 setContentView 方法,也就是和一般的設置佈局文件的操作是一樣的,後面就是通過 Activity 拿到 DecorView ,再通過 DecorView 拿到對應的 ViewGroup,最後通過 bindToAddedViews 去生成對應的 Binding 類。

private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component, ViewGroup parent, int startChildren, int layoutId) {
    final int endChildren = parent.getChildCount();
    final int childrenAdded = endChildren - startChildren;
    if (childrenAdded == 1) {
        final View childView = parent.getChildAt(endChildren - 1);
        return bind(component, childView, layoutId);
    } else {
        final View[] children = new View[childrenAdded];
        for (int i = 0; i < childrenAdded; i++) {
            children[i] = parent.getChildAt(i + startChildren);
        }
        return bind(component, children, layoutId);
    }
}

其實就可以發現最後返回的都是通過對應類的 bind 函數去獲取的 Binding 類。然後一系列調用 bind 函數過後就到了 DataBinderMapper 的實現類 DataBinderMapperImpl 的 getDataBinder 方法中。

@Override
public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
  int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
  if(localizedLayoutId > 0) {
    final Object tag = view.getTag();
    if(tag == null) {
      throw new RuntimeException("view must have a tag");
    }
    switch(localizedLayoutId) {
      case  LAYOUT_ACTIVITYMVVM: {
        if ("layout/activity_mvvm_0".equals(tag)) {
          return new ActivityMvvmBindingImpl(component, view);
        }
        throw new IllegalArgumentException("The tag for activity_mvvm is invalid. Received: " + tag);
      }
    }
  }
  return null;
}

首先先去比對 layoutId,如果是 LAYOUT_ACTIVITYMVVM 的話,再去比較對應的 tag 是否相同,如果相同則 new 出 ActivityMvvmBindingImpl 這個實現類。

private ActivityMvvmBindingImpl(android.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
    super(bindingComponent, root, 1
        , (android.widget.TextView) bindings[3]
        , (android.widget.Button) bindings[2]
        , (android.widget.EditText) bindings[1]
        );
    this.mboundView0 = (android.widget.LinearLayout) bindings[0];
    this.mboundView0.setTag(null);
    this.result.setTag(null);
    this.submit.setTag(null);
    this.userName.setTag(null);
    setRootTag(root);
    // listeners
    mCallback1 = new com.xjh.queryuser.generated.callback.OnClickListener(this, 1);
    invalidateAll();
}

這裏我們就能發現,我們將控件放入對應的數組中,方便之後進行調用。
整體的主要流程就是:

開始編譯
處理 Layout 文件
解析表達式
Java 編譯
解析依賴
找到 setter

Data Binding 特性

自動空指針檢查

Data Binding 會對變量進行空判斷,假如說未對給定的變量賦值的話,就會給予變量一個默認的值,比如:
{ user.name } -> null
{ user.age } -> 0

include

Data Binding 支持 include 傳遞變量,如:

<include layout = "@layout/name" bind:user = "@{user}"/>

但是 Data Binding 並不支持 direct child,例如引入的 layout 根標籤爲 merge

動態更新數據

現在我們去更新數據的話,就需要查詢傳遞一個類進去,將這個類中的所有值重新進行賦值,這樣其實是不好的,效率很低,所以說 Data Binding 提供了幾種手段去只刷新更新的值。

BaseObservable

我們的類需要繼承 BaseObservable。然後在元素的 get 和 set 方法做出修改,get 方法需要添加註解 @Bindable ,而 set 方法中需要加入刷新顯示的代碼 notifyPropertyChanged(BR.XXX)

package com.xjh.queryuser.mvvm

import android.databinding.BaseObservable
import android.databinding.Bindable
import android.view.View

import com.xjh.queryuser.bean.Account
import com.xjh.queryuser.callback.MCallback

import com.xjh.queryuser.BR

class MVVMViewModel : BaseObservable() {

    private val mvvmModel: MVVMModel = MVVMModel()
    @get:Bindable
    var userInput: String? = null
        set(userInput) {
            field = userInput
            notifyPropertyChanged(BR.userInput)
        }
    @get:Bindable
    var result: String? = null
        set(result) {
            field = result
            notifyPropertyChanged(BR.result)
        }

    fun getData(view: View) {
        this.userInput?.let {
            mvvmModel.getAccountData(it, object : MCallback {
                override fun onSuccess(account: Account) {
                    result = "用戶賬號:" + account.name + " | " + "用戶等級:" + account.level
                }

                override fun onFailed() {
                    result = "獲取數據失敗"
                }
            })
        }
    }
}


Observable Fields

如果我們只有簡單的幾個變量需要傳遞的話,爲這幾個變量封裝一個類的話,他的消耗會比較大,那我們應該怎麼去做呢。其實就是是在 變量類型前加上 Observable,比如:ObservableBoolean,ObservableByte,ObservableChar … ObservableParcelable。然後修改或者獲取值的話就要調用他的 get 或者 set 方法,這樣的話,就可以做到動態更新變量。

Observable Collection

如果說我們需要用到一些容器類的話怎麼辦呢,和 Observable Fields 一樣,我們只需要使用 ObservableArrayMap 和 ObservableArrayList 就可以了。

刷新

類或者 Observable 發生改變後,會在下一個幀進行綁定的時候發生改變,如果需要立即刷新的話,可以執行 executePendingBindings() 方法去進行立即執行。
Data Binding 會在本地化變量或者值域,以避免同步的問題發生,但是對於 Collection 是不會的。

生成

Data Binding 生成 Binding 類的規則有兩種,一種就是之前說的那種,直接默認生成,下劃線分割,大寫開頭,比如:

activity_main - > ActivityMainBinding

第二種方法的化就是自定義 class:

<layout>
	<data class = "XXX">
		...
	</data>
</layout>

這樣就可以生成我們想要的名字的類,直接使用 XXX 就可以了。

RecycleView

我們如何在 RecycleView 中使用 DataBinding 功能呢,這樣的話我們就可以省略掉對 viewholder 的控件賦值的一系列操作了,只需要對數據源做相對應的改變就可以實現。
首先我們新建三個佈局文件,這樣我們就能模擬出兩個不同樣式的 View 在同一個 list 中的顯示了。
activity_list.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="presenter"
            type="com.xjh.databinding.list.ListActivity.Presenter" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".list.ListActivity">

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{presenter::onClickAddItem}"
            android:text="增加" />

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{presenter::onClickRemoveItem}"
            android:text="刪除" />

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        </android.support.v7.widget.RecyclerView>
    </LinearLayout>
</layout>

item_employee.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="item"
            type="com.xjh.databinding.Employee" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:gravity="left"
            android:text="@{item.firstName}" />

        <TextView
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:gravity="left"
            android:text="@{item.lastName}" />
    </LinearLayout>
</layout>

item_employee_off.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="item"
            type="com.xjh.databinding.Employee" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:gravity="left"
            android:text="@{item.firstName}" />

        <TextView
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:gravity="left"
            android:text=" is fired" />
    </LinearLayout>
</layout>

這兩個 item 的數據都是通過同一個數據源進行傳遞的,直接在代碼中進行調用這個類的相關屬性。
Employee.kt

package com.xjh.databinding

import android.databinding.BaseObservable
import android.databinding.Bindable
import android.databinding.ObservableBoolean

class Employee constructor(firstName: String, lastName: String, isFired: Boolean) : BaseObservable() {
    @get:Bindable
    var firstName: String? = null
        set(userInput) {
            field = userInput
            notifyPropertyChanged(BR.firstName)
        }
    @get:Bindable
    var lastName: String? = null
        set(result) {
            field = result
            notifyPropertyChanged(BR.lastName)
        }
    var isFired = ObservableBoolean()

    init {
        this.firstName = firstName
        this.lastName = lastName
        this.isFired.set(isFired)
    }
}

既然用到了 RecyclerView,那首先就要去實現他的 ViewHolder,使用了 Data Binding 的 ViewHolder 和原有使用的地方有一些不同,不再是傳遞 View 去進行展示了,而是傳遞 Binding 對象去進行實現,展現的時候就直接調用 Binding 對象的 root 對象。
BindingViewHolder.kt

package com.xjh.databinding.list

import android.databinding.ViewDataBinding
import android.support.v7.widget.RecyclerView

class BindingViewHolder<T : ViewDataBinding>(private val mBinding: T) : RecyclerView.ViewHolder(mBinding.root) {

    fun getmBinding(): T {
        return mBinding
    }
}

在不使用 DataBinding 的 Adapter 中可能需要重新一項一項的加載數據進去,但是如果使用了 Data Binding 的話,就直接在 set 更新後的對象進行就可以了,其他的功能 Data Binding 都幫我們做好了。
EmployeeAdapter.kt

package com.xjh.databinding.list

import android.content.Context
import android.databinding.DataBindingUtil
import android.databinding.ViewDataBinding
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import com.xjh.databinding.BR
import com.xjh.databinding.Employee
import com.xjh.databinding.R
import java.util.*

class EmployeeAdapter() : RecyclerView.Adapter<BindingViewHolder<*>>() {

    companion object {
        private const val ITEM_VIEW_TYPE_ON = 1
        private const val ITEM_VIEW_TYPE_OFF = 2
    }

    private lateinit var mLayoutInflater: LayoutInflater
    private var mListener: OnItemClickListener? = null
    private lateinit var mEmployeeList: ArrayList<Employee>

    constructor(context: Context) : this() {
        mLayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        mEmployeeList = ArrayList()
    }

    override fun getItemViewType(position: Int): Int {
        val employee = mEmployeeList.get(position)
        if (employee.isFired.get()) {
            return ITEM_VIEW_TYPE_OFF
        }
        return ITEM_VIEW_TYPE_ON
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder<*> {
        var binding: ViewDataBinding = if (viewType == ITEM_VIEW_TYPE_ON) {
            DataBindingUtil.inflate(mLayoutInflater, R.layout.item_employee, parent, false)
        } else {
            DataBindingUtil.inflate(mLayoutInflater, R.layout.item_employee_off, parent, false)
        }
        return BindingViewHolder(binding)
    }

    override fun onBindViewHolder(holder: BindingViewHolder<*>, position: Int) {
        val employee = mEmployeeList[position]
        holder.getmBinding().setVariable(BR.item, employee)
        holder.getmBinding().executePendingBindings()
        holder.itemView.setOnClickListener {
            mListener?.onEmployeeClick(employee)
        }
    }

    override fun getItemCount(): Int {
        return mEmployeeList.size
    }

    fun setListener(listener: OnItemClickListener) {
        mListener = listener
    }

    fun addAll(employees: List<Employee>) {
        mEmployeeList.addAll(employees)
        notifyDataSetChanged()
    }

    val mRandom = Random(System.currentTimeMillis())

    fun add(employee: Employee) {
        val position = mRandom.nextInt(mEmployeeList.size + 1)
        mEmployeeList.add(position, employee)
        notifyItemInserted(position)
    }

    fun remove() {
        if (mEmployeeList.size != 0) {
            val position = mRandom.nextInt(mEmployeeList.size + 1)
            mEmployeeList.removeAt(position)
            notifyItemRemoved(position)
        }
    }

    interface OnItemClickListener {
        fun onEmployeeClick(employee: Employee)
    }
}

如何使用的話就和一般的 RecycleView 一樣進行使用就可以了
ListActivity.kt

package com.xjh.databinding.list

import android.databinding.DataBindingUtil
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.View
import android.widget.Toast
import com.xjh.databinding.Employee
import com.xjh.databinding.R
import com.xjh.databinding.databinding.ActivityListBinding

class ListActivity : AppCompatActivity() {

    lateinit var mBinding: ActivityListBinding
    lateinit var mEmployeeAdapter: EmployeeAdapter

    inner class Presenter {
        fun onClickAddItem(view: View) {
            mEmployeeAdapter.add(Employee("X", "JH", false))
        }

        fun onClickRemoveItem(view: View) {
            mEmployeeAdapter.remove()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_list)
        mBinding.presenter = Presenter()
        mBinding.recyclerView.layoutManager = LinearLayoutManager(this)
        mEmployeeAdapter = EmployeeAdapter(this)
        mEmployeeAdapter.setListener(object : EmployeeAdapter.OnItemClickListener {
            override fun onEmployeeClick(employee: Employee) {
                Toast.makeText(this@ListActivity, employee.firstName, Toast.LENGTH_SHORT).show()
            }
        })
        var list = ArrayList<Employee>()
        list.add(Employee("Xiong1", "JH1", false))
        list.add(Employee("Xiong2", "JH2", false))
        list.add(Employee("Xiong3", "JH3", true))
        list.add(Employee("Xiong4", "JH4", false))
        mEmployeeAdapter.addAll(list)
        mBinding.recyclerView.adapter = mEmployeeAdapter
    }
}

自定義屬性

在 Android 的開發中我們會用到很多的自定義 View,也就會有一些自定義屬性會讓我們用到,而這些屬性如果 Data Binding 不支持的話我們怎麼去做呢?

自動 set 方法

一種就是系統中有方法去 set 這個值,但是在 XML 中沒有屬性可以去設置,這樣的話 Data Binding 就會去自動尋找我們需要的 set 方法了。

<android.support.v4.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor ="@{@color/scrimColor}"/>

這樣的話 Data Binding 就會去調用 setScrimColor 方法去改變屬性的值。

Binding 方法

就是在一些屬性上,他所對應的 set 方法並不是以該屬性名爲後綴的,使用在 DataBinding 中沒有辦法順利的訪問到這個方法,所以這裏我們就需要對該屬性和對應的 set 方法進行映射,我們只需要在一個類之前加下 @BindingMethods 註解,就可以定義這樣的一個映射了。
Java 版本

@BindingMethods({
    @BindingMethod(
        type = ImageView.class,
        attribute = "android:tint",
        method = "setImageTintList"),
})

Kotlin 版本

@BindingMethods(
    BindingMethod(
        type = ImageView::class,
        attribute = "android:tint",
        method = "setImageTintList"
    )
)

這樣的話,DataBinding 就知道在 XML 文件中使用 android:tint 的屬性對應的是應該調用 setImageTintList 方法了。

BindingAdapter

假如我們完全自定義一個 View,我們有自己的屬性和方法,我們應該怎麼做呢,這裏就要使用 BindingAdapter 來實現了。
首先就要定義一個適配器,來進行方法的實現:
DemoBindingAdapter.kt

package com.xjh.databinding.expression

import android.databinding.BindingAdapter
import android.graphics.drawable.Drawable
import android.widget.ImageView
import com.bumptech.glide.Glide

class DemoBindingAdapter {

    companion object {
        @BindingAdapter("app:imageUrl", "app:placeholder")
        @JvmStatic
        fun loadImageFromUrl(view: ImageView, url: String, drawable: Drawable) {
            Glide.with(view.context).load(url).placeholder(drawable).into(view)
        }
    }
}

這樣就可以利用這兩個屬性傳遞相關變量,然後完成對圖片進行展示。
activity_expression.xml

<?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="employee"
            type="com.xjh.databinding.Employee" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="150dp"
            android:layout_height="150dp"
            app:imageUrl="@{employee.avatar}"
            app:placeholder="@{@drawable/ic_launcher}" />

    </LinearLayout>
</layout>

ExpressionActivity.kt

package com.xjh.databinding.expression

import android.databinding.DataBindingUtil
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import com.xjh.databinding.Employee
import com.xjh.databinding.R
import com.xjh.databinding.databinding.ActivityExpressionBinding

class ExpressionActivity : AppCompatActivity() {

    private lateinit var binding: ActivityExpressionBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_expression)
        val employee = Employee("X", "JH", false)
        employee.avatar = "https://avatar.csdnimg.cn/D/4/D/1_xjh_shin.jpg"
        binding.employee = employee
    }
}

BindingConversion

有些時候 Android 系統中幫我們實現了相關方法,但是傳入的參數並不是符合方法要求的,這時候我們就要將屬性進行轉換,這裏就要用到 BindingConversion 方法了。

<View
	android:layout_width="match_parent"
	android:layout_height="wrap_content"
	android:background="@{isError ? @color/red : @color/white}" />

Java 版本

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
	return new ColorDrawable(color);
}

Kotlin 版本

companion object {
    @BindingConversion
    @JvmStatic
    fun convertColorToDrawable(color: Int): ColorDrawable {
        return ColorDrawable(color)
    }
}

雙向綁定

假如我們想要監聽輸入框的值怎麼辦呢?這裏單純的用單向綁定就沒有辦法實現效果了,就要使用到 DataBinding 的雙向綁定,其實現在 DataBinding 實現雙向綁定其實很簡單,就是將 XML 文件中的 @ 改爲 @= 就實現了數據的雙向綁定。

<EditText
	android:id="@+id/userName"
	android:layout_width="match_parent"
	android:layout_height="wrap_content"
	android:layout_margin="10dp"
	android:hint="請輸入用戶名"
	android:text="@={viewModel.userInput}"
	android:textSize="14sp" />

綁定方式就是和單向綁定是一樣的,這裏就不再重複說明了。
但是雙向綁定並不是可以支持所有屬性,他主要是用於那些帶有額外事件的屬性,比如:text,checked,year,month,hour,rating,progress 等。

表達式鏈

重複表達式

當我們有很多的 View 需要用到同一個表達式運算的結果進行顯示的話,我們可能需要在這些 View 的屬性上重複的寫同一個表達式,這樣的話就導致代碼較爲累贅,其實我們就可以直接用之前計算好的屬性給他進行賦值,這樣的話就避免了多次的計算。

<?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>

        <import type="android.view.View" />

        <variable
            name="employee"
            type="com.xjh.databinding.Employee" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/avatar"
            android:layout_width="150dp"
            android:layout_height="150dp"
            android:visibility="@{employee.isFired() ? View.INVISIBLE : View.VISIBLE}"
            app:imageUrl="@{employee.avatar}"
            app:placeholder="@{@drawable/ic_launcher}" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{employee.firstName}"
            android:visibility="@{avatar.visibility}" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{employee.lastName}"
            android:visibility="@{avatar.visibility}" />

    </LinearLayout>
</layout>
隱式更新

如果我們需要綁定兩個 View,一個 View 的樣式改變依賴於另一個 View 的結果,這樣的話我們就需要去監聽這個 View 的值,然後手動去改變另一個 View,在 Data Binding 中我們就直接使用隱式更新就可以了。

<?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>
        <import type="android.view.View" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        
        <CheckBox
            android:id="@+id/seeAds"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="@{seeAds.checked ? View.INVISIBLE : View.VISIBLE}" />
    </LinearLayout>
</layout>

動畫

當有一個 View 要隨着選擇的狀態進行顯示的時候,如果我們直接進行刷新的話就會讓整個用戶的體驗很差,所以要使用動畫的效果來優化整個體驗,而 DataBinding 已經幫我們實現了動畫效果,只需要我們實現 OnRebindCallback 回調就可以進行實現了。
activity_animation.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <import type="android.view.View" />

        <variable
            name="presenter"
            type="com.xjh.databinding.animation.AnimationActivity.Presenter" />

        <variable
            name="showImage"
            type="boolean" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <ImageView
            android:layout_width="150dp"
            android:layout_height="150dp"
            android:src="@drawable/ic_launcher"
            android:visibility="@{showImage ? View.VISIBLE : View.GONE}" />

        <CheckBox
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onCheckedChanged="@{presenter.onCheckedChanged}"
            android:text="show Image" />

    </LinearLayout>
</layout>

AnimationActivity.kt

package com.xjh.databinding.animation

import android.databinding.DataBindingUtil
import android.databinding.OnRebindCallback
import android.os.Build
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.annotation.RequiresApi
import android.transition.TransitionManager
import android.view.View
import android.view.ViewGroup
import com.xjh.databinding.R
import com.xjh.databinding.databinding.ActivityAnimationBinding

class AnimationActivity : AppCompatActivity() {

    private lateinit var binding: ActivityAnimationBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_animation)
        binding.presenter = Presenter()
        binding.addOnRebindCallback(object : OnRebindCallback<ActivityAnimationBinding>() {
            @RequiresApi(Build.VERSION_CODES.KITKAT)
            override fun onPreBind(binding: ActivityAnimationBinding?): Boolean {
                val view = binding?.root as ViewGroup
                TransitionManager.beginDelayedTransition(view)
                return true
            }
        })
    }


    inner class Presenter {
        fun onCheckedChanged(view: View, isChecked: Boolean) {
            binding.showImage = isChecked
        }
    }
}


項目GitHub地址:傳送門

發佈了329 篇原創文章 · 獲贊 117 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章