RecyclerView實例-實現可下拉刷新上拉加載更多並可切換線性流和瀑布流模式(1)

摘要

最近項目有個列表頁須要實現線性列表和瀑布流展現的切換,首先我想到的就是上
[RecyclerView],他自己已經很好的提供了三種佈局方式,只是簡單作個切換應該是很簡單的事情,若是要用RecyclerView的方式來實現,那就是目前的設計方案(listView)都不能用,更改成RecyclerView,我須要作以下工做:java

  • 下拉刷新,
  • 上拉自動加載更多,
  • 同時支持切換佈局方式。

自定義RecyclerView實現自動加載

爲了實現自動加載更多的功能,我選擇自定義實現一個特殊的RecyclerView來實現,網絡上也有自定義一個包含RecyclerView和loading_more的佈局,如今我想試試adapter處理的方式,因此有了這個自定義實現類android

java片斷1: RecyclerView實現LoadMoreRecyclerViewgit

/**
 * Alipay.com Inc.
 * Copyright (c) 2004-2015 All Rights Reserved.
 */
package com.leaf8.alicx.myapplication;

import android.content.Context;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

/**
 * 支持上拉加載更多的
 *
 * @author 肖肖
 * @version $$Id: LoadMoreRecyclerView.java, v 0.1 11/17/15 10:07 alicx Exp $$
 */
public class LoadMoreRecyclerView extends RecyclerView {
    /**
     * item 類型
     */
    public final static int TYPE_NORMAL = 0;
    public final static int TYPE_HEADER = 1;//頭部--支持頭部增長一個headerView
    public final static int TYPE_FOOTER = 2;//底部--每每是loading_more
    public final static int TYPE_LIST = 3;//表明item展現的模式是list模式
    public final static int TYPE_STAGGER = 4;//代碼item展現模式是網格模式

    private boolean mIsFooterEnable = false;//是否容許加載更多

    /**
     * 自定義實現了頭部和底部加載更多的adapter
     */
    private AutoLoadAdapter mAutoLoadAdapter;
    /**
     * 標記是否正在加載更多,防止再次調用加載更多接口
     */
    private boolean mIsLoadingMore;
    /**
     * 標記加載更多的position
     */
    private int mLoadMorePosition;
    /**
     * 加載更多的監聽-業務須要實現加載數據
     */
    private LoadMoreListener mListener;

    public LoadMoreRecyclerView(Context context) {
        super(context);
        init();
    }

    public LoadMoreRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public LoadMoreRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    /**
     * 初始化-添加滾動監聽
     * <p/>
     * 回調加載更多方法,前提是
     * <pre>
     *    一、有監聽而且支持加載更多:null != mListener && mIsFooterEnable
     *    二、目前沒有在加載,正在上拉(dy>0),當前最後一條可見的view是不是當前數據列表的最好一條--及加載更多
     * </pre>
     */
    private void init() {
        super.addOnScrollListener(new OnScrollListener() {

            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                if (null != mListener && mIsFooterEnable && !mIsLoadingMore && dy > 0) {
                    int lastVisiblePosition = getLastVisiblePosition();
                    if (lastVisiblePosition + 1 == mAutoLoadAdapter.getItemCount()) {
                        setLoadingMore(true);
                        mLoadMorePosition = lastVisiblePosition;
                        mListener.onLoadMore();
                    }
                }
            }
        });
    }

    /**
     * 設置加載更多的監聽
     *
     * @param listener
     */
    public void setLoadMoreListener(LoadMoreListener listener) {
        mListener = listener;
    }

    /**
     * 設置正在加載更多
     *
     * @param loadingMore
     */
    public void setLoadingMore(boolean loadingMore) {
        this.mIsLoadingMore = loadingMore;
    }

    /**
     * 加載更多監聽
     */
    public interface LoadMoreListener {
        /**
         * 加載更多
         */
        void onLoadMore();
    }

    /**
     *
     */
    public class AutoLoadAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

        /**
         * 數據adapter
         */
        private RecyclerView.Adapter mInternalAdapter;

        private boolean mIsHeaderEnable;
        private int mHeaderResId;

        public AutoLoadAdapter(RecyclerView.Adapter adapter) {
            mInternalAdapter = adapter;
            mIsHeaderEnable = false;
        }

        @Override
        public int getItemViewType(int position) {
            int headerPosition = 0;
            int footerPosition = getItemCount() - 1;

            if (headerPosition == position && mIsHeaderEnable && mHeaderResId > 0) {
                return TYPE_HEADER;
            }
            if (footerPosition == position && mIsFooterEnable) {
                return TYPE_FOOTER;
            }
            /**
             * 這麼作保證layoutManager切換以後能及時的刷新上對的佈局
             */
            if (getLayoutManager() instanceof LinearLayoutManager) {
                return TYPE_LIST;
            } else if (getLayoutManager() instanceof StaggeredGridLayoutManager) {
                return TYPE_STAGGER;
            } else {
                return TYPE_NORMAL;
            }
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            if (viewType == TYPE_HEADER) {
                return new HeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(
                        mHeaderResId, parent, false));
            }
            if (viewType == TYPE_FOOTER) {
                return new FooterViewHolder(
                        LayoutInflater.from(parent.getContext()).inflate(
                                R.layout.list_foot_loading, parent, false));
            } else { // type normal
                return mInternalAdapter.onCreateViewHolder(parent, viewType);
            }
        }

        public class FooterViewHolder extends RecyclerView.ViewHolder {

            public FooterViewHolder(View itemView) {
                super(itemView);
            }
        }

        public class HeaderViewHolder extends RecyclerView.ViewHolder {
            public HeaderViewHolder(View itemView) {
                super(itemView);
            }
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, int position) {
            int type = getItemViewType(position);
            if (type != TYPE_FOOTER && type != TYPE_HEADER) {
                mInternalAdapter.onBindViewHolder(holder, position);
            }
        }

        /**
         * 須要計算上加載更多和添加的頭部倆個
         *
         * @return
         */
        @Override
        public int getItemCount() {
            int count = mInternalAdapter.getItemCount();
            if (mIsFooterEnable) count++;
            if (mIsHeaderEnable) count++;

            return count;
        }

        public void setHeaderEnable(boolean enable) {
            mIsHeaderEnable = enable;
        }

        public void addHeaderView(int resId) {
            mHeaderResId = resId;
        }
    }

    @Override
    public void setAdapter(RecyclerView.Adapter adapter) {
        if (adapter != null) {
            mAutoLoadAdapter = new AutoLoadAdapter(adapter);
        }
        super.swapAdapter(mAutoLoadAdapter, true);
    }

    /**
     * 切換layoutManager
     *
     * 爲了保證切換以後頁面上仍是停留在當前展現的位置,記錄下切換以前的第一條展現位置,切換完成以後滾動到該位置
     * 另外切換以後必需要從新刷新下當前已經緩存的itemView,不然會出現佈局錯亂(倆種模式下的item佈局不一樣),
     * RecyclerView提供了swapAdapter來進行切換adapter並清理老的itemView cache
     *
     * @param layoutManager
     */
    public void switchLayoutManager(LayoutManager layoutManager) {
        int firstVisiblePosition = getFirstVisiblePosition();
//        getLayoutManager().removeAllViews();
        setLayoutManager(layoutManager);
        //super.swapAdapter(mAutoLoadAdapter, true);
        getLayoutManager().scrollToPosition(firstVisiblePosition);
    }

    /**
     * 獲取第一條展現的位置
     *
     * @return
     */
    private int getFirstVisiblePosition() {
        int position;
        if (getLayoutManager() instanceof LinearLayoutManager) {
            position = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();
        } else if (getLayoutManager() instanceof GridLayoutManager) {
            position = ((GridLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();
        } else if (getLayoutManager() instanceof StaggeredGridLayoutManager) {
            StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) getLayoutManager();
            int[] lastPositions = layoutManager.findFirstVisibleItemPositions(new int[layoutManager.getSpanCount()]);
            position = getMinPositions(lastPositions);
        } else {
            position = 0;
        }
        return position;
    }

    /**
     * 得到當前展現最小的position
     *
     * @param positions
     * @return
     */
    private int getMinPositions(int[] positions) {
        int size = positions.length;
        int minPosition = Integer.MAX_VALUE;
        for (int i = 0; i < size; i++) {
            minPosition = Math.min(minPosition, positions[i]);
        }
        return minPosition;
    }

    /**
     * 獲取最後一條展現的位置
     *
     * @return
     */
    private int getLastVisiblePosition() {
        int position;
        if (getLayoutManager() instanceof LinearLayoutManager) {
            position = ((LinearLayoutManager) getLayoutManager()).findLastVisibleItemPosition();
        } else if (getLayoutManager() instanceof GridLayoutManager) {
            position = ((GridLayoutManager) getLayoutManager()).findLastVisibleItemPosition();
        } else if (getLayoutManager() instanceof StaggeredGridLayoutManager) {
            StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) getLayoutManager();
            int[] lastPositions = layoutManager.findLastVisibleItemPositions(new int[layoutManager.getSpanCount()]);
            position = getMaxPosition(lastPositions);
        } else {
            position = getLayoutManager().getItemCount() - 1;
        }
        return position;
    }

    /**
     * 得到最大的位置
     *
     * @param positions
     * @return
     */
    private int getMaxPosition(int[] positions) {
        int size = positions.length;
        int maxPosition = Integer.MIN_VALUE;
        for (int i = 0; i < size; i++) {
            maxPosition = Math.max(maxPosition, positions[i]);
        }
        return maxPosition;
    }

    /**
     * 添加頭部view
     *
     * @param resId
     */
    public void addHeaderView(int resId) {
        mAutoLoadAdapter.addHeaderView(resId);
    }

    /**
     * 設置頭部view是否展現
     * @param enable
     */
    public void setHeaderEnable(boolean enable) {
        mAutoLoadAdapter.setHeaderEnable(enable);
    }

    /**
     * 設置是否支持自動加載更多
     *
     * @param autoLoadMore
     */
    public void setAutoLoadMoreEnable(boolean autoLoadMore) {
        mIsFooterEnable = autoLoadMore;
    }

    /**
     * 通知更多的數據已經加載
     *
     * 每次加載完成以後添加了Data數據,用notifyItemRemoved來刷新列表展現,
     * 而不是用notifyDataSetChanged來刷新列表
     *
     * @param hasMore
     */
    public void notifyMoreFinish(boolean hasMore) {
        setAutoLoadMoreEnable(hasMore);
        getAdapter().notifyItemRemoved(mLoadMorePosition);
        mIsLoadingMore = false;
    }
}

實現原理是利用RecyclerView.Adapter的getItemType來區別不同的item佈局,這裏是實現了一個支持頭部和底部的額外itemVIew的apdater殼子,將數據的adapter放到這個殼子中去代理,保證業務方實現的adapter的純淨。
另外LoadMoreRecyclerView管理了5個itemType,除了TYPE_HEADER和TYPE_FOOTER以外,其他三個會傳遞給業務方的onCreateViewHolder方法,業務方根據這個參數決定使用佈局,固然業務方頁能夠用本身的方式實現判斷條件:github

java片斷2: 業務方可能的onCreateViewHolder實現緩存

@Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == LoadMoreRecyclerView.TYPE_STAGGER) {
            View view = LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.fragment_item_staggel, parent, false);
            return new StaggerViewHolder(view);
        } else {
            View view = LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.fragment_item, parent, false);
            return new ViewHolder(view);
        }
    }

實現下拉刷新

下拉刷新的實現,我也是直接用了google提供的support-v4包種的SwipeRefreshLayout,只要實現下拉刷新的獲取數據動做便可,網絡

layout1:架構

<android.support.v4.widget.SwipeRefreshLayout
    android:id="@+id/refresh_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.leaf8.alicx.myapplication.LoadMoreRecyclerView
         android:id="@+id/list"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         app:layoutManager="LinearLayoutManager"
         tools:listitem="@layout/fragment_item" />
</android.support.v4.widget.SwipeRefreshLayout>

java片斷3: refreshlistenerapp

swipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
     @Override
     public void onRefresh() {
           swipeRefreshLayout.setRefreshing(false);
           page = 0;
           myItemRecyclerViewAdapter.setData(DummyContent.generyData(page));
           recyclerView.setAutoLoadMoreEnable(DummyContent.hasMore(page));
           myItemRecyclerViewAdapter.notifyDataSetChanged();
      }
});

SwipeRefreshLayout沒什麼能夠多說的,你們都知道的東西。ide

實現上拉加載更多

我本身實現了一個LoadMoreRecyclerView,來完成自動加載的功能。代碼見#java片斷1#,這個LoadMoreRecyclerView須要業務方設置LoadMoreListener,當分頁結束還須要設置再也不繼續自動加載,LoadMoreRecyclerView開放了一個接口notifyMoreFinish來支持該設置。佈局

java片斷4

recyclerView.setLoadMoreListener(new LoadMoreRecyclerView.LoadMoreListener() {
      @Override
      public void onLoadMore() {
          recyclerView.postDelayed(new Runnable() {
               @Override
               public void run() {
                    swipeRefreshLayout.setRefreshing(false);
                    myItemRecyclerViewAdapter.addDatas(DummyContent.generyData(++page));
                    recyclerView.notifyMoreFinish(DummyContent.hasMore(page));
               }
          }, 1000);
     }
});

這樣完了以後,就能實現上拉自動加載,而且加載中還有一個加載中...顯示。

在notifyMoreFinish方法中爲了實現新數據被刷新到頁面上,用了adapter.notifyItemRemoved(lastvisiblePosition),發現用notifyDataSetChanged在此時不能起做用,緣由應該是加載中...這個,在列表中是一個正常的view,而新數據加上以後,實際上是在這個view的位置插入數據的操做,因此此處adapter.notifyItemInsert/adapter.notifyItemRemoved都能起做用,可是notifyDataSetChanged不會從新執行該position的CreateView(緣由應該是他認爲該position的view已經被緩存了,因此直接展現了緩存中的view)。這點在平常運用中實現對數據的增刪的時候也有遇到。

實現線性及瀑布流切換

新版列表還要作的一個事情是要實現瀑布流和線性流的切換,這個開始作以前我認爲應該很簡單,可是當我真的去實施的時候,發現其實還有不少細節問題。

demo中我只是對這個切換簡單的作了一個文案按鈕,點擊實現切換:

java片斷5

if (1 == mColumnCount) {
    mColumnCount = 2;
    ((TextView) v).setText(R.string.list_mode_stagger);
    myItemRecyclerViewAdapter.switchMode(true);
    recyclerView.switchLayoutManager(new StaggeredGridLayoutManager(mColumnCount, StaggeredGridLayoutManager.VERTICAL));
} else {
     mColumnCount = 1;
     ((TextView) v).setText(R.string.list_mode_list);
     myItemRecyclerViewAdapter.switchMode(false);
     recyclerView.switchLayoutManager(new LinearLayoutManager(getActivity()));
}

看看switchLayoutManager方法實現:

java片斷6

int firstVisiblePosition = getFirstVisiblePosition();
//        getLayoutManager().removeAllViews();
setLayoutManager(layoutManager);
//super.swapAdapter(mAutoLoadAdapter, true);
getLayoutManager().scrollToPosition(firstVisiblePosition);

實現原則就是切換佈局以後,頁面仍是停留在當前瀏覽的position位置,因此切換以前,我得先記錄下當前的第一個可見的item的位置,代碼不貼了,就看LoadMoreRecyclerView.getFirstVisiblePosition()方法。而後我setLayoutManager設置佈局,而後我經過scrollToPosition滾動到先前停留的位置,結果發現切換以後頁面上會出現倆種佈局錯亂。恩應該是LoadMoreRecyclerView中還保留了先前佈局的item view cache致使,必需要讓他知道個人模板已通過時了須要從新生成了,經過什麼方法告知我須要刷新佈局呢,這個我嘗試了幾種方法,如上註釋掉的,可是仍是不頂用,最後我想到adapter.getItemType(),這個應該可行,因而我針對倆種佈局添加了倆個類型TYPE_LIST和TYPE_STAGGER,傳遞到業務的onCreateViewHolder,讓業務知道當前的不一樣佈局標識,結果證明該方案可行。

待解問題 解決了切換佈局混亂的問題,又遇到了一個新問題,從list切換到StaggerGride,若是當前的position是在第二屏,切換過來以後,滑動到第一屏發現瀑布流的第一行倆個列是不對齊的,右邊一列會滑動上去的一個過程。這個時候再上拉翻到第三屏就會出現一個IndexOutOfBoundsException的crash異常,可是當我切換過來以後,不去下滑,而是繼續上拉,則不會出現這個問題,可是下滑到第一屏仍是會有一個第二列往上移動的過程。感受是這個移動的過程形成了後續數據的一個position錯亂,目前還在排查這個問題

無圖無真相

  • 列表模式

列表模式

  • 瀑布流模式

瀑布流模式

  • 問題:切換以後滑動到第一屏出現不對齊現象

切換以後滑動到第一屏出現不對齊現象

總結

RecyclerView某些狀況下確實很好用的,很方便的就實現了一些特性。可是由於是個新東西,不少方面仍是欠缺了一些成熟性,不可控性,在實際應用中仍是須要多多演練才行。

Demo地址

打上個人demo地址:供你們參考-->

:由於業務須要,該業務涉及的list須要放到Fragment中實現,因此本demo也是在這種架構中實現

相關文章
相關標籤/搜索