之前我们介绍过RecyclerView和SwipeRefreshLayout的交互工作模式,现在,让我们扩展一下SwipeRefreshLayout的功能让它能支持上拉加载,并且能和原生的SwipeRefreshLayout效果切换。
1.功能
支持RecyclerView和ListView下拉刷新和上拉加载
支持主动刷新
下拉刷新能使用SwipeRefreshLayout的原生样式或自定义的延展样式
可自定义ViewGroup定制刷新样式和加载样式
2.说明
总体来说,因为SwipeRefreshLayout本身就支持RecyclerView和ListView的下拉刷新功能,并且一定程度上支持嵌套滑动(对ListView无法支持,当RecyclerView开启嵌套滑动时可以支持),所以我们这个自定义控件继承SwipeRefreshLayout实现是相当方便的,但我们还要考虑到以下这些情况,这也是我们这个自定义控件的主要思路。
要实现延展样式,需要在RecyclerView和ListView中加入headView和footView动态改变它们的高度,当控件处于最顶端继续下拉时,让headView的height随手势的滑动而变化,当控件处于最底端继续上拉时让footView的height随滑动而变化。
ListView本身支持添加headView和footView,但RecyclerView不支持,所以需要重写RecyclerView.Adapter
RecyclerView是否开启嵌套滑动决定了我们实现RecyclerView下拉刷新和上拉加载功能的难易程度,开启嵌套滑动时我们可以直接利用SwipeRefreshLayout支持的嵌套滑动方法来实现。其他情况都需要拦截事件进行滑动处理,我们看一下下面这张图对于事件进行拦截处理的分类情况
从图中我们可以具体看到什么情况下需要拦截事件,我们这个自定义SwipeRefreshLayout实现的关键地方就是利用嵌套滑动或者事件拦截来处理,下面进入正题看下到底该怎么做。
- 类的说明
类 | 描述 |
---|---|
SwipeRefreshLoadLayout | 继承SwipeRefreshLayout,增加上拉加载功能以及各种事件的处理,整个项目的和心类 |
RecycleViewAdapter | 如果要用RecyclerView实现功能,使用此类代替原RecyclerView.Adapter |
SwipeLinearLayoutManager | RecyclerView布局管理器,继承LinearLayoutManager,一个辅助类,使用RecyclerView时必须使用此类 |
Swipe | 内部有公开和非公开的监听接口 |
ListViewHeadAndFootManager | ListView的headView和footView管理器 |
3.通过嵌套滑动方式实现
我们从最简单的地方入手,就是当RecyclerView开启嵌套滑动功能的情况下实现延展样式,这个时候SwipeRefreshLayout能监听到RecyclerVIew是否滑动到了最顶端或最底部,我们需要覆写它下面几个方法:
- void onNestedScrollAccepted(View child, View target, int axes):开启嵌套滑动功能后才会调用,仅在滑动开始时调用一次,可进行滑动相关数据的初始化操作。
- void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed):当RecyclerView滑动到顶端并继续下拉或者滑动到底部并继续上拉时SwipeRefreshLayout会调用此方法,覆写此方法后可通过dyUnconsumed参数获取到当前滑动的距离,从而计算出headView或footView的height增大的值。
- void onNestedPreScroll(View target, int dx, int dy, int[] consumed):当从下拉刷新或上拉加载状态返回时,SwipeRefreshLayout会调用此方法,覆写此方法可计算headView或footView的height减小的值。
- void onStopNestedScroll(View target):手指离开屏幕时SwipeRefreshLayout会调用此方法,覆写此方法我们可以从这个状态开始计算headView或footView回收时其height的值。
同理,按照这个思路,上拉加载也完全可以通过覆写这四个方法实现,当然,这是在RecyclerView开启嵌套滑动的前提下,我们结合代码看一下这部分的具体实现。
覆写SwipeRefreshLayout类中四个关于嵌套滑动的方法
//下拉刷新时手指的滑动距离,意味着headView的height需要改变的大小
private int mTotalUnconsumed;
//上拉加载时手指的滑动距离,意味着footView的height需要改变的大小
private int mTotalUnconsumed2;
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
super.onNestedScrollAccepted(child, target, axes);
mTotalUnconsumed = 0;
mTotalUnconsumed2 = 0;
}
/**
* 下拉刷新返回或者上拉加载返回时需要覆写的方法,
* 通过dy计算headView或者footView的height值,减小其高度,
* 通过consumed[]改变RecyclerView的滑动距离,使之不会滑动的过快造成滑动效果降低,
* 这里我们通过下拉刷新样式决定是调用父类的方法还是自己的方法,如果时需要原生样式,直接调用父类方法,
* 否则调用自己的方法
*/
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
if (refreshStyle == CIRCLE) {
super.onNestedPreScroll(target, dx, dy, consumed);
} else if (refreshStyle == SPREAD) {
if (dy > 0 && mTotalUnconsumed > 0) {
//下拉刷新返回操作
if (dy > mTotalUnconsumed) {
consumed[1] = dy - mTotalUnconsumed;
mTotalUnconsumed = 0;
finishParentDrag();
} else {
mTotalUnconsumed -= dy;
consumed[1] = dy;
}
onPullDownBack(dy);
}
}
//上拉加载返回操作
if (dy < 0 && mTotalUnconsumed2 < 0) {
if (dy < mTotalUnconsumed2) {
consumed[1] = mTotalUnconsumed2 - dy;
mTotalUnconsumed2 = 0;
finishParentDrag();
} else {
mTotalUnconsumed2 -= dy;
consumed[1] = dy;
}
onPullUpBack(dy);
}
}
/**
* 下拉刷新或者上拉加载时需要覆写的方法,
* 通过dyUnconsumed计算headView或者footView的height值,增大其高度,
* 通过下拉刷新样式决定是调用父类的方法还是自己的方法,如果时需要原生样式,直接调用父类方法,
* 否则调用自己的方法
*/
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
if (refreshStyle == CIRCLE) {
super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
} else if (refreshStyle == SPREAD) {
//下拉刷新操作
if (dyUnconsumed < 0 && !isRefreshing) {
mTotalUnconsumed += -dyUnconsumed;
onPullDown(dyUnconsumed);
}
}
//上拉加载操作
if (dyUnconsumed > 0 && !isLoading && footViewVisibility == VISIBLE) {
mTotalUnconsumed2 += -dyUnconsumed;
onPullUp(dyUnconsumed);
}
}
/**
* 手指释放时调用,开启一个动画让headView或者footView的height恢复
*/
@Override
public void onStopNestedScroll(View target) {
if (refreshStyle == CIRCLE) {
super.onStopNestedScroll(target);
} else if (refreshStyle == SPREAD) {
//下拉刷新释放操作
if (mTotalUnconsumed > 0 && !isRefreshing) {
release();
mTotalUnconsumed = 0;
}
}
//上拉加载释放操作
if (mTotalUnconsumed2 < 0) {
release();
mTotalUnconsumed2 = 0;
}
}
对于以上代码来说,我们加入了下拉刷新的样式判断,如果样式是CIRCLE即SwipeRefreshLayout原生样式,那就直接调用父方法,否则如果是SPREAD即延展样式,我们才需要调用我们自己的实现代码。
下面是下拉刷新或上拉加载时执行的代码,会分别调用onPullDown(int dy)、onPullDownBack(int dy)、onPullUp(int dy)、onPullUpBack(int dy)方法,然后通过Swipe.OnChangeViewHeight接口让headView或footView的height发生变化改变它的高度,这里只贴一小段代码,具体的可查看项目源码。
SwipeRefreshLoadLayout中:
/**
* 下拉刷新
*/
private void onPullDown(int dy) {
dragAction = DRAG_ACTION_PULL_DOWN;
pullDownDistance += Math.abs(dy) / 2;
changeTipsRefresh();
if (onChangeViewHeight != null) {
onChangeViewHeight.changeHeadViewHeight(pullDownDistance);
}
}
RecycleViewAdapter实现了Swipe.OnChangeViewHeight接口,其中代码:
@Override
public void changeHeadViewHeight(int headViewHeight) {
if (headViewHolder.headView != null && headViewHeight >= 0) {
headViewLayoutParams.height = headViewHeight;
headViewHolder.headView.setLayoutParams(headViewLayoutParams);
}
}
现在,通过SwipeRefreshLayout的帮助我们RecyclerView的下拉刷新和上拉加载已经实现了,但当RecyclerView禁止嵌套滑动时,上面的四个方法就会无法使用,并且如果我们要实现ListView的这一功能,我们必须要通过事件拦截来实现。
4.通过事件拦截方式实现
事件分发我们不多做介绍了,这里就两点要说的,什么时候拦截事件,拦截以后要做什么,这是我们这篇自定义ViewGroup要面临的问题。在RecyclerView中,我们通过View类的boolean canScrollVertically(int direction)方法确定它是否滑到了最顶端或最底部。在ListView中,我们通过ListViewCompat类的boolean canScrollList(ListView listView, int direction)判断它是否滑到了最顶端或最底部,有了这两个方法,我们在onInterceptTouchEvent(MotionEvent ev)
中进行判断即可,符合情况的我们返回true然后交给onTouchEvent(MotionEvent ev)处理,不符合的直接调用父方法即可。
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (action == null) {
return super.onTouchEvent(ev);
}
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
float currentY = ev.getY();
int dy = (int) (currentY - initialDownY);
if (dy >= 0) {//dy>0说明此时执行的是下拉刷新或者上拉加载返回操作
if (action == ACTION_PULL_DOWN && !isRefreshing) {
onPullDown(dy);
} else if (action == ACTION_PULL_UP && !isLoading) {
onPullUpBack(dy);
if (recyclerView != null) {
recyclerView.scrollBy(0, dy);
}
}
} else {//dy<0说明此时执行的是上拉加载或者下拉刷新返回操作
if (action == ACTION_PULL_DOWN && !isRefreshing) {
onPullDownBack(dy);
} else if (action == ACTION_PULL_UP && !isLoading && footViewVisibility == VISIBLE) {
onPullUp(dy);
if (recyclerView != null) {
recyclerView.scrollBy(0, -dy);
}
if (listView != null) {
ListViewCompat.scrollListBy(listView, -dy);
}
}
}
initialDownY = currentY;
break;
case MotionEvent.ACTION_UP:
action = null;
release();
break;
}
return action != null || super.onTouchEvent(ev);
}
private float initialDownY;
private final int ACTION_PULL_DOWN = 0X00C1;
private final int ACTION_PULL_UP = 0X00D1;
private Integer action = null;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (recyclerView != null) {
//当刷新类型为CIRCLE且处于刷新状态时,recyclerView的嵌套滑动不再响应,所以无法进行上拉加载,因此通过拦截事件利用onTouchEvent()实现上拉加载
if (refreshStyle == CIRCLE && isRefreshing && !recyclerView.canScrollVertically(1)) {
Boolean x = isIntercept(ev, true);
if (x != null) return x;
}
//当recyclerView禁止嵌套滑动时,上拉加载需要使用onTouchEvent()实现,所以这里要进行拦截
if (!recyclerView.isNestedScrollingEnabled() && !recyclerView.canScrollVertically(1)) {
Boolean x = isIntercept(ev, true);
if (x != null) return x;
}
//当刷新类型为SPREAD时,如果recyclerView禁止了嵌套滑动,那这里需要拦截事件让onTouchEvent()实现下拉刷新
if (refreshStyle == SPREAD && !recyclerView.isNestedScrollingEnabled() && !recyclerView.canScrollVertically(-1)) {
Boolean x = isIntercept(ev, false);
if (x != null) return x;
}
}
if (listView != null) {
if (refreshStyle == CIRCLE && isRefreshing && !ListViewCompat.canScrollList(listView, 1)) {
Boolean x = isIntercept(ev, true);
if (x != null) return x;
}
if (!ListViewCompat.canScrollList(listView, 1)) {
Boolean x = isIntercept(ev, true);
if (x != null) return x;
}
if (refreshStyle == SPREAD && !ListViewCompat.canScrollList(listView, -1)) {
Boolean x = isIntercept(ev, false);
if (x != null) return x;
}
}
return super.onInterceptTouchEvent(ev);
}
@Nullable
private Boolean isIntercept(MotionEvent ev, boolean type) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
initialDownY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
float y = ev.getY();
if (type) {
if (y - initialDownY < 0) {//判断上滑或者下滑操作
initialDownY = y;
action = ACTION_PULL_UP;
return true;
}
} else {
if (y - initialDownY > 0) {
initialDownY = y;
action = ACTION_PULL_DOWN;
return true;
}
}
break;
}
return null;
}
上面这段代码实现了何时拦截事件的逻辑,以及拦截后的相应处理。当然,这只是我个人实现的一种方式或许并不适用于所有人,对于每个人而言都各有一种实现方式,所以我也不准备强行解释上面的代码了,以上这段代码只作为参考,提供一种实现思路。
我们可以看到,在事件拦截后,将相应的操作交给了onTouchEvent()方法处理,在onTouchEvent()方法中,通过判断上下滑动,确定手指滑动的距离,调用onPullDown(int dy)、onPullDownBack(int dy)、onPullUp(int dy)、onPullUpBack(int dy)方法来改变headView和footView的高度,从而达到之前利用嵌套滑动方式同样的效果。
现在,我们实现了图中所有情况的下拉刷新和上拉加载操作,我们再看看其他几个类。
5.RecycleViewAdapter类
我们知道在RecyclerView中需要手动添加headView和footView,所以我们需要继承RecyclerView.Adapter实现一个自己的Adapter,在这个Adapter中,需要实现原有的Adapter抽象方法,并再次抽象出相应的方法供外部使用,看一下该类中和原有Adapter的对应方法, 我们使用时只需要实现这几个新的抽象方法即可。
RecyclerView.Adapter的原有方法 | RecycleViewAdapter中对应的方法 |
---|---|
int getItemCount() | int getCounts() |
RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) | RecyclerView.ViewHolder onNewViewHolder(@NonNull ViewGroup parent, int viewType) |
void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) | void onSetViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) |
int getItemViewType(int position) | int getItemType(int position) |
- 有一点要注意,在使用RecyclerView.Adapter类中其他没有覆写过的方法(也就是除了上述表中的其他方法)时要注意position,因为我们的RecyclerView默认已经有两个View了,headView和footView,所以在碰到有方法中有position时要注意当前position具体是哪一个,一般真正的值是position-1,因为要减去一个headView。
自定义headView或footView
此外,在此类中额外实现了一个功能,那就是可以自定义ViewGrou实现延展样式的headView和footView,如果不满意我实现的样式,可以自己定义。实现此功能的代码如下:
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == VIEW_TYPE_FOOT) {
if (footViewHolder == null) {
//在这里判断一下是否有自定义的ViewGroup作为headView或footView
if (mySwipe.getFootView() == null) {
footViewHolder = new FootViewHolder(LayoutInflater.from(mContext).inflate(R.layout.srll_foot, null, false), true);
} else {
footViewHolder = new FootViewHolder(mySwipe.getFootView(), false);
}
}
return footViewHolder;
} else if (viewType == VIEW_TYPE_HEAD) {
if (headViewHolder == null) {
if (mySwipe.getHeadView() == null) {
headViewHolder = new HeadViewHolder(LayoutInflater.from(mContext).inflate(R.layout.srll_head, null, true), true);
} else {
headViewHolder = new HeadViewHolder(mySwipe.getHeadView(), false);
}
}
return headViewHolder;
}
return onNewViewHolder(parent, viewType);
}
private class FootViewHolder extends RecyclerView.ViewHolder {
private TextView tvFootTip;
private ViewGroup footView;
private ImageView ivFootRefresh;
private FootViewHolder(View itemView, boolean isFromSelf) {
super(itemView);
this.footView = (ViewGroup) itemView;
this.footView.setVisibility(mySwipe.footViewVisibility);
if (isFromSelf) {
ViewGroup childAt = (ViewGroup) footView.getChildAt(0);
ViewGroup.LayoutParams childLayoutParams = childAt.getLayoutParams();
childLayoutParams.height = refreshViewHeight;
//设置底部加载子视图高度
childAt.setLayoutParams(childLayoutParams);
this.tvFootTip = footView.findViewById(R.id.tv_foot_tip);
this.ivFootRefresh = footView.findViewById(R.id.iv_foot_refresh);
this.ivFootRefresh.animate().setInterpolator(new LinearInterpolator());
LinearLayout llLoadMore = footView.findViewById(R.id.ll_load_more);
llLoadMore.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mySwipe.doLoadMore();
}
});
}
//设置底部加载父视图高度
footViewLayoutParams = new RecyclerView.LayoutParams(-1, refreshViewHeight);
footView.setLayoutParams(footViewLayoutParams);
}
}
private class HeadViewHolder extends RecyclerView.ViewHolder {
private ViewGroup headView;
private TextView tvHeadTip;
private ImageView ivHeadArrow;
private TextView tvRefreshTime;
private ImageView ivHeadRefresh;
private HeadViewHolder(View itemView, boolean isFromSelf) {
super(itemView);
this.headView = (ViewGroup) itemView;
if (isFromSelf) {
ViewGroup childAt = (ViewGroup) headView.getChildAt(0);
ViewGroup.LayoutParams childLayoutParams = childAt.getLayoutParams();
childLayoutParams.height = refreshViewHeight;
childAt.setLayoutParams(childLayoutParams);
this.tvHeadTip = headView.findViewById(R.id.tv_head_tip);
this.ivHeadArrow = headView.findViewById(R.id.iv_head_arrow);
this.ivHeadArrow.animate().setInterpolator(new LinearInterpolator());
this.ivHeadRefresh = headView.findViewById(R.id.iv_head_refresh);
this.ivHeadRefresh.animate().setInterpolator(new LinearInterpolator());
this.tvRefreshTime = headView.findViewById(R.id.tv_refresh_time);
if (!TextUtils.isEmpty(Swipe.getLastRefreshTime(mContext))) {
this.tvRefreshTime.setText("最后更新:" + Swipe.getLastRefreshTime(mContext));
} else {
this.tvRefreshTime.setVisibility(View.GONE);
}
}
headViewLayoutParams = new RecyclerView.LayoutParams(-1, 0);
headView.setLayoutParams(headViewLayoutParams);
}
}
原理很简单,在onCreateViewHolder()中,先判断是否有自定义的ViewGroup作为headView或者footView,如果有,传入ViewHolder中,作为当前的headView或footView,当我们做下拉刷新或者上拉加载时,就动态的改变此ViewGroup的height,当然,此时只能做到把当前headView或footView拉伸或压缩,无法改变其他的状态(比如自己定义的箭头图片或者刷新图片什么时候做出相应的变化),所以需要一个接口来监听我们做下拉刷新或上拉加载的状态,继续往下看。
6.Swipe类
这是一个为了满足各种监听和通知实现的一个类,里面是各种接口,其中有两个public接口和两个protect接口。
/**
* 上拉和下拉时滑动监听
*/
public interface OnSlideActionListener {
/**
* 释放刷新行为
*/
void releaseRefreshAction();
/**
* 下拉刷新行为
*/
void downRefreshAction();
/**
* 释放加载行为
*/
void releaseLoadAction();
/**
* 上拉加载行为
*/
void upLoadAction();
}
/**
* 改变头尾提示信息,仅限本包类
*/
interface OnChangeViewTip {
/**
* 改变底部view提示
*
* @param tips
*/
void changeFootTips(String tips);
/**
* 改变头view提示
*
* @param tips
*/
void changeHeadTips(String tips);
}
/**
* 监听刷新和加载更多
*/
public interface OnRefreshAndLoadListener {
/**
* 下拉刷新
*/
void refresh();
/**
* 上拉加载
*/
void loadMore();
}
/**
* 监听头尾view的高度变化
*/
interface OnChangeViewHeight {
/**
* 改变头view高度
*
* @param headViewHeight
*/
void changeHeadViewHeight(int headViewHeight);
/**
* 改变底部view高度
*
* @param footViewHeight
*/
void changeFootViewHeight(int footViewHeight);
}
- OnSlideActionListener:上拉和下拉时滑动监听,一般不用实现,它监听了所有的滑动事件,只有在自定义headView和footView时需要实现这个接口,可以通过这个接口中的方法改变状态。
- OnChangeViewTip:用protect修饰,不对外开放,改变提示信息,如是自定义headView或footView,直接实现OnSlideActionListener接口即可,所以开放此接口无意义,仅由RecycleViewAdapter类和ListViewHeadAndFootManager类实现。
- OnRefreshAndLoadListener:刷新或加载监听。
- OnChangeViewHeight:改变headView或footView高度,实现延展样式,用protect修饰,由RecycleViewAdapter类和ListViewHeadAndFootManager类实现。
以上这些接口在SwipeRefreshLoadLayout类中都有调用的地方,都非常容易理解所以不贴代码了,感兴趣的可以看下整个项目。
7.ListViewHeadAndFootManager类
此类和RecycleViewAdapter类功能相似,由于ListView本身有setHeadView()和setFootView()方法,所以不需要继承BaseAdapter来实现headView和footView,里面的代码和RecycleViewAdapter的代码大致相同,所以就不多做介绍了。
8.SwipeLinearLayoutManager类
继承于LinearLayoutManager类,里面只覆写了一个方法:
@Override
public int getDecoratedBottom(View child) {
if (mySwipe != null && child.getId() == mySwipe.headViewId) {
return 1 + getBottomDecorationHeight(child);
}
return super.getDecoratedBottom(child);
}
- 我们在使用RecyclerView时,禁用嵌套滑动的情况下需要使用boolean canScrollVertically(int direction)方法判断是否滑到了最顶端或最底部,当滑到最顶端时,如果headView的height为0,那么此方法就会判断失误,无法触发下拉刷新,具体原因可以追踪一下源码,此方法会调用一次int getDecoratedBottom(View child)方法,我们只要判断当child为headView时返回值大于0即可。
以上,就是本问自定义ViewGroup的全部内容了,具体使用方法和源码可以在GitHub上查看: