1. 程式人生 > >安卓開發學習之dataBinding的學習使用

安卓開發學習之dataBinding的學習使用

背景

dataBinding資料繫結是谷歌推出的基於觀察者模式的資料和頁面內容的繫結,擁有廣闊的應用前景。

 

使用

使能

dataBinding使能只需要在module的gradle檔案里加上這麼一段話

android {
    ...

    dataBinding {
        // 使能dataBinding
        enabled = true
    }

    ...
}

就可以了

 

建立資料實體

官方推薦的資料實體是javaBean,先讓這個類繼承自BaseObservable,然後在需要和佈局內容繫結的屬性的get方法前,加上@Bindable註解,以及set方法裡,屬性更改後,呼叫notifyPropertyChanged()方法。

ppackage applicationmanager.com.example.a123.studyofdatabinding.javaBean;

import android.databinding.BaseObservable;
import android.databinding.Bindable;

import applicationmanager.com.example.a123.studyofdatabinding.BR;

public class Person  extends BaseObservable {
    private String name;
    private int age;
    private String url;

    public Person(String name, int age, String url) {
        this.name = name;
        this.age = age;
        this.url = url;
    }

    @Bindable
    // 在需要觀察的屬性對應的get方法上加Bindable註解
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
        // BR相當於R,name是其屬性對應的id。呼叫此方法進行實時顯示
    }

    @Bindable
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
        notifyPropertyChanged(BR.age);
    }

    @Bindable
    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
        notifyPropertyChanged(BR.url);
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("Person{");
        sb.append("name='").append(name).append('\'');
        sb.append(", age=").append(age);
        sb.append(", url='").append(url).append('\'');
        sb.append('}');
        return sb.toString();
    }
}

這樣,一個可觀察的javaBean就建立完了

 

佈局內容

最大的變化感覺還是在佈局方面,dataBinding把原來的根標籤換成了<layout>標籤,下面有<data>和真正的根佈局。

因此,要把需要繫結的資料宣告到<data>資料域裡

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

    <!-- 資料區包裹在<data>標籤下 -->
    <data>

        <variable
            name="person"
            type="applicationmanager.com.example.a123.studyofdatabinding.javaBean.Person"/>
        <!-- 宣告要用到的變數,名字和型別 -->
        
    </data>

    <!-- 原來的佈局 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{person.name + @string/separator + person.age}" />
        <!-- 獲取屬性 -->

        <Button
            android:id="@+id/increment"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="點選改變"
            />

    </LinearLayout>
</layout>

資料域裡的變數要冠以variable標籤,裡面寫上變數的名字和型別。而後在佈局裡用的時候用@{}來對變數進行引用。引用時,{}裡不能直接出現要顯示的字串,如果要顯示固定的文字,必須在資原始檔(res/values/strings.xml)裡定義,比如這樣

<resources>
    <string name="app_name">StudyOfDataBinding</string>
    <string name="separator">-------</string>
</resources>

為了展示資料的實時變化,我加了一個按鈕,點選一下person的年齡+1

 

Activity裡進行繫結

方才是設計了一個javaBean實體,而後在佈局檔案裡聲明瞭一個變數,下面就是把它們倆繫結到一起的過程

package applicationmanager.com.example.a123.studyofdatabinding;

import android.databinding.ViewDataBinding;
import android.os.Bundle;

import applicationmanager.com.example.a123.studyofdatabinding.javaBean.Person;

public class MainActivity extends Activity {
 
    private ViewDataBinding mBinding;
    private Person person;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        // 載入繫結佈局

        person = new Person("Jason", 24, "http://p0.so.qhmsg.com/bdr/_240_/t01825773612648be9f.jpg");
        mBinding.setVariable(BR.person, person);

        findViewById(R.id.increment).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                person.setAge(person.getAge() + 1);
            }
        });

    }

}

步驟如上所示,利用DataBindingUtil的setContentView替換原來的setContentView,來把當前Activity和指定的佈局檔案繫結在一起,同時獲取一個ViewDataBinding物件,而後利用此物件,把佈局裡的person變數和activity裡的person物件繫結在一起。

執行之後,效果如圖所示

 

 

這樣,最基本的資料繫結就實現了。

ObservableInt等

如果需要使用一個可觀察的基本資料型別或內建物件,可以使用DataBinding裡的ObservableInt、ObservableString、ObservableArrayMap等,使用方法和繫結javaBean相似,但亦有不同。

佈局檔案裡宣告

這時宣告的話需要在資料域裡導包,因為這些類不在我們的工程裡

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

    <!-- 資料區包裹在<data>標籤下 -->
    <data>
        <import type="android.databinding.ObservableArrayMap"/>
        <import type="android.databinding.ObservableInt"/>
        <import type="android.databinding.ObservableField"/>
        <import type="android.databinding.ObservableArrayList"/>
        <!-- 導包 -->

        <variable
            name="person"
            type="applicationmanager.com.example.a123.studyofdatabinding.javaBean.Person"/>
        <!-- 宣告要用到的變數,名字和型別 -->
        <variable
            name="count"
            type="ObservableInt"/>
        <variable
            name="info"
            type="ObservableField&lt;String&gt;"/>
        <!-- <>括號要轉義,<改成&lt; >改成&gt; -->
        <variable
            name="maps"
            type="ObservableArrayMap&lt;String, String&gt;"/>
        <variable
            name="arrays"
            type="android.databinding.ObservableArrayList&lt;String&gt;"/>
        <variable
            name="index"
            type="ObservableInt"/>
        
    </data>

    <!-- 原來的佈局 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{person.name + @string/separator + person.age}" />
        <!-- 獲取屬性 -->

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf((Integer)count)}"/>
        <!-- 整數必須先轉成Integer,再轉成String -->

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{info}"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text='@{maps["name"]}'/>
        <!-- 根據鍵取值 -->

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text='@{maps["description"]}'/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{arrays[index]}"/>
        <!-- 遍歷陣列 -->
        <ListView
            android:id="@+id/list_people"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

        <Button
            android:id="@+id/increment"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="點選改變"
            />
        <!-- 繫結監聽,自動傳入view -->
        <Button
            android:id="@+id/decrement"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="點選減少"
            />
    </LinearLayout>
</layout>

其中對映根據鍵取值,很像python裡的操作,彷彿是在運算元組,此刻為了區分,我們可以給鍵加上雙引號,@{}外面換成單引號。

再者,就是<>的轉義了,把<改成&lt; >改成&gt;才能編譯通過。

最後,引用時,要把ObservableInt轉成字串,就像上面的

android:text="@{String.valueOf((Integer)count)}"

 

activity裡繫結

為了實現可觀察陣列和對映,我事先定義了一組資料

    private String[] names = new String[]{
            "A", "B", "C", "D"
    };
    private String[] descriptions = new String[]{
      "aa", "bb", "cc", "dd"
    };

然後在定義一堆Observable物件,其中最後的indexObservable是陣列元素的下標

    private ObservableInt countObservable = new ObservableInt();
    private ObservableField<String> infoObservable = new ObservableField<>();
    private ObservableArrayMap<String, String> mapObservable = new ObservableArrayMap<>();
    private ObservableArrayList<String> arraysObservable = new ObservableArrayList<>();
    private ObservableInt indexObservable = new ObservableInt();

在onCreate()和佈局裡的變數的id進行繫結

        mBinding.setVariable(applicationmanager.com.example.a123.studyofdatabinding.BR.person, person);
        mBinding.setVariable(applicationmanager.com.example.a123.studyofdatabinding.BR.count, countObservable);
        mBinding.setVariable(applicationmanager.com.example.a123.studyofdatabinding.BR.info, infoObservable);
        mBinding.setVariable(applicationmanager.com.example.a123.studyofdatabinding.BR.maps, mapObservable);
        mBinding.setVariable(applicationmanager.com.example.a123.studyofdatabinding.BR.arrays, arraysObservable);
        mBinding.setVariable(BR.index, indexObservable);

而後,開啟兩個執行緒,進行資料的改變。為了實現陣列的自行遍歷,單門為index開了一個執行緒。

對於Observable物件,基本就是set設值,get取值。對於對映和陣列,set/get方法和普通的對映陣列一樣,其中陣列也可以給指定下標的元素設值。

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    String name = names[i % names.length];
                    String decription = descriptions[i % descriptions.length];

                    countObservable.set(i + 1); // set方法設值
                    infoObservable.set("當前計數器的值是:" + (i + 1) + ";執行緒名:" + Thread.currentThread().getName());
                    mapObservable.put("name", name);
                    mapObservable.put("description", decription);
                    arraysObservable.add(name);
//                    arraysObservable.set(0, name);

                    try {
                        Thread.sleep(3 * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    indexObservable.set(i);
                    try {
                        Thread.sleep(3 * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

執行效果如圖所示

時間間隔有點長,但可以看到實現了資料的自行更新。

 

ListView的資料適配

其實要想顯示資料列表的話,還是推薦用ListView而不是在佈局裡宣告一個數組,畢竟ListView是單門用來顯示列表的

main佈局裡宣告

在activity佈局裡新增一個ListView

        <ListView
            android:id="@+id/list_people"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

建立列表項的佈局檔案

然後給列表每一項建立通用的佈局檔案,這個佈局檔案要採用dataBinding格式

<?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="personItem"
            type="applicationmanager.com.example.a123.studyofdatabinding.javaBean.Person"/>
    </data>
    <LinearLayout
        android:orientation="horizontal" android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{@string/personName + personItem.name}"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{@string/separator + @string/personAge + String.valueOf(personItem.age)}"/>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            app:imageUrl = "@{personItem.url}"
            android:layout_marginLeft="40dp"/>
    </LinearLayout>
</layout>

就是從左往右顯示名字、年齡和圖片。對於圖片,我也是利用dataBinding的方法來實現載入的,一會兒再說它。

建立介面卡

ListView顯示資料當然要建立介面卡

package applicationmanager.com.example.a123.studyofdatabinding.adapter;

import android.content.Context;
import android.databinding.DataBindingUtil;
import android.databinding.ViewDataBinding;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

import java.util.LinkedList;

import applicationmanager.com.example.a123.studyofdatabinding.BR;
import applicationmanager.com.example.a123.studyofdatabinding.R;
import applicationmanager.com.example.a123.studyofdatabinding.javaBean.Person;

public class MyListViewBindingAdapter extends BaseAdapter {
    private LinkedList<Person> people;
    private Context mContext;

    public MyListViewBindingAdapter(LinkedList<Person> people, Context mContext) {
        this.people = people;
        this.mContext = mContext;
    }

    @Override
    public int getCount() {
        return people.size();
    }

    @Override
    public Object getItem(int i) {
        return people.get(i);
    }

    @Override
    public long getItemId(int i) {
        return i;
    }

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        ViewDataBinding binding = null;
        if (view == null) {
            Person person = people.get(i);
            if (person != null) {
                binding = DataBindingUtil.inflate(LayoutInflater.from(mContext), R.layout.list_item, viewGroup, false);
                binding.setVariable(BR.personItem, person);
            }
        } else {
            binding = DataBindingUtil.getBinding(view);
            // return view;
        }

        return binding.getRoot();
    }
}

和傳統介面卡唯一的區別來自getView()方法。

如果是第一次顯示這一項,入參view必然是空,那麼我們就獲取people列表裡第i個person,載入列表項的佈局檔案,獲取ViewDataBinding物件,並利用此物件把person物件和佈局中的personItem繫結起來。

如果不是第一次顯示這一項,其實可以直接返回view了,因為此時view肯定不是空。但如果要對view資料改變繫結的話,可以通過DataBindingUtil的getBinding()方法,獲取view的資料繫結物件,然後重新setVariable。

最後返回binding.getRoot(),也就是繫結的view。

但是在第一次顯示的時候,不要直接返回view,因為DataBindingUtil.inflate()方法並沒有給view賦值,view還是null

Activity裡初始化ListView

初始化很常規

package applicationmanager.com.example.a123.studyofdatabinding;

import android.app.Activity;

import android.databinding.ViewDataBinding;
import android.os.Bundle;

import android.widget.ListView;

import applicationmanager.com.example.a123.studyofdatabinding.adapter.MyListViewBindingAdapter;

public class MainActivity extends Activity {
   
    private ViewDataBinding mBinding;

    private ListView mListView;
    private MyListViewBindingAdapter mAdapter;
    private LinkedList<Person> mPeole = new LinkedList<>();

    private String[] names = new String[]{
            "A", "B", "C", "D"
    };


    private String[] urls = new String[]{
        "http://p5.so.qhimgs1.com/bdr/_240_/t0158432ac9d02c74bb.jpg",
        "http://p4.so.qhmsg.com/bdr/_240_/t015300f4f65dc07f89.jpg",
        "http://p3.so.qhimgs1.com/bdr/_240_/t014bbc3f5ab3755383.jpg",
        "http://p0.so.qhimgs1.com/bdr/_240_/t017ee2f5c7e5fcef90.jpg",
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...

        mListView = findViewById(R.id.list_people);
        mAdapter = new MyListViewBindingAdapter(mPeole, this);
        mListView.setAdapter(mAdapter);

        for (int i = 0; i < 10; i++) {
            String name = names[i % names.length];
            int age = i + 1;
            String url = urls[i % urls.length];
            mPeole.add(new Person(name, age, url));
        }

        ...
    }
}

執行的結果如圖所示

可見ListView的適配也完成了。下面,我們既就看看dataBinding是怎麼載入圖片的

 

圖片的載入

以上面的ListView中的圖片為例,我們在佈局檔案裡自定義圖片載入的方法app:imageUrl

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            app:imageUrl = "@{personItem.url}"
            android:layout_marginLeft="40dp"/>

為了實現這個app:imageUrl,我們再定義一個Monitor類,裡面進行資料繫結方面的排程。

package applicationmanager.com.example.a123.studyofdatabinding.monitor;

import android.databinding.BindingAdapter;
import android.net.Uri;
import android.view.View;

import com.bumptech.glide.Glide;

import applicationmanager.com.example.a123.studyofdatabinding.R;
import applicationmanager.com.example.a123.studyofdatabinding.myInterface.IActivity;

public class Monitor {
    private IActivity mActivity;


    public Monitor(IActivity mActivity) {
        this.mActivity = mActivity;
    }


    // 繫結方法,對應app:imageUrl
    @BindingAdapter("imageUrl")
    public static void loadImage(ImageView view, String url) {
        Glide.with(view.getContext().getApplicationContext()).load(Uri.parse(url)).into(view);
    }
}

通過@BindingAdapter註解,就實現了佈局檔案裡的方法和java裡的方法的繫結,引數都是自定義的,但要注意,返回值最好是void,而且引數要合理,比如設定imageView的url,入參定義為view和url就可以了,這樣也有利於高內聚。

另外,java方法一定要是靜態的,如果不設定成靜態的,也可以,這樣就要利用dataBinding的component來實現,感覺component用處不是很大,而且不如static靈活,特別是要呼叫recreate()重新構造activity實現資料的變化,讓我覺得不是太好用,這裡我貼出一篇關於使用component的博文,供大家參考,連結

這樣,我們就實現了圖片的載入。如果要改變圖片,只需要在MainActivity裡設定people列表裡的某一項的url即可。

 

方法的繫結

方才,我們看到了@BindingAdapter註解,可以實現佈局檔案裡的方法和java方法的繫結。說到方法的繫結,最常用的是佈局屬性的統一更改或點選事件的統一排程。

佈局屬性的更改

這一點和方才的imageUrl類似,只不過註解傳參要傳指定的屬性名字比如android:layout_marginLeft。這樣只要在佈局檔案裡設定了android:layout_marginLeft屬性,就會調到我們的setLeftMargin()方法裡,

    // 安卓屬性setter
    @BindingAdapter("android:layout_marginLeft")
    public static void setLeftMargin(View view, int margin) {
        ViewGroup.LayoutParams layoutParams= view.getLayoutParams();
        if (layoutParams instanceof LinearLayout.LayoutParams) {
            ((LinearLayout.LayoutParams) layoutParams).leftMargin = margin + 5;
        } else if (layoutParams instanceof FrameLayout.LayoutParams) {
            ((FrameLayout.LayoutParams) layoutParams).leftMargin = margin + 5;
        } else if (layoutParams instanceof RelativeLayout.LayoutParams) {
            ((RelativeLayout.LayoutParams) layoutParams).leftMargin = margin + 5;
        }

        view.setLayoutParams(layoutParams);
    }

例子裡,只是讓左外間距多了5畫素

點選事件的統一排程

這個需要在佈局檔案裡傳入monitor物件

        <variable
            name="monitor"
            type="applicationmanager.com.example.a123.studyofdatabinding.monitor.Monitor"/>

然後在View(比如Button)的onClick裡,傳入monitor的onClick

        <Button
            android:id="@+id/increment"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="點選改變"
            android:onClick="@{monitor::onClick}"
            />
        <!-- 繫結監聽,自動傳入view -->
        <Button
            android:id="@+id/decrement"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="點選減少"
            android:onClick="@{monitor::onClick}"/>

注意引用方法時要用雙冒號

最後我們就實現monitor的onClick,注意,此時不用加註解

    public void onClick(View view) {
        if (mActivity == null) {
            return;
        }

        int old = mActivity.getData();
        switch(view.getId()){
            case R.id.increment:
                old++;
                break;
            case R.id.decrement:
                old--;
                break;
        }
        mActivity.onDataChanged(old);
    }

這裡,我用了MVP模式在實現資料的更新。mActivity是介面物件,實現在Activity裡

    @Override
    public void onDataChanged(int data) {
        mPeole.get(0).setAge(data);
    }

    @Override
    public int getData() {
        return mPeole.get(0).getAge();
    }

執行效果如圖所示,注意看listView第一項的年齡變動

 

結語

dataBinding的常用用法就如上所示了,還有一些型別強轉,佈局裡的運算子什麼的,我不是很推薦使用,首先是AS對於xml檔案的除錯定位能力有限,再者我覺得還是讓佈局檔案只處理資料顯示的好,要把邏輯處理什麼的放在java程式碼裡比較合適。