自从使用了RecyclerView再也回不去了,什么ListView、GridView统统让他们退休了。必须安利起来,用了才能体会它的神奇!

根据使用RecyclerView以来,拓展的一些功能及对RecyclerView.Adapter的封装,想在这里跟大家分享一些经验,还望指正。

1. 功能介绍

基于对RecyclerView在使用过程中的一些痛点写了这个开源项目 TurboRecyclerViewHelper 。功能点详见README。

本次主要介绍针对TurboRecyclerView上拉/左滑的功能的实现及思路。

下面直接进入正题…

2. 状态分析

以上拉加载的过程作为本次分享的一个栗子(左滑同理)。状态如下图:

我们所关心的是RecyclerView滑动到底部的状态,这个状态下是我们需要处理的临界状态

下文中出现的代码均为精简后的代码片段,本文重在介绍思路,所涉及的知识点假设您已经掌握,故不再展开赘述。

3. 临界状态的限制条件

废话不多说直接看代码:

1
2
3
if (!mLoadEnabled || canScrollEnd() || mIsLoading || isEmpty()) {
return super...;
}

我们从两个方面来分析可以开始处理Touch事件的条件:

3.1 客观条件

所谓客观条件即是RecyclerView滑动到底部的这个状态的物理状态,体现在代码上就是

1
2
3
4
private boolean canScrollEnd() {
//判断在纵向是否还能向上滑动
return ViewCompat.canScrollVertically(this, 1);
}

这里为了简化只判断了纵向是否可以向下滑动,实际代码中这里是判断条件为ViewCompat.canScrollVertically(this, 1) || ViewCompat.canScrollHorizontally(this, 1)

这个条件返回值如果是false,则代表我们可以从这个临界状态开始处理Touch事件,否则不处理。

3.2 逻辑条件(主观条件)

逻辑条件(或者称为主观条件)是设计控件本身所考虑的限制条件。

判断条件如下:

1
2
//是否允许上拉 || 是否正处于刷新状态 || 是否处理空状态
if (!mLoadEnabled || mIsLoading || isEmpty())

在以上条件下我们认为是控件本身应处于不可上拉的状态,我们不做处理。

4. Touch事件的处理

在同时满足客观条件和逻辑条件下,我们就可以开始处理上拉的效果。

4.1 记录初始值

我们需要在MotionEvent.ACTION_DOWN MotionEvent.ACTION_POINTER_DOWN时记录初始值:

1
2
mInitialMotionX = getMotionEventX(e, actionIndex);
mInitialMotionY = getMotionEventY(e, actionIndex);

4.2 判断滑动是否符合预期值

MotionEvent.ACTION_MOVE时判断是否是上拉的状态:

1
2
3
4
5
6
//LayoutManager中提供的判断是否纵向可以滑动的方法
final boolean canScrollVertically = getLayoutManager().canScrollVertically();
...
final int y = getMotionEventY(e, index);
int deltaY = y - mInitialMotionY;
if (canScrollVertically && Math.abs(deltaY) > mTouchSlop && deltaY < 0) { ... //处理上拉效果}

记录当前Y值,且判断是否手指在上滑状态。

4.3 实现上拉效果

在自定义控件中,实现上拉效果有多种途径,例如大家常用的利用Scroller配合scrollTo来实现滑动,但是在RecyclerView的实现中并不支持这种方式。这个方案Close!

1
2
3
4
5
@Override
public void scrollTo(int x, int y) {
Log.w(TAG, "RecyclerView does not support scrolling to an absolute position. "
+ "Use scrollToPosition instead");
}

这里采用setTranslationY来实现上拉效果,根据手指移动的距离计算出移动距离来改变RecyclerView的位置。

1
2
3
4
...
float targetEnd = -dampAxis(deltaY); //阻尼值的计算
setTranslationY(targetEnd);
return true; //消费掉此事件

到这里上拉的效果已经实现完毕。

4.4 复位及刷新

距离成功只差一点点。
在用户手指松开以后,我们要考虑做两件事:RecyclerView的复位及是否可以处于刷新状态。
针对复位操作,我们只需要逆向setTranslationY值即可。这里我们采用属性动画来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void animateOffsetToEnd(final String propertyName, final Interpolator interpolator, float... value) {
if (mResetAnimator == null) {
mResetAnimator = new ObjectAnimator();
mResetAnimator.setTarget(this);
}
mResetAnimator.cancel();
mResetAnimator.setPropertyName(propertyName);
mResetAnimator.setFloatValues(value);
mResetAnimator.setInterpolator(interpolator);
mResetAnimator.start();
}

...

if(canScrollVertically)
animateOffsetToEnd("translationY", mInterpolator, 0f);
...

对于刷新我们要做的事情也比较简单,判断当前移动距离达到阈值后,回调监听事件并显示LOAING_VIEW。

1
2
3
4
Log.i(TAG, "refreshing...");
mIsLoading = true;
dispatchOnLoadingMoreListeners();
smoothScrollToPosition(mLastVisibleItemPosition + 1);

刷新完毕后,记得通知TurboRecyclerView更新状态哦!

1
2
3
4
5
6
7
8
9
10
11
mTurboRecyclerView.addOnLoadingMoreListener(new OnLoadMoreListener() {
@Override
public void onLoadingMore() {
handler.postDelayed(new Runnable() {
@Override
public void run() {
mRecyclerView.loadMoreComplete(Arrays.asList(sCheeseStrings));
}
}, 2000);
}
});

至此整个上拉到复位刷新的过程完成。
完整代码详见 TurboRecyclerView.java

希望我的分享能让您能有所收获。也欢迎支持一下这个项目~ 持续维护~

下次准备介绍一下对RecyclerView.Adapter的封装,还请关注!😊