【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的方法
停止刷新效果
可以半定义异常界面的效果
下载地址:
自定义组合下拉刷新上拉加载更多控件
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。