这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战
前言
在Android
开发中时常也会遇到多个View视图嵌套实现后发现页面点击滑动事件失效的问题。例如使用了ScrollView
和ViewPager
相互嵌套导致滑动ScrollView
时发现无法滑动。这是因为两个滑动视图嵌套导致的,两个视图都支持触摸滑动事件,但也因为两者都能做滑动操作使得滑动操作不知道具体被哪个视图所消费。解决办法当然是有的,通过重写触摸事件根据判断不同操作动作来区分哪些事件是该视图需要消费,哪些又是其他视图需要消费就行。
通常情况下滑动冲突情况分以下几种:
- 外部视图的滑动方向和内部视图滑动方向一致。
- 外部视图的滑动方向和内部视图滑动方向不一致。
- 以上两种嵌套情况都存在。
举个例子说明
例如ViewPager
+Fragment
是常用分页布局,在Fragment
内部是ListView
布局,但这种情况下不会发生滑动冲突。这是因为ViewPager
内部是帮助开发者已经处理过这个问题了。如前言所说若换成ScrollView
+Fragment
就会遇到滑动冲突问题了,两层视图只有一层可以滑动,那是因为对于ScrollView
并没有内部处理过需要手动处理滑动冲突了。
冲突问题解决办法
解决滑动冲突问题核心开发工作就是去自定义实现事件分发。做好处理事件分发工作就能在某个View视图下去中断拦截分发事件并消费,避免事件继续传递下去导致多个视图消费同一事件,从而导致滑动操作不能执行。
- 父级拦截
通过父视图的onInterceptTouchEvent
方法做内部拦截操作。通过父级视图拦截触摸事件,从而使事件不继续往下分发子视图没有消费事件。
/// 伪代码
public boolean onTouch(event){
boolean intercepted = false;
switch(event){
case event.down:
intercepted = false
break;
case event.move: // 在触摸移动中拦截事件即可
intercepted = true
break;
case event.up:
intercepted = false
break;
}
return intercepted;
}
复制代码
- 子级拦截
通过父级拦截是一种办法。但事件分发是由外向内的,这就导致如果希望优先由子视图判定是否消费事件时就没有办法了。在事件分发中还有requestDisallowInterceptTouchEvent
方法可以让子视图来控制父视图事件分发。
///伪代码
public boolean onTouch(event){
boolean intercepted = false;
switch(event){
case event.down: // 控制父视图事件向下分发
parent.requestDisallowInterceptTouchEvent(true);
break;
case event.move: // 控制父视图事件不向下分发
parent.requestDisallowInterceptTouchEvent(false);
break;
case event.up:
break;
}
return super.onTouch(event);
}
复制代码
以上两种都是解决滑动冲突的方式,但具体问题具体分析如何做拦截在哪拦截事件需要根据实际业务场景而定。
实战演练
场景一
例如以下场景SwipeRefreshLayout
+ViewPager
+SwipeRefreshLayout
时再左右切换视图时很容易触发SwipeRefreshLayout
的下拉刷新操作。这时候就需要自定义一个SwipeRefreshLayout
来判定手势滑动是横向滑动还是纵向滑动了。
public class TouchSwipeRefreshLayout extends SwipeRefreshLayout{
private float startX;
private float startY;
private float mTouchSlop;
public MySwipeRefreshLayout(Context context) {
super(context);
// 获取最小的滑动距离
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
startX = ev.getX();
startY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
float distanceX = Math.abs(ev.getX() - startX);
float distanceY = Math.abs(ev.getY() - startY);
if(distanceX > mTouchSlop && distanceX > distanceY){ //当X滑动距离大于最小滑动距离且X滑动距离大于Y滑动距离,则判断为横向滑动
return false;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
}
复制代码
场景二
例如以下场景ViewPager
+ViewPager
,当内部ViewPager
滑动到最后时再去滑动最外层的ViewPager
/// 自定义内部ViewPager
public class InerViewPager extends ViewPager{
public InerViewPager(Context context) {
super(context);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN: // 要求父级视图不拦截事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
// 获取分页数
int itemCount = getAdapter().getCount();
// 判断当前是首页还是末页。若是首页或者末页则要求父级视图拦截事件 不继续分发事件;若相反则要求父级视图不拦截事件
if(getCurrentItem() == 0 || getCurrentItem() == itemCount - 1){
getParent().requestDisallowInterceptTouchEvent(false);
}else{
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
}
return super.onInterceptTouchEvent(ev);
}
复制代码