Search

Android - RecyclerView 與 SwipeRefreshLayout 以及捲動加載下一頁資料的完整建置

2015-10-17 11:10 AM

RecyclerView 是 Android 釋出用以取代 ListView 的元件

他比 ListView 效能更好 資源運用更靈活

不過在初始化方面是稍微複雜了點

現在我們就來一步步介紹該怎麼加入 RecyclerView 並使用 SwipeRefreshLayout

實作向下滑動更新的功能

首先我們先來看看 xml layout 的部分

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:orientation="vertical"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent">
  6.  
  7. <RelativeLayout
  8. android:id="@+id/favItemHolder"
  9. android:layout_width="match_parent"
  10. android:layout_height="match_parent">
  11.  
  12. <android.support.v4.widget.SwipeRefreshLayout
  13. android:id="@+id/pullToRefreshCateRecycler"
  14. android:layout_width="match_parent"
  15. android:layout_height="match_parent"
  16. android:visibility="visible">
  17.  
  18. <android.support.v7.widget.RecyclerView
  19. xmlns:android="http://schemas.android.com/apk/res/android"
  20. android:layout_width="match_parent"
  21. android:layout_height="match_parent"
  22. android:id="@+id/cateRecyclerView"
  23. android:layout_centerVertical="true"
  24. android:layout_centerHorizontal="true"/>
  25.  
  26. </android.support.v4.widget.SwipeRefreshLayout>
  27.  
  28. </RelativeLayout>
  29. </LinearLayout>

這時你會看到右方預覽介面是空白的



這是正常的結果所以不用擔心

接下來我們要新增 RecyclerView 裡面的 Item Layout

並將內容置中

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:orientation="vertical"
  4. android:layout_width="match_parent"
  5. android:layout_height="wrap_content"
  6. android:background="@drawable/fav_item_background_shape"
  7. android:layout_marginBottom="5px"
  8. android:layout_marginTop="10px"
  9. android:layout_marginLeft="5px"
  10. android:layout_marginRight="5px">
  11.  
  12. <LinearLayout
  13. android:paddingLeft="5dp"
  14. android:paddingTop="3dp"
  15. android:paddingRight="5dp"
  16. android:paddingBottom="5dp"
  17. android:orientation="vertical"
  18. android:layout_width="match_parent"
  19. android:layout_height="wrap_content">
  20.  
  21. <RelativeLayout
  22. android:layout_width="match_parent"
  23. android:layout_height="wrap_content">
  24.  
  25. <TextView
  26. android:id="@+id/cateItemTitle"
  27. android:layout_width="wrap_content"
  28. android:layout_height="wrap_content"
  29. android:textStyle="bold"
  30. android:textColor="#E7E7E7"
  31. android:layout_centerVertical="true"
  32. android:layout_centerHorizontal="true"
  33. android:text="Title"/>
  34.  
  35. </RelativeLayout>
  36.  
  37. <RelativeLayout
  38. android:layout_width="match_parent"
  39. android:layout_height="wrap_content"
  40. android:layout_marginTop="10px">
  41.  
  42. <TextView
  43. android:id="@+id/cateItemDesc"
  44. android:layout_width="wrap_content"
  45. android:layout_height="wrap_content"
  46. android:text="Description of this item."
  47. android:textColor="#E1E1E1"
  48. android:layout_centerVertical="true"
  49. android:layout_centerHorizontal="true"/>
  50.  
  51. </RelativeLayout>
  52.  
  53. </LinearLayout>
  54.  
  55. </FrameLayout>

看起來就會像這樣



那麼介面完成了 現在就要開始程式碼的部分

RecyclerView 需要一個 Adapter 配合

主要用途是 Item 的操作, 以及 layout 的載入

我們先來看 Adapter 該如何建立

  1. import android.support.v7.widget.RecyclerView;
  2. import android.view.LayoutInflater;
  3. import android.view.View;
  4. import android.view.ViewGroup;
  5. import android.widget.TextView;
  6. import com.ihad.ptt.model.bean.CategoryBean;
  7.  
  8. import java.util.Map;
  9. import java.util.Set;
  10.  
  11. public class CateRecyclerAdapter extends RecyclerView.Adapter<CateRecyclerAdapter.ItemHolder> {
  12.  
  13. // 物件儲存 Map
  14. private Map<Integer, CategoryBean> categoryBeans;
  15. // 點擊處理
  16. private ItemHolder.IClickHandler clickHandler;
  17.  
  18. public CateRecyclerAdapter(Map<Integer, CategoryBean> categoryBeans, ItemHolder.IClickHandler handler) {
  19. this.categoryBeans = categoryBeans;
  20. this.clickHandler = handler;
  21. }
  22.  
  23. // 載入 Layout
  24. @Override
  25. public ItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  26. // create a new view
  27. View itemLayoutView = LayoutInflater.from(parent.getContext()).inflate(R.layout.cate_item, parent, false);
  28.  
  29. // create ViewHolder
  30. ItemHolder viewHolder = new ItemHolder(itemLayoutView, clickHandler);
  31. return viewHolder;
  32. }
  33.  
  34. // 更新 View
  35. @Override
  36. public void onBindViewHolder(ItemHolder holder, int position) {
  37.  
  38. CategoryBean categoryBean = categoryBeans.get( position + 1 );
  39.  
  40. if( categoryBean == null ) return;
  41.  
  42. holder.cateItemTitle.setText( categoryBean.getName() );
  43. holder.cateItemDesc.setText( categoryBean.getDesc() );
  44.  
  45. }
  46.  
  47. // 取得已加入 Item 數量
  48. @Override
  49. public int getItemCount() {
  50. return categoryBeans.size();
  51. }
  52.  
  53. // 取得 Item
  54. public CategoryBean getItem(int key){
  55. return categoryBeans.get( key );
  56. }
  57.  
  58. // 已加入 Item 是否重複判斷
  59. public boolean isDuplicate( Map<Integer, CategoryBean> categoryBeanMap ){
  60.  
  61. if( !categoryBeans.isEmpty() ){
  62. Set<Integer> keySet = categoryBeanMap.keySet();
  63. int foundCount = 0;
  64.  
  65. for (Integer key : keySet) {
  66. if ( categoryBeans.containsKey(key) ) foundCount++;
  67. }
  68.  
  69. if ( foundCount == categoryBeanMap.size() ) return true;
  70. }
  71.  
  72. return false;
  73. }
  74.  
  75. // 加入 Item
  76. public void addItem(CategoryBean categoryBean){
  77. categoryBeans.put( categoryBean.getSerialNum(), categoryBean );
  78. }
  79.  
  80. // 清除所有 Item
  81. public void clear(){
  82. this.notifyItemRangeRemoved( 0, categoryBeans.size() );
  83. categoryBeans.clear();
  84. }
  85.  
  86. // Item
  87. public static class ItemHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
  88.  
  89. public TextView cateItemTitle;
  90. public TextView cateItemDesc;
  91. public IClickHandler clickHandler;
  92.  
  93. public ItemHolder(View view, IClickHandler handler) {
  94. super(view);
  95. view.setOnClickListener(this);
  96. cateItemTitle = (TextView) view.findViewById(R.id.cateItemTitle);
  97. cateItemDesc = (TextView) view.findViewById(R.id.cateItemDesc);
  98.  
  99. clickHandler = handler;
  100. }
  101.  
  102. @Override
  103. public void onClick(View view) {
  104. int position = getLayoutPosition();
  105.  
  106. clickHandler.onClick(view, position);
  107. }
  108.  
  109. public interface IClickHandler {
  110. void onClick(View caller, int position);
  111. }
  112. }
  113. }

這個部分應該沒什麼問題

接下來我們來看該如何連結所有的 Layout 及 Adapter 並將 Item 讀入 RecyclerView

  1. import android.os.Bundle;
  2. import android.support.v4.widget.SwipeRefreshLayout;
  3. import android.support.v7.widget.DefaultItemAnimator;
  4. import android.support.v7.widget.LinearLayoutManager;
  5. import android.support.v7.widget.RecyclerView;
  6. import android.view.LayoutInflater;
  7. import android.view.View;
  8. import android.view.ViewGroup;
  9. import android.widget.RelativeLayout;
  10. import android.widget.Toast;
  11. import cmc.toolkit.CTrace;
  12. import com.ihad.ptt.model.bean.CategoryBean;
  13. import com.ihad.ptt.model.bean.FavoriteBoardBean;
  14. import roboguice.fragment.RoboFragment;
  15.  
  16. import java.util.LinkedHashMap;
  17. import java.util.Map;
  18. import java.util.Set;
  19.  
  20. public class CategoryFragment extends RoboFragment {
  21.  
  22. public static final String CATE_PAGE = "CATE_PAGE";
  23. private int mPage;
  24.  
  25. // 滑動更新用
  26. private SwipeRefreshLayout cateSwipeRefreshLayout;
  27. private SwipeRefreshLayout cateSubSwipeRefreshLayout;
  28.  
  29. // RecyclerView 的呈現方式
  30. // 有線性(LinearLayoutManager), 塊狀(GridLayoutManager) 以及不規則塊狀(StaggeredGridLayoutManager) 三種
  31. private LinearLayoutManager mCateLinearLayoutManager;
  32. // Adapter
  33. private CateRecyclerAdapter mCateAdapter;
  34.  
  35. // 是否正在讀取
  36. private boolean isCateLoading = false;
  37. // 是否已讀取完整資料
  38. private boolean noCateMoreData = false;
  39.  
  40. // 若是在 Activity 中建立的不需要管這個 本例為使用 Fragment 加載
  41. public static CategoryFragment newInstance(int page) {
  42. Bundle args = new Bundle();
  43. args.putInt(CATE_PAGE, page);
  44. CategoryFragment fragment = new CategoryFragment();
  45. fragment.setArguments(args);
  46. return fragment;
  47. }
  48.  
  49. @Override
  50. public void onCreate(Bundle savedInstanceState) {
  51. super.onCreate(savedInstanceState);
  52. mPage = getArguments().getInt(CATE_PAGE);
  53.  
  54. // 分類目錄
  55. mCateAdapter = new CateRecyclerAdapter( new LinkedHashMap<Integer, CategoryBean>(), new CateRecyclerAdapter.ItemHolder.IClickHandler() {
  56. @Override
  57. public void onClick(View view, int position) {
  58. // 使用 Position 取得物件 這裡 +1 是因為我使用 1 based 方式建立的序號
  59. CategoryBean categoryBean = mCateAdapter.getItem( position + 1 );
  60.  
  61. if( categoryBean == null ) {
  62. Toast.makeText(view.getContext(), "Find nothing", Toast.LENGTH_SHORT).show();
  63. }
  64. else{
  65. // 點擊後的處理 可自行設定 若是在 Activity 新增則不須再取得 Activity
  66. MainActivity mainActivity = (MainActivity) getActivity();
  67. mainActivity.goCateSubList(categoryBean);
  68. }
  69. }
  70. });
  71. }
  72.  
  73. // 取得 Layout 並初始化各項元件
  74. @Override
  75. public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
  76. // 這個是放 RecyclerView 的 Layout
  77. View view = inflater.inflate(R.layout.cate_frag_page, container, false);
  78.  
  79. cateSwipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.pullToRefreshCateRecycler);
  80.  
  81. // 設定滑動更新 loader 的顏色 預設為黑色
  82. cateSwipeRefreshLayout.setColorSchemeColors(0xFF4FC3F7, 0xFF8558E0, 0xFFFF326F, 0xFFF9F765);
  83.  
  84. // 滑動更新監聽設定
  85. cateSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
  86. @Override
  87. public void onRefresh() {
  88. // 清除所有 Item
  89. mCateAdapter.clear();
  90.  
  91. // 呼叫重新整理 function
  92. MainActivity mainActivity = (MainActivity) getActivity();
  93. mainActivity.reloadCatePage();
  94.  
  95. // 設定為尚有資料
  96. noCateMoreData = false;
  97. }
  98. });
  99.  
  100. // new 一個 LinearLayoutManager 結果將呈現為縱向捲動清單
  101. mCateLinearLayoutManager = new LinearLayoutManager(getActivity());
  102. mCateLinearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
  103.  
  104. // 取得 RecyclerView
  105. RecyclerView cateRecyclerView = (RecyclerView) view.findViewById(R.id.cateRecyclerView);
  106.  
  107. // 底下一併介紹其他呈現方式的設定方法
  108. // List layout
  109. cateRecyclerView.setLayoutManager(mCateLinearLayoutManager);
  110. // Grid layout
  111. //cateRecyclerView.setLayoutManager(new GridLayoutManager(this, 2));
  112. // StaggeredGrid layout
  113. //cateRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(2, OrientationHelper.VERTICAL));
  114.  
  115. // 將 Adapter 指定給 RecyclerView
  116. cateRecyclerView.setAdapter(mCateAdapter);
  117. // 加入動畫 若指定 null 就不會有動畫
  118. cateRecyclerView.setItemAnimator(new DefaultItemAnimator());
  119.  
  120. // 向下捲動自動加載新資料
  121. cateRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
  122. @Override
  123. public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
  124.  
  125. int visibleItemCount = mCateLinearLayoutManager.getChildCount();
  126. int totalItemCount = mCateLinearLayoutManager.getItemCount();
  127. int pastVisiblesItems = mCateLinearLayoutManager.findFirstVisibleItemPosition();
  128.  
  129. // 視情況修改在第幾項 Item 顯示時自動讀取下一頁
  130. // 本範例為單次讀取 20 筆資料, 捲動至第 10 筆時自動讀取下一頁
  131. if( totalItemCount > 10 ){
  132. totalItemCount = totalItemCount - 10;
  133. }
  134.  
  135. // 確保為向下捲動
  136. if( dy < 0 ) return;
  137.  
  138. // 用來控制重複讀取以及已無新資料的狀況
  139. if (!isCateLoading && !noCateMoreData) {
  140. // 顯示項目已超過十項 自動讀取資料
  141. if ((visibleItemCount + pastVisiblesItems) >= totalItemCount) {
  142. isCateLoading = true;
  143. // 呼叫讀取下一頁資料的 function
  144. MainActivity mainActivity = (MainActivity) getActivity();
  145. mainActivity.nextCatePage();
  146. }
  147. }
  148. }
  149. });
  150.  
  151. return view;
  152. }
  153.  
  154. // 設定為讀取中 顯示 loader
  155. public void cateRefreshing(boolean refreshing){
  156. if( cateSwipeRefreshLayout == null ) return;
  157.  
  158. cateSwipeRefreshLayout.setRefreshing(refreshing);
  159. }
  160.  
  161. // 加入讀取完成的資料
  162. public boolean addCateItems(Map<Integer, CategoryBean> categoryBeanMap){
  163.  
  164. Set<Integer> keySet = categoryBeanMap.keySet();
  165.  
  166. for( Integer key : keySet ){
  167. CategoryBean categoryBean = categoryBeanMap.get(key);
  168. mCateAdapter.addItem(categoryBean);
  169. // 因資料建立時是使用 1 based 方式所以需要 - 1
  170. mCateAdapter.notifyItemChanged(categoryBean.getSerialNum() - 1);
  171. }
  172.  
  173. return false;
  174. }
  175.  
  176. // 資料已加入 設定為可讀取下一頁資料
  177. public void addCateFinished(){
  178. isCateLoading = false;
  179. }
  180.  
  181. }
  182.  

到這裡就完成了

本範例使用的雖然是 Fragment 但大致上使用方式使相同的

若要在直接在 Activity 中加入 RecyclerView 方法是一樣的

但 Adapter 的建立就不需要分開在不同的地方

Fragment 是因為若不先在 OnCreate 時建立 Adapter

而在 OnCreateView 內建立的話會有問題

Log 會顯示 RecyclerView 沒有配對的 Adapter 將忽略載入

這種情況即使你有將物件加入 Adapter 也會呈現空白的資料

原因目前還不清楚 但若在 Activity 內就不會有這個問題

以下是在 Activity 內建立的範例

  1.  
  2. @InjectView(R.id.pullToRefreshCateRecycler)
  3. private SwipeRefreshLayout cateSwipeRefreshLayout;
  4.  
  5. @InjectView(R.id.cateRecyclerView)
  6. private RecyclerView cateRecyclerView;
  7.  
  8. @Override
  9. protected void onCreate(Bundle savedInstanceState) {
  10. super.onCreate(savedInstanceState);
  11.  
  12. mCateAdapter = new CateRecyclerAdapter( new LinkedHashMap<Integer, CategoryBean>(), new CateRecyclerAdapter.ItemHolder.IClickHandler() {
  13. @Override
  14. public void onClick(View view, int position) {
  15.  
  16. CategoryBean categoryBean = mCateAdapter.getItem( position + 1 );
  17.  
  18. if( categoryBean == null ) {
  19. Toast.makeText(view.getContext(), "Find nothing", Toast.LENGTH_SHORT).show();
  20. }
  21. else{
  22. goCateSubList(categoryBean);
  23. }
  24. }
  25. });
  26.  
  27. cateSwipeRefreshLayout.setColorSchemeColors(0xFF4FC3F7, 0xFF8558E0, 0xFFFF326F, 0xFFF9F765);
  28.  
  29. cateSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
  30. @Override
  31. public void onRefresh() {
  32. mCateAdapter.clear();
  33. reloadCatePage();
  34. noCateMoreData = false;
  35. }
  36. });
  37.  
  38. mCateLinearLayoutManager = new LinearLayoutManager(getActivity());
  39. mCateLinearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
  40.  
  41. RecyclerView cateRecyclerView = (RecyclerView) view.findViewById(R.id.cateRecyclerView);
  42.  
  43. cateRecyclerView.setAdapter(mCateAdapter);
  44. cateRecyclerView.setItemAnimator(new DefaultItemAnimator());
  45.  
  46. cateRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
  47. @Override
  48. public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
  49.  
  50. int visibleItemCount = mCateLinearLayoutManager.getChildCount();
  51. int totalItemCount = mCateLinearLayoutManager.getItemCount();
  52. int pastVisiblesItems = mCateLinearLayoutManager.findFirstVisibleItemPosition();
  53.  
  54. if( totalItemCount > 10 ){
  55. totalItemCount = totalItemCount - 10;
  56. }
  57.  
  58. if( dy < 0 ) return;
  59.  
  60. if (!isCateLoading && !noCateMoreData) {
  61. if ((visibleItemCount + pastVisiblesItems) >= totalItemCount) {
  62. isCateLoading = true;
  63. nextCatePage();
  64. }
  65. }
  66. }
  67. });
  68. }

就這樣囉

一開始可能稍嫌複雜, 但習慣後其實滿方便的

同一個 Adapter 可以重複使用

寫程式的效率也自然跟著變高了

以上就是這次落落長的教學文...

各項資料連結
Creating Lists and Cards
Android RecyclerView

No comments:

Post a Comment