Search

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

2015-10-17 11:10 AM

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

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

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

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

實作向下滑動更新的功能

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

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <RelativeLayout
            android:id="@+id/favItemHolder"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <android.support.v4.widget.SwipeRefreshLayout
                android:id="@+id/pullToRefreshCateRecycler"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:visibility="visible">

            <android.support.v7.widget.RecyclerView
                    xmlns:android="http://schemas.android.com/apk/res/android"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:id="@+id/cateRecyclerView"
                    android:layout_centerVertical="true"
                    android:layout_centerHorizontal="true"/>

        </android.support.v4.widget.SwipeRefreshLayout>

    </RelativeLayout>
</LinearLayout>

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



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

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

並將內容置中

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             android:orientation="vertical"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:background="@drawable/fav_item_background_shape"
             android:layout_marginBottom="5px"
             android:layout_marginTop="10px"
             android:layout_marginLeft="5px"
             android:layout_marginRight="5px">

    <LinearLayout
            android:paddingLeft="5dp"
            android:paddingTop="3dp"
            android:paddingRight="5dp"
            android:paddingBottom="5dp"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content">

            <TextView
                    android:id="@+id/cateItemTitle"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textStyle="bold"
                    android:textColor="#E7E7E7"
                    android:layout_centerVertical="true"
                    android:layout_centerHorizontal="true"
                    android:text="Title"/>

        </RelativeLayout>

        <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="10px">

            <TextView
                    android:id="@+id/cateItemDesc"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Description of this item."
                    android:textColor="#E1E1E1"
                    android:layout_centerVertical="true"
                    android:layout_centerHorizontal="true"/>

        </RelativeLayout>

    </LinearLayout>

</FrameLayout>

看起來就會像這樣



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

RecyclerView 需要一個 Adapter 配合

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

我們先來看 Adapter 該如何建立

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.ihad.ptt.model.bean.CategoryBean;

import java.util.Map;
import java.util.Set;

public class CateRecyclerAdapter extends RecyclerView.Adapter<CateRecyclerAdapter.ItemHolder> {

    // 物件儲存 Map
    private Map<Integer, CategoryBean> categoryBeans;
    // 點擊處理
    private ItemHolder.IClickHandler clickHandler;

    public CateRecyclerAdapter(Map<Integer, CategoryBean> categoryBeans, ItemHolder.IClickHandler handler) {
        this.categoryBeans = categoryBeans;
        this.clickHandler = handler;
    }

    // 載入 Layout
    @Override
    public ItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // create a new view
        View itemLayoutView = LayoutInflater.from(parent.getContext()).inflate(R.layout.cate_item, parent, false);

        // create ViewHolder
        ItemHolder viewHolder = new ItemHolder(itemLayoutView, clickHandler);
        return viewHolder;
    }

    // 更新 View
    @Override
    public void onBindViewHolder(ItemHolder holder, int position) {

        CategoryBean categoryBean = categoryBeans.get( position + 1 );

        if( categoryBean == null ) return;

        holder.cateItemTitle.setText( categoryBean.getName() );
        holder.cateItemDesc.setText( categoryBean.getDesc() );

    }

    // 取得已加入 Item 數量
    @Override
    public int getItemCount() {
        return categoryBeans.size();
    }

    // 取得 Item
    public CategoryBean getItem(int key){
        return categoryBeans.get( key );
    }

    // 已加入 Item 是否重複判斷
    public boolean isDuplicate( Map<Integer, CategoryBean> categoryBeanMap ){

        if( !categoryBeans.isEmpty() ){
            Set<Integer> keySet = categoryBeanMap.keySet();
            int foundCount = 0;

            for (Integer key : keySet) {
                if ( categoryBeans.containsKey(key) ) foundCount++;
            }

            if ( foundCount == categoryBeanMap.size() ) return true;
        }

        return false;
    }

    // 加入 Item
    public void addItem(CategoryBean categoryBean){
        categoryBeans.put( categoryBean.getSerialNum(), categoryBean );
    }

    // 清除所有 Item
    public void clear(){
        this.notifyItemRangeRemoved( 0, categoryBeans.size() );
        categoryBeans.clear();
    }

    // Item
    public static class ItemHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        public TextView cateItemTitle;
        public TextView cateItemDesc;
        public IClickHandler clickHandler;

        public ItemHolder(View view, IClickHandler handler) {
            super(view);
            view.setOnClickListener(this);
            cateItemTitle = (TextView) view.findViewById(R.id.cateItemTitle);
            cateItemDesc = (TextView) view.findViewById(R.id.cateItemDesc);

            clickHandler = handler;
        }

        @Override
        public void onClick(View view) {
            int position = getLayoutPosition();

            clickHandler.onClick(view, position);
        }

        public interface IClickHandler {
            void onClick(View caller, int position);
        }
    }
}

這個部分應該沒什麼問題

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

import android.os.Bundle;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.Toast;
import cmc.toolkit.CTrace;
import com.ihad.ptt.model.bean.CategoryBean;
import com.ihad.ptt.model.bean.FavoriteBoardBean;
import roboguice.fragment.RoboFragment;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

public class CategoryFragment extends RoboFragment {

    public static final String CATE_PAGE = "CATE_PAGE";
    private int mPage;

    // 滑動更新用
    private SwipeRefreshLayout cateSwipeRefreshLayout;
    private SwipeRefreshLayout cateSubSwipeRefreshLayout;

    // RecyclerView 的呈現方式
    // 有線性(LinearLayoutManager), 塊狀(GridLayoutManager) 以及不規則塊狀(StaggeredGridLayoutManager) 三種
    private LinearLayoutManager mCateLinearLayoutManager;
    // Adapter
    private CateRecyclerAdapter mCateAdapter;

    // 是否正在讀取
    private boolean isCateLoading   = false;
    // 是否已讀取完整資料
    private boolean noCateMoreData  = false;

    // 若是在 Activity 中建立的不需要管這個 本例為使用 Fragment 加載
    public static CategoryFragment newInstance(int page) {
        Bundle args = new Bundle();
        args.putInt(CATE_PAGE, page);
        CategoryFragment fragment = new CategoryFragment();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPage = getArguments().getInt(CATE_PAGE);

        // 分類目錄
        mCateAdapter = new CateRecyclerAdapter( new LinkedHashMap<Integer, CategoryBean>(), new CateRecyclerAdapter.ItemHolder.IClickHandler() {
            @Override
            public void onClick(View view, int position) {
                // 使用 Position 取得物件 這裡 +1 是因為我使用 1 based 方式建立的序號
                CategoryBean categoryBean = mCateAdapter.getItem( position + 1 );

                if( categoryBean == null ) {
                    Toast.makeText(view.getContext(), "Find nothing", Toast.LENGTH_SHORT).show();
                }
                else{
                    // 點擊後的處理 可自行設定 若是在 Activity 新增則不須再取得 Activity
                    MainActivity mainActivity = (MainActivity) getActivity();
                    mainActivity.goCateSubList(categoryBean);
                }
            }
        });
    }

    // 取得 Layout 並初始化各項元件
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        // 這個是放 RecyclerView 的 Layout
        View view = inflater.inflate(R.layout.cate_frag_page, container, false);

        cateSwipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.pullToRefreshCateRecycler);

        // 設定滑動更新 loader 的顏色 預設為黑色
        cateSwipeRefreshLayout.setColorSchemeColors(0xFF4FC3F7, 0xFF8558E0, 0xFFFF326F, 0xFFF9F765);

        // 滑動更新監聽設定
        cateSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                // 清除所有 Item
                mCateAdapter.clear();

                // 呼叫重新整理 function
                MainActivity mainActivity = (MainActivity) getActivity();
                mainActivity.reloadCatePage();

                // 設定為尚有資料
                noCateMoreData = false;
            }
        });

        // new 一個 LinearLayoutManager 結果將呈現為縱向捲動清單
        mCateLinearLayoutManager = new LinearLayoutManager(getActivity());
        mCateLinearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);

        // 取得 RecyclerView
        RecyclerView cateRecyclerView = (RecyclerView) view.findViewById(R.id.cateRecyclerView);

        // 底下一併介紹其他呈現方式的設定方法
        // List layout
        cateRecyclerView.setLayoutManager(mCateLinearLayoutManager);
        // Grid layout
        //cateRecyclerView.setLayoutManager(new GridLayoutManager(this, 2));
        // StaggeredGrid layout
        //cateRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(2, OrientationHelper.VERTICAL));

        // 將 Adapter 指定給 RecyclerView
        cateRecyclerView.setAdapter(mCateAdapter);
        // 加入動畫 若指定 null 就不會有動畫
        cateRecyclerView.setItemAnimator(new DefaultItemAnimator());

        // 向下捲動自動加載新資料
        cateRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {

                int visibleItemCount = mCateLinearLayoutManager.getChildCount();
                int totalItemCount = mCateLinearLayoutManager.getItemCount();
                int pastVisiblesItems = mCateLinearLayoutManager.findFirstVisibleItemPosition();

                // 視情況修改在第幾項 Item 顯示時自動讀取下一頁
                // 本範例為單次讀取 20 筆資料, 捲動至第 10 筆時自動讀取下一頁
                if( totalItemCount > 10 ){
                    totalItemCount = totalItemCount - 10;
                }

                // 確保為向下捲動
                if( dy < 0 ) return;

                // 用來控制重複讀取以及已無新資料的狀況
                if (!isCateLoading && !noCateMoreData) {
                    // 顯示項目已超過十項 自動讀取資料
                    if ((visibleItemCount + pastVisiblesItems) >= totalItemCount) {
                        isCateLoading = true;
                        // 呼叫讀取下一頁資料的 function
                        MainActivity mainActivity = (MainActivity) getActivity();
                        mainActivity.nextCatePage();
                    }
                }
            }
        });

        return view;
    }

    // 設定為讀取中 顯示 loader
    public void cateRefreshing(boolean refreshing){
        if( cateSwipeRefreshLayout == null ) return;

        cateSwipeRefreshLayout.setRefreshing(refreshing);
    }

    // 加入讀取完成的資料
    public boolean addCateItems(Map<Integer, CategoryBean> categoryBeanMap){

        Set<Integer> keySet = categoryBeanMap.keySet();

        for( Integer key : keySet ){
            CategoryBean categoryBean = categoryBeanMap.get(key);
            mCateAdapter.addItem(categoryBean);
            // 因資料建立時是使用 1 based 方式所以需要 - 1
            mCateAdapter.notifyItemChanged(categoryBean.getSerialNum() - 1);
        }

        return false;
    }

    // 資料已加入 設定為可讀取下一頁資料
    public void addCateFinished(){
        isCateLoading = false;
    }

}

到這裡就完成了

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

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

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

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

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

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

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

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

以下是在 Activity 內建立的範例


@InjectView(R.id.pullToRefreshCateRecycler)
private SwipeRefreshLayout cateSwipeRefreshLayout;

@InjectView(R.id.cateRecyclerView)
private RecyclerView cateRecyclerView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

 mCateAdapter = new CateRecyclerAdapter( new LinkedHashMap<Integer, CategoryBean>(), new CateRecyclerAdapter.ItemHolder.IClickHandler() {
     @Override
     public void onClick(View view, int position) {

         CategoryBean categoryBean = mCateAdapter.getItem( position + 1 );

         if( categoryBean == null ) {
             Toast.makeText(view.getContext(), "Find nothing", Toast.LENGTH_SHORT).show();
         }
         else{
             goCateSubList(categoryBean);
         }
     }
 });

 cateSwipeRefreshLayout.setColorSchemeColors(0xFF4FC3F7, 0xFF8558E0, 0xFFFF326F, 0xFFF9F765);

 cateSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
     @Override
     public void onRefresh() {
         mCateAdapter.clear();
         reloadCatePage();
         noCateMoreData = false;
     }
 });

 mCateLinearLayoutManager = new LinearLayoutManager(getActivity());
 mCateLinearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);

 RecyclerView cateRecyclerView = (RecyclerView) view.findViewById(R.id.cateRecyclerView);

 cateRecyclerView.setAdapter(mCateAdapter);
 cateRecyclerView.setItemAnimator(new DefaultItemAnimator());

 cateRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
     @Override
     public void onScrolled(RecyclerView recyclerView, int dx, int dy) {

         int visibleItemCount = mCateLinearLayoutManager.getChildCount();
         int totalItemCount = mCateLinearLayoutManager.getItemCount();
         int pastVisiblesItems = mCateLinearLayoutManager.findFirstVisibleItemPosition();

         if( totalItemCount > 10 ){
             totalItemCount = totalItemCount - 10;
         }

         if( dy < 0 ) return;

         if (!isCateLoading && !noCateMoreData) {
             if ((visibleItemCount + pastVisiblesItems) >= totalItemCount) {
                 isCateLoading = true;
                 nextCatePage();
             }
         }
     }
 });
}

就這樣囉

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

同一個 Adapter 可以重複使用

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

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

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

No comments:

Post a Comment