哪有那么多为什么,哪有那么多的抱怨,因为不管你多努力,你总是不够努力。
自我的反思:
我们学习了事件分发的原理,经常用来解决一些滑动冲突的问题,但是我能不能实现一个下拉刷新的操作呢?以前ListView横行的时候,我们经常在ListView头部加一个下拉刷新的头部View,这个方法难道不是很不优雅吗?更主要的是,
现在流行的RecyclerView也没有addHeader这个方法呀?列表就是缓存的,就是展示数据的,下拉刷新的header本来就不属于列表数据渲染,是你自己魔改,增加的,对吧。那我们有么有一个优雅的方式实现下拉刷新,实现无侵入支持各种
列表的下拉刷新呢?
问题
我们回忆一下我们的事件分发相关的只是,先抛出几个问题:
-
怎么实现下拉刷新的效果呢?定义一个header放在RecyclerView里面?还是定义一个header“拼接”在RecyclerView的头部呢?
- 很明显,之前反思有说过,滑动列表就是展示列表内容的,而且我们还要打造无侵入式的下拉刷新,我们一定是创建一个header,然后让header和RecyclerView“拼接”起来。那么这部分工作,我们可以包裹在一个ViewGroup,刚开始让这个ViewGroup往上滑动header的高度,实现隐藏的效果,达到下拉刷新的阈值的时候,跟随手势下拉。
-
下拉,很显然是下拉到列表内容区全部漏出来之后,才开始显示出头部,出现loading效果,那么这个手势会有滑动冲突吗?
-
展示列表是上下滑动的,达到下拉刷新的阈值的时候,再往下拉,这个时候,RecyclerView全部展示了,要展示header了。RecyclerView是“滑不动了”,但是还是要列表整齐下拉,直观的感受是,RecyclerView“不能动了”,它要随着“凭借”头部,整体一起往下移动,松手展示loading动画,刷新结束之后,整体复位。考虑另外一种情况,下拉到阈值了,继续下拉,header漏出来了,没有松手,再往上啦,直到header隐藏,那么继续往上,应该是RecyclerView“可以滑动”了,属于正常的RecyclerView自己的操作,RecyclerView可以“滑动了”。
-
-
RecyclerView“滑不动”,应该是不接受事件,“可以滑动”可以接受事件了,负责给RecyclerView分发时间的,指定就是包裹它的ViewGroup,那么事件分发重写哪一个方法,或者重写哪些方法呢?
-
onTouchEvent,这个方法主要是用来处理事件,做一些手势操作,监测左滑右滑,以及滑动距离速度等。当RecyclerView“不能动”,应该就是不接受事件了,处理不了,我只能设置让父View不拦截,不能设置父View拦截。有人说,我不处理不就行了,能到我还要设置RecyclerView的事件分发return false??这个就不是无侵入式了,而且越来越复杂,不可取。
-
onIntercept,这个方法主要用来判断当前View是否拦截此事件,而且这个方法不是一直调用的。考虑一种情况,当RecyclerView在滑动的时候,很显然RecyclerView要接受事件的,当达到阈值的时候,“滑不动”了,这个时候事件不能接受,需要父ViewGroup控制整体滑动,展示header。当手指按下到抬起,这一系列事件,如果RecyclerView处理了,很显然哪怕到达阈值,事件依旧需要RecyclerView处理。但是我们希望到达阈值,不能处理了,需要给父ViewGoup处理,实现下拉刷新。刚开始自己处理,判断RecyclerView有没有到顶就知道有没有到达阈值,没有到顶,就让RecyclerView处理,否自自己处理,这个可以实现。但是呢,当自己处理的时候,有一种case。下拉到阈值,但是呢没有放手展示loading加载中,继续往上拉,直到header隐藏,任然继续,这个时候事件依旧是父ViewGroup处理。为什么呢?因为看过源码知道,当父ViewGroup自己处理事件的时候,onIntercept不糊走进去的,导致父View一致在处理事件,RecyclerView就无法正常滑动了。
-
dispatchTouchEvent,这个方法主要负责事件的分发,是事件分发的入口,每次的事件都会经过它,都会调用。既然这样,RecyclerView“滑不动”,我就控制事件入口不给RecyclerView分发事件,“可以滑动”,我就给RecyclerView分发事件不就行了吗,是的,可取。
-
下拉刷新库PTRFrameLayout
根据生面的分析,很显然,我们只需要分析PTRFrameLayout这个自定义View的dispatchTouchEvent即可。而且我建议大家,对照着我的注释,自己梳理一下代码逻辑,加强学习和理解。
// 这个方法走默认的事件分发逻辑
public boolean dispatchTouchEventSupper(MotionEvent e) {
return super.dispatchTouchEvent(e);
}
// 正常分发cancel事件,让子View忽略事件处理。
private void sendCancelEvent() {
// The ScrollChecker will update position and lead to send cancel event when mLastMoveEvent is null.
// fix #104, #80, #92
if (mLastMoveEvent == null) {
return;
}
MotionEvent last = mLastMoveEvent;
MotionEvent e = MotionEvent.obtain(last.getDownTime(), last.getEventTime() + ViewConfiguration.getLongPressTimeout(),
MotionEvent.ACTION_CANCEL, last.getX(), last.getY(), last.getMetaState());
dispatchTouchEventSupper(e);
}
@Override
public boolean dispatchTouchEvent(MotionEvent e) {
// mHeaderView 是下拉刷新的头部 mContent 表示的内容区(比如RecyclerView)
// 满足一下情况,不拦截任何事件,直接把事件分发下去
if (!isEnabled() || mContent == null || mHeaderView == null || isDisabledForRefresh) {
return dispatchTouchEventSupper(e);
}
int action = e.getAction();
switch (action) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 判断逻辑,看手势阈值以及当前状态,是否展示headerView,执行下拉刷新
mPtrIndicator.onRelease();
// 表明抬起手的时候,
if (mPtrIndicator.hasLeftStartPosition()) {
onRelease(false);
// 表明抬起手指的时候,headerView漏出来了,触发了阈值,那么之间就是自己处理
// 同时把放下去的一些事件(放置产生onClick等)取消掉,发送cancel事件
// 其他情况下,都是走默认的事件分发逻辑,因为这个时候没有达到阈值,属于RecyclerView的正常滑动
if (mPtrIndicator.hasMovedAfterPressedDown()) {
sendCancelEvent();
return true;
}
return dispatchTouchEventSupper(e);
} else {
return dispatchTouchEventSupper(e);
}
case MotionEvent.ACTION_DOWN:
// 重置标志位
mHasSendCancelEvent = false;
// 更新按下的坐标
mPtrIndicator.onPressDown(e.getX(), e.getY());
// 如果当前在滑动,按住(很好理解,比如你一直下拉,拉到很长,松手收上去的时候,你是可以从新触摸屏幕,按住正在收起的控件的)
mScrollChecker.abortIfWorking();
mPreventForHorizontal = false;
// The cancel event will be sent once the position is moved.
// So let the event pass to children.
// fix #93, #102
// 分析一
dispatchTouchEventSupper(e);
return true;
case MotionEvent.ACTION_MOVE:
mLastMoveEvent = e;
// 更新坐标,这几行代码都很好理解
mPtrIndicator.onMove(e.getX(), e.getY());
float offsetX = mPtrIndicator.getOffsetX();
float offsetY = mPtrIndicator.getOffsetY();
// 判断是不是需要禁止水平滑动
if (mDisableWhenHorizontalMove && !mPreventForHorizontal && (Math.abs(offsetX) > mPagingTouchSlop
&& Math.abs(offsetX) > Math.abs(offsetY))) {
if (mPtrIndicator.isInStartPosition()) {
mPreventForHorizontal = true;
}
}
// 如果不禁止水平滑动,那么用户左右滑动的时候,下拉刷新操作是不会走进来的,直接把左右滑动的事件放下去即可
// 根据子View的返回值判断当前事件是不是需要消费
if (mPreventForHorizontal) {
return dispatchTouchEventSupper(e);
}
boolean moveDown = offsetY > 0;
boolean moveUp = !moveDown;
boolean canMoveUp = mPtrIndicator.hasLeftStartPosition();
// 没有达到下拉刷新的阈值,需要把事件放下去,因为这个时候PTRFrameLayout不需要出来,PTRFrameLayout需要处理的就是下拉刷新的时候而已
// 是否处理,就看子View是否处理此事件即可
// disable move when header not reach top
if (moveDown && mPtrHandler != null && !mPtrHandler.checkCanDoRefresh(this, mContent, mHeaderView)) {
return dispatchTouchEventSupper(e);
}
// 如果当前达到了阈值,并且持续下拉(逐渐漏出了headerView),这个更新事件交给PTRFrameLayout处理
// 如果headerView以及全部漏出来了,这个时候往上移动,还可以继续往上(headerView还没有合起来),那么这个事件也是交给PTRFrameLayout处理
if ((moveUp && canMoveUp) || moveDown) {
if (mPtrHandler != null && !isStartDragging) {
mPtrHandler.onStartDragging();
isStartDragging = true;
}
movePos(offsetY);
return true;
} else {
if (mPtrHandler != null && isStartDragging) {
isStartDragging = false;
mPtrHandler.onRefreshComplete();
}
}
default:
return dispatchTouchEventSupper(e);
}
}
分析一:1,为什么要调用默认的时间分发方法?2,为什么要return true?
- 1,首先调用dispatchTouchEventSupper(e),是为了把down事件都放下去,不干扰子View的正常响应。因为很多时间是需要down的,比如longClick,onClick都需要接受到down事件,如果,当前还没有到下拉刷新,我就是点击了RecyclerView的一个item,需要跳转某一个页面,但是你没有把down时间放下去,不就出问题了吗?那就有人问,如果我不想子View处理,你把down放下去了怎么办?那不还是有cancel吗,对不。
- 之前说过,当一个事件序列的down事件被我消费了,那么这个序列都会交给我处理,是的,目的就是这个下拉刷新控件消费事件,接管所有的事件,保证这个事件序列都是我的。又有人问了,dispatchTouchEvent,不是默认每次都会走进来吗?我有必要return true吗?好问题,如果你不return true,那么这个down必须要有人消费,谁来消费?headerView还是contentView?假如给其中的一个消费了,后续事件理应也是它的,这个没问题吧,然后你在move事件的时候拦截,OK,说的通?但是如果他们都不消费,咋办?然后,你说他们不消费我在消费,这不是有毒吗?或者我不消费,我再抛上去,让PTRFrameLayout父亲消费,这个更不行了?你down事件不消费,PTRFrameLayout就不会受到后续事件了,也就没有headerView和contentView啥事了。所以,干脆PTRFrameLayout直接return true,接管所有事件,负责分发不就行了,是的,PTRFrameLayout的核心就是接管所有事件,统一调度,合适的时机,把事件放下去。
总结
- PTRFrameLayout核心就是down事件来的时候,return true接管所有的事件。
- 然后根据有没有达到下拉刷新的阈值,来决定move事件到底交给谁处理,自己处理的话,就给子View发送cancel事件(这个需要注意一下)。
- 抬起手的时候,同样判断有没有触发了下拉刷新,触发了,就给子View发送cancel事件,return true自己处理,让子View不响应,否则就走正常的事件分发逻辑,把事件正常发下去即可。