背景
下拉刷新是App交互中非常常见的场景,而与其对应的上拉加载,在很多场景中也已经是用户意识中理所当然的一种交互了。
在很久之前的项目开发中,就已经有上拉加载的这个需求。但是那时苦于没有找到一个合适的上拉加载的库,而项目迭代又紧,那时自己实现恐时间上来不及或者引入其他bug,就暂时用了秋百万的cube-sdk中的点击加载。
在今年该项目的又一次迭代开发中,由于使用到了RecyclerView,而对应的RecyclerView.Adapter又无法使用cube-sdk中的adapter,因此用不了其点击加载,考虑到自己这两年所积累的相关知识及对上拉加载的思考应已足够,就花了些时间,实现了一个相对简单的上拉加载布局。
思考
我对上拉加载的思考起始于两年前读过的秋百万的一篇文章《我眼中的下拉刷新》。但是上拉加载与下拉刷新的差异,不止是拉的方向不同,它们所拉出来的Header或Footer在加载完成后的消失方式也会不同,这就导致了在实现层面上会有些区别。
先说下拉刷新,通常是先让一个HeaderView位于ContentView外部而不显示出来,然后在下拉的时候让它与ContentView(或只有HeaderView)跟着移动下来,然后到一定距离触发刷新,HeaderView回滚到顶部停留,等刷新完成再慢慢滑动出去。
而上拉加载,通常的场景是用于AbsListView或RecyclerView。它与下拉刷新的最大不同是,所加载出来的内容会插入到当前所显示的AbsListView或ReyclcerView中,并显示在原来最后显示的内容与FooterView之间。
以RecyclerView举例,当我们在上拉加载更多的布局里放一个RecyclerView与一个FooterView,并把FooterView设置在布局底部范围之后,然后让它随着RecyclerView一起上拉,并显示出来,这点并没有问题。这时的界面如下图:
这时我们思考一个问题:当数据加载完成,更新到RecyclerView中时,界面应该如何处理?
通常而言,这时候应该是新加载的数据从FooterView的位置开始显示,而FooterView消失。但我们让FooterView消失(移出显示范围之外),而让RecyclerView移回来,所加载的新内容就会在屏幕外面,需要用户再去手动滑动上来才能看到。这种体验就很不好了。
因此我个人觉得,这个FooterView不应该由我们的上拉加载的布局去控制,而是交由具体场景去实现,在上拉加载的布局当中,应只做ContentView的位移,以及相关的界面及功能接口的回调。而除此外我们需要做的,是提供一些接口,来实现上拉UI需求上的灵活性及可定制化。
基本接口
为了让UI上有更大的灵活性,我们需要对上拉加载的UI变化进行一些解耦。参考秋百万的下拉刷新的库,又考虑到目前实现比较简单的上拉加载,所以我先定义了以下两个接口:
一是上拉加载的UI回调接口,它应该至少有三个状态变化的回调:可以上拉,已经触发加载回调,上拉完成。除此之外,为配合实现一些更好的提示或动画,它至少需要提供两个值:能够触发加载的位移量,以及当前的位移量。当然,多一些其他参数,比如当前的位移方向、速度等的话,可以实现更多的效果,不过这里只是先完成基本功能,所以实现上就先简单点。根据所需要的这些回调,LoadMoreUIHandler接口定义如下:
1 | /* |
第二个接口是触发加载的回调接口,只有一个方法,如下:
1 | /* |
具体实现
我们首先来实现上拉。注意,由于API 14已能适配目前市场上所有Android设备,所以这里像判断是否可以上下拉动或对View进行位移操作,会直接使用到一些API 14以上才有的接口。
首先布局直接继承自FrameLayout。其次,上拉过程需要知道当前的状态,能触发拉动的位移量,当前位移量,是否可以上拉等,所以定义变量,构造方法及一些基本的getter和setter方法如下:
1 | public class LoadMoreLayout extends FrameLayout { |
接下来,我们需要找到我们的ContentView,这里提供两种方式:一是获取布局里的第一个子View,二是提供一个设置ContentView的方法:
1 | public void setContentView(View view) { |
再接下来重写onLayout方法,确保在整个过程当中不会因layout操作导致内容位移位置不正确。
1 |
|
然后就是对手指的事件处理了,这也是完成上拉加载的关键之一。
首先是事件拦截,我们要先判断是否可以进行上拉或由LoadMoreLayout下拉,如果可以,则拦截事件,不让事件再往下传递,所以这里重写onInterceptTouchEvent(MotionEvent ev)
方法:
1 |
|
然后重写onTouchEvent(MotionEvent ev)
方法,进行上拉加载的逻辑,以及移动ContentView的位置。
1 |
|
movePos(int)
实现对ContentView的位移,如下:
1 | private void movePos(int offsetY) { |
onRelease()
是手放开后判断是否触发加载,以及让ContentView归位的操作:
1 | private void onRelease() { |
以上完成了上拉时对ContentView的位移,以及回调加载方法。但这只是完成了从最初的状态到开始的状态,我们还需要知道加载完成,这样才能让状态重置,以及知道是否还可以继续加载。所以还需要有如下方法:
1 | public void loadMoreComplete(boolean hasMore) { |
除此之外,我们还增加一个方法,用于外界触发它开始加载,可用于自动加载的实现。
1 | public void triggerToLoadMore() { |
到这里,我们已经完成了从初始状态到上拉到加载到完成的整个过程。但是如果你够细心会发现,目前为止并没有提到如何让ContentView回去,并且上面的代码中有两处TODO的标记。因此如果一直上拉,最终是会把ContentView给拉出外面的。所以,我们接下来还要实现让ContentView回来的代码。
我们知道,让一个View产生位移有多种方式,比如设置它的margin,设置父布局的padding,调用它的layout方法,或者是如上面我们的实现中使用setTranslationY(float)
方法。而让View滑动回去,由于此过程当中并不需要跟着手指来移动,所以也会有几种选择。
首先,既然前面我们是使用setTranslationY(float)
来设置它的位置,那么最终肯定也是需要调用这个方法来恢复原位的。而在中间的过程当中,可供选择的处理方式至少有:
- 先调用该方法直接设置回去,然后播放一个位移动画。简单粗暴。
- 使用Scroller计算每次的位移量,然后调用这个ContentView的
setTranslationY(float)
方法设置它的位置让它慢慢回去。
由于第二种方式它所处的位置与我们所记录的位移量是对应上的,并且在回滚过程当中当我们的手指按下去,是可以让它停住的,相对而言更为真实,所以这里选用第二种方式。
参考了秋百万的下拉刷新的库,这里定义了一个内部类,代码如下:
1 | class ScrollChecker implements Runnable { |
它的代码很简单,首先有一个Scroller,用于计算位移量。然后当触发回滚时,我们每12毫秒就执行我们的这个Runnable的回调,获取当前Scroller的结果,设置到位移中去。并且它还提供了一个方法abortIfRunning()
,用于在回滚过程中当手指继续操作我们的LoadMoreLayout时让ContentView暂停下来。
最后,我们修改一下前面的代码,实现ContentView的归位。
1 |
|
最终成果
完整代码已经上传到Github,项目地址为:https://github.com/msdx/hi-loadmore
项目运行效果如下:
后续扩展
我在前面提到,上拉加载的Footer可能不适合在LoadMoreLayout里实现,所以在我的实现当中也是不包含这一方面的代码的。一般可以实现LoadMoreUILayout接口,来自定义自己的FooterView。而对于像ListView或RecyclerView,个人倾向于使用ListView的FooterView或在RecyclerView的Adapter中添加FooterView来实现。后续会更新Github上的项目,补充对LoadMoreLayout的扩展以实现RecyclerView的上拉加载。但是否会再写一篇,视补充的内容多少而定,若可写内容较少或简单,则只更新项目。有相关疑问或建议请移步github该项目上提issue。
参考资料
- 《我眼中的下拉刷新》
- liaohuqiu/android-Ultra-Pull-To-Refresh
- nukc/LoadMoreLayout