本文通过修改和理解 https://github.com/HomHomLin/Android-PullToRefreshRecyclerView 中的源码来探索下拉刷新上拉加载的原理,大家可以自行下载其源码来看,我这里就不贴了。其下拉刷新使用的是原生安卓系统的,后面我会教大家使用原声下拉刷新来进行自定制我们需要的动画效果。
其实原理跟ListView这些大同小异,都需要使用RecyclerView.OnScrollListener滚动的监听器来监听RecyclerView是否滑动到了底部,这样我们就可以执行添加底部动画的操作。
根据原理我们能够知道,做一个下拉刷新的动画加载,我们需要做的有两个动作:
1.如何判断RecyclerView已经滑动到底部。
2.如何添加RecyclerView的底部动画。
只要解决以上问题,你将可以实现你所想要的任何底部动画的自定制,当然,在当今社会,随着知识越来越复杂和多变,所以我建议大家应该采纳别人的代码来修改 而不是自己造轮子,只要还是理解其原理,理解原理后想怎么改都行。同时也建立使用源代码lib而不是直接在build文件中直接依赖,这里我们可以自己写注释,自己修改别人的代码,百利无一害啊。
说那么,我们要开始撸一发了 先上代码结构图,我们先来理解一下思路先:
layout文件:
只有一个 ptrrv_root_view.xml 布局文件 用来包含一个相对布局和里面嵌套一个RecyclerView 作为布局的父布局
com.lhh.ptrrv.library
CircleImageView:下拉刷新圆圈转动的的团案,来自于源码直接复制抽取
MaterialProgressDrawable:下拉刷新进度展示控制Drawable源码直接抽取
CustomSwipeRefreshLayout:自定义的下拉刷新修改样式后的SwipeRefreshLayout,来自源码复制后修改的代码
CustomProgressDrawable:自定义重载MaterialProgressDrawable方法来绘制新图案控制部分动画
PullToRefreshRecyclerViewCustom:自定义上拉下拉实现的ViewGroup,继承自CustomSwipeRefreshLayout或者SwipeRefreshLayout,以便直接获取下拉刷新的能力。
com.lhh.ptrrv.library.footer.loadmore
BaseLoadMoreView:加载刷新视图的基础,继承自RecyclerView.ItemDecoration
DefaultLoadMoreView:默认的加载刷新动画视图
com.lhh.ptrrv.library.footer.header
其文件夹内的文件跟footer.loadmore文件夹一样的效果,只是这个是头部
impl
PrvInterface:这个就是一个接口,这个接口就是在PullToRefreshRecyclerViewCustom继承它,实现一些必要的方法。
结构大概就是这样样子了,大家是不是觉得很简单,其实,RecyclerView的下拉刷新上拉加载的原理也很简单,只要我们耐心看下去,下次看到产品狗改动画我们就不会那么无语了。
我们看一个东西的源码,首先要找到入口,然后一步步去探索:
我们入口就是PullToRefreshRecyclerViewCustom类,其他所有的类都是为这个类服务的,所以我们先看看他的构造方法:
完成的就是我们常写的初始化,初始化数据,初始化view,初始化设置监听器。不叼说那么多,为了你们我直接贴代码了
以大家的聪明才智,估计这都不是事情了,但我要说一点,就是为啥第二张图,我们可以设置下拉旋转的颜色捏,有木有想到一个类,对了。。就是原声支持的下拉刷新类库SwipeRefreshLayout,但是我这里对其进行了重写。所以我们来一起看下面PullToRefreshRecyclerViewCustom类的继承:
public class PullToRefreshRecyclerViewCustom extends CustomSwipeRefreshLayout implements PrvInterface {
PullToRefreshRecyclerViewCustom类就是通过继承SwipeRefreshLayout来实现下拉的,我们使用的时候把他当成SwipeRefreshLayout来用就好了,我们也是尽量实现这样的封装的呢。
好啦,是不是觉得下拉刷新的迷雾拨开了,当然要自定制SwipeRefreshLayout需要了解其源码,下面提供一个文章供大家学习,往后我也会自己写出自己的理解。
http://mp.weixin.qq.com/s?__biz=MzAxMTI4MTkwNQ==&mid=2650820782&idx=1&sn=a6012e78222fc1647d3ee95c71d4d4ea&scene=21#wechat_redirect
前面我们说了,下拉刷新需要的两个条件,其中一个就是如何判断RecyclerView已经滑动到底部,那么我们开始吧,看过listview下拉刷新原理都知道,我们是如何判断的,其实就是依靠滚动监听,那么我们该处理RecyclerView的滚动事件还是PullToRefreshRecyclerViewCustom的呢,学事件分发学的好的朋友应该都知道一种事情,那就是,如果父布局不拦截,事件会一直传递到子view中执行,所以我们监听的应该是RecyclerView的滚动事件,但是呢,有一种情况的就是,当我们需要扩展滚动监听的时候如何做呢?下面我们都将会讲到,很多的困惑留存在我们的心中。就像游戏通关一样,每一个技术的攻克都会给你带来强大的自信。。。。。
InterOnScrollListener类就是我们定义的内部RecyclerView滚动监听,之前初始化的时候就是设置这个监听器,这个因为代码太多 我直接复制下来
/**
* 滚动监听器
*/
private class InterOnScrollListener extends RecyclerView.OnScrollListener {
/**
* 滚动状态改变
*
* @param recyclerView
* @param newState
*/
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
//do super before callback
if (mOnScrollLinstener != null) {
mOnScrollLinstener.onScrollStateChanged(recyclerView, newState);
}
}
/**
* 滚动位移发生改变
*
* @param recyclerView
* @param dx
* @param dy
*/
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
//do super before callback
if (getLayoutManager() == null) {
//here layoutManager is null
return;
}
//计算头布局的偏移量 然后滚动时对头部进行滚动 哈哈哈——+
mCurScroll = dy + mCurScroll;
if (mHeader != null) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
mHeader.setTranslationY(-mCurScroll);
} else {
ViewHelper.setTranslationY(mHeader, -mCurScroll);
}
}
//声明第一个item、可见的item数量,item的总数量、最后可见的item
int firstVisibleItem, visibleItemCount, totalItemCount, lastVisibleItem;
//获取和其绑定的item总数量、其中并不包括暂时分离的和报废的,既获取的时候可见的item
visibleItemCount = getLayoutManager().getChildCount();
//返回绑定的适配器item的数量 既总数量
totalItemCount = getLayoutManager().getItemCount();
//获取第一个可见item的position
firstVisibleItem = findFirstVisibleItemPosition();
//sometimes ,the last item is too big so as that the screen cannot show the item fully
//获取最后一个可见item的position
lastVisibleItem = findLastVisibleItemPosition();
// lastVisibleItem = mLinearLayoutManager.findLastCompletelyVisibleItemPosition();
//判断是否可以下拉刷新
if (mIsSwipeEnable) {
if (findFirstCompletelyVisibleItemPosition() != 0) {
//here has a bug, if the item is too big , use findFirstCompletelyVisibleItemPosition will cannot swipe
PullToRefreshRecyclerViewCustom.this.setEnabled(false);
} else {
PullToRefreshRecyclerViewCustom.this.setEnabled(true);
}
}
Log.e("canScrollVertically(1)", canScrollVertically(1) + "");
//如果总数量少于一次性加载数量 就没有必要加载了
if (totalItemCount < mLoadMoreCount) {
setHasMoreItems(false);
isLoading = false;
//1.0 没有正在加载
//2.0 还有更多的item
//3.0 isSlideToBottom(getRecyclerView()) 是否拉到底部
} else if (!isLoading && hasMoreItems && mRecyclerView.canScrollVertically(1)) {
if (mPagingableListener != null) {
isLoading = true;
mPagingableListener.onLoadMoreItems();
}
}
if (mOnScrollLinstener != null) {
mOnScrollLinstener.onScrolled(recyclerView, dx, dy);
mOnScrollLinstener.onScroll(recyclerView, firstVisibleItem, visibleItemCount, totalItemCount);
}
}
}
上面是所有的代码,现在我们看一下核心的代码:
核心的代码很容易,就是先判断一下适配器中的数据是否小于默认情况下加载的数量,如果小于后面肯定没有数据了,也就不需要再添加,如果大于或者等于的时候,我们就进行判断,查看是否到达底部,执行PagingableListener这个接口的方法,这个接口是对外的,当我们加载完成后,可以通过该接口实现我们想要的操作,比如 把加载动画显示出来,加载完成后加载动画消失的方法当然是放在网络加载完成之后。
我们看到条件有三个:
1.0 没有正在加载
2.0 还有更多的item
3.0 mRecyclerView.canScrollVertically(1) 是否拉到底部
这里我说一下mRecyclerView.canScrollVertically(1)这个方法参数为1时,返回false 说明该view实现了拖动到底部的操作,这个参数为-1时 返回false说明拖动到顶部。
如果想更深入的了解这个方法,我建议 看一下这个网站的:
http://www.open-open.com/lib/view/open1474266969512.html#
上面我们完成了拖动到布局的监听,但是为了更加容易拓展滚动监听,我们还需要完成一部,就是在事件监听的后面再加上我们在外部父布局添加的监听:
public interface OnScrollListener {
void onScrollStateChanged(RecyclerView recyclerView, int newState);
void onScrolled(RecyclerView recyclerView, int dx, int dy);
//old-method, like listview 's onScroll ,but it's no use ,right ? by linhonghong 2015.10.29
void onScroll(RecyclerView recyclerView, int firstVisibleItem, int visibleItemCount, int totalItemCount);
}
上面接口就是PullToRefreshRecyclerViewCustom类中的自定义滚动监听,我们为了尽量封装成RecyclerView来使用,PullToRefreshRecyclerViewCustom类对外扩展的类是:
//添加外部的滚动监听器扩展
@Override
public void addOnScrollListener(PullToRefreshRecyclerViewCustom.OnScrollListener onScrollLinstener) {
mOnScrollLinstener = onScrollLinstener;
}
而我们在滚动事件中的后面也需要执行接口的方法,以下是部分代码:
/**
* 滚动监听器
*/
private class InterOnScrollListener extends RecyclerView.OnScrollListener {
/**
* 滚动状态改变
*
* @param recyclerView
* @param newState
*/
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
//do super before callback
if (mOnScrollLinstener != null) {
mOnScrollLinstener.onScrollStateChanged(recyclerView, newState);
}
}
/**
* 滚动位移发生改变
*
* @param recyclerView
* @param dx
* @param dy
*/
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (mOnScrollLinstener != null) {
mOnScrollLinstener.onScrolled(recyclerView, dx, dy);
mOnScrollLinstener.onScroll(recyclerView, firstVisibleItem, visibleItemCount, totalItemCount);
}
}
}
说完了怎么去监听拉到底部,我们要实现怎么去添加底部的动画加载了:
/**
* 显示加载数据的进度view
*
* @param needSetSelection 是否需要跳转到第一位
*/
public void showLoadView(boolean needSetSelection) {
boolean hasMoreItems = true;
if (getLayoutManager() == null) {
return;
}
//如果items太短,不展示加载页面
// if items is too short, don't show loadingview
if (getLayoutManager().getItemCount() < mLoadMoreCount) {
hasMoreItems = false;
}
//根据传入参数设置加载页面是否显示
setHasMoreItems(hasMoreItems);
//设置正在加载
isLoading = true;
//判断是否需要跳至可见的第一个item的位置
if (needSetSelection) {
int first = findFirstVisibleItemPosition();
mRecyclerView.scrollToPosition(first);
}
}
/**
* 隐藏加载显示
*/
public void hideShowView() {
boolean hasMoreItems = false;
if (getLayoutManager() == null) {
return;
}
if (mLoadMoreFooter != null) {
//if it's last line, minus the extra height of loadmore
mCurScroll = mCurScroll - mLoadMoreFooter.getLoadMorePadding();
}
//如果items太短,不展示加载页面
// if items is too short, don't show loadingview
if (getLayoutManager().getItemCount() < mLoadMoreCount) {
hasMoreItems = false;
}
//根据传入参数设置加载页面是否显示
setHasMoreItems(hasMoreItems);
//设置正在加载结束
isLoading = false;
}
上面就是我改写后隐藏和显示的暴露方法,就是做一些安全检测然后调用setHasMoreItems()这个方法来执行出现动画或者隐藏动画,各位要注意一点这里的hasMoreItems是一个局部变量 不是全局变量,当然显示的时候设置正在加载,隐藏时候顺手设置一下加载结束掉,下面我们来看一下setHasMoreItems()方法的实现:
/**
* 设置还有更多的数据展示 并显示出动画
*
* @param hasMoreItems
*/
private void setHasMoreItems(boolean hasMoreItems) {
// this.hasMoreItems = hasMoreItems;
//如果没有加载布局就使用默认的
if (mLoadMoreFooter == null) {
mLoadMoreFooter = new DefaultLoadMoreView(getContext(), getRecyclerView());
}
//如果没有数据了删除动画 如果还有展示动画
if (!hasMoreItems) {
//remove loadmore
mRecyclerView.removeItemDecoration(mLoadMoreFooter);
} else {
//add loadmore
mRecyclerView.removeItemDecoration(mLoadMoreFooter);
mRecyclerView.addItemDecoration(mLoadMoreFooter);
}
}
看看,多简单的逻辑,就是加载结束后删除分割线,加载时删除后添加嘛。。问题再次来了,特码为什么添加分割线他就能实现这样的效果呢?带着这些疑问,我们去探索一下默认底部分割线是什么样子的:
@Override
public void onDrawLoadMore(Canvas c, RecyclerView parent) {
.....
}
这是默认的,我们只看到他实现了这个方法,然后没鸟。。我们能猜出来这个方法其实就是画,绘制我们想要的效果。
我们再去看BaseLoadMoreView,特么看起来东西挺多其实只有一点核心的东西
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
mInvalidateHanlder.removeMessages(MSG_INVILIDATE);
onDrawLoadMore(c, parent);
mInvalidateHanlder.sendEmptyMessageDelayed(MSG_INVILIDATE, mUpdateTime);
}
/**
* @param outRect
* @param itemPosition
* @param parent
*/
@Override
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
if(itemPosition == parent.getAdapter().getItemCount() - 1) {
outRect.set(0, 0, 0, getLoadMorePadding());
}
}
核心的就是在这两个方法,其实就是getItemOffsets产生间隔,然后在onDrawOver里面绘制,
protected Handler mInvalidateHanlder = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (mRecyclerView == null || mRecyclerView.getAdapter() == null) {
return;
}
int lastItemPosition = mRecyclerView.getAdapter().getItemCount() - 1;
if (mPtrrvUtil.findLastVisibleItemPosition(mRecyclerView.getLayoutManager()) == lastItemPosition) {
//如果当前可见的最后一个item
mRecyclerView.invalidate();
}
}
};
Handler这里发送了一个延时的操作,是为了保障圈圈一直在转。。。
onDrawLoadMore方法中最重要的就是这一段:
final View child = parent.getChildAt( childSize - 1 ) ;
是为了取出最后的子item来进行绘制分割线的,如果你想更深入了解分割线的绘制,可以看下面这篇文章:
http://blog.csdn.net/u010782846/article/details/52620175(解密RecyclerView自定义分割线”)