1 前言
关于RecyclerView 添加上拉加载更多和下拉刷新的封装很多,例如有自定义ViewGroup来实现的,也有使用SwipeRefreshLayout来实现的,我觉得都不是太好,因为对于在项目中,需要各种下拉刷新和上拉加载更多的效果,甚至同一项目中都有不同的效果,网上的大多耦合性太严重,无法解耦,按需定制性较差。现在提供一种对RecyclerView的封装,设计如下:
2 下拉刷新和上拉加载更多实现思路
首先来看下拉刷新的效果,如下:
首先我们分析下,有以下几点:
1. 有一个下拉刷新头,即这里的下拉显示“下拉即可刷新”,这个可以用上一节的addHeaderView来实现
2. 下拉时,一共有这几种状态:
- 正常状态:这种状态就是没有下拉时的状态
- 下拉状态:正在下拉,但是拉动的距离小于下拉刷新头的高度,或者其他值,此时提示下拉可以刷新等
- 待松开状态:下拉的距离大于下拉刷新头的高度,此时需提示松开刷新子类的
- 正在运行状态:下拉刷新运行状态,一般会回调自定义的Listener的onRefresh()接口,表示正在刷新
- 以及回退状态:运行结束,下拉刷新头回退到恢复原样,一般需用动画来实现。
另外我们对于RecycerView 的封装下拉刷新功能,有如下要求:
- 下拉刷新头可以自定义
- 下拉效果可以自定义
- 回退效果可以自定义
基于以上的分析,我们的设计了以下的类来实现设计,先来看类图
IViewCreator负责下拉刷新头的创建,以及下拉效果,运行效果,回退效果的实现
XRecyclerHelper 负责下拉刷新,上拉加载更多的View的状态的维护和切换
XRecyclerListener下拉刷新,上拉加载更多回调
XStatus 下拉刷新,上拉加载更多 的状态
XRecyclerView下拉刷新,上拉加载更多的RecyclerView的封装
这里我们知道,在添加了下拉刷新头是,默认我们是需要隐藏的,这里直接通过改变mRefresh的topMargin来实现,在下拉的过程中,不断的改变topMargin,使其具有下拉效果,上拉加载更多也类似。
对于上拉加载,我们不断的改变bottomMargin,使其具有上拉效果
另外:我们需要重写RecyclerView的dispatchTouchEvent(),onTouchEvent()来做事件的监听。
3 代码实现
先来看XStatus.java,这里只定义了四种状态
/**
* Email: 1273482124@qq.com
* Created by qiyei2015 on 2017/5/25.
* Version: 1.0
* Description: 下拉刷新或者上拉加载的状态
*/
public enum XStatus {
NORMAL, // 默认状态
PULL, // 下拉或者上拉状态
RELEASE, // 松开释放状态
RUNNING // 正在刷新状态或者正在加载状态
}
/**
* Email: 1273482124@qq.com
* Created by qiyei2015 on 2017/5/21.
* Version: 1.0
* Description:
*/
public interface IViewCreator {
/**
* 获取下拉刷新的View
* @param context
* @param parent
*/
View getView(Context context, ViewGroup parent);
/**
* 正在下拉或者正在上拉
* @param currentHeight
* @param refreshHeight
* @param status
*/
void onPull(int currentHeight, int refreshHeight, XStatus status);
/**
* 正在刷新或者正在加载
*/
void onRunning();
/**
* 停止刷新,或者停止加载
*/
void onStopRunning();
}
定义了创建View的接口,以及在下拉,上拉的动画效果接口,以及正在刷新,正在加载的效果接口,以及停止恢复的接口
/**
* Email: 1273482124@qq.com
* Created by qiyei2015 on 2017/5/24.
* Version: 1.0
* Description:
*/
public class CommonRefreshView implements IViewCreator {
// 加载数据的ImageView
private View mRefreshIv;
@Override
public View getView(Context context, ViewGroup parent) {
View refreshView = LayoutInflater.from(context).inflate(R.layout.layout_refresh_header_view, parent, false);
mRefreshIv = refreshView.findViewById(R.id.refresh_iv);
return refreshView;
}
@Override
public void onPull(int currentHeight, int refreshHeight, XStatus status) {
float rotate = ((float) currentHeight) / refreshHeight;
// 不断下拉的过程中不断的旋转图片
mRefreshIv.setRotation(rotate * 360);
}
@Override
public void onRunning() {
// 刷新的时候不断旋转
RotateAnimation animation = new RotateAnimation(0, 720,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
animation.setRepeatCount(-1);
animation.setDuration(1000);
mRefreshIv.startAnimation(animation);
}
@Override
public void onStopRunning() {
// 停止加载的时候清除动画
mRefreshIv.setRotation(0);
mRefreshIv.clearAnimation();
}
}
IViewCretaor的一个默认实现,只是为了演示效果而编写
/**
* Email: 1273482124@qq.com
* Created by qiyei2015 on 2017/5/23.
* Version: 1.0
* Description:
*/
public class XRecyclerView extends WrapRecyclerView{
/**
* 调试标志
*/
private static final String TAG = XRecyclerView.class.getSimpleName();
/**
* 下拉刷新
*/
private XRecyclerHelper mRefresh;
/**
* 上拉加载更多
*/
private XRecyclerHelper mLoadMore;
/**
* 是否下拉刷新
*/
private boolean isPullRefresh;
/**
* 是否上拉加载更多
*/
private boolean isPullLoad;
/**
* 下拉刷新和上拉加载更多的监听器
*/
private XRecyclerListener mListener;
/**
* 第一个可见的item
*/
private int mFirstVisibile = -1;
/**
* 最后一个可见的item
*/
private int mLastVisibile = -1;
/**
* 布局管理器
*/
private LayoutManager mLayoutManager;
public XRecyclerView(Context context) {
this(context,null);
}
public XRecyclerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public XRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (changed && isPullRefresh){
int refreshHeight = mRefresh.view.getMeasuredHeight();
mRefresh.viewHeight = refreshHeight;
Log.d(TAG,"onLayout,mRefresh.viewHeight:" + refreshHeight);
if (refreshHeight > 0){
mRefresh.setViewTopMargin(-refreshHeight); //隐藏掉RefreshView
}
}
}
/**
* 记录手指按下的位置 ,之所以写在dispatchTouchEvent那是因为如果我们处理了条目点击事件,
* 那么就不会进入onTouchEvent里面,所以只能在这里获取
* @param ev
* @return
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mRefresh.downY = (int) ev.getRawY();
mLoadMore.downY = (int)ev.getRawY();
break;
case MotionEvent.ACTION_UP: //手指离开时,如果是下拉或者上拉,就恢复refreshview
if (mRefresh.drag){
mRefresh.restoreViewTopMargin(mListener);
}
if (mLoadMore.drag){
mLoadMore.restoreViewBottomMargin(mListener);
}
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent e) {
boolean isScrollTop = false;
mLayoutManager = getLayoutManager();
if (mLayoutManager instanceof LinearLayoutManager){
mFirstVisibile = ((LinearLayoutManager) mLayoutManager).findFirstVisibleItemPosition();
if (mFirstVisibile == 0 || mFirstVisibile == 1){
isScrollTop = true;
}else {
isScrollTop = false;
}
}else {
isScrollTop = false;
}
switch (e.getAction()){
case MotionEvent.ACTION_MOVE:
Log.d(TAG,"onTouchEvent,mRefresh.status:" + mRefresh.status);
//如果在最顶部或者底部才处理,否则不需要处理
if (mRefresh.status == XStatus.RUNNING || mLoadMore.status == XStatus.RUNNING){
return super.onTouchEvent(e);
}
// //如果在下拉就滚动到第一个位置,要不然的话,看不到
if (mRefresh.drag && isScrollTop){
scrollToPosition(0);
}
if (mLoadMore != null && isPullLoad){
mLoadMore.viewHeight = mLoadMore.view.getMeasuredHeight();
}
//如果在上拉就滚动到最后一个位置,要不然看不到
if (mLoadMore.drag && !canScrollDown()){
scrollToPosition(getAdapter().getItemCount() - 1);
}
int refreshY = (int)((e.getRawY() - mRefresh.downY) * mRefresh.dragValue);
int loadMoreY = (int)((e.getRawY() - mLoadMore.downY) * mLoadMore.dragValue);
//手指下拉
if (refreshY > 0 && isScrollTop && isPullRefresh){
refreshY = refreshY - mRefresh.viewHeight;
Log.d(TAG,"onTouchEvent,refreshY:" + refreshY);
mRefresh.setViewTopMargin(refreshY);
mRefresh.updateViewStatus(refreshY);
mRefresh.drag = true;
return false;
}
//手指上拉
if (loadMoreY < 0 && !canScrollDown() && isPullLoad){
Log.d(TAG,"onTouchEvent,loadMoreY:" + -loadMoreY);
mLoadMore.setViewBottomMargin(-loadMoreY);
mLoadMore.updateViewStatus(-loadMoreY);
mLoadMore.drag = true;
return false;
}
break;
}
return super.onTouchEvent(e);
}
/**
* 初始化
*/
private void init(){
mRefresh = new XRecyclerHelper(XRecyclerHelper.REFRESH);
mLoadMore = new XRecyclerHelper(XRecyclerHelper.LOAD_MORE);
}
/**
* 添加下拉刷新的Creator
* @param creator
*/
public void addRefreshViewCreator(IViewCreator creator){
isPullRefresh = true;
mRefresh.addViewCreator(creator);
RecyclerView.Adapter adapter = getAdapter();
if (adapter != null && mRefresh.creator != null){
View header = mRefresh.creator.getView(getContext(),this);
if (header != null){
addRefreshView(header);
mRefresh.view = header;
}
}
}
/**
* 添加上拉加载更多的的Creator
* @param creator
*/
public void addLoadMoreViewCreator(IViewCreator creator){
isPullLoad = true;
mLoadMore.addViewCreator(creator);
RecyclerView.Adapter adapter = getAdapter();
if (adapter != null && mLoadMore.creator != null){
View foot = mLoadMore.creator.getView(getContext(),this);
if (foot != null){
addLoadMoreView(foot);
mLoadMore.view = foot;
}
}
}
/**
* @param pullRefresh the {@link #isPullRefresh} to set
*/
public void setPullRefresh(boolean pullRefresh) {
isPullRefresh = pullRefresh;
if (isPullRefresh){
addRefreshViewCreator(new CommonRefreshView());
}
}
/**
* @param pullLoad the {@link #isPullLoad} to set
*/
public void setPullLoad(boolean pullLoad) {
isPullLoad = pullLoad;
if (isPullLoad){
addLoadMoreViewCreator(new CommonRefreshView());
}
}
/**
* 停止刷新
*/
public void stopRefresh(){
mRefresh.status = XStatus.NORMAL;
mRefresh.restoreViewTopMargin(mListener);
if (mRefresh.creator != null){
mRefresh.creator.onStopRunning();
}
}
/**
* 停止加载
*/
public void stopLoadMore(){
mLoadMore.status = XStatus.NORMAL;
mLoadMore.restoreViewBottomMargin(mListener);
if (mLoadMore.creator != null){
mLoadMore.creator.onStopRunning();
}
}
/**
* @param listener the {@link #mListener} to set
*/
public void setXRecyclerListener(XRecyclerListener listener) {
mListener = listener;
}
/**
* @return Whether it is possible for the child view of this layout to
* scroll up. Override this if the child view is a custom view.
* 判断是不是滚动到了最顶部,这个是从SwipeRefreshLayout里面copy过来的源代码
*/
public boolean canScrollUp() {
if (android.os.Build.VERSION.SDK_INT < 14) {
return ViewCompat.canScrollVertically(this, -1) || this.getScrollY() > 0;
} else {
return ViewCompat.canScrollVertically(this, -1);
}
}
/**
* @return Whether it is possible for the child view of this layout to
* scroll up. Override this if the child view is a custom view.
* 判断是不是滚动到了最顶部,这个是从SwipeRefreshLayout里面copy过来的源代码
*/
public boolean canScrollDown() {
return ViewCompat.canScrollVertically(this, 1);
}
}
WarpRecyclerView是定义了addHeaderView和addFooterView的RecyclerView,详细见上一篇博客。
可以看到,这里只关联了XRecyclerHelper ,代码逻辑简单,后续对下拉,上拉的修改,都可以直接在XRecyclerHelper中修改
XRecyclerHelper.java
/**
* Email: 1273482124@qq.com
* Created by qiyei2015 on 2017/5/25.
* Version: 1.0
* Description: 下拉刷新,上拉加载的帮助类
*/
public class XRecyclerHelper {
/**
* 调试标志
*/
private static final String TAG = XRecyclerHelper.class.getSimpleName();
/**
* 下拉刷新
*/
public static final int REFRESH = 1;
/**
* 上拉加载更多
*/
public static final int LOAD_MORE = 2;
/**
* view的类型
*/
public int type;
/**
* 下拉刷新上拉加载更多的
*/
public IViewCreator creator;
/**
* 下拉刷新上拉加载的view的高度
*/
public int viewHeight = 0;
/**
* 下拉刷新上拉加载的View
*/
public View view;
/**
* 手指按下的y位置
*/
public int downY = 0;
/**
* 手指下拉或者上拉的阻力系数
*/
public float dragValue = 0.35f;
/**
* 是否在下拉或者上拉
*/
public boolean drag = false;
/**
* 当前下拉状态
*/
public XStatus status = XStatus.NORMAL;
public XRecyclerHelper(int type) {
this.type = type;
}
/**
* 添加下拉刷新的Creator
* @param creator
*/
public void addViewCreator(IViewCreator creator){
this.creator = creator;
}
/**
* 设置RefrshView的MarginTop
* @param topMargin
*/
public void setViewTopMargin(int topMargin) {
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) this.view.getLayoutParams();
if (topMargin < -this.viewHeight){
topMargin = -this.viewHeight;
}
params.topMargin = topMargin;
Log.d(TAG,"setRefreshViewMarginTop, params.topMargin:" + params.topMargin);
this.view.setLayoutParams(params);
this.view.requestLayout();
}
/**
* 恢复并刷新
* @param listener
*/
public void restoreViewTopMargin(XRecyclerListener listener) {
int currentTopMargin = ((ViewGroup.MarginLayoutParams)this.view.getLayoutParams()).topMargin;
int finalTopMargin = -this.viewHeight; //最终顶部会不显示
//如果是松开刷新状态,没有下拉了
if (this.status == XStatus.RELEASE){
finalTopMargin = 0;
//转变成正在刷新状态
this.status = XStatus.RUNNING;
if (this.creator != null){
this.creator.onRunning();
}
//回调刷新监听器的刷新接口
if (listener != null){
listener.onRefresh();
}
}
int y = currentTopMargin - finalTopMargin;
Log.d(TAG,"restoreRefreshView, currentTopMargin:" + currentTopMargin + ",finalTopMargin:" + finalTopMargin);
//从当前状态恢复到最终状态
ValueAnimator animator = ObjectAnimator.ofFloat(currentTopMargin,finalTopMargin).setDuration(300);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float dy = (float) animation.getAnimatedValue();
setViewTopMargin((int)dy);
}
});
animator.start();
this.drag = false;
}
/**
* 设置RefrshView的MarginTop
* @param marginBottom
*/
public void setViewBottomMargin(int marginBottom) {
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) this.view.getLayoutParams();
if (marginBottom < 0){
marginBottom = 0;
}
params.bottomMargin = marginBottom;
Log.d(TAG,"setViewBottomMargin, params.marginBottom:" + params.bottomMargin);
this.view.setLayoutParams(params);
}
/**
* 恢复并调用listenerde加载方法
* @param listener
*/
public void restoreViewBottomMargin(XRecyclerListener listener) {
int currentBottomMargin = ((ViewGroup.MarginLayoutParams)this.view.getLayoutParams()).bottomMargin;
int finalBottomMargin = 0; //最终底部会不显示
//如果是松开,没有上拉加载更多了
if (this.status == XStatus.RELEASE){
//转变成正在加载的状态
this.status = XStatus.RUNNING;
if (this.creator != null){
this.creator.onRunning();
}
//回调监听器的加载更多接口
if (listener != null){
listener.onLoadMore();
}
}
int y = currentBottomMargin - finalBottomMargin;
Log.d(TAG,"restoreViewBottomMargin, currentBottomMargin:" + currentBottomMargin + ",finalBottomMargin:" + finalBottomMargin);
//从当前状态恢复到最终状态
ValueAnimator animator = ObjectAnimator.ofFloat(currentBottomMargin,finalBottomMargin).setDuration(300);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float dy = (float) animation.getAnimatedValue();
setViewBottomMargin(-(int)dy);
}
});
animator.start();
this.drag = false;
}
/**
* 根据margin更新View的状态
* @param margin
*/
public void updateViewStatus(int margin){
switch (type){
case REFRESH:
if (margin <= -this.viewHeight){ //正常状态
this.status = XStatus.NORMAL;
}else if (margin < 0){ // 下拉或者上拉状态
this.status = XStatus.PULL;
}else { // 该释放状态
this.status = XStatus.RELEASE;
}
break;
case LOAD_MORE:
if (margin <= 0){ //正常状态
this.status = XStatus.NORMAL;
}else if (margin < this.viewHeight){ // 上拉状态
this.status = XStatus.PULL;
}else { // 该释放状态
this.status = XStatus.RELEASE;
}
break;
default:
break;
}
if (this.creator != null){
this.creator.onPull(margin,this.viewHeight,this.status);
}
}
}
这里面主要就是提供了几个接口,
第一就是addViewCreator(),用于为不同的需求定制View
第二提供接口改变和恢复view的topMargin 或者bottomMargin
第三就是提供改变View的状态,以便根据状态进行相应的效果和状态处理
4 运行效果
以上的分析可以看出,我们做到了下拉,上拉的View与RecyclerView的解耦,方便我们进行项目的定制,因为我采用的演示Demo,效果如下: