开篇
当前市面上很多支持刷新、加载更多RecyclerView开源库,为何我这里还要自己再写一个?因为市面上的这些支持刷新加载更多的RecyclerView开源库实现方式基本上都是:在Adapter
的外层在包裹一层Adapter
,这种实现方式主要有以下两个不方便。
- 1、在用户添加ItemDecoration的时候,会影响到刷新头部和加载更多底部的样式。
- 2、在用户更新列表某条记录时,不方便找到该记录对应的position。例如
notifyItemInserted(int position)
等。
效果截屏
立即体验
扫描以下二维码下载体验App(体验App内嵌版本更新检测功能):
传送门:https://github.com/JustinRoom/SimpleAdapterDemo
gradle引用
implementation 'jsc.kit.adapter:adapter-component:_latestVersion'
属性
PullToRefreshRecyclerView
名称 | 类型 | 描述 |
---|---|---|
prvHeaderLayout |
reference | 下拉刷新头部layout |
prvFooterLayout |
reference | 上拉加载更多底部layout |
prvPullDownToRefreshText |
string | 下拉刷新提示 |
prvReleaseToRefreshText |
string | 释放刷新提示 |
prvRefreshingText |
string | 正在刷新提示 |
prvRefreshCompletedText |
string | 刷新完成提示 |
prvPullUpToLoadMoreText |
string | 上拉加载更多提示 |
prvReleaseToLoadMoreText |
string | 释放加载更多提示 |
prvLoadingMoreText |
string | 正在加载更多提示 |
prvLoadMoreCompletedText |
string | 加载更多完成提示 |
简析源码
public class PullToRefreshRecyclerView extends ViewGroup {}
1、初始化布局
private void initView(Context context) {
inflate(context, R.layout.recycler_pull_to_refresh_recycler_view, this);
recyclerView = findViewById(R.id.recycler_view);
final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
mMinimumVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
mMaximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
scaledTouchSlop = viewConfiguration.getScaledTouchSlop();
}
private void initAttrs(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PullToRefreshRecyclerView, defStyleAttr, 0);
int headerLayoutId = a.getResourceId(R.styleable.PullToRefreshRecyclerView_prvHeaderLayout, -1);
int footerLayoutId = a.getResourceId(R.styleable.PullToRefreshRecyclerView_prvFooterLayout, -1);
//refresh text
pullDownToRefreshText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvPullDownToRefreshText) ?
a.getString(R.styleable.PullToRefreshRecyclerView_prvPullDownToRefreshText) :
getResources().getString(R.string.recycler_default_pull_down_to_refresh);
releaseToRefreshText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvReleaseToRefreshText) ?
a.getString(R.styleable.PullToRefreshRecyclerView_prvReleaseToRefreshText) :
getResources().getString(R.string.recycler_default_release_to_refresh);
refreshingText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvRefreshingText) ?
a.getString(R.styleable.PullToRefreshRecyclerView_prvRefreshingText) :
getResources().getString(R.string.recycler_default_refreshing);
refreshCompletedText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvRefreshCompletedText) ?
a.getString(R.styleable.PullToRefreshRecyclerView_prvRefreshCompletedText) :
getResources().getString(R.string.recycler_default_refresh_completed);
//load more text
pullUpToLoadMoreText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvPullUpToLoadMoreText) ?
a.getString(R.styleable.PullToRefreshRecyclerView_prvPullUpToLoadMoreText) :
getResources().getString(R.string.recycler_default_pull_up_to_load_more);
releaseToLoadMoreText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvReleaseToLoadMoreText) ?
a.getString(R.styleable.PullToRefreshRecyclerView_prvReleaseToLoadMoreText) :
getResources().getString(R.string.recycler_default_release_to_load_more);
loadingMoreText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvLoadingMoreText) ?
a.getString(R.styleable.PullToRefreshRecyclerView_prvLoadingMoreText) :
getResources().getString(R.string.recycler_default_loading_more);
loadMoreCompletedText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvLoadMoreCompletedText) ?
a.getString(R.styleable.PullToRefreshRecyclerView_prvLoadMoreCompletedText) :
getResources().getString(R.string.recycler_default_load_more_completed);
a.recycle();
if (headerLayoutId == -1) {
headerView = LayoutInflater.from(context).inflate(R.layout.recycler_default_header_view, this, false);
setHeader(createDefaultHeader());
} else {
headerView = LayoutInflater.from(context).inflate(headerLayoutId, this, false);
}
if (footerLayoutId == -1) {
footerView = LayoutInflater.from(context).inflate(R.layout.recycler_default_footer_view, this, false);
setFooter(createDefaultFooter());
} else {
footerView = LayoutInflater.from(context).inflate(footerLayoutId, this, false);
}
addView(headerView, 0);
addView(footerView);
setHaveMore(false);
}
2、测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
headerHeight = headerView.getMeasuredHeight();
footerHeight = footerView.getMeasuredHeight();
}
3、排版页面元素
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
headerView.layout(0, 0 - headerView.getMeasuredHeight(), getMeasuredWidth(), 0);
recyclerView.layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
footerView.layout(0, getMeasuredHeight(), getMeasuredWidth(), getMeasuredHeight() + footerView.getMeasuredHeight());
}
4、touch事件分发拦截。这里我们只拦截滑动事件,其他事件交由
RecyclerView
自己去处理。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (getState() == REFRESH_COMPLETED
|| getState() == LOAD_MORE_COMPLETED)
return super.onInterceptTouchEvent(ev);
int action = ev.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
stopReboundAnim();
recyclerView.stopScroll();
lastTouchY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
float curTouchY = ev.getY();
float dy = curTouchY - lastTouchY;
dy = dy > 0 ? dy + 0.5f : dy - 0.5f;
lastTouchY = curTouchY;
//如果滑动距离小于scaledTouchSlop,则把事件交给子View消耗;
//否则此事件交由自己的onTouchEvent(MotionEvent event)方法消耗。
if (Math.abs((int) dy) >= scaledTouchSlop / 2)
return true;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
break;
}
return super.onInterceptTouchEvent(ev);
}
5、处理拦截到的滑动事件。
VelocityTracker
跟踪滑动速度。
@Override
public boolean onTouchEvent(MotionEvent ev) {
enSureVelocityTrackerNonNull();
int action = ev.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
trackMotionEvent(ev);
lastTouchY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
trackMotionEvent(ev);
float curTouchY = ev.getY();
float dy = curTouchY - lastTouchY;
if (dy != 0) {
dy = dy > 0 ? dy + 0.5f : dy - 0.5f;
lastTouchY = curTouchY;
executeMove((int) -dy);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
final VelocityTracker tracker = velocityTracker;
tracker.computeCurrentVelocity(1000, mMaximumVelocity);
int velocity = (int) tracker.getYVelocity();
recycleVelocityTracker();
executeUpOrCancelMotionEvent(velocity);
break;
}
return true;
}
6、执行滑动。
private void executeMove(int distance) {
if (distance == 0)
return;
int scrollY = getScrollY();
int scrolledY = 0;
if (distance < 0) {//向下滑动
//如果正在加载更多,我们避免加载更多底部视图被滑动至不可见。
if (!isLoadingMore() && scrollY > 0) {
scrolledY = Math.max(0 - scrollY, distance);
scrollBy(0, scrolledY);
distance = distance - scrolledY;
}
//滑动列表。
scrolledY = Math.max(0 - getRecyclerViewMaxCanPullDownDistance(), distance);
if (scrolledY != 0)
recyclerView.scrollBy(0, scrolledY);
//如果正在加载更多且已滑动至列表顶部,不可再向下滑动。
if (!isLoadingMore()) {
distance = distance - scrolledY;
distance = toScaledValue(distance);
if (distance != 0)
scrollBy(0, distance);
}
} else {//向上滑动
//如果正在刷新,我们避免刷新头部视图别滑动至不可见。
if (!isRefreshing() && scrollY < 0) {
scrolledY = Math.min(Math.abs(scrollY), distance);
scrollBy(0, scrolledY);
distance = distance - scrolledY;
}
//滑动列表
scrolledY = Math.min(getRecyclerViewMaxCanPullUpDistance(), distance);
if (scrolledY != 0)
recyclerView.scrollBy(0, scrolledY);
//如果正在刷新且已滑动至列表底部,不可再向上滑动。
if (!isRefreshing()) {
distance = distance - scrolledY;
distance = toScaledValue(distance);
if (distance != 0)
scrollBy(0, distance);
}
}
if (getScrollY() < 0) {
if (!isRefreshEnable() || isRefreshing()) {
header.onScroll(getState(), isRefreshEnable(), isRefreshing(), getScrollY(), headerHeight, getRefreshThresholdValue());
return;
}
// getRefreshThresholdValue()释放执行刷新阈值
if (getScrollY() < getRefreshThresholdValue()) {
//release to refresh
setState(RELEASE_TO_REFRESH);
} else {
//pull down to refresh
setState(PULL_DOWN_TO_REFRESH);
}
header.onScroll(getState(), isRefreshEnable(), isRefreshing(), getScrollY(), headerHeight, getRefreshThresholdValue());
} else if (getScrollY() > 0) {
if (!isLoadMoreEnable() || isLoadingMore()) {
footer.onScroll(getState(), isLoadMoreEnable(), isLoadingMore(), getScrollY(), footerHeight);
return;
}
// getLoadMoreThresholdValue()释放执行加载更多阈值
if (getScrollY() > getLoadMoreThresholdValue()) {
//release to load more
setState(RELEASE_TO_LOAD_MORE);
} else {
//pull up to load more
setState(PULL_UP_TO_LOAD_MORE);
}
footer.onScroll(getState(), isLoadMoreEnable(), isLoadingMore(), getScrollY(), footerHeight);
} else {
header.onScroll(getState(), isRefreshEnable(), isRefreshing(), getScrollY(), headerHeight, getRefreshThresholdValue());
footer.onScroll(getState(), isLoadMoreEnable(), isLoadingMore(), getScrollY(), footerHeight);
}
}
7、执行touch结束事件。
private void executeUpOrCancelMotionEvent(int velocity) {
switch (getState()) {
case REFRESHING:
executeRebound(0 - headerHeight);
recyclerView.fling(0, 0 - velocity);
break;
case LOADING_MORE:
executeRebound(footerHeight);
recyclerView.fling(0, 0 - velocity);
break;
case RELEASE_TO_REFRESH:
executeRebound(0 - headerHeight);
break;
case RELEASE_TO_LOAD_MORE:
executeRebound(isHaveMore() ? footerHeight : 0);
break;
default:
executeRebound(0);
recyclerView.fling(0, 0 - velocity);
break;
}
}
private void executeRebound(int destinationScrollY) {
int scrollYDistance = destinationScrollY - getScrollY();
int duration = Math.abs(scrollYDistance);
duration = Math.max(200, duration);
duration = Math.min(500, duration);
if (animator == null) {
animator = ObjectAnimator.ofPropertyValuesHolder(this, PropertyValuesHolder.ofInt(SCROLL_Y, getScrollY(), destinationScrollY));
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.addListener(new SimpleAnimatorListener() {
@Override
public void onAnimationEnd(Animator animation) {
switch (getState()) {
case RELEASE_TO_REFRESH:
if (!isRefreshing() && onRefreshListener != null) {
setState(REFRESHING);
currentPage = startPage;
onRefreshListener.onRefresh(getContext(), currentPage, pageSize);
}
break;
case RELEASE_TO_LOAD_MORE:
if (isHaveMore() && !isLoadingMore() && onRefreshListener != null) {
setState(LOADING_MORE);
currentPage++;
onRefreshListener.onLoadMore(getContext(), currentPage, pageSize);
}
break;
case REFRESH_COMPLETED:
setState(INIT);
lastRefreshTimeStamp = System.currentTimeMillis();
header.updateLastRefreshTime(lastRefreshTimeStamp);
break;
case LOAD_MORE_COMPLETED:
setState(INIT);
break;
}
}
});
} else {
animator.setIntValues(getScrollY(), destinationScrollY);
}
animator.setDuration(duration);
animator.start();
}
使用示例
- 1、简单使用示例:
PullToRefreshRecyclerView pullToRefreshRecyclerView;
//设置分页加载的起始页序号以及每页数据数量
pullToRefreshRecyclerView.initializeParameters(1, 10);
//关闭下拉刷新
// pullToRefreshRecyclerView.setRefreshEnable(false);
//关闭加载更多
// pullToRefreshRecyclerView.setLoadMoreEnable(false);
//设置下拉刷新和上拉加载更多监听
pullToRefreshRecyclerView.setOnRefreshListener(new PullToRefreshRecyclerView.OnRefreshListener() {
@Override
public void onRefresh(@NonNull Context context, int currentPage, int pageSize) {
index = -1;
loadNetData();
}
@Override
public void onLoadMore(@NonNull Context context, int currentPage, int pageSize) {
loadNetData();
}
});
RecyclerView recyclerView = pullToRefreshRecyclerView.getRecyclerView();
recyclerView.setLayoutManager(new LinearLayoutManager(inflater.getContext()));
recyclerView.addItemDecoration(new SpaceItemDecoration(
CompatResourceUtils.getDimensionPixelSize(this, R.dimen.space_16),
CompatResourceUtils.getDimensionPixelSize(this, R.dimen.space_2)
));
//模拟加载网络数据
private int index = -1;
private Random random = new Random();
private void loadNetData(){
pullToRefreshRecyclerView.postDelayed(new Runnable() {
@Override
public void run() {
//刷新(或加载更多)完成
pullToRefreshRecyclerView.completed();
List items = new ArrayList<>();
int count = 7 + random.nextInt(12);
for (int i = 0; i < count; i++) {
index ++;
ClassItem item = new ClassItem();
item.setLabel("this is " + index);
items.add(item);
}
//判定是否是第一页数据
if (pullToRefreshRecyclerView.isFirstPage()) {
adapter3.setData(items);
} else {
adapter3.addData(items);
}
//设置是否还有下一页数据
pullToRefreshRecyclerView.setHaveMore(items.size() >= pullToRefreshRecyclerView.getPageSize());
}
}, 50 + random.nextInt(2000));
}
- 2、自定义下拉刷新:
2.1、设置刷新(头部)
2.2、设置刷新逻辑监听
public void setHeader(@NonNull H header)
2.3、实现刷新逻辑
IHeader header = new IHeader() {
@Override
public void initChildren(@NonNull View headerView) {
//这里初始化下拉刷新view
//也就是app:prvHeaderLayout="@layout/xxx"属性对应的布局文件
}
@Override
public void updateLastRefreshTime(long lastRefreshTimeStamp) {
//这里是上次刷新时间更新监听
}
@Override
public void onUpdateState(int state, CharSequence txt) {
//这里是监听下拉刷新的各种状态
//监听到的状态有:PULL_DOWN_TO_REFRESH、RELEASE_TO_REFRESH、REFRESHING、REFRESH_COMPLETED
switch (state) {
case PullToRefreshRecyclerView.REFRESHING:
//正在刷新,我们可以正在这里启动正在刷新的动画
break;
case PullToRefreshRecyclerView.REFRESH_COMPLETED:
//刷新完成,我们可以在这里关闭正在刷新的动画以及头部复位
break;
default:
break;
}
}
@Override
public void onScroll(int state, boolean refreshEnable, boolean isRefreshing, int scrollY, int headerHeight, int refreshThresholdValue) {
//这里是监听下拉刷新动作
//监听到的状态有:INIT、PULL_DOWN_TO_REFRESH、RELEASE_TO_REFRESH、REFRESHING、REFRESH_COMPLETED
}
};
- 3、自定义上拉加载更多:
3.1、设置加载更多(底部)
3.2、设置加载更多逻辑监听
public void setHeader(@NonNull H header)
3.3、实现加载更多逻辑
IFooter footer = new IFooter() {
@Override
public void initChildren(@NonNull View footerView) {
//这里初始化上拉加载更多view
//也就是app:prvFooterLayout="@layout/xxx"属性对应的布局文件
}
@Override
public void onUpdateState(@State int state, CharSequence txt) {
//这里是监听上拉加载更多的各种状态
//监听到的状态有:PULL_UP_TO_LOAD_MORE、RELEASE_TO_LOAD_MORE、LOADING_MORE、LOAD_MORE_COMPLETED
switch (state) {
case PullToRefreshRecyclerView.LOADING_MORE:
//正在加载更多,我们可以正在这里启动正在加载更多的动画
break;
case PullToRefreshRecyclerView.LOAD_MORE_COMPLETED:
//加载更多完成,我们可以在这里关闭正在加载更多的动画以及底部复位
break;
default:
break;
}
}
@Override
public void onScroll(int state, boolean loadMoreEnable, boolean isLoadingMore, int scrollY, int footerHeight) {
//这里是监听上拉加载更多动作
//监听到的状态有:INIT、PULL_UP_TO_LOAD_MORE、RELEASE_TO_LOAD_MORE、LOADING_MORE、LOAD_MORE_COMPLETED
}
};
使用介绍就到这里。
从0撸出这个开源库不容易,希望童鞋们在GitHub上给一颗星星✨支持下。谢谢!如果在使用过程中不懂(或需要改进的地方),可以在评论里给我留言,也可以联系我。
微信:eoy9527
、QQ:1006368252
。
篇尾
在人生的道路上,当你的希望一个个落空的时候,你也要坚定,要沉着。 —— 朗费罗