项目地址:TwinklingRefreshLayout 本文分析版本:0a9b613
1.简介
以下是 TwinklingRefreshLayout 的官方介绍:
TwinklingRefreshLayout 延伸了 Google 的 SwipeRefreshLayout 的思想,不在列表控件上动刀,而是使用一个 ViewGroup 来包含列表控件,以保持其较低的耦合性和较高的通用性。其主要特性有:
- 支持 RecyclerView、ScrollView、AbsListView 系列(ListView、GridView)、WebView 以及其它可以获取到 scrollY 的控件
- 支持加载更多
- 默认支持 越界回弹,随手势速度有不同的效果
- 可开启没有刷新控件的纯净越界回弹模式
- setOnRefreshListener 中拥有大量可以回调的方法
- 将 Header 和 Footer 抽象成了接口,并回调了滑动过程中的系数,方便实现个性化的 Header 和 Footer
2.使用方法
下面以 scrollView 为例子对下拉刷新和上拉加载来做分析
1、在 XML 文件中声明
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/inc_toolbar" />
<com.lcodecore.tkrefreshlayout.TwinklingRefreshLayout
android:id="@+id/refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#afbfff"
android:overScrollMode="never">
.
.
.
</ScrollView>
</com.lcodecore.tkrefreshlayout.TwinklingRefreshLayout>
</LinearLayout>
在 XML 文件中只要在滑动控件 ScrollView 外嵌套 TwinklingRefreshLayout 就可以了,TwinklingRefreshLayout 包含很多自定义的属性在下面的分析中我们会看到。
2、在 JAVA 文件中设置回调
refreshLayout.setOnRefreshListener(new RefreshListenerAdapter() {
@Override
public void onRefresh(final TwinklingRefreshLayout refreshLayout) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
refreshLayout.finishRefreshing();
}
}, 4000);
}
});
TwinklingRefreshLayout 项目中给出了一个控件状态的回调监听,上面的代码中只监听了 onRefresh 的状态,我们来看下 RefreshListenerAdapter 的实现,看看有多少状态。
public abstract class RefreshListenerAdapter implements PullListener {
@Override
public void onPullingDown(TwinklingRefreshLayout refreshLayout, float fraction) {
}
@Override
public void onPullingUp(TwinklingRefreshLayout refreshLayout, float fraction) {
}
@Override
public void onPullDownReleasing(TwinklingRefreshLayout refreshLayout, float fraction) {
}
@Override
public void onPullUpReleasing(TwinklingRefreshLayout refreshLayout, float fraction) {
}
@Override
public void onRefresh(TwinklingRefreshLayout refreshLayout) {
}
@Override
public void onLoadMore(TwinklingRefreshLayout refreshLayout) {
}
@Override
public void onFinishRefresh() {
}
@Override
public void onFinishLoadMore() {
}
}
RefreshListenerAdapter 是一个抽象类,实现了 PullListener 接口:
public interface PullListener {
/**
* 下拉中
*/
void onPullingDown(TwinklingRefreshLayout refreshLayout, float fraction);
/**
* 上拉
*/
void onPullingUp(TwinklingRefreshLayout refreshLayout, float fraction);
/**
* 下拉松开
*/
void onPullDownReleasing(TwinklingRefreshLayout refreshLayout, float fraction);
/**
* 上拉松开
*/
void onPullUpReleasing(TwinklingRefreshLayout refreshLayout, float fraction);
/**
* 刷新中。。。
*/
void onRefresh(TwinklingRefreshLayout refreshLayout);
/**
* 加载更多中
*/
void onLoadMore(TwinklingRefreshLayout refreshLayout);
/**
* 手动调用finishRefresh或者finishLoadmore之后的回调
*/
void onFinishRefresh();
void onFinishLoadMore();
}
RefreshListenerAdapter 实现了 PullListener 接口变成一个抽象类,这让使用者更加的灵活,只关注使用到的状态回调,而不需要所有的状态回调,抽象类可以做到这一点,只覆写需要的函数,而接口不一样,继承了接口就必须覆写接口内部所有函数。这一点很值得学习!
3、核心类 TwinklingRefreshLayout 的分析:
首先看一张官方的类解析图:
通过图中可以看到 TwinklingRefreshLayout 包含了一个 HeaderView 和一个 BottomView ,这两个 View 分别在上拉和下拉的时候显示出来。其中内部还包含了一个名字叫 CoProcessor 的类, CoProcessor 又包含三个 Processor ,我们来看下 CoProcessor 的构造函数就明白了:
public class CoProcessor {
private RefreshProcessor refreshProcessor;
private OverScrollProcessor overScrollProcessor;
private AnimProcessor animProcessor;
public CoProcessor() {
animProcessor = new AnimProcessor(this);
overScrollProcessor = new OverScrollProcessor(this);
refreshProcessor = new RefreshProcessor(this);
}
......
}
CoProcessor
相当于是一个总的调度器,把任务分配给三个不同的Processor
来处理,从名字上来看AnimProcessor
处理动画,OverScrollProcessor
处理滑动中的越界回弹,RefreshProcessor
处理刷新动作。 CoProcessor
的初始化工作是在TwinklingRefreshLayout
的构造函数中完成。接下来看一下CoProcessor
如何调度的,首先从拦截事件开始看:
/**
* 拦截事件
*
*
@return return true时,ViewGroup的事件有效,执行onTouchEvent事件
* return false时,事件向下传递,onTouchEvent无效 */
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = cp.interceptTouchEvent(ev);
return intercept || super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent e) {
boolean resume = cp.consumeTouchEvent(e);
return resume || super.onTouchEvent(e);
}
cp.interceptTouchEvent(ev)
调用了refreshProcessor.interceptTouchEvent(ev)
,来看下interceptTouchEvent
做了些什么事:
public boolean interceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mTouchX = ev.getX();
mTouchY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
float dx = ev.getX() - mTouchX;
float dy = ev.getY() - mTouchY;
if (Math.abs(dx) <= Math.abs(dy)) {//滑动允许最大角度为45度
if (dy > 0 && !ScrollingUtil.canChildScrollUp(cp.getScrollableView()) && cp.allowPullDown()) {
cp.setStatePTD();
return true;
} else if (dy < 0 && !ScrollingUtil.canChildScrollDown(cp.getScrollableView()) && cp.allowPullUp()) {
cp.setStatePBU();
return true;
}
}
break;
}
return false;
}
ACTION_DOWN
产生时不拦截事件只记录坐标,当ACTION_MOVE
产生的时候,计算移动距离dy
、ScrollingUtil.canChildScrollUp(cp.getScrollableView())
用来判断子 View 是否还可以下拉,如果条件都满足,就调用cp.setStatePTD()
改变状态,并且返回true
表示拦截事件,接下去的事件就不下发给子View,直接交给onTouchEvent
来处理:
public boolean consumeTouchEvent(MotionEvent e) {
if (cp.isRefreshVisible() || cp.isLoadingVisible()) return false;
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float dy = e.getY() - mTouchY;
if (cp.isStatePTD()) {
dy = Math.min(cp.getMaxHeadHeight() * 2, dy);
dy = Math.max(0, dy);
cp.getAnimProcessor().scrollHeadByMove(dy);
} else if (cp.isStatePBU()) {
//加载更多的动作
dy = Math.min(cp.getBottomHeight() * 2, Math.abs(dy));
dy = Math.max(0, dy);
cp.getAnimProcessor().scrollBottomByMove(dy);
}
return true;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (cp.isStatePTD()) {
cp.getAnimProcessor().dealPullDownRelease();
} else if (cp.isStatePBU()) {
cp.getAnimProcessor().dealPullUpRelease();
}
return true;
}
return false;
}
根据ACTION_MOVE
中的 if 条件计算出 dy 再通过 cp 来调试 AnimProcessor 做相应的动画,我们来看下scrollHeadByMove
函数是如何实现头部的移动的:
public void scrollHeadByMove(float moveY) {
float offsetY = decelerateInterpolator.getInterpolation(moveY / cp.getMaxHeadHeight() / 2) * moveY / 2;
if (cp.getHeader().getVisibility() != VISIBLE) cp.getHeader().setVisibility(VISIBLE);
if (cp.isPureScrollModeOn()) cp.getHeader().setVisibility(GONE);
cp.getHeader().getLayoutParams().height = (int) Math.abs(offsetY);
cp.getHeader().requestLayout();
if (!cp.isOpenFloatRefresh()) {
cp.getContent().setTranslationY(offsetY);
translateExHead((int) offsetY);
}
cp.onPullingDown(offsetY);
}
根据cp.getAnimProcessor().scrollHeadByMove(dy)
之前的计算可以看出dy
的最大值是cp.getMaxHeadHeight()
的两倍,所以scrollHeadByMove
函数中offsetY
的值最大应该等于cp.getMaxHeadHeight()
,这就是为什么你再怎么拖动ScrollView
,上面的HeadView
最多也就显示这么点。接下去两个if
判断是否显示HeaderView
。
//如果没有设置悬浮,那么需要手动对 ScrollView 做移动操作
//因为 ACTION_MOVE 已经被 TwinklingRefreshLayout 拦截,ScrollView 无法响应 ACTION_MOVE。
if (!cp.isOpenFloatRefresh()) {
cp.getContent().setTranslationY(offsetY);
translateExHead((int) offsetY);
}
继续回到consumeTouchEvent
函数中,ACTION_MOVE
动作完了以后,产生ACTION_UP
调用cp.getAnimProcessor().dealPullDownRelease()
:
public void dealPullDownRelease() {
if (!cp.isPureScrollModeOn() && getVisibleHeadHeight() >= cp.getHeadHeight() - cp.getTouchSlop()) {
//下拉出的高度大于Head的高度就做刷新操作
animHeadToRefresh();
} else {
//下拉的距离不够,回弹动画
animHeadBack();
}
}
执行animHeadToRefresh
函数时对于HeadView
做了,回到正常高度动画的操作。动画完成后会调用pullListener.onRefresh(TwinklingRefreshLayout.this);
这就是我们在自己的 Activity 可以捕捉到的回调。
animHeadBack
就是一个收起头部的动画。
至此,一次下拉刷新的操作算是完成了。上拉加载更多,原理是一样的就是方向变化了,有兴趣的可以看下实现,这边就不做重复的分析了。
目前还没提到的就是OverScrollProcessor
这个处理器了,这个处理器是用来做越界回弹的,代码比较少,有兴趣的同学可以自行分析。
4、总结
整个项目,代码逻辑很清晰,亮点在于把整个 ViewGroup 的操作,按三个处理器分拆了,扩展性很好,可以很容易的去理解整个流程,这种分拆方式很值得学习!