安卓開發學習之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<String>"/>
<!-- <>括號要轉義,<改成< >改成> -->
<variable
name="maps"
type="ObservableArrayMap<String, String>"/>
<variable
name="arrays"
type="android.databinding.ObservableArrayList<String>"/>
<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裡的操作,彷彿是在運算元組,此刻為了區分,我們可以給鍵加上雙引號,@{}外面換成單引號。
再者,就是<>的轉義了,把<改成< >改成>才能編譯通過。
最後,引用時,要把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程式碼裡比較合適。