編輯:關於Android編程
vci7uvMgc2V0o6xkdWFuZywsLCwsIMv509C1xNa1vs3X1Ly61bnP1tTauMPT0LXEtdi3vcHLoaM8L3A+DQo8cD5EZW1vOiA8YSBocmVmPQ=="https://github.com/Afra55/DataBindingApplication">https://github.com/Afra55/DataBindingApplication
我自己認為,先看Demo,然後帶著疑問 去閱讀,會有一種解惑的情懷。>~<
原文:https://developer.android.com/intl/zh-cn/tools/data-binding/guide.html
本文介紹了如何使用數據綁定庫
數據綁定庫提供了靈活性和廣泛的兼容性-這是一個支持庫,所以你可以在所有的android平台上使用它。這個庫需要Android Plugin for Gradle 1.5.0-alpha1或更高版本。
在 Android SDK中 下載支持庫。
在 build.gradle 文件中添加 dataBinding 元素。
android {
....
dataBinding {
enabled = true
}
}
此外,確保使用的是 Android Studio,
數據綁定布局和普通的布局稍有不同,有一個標簽為 layout 的根布局,其次是元素 data 和一個視圖根元素。例子:
variable 的寫法如下, type 的值指定完整路徑的 java 類:
在布局屬性參數中的表達式都要使用 “@{}”
語法,在這裡,TextView 的文本設置為 user 的 firstName 屬性的值:
現在假設你有了一個 User 對象:
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
這種類型的對象有一個永遠不會改變的數據。這是應??用中是常見的,以具有被寫入一次數據並隨後從不改變。另外,也可以使用一個JavaBeans對象:
public class User {
private final String firstName;
private final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
}
從數據綁定的角度來看,這兩個類是等價的。TextView
的 android:text
屬性中使用的表達式 @{user.firstName}
會訪問前面那個 User 類的 fristName 字段,或者後面那個 User 類的 getFirstName()
方法。或者,如果firstName()
方法存在,它也將被解析為的 firstName()
。
默認情況下,綁定類將基於布局文件的名稱來產生,上面的布局文件的名字是 main_activity.xml
通常情況下生成的類是 MainActivityBinding
, 這個類包含所有從布局屬性到布局視圖的綁定 (e.g. the user variable),並且知道如何分配綁定表達式的值。下面是一個最簡單的綁定的例子:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
User user = new User("Test", "User");
binding.setUser(user);
}
這樣,就完成了綁定,運行程序,就可以在 ui 上看到 user 對象的數據了。另外,還可以這樣獲取視圖:
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
如果你正在使用數據綁定在一個ListView或RecyclerView的適配器中,最好這麼用:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
事件的綁定可能直接用來處理方法,例如 android:onClick
可以直接給 Activity 分配一個方法。事件的屬性名是由監聽的方法分配的,例如View.OnLongClickListener
有一個方法onLongClick()
,因此這個事件的屬性是 android:onLongClick
。
分配一個事件處理機制,就是在表達式中調用這個方法名。例如,如果你的數據對象有兩個方法:
public class MyHandlers {
public void onClickFriend(View view) { ... }
public void onClickEnemy(View view) { ... }
}
下面的例子就是通過表達式給 view 分配了個點擊監聽的事件:
一些特殊的點擊事件需要其他的屬性來避免與 android:onClick 的沖突,已經創建了下面的屬性,以避免這樣的沖突:
import
元素允許你的布局文件引用類,就像在java文件中引用一樣:
現在,View 就可以在表達式中使用了:
當你想引入自己寫的一個 View 類的時候,你可以使用“alias”起個別名:
現在,Vista 用來引用 com.example.real.estate.View,View 用來引用 android.view.View。引用類型也可能在變量的type裡使用:
import類型可能在要引用靜態方法的時候使用:
…
就好像 Java, java.lang.* ,自動導入。
可以在 data 元素中使用任意的 variable 元素。一個 variable 元素描述了一個可能在布局文件中的綁定表達式中使用的屬性。
變量的 type 是在編譯時檢查,因此如果一個變量實現了 Observable 或者是一個 Observable 集合,應在 type 中進行反映。如果變量是一個沒有實現 Observable* 接口的基類或者接口,變量將不會被發現。
當有不同配置(如橫向或縱向)的不同布局文件,變量將被合並。有這些布局文件之間一定不能有相互沖突的變量定義。
生成的綁定類會為每一個描述的變量生成一個 getter 和 setter 方法。在 setter 被調用之前,變量將采取默認的 java 值——null(引用類型),0(int),false(布爾類型)等。
一個叫 context 的特殊變量會在綁定表達式中需要的時候生成,這是 context 變量的 value 值是 Context (從根視圖的 getContext 方法中得到的)。這個 context 變量會被同名的 顯示聲明 變量覆蓋掉。
默認情況下,綁定類的名字是基於布局文件的名字生成的。將第一個字母大寫,去掉下劃線將接下來的字母接上並大寫第一個字母,最後跟上“Binding”。例如,布局文件 contact_item.xml
會生成 ContactItemBinding
,如果這個模塊的包名是 com.example.my.app
,那麼生成的綁定類會放置在 com.example.my.app.databinding
。
綁定類可以通過改變 data 元素下的 class
屬性來重命名或者放置到其他包裡。例如:
...
這樣綁定類就被重命名為 ContactItem
。如果綁定類要在模塊包下的其他package下生成,則要加前綴 “.”
。例如:
...
這種情況下,綁定類會放到 模塊包.ContactItem
下即 com.example.my.app.ContactItem
包下。如果要完全自定義包名,則提供完整路徑即可:
...
變量可能會被傳入到引用的布局中,這麼做:
included 的布局必須要有 user 變量,例如:
綁定數據不支持 merge 元素的布局引用,以下布局是不支持的:
這些表達式看起來很像java表達式,下面這些都是一樣的:
數學計算+ - / *%
連接字符串 +
邏輯運算 && ||
二進制運算 & | ^
單目運算 + - ! ~
移位運算 >> >>> <<
比較運算 == > < >= <=
instanceof
分組 ()
常量 character, String, numeric, null
造型運算符(Cast) 方法調用 字段訪問 訪問數組 []
三元運算 ?:
例如:
ndroid:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
空合並運算符(??
),如果左面的不為 null 則選擇左邊的,否則選擇右邊。
android:text="@{user.displayName ?? user.lastName}"
它等價於下面的公式:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
android:text="@{user.lastName}"
#### 避免 NullPointerException
生成的數據綁定代碼會自動檢查空值和避免空指針異常。例如表達式@{user.name}
,如果 user 是 null, user.name
就會被指定默認值 (null)。如果引用 user.age
,由於 age 是 int 類型,所以指定默認值為 0。
常見的集合:arrays, lists, sparse lists, 和 maps,可能使用 []
操作符來進行訪問。
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"
當使用單引號包含屬性值時,在表達式中使用雙引號是個簡易的方法來表示常量:
android:text='@{map["firstName"]}'
如果使用雙引號包含屬性值時,在表達式中使用單引號或者 "
表示常量:
android:text="@{map[`firstName`}"
android:text="@{map["firstName"]}"
可以使用普通的訪問資源的表達式,如下事例:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
當格式化 string 或者 plurals 需要提供參數:
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
當 plurals 有多個參數時,每個參數都要傳入:
Have an orange
Have %d oranges
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
下面一些資源的顯示調用跟普通的調用略有不同:
當綁定了 java 對象後,再對他進行修改並不會讓 UI 更新,這裡有三個不同的數據通知機制:
Observable objects observable fields observable collection當它們中的一個觀察到綁定在 UI 上的數據對象被修改時,UI 就會自動更新。
一個類實現 Observable
接口允許綁定對象綁定一個 listener 來監聽對象所有的 屬性變化。
為了讓開發變得簡單,有個名為 BaseObservable
的基類用來實現 listener 的注冊機制,實現了這個基類的數據類會一直負責通知屬性的變化,通知的方式是通過指定 getter 方法 Bindable
注解和在 setter 方法的通知來實現的。
private static class User extends BaseObservable {
private String firstName;
private String lastName;
@Bindable
public String getFirstName() {
return this.firstName;
}
@Bindable
public String getLastName() {
return this.lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}
在編譯的時候注解 Bindable
會在 BR 類中生成一個屬性的入口,以便在 setter 方法中通知。BR 類會在模塊包下生成。
Observale 類的創建會有一小部分的准備工作,因此開發者想要節約時間或者只監聽少部分的屬性,這時可以使用 ObservableField 和它的兄弟們 ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, 和 ObservableParcelable。ObservableFields 是一個獨立的自給自足的 ObservableFields 對象,擁有一個獨立的字段。在訪問原始數據時避免了 boxing 和 unboxing 操作。如下事例,在數據類中創建 public final 字段:
public class Son {
public final ObservableField firstName =
new ObservableField<>();
public final ObservableField lastName =
new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
下面是訪問和設置值的例子:
son.firstName.set("Child");
int age = son.age.get();
一些應用使用更多的動態結構來保存數據。Observable 集合允許使用 key-value 的形式來保存數據。 ObservableArrayMap
在當 key 是個引用類型(比如 string)的時候非常有用。
ObservableArrayMap user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
在 布局中, 使用 string key 來訪問數據:
…
ObservableArrayList
在key 是 數字的時候使用,和 ArrayList 類似:
ObservableArrayList
在布局中,通過索引來訪問數據:
…
生成的綁定類用來鏈接變量和布局中的視圖。上文已經說過了,綁定類的名字和存儲的位置都是可以自定義的。綁定類都會繼承一個父類 ViewDataBinding。
在布局中的視圖和表達式綁定之後會很快生成綁定類。這裡有幾種綁定布局的方法,其中最常見的是使用綁定類中的靜態方法,只需調用 inflate 方法這一步即可填充布局和綁定布局:
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
如果只綁定布局,可以使用下面的方法:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
有時候綁定不能被提前知道,在這種情況下可以使用 DataBindingUtil 類(該類的一些方法與文中有不同,具體看源碼):
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
布局中每一個視圖的 ID 都會生成一個 public final 字段。綁定機制會在視圖層次提取視圖的 ID,這種方式來獲取視圖要比調用 findViewById 快。例如:
生成的綁定類會有下面的字段:
public final TextView firstName;
public final TextView lastName;
然後可以直接使用:
mBinding.lastName.setText("Afra55");
每一個變量都會有一個方法去訪問它。
會在綁定類中生成相應的 setter 和 getter 方法:
public abstract com.example.User getUser();
public abstract void setUser(com.example.User user);
public abstract Drawable getImage();
public abstract void setImage(Drawable image);
public abstract String getNote();
public abstract void setNote(String note);
ViewStubs 默認是不顯示的,加載時才會被其他布局替換而顯示出來。
ViewStub 本質上是不會出現在視圖層次的, 由於 Views 是final類型,所以 ViewStub綁定後就會轉換為 ViewStubProxy對象,讓開發著可以在 ViewStub 存在並被填充時訪問 ViewStub。
當填充另一個布局時,新布局的綁定也要被建立。因此, ViewStubProxy 一定要監聽 ViewStub 的 ViewStub.OnInflateListener
接口 和及時建立綁定。因此當綁定的建立完成時 ViewStubProxy 允許開發者設置 OnInflateListener 來回調。
舉例:
布局中添加 StubView:
test_viewstub.xml:
在代碼中引用:
// 設置布局加載監聽用來綁定數據,綁定後 viewStub 轉換成 ViewStubProxy
mBinding.viewstub.setOnInflateListener(new ViewStub.OnInflateListener() {
@Override
public void onInflate(ViewStub stub, View inflated) {
TestViewstubBinding viewDataBinding = DataBindingUtil.bind(inflated);
User user = new User("xx", "gg", true);
viewDataBinding.setUser(user);
}
});
// 顯示布局,Andoird studio會報紅,可能還麼支持,直接運行即可。
if (!mBinding.viewstub.isInflated()) {
mBinding.viewstub.getViewStub().inflate();
}
有時,一些特殊的綁定類不會被知道。例如,一個 RecyclerView.Adapter 裡的操作不允許任何布局知道綁定類。但是依舊會在 onBindViewHolder(VH, int).
裡分配綁定值。
在這個例子裡,所有RecyclerView 布局的綁定都會有一個 “item” 變量。BindingHolder
自己實現一個 getBinding
方法用來獲取 ViewDataBinding。
@Override
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}
這個例子需要去實現 RecyclerView.Adapter 。我自己實現了個例子:
主布局:
Item 布局:
<framelayout android:layout_height="wrap_content" android:layout_width="match_parent">
</framelayout>
ViewHolder:
/**
* Created by yangshuai in the 21:23 of 2016.06.01 .
*/
public class RecyclerViewHolder extends RecyclerView.ViewHolder {
private LayoutSexBinding mBinding;
public RecyclerViewHolder(View itemView) {
super(itemView);
mBinding = LayoutSexBinding.bind(itemView);
}
public ViewDataBinding getBinding() {
return mBinding;
}
}
Adapter:
/**
* Created by yangshuai in the 21:21 of 2016.06.01 .
*/
public class RecyclerViewAdapter extends RecyclerView.Adapter {
private Context mContext;
private List mUsers = new ArrayList<>();
public RecyclerViewAdapter(Context context, List users) {
mContext = context;
mUsers.addAll(users);
}
public void updata(List users) {
mUsers.addAll(users);
notifyDataSetChanged();
}
public void clear() {
mUsers.clear();
notifyDataSetChanged();
}
@Override
public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(mContext).inflate(R.layout.layout_sex, parent, false);
return new RecyclerViewHolder(view);
}
@Override
public void onBindViewHolder(RecyclerViewHolder holder, int position) {
holder.getBinding().setVariable(BR.user, mUsers.get(position));
holder.getBinding().executePendingBindings();
}
@Override
public int getItemCount() {
return mUsers.size();
}
}
代碼配置:
private void initRecyclerAdapter() {
List users = new ArrayList<>();
users.add(new User("unKnow"));
users.add(new User("man"));
users.add(new User("woman"));
users.add(new User("woman"));
users.add(new User("woman"));
users.add(new User("woman"));
users.add(new User("woman"));
users.add(new User("woman"));
mRecyclerViewAdapter = new RecyclerViewAdapter(this, users);
mBinding.recylerview.setLayoutManager(new LinearLayoutManager(this));
mBinding.recylerview.setItemAnimator(new DefaultItemAnimator());
mBinding.recylerview.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
mBinding.recylerview.setAdapter(mRecyclerViewAdapter);
}
效果圖:
當綁定數據有變化時,視圖的對應變化會有一段時間,所以當綁定要立刻強制執行時,使用 executePendingBindings()
方法即可。
可以在後台線程中改變一個數據模型(非集合)。數據綁定會本地化 變量/字段 用來避免並發問題。
不論什麼時候只要被綁定的值改變了,生成的綁定類都會調用 setter 方法來執行表達式並將結果展現在視圖上。數據綁定框架有幾種方式來自定義方法去設置值。
在布局文件中與綁定表達式對應的參數,比如 TextView 的 android:text 的 值是表達式得到的結果,則如果表達式返回的是 String 類型,則回去檢索 setText(String) 方法,如果表達式返回的是 int 類型,則數據綁定會去檢索 setText(int) 方法。所以一定要當心表達式所返回的結果類型。可以借助這種特性,來實現自定義的 setter。比如:
DrawerLayout 沒有 scrimColor 和 drawerListener 兩個參數,這兩個參數與數據綁定相關聯,則他們會自動去調用 方法 setScrimColor 和 setDrawerListener
方法去傳遞表達式的結果。
可以在自定義 view 中使用:
app:aaaa 和 app:bbbb 是自定義的方法:
/**
* Created by yangshuai in the 20:18 of 2016.06.02 .
*/
public class MTextView extends TextView {
public MTextView(Context context) {
super(context);
}
public MTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setBbbb(int color) {
setBackgroundColor(color);
}
public void setAaaa(String text) {
setText(text);
}
}
效果如下:
我們可以使用 注解 @bindingMethods 來重命名參數的 setter 方法,例如官方的這個例子 android:tint 它鏈接的方法是 setImageTintList(ColorStateList) 而不是 setTint:
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
不推薦這麼做。
一些參數需要自定義綁定邏輯。例如,沒有 setter 方法 與參數 android:paddingLeft 鏈接,只有 setPadding(left, top, right, bottom) 存在。
注解 @BindingAdapter 可以實現該參數被調用時,鏈接自定義方法:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
也可以有多個參數的自定義情況:
@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.with(view.getContext()).load(url).error(error).into(view);
}
這個綁定適配器loadImage會在 參數 imageUrl 是 string 類型和 error 是drawable 類型時被調用,並傳入這兩個參數的值。
綁定適配器會在 handler 持有老數據,因此方法可以 得到老數據和新數據進行比較,老數據的位置在新數據的前面:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
if (oldPadding != newPadding) {
view.setPadding(newPadding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
}
還有事件的監聽獲取只允許一個 Listener 來監聽:
@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
View.OnLayoutChangeListener newValue) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (oldValue != null) {
view.removeOnLayoutChangeListener(oldValue);
}
if (newValue != null) {
view.addOnLayoutChangeListener(newValue);
}
}
}
當一個 接口 有多個抽象方法時,一定要把他們分離,舉例:
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}
因為在布局表達式中改變其一就可能影響到另一個,所以上面這種情況有兩個方法的接口,要有三個綁定適配器,兩個參數各有一個方法,其次還有個整體改變的方法:
@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
setListener(view, null, attached);
}
@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
setListener(view, detached, null);
}
@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
final OnViewAttachedToWindow attach) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
final OnAttachStateChangeListener newListener;
if (detach == null && attach == null) {
newListener = null;
} else {
newListener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (attach != null) {
attach.onViewAttachedToWindow(v);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
if (detach != null) {
detach.onViewDetachedFromWindow(v);
}
}
};
}
final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
newListener, R.id.onAttachStateChangeListener);
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener);
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener);
}
}
}
android.databinding.adapters.ListenerUtil 類幫助跟蹤獲取之前的 Listenner。
當綁定表達式返回一個 Object 時,Object 會根據 鏈接的 setter 方法的輸入參數來轉換自己的類型。
例如:
userMap 返回了一個 Object,這個Object 會根據檢索到的方法 setText(CharSequene)自動轉變類型為 CharSequene。 但有時候還需要我們自己在表達式中進行轉換,看情況喽。
有些時候需要在特殊類型之間進行轉換,例如 background,下面的寫法是錯誤的:
background 參數的值應該是 Drawable,但是傳入的 color 是 integer類型。 int 類型的值應該被轉換成 ColorDrawable。
例如:
普通情況下是錯誤的,但是在 MTextView中加上靜態方法:
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
background 參數的值檢查到是 int 類型時就會調用 convertColorToDrawable 方法自動轉換 int 型到 ColorDrawable 從而運行成功。
下面這種情況是不允許的, 不允許混合類型:
數組和泛型,就像 Observable類一樣,可能在沒有錯誤的時候顯示錯誤,不用擔心直接運行即可。
可以使用下面的方法在綁定表達式沒有返回有效值時來顯示默認數據:
這個 TextView 就會默認顯示 ss 字符串。
這篇文章前前後後寫了有一個多月,對原文的一邊翻譯一邊也有了自己的見解,我覺得數據綁定庫是個很好的方向,希望 Google 能繼續完善,比如代碼的自動補全,錯誤提示等。
生活之於點點滴滴,積累以得源泉,祝你健康。
一. 再探Activity生命周期 為了研究activity的生命周期,簡單測試代碼如下。 package com.example.testactivity; impo
Android系統架構Linux內核層。Android系統是基於Linux 2.6內核的,這一層為Android設備的各種硬件提供了底層的驅動,如顯示驅動、音頻驅動、照相
一 IntentService介紹IntentService定義的三個基本點:是什麼?怎麼用?如何work?官方解釋如下://IntentService定義的三個基本點:
零、Gallery的使用回顧我們有時候在iPhone手機上或者Windows上面看到動態的圖片,可以通過鼠標或者手指觸摸來移動它,產生動態的圖片滾動效果,還可以根據你的點