【android】自定义组合控件PullToRefreshRecyeclerView

场景:自从Android 5.0发布以来,越来越多的开发者开始接触RecyeclerView,但是RecyclerView如何实现下拉刷新,上拉加在更多。于是我就偷懒 写了一个,以供大家参考和学习,以待大家改进。

技术分享


构思:想必大家对SwipeRefreshLayout这个控件有一定了解,没错本次自定义组合控件也就是SwipeRefreshLayout与RecyeclerView的组合。

那么我们一步一步来实现:

1.首先写一个组合布局如下:pulltorefreshrecyclerview.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    android:orientation="vertical" >


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

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scrollbars="vertical" />
    </android.support.v4.widget.SwipeRefreshLayout>

</LinearLayout>
2.声明一个类PulltoRefreshRecyclerView.java继承自LinearLayout

a.声明一个接口

public interface RefreshLoadMoreListener {
		public void onRefresh();

		public void onLoadMore();
	}
在Activity或者Fragment中去实现PulltoRefreshRecyclerView.RefreshLoadMoreListener接口

PulltoRefreshRecyclerView调用刷新和加在更多,实质上是回调Activity或者Fragment实现的接口方法。

b.即 在PulltoRefreshRecyclerView有loadMore和refresh方法

public void loadMore() {
		if (mRefreshLoadMoreListner != null && hasMore) {
			mRefreshLoadMoreListner.onLoadMore();
			
		}
	}
public void refresh() {
		if (mRefreshLoadMoreListner != null) {
			mRefreshLoadMoreListner.onRefresh();
		}
	}
c.那么就需要拿到这个实现的刷新加载监听实例

PulltoRefreshRecyclerView实现一个设置监听的方法

public void setRefreshLoadMoreListener(RefreshLoadMoreListener listener) {
		mRefreshLoadMoreListner = listener;
	}

d.RecyeclerView是基于Adapter的开发,那么这里也需要可以设置Adapter

public void setAdapter(RecyclerView.Adapter adapter) {
		if (adapter != null)
			recyclerView.setAdapter(adapter);
	}

e.考虑一下特殊的需求,有时禁止刷新,有时禁止加载更多那么我们来完善一下

public void setPullLoadMoreEnable(boolean enable) {
		this.hasMore = enable;
	}

public boolean getPullLoadMoreEnable() {
		return hasMore;
	}

public void setPullRefreshEnable(boolean enable) {
		swipeRfl.setEnabled(enable);
	}

public boolean getPullRefreshEnable() {
		return swipeRfl.isEnabled();
	}
只要对相应的enable传入true或false则可进行开关控制

f.RecyeclerView可以是水平也可以是垂直,把这项功能也加入一下默认我们就让它为垂直

技术分享

public void setOrientation(int orientation) {
		if (orientation != 0 && orientation != 1) {
			layoutManager.setOrientation(VERTICAL);
		} else {
			layoutManager.setOrientation(HORIZONTAL);
		}
	}

public int getOrientation() {
		return layoutManager.getOrientation();
	}

7.SwipeRefreshLayout的停止刷新效果

public void stopRefresh() {
		isRefresh = false;
		swipeRfl.setRefreshing(false);
	}

g.刷新原理和加载更多原理

刷新实际上是添加SwipeRefreshLayout的OnRefreshListener监听,在符合!isRefresh的前提下回调RefreshLoadMoreListener的onRefresh方法

加载更多原理是添加RecyeclerView的OnScrollListener监听,在符合hasMore且显示最后一项的前提下回调

RefreshLoadMoreListener的onLoadMore方法

技术分享


2.那么我们来看看具体的PulltoRefreshRecyclerView代码

package com.jabony.recyclerpulltorefresh;

import android.content.Context;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
/**
 * 
 * @author jabony
 * @since 2015年3月31日 16:06:59
 *
 */
public class PulltoRefreshRecyclerView extends LinearLayout {
	/**
	 * 垂直方向
	 */
	static final int VERTICAL = LinearLayoutManager.VERTICAL;
	/**
	 * 水平方向
	 */
	static final int HORIZONTAL = LinearLayoutManager.HORIZONTAL;
	/**
	 * 内容控件
	 */
	private RecyclerView recyclerView;
	/**
	 * 刷新布局控件
	 */
	private SwipeRefreshLayout swipeRfl;
	private LinearLayoutManager layoutManager;
	/*
	 * 刷新布局的监听
	 */
	private OnRefreshListener mRefreshListener;
	/**
	 * 内容控件滚动监听 
	 */
	private RecyclerView.OnScrollListener mScrollListener;
	/**
	 * 内容适配器
	 */
	private RecyclerView.Adapter mAdapter;
	/*
	 * 刷新加载监听事件
	 */
	private RefreshLoadMoreListener mRefreshLoadMoreListner;
	/**
	 * 是否可以加载更多
	 */
	private boolean hasMore = true;
	/**
	 * 是否正在刷新
	 */
	private boolean isRefresh = false;
	/**
	 * 是否正在加载更多
	 */
	private boolean isLoadMore = false;

	public PulltoRefreshRecyclerView(Context context) {
		super(context);
		// TODO Auto-generated constructor stub
	}

	@SuppressWarnings("deprecation")
	public PulltoRefreshRecyclerView(Context context, AttributeSet attrs) {
		super(context, attrs);
		// 导入布局
		LayoutInflater.from(context).inflate(
				R.layout.pulltorefreshrecyclerview, this, true);
		swipeRfl = (SwipeRefreshLayout) findViewById(R.id.all_swipe);
		recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
		// 加载颜色是循环播放的,只要没有完成刷新就会一直循环,color1>color2>color3>color4
		// swipeRfl.setColorScheme(Color.BLUE, Color.GREEN, Color.RED,
		// Color.YELLOW);

		/**
		 * 监听上拉至底部滚动监听
		 */
		mScrollListener = new RecyclerView.OnScrollListener() {
			@Override
			public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
				super.onScrolled(recyclerView, dx, dy);
				//最后显示的项
				int lastVisibleItem = layoutManager
						.findLastVisibleItemPosition();
				int totalItemCount = layoutManager.getItemCount();
				// lastVisibleItem >= totalItemCount - 4 表示剩下4个item自动加载
				// dy>0 表示向下滑动
				/*	if (hasMore && (lastVisibleItem >= totalItemCount - 1)
						&& dy > 0 && !isLoadMore) {
					isLoadMore = true;
					loadMore();
				}*/
				/**
				 * 无论水平还是垂直
				 */
				if (hasMore && (lastVisibleItem >= totalItemCount - 1)
						&& !isLoadMore) {
					isLoadMore = true;
					loadMore();
				}

			}
		};

		/**
		 * 下拉至顶部刷新监听
		 */
		mRefreshListener = new OnRefreshListener() {

			@Override
			public void onRefresh() {
				if (!isRefresh) {
					isRefresh = true;
					refresh();
				}
			}
		};
		swipeRfl.setOnRefreshListener(mRefreshListener);
		recyclerView.setHasFixedSize(true);
		layoutManager = new LinearLayoutManager(context);
		layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
		recyclerView.setLayoutManager(layoutManager);
		recyclerView.setOnScrollListener(mScrollListener);
	}

	public void setOrientation(int orientation) {
		if (orientation != 0 && orientation != 1) {
			layoutManager.setOrientation(VERTICAL);
		} else {
			layoutManager.setOrientation(HORIZONTAL);
		}
	}

	public int getOrientation() {
		return layoutManager.getOrientation();
	}

	public void setPullLoadMoreEnable(boolean enable) {
		this.hasMore = enable;
	}

	public boolean getPullLoadMoreEnable() {
		return hasMore;
	}

	public void setPullRefreshEnable(boolean enable) {
		swipeRfl.setEnabled(enable);
	}

	public boolean getPullRefreshEnable() {
		return swipeRfl.isEnabled();
	}

	public void setLoadMoreListener() {
		recyclerView.setOnScrollListener(mScrollListener);
	}

	public void loadMore() {
		if (mRefreshLoadMoreListner != null && hasMore) {
			mRefreshLoadMoreListner.onLoadMore();
			
		}
	}

	/**
	 * 加载更多完毕,为防止频繁网络请求,isLoadMore为false才可再次请求更多数据
	 */
	public void setLoadMoreCompleted(){
		isLoadMore = false;
	}
	
	public void stopRefresh() {
		isRefresh = false;
		swipeRfl.setRefreshing(false);
	}

	public void setRefreshLoadMoreListener(RefreshLoadMoreListener listener) {
		mRefreshLoadMoreListner = listener;
	}

	public void refresh() {
		if (mRefreshLoadMoreListner != null) {
			mRefreshLoadMoreListner.onRefresh();
		}
	}

	public void setAdapter(RecyclerView.Adapter adapter) {
		if (adapter != null)
			recyclerView.setAdapter(adapter);
	}

	public interface RefreshLoadMoreListener {
		public void onRefresh();

		public void onLoadMore();
	}
}

3.那么我们来看看如何使用

package com.example.pulltorefreshrecyeclerviewdemo;

import java.util.ArrayList;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.LinearLayout;
import com.jabony.recyclerpulltorefresh.PullRefreshRecyclerView;

/**
 * 
 * @author jabony
 * @since 2015年3月31日 16:08:56
 */
public class MainActivity extends Activity implements
		PullRefreshRecyclerView.RefreshLoadMoreListener {
	private PullRefreshRecyclerView prrv;
	private ActivityAdapter allActAdapter;
	private JSONObject response = null;
	private int page = 0;
	/**
	 * 全部活动数据源
	 */
	private ArrayList<ActivityBean> actAllList = new ArrayList<ActivityBean>();
	private String jsonStr = "{\"datas\":{\"count\":\"6\",\"list\":[{\"id\":\"311\",\"end_date\":\"1437235200\",\"shopid\":\"387\",\"subject\":\"金鹏会员里程百日冲刺,最高可获35000里程奖励\",\"is_collected\":\"2\",img:\"http://jfgj.wadiankeji.com/Uploads/2015-03-26/5513bd4120d64.jpg\",\"collec_num\":\"1\",\"start_date\":\"1428681600\",\"url\":\"http://jfgj.wadiankeji.com/api/html/detail/actid/311\"},{\"id\":\"302\",\"end_date\":\"1437235200\",\"shopid\":\"387\",\"subject\":\"金鹏会员里程百日冲刺,最高可获35000里程奖励\",\"is_collected\":\"2\",img:\"http://jfgj.wadiankeji.com/Uploads/2015-03-26/5513bd4120d64.jpg\",\"collec_num\":\"1\",\"start_date\":\"1428681600\",\"url\":\"http://jfgj.wadiankeji.com/api/html/detail/actid/311\"},{\"id\":\"311\",\"end_date\":\"1437235200\",\"shopid\":\"387\",\"subject\":\"金鹏会员里程百日冲刺,最高可获35000里程奖励\",\"is_collected\":\"2\",img:\"http://jfgj.wadiankeji.com/Uploads/2015-03-26/5513bd4120d64.jpg\",\"collec_num\":\"1\",\"start_date\":\"1428681600\",\"url\":\"http://jfgj.wadiankeji.com/api/html/detail/actid/311\"},{\"id\":\"311\",\"end_date\":\"1437235200\",\"shopid\":\"387\",\"subject\":\"金鹏会员里程百日冲刺,最高可获35000里程奖励\",\"is_collected\":\"2\",img:\"http://jfgj.wadiankeji.com/Uploads/2015-03-26/5513bd4120d64.jpg\",\"collec_num\":\"1\",\"start_date\":\"1428681600\",\"url\":\"http://jfgj.wadiankeji.com/api/html/detail/actid/311\"},{\"id\":\"311\",\"end_date\":\"1437235200\",\"shopid\":\"387\",\"subject\":\"金鹏会员里程百日冲刺,最高可获35000里程奖励\",\"is_collected\":\"2\",img:\"http://jfgj.wadiankeji.com/Uploads/2015-03-26/5513bd4120d64.jpg\",\"collec_num\":\"1\",\"start_date\":\"1428681600\",\"url\":\"http://jfgj.wadiankeji.com/api/html/detail/actid/311\"},{\"id\":\"311\",\"end_date\":\"1437235200\",\"shopid\":\"387\",\"subject\":\"金鹏会员里程百日冲刺,最高可获35000里程奖励\",\"is_collected\":\"2\",img:\"http://jfgj.wadiankeji.com/Uploads/2015-03-26/5513bd4120d64.jpg\",\"collec_num\":\"1\",\"start_date\":\"1428681600\",\"url\":\"http://jfgj.wadiankeji.com/api/html/detail/actid/311\"}]},\"status\":\"10000\",\"msg\":\"faxian\"}";

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		/**
		 * 绑定组合控件
		 */
		prrv = (PullRefreshRecyclerView) findViewById(R.id.prrv);
		/**
		 * 初始化适配器
		 */
		allActAdapter = new ActivityAdapter(actAllList, MainActivity.this);
		/**
		 * 下拉上拉加载更多监听
		 */
		prrv.setRefreshLoadMoreListener(this);
		/**
		 * 禁用刷新
		 */
		// prrv.setPullRefreshEnable(false);
		/**
		 * 禁用加载更多
		 */
		// prrv.setPullLoadMoreEnable(false);

		/**
		 * 设置布局方向
		 */
		prrv.setVertical();

		/**
		 * 设置适配器
		 */
		prrv.setAdapter(allActAdapter);
		/**
		 * 首次进入刷新
		 */
		prrv.refresh();
	}

	/**
	 * 加载分页数据
	 */
	public void loadNetDatas(int currentPage) {
		if (currentPage > 2) {
			prrv.setPullLoadMoreEnable(false);
			return;
		}
		try {
			response = new JSONObject(jsonStr);
			String msg = null;
			int count = 0;
			int status = 0;
			if (!response.isNull("datas")) {
				JSONObject dataObj = response.getJSONObject("datas");

				if (!dataObj.isNull("count")) {
					count = dataObj.getInt("count");
				}

				if (!dataObj.isNull("list")) {
					JSONArray listArray = dataObj.getJSONArray("list");
					int size = listArray.length();
					for (int i = 0; i < size; i++) {

						JSONObject obj = listArray.getJSONObject(i);
						ActivityBean ab = new ActivityBean();
						if (!obj.isNull("id")) {
							ab.setActId(obj.getString("id"));
						}
						if (!obj.isNull("shopid")) {
							ab.setShopId(obj.getString("shopid"));
						}
						if (!obj.isNull("is_top")) {
							if ("1".equals(obj.getString("is_top"))) {
								ab.setTop(true);
							}

						}

						if (!obj.isNull("img")) {
							ab.setImgUrl(obj.getString("img"));
						}
						if (!obj.isNull("subject")) {
							ab.setActTitile(obj.getString("subject"));
						}
						if (!obj.isNull("start_date")) {
							if (obj.getString("start_date") != null
									&& !"".equals(obj.getString("start_date"))) {
								ab.setStartDate(obj.getLong("start_date"));
							}

						}
						if (!obj.isNull("end_date")) {
							if (obj.getString("end_date") != null
									&& !"".equals(obj.getString("end_date"))) {
								ab.setEndDate(obj.getLong("end_date"));
							}

						}
						if (!obj.isNull("collec_num")) {
							String collcNum = obj.getString("collec_num");
							if (collcNum == null || "".equals(collcNum)) {
								ab.setCollectNum(0);
							} else {
								ab.setCollectNum(obj.getInt("collec_num"));
							}
							collcNum = null;
						}
						if (!obj.isNull("is_collected")) {
							ab.setCollected(obj.getInt("is_collected") == 1 ? true
									: false);
						}
						if (!obj.isNull("url")) {
							ab.setUrl(obj.getString("url"));
						}
						actAllList.add(ab);
						/**
						 * 刷新适配器
						 */
						allActAdapter.notifyDataSetChanged();
						/**
						 * 如果刷新则停止刷新
						 */
						prrv.stopRefresh();
						/**
						 * 加载更多完毕
						 */
						prrv.setLoadMoreCompleted();
						/**
						 * 如果没有更多数据则设置不可加载更多
						 */
						// prrv.setPullLoadMoreEnable(false);
					}
				}
			}
		} catch (JSONException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	@Override
	public void onRefresh() {
		// TODO Auto-generated method stub
		prrv.setPullRefreshEnable(true);
		prrv.setPullLoadMoreEnable(true);
		actAllList.clear();
		page = 1;
		loadNetDatas(page);
	}

	@Override
	public void onLoadMore() {
		// TODO Auto-generated method stub
		page++;
		loadNetDatas(page);
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// Inflate the menu; this adds items to the action bar if it is present.
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		// Handle action bar item clicks here. The action bar will
		// automatically handle clicks on the Home/Up button, so long
		// as you specify a parent activity in AndroidManifest.xml.
		int id = item.getItemId();
		if (id == R.id.action_delete) {
			actAllList.clear();
			allActAdapter.notifyDataSetChanged();
			page = 1;
			prrv.customExceptView(R.drawable.no_data, "这里空空如也");
			return true;
		}
		if (id == R.id.action_orintation) {
			int orientation = prrv.getOrientation();
			if (orientation == LinearLayout.HORIZONTAL) {
				prrv.setVertical();
			} else {
				prrv.setHorizontal();
			}
			return true;
		}
		if (id == R.id.action_refreshable) {
			if (prrv.getPullRefreshEnable()) {
				prrv.setPullRefreshEnable(!prrv.getPullRefreshEnable());
			} else {
				prrv.setPullRefreshEnable(!prrv.getPullRefreshEnable());
			}
			return true;
		}
		if (id == R.id.action_loadmoreable) {
			if (prrv.getPullLoadMoreEnable()) {
				prrv.setPullLoadMoreEnable(!prrv.getPullLoadMoreEnable());
			} else {
				prrv.setPullLoadMoreEnable(!prrv.getPullLoadMoreEnable());
			}
			return true;
		}
		if (id == R.id.action_stoprefresh) {
			prrv.stopRefresh();
			return true;
		}
		if (id == R.id.action_scrollToTop) {
			prrv.scrollToTop();
			return true;
		}
		return super.onOptionsItemSelected(item);
	}
}


4.看上去已经搞定了,但你会发现需要引入一个库项目没错RecyclerView的库项目

这都好办,我们考虑的重点不在这里,这样就完美了吗?我们在使用的时候在布局中这么写,看是没有什么

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.jabony.recyclerpulltorefresh.MainActivity" >
    <com.jabony.recyclerpulltorefresh.PullRefreshRecyclerView
        android:id="@+id/prrv"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

5.我们能不能把自定义的View布局省掉?

答案当然是肯定的:我们来做个小实验在原来采用

// 导入布局
LayoutInflater.from(context).inflate(R.layout.pulltorefreshrecyclerview, this, true);

在老的方式做下调整,完全脱离布局,代码写布局

		LinearLayout rootLl = new LinearLayout(context);
		rootLl.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
				LayoutParams.MATCH_PARENT));
		<span style="color:#ff0000;">mExceptView = initExceptionView(context);//异常布局</span>
		mExceptView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
				LayoutParams.MATCH_PARENT));
		mExceptView.setVisibility(View.GONE);
		swipeRfl = new SwipeRefreshLayout(context);
		swipeRfl.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
				LayoutParams.MATCH_PARENT));
		FrameLayout bootLl = new FrameLayout(context);
		bootLl.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
				LayoutParams.WRAP_CONTENT));
		recyclerView = new RecyclerView(context);
		recyclerView.setVerticalScrollBarEnabled(true);
		recyclerView.setHorizontalScrollBarEnabled(true);
		recyclerView.setLayoutParams(new LayoutParams(
				LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
		bootLl.addView(recyclerView);
		bootLl.addView(mExceptView);
		swipeRfl.addView(bootLl);
		rootLl.addView(swipeRfl);
		this.addView(rootLl);
这样就把需要有xml布局引入的布局改动为纯代码的,虽然不怎么完美,但是测试使用是没有问题了。

6.没错我引入了异常布局,为了交互友好,允许用户半自定义异常界面

技术分享

也就是其实布局什么样子 我早就定好了,你只需要按照方法传图片的id和提示的字符串,你一定会好奇到底是什么样子的,马上贴图。

那么刷新呢,非常简单,点击图片可以刷新操作,这样就看似比较完善了。


7.总结一下:

PulltoRefreshRecyclerView是在SwipeRefreshLayout和RecyclerView的基础上组合改造的。

需要实现OnRefreshListener的方法

可以控制能否刷新,能否加在更多

可以控制显示的方向

提供回到Top的方法

停止刷新效果

可以半定义异常界面的效果


下载地址:

自定义组合下拉刷新上拉加载更多控件



郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。