在进这家公司之前产品提过一个需求需要实现竖直方向跟水平方向的滑动翻页功能,但竖直方向又限定只能上拉翻到下一页。其实这个功能之前的哥们已经实现了,但考虑到他的实现方式不够优雅最重要的是不利于后期版本迭代,所以重新实现了这个功能。
之前的哥们是通过 RecyclerView 嵌套 ViewPager 的方式处理,但 RecyclerView 中的 item 从第二个开始显示空白,具体原因不得而知,估计跟复用有关,有知道的朋友可以提醒。
我这里用的是 VerticalViewPager + RecyclerView 嵌套的方式,更便于更新重新绘制UI。因为外部的 ViewPager 跟 RecyclerView 都是充满屏幕实现翻页效果的。代码不多,关键是要理解思想,并对事件传递有清晰的判断并进行拆分。
好了,下面进入正题,让我们来借助 gif 图来捋清楚需求 竖直方向从下往上滑动翻页 + 水平翻页
实现如下:
禁用 VerticalViewPager 从上往下翻到上一页事件
1)其中竖直方向的 ViewPager 可用 VerticalViewPager ,从下往上翻页即使 VerticalViewPager 从上往下滑动的手势失失效,可以通过重写 dispatchTouchEvent 方法控制 ,如下:
private int xDispatchLast;
private int yDispatchLast;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "xLastDown:" + xInterceptLast);
Log.d(TAG, "yLastDown:" + yInterceptLast);
xDispatchLast = (int) ev.getX();
yDispatchLast = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
final int curX = (int) ev.getX();
final int curY = (int) ev.getY();
Log.d(TAG, "curXMove:" + curX);
Log.d(TAG, "curYMove:" + curY);
int xDiff = curX - xDispatchLast;
int yDiff = curY - yDispatchLast;
int xAbsDiff = Math.abs(xDiff);
int yAbsDiff = Math.abs(yDiff);
Log.d(TAG, "xDiffMove:" + xDiff);
Log.d(TAG, "yDiffMove:" + yDiff);
if (yAbsDiff > xAbsDiff && yDiff > 0) {//竖直方向手指从上往下滑动,即 ViewPager 向前一页滑动时,不进行事件处理即不可向下滑动
Log.d(TAG, "dispatchTouchEvent");
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
xDispatchLast = (int) ev.getX();
yDispatchLast = (int) ev.getY();
Log.d(TAG, "xLastUp:" + xDispatchLast);
Log.d(TAG, "yLastUp:" + yDispatchLast);
break;
}
return super.dispatchTouchEvent(ev);
}
2) VerticalViewPager+ RecyclerView 会产生事件冲突,此时只需将水平方向滑动的事件交给 RecyclerView即 RecyclerView 内部在 dispatchTouchEvent() 方法内通过 getParent().requestDisallowInterceptTouchEvent(true) 控制父控件拦截父控件的事件交给自己处理;竖直方向滑动的事件交给 VerticalViewPager,即在其 onInterceptTouchEvent() 方法直接 return true 交给自己处理。
VerticalViewPager 拦截竖直方向事件给子 View,自己处理:
private int xInterceptLast;
private int yInterceptLast;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "xLastDown:" + xInterceptLast);
Log.d(TAG, "yLastDown:" + yInterceptLast);
xInterceptLast = (int) ev.getX();
yInterceptLast = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
final int curX = (int) ev.getX();
final int curY = (int) ev.getY();
Log.d(TAG, "curXMove:" + curX);
Log.d(TAG, "curYMove:" + curY);
int xDiff = curX - xInterceptLast;
int yDiff = curY - yInterceptLast;
int xAbsDiff = Math.abs(xDiff);
int yAbsDiff = Math.abs(yDiff);
Log.d(TAG, "xDiffMove:" + xDiff);
Log.d(TAG, "yDiffMove:" + yDiff);
if (yAbsDiff > xAbsDiff && yDiff <= 0) {//竖直方向手指从下往上滑动,即 ViewPager 向下一页滑动时,事件由 ViewPager 自身处理
Log.d(TAG, "onInterceptTouchEvent");
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
xInterceptLast = (int) ev.getX();
yInterceptLast = (int) ev.getY();
Log.d(TAG, "xLastUp:" + xInterceptLast);
Log.d(TAG, "yLastUp:" + yInterceptLast);
break;
}
boolean intercepted = super.onInterceptTouchEvent(swapXY(ev));
swapXY(ev); // return touch coordinates to original reference frame for any child views
return intercepted;
}
RecyclerView 使父控件水平方向的事件不处理交由子控件处理
3)这个代码我没有选择放在 RecyclerView 中处理,而放在其包含它的根布局中,放哪个地方可根据需求来定,如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "xLastDown:" + xLast);
Log.d(TAG, "yLastDown:" + yLast);
xLast = (int) ev.getX();
yLast = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
final int curX = (int) ev.getX();
final int curY = (int) ev.getY();
Log.d(TAG, "curXMove:" + curX);
Log.d(TAG, "curYMove:" + curY);
int xDiff = curX - xLast;
int yDiff = curY - yLast;
int xAbsDiff = Math.abs(xDiff);
int yAbsDiff = Math.abs(yDiff);
Log.d(TAG, "xDiffMove:" + xDiff);
Log.d(TAG, "yDiffMove:" + yDiff);
if (yAbsDiff < xAbsDiff || (yAbsDiff > xAbsDiff && yDiff > 0)) {//水平滑动时拦截父控件的事件
Log.d(TAG, "requestDisallowInterceptTouchEvent");
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
getParent().requestDisallowInterceptTouchEvent(false);
xLast = (int) ev.getX();
yLast = (int) ev.getY();
Log.d(TAG, "xLastUp:" + xLast);
Log.d(TAG, "yLastUp:" + yLast);
break;
}
return super.dispatchTouchEvent(ev);
}
以上就能实现所想要的效果,看起来很简单吧。最后对事件分析做个总结:
1.内部 RecyclerView 中在dispatchTouchEvent() 获取水平滑动事件。
2.外部 VerticalViewPager 中在 onInterceptTouchEvent() 获取竖直方向滑动事件。
3.外部 VerticalViewPager 中在 dispatchTouchEvent() 不处理下拉翻至上一页手势事件。
解决事件冲突的关键在于将事件分发进行分析,分析清楚在什么时候哪个控件处理什么手势为首要前提。当然也必须对事件分发有深入的了解。
VerticalViewPager 源代码来源于 stackoverflow
代码已上传至github: NestViewPager