Android Tutorial 第六堂(2)Material Design - RecylerView
專欄作者新書出版:Android App程式開發剖析 第三版(適用Android 8 Oreo與Android Studio 3) Android 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:21.0.+' } 加入上列的設定後,選擇畫面右上角的「Sync Now」,讓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/retangle_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/retangle_drawable" android:divider="@color/divider_color" /> --> 為了讓畫面的操作可以符合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> 上面加入畫面資源檔的設定,需要新增下列的尺寸資源到「res/values/dimens.xml」: <dimen name="divider_size">2sp</dimen> 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.content.Context; 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<ItemApapterRV.ViewHolder>{ // 包裝的記事資料 private List<Item> items; public ItemAdapterRV(List<Item> items) { this.items = items; } @Override public ItemApapterRV.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 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(); } // 一定要使用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; 因為從原來的ListView改為RecyclerView元件,所以在「MainActivity」類別找到「processViews」方法,參考下列的程式碼執行修改的工作: private void processViews() { // 把ListView改為RecyclerView item_list = (RecyclerView) findViewById(R.id.item_list); show_app_name = (TextView) findViewById(R.id.show_app_name); } 完成上列的修改以後,程式碼會產生很多錯誤,接下來會分成幾個段落完成所有修改的工作。首先找到「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」,找到「processControllers」方法,先移除方法中所有的程式碼,參考下列程式片段完成這個方法的實作: private void processControllers() { // 實作ItemAdapterRVX類別,加入註冊監聽事件的工作 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/AndroidTutorial 後續 >> Android Tutorial 第六堂(3)Material Design – Shared Element與自定動畫效果 |