專欄作者新書出版:Android App程式開發剖析 第三版(適用Android 8 Oreo與Android Studio 3)
Android 6 Tutorial 第六堂(1)Material Design – Theme 與 Transition << 前情
Android在發表Android 5 Lollipop的同時,也發表Material Design和許多新的API,其中用來取代ListView元件的RecylerView,對應用程式設計有比較大的影響。
一般的應用程式很常使用ListView元件,提供使用者一些資料的瀏覽與操作,它可以設計自己的項目畫面,加入需要的控制元件,讓ListView元件可以應付大部份的需求。不過ListView元件在處理比較大量的資料時,效率就會比較差一些。另外需要設計比較符合使用者操作經驗的介面時,ListView元件的實作方式也會變的非常複雜。
從Android 5 Lollipop、API Level 21開始提供的RecylerView元件,採用全新的Material Design設計,比ListView元件有更好的效率,也更符合使用者的操作經驗。這一章把記事應用程式的主畫面元件,採用ListView元件實作的記事資料瀏覽與操作畫面,改為RecylerView與Material Design的設計。接下來的修正幅度會比較大一些,所以會分為幾個階段,完成所有工作以後再執行測試。
把記事應用程式的主畫面改為RecylerView以後,跟原來的樣子差不多,不過在完成應用程式以後,使用者操作的回應上就會很不一樣了。
19-1 使用RecylerView元件
RecylerView元件在「android.support.v7.widget」套件下,應用程式需要使用RecylerView元件,必須加入需要的設定。開啟「Gradle Scripts -> build.gradle (Module: app)」,參考下列的片段,在「dependencies」區塊加入需要的設定:
dependencies {
...
compile 'com.android.support:recyclerview-v7:23.0.+'
}
加入上列的設定後,選擇功能表「Tools -> Android -> Sync Project with Gradle Files」,讓Android Studio執行相關的設定。
接下來修改應用程式的主畫面資源檔,開啟「res/layout/activity_main.xml」,參考下列的片段,把原來的ListView元件改為RecylerView元件:
<!-- 使用RecylerView元件 -->
<android.support.v7.widget.RecyclerView
android:id="@+id/item_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_margin="@dimen/default_margin"
android:dividerHeight="1sp"
android:background="@drawable/rectangle_drawable"
android:scrollbars="vertical" />
<!-- 移除原來的ListView元件
<ListView
android:id="@+id/item_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_margin="@dimen/default_margin"
android:dividerHeight="1sp"
android:background="@drawable/rectangle_drawable"
android:divider="@color/divider_color" />
-->
開啟「res\values\dimens.xml」,加入下列的尺寸資源:
<dimen name="divider_size">2sp</dimen>
為了讓畫面的操作可以符合RecyclerView與Material Design的設計,開啟「res/layout/single_item.xml」,參考下列的片段加入需要的設定:
<?xml version="1.0" encoding="utf-8"?>
<!-- 加入「android:background」的設定 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground">
<RelativeLayout ... >
...
</RelativeLayout>
<LinearLayout ... >
<TextView ... />
<TextView ... />
<!-- 加入分隔線,因為RecylerView不能像ListView一樣設定項目分隔線 -->
<View
android:layout_width="match_parent"
android:layout_height="@dimen/divider_size"
android:layout_alignParentBottom="true"
android:layout_marginRight="@dimen/divider_size"
android:background="@color/divider_color"/>
</LinearLayout>
</LinearLayout>
19-2 建立資料來源類別 – RecyclerView.Adapter
使用ListView元件時,通常會搭配「android.widget.ArrayAdapter」提供資料來源。RecyclerView元件使用「Adapter」為資料來源,在作法上也有很大的差異。例如「RecyclerView.ViewHolder」的使用,ListView元件也可以使用ViewHolder包裝畫面元件,不過並沒有強制規定,所以一般的作法都不會特別使用它。在實作提供給RecyclerView元件使用的資料來源時,就一定要使用RecyclerView.ViewHolder包裝每一個項目的畫面元件。
為了讓接下來的修改比較容易一些,保留ListView元件原來使用的「ItemAdapter」類別。在應用程式的主套件建立一個名稱為「ItemAdapterRV」的類別,這是提供給RecyclerView元件使用的資料來源類別。參考下列的程式碼完成這個類別的實作:
package net.macdidi.myandroidtutorial;
import android.graphics.drawable.GradientDrawable;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.util.List;
public class ItemAdapterRV extends RecyclerView.Adapter<ItemAdapterRV.ViewHolder>{
// 包裝的記事資料
private List<Item> items;
public ItemAdapterRV(List<Item> items) {
this.items = items;
}
@Override
public ItemAdapterRV.ViewHolder onCreateViewHolder(
ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(
R.layout.single_item, parent, false);
ViewHolder viewHolder = new ViewHolder(v);
return viewHolder;
}
@Override
public void onBindViewHolder(final ItemAdapterRV.ViewHolder holder, int position) {
final Item item = items.get(position);
// 設定記事顏色
GradientDrawable background = (GradientDrawable)
holder.typeColor.getBackground();
background.setColor(item.getColor().parseColor());
// 設定標題與日期時間
holder.titleView.setText(item.getTitle());
holder.dateView.setText(item.getLocaleDatetime());
// 設定是否已選擇
holder.selectedItem.setVisibility(
item.isSelected() ? View.VISIBLE : View.INVISIBLE);
}
@Override
public int getItemCount() {
return items.size();
}
public void add(Item item) {
items.add(item);
notifyItemInserted(items.size());
}
// 一定要使用ViewHolder包裝畫面元件
public class ViewHolder extends RecyclerView.ViewHolder {
protected RelativeLayout typeColor;
protected ImageView selectedItem;
protected TextView titleView;
protected TextView dateView;
protected View rootView;
public ViewHolder(View view) {
super(view);
typeColor = (RelativeLayout) itemView.findViewById(R.id.type_color);
selectedItem = (ImageView) itemView.findViewById(R.id.selected_item);
titleView = (TextView) itemView.findViewById(R.id.title_text);
dateView = (TextView) itemView.findViewById(R.id.date_text);
rootView = view;
}
}
}
19-3 完成主畫面元件
接下來剩下主畫面元件的修正工作,因為記事資料從ListView換成RecylerView元件,有許多需要修改的地方,你也可以經由修改的過程,瞭解它們的差異。開啟「MainActivity」,參考下列的程式片段,修改下列的欄位變數:
// 移除原來的ListView元件
//private ListView item_list;
// 加入下列需要的元件
private RecyclerView item_list;
private RecyclerView.Adapter itemAdapter;
private RecyclerView.LayoutManager rvLayoutManager;
private TextView show_app_name;
// 移除原來的ItemAdapter
//private ItemAdapter itemAdapter;
完成上列的修改以後,程式碼會產生很多錯誤,接下來會分成幾個段落完成所有修改的工作。首先找到「onCreate」方法,參考下列的程式片段,依照註解的說明執行修改的工作:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
processViews();
// 移除註冊監聽事件的工作,要移到下面執行
// processControllers();
itemDAO = new ItemDAO(getApplicationContext());
if (itemDAO.getCount() == 0) {
itemDAO.sample();
}
items = itemDAO.getAll();
// 移除原來ListView元件執行的工作
//itemAdapter = new ItemAdapter(this, R.layout.single_item, items);
//item_list.setAdapter(itemAdapter);
// 執行RecyclerView元件的設定
item_list.setHasFixedSize(true);
rvLayoutManager = new LinearLayoutManager(this);
item_list.setLayoutManager(rvLayoutManager);
// 在這裡執行註冊監聽事件的工作
processControllers();
}
同樣在「MainActivity」,找到「processViews」方法,參考下列程式片段修改原來的程式碼:
private void processViews() {
// 把ListViewe改為RecyclerView
item_list = (RecyclerView)findViewById(R.id.item_list);
show_app_name = (TextView) findViewById(R.id.show_app_name);
}
同樣在「MainActivity」,找到「processControllers」方法,先移除方法中所有的程式碼,參考下列程式片段完成這個方法的實作:
private void processControllers() {
// 實作ItemAdapterRV類別,加入註冊監聽事件的工作
itemAdapter = new ItemAdapterRV(items) {
@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
super.onBindViewHolder(holder, position);
// 建立與註冊項目點擊監聽物件
holder.rootView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 讀取選擇的記事物件
Item item = items.get(position);
// 如果已經有勾選的項目
if (selectedCount > 0) {
// 處理是否顯示已選擇項目
processMenu(item);
// 重新設定記事項目
items.set(position, item);
} else {
Intent intent = new Intent(
"net.macdidi.myandroidtutorial.EDIT_ITEM");
// 設定記事編號與記事物件
intent.putExtra("position", position);
intent.putExtra("net.macdidi.myandroidtutorial.Item", item);
// 依照版本啟動Acvitity元件
startActivityForVersion(intent, 1);
}
}
});
// 建立與註冊項目長按監聽物件
holder.rootView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
// 讀取選擇的記事物件
Item item = items.get(position);
// 處理是否顯示已選擇項目
processMenu(item);
// 重新設定記事項目
items.set(position, item);
return true;
}
});
}
};
// 設定RecylerView使用的資料來源
item_list.setAdapter(itemAdapter);
}
同樣在「MainActivity」,找到「processMenu」方法,參考下列程式片段,在方法的最後加入需要的敘述:
private void processMenu(Item item) {
...
// 通知項目勾選狀態改變
itemAdapter.notifyDataSetChanged();
}
同樣在「MainActivity」,找到「clickMenuItem」方法,參考下列的程式片段,依照註解的說明執行修改的工作:
public void clickMenuItem(MenuItem item) {
int itemId = item.getItemId();
switch (itemId) {
...
case R.id.revert_item:
for (int i = 0; i < items.size(); i++) {
Item ri = items.get(i);
if (ri.isSelected()) {
ri.setSelected(false);
// 移除
//itemAdapter.set(i, ri);
}
}
selectedCount = 0;
processMenu(null);
break;
// 刪除
case R.id.delete_item:
if (selectedCount == 0) {
break;
}
AlertDialog.Builder d = new AlertDialog.Builder(this);
String message = getString(R.string.delete_item);
d.setTitle(R.string.delete)
.setMessage(String.format(message, selectedCount));
d.setPositiveButton(android.R.string.yes,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int index = items.size() - 1;
while (index > -1) {
// 改為使用items物件
Item item = items.get(index);
if (item.isSelected()) {
// 改為使用items物件
items.remove(item);
itemDAO.delete(item.getId());
}
index--;
}
// 移除
//itemAdapter.notifyDataSetChanged();
selectedCount = 0;
processMenu(null);
}
});
d.setNegativeButton(android.R.string.no, null);
d.show();
break;
...
}
}
完成所有修改的工作了,你可以認識RecylerView的實作方式,比較原來使用ListView元件的作法,RecylerView會比較簡化一些,而且不用撰寫一堆程式碼,就可以提供Material Design的設計。
課程相關的檔案都可以GitHub瀏覽與下載。
http://github.com/macdidi5/Android-6-Tutorial
後續 >> Android 6 Tutorial 第六堂(3)Material Design – Shared Element 與 Floating Action Button
|