一、问题原因
昨天,突然一个问题丢在了我的头上,用户反馈说某某界面下拉刷新不好使啊,怎么回事。二话不说直接运行项目,经过测试,发现果然不好使。一看代码提交日期好家伙2020年,百思不得其解,为啥20年的下拉刷新不好使,到现在才反馈。
还是看问题原因吧。
看了下下拉刷新框架是PullToRefreshView(好像没有用过这个框架),经过测试发现下拉手势怎么都不能回调到刷新回调,
只能断点调试了。
有几个方向可以试探下。
- ViewPager拦截我们的触摸事件了,导致触摸事件没有下发到我们的listView中
- 没有正确注册刷新回调接口
- ListView本身没有处理好下拉事件
以上都是几个猜想方向,我们一一验证。
我们就直接注册ListView触摸事件回调,就可以验证上述猜想。经过验证发现,以上猜想都错了。那么问题是出在哪呢。
跟着代码一步一步走下去就能发现原因了。
二、问题分析
经过调试,最终定位到PullToRefreshBase这个类中。在这个类的onInterceptTouchEvent方法中找到了处理手势的逻辑。
public final boolean onInterceptTouchEvent(MotionEvent event) {
if (!isPullToRefreshEnabled()) {
return false;
}
final int action = event.getAction();
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mIsBeingDragged = false;
return false;
}
if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged) {
return true;
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
// If we're refreshing, and the flag is set. Eat all MOVE events
if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
return true;
}
if (isReadyForPull()) {
final float y = event.getY(), x = event.getX();
final float diff, oppositeDiff, absDiff;
// We need to use the correct values, based on scroll
// direction
switch (getPullToRefreshScrollDirection()) {
case HORIZONTAL:
diff = x - mLastMotionX;
oppositeDiff = y - mLastMotionY;
break;
case VERTICAL:
default:
diff = y - mLastMotionY;
oppositeDiff = x - mLastMotionX;
break;
}
absDiff = Math.abs(diff);
if (absDiff > mTouchSlop && (!mFilterTouchEvents || absDiff > Math.abs(oppositeDiff))) {
if (mMode.showHeaderLoadingLayout() && diff >= 1f && isReadyForPullStart()) {
mLastMotionY = y;
mLastMotionX = x;
mIsBeingDragged = true;
if (mMode == Mode.BOTH) {
mCurrentMode = Mode.PULL_FROM_START;
}
} else if (mMode.showFooterLoadingLayout() && diff <= -1f && isReadyForPullEnd()) {
mLastMotionY = y;
mLastMotionX = x;
mIsBeingDragged = true;
if (mMode == Mode.BOTH) {
mCurrentMode = Mode.PULL_FROM_END;
}
}
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
if (isReadyForPull()) {
mLastMotionY = mInitialMotionY = event.getY();
mLastMotionX = mInitialMotionX = event.getX();
mIsBeingDragged = false;
}
break;
}
}
return mIsBeingDragged;
}
到这里就很简单了,给每一个判断都打上断点,一步步走。最后发现mIsBeingDragged这个值为false。这就导致在onTouchEvent中并不会执行我们的刷新逻辑。
switch (event.getAction()) {
//...省略部分代码
case MotionEvent.ACTION_MOVE: {
if (mIsBeingDragged) {
mLastMotionY = event.getY();
mLastMotionX = event.getX();
//处理刷新和加载事件
pullEvent();
return true;
}
break;
}
case MotionEvent.ACTION_DOWN: {
if (isReadyForPull()) {
mLastMotionY = mInitialMotionY = event.getY();
mLastMotionX = mInitialMotionX = event.getX();
return true;
}
break;
}
//...省略部分代码
}
可以看到在move事件中,并不会执行我们刷新逻辑。
到这里就可以明白肯定是在onInterceptTouchEvent中,部分逻辑判断失败了,导致mIsBeingDragged值为false。
重新回到上一个逻辑中,就可以发现isReadyForPullStart()方法返回值为false就无法执行到mIsBeingDragged赋值为true的逻辑。
最终会执行到isFirstItemVisible()中,一起来看看吧。
private boolean isFirstItemVisible() {
final Adapter adapter = mRefreshableView.getAdapter();
if (null == adapter || adapter.isEmpty()) {
if (DEBUG) {
Log.d(LOG_TAG, "isFirstItemVisible. Empty View.");
}
return true;
} else {
/**
* This check should really just be:
* mRefreshableView.getFirstVisiblePosition() == 0, but PtRListView
* internally use a HeaderView which messes the positions up. For
* now we'll just add one to account for it and rely on the inner
* condition which checks getTop().
*/
if (mRefreshableView.getFirstVisiblePosition() <= 1) {
final View firstVisibleChild = mRefreshableView.getChildAt(0);
if (firstVisibleChild != null) {
return firstVisibleChild.getTop() >= mRefreshableView.getTop();
}
}
}
return false;
}
最后就是看这句代码return firstVisibleChild.getTop() >= mRefreshableView.getTop();
在debug下计算这段,就会发现firstVisibleChild.getTop()的值为0,而mRefreshableView.getTop()的值为30,什么情况呢。
再去看源码会发现,mRefreshableView其实就是我们的ListView。那么这里判断的就是第一个item距离上边界的距离和ListView距离上边界的距离。
那么这里我们就要去找找为什么ListView的距离要比item的距离大。
在项目中找到了如下代码,没想到简简单单的一句代码影响这么大。
lp.setMargins(0, ViewUtil.dip2px(mActivity, 10), 0, 0);
mListview.setLayoutParams(lp);
注释这段设置margin的代码,转为在上一个控件设置margin或者在item中设置margin。不能下拉刷新就完美解决了,泰裤辣!!!