這篇文章應該是晚到了好幾個月,之前想寫,但是中途遇到了一些棘手的問題,無奈沒有去寫。寫這篇文章的最初來源是一個朋友問我在Android中如何實作瀑布流布局?,當時我的回答是使用RecyclerView,後來他又問我那怎麼實作上拉加載并添加Footer呢?我想都沒想就回答他根據type的不同去添加一個Footer,監聽滾動事件,當滑動到最後顯示footer并回調對應的接口,那麼,這個過程就會遇到一個Footer布局顯示的問題,就像下面這張圖一樣。
可以看到,我們 的 Footer布局并沒有占據全屏,而是作為瀑布流布局的一部分了。這顯然不是我們想要的。
當然,除了為瀑布流布局添加Footer實作上拉加載外,我們還想要對GridLayout添加Footer實作上拉加載,同樣的,在GridLayout中,布局也不是我們想要的結果。效果也是這樣。
也把我們的Footer作為了GridLayout的一部分,并沒有全屏進行顯示。
那麼接下來,我們需要解決幾個問題,即可以實作正确的添加Footer并實作上拉加載功能了。
- 如何實作上拉加載更多?這個解決方法很簡單,就是為RecyclerView添加滾動監聽事件,根據布局的不同編寫對應的邏輯代碼,實作滑到到底部加載更多的功能。
- 如何正确的添加Footer,使其能夠占據寬度為全屏顯示?這個有點棘手,但是并不是不能解決,我們需要對擴充卡進行一些處理就能實作該功能。
- 此外我們還想要添加一個Header,用于展示ViewPager或者圖檔等資訊。這個原理和添加Footer是一樣的。
首先定義一個處理上拉加載的接口
public interface OnLoadMoreListener<T> {
/**
* 加載更多前回調,比如顯示Footer的操作
*/
void onStart();
/**
* 加載更多業務處理,如網絡請求資料
*/
void onLoadMore();
/**
* 由于onLoadMore可能是異步調用的,是以onFinish需要手動調用,完成資料的重新整理,隐藏Footer等
* @param list onLoadMore中傳回的資料
*/
void onFinish(List<T> list);
}
然後我們自己定義一個抽象類,用于繼承RecyclerView.OnScrollListener并實作我們定義的接口OnLoadMoreListener,如果需要上拉加載更多,直接為RecyclerView添加滾動監聽為我們的實作類即可,就像這樣子
mRecyclerView.addOnScrollListener(new OnRecyclerViewScrollListener<Content>(){
@Override
public void onStart() {
}
@Override
public void onLoadMore() {
}
@Override
public void onFinish(List<Content> contents) {
}
});
由于RecyclerView預設有三種布局,是以我們要對這三種布局分别進行判斷上拉加載,處理的邏輯有點不同,首先添加如下定義
public abstract class OnRecyclerViewScrollListener<T extends RecyclerViewAdapter.Item> extends RecyclerView.OnScrollListener implements OnLoadMoreListener<T> {
public static enum layoutManagerType {
LINEAR_LAYOUT,
GRID_LAYOUT,
STAGGERED_GRID_LAYOUT
}
protected layoutManagerType mLayoutManagerType;
private boolean mIsLoadingMore = false;
public boolean isLoadingMore() {
return mIsLoadingMore;
}
public void setLoadingMore(boolean loadingMore) {
mIsLoadingMore = loadingMore;
}
}
這個類是泛型的,接收一個實作了Item接口的類。主要是定義了一個枚舉類,裡面是布局的類型,然後是一個布爾變量,用于判斷目前是否正在加載更多。
RecyclerViewAdapter.Item主要是一個接口,其定義如下
public interface Item {
int TYPE_HEADER = ;
int TYPE_FOOTER = ;
/**
* 傳回item類型,其值不能為0或者1;
*
* @return
*/
int getType();
}
我們的RecyclerView的Item實體類需要實作Item接口,并返還item的類型,預設情況下header的類型為0,footer的類型為1。
接下來最重要的事就是實作onScrolled和onScrollStateChanged方法,根據布局的不同判斷是否需要加載更多操作。
private int[] lastPositions;
private int lastVisibleItemPosition;
private int currentScrollState = ;
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (mLayoutManagerType == null) {
if (layoutManager instanceof LinearLayoutManager) {
mLayoutManagerType = layoutManagerType.LINEAR_LAYOUT;
} else if (layoutManager instanceof GridLayoutManager) {
mLayoutManagerType = layoutManagerType.GRID_LAYOUT;
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
mLayoutManagerType = layoutManagerType.STAGGERED_GRID_LAYOUT;
} else {
throw new RuntimeException("Unsupported LayoutManager used. Valid ones are LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager");
}
}
switch (mLayoutManagerType) {
case LINEAR_LAYOUT:
lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
break;
case GRID_LAYOUT:
lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
break;
case STAGGERED_GRID_LAYOUT:
StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
if (lastPositions == null) {
lastPositions = new int[staggeredGridLayoutManager.getSpanCount()];
}
staggeredGridLayoutManager.findLastVisibleItemPositions(lastPositions);
lastVisibleItemPosition = findMax(lastPositions);
break;
default:
break;
}
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
currentScrollState = newState;
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
int visibleItemCount = layoutManager.getChildCount();
int totalItemCount = layoutManager.getItemCount();
if (visibleItemCount > && currentScrollState == RecyclerView.SCROLL_STATE_IDLE
&& lastVisibleItemPosition >= totalItemCount - ) {
if (!isLoadingMore()){
mIsLoadingMore =true;
onStart();
onLoadMore();
}
}
}
private int findMax(int[] lastPositions) {
int max = lastPositions[];
for (int value : lastPositions) {
if (value > max) {
max = value;
}
}
return max;
}
具體邏輯見代碼,LinearLayoutManager 和 GridLayoutManager的處理邏輯類似,隻不過StaggeredGridLayoutManager 的處理稍微複雜一點,因為布局是錯亂的,是以需要自己找到最底下的布局是哪一個,關鍵代碼就是這兩句
staggeredGridLayoutManager.findLastVisibleItemPositions(lastPositions);
lastVisibleItemPosition = findMax(lastPositions);
就這樣子,其實已經出現了上拉加載更多的功能了,這時候你使用一下這個滾動監聽,是完全沒有什麼問題的,隻不過沒有顯示Footer布局而已。接下來我們最重要的事就是改造擴充卡。
public abstract class RecyclerViewAdapter<T extends RecyclerViewAdapter.Item> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
public interface Item {
int TYPE_HEADER = ;
int TYPE_FOOTER = ;
/**
* 傳回item類型,其值不能為0或者1;
*
* @return
*/
int getType();
}
}
這是最基本的結構,内部定義了上面提到的Item接口,我們的Item實體類需要實作該接口,用于判斷Item的類型。
定義Getter和Setter方法
protected List<T> list = null;
protected int headerViewRes;
protected int footerViewRes;
protected boolean hasHeader = false;
protected boolean hasFooter = false;
public List<T> getList() {
return list;
}
public void setList(List<T> list) {
this.list = list;
}
public boolean isHeader(int position) {
return hasHeader() && position == ;
}
public boolean isFooter(int position) {
if(hasHeader()){
return hasFooter() && position == list.size() + ;
}else {
return hasFooter() && position == list.size();
}
}
public int getHeaderView() {
return headerViewRes;
}
public int getFooterView() {
return footerViewRes;
}
public void setHeaderView(int headerViewRes) {
if (headerViewRes != ) {
if (!hasHeader()){
this.headerViewRes = headerViewRes;
this.hasHeader = true;
notifyItemInserted();
}else{
this.headerViewRes = headerViewRes;
notifyDataSetChanged();
}
} else {
if (hasHeader()){
this.hasHeader = false;
notifyItemRemoved();
}
}
}
public void setFooterView(int footerViewRes) {
if (footerViewRes != ) {
if (!hasFooter()){
this.footerViewRes = footerViewRes;
this.hasFooter = true;
if (hasHeader()){
notifyItemInserted(list.size()+);
}else{
notifyItemInserted(list.size());
}
}else{
this.footerViewRes = footerViewRes;
notifyDataSetChanged();
}
} else {
if(hasFooter()){
this.hasFooter = false;
if (hasHeader()){
notifyItemRemoved(list.size()+);
}else{
notifyItemRemoved(list.size());
}
}
}
}
public boolean hasHeader() {
return hasHeader;
}
public boolean hasFooter() {
return hasFooter;
}
内部邏輯看上去一大堆,其實并不複雜,關鍵是需要判斷Header存不存在,Header存在與不存在的情況下Footer的位置是不同的,注意這一點,編寫對應的邏輯即可,當然你的邏輯可以與我不同。
接下來是構造函數,傳入我們的資料集,Header和Footer的布局資源
public RecyclerViewAdapter(List<T> list) {
this.list = list;
}
public RecyclerViewAdapter(List<T> list, int headerViewRes) {
this.list = list;
setHeaderView(headerViewRes);
}
public RecyclerViewAdapter(List<T> list, int headerViewRes, int footerViewRes) {
this.list = list;
setHeaderView(headerViewRes);
setFooterView(footerViewRes);
}
實作我們的Header布局和Footer布局的ViewHolder,其實就是定義兩個類
static class HeaderViewHolder extends RecyclerView.ViewHolder {
public HeaderViewHolder(View itemView) {
super(itemView);
}
}
static class FooterViewHolder extends RecyclerView.ViewHolder {
public FooterViewHolder(View itemView) {
super(itemView);
}
}
重寫getItemCount和getItemViewType方法
getItemCount中我們需要根據是否有Header和Footer來傳回對應的Item數
@Override
public int getItemCount() {
int count = ;
count += (hasHeader() ? : );
count += (hasFooter() ? : );
count += list.size();
return count;
}
getItemViewType就需要根據判斷位置判斷是否具有Header來判斷對應的Item的類型
@Override
public int getItemViewType(int position) {
int size = list.size();
if (hasHeader()) {
if (position == ) {
return Item.TYPE_HEADER;
} else {
if (position == size + ) {
return Item.TYPE_FOOTER;
} else {
return list.get(position - ).getType();
}
}
} else {
if (position == size) {
return Item.TYPE_FOOTER;
} else {
return list.get(position).getType();
}
}
}
建立ViewHolder,根據類型的不同建立對應的ViewHolder,如果不是Header和Footer之外的類型,交由抽象方法onCreateHolder處理
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (hasHeader() && viewType == Item.TYPE_HEADER) {
View v = LayoutInflater.from(parent.getContext()).inflate(getHeaderView(), parent, false);
return new HeaderViewHolder(v);
} else if (hasFooter() && viewType == Item.TYPE_FOOTER) {
View v = LayoutInflater.from(parent.getContext()).inflate(getFooterView(), parent, false);
return new FooterViewHolder(v);
} else {
return onCreateHolder(parent, viewType);
}
}
public abstract RecyclerView.ViewHolder onCreateHolder(ViewGroup parent, int viewType);
綁定資料,同建立ViewHolder,根據位置的不同來獲得item的類型,如果是Header就回調抽象方法onBindHeaderView,如果是Footer就回調抽象方法onBindFooterView,否則就回調抽象方法onBindItemView,将對應的holder和實體類傳入。
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (getItemViewType(position) == Item.TYPE_HEADER) {
HeaderViewHolder headerHolder = (HeaderViewHolder) holder;
View headerView = headerHolder.itemView;
onBindHeaderView(headerView);
} else if (getItemViewType(position) == Item.TYPE_FOOTER) {
FooterViewHolder footerHolder = (FooterViewHolder) holder;
View footerView = footerHolder.itemView;
onBindFooterView(footerView);
} else {
T i = getItemByPosition(position);
onBindItemView(holder, i);
}
}
protected abstract void onBindHeaderView(View headerView);
protected abstract void onBindFooterView(View footerView);
protected abstract void onBindItemView(RecyclerView.ViewHolder holder, T item);
這樣子,已經能夠處理Header和Footer了,但是顯示位置還是不正确的,接下來我們需要對GridLayout和StaggeredGridLayout做特殊處理。
定義抽象類GridLayoutAdapter繼承RecyclerViewAdapter
public abstract class GridLayoutAdapter<T extends RecyclerViewAdapter.Item> extends RecyclerViewAdapter<T> {
public GridLayoutAdapter(List list) {
super(list);
}
public GridLayoutAdapter(List list, int headerViewRes) {
super(list, headerViewRes);
}
public GridLayoutAdapter(List list, int headerViewRes, int footerViewRes) {
super(list, headerViewRes, footerViewRes);
}
}
定義一個内部類GridSpanSizeLookup 繼承GridLayoutManager.SpanSizeLookup,調用父類isHeader和isFooter方法判斷是否是頭或者尾,如果是則傳回gridManager.getSpanCount();即一個item占據一行的span數,否則就傳回1
class GridSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
@Override
public int getSpanSize(int position) {
if (isHeader(position) || isFooter(position)) {
return gridManager.getSpanCount();
}
return ;
}
}
最重要的一步就是重寫onAttachedToRecyclerView,判斷是否是GridLayout布局,然後通過setSpanSizeLookup設定為我們的内部類
private GridSpanSizeLookup mGridSpanSizeLookup;
private GridLayoutManager gridManager;
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
if (manager instanceof GridLayoutManager) {
gridManager = ((GridLayoutManager) manager);
if (mGridSpanSizeLookup == null) {
mGridSpanSizeLookup = new GridSpanSizeLookup();
}
gridManager.setSpanSizeLookup(mGridSpanSizeLookup);
}
}
同理,瀑布流布局也需要進行同樣的操作。
public abstract class StaggeredGridLayoutAdapter<T extends RecyclerViewAdapter.Item> extends RecyclerViewAdapter<T> {
public StaggeredGridLayoutAdapter(List<T> list) {
super(list);
}
public StaggeredGridLayoutAdapter(List<T> list, int headerViewRes) {
super(list, headerViewRes);
}
public StaggeredGridLayoutAdapter(List<T> list, int headerViewRes, int footerViewRes) {
super(list, headerViewRes, footerViewRes);
}
}
但是 StaggeredGridLayoutManager中沒有setSpanSizeLookup方法,慶幸的是StaggeredGridLayoutManager.LayoutParams中有setFullSpan方法可以達到同樣的效果。
這時候重寫的不再是onAttachedToRecyclerView方法而是onViewAttachedToWindow方法
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
if (isStaggeredGridLayout(holder)) {
handleLayoutIfStaggeredGridLayout(holder, holder.getLayoutPosition());
}
}
private boolean isStaggeredGridLayout(RecyclerView.ViewHolder holder) {
ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
if (layoutParams != null && layoutParams instanceof StaggeredGridLayoutManager.LayoutParams) {
return true;
}
return false;
}
protected void handleLayoutIfStaggeredGridLayout(RecyclerView.ViewHolder holder, int position) {
if (isHeader(position) || isFooter(position)) {
StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams();
p.setFullSpan(true);
}
}
基本上,到這裡為止,就完成了所有的工作,在使用的時候要實作上拉加載顯示Footer,如果是瀑布流布局,就需要繼承StaggeredGridLayoutAdapter,如果是網格布局,就需要繼承GridLayoutAdapter,其他情況下,繼承RecyclerViewAdapter即可。
為了示範,這裡簡單進行使用,首先定義一個Item的實作類
public class Content implements RecyclerViewAdapter.Item {
private int TYPE = ;
private String title;
private String desc;
private String url;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public String getIconUrl() {
return url;
}
public void setIconUrl(String iconUrl) {
this.url = iconUrl;
}
@Override
public String toString() {
return "Content{" +
"title='" + title + '\'' +
", desc='" + desc + '\'' +
", icon=" + url +
'}';
}
@Override
public int getType() {
return TYPE;
}
}
我們這裡以瀑布流布局為例,是以繼承StaggeredGridLayoutAdapter實作我們的擴充卡。
public class MyAdapter extends StaggeredGridLayoutAdapter<Content> {
public MyAdapter(List<Content> list, int headerViewRes) {
super(list, headerViewRes);
}
public MyAdapter(List<Content> list) {
super(list);
}
public MyAdapter(List<Content> list, int headerViewRes, int footerViewRes) {
super(list, headerViewRes, footerViewRes);
}
@Override
public RecyclerView.ViewHolder onCreateHolder(ViewGroup parent, int viewType) {
View view=LayoutInflater.from(parent.getContext()).inflate(R.layout.item_content,parent,false);
return new ItemViewHolder(view);
}
@Override
protected void onBindHeaderView(View headerView) {
Log.e("TAG","這是HeadView資料綁定的過程");
ImageView imageView= (ImageView) headerView.findViewById(R.id.icon);
Picasso.with(headerView.getContext()).load("https://img-my.csdn.net/uploads/201508/05/1438760758_3497.jpg").into(imageView);
}
@Override
protected void onBindFooterView(View footerView) {
Log.e("TAG","這是FootView資料綁定的過程");
}
@Override
protected void onBindItemView(RecyclerView.ViewHolder holder, Content item) {
ItemViewHolder itemViewHolder = (ItemViewHolder) holder;
Picasso.with(holder.itemView.getContext()).load(item.getIconUrl()).into( itemViewHolder.icon);
itemViewHolder.title.setText(item.getTitle());
itemViewHolder.desc.setText(item.getDesc());
}
static class ItemViewHolder extends RecyclerView.ViewHolder {
ImageView icon;
TextView title;
TextView desc;
public ItemViewHolder(View itemView) {
super(itemView);
icon = (ImageView) itemView.findViewById(R.id.icon);
title = (TextView) itemView.findViewById(R.id.title);
desc = (TextView) itemView.findViewById(R.id.desc);
}
}
}
使用也很簡單,在onStart中顯示footer,在onLoadMore中加載資料,這裡是模拟操作,異步傳回資料後将資料傳入onFinish進行回調,回調完成後記得調用 setLoadingMore(false);來通知目前處于沒在加載的狀态,通過Handler發送資料到主線程進行UI更新,并是以Footer
public class MainActivity extends AppCompatActivity {
private RecyclerView mRecyclerView;
private List<Content> list = new ArrayList<Content>();
private RecyclerViewAdapter<Content> myAdapter;
private ArrayList<Content> arrayList;
Handler mHandler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
List<Content> list= (List<Content>) msg.obj;
myAdapter.getList().addAll(list);
myAdapter.notifyDataSetChanged();
myAdapter.setFooterView();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecyclerView = (RecyclerView) findViewById(R.id.recyclerview);
initData();
//mRecyclerView.setLayoutManager(new LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false));
mRecyclerView.setLayoutManager(new GridLayoutManager(this,));
// mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(2,StaggeredGridLayoutManager.VERTICAL));
myAdapter = new MyAdapter(list);
//myAdapter.setHeaderView(R.layout.item_header);
//myAdapter.setFooterView(R.layout.item_footer);
mRecyclerView.setAdapter(myAdapter);
arrayList=new ArrayList<Content>(myAdapter.getList());
mRecyclerView.addOnScrollListener(new OnRecyclerViewScrollListener<Content>(){
@Override
public void onStart() {
myAdapter.setFooterView(R.layout.item_footer);
if (myAdapter.hasHeader()){
mRecyclerView.smoothScrollToPosition(myAdapter.getItemCount()+);
}else{
mRecyclerView.smoothScrollToPosition(myAdapter.getItemCount());
}
}
@Override
public void onLoadMore() {
new Thread(new Runnable() {
@Override
public void run() {
try {
Log.e("TAG","模拟網絡請求資料");
Thread.sleep();
//手動調用onFinish()
onFinish(arrayList);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
@Override
public void onFinish(List<Content> contents) {
Message message=Message.obtain();
message.obj=contents;
mHandler.sendMessage(message);
setLoadingMore(false);
}
});
}
private void initData() {
Content c = new Content();
c.setIconUrl("http://p1.meituan.net/63.90/movie/7a29814fe6549b929df6e0ef9575ce699434172.jpg");
c.setTitle("搖滾水果");
c.setDesc("比基尼女郎,掀搖滾熱浪。濱江區濱文路577号華潤超市4樓。");
list.add(c);
//類似這樣的添加資料的過程,還有很多資料。。這裡不貼出來了
}
}
最終的效果看動圖,如下
最後上源代碼
http://download.csdn.net/detail/sbsujjbcy/9312425