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