13.建立可擴充套件的集合檢視
13.1 問題
希望以獨特的方式展示大型資料集合,而不是以垂直滾動列表顯示;或者,要以AdapterView小部件無法輕鬆支援的方式樣式化此集合。
13.2 解決方案
(API Level 1)
以Android支援庫中的RecyeclerView為基礎構建解決方案。RecyclerView小部件利用與AdapterView元件相同的檢視回收功能來提供大型資料集的高效記憶體使用顯示方式。然而,與核心框架中的AdapterView不同的是,RecyclerView以更加靈活的模型為基礎,在此模型中,子檢視元件的放置委託給LayoutManager例項完成。Android庫支援兩個內建的佈局管理器:
- LineLayoutManager :將子檢視垂直(自上而下)或水平(自左而右)放置在列表中。垂直佈局行為類似於ListView框架。
- GridLayoutManager : 將子檢視垂直(自上而下)或水平(自左而右)放置在網格中。該管理器支援新增行/列跨度值以使網格中的子檢視交錯顯示。對於單一跨度項的垂直佈局行為類似於GridView框架。
RecyclerView.ItemDecoration例項使用應用程式支援在子檢視的上方和下方進行自定義的繪製操作,此外還直接支援頁邊距以在子檢視之間新增間距。該例項可用於繪製像網格線和連線線這樣的簡單物件,還可以用於在內容區域中繪製更多複雜的圖案或圖片。
RecyclerView.Adapter例項還包括用於通知資料集檢視變化的新方法,使得該小部件可以更好地處理各種變化的動畫,如新增或刪除元素,而使用AdapterView較難處理這些動畫:
- notifyItemInserted()、notifyItemRemoved()、notifyItemChanged() : 表明已新增、刪除關聯資料集中的單個項,或者該項已改變位置。
- notifyItemRangeInserted()、notifyItemRangeRemoved()、notifyItemRangeChanged() : 表明關聯資料集中已修改的一定位置範圍的項。
這些方法接受的引數是特定項的位置,因此RecyclerView可以智慧地判斷如何製作變化的動畫。標準的notifyDataSetChanged()方法仍然得到支援,但它不會製作變化的動畫。
要點:
RecyclerView僅在Android支援庫中提供:它不是任意平臺級別中的原生SDK的一部分。然而,目標平臺為API Level 7或之後版本的任意應用程式都可以通過包含支援庫使用此小部件。有關在專案中包含支援庫的更多資訊,請參考如下網址: ofollow,noindex">http://developer.android.com/tools/support-library/index.html 。
13.3 實現機制
下面的示例使用4個不同的LayoutManager例項,通過RecycleView顯示相同的項資料。以下兩圖顯示了在垂直和水平列表中顯示的資料:

垂直列表集合

水平列表集合
以下兩圖分別在交錯的垂直網格和均勻的水平網格中顯示相同的資料:

垂直網格集合

水平網格集合
首先,以下兩段程式碼顯示了Activity和用於選擇佈局的選項選單。
使用了RecyclerView的Activity
public class SimpleRecyclerActivity extends ActionBarActivity implements SimpleItemAdapter.OnItemClickListener { private RecyclerView mRecyclerView; private SimpleItemAdapter mAdapter; /* 佈局管理器 */ private LinearLayoutManager mHorizontalManager; private LinearLayoutManager mVerticalManager; private GridLayoutManager mVerticalGridManager; private GridLayoutManager mHorizontalGridManager; /* 修飾 */ private ConnectorDecoration mConnectors; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mRecyclerView = new RecyclerView(this); mHorizontalManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false); mVerticalManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); mVerticalGridManager = new GridLayoutManager(this, 2, /* 網格的列數 */ LinearLayoutManager.VERTICAL, /* 垂直定位網格 */ false); mHorizontalGridManager = new GridLayoutManager(this, 3, /*網路的行數 */ LinearLayoutManager.HORIZONTAL, /* 水平定位網格*/ false); //垂直網格的連線線修飾 mConnectors = new ConnectorDecoration(this); //交錯垂直網格 mVerticalGridManager.setSpanSizeLookup(new GridStaggerLookup()); mAdapter = new SimpleItemAdapter(this); mAdapter.setOnItemClickListener(this); mRecyclerView.setAdapter(mAdapter); //對所有連線音樂應用邊緣修飾 mRecyclerView.addItemDecoration(new InsetDecoration(this)); //預設為垂直佈局 selectLayoutManager(R.id.action_vertical); setContentView(mRecyclerView); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.layout_options, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { return selectLayoutManager(item.getItemId()); } private boolean selectLayoutManager(int id) { switch (id) { case R.id.action_vertical: mRecyclerView.setLayoutManager(mVerticalManager); mRecyclerView.removeItemDecoration(mConnectors); return true; case R.id.action_horizontal: mRecyclerView.setLayoutManager(mHorizontalManager); mRecyclerView.removeItemDecoration(mConnectors); return true; case R.id.action_grid_vertical: mRecyclerView.setLayoutManager(mVerticalGridManager); mRecyclerView.addItemDecoration(mConnectors); return true; case R.id.action_grid_horizontal: mRecyclerView.setLayoutManager(mHorizontalGridManager); mRecyclerView.removeItemDecoration(mConnectors); return true; case R.id.action_add_item: //插入新的項 mAdapter.insertItemAtIndex("Android Recipes", 1); return true; case R.id.action_remove_item: //刪除第一項 mAdapter.removeItemAtIndex(1); return true; default: return false; } } /** OnItemClickListener 方法 */ @Override public void onItemClick(SimpleItemAdapter.ItemHolder item, int position) { Toast.makeText(this, item.getSummary(), Toast.LENGTH_SHORT).show(); } }
res/menu/layout_options.xml
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/action_add_item" android:title="Add Item" android:icon="@android:drawable/ic_menu_add" app:showAsAction="ifRoom" /> <item android:id="@+id/action_remove_item" android:title="Remove Item" android:icon="@android:drawable/ic_menu_delete" app:showAsAction="ifRoom" /> <item android:id="@+id/action_vertical" android:title="Vertical List" app:showAsAction="never"/> <item android:id="@+id/action_horizontal" android:title="Horizontal List" app:showAsAction="never"/> <item android:id="@+id/action_grid_vertical" android:title="Vertical Grid" app:showAsAction="never"/> <item android:id="@+id/action_grid_horizontal" android:title="Horizontal Grid" app:showAsAction="never"/> </menu>
此例使用選項選單選擇應該應用於RecyclerView的佈局管理器。任何改動都會觸發selectLayoutManager()輔助方法,該方法將請求的管理器傳遞給setLayoutManager()。這會從現有的介面卡中重新載入目前的資料,因此我們不需要維護多個RecyclerView例項即可動態更改佈局。
可以看到,利用內建的佈局管理器不需要太多的程式碼。此例構造兩個LinearlayoutManager例項,它們的建構函式以方向常量為引數(VERTICAL或HORIZONTAL)。該管理器還支援(通過最後的布林引數)翻轉佈局,以便按照最後項最先顯示的順序佈置介面卡資料。
同樣,我們構造兩個GridLayoutManager例項,分別用於水平和垂直佈局。此物件獲取一個附加引數,即spanCount,表示佈局應使用的行數(水平網格)或列數(垂直網格)。此引數與支援交錯網格沒有任何關係;稍後將對此進行討論。
與所有集合檢視一樣,我們需要讓介面卡將資料項與檢視關聯。你可能已注意到,在Activity的程式碼清單中建立了SimpleItemAdapter類。該介面卡的實現如以下兩段程式碼所示:
res/layout/collection_item.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="8dp" android:background="#CCF"> <TextView android:id="@+id/text_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:textAppearanceLarge"/> <TextView android:id="@+id/text_summary" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:textAppearanceMedium"/> </LinearLayout>
RecyclerView的介面卡實現
public class SimpleItemAdapter extends RecyclerView.Adapter<SimpleItemAdapter.ItemHolder> { /* * 單擊處理程式介面。與AdapterViews不同,RecyclerView 沒有自己的內建介面 */ public interface OnItemClickListener { public void onItemClick(ItemHolder item, int position); } private static final String[] ITEMS = { "Apples", "Oranges", "Bananas", "Mangos", "Carrots", "Peas", "Broccoli", "Pork", "Chicken", "Beef", "Lamb" }; private List<String> mItems; private OnItemClickListener mOnItemClickListener; private LayoutInflater mLayoutInflater; public SimpleItemAdapter(Context context) { mLayoutInflater = LayoutInflater.from(context); //建立虛擬項的靜態列表 mItems = new ArrayList<String>(); mItems.addAll(Arrays.asList(ITEMS)); mItems.addAll(Arrays.asList(ITEMS)); } @Override public ItemHolder onCreateViewHolder(ViewGroup parent, int viewType) { View itemView = mLayoutInflater.inflate(R.layout.collection_item, parent, false); return new ItemHolder(itemView, this); } @Override public void onBindViewHolder(ItemHolder holder, int position) { holder.setTitle("Item #"+(position+1)); holder.setSummary(mItems.get(position)); } @Override public int getItemCount() { return mItems.size(); } public OnItemClickListener getOnItemClickListener() { return mOnItemClickListener; } public void setOnItemClickListener(OnItemClickListener listener) { mOnItemClickListener = listener; } /* 管理資料集修改的方法 */ public void insertItemAtIndex(String item, int position) { mItems.add(position, item); //通知檢視觸發變化動畫 notifyItemInserted(position); } public void removeItemAtIndex(int position) { if (position >= mItems.size()) return; mItems.remove(position); //通知檢視觸發變化動畫 notifyItemRemoved(position); } /* 封裝項檢視所需的ViewHolder實現*/ public static class ItemHolder extends RecyclerView.ViewHolder implements View.OnClickListener { private SimpleItemAdapter mParent; private TextView mTitleView, mSummaryView; public ItemHolder(View itemView, SimpleItemAdapter parent) { super(itemView); itemView.setOnClickListener(this); mParent = parent; mTitleView = (TextView) itemView.findViewById(R.id.text_title); mSummaryView = (TextView) itemView.findViewById(R.id.text_summary); } public void setTitle(CharSequence title) { mTitleView.setText(title); } public void setSummary(CharSequence summary) { mSummaryView.setText(summary); } public CharSequence getSummary() { return mSummaryView.getText(); } @Override public void onClick(View v) { final OnItemClickListener listener = mParent.getOnItemClickListener(); if (listener != null) { listener.onItemClick(this, getPosition()); } } } }
RecyclerView.Adapter重點實施ViewHolder設計模式,在此模式中,它要求實現返回RecyclerView.ViewHolder型別的子集。該類在內部用作與子項關聯的元資料(例如其當前位置和穩定的ID)的儲存位置。具體的實現通常還提供對檢視內部欄位的直接訪問,從而儘量減少對findViewById()的重複呼叫,該方法呼叫大量系統資源,因為它會遍歷整個檢視層次結構以查詢請求的子項。
RecyclerView.Adapter實現與CursorAdapter類似的模式,其中通過onCreateViewHolder()和onBindViewHolder()分開執行建立和繫結步驟。如果必須重頭建立新的檢視,則呼叫前一個方法,因此我們在此構造一個新的ItemHolder返回。如果特定位置的資料(在此是簡單的字串)需要附加到新的檢視,則呼叫最後一個方法;這可能是新建立的或回收的檢視。這種模式與ArrayAdapter相反,後者將兩個方法的功能結合到一個getView()方法中。
在我們的示例中,還利用此介面卡提供來自AdapterView的一個附加功能,RecyclerView本質上不支援此功能 : 項單擊偵聽器。為了使用最少量的引用交換處理子檢視的單擊事件,我們將每個ViewHolder設定為根項檢視的OnClickListener。然後,ViewHolder處理這些事件,並將其傳送回在介面卡上定義的公共偵聽器介面。完成上述操作後,ViewHolder就可以在最後的偵聽器回撥中新增位置元資料,我們過去期望在諸如AdapterView.OnItemClickListener的類似偵聽器中看到這樣的結果。
1. 交錯網格
在Activity示例中,垂直網格佈局管理器還配備有SpanSizeLookup輔助類,該類用於生成上圖(垂直與水平網格集合)所示的交錯效果。以下程式碼顯示了具體的實現:
交錯網格SpanSizeLookup
public class GridStaggerLookup extends GridLayoutManager.SpanSizeLookup { @Override public int getSpanSize(int position) { return (position % 3 == 0 ? 2 : 1); } }
getSpanSize()方法用於提供查詢,告訴佈局管理器給定位置應占據多少跨度(行數或列數,具體取決於佈局方向)。該例表明每隔三個位置應戰據兩列,而所有其他位置應僅佔據一列。
2. 修飾項
可以注意到,在Activity示例中還添加了兩個ItemDecoration例項。第一個修飾例項Insetdecoration應用於所有示例佈局管理器,為每個子項提供頁邊距。第二個修飾例項ConnectorDecoration僅應用於垂直交錯網路,用於在主要和次要網格項之間繪製連線線。以下三段程式碼定義了這些修飾例項:
提供嵌入式頁邊距的ItemDecoration
public class InsetDecoration extends RecyclerView.ItemDecoration { private int mInsetMargin; public InsetDecoration(Context context) { super(); mInsetMargin = context.getResources() .getDimensionPixelOffset(R.dimen.inset_margin); } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { //對子檢視的所有4個邊界應用計算得出的頁邊距 outRect.set(mInsetMargin, mInsetMargin, mInsetMargin, mInsetMargin); } }
提供連線線的ItemDecoration
public class ConnectorDecoration extends RecyclerView.ItemDecoration { private Paint mLinePaint; private int mLineLength; public ConnectorDecoration(Context context) { super(); mLineLength = context.getResources() .getDimensionPixelOffset(R.dimen.inset_margin); int connectorStroke = context.getResources() .getDimensionPixelSize(R.dimen.connector_stroke); mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mLinePaint.setColor(Color.BLACK); mLinePaint.setStyle(Paint.Style.STROKE); mLinePaint.setStrokeWidth(connectorStroke); } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { final RecyclerView.LayoutManager manager = parent.getLayoutManager(); for (int i=0; i < parent.getChildCount(); i++) { final View child = parent.getChildAt(i); boolean isLarge = parent.getChildViewHolder(child).getPosition() % 3 == 0; if (!isLarge) { final int childLeft = manager.getDecoratedLeft(child); final int childRight = manager.getDecoratedRight(child); final int childTop = manager.getDecoratedTop(child); final int x = childLeft + ((childRight - childLeft) / 2); c.drawLine(x, childTop - mLineLength, x, childTop + mLineLength, mLinePaint); } } } }
res/values/dimens.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <dimen name="inset_margin">8dp</dimen> <dimen name="connector_stroke">2dp</dimen> </resources>
ItemDecoration可以實現3個主要的回撥。第一個回撥是getItemOffsets(),它提供修飾器可用於對給定子檢視應用頁邊距的Rect例項。在此例中,我們希望所有子檢視具有相同的頁邊距,因此在每次呼叫中設定相同的值。
提示:
即使沒有在getItemOffsets()中採用位置引數,也仍然可以在需要此位置確定如何應用頁邊距時,通過getChildViewHolder(view).getPosition()從RecyclerView獲得位置。
另外兩個呼叫——onDraw()和onDrawOver()提供修飾器可用於繪製其他內容的Canvas。這些方法分別在子檢視的下方和上方繪製內容。ConnectorDecoration()使用onDraw()渲染任何可見子項之間的連線線。為此,我們遍歷子檢視,並且在未佔據兩列的每個子項(根據前面描述的交錯查詢)上繪製中心線。
這些繪製回撥方法將在RecyclerView需要重繪時被呼叫,例如當內容滾動時,因此我們必須經常瞭解檢視當前所在位置,以便知道在何處繪製線。相比於直接詢問子檢視左/右/上/下座標,我們更喜歡通過getDecoratedXxx()從佈局管理器請求此資訊。這是因為其他修飾例項(例如InsetDecoration)可能在事後修改檢視的邊界,我們的繪製方法需要考慮這些因素。
3. 項動畫
支援介面卡資料集的變化動畫的邏輯內建在每個佈局管理器中。為了使管理器適當地確定如何在資料集改變時製作動畫,我們必須使用特定於RecyclerView的介面卡更新方法,而不是傳統的notifyDataSetChanged()。
修改介面卡資料含兩個步驟:必須首先新增或刪除資料項,然後介面卡必須通知檢視變化發生的確切位置。在本例中,資料項新增選項觸發介面卡上的notifyItemInserted(),而資料項刪除選項觸發notifyItemRemoved()。