设计思路
ListView 上拉加载很容易实现,监听 ListView 滑动到底部,显示 FootView 即可,但是 RecyclerView 没有 addFootView(View view) 这个方法,有一种方案是在 adapter 中加一个 itemView 用于显示 FootView,但是这样做就得每一个使用到 RecyclerView 的地方都写一遍重复代码,而且不能适配多种类型的 LayoutManager,所以 RecyclerView 就需要另外一种思路来实现,我的解决方案是:自定义 PullToRefreshRecyclerView 继承 FrameLayout,将 RecyclerView 与 FootView 添加到 PullToRefreshRecyclerView 中,上拉时显示 FootView。
具体方案
布局文件中很简单,一个 FrameLayout 内部放一个 RecyclerView 和 一个 FootView,布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:id="@+id/root_view"
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="wrap_content"/>
<LinearLayout
android:id="@+id/footer_view"
android:layout_width="match_parent"
android:layout_height="80dp"
android:orientation="horizontal"
android:gravity="center"
android:layout_gravity="bottom">
<ProgressBar
android:id="@+id/progress"
android:layout_width="20dp"
android:layout_height="20dp"
android:visibility="gone"
android:layout_marginRight="5dp"/>
<ImageView
android:id="@+id/img_arrow"
android:layout_width="20dp"
android:layout_height="20dp"
android:scaleType="centerInside"
android:src="@mipmap/arrow"/>
<TextView
android:id="@+id/tv_load_tag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginLeft="5dp"
android:text="上拉加载数据"/>
</LinearLayout>
</FrameLayout>
这个布局的效果如下图:</br>
</br>
可以看到有个很明显的问题,FootView 是一直在屏幕内部的,即使隐藏掉滑动到底部在显示效果也很差劲,我的解决方案是重写 PullToRefreshRecyclerView 的 onMeasure 方法,将 PullToRefreshRecyclerView 高度设置为屏幕高度加上 FootView 的高度,上拉时使用 scrollTo 方法显示 FootView。
onMeasure 方法如下:
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int height = getMeasuredHeight() + footViewHeight;
setMeasuredDimension(getMeasuredWidth(), height);
LayoutParams rootLP = (LayoutParams) rootView.getLayoutParams();
rootLP.height = height;
rootView.setLayoutParams(rootLP);//设置 FootView的高度
LayoutParams recyclerLp = (LayoutParams) recyclerView.getLayoutParams();
recyclerLp.height = height - footViewHeight;
recyclerView.setLayoutParams(recyclerLp);//将 RecyclerView 的高度设置为屏幕高度
}
这样基本布局就完成了,然后就是对滑动事件的拦截与分发,如果滑动到底部则拦截事件,并且根据滑动距离来计算 scrollTo 的移动距离,具体代码如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (recyclerView == null || recyclerView.getChildCount() == 0)
return super.onInterceptTouchEvent(ev);
if(isLoading) return true;//如果正在加载中则拦截滑动事件
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
lastDownY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
int lastPosition = -1;
//以下代码用于获取当前 RecyclerView 中显示的最后一个 position
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
lastPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
} else if (layoutManager instanceof LinearLayoutManager) {
lastPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
int[] lastPositions = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()];
((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(lastPositions);
lastPosition = findMax(lastPositions);
}
int offerY = (int) ev.getY() - lastDownY;
if (offerY < 0) {
//如果正在上拉
View lastView = recyclerView.getChildAt(recyclerView.getChildCount() - 1);
if (lastView != null && lastView.getBottom() + footViewHeight >= getHeight() && lastPosition == recyclerView.getLayoutManager().getItemCount() - 1) {
//如果滑动到最底部则拦截事件并设置标志位
canScroll = true;
return true;
} else {
canScroll = false;
}
} else {
canScroll = false;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (recyclerView == null || recyclerView.getChildCount() == 0)
return super.onInterceptTouchEvent(ev);
int offerY;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
lastDownY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
if (canScroll) {
offerY = (int) ev.getY() - lastDownY;
lastOfferY = offerY;
if(footView.getVisibility() == GONE) footView.setVisibility(VISIBLE);
//将 PullToRefreshRecyclerView 内部跟着手指向上移动,移动距离为手指滑动距离的一半
scrollTo(getScrollX(), -offerY / 2);
imgArrow.setVisibility(VISIBLE);
if (Math.abs(offerY) / 2 < footViewHeight) {
progressBar.setVisibility(GONE);
tvLoadTag.setText("上拉加载数据");
if (!arrowIsTop) {
imgArrow.startAnimation(topAnimation);
arrowIsTop = true;
}
canLoad = false;
} else {
progressBar.setVisibility(GONE);
tvLoadTag.setText("松手加载更多");
if (arrowIsTop) {
imgArrow.startAnimation(bottomAnimation);
arrowIsTop = false;
}
canLoad = true;
}
}
break;
case MotionEvent.ACTION_UP:
if (canScroll) {
if (!canLoad) {
//手指松开后如果滑动距离小于设定距离则回到初始状态
scrollTo(getScrollX(), 0);
} else {
//如果滑动距离大于设定距离则加载数据并回弹到加载状态
mScroller.startScroll(getScrollX(), getScrollY(), getScrollX(), -(Math.abs(lastOfferY) / 2 - footViewHeight), 500);
lastOfferY = 0;
loadData();
}
canScroll = false;
}
break;
}
return super.onTouchEvent(ev);
}
以上便是事件的拦截与分发,另外需要注意的是:如果与 SwipeRefreshLayout 结合使用会出现一个小问题,就是当处于上拉加载状态时如果下拉可能会出现滑动冲突,当然了也有解决方案,自定义的这个 PullToRefreshRecyclerView 类实现了 SwipeRefreshLayout.OnChildScrollUpCallback 接口,当 SwipeRefreshLayout 判断当前 View 是否处于可下拉刷新状态时会首先使用这个接口来判断,我这里做了相应的方法,使用时只需要将 PullToRefreshRecyclerView 对象传给 SwipeRefreshLayout.OnChildScrollUpCallback 接口即可,如下:
swipeRefresh.setOnChildScrollUpCallback(pullToRefreshRecyclerView);
下面放上 PullToRefreshRecyclerView 源码:
package com.zhangke.widget;
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewPager;
import android.support.v4.widget.SwipeRefreshLayout;
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.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.Scroller;
import android.widget.TextView;
import com.seagetech.ptduser.test.R;
/**
* Created by 张可 on 2017/7/5.
*/
public class PullToRefreshRecyclerView extends FrameLayout implements SwipeRefreshLayout.OnChildScrollUpCallback {
private static final String TAG = "PullToRefreshRecycler";
private View rootView;
private RecyclerView recyclerView;
private View footView;
private ProgressBar progressBar;
private TextView tvLoadTag;
private ImageView imgArrow;
private int footViewHeight = 100;
private int lastDownY;
private boolean canScroll = false;
private boolean canLoad = false;
private boolean isLoading = false;//是否正在加载,正在加载时拦截滑动事件
/**
* 箭头方向是否向上
*/
private boolean arrowIsTop = true;
private RotateAnimation bottomAnimation;//箭头由上到下的动画
private RotateAnimation topAnimation;//箭头由下到上的动画
private Scroller mScroller;
private OnPullToBottomListener onPullToBottomListener;
private int lastOfferY = 0;
public PullToRefreshRecyclerView(Context context) {
super(context);
init();
}
public PullToRefreshRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
inflate(getContext(), R.layout.view_pull_to_refresh_recycler, this);
rootView = findViewById(R.id.root_view);
recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
footView = findViewById(R.id.footer_view);
progressBar = (ProgressBar) findViewById(R.id.progress);
tvLoadTag = (TextView) findViewById(R.id.tv_load_tag);
imgArrow = (ImageView) findViewById(R.id.img_arrow);
footViewHeight = dip2px(getContext(), 80);
bottomAnimation = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
bottomAnimation.setDuration(200);
bottomAnimation.setFillAfter(true);
topAnimation = new RotateAnimation(180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
topAnimation.setDuration(200);
topAnimation.setFillAfter(true);
mScroller = new Scroller(getContext());
}
public void setLayoutManager(RecyclerView.LayoutManager layout) {
recyclerView.setLayoutManager(layout);
}
public void setAdapter(RecyclerView.Adapter adapter) {
recyclerView.setAdapter(adapter);
adapter.notifyDataSetChanged();
}
public RecyclerView getRecyclerView() {
return recyclerView;
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int height = getMeasuredHeight() + footViewHeight;
setMeasuredDimension(getMeasuredWidth(), height);
LayoutParams rootLP = (LayoutParams) rootView.getLayoutParams();
rootLP.height = height;
rootView.setLayoutParams(rootLP);
LayoutParams recyclerLp = (LayoutParams) recyclerView.getLayoutParams();
recyclerLp.height = height - footViewHeight;
recyclerView.setLayoutParams(recyclerLp);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (recyclerView == null || recyclerView.getChildCount() == 0)
return super.onInterceptTouchEvent(ev);
if(isLoading) return true;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
lastDownY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
int lastPosition = -1;
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
lastPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
} else if (layoutManager instanceof LinearLayoutManager) {
lastPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
int[] lastPositions = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()];
((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(lastPositions);
lastPosition = findMax(lastPositions);
}
int offerY = (int) ev.getY() - lastDownY;
if (offerY < 0) {
View lastView = recyclerView.getChildAt(recyclerView.getChildCount() - 1);
if (lastView != null && lastView.getBottom() + footViewHeight >= getHeight() && lastPosition == recyclerView.getLayoutManager().getItemCount() - 1) {
canScroll = true;
return true;
} else {
canScroll = false;
}
} else {
canScroll = false;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (recyclerView == null || recyclerView.getChildCount() == 0)
return super.onInterceptTouchEvent(ev);
int offerY;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
lastDownY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
if (canScroll) {
offerY = (int) ev.getY() - lastDownY;
lastOfferY = offerY;
if(footView.getVisibility() == GONE) footView.setVisibility(VISIBLE);
scrollTo(getScrollX(), -offerY / 2);
imgArrow.setVisibility(VISIBLE);
if (Math.abs(offerY) / 2 < footViewHeight) {
progressBar.setVisibility(GONE);
tvLoadTag.setText("上拉加载数据");
if (!arrowIsTop) {
imgArrow.startAnimation(topAnimation);
arrowIsTop = true;
}
canLoad = false;
} else {
progressBar.setVisibility(GONE);
tvLoadTag.setText("松手加载更多");
if (arrowIsTop) {
imgArrow.startAnimation(bottomAnimation);
arrowIsTop = false;
}
canLoad = true;
}
}
break;
case MotionEvent.ACTION_UP:
if (canScroll) {
if (!canLoad) {
scrollTo(getScrollX(), 0);
} else {
mScroller.startScroll(getScrollX(), getScrollY(), getScrollX(), -(Math.abs(lastOfferY) / 2 - footViewHeight), 500);
lastOfferY = 0;
loadData();
}
canScroll = false;
}
break;
}
return super.onTouchEvent(ev);
}
@Override
public boolean canChildScrollUp(SwipeRefreshLayout parent, @Nullable View child) {
if(recyclerView.getChildCount() > 0 && recyclerView.getChildAt(0).getTop() < recyclerView.getPaddingTop()){
Log.e(TAG, "canChildScrollUp return true");
return true;
}
Log.e(TAG, "canChildScrollUp return false");
return false;
}
private void loadData() {
isLoading = true;
imgArrow.clearAnimation();
imgArrow.setVisibility(GONE);
progressBar.setVisibility(VISIBLE);
tvLoadTag.setText("正在加载...");
if (this.onPullToBottomListener != null) {
postDelayed(new Runnable() {
@Override
public void run() {
PullToRefreshRecyclerView.this.onPullToBottomListener.onPullToBottom();
}
}, 500);
} else {
progressBar.setVisibility(GONE);
scrollTo(getScrollX(), 0);
}
}
/**
* 设置是否正在加载,一般来书,在加载完毕之后应该调用此方法
*
* @param loading
*/
public void setLoading(final boolean loading) {
isLoading = loading;
if (!isLoading) {
post(new Runnable() {
@Override
public void run() {
if (!loading) {
progressBar.setVisibility(GONE);
scrollTo(getScrollX(), 0);
}
}
});
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
//找到数组中的最大值
private int findMax(int[] lastPositions) {
int max = lastPositions[0];
for (int value : lastPositions) {
if (value > max) {
max = value;
}
}
return max;
}
public void setOnPullToBottomListener(OnPullToBottomListener onPullToBottomListener) {
this.onPullToBottomListener = onPullToBottomListener;
}
public interface OnPullToBottomListener {
void onPullToBottom();
}
/**
* 将dip或dp值转换为px值,保证尺寸大小不变
*
* @param dipValue
* @param dipValue (DisplayMetrics类中属性density)
* @return
*/
public static int dip2px(Context context, float dipValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
}