网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
downY = ev.getY();
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("touch_event_test", "ScrollViewParent onInterceptTouchEvent. deltaY:" + (ev.getY() - downY));
if (ev.getAction() == MotionEvent.ACTION_MOVE) {
return Math.abs(ev.getY() - downY) > 15;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.i("touch_event_test", "ScrollViewParent onTouchEvent");
return super.onTouchEvent(ev);
}
}
代码32-33行实现了MOVE事件的拦截逻辑。然后观察日志:
红框内的日志,可以看到此时ScrollViewParent会拦截MOVE事件,直接分发给自己的onTouchEvent,后续的MOVE事件也会直接到onTouchEvent事件去。HorizontalScrollViewChild将收不到MOVE事件。实现效果上也符合我们的预期,当我们斜方向滑动时,滑动的大概率(除非dy足够小)都是外部容器ScrollViewParent了。
当然,这其实是我们假定的一个处理冲突的逻辑,真实的产品逻辑需要根据业务情况去调整。比如当竖直滑动速度超过xx时,滑动外部容器;或者当HorizontalScrollViewChild内部某个View可见时,滑动外部容器,都有可能,但万变不离其宗,最终都是通过改变事件分发的路径去实现。
### 内部拦截法
下面再讲“内部拦截法”怎么处理滑动冲突。“内部拦截法”跟“外部拦截法”相反,是从内部容器出发去解决冲突。这依赖于ViewParent#requestDisallowInterceptTouchEvent(),看其源码的注释我们很容器知道它是什么意思:
Called when a child does not want this parent and its ancestors to intercept touch events with ViewGroup.onInterceptTouchEvent(MotionEvent).
This parent should pass this call onto its parents. This parent must obey this request for the duration of the touch (that is, only clear the flag after this parent has received an up or a cancel.
Params:
disallowIntercept – True if the child does not want the parent to intercept touch events.
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);
当子类不想让其父类/祖先ViewGroup.onInterceptTouchEvent(MotionEvent)方法时,可以调用requestDisallowInterceptTouchEvent(),从而保证父类/祖先无法通过ViewGroup.onInterceptTouchEvent(MotionEvent)方法对事件进行拦截。且一旦设置了这个flag,那么这次事件序列中后续的所有事件,都不会经过父类/祖先的onInterceptTouchEvent(MotionEvent)方法了。直到下次触摸发生,才会清楚掉这个flag。
我们可以直接看源码,看看这是怎么起作用的。requestDisallowInterceptTouchEvent实现:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
做的事情就是更改标记位,并递归访问父节点执行该方法。在View的dispatchTouchEvent方法中,对该标记位进行了判断:
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
核心代码是4-9行,当disallowIntercept结果为true时,就不会走onInterceptTouchEvent()方法了。
现在我们仍是以上面的ScrollViewParent和HorizontalScrollViewChild举例去解决滑动冲突。“内部”要求我们以内部容器的视角去考虑冲突,那么就假定当在水平方向滑动超过15像素时,滑动内部容器HorizontalScrollViewChild。这时候就需要这样编码:
public class HorizontalScrollViewChild extends HorizontalScrollView {
public HorizontalScrollViewChild(Context context) {
super(context);
}
public HorizontalScrollViewChild(Context context, AttributeSet attrs) {
super(context, attrs);
}
public HorizontalScrollViewChild(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private float downX;
private float downY;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i("touch_event_test", "HorizontalScrollViewChild dispatchTouchEvent. deltaX:" + Math.abs(ev.getX() - downX));
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = ev.getX();
downY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
if (Math.abs(ev.getX() - downX) > 15) {
getParent().requestDisallowInterceptTouchEvent(true);
} else {
getParent().requestDisallowInterceptTouchEvent(false);
}
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("touch_event_test", "HorizontalScrollViewChild onInterceptTouchEvent");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.i("touch_event_test", "HorizontalScrollViewChild onTouchEvent");
return super.onTouchEvent(ev);
}
}
在27行,调用了requestDisallowInterceptTouchEvent()方法。上面有说到,一旦标记位被设置为true,后续事件序列中的所有事件都不会调用了父类的onInterceptTouchEvent了。因此我们需要视情况将限制放开,如30行的写法就将flag重新设为false。另外还要注意,因为子View是DOWN事件的消费点,那么MOVE事件是不会经过子View的onInterceptTouchEvent方法的,所以在dispatchTouchEvent或者onTouchEvent中都可以设置标记位,唯独不能放到onInterceptTouchEvent方法;
现在我们斜方向滑动内部容器HorizontalScrollViewChild,观察日志:
可以看到红框区域,当dx大于15后,后续的MOVE事件就不会经过ScrollViewParent的onInterceptTouchEvent了,我们要的效果也就达到了。
## 小结
虽然上面只举了不同方向滑动冲突的例子,但不同方向冲突与同方向冲突,其最终处理的思路都是一样的,要么改变事件分发的路径,要么设置FLAG\_DISALLOW\_INTERCEPT标记位。它们在解决上的不同之处,就只在于“条件判断” 。
对于不同方向的冲突,我们给的条件判断可以是:当DX>X时,水平滑动、当DY>Y时,竖直滑动。
对于同方向的冲突,我们给的条件判断可以是:当滑动速度大于X时,滑动外部View,反之滑动内部View;当内部View已经滑到底了,才滑动外部View;当内部View中的某个子View不在屏幕中时,滑动外部View。
一般这些条件判断也都是基于各自的业务出发去进行选择,但只要我们知道通用的处理思路,问题就都可以迎刃而解了。至此,对滑动冲突的通用处理方法(“内部拦截法”与“外部拦截法”)就讲完了。下面进行个小结:
“外部拦截法”所使用的原理是运用事件分发机制,去改变事件分发的路径,拦截内部容器的事件。
“内部拦截法”使用的是requestDisallowInterceptTouchEvent()方法设置FLAG,不让父容器/祖先容器用onInterceptTouchEvent拦截方法。
使用“内部拦截法”还是“外部拦截法”,首先需要去看实际业务需要我们怎么做,是从“内部”实现比较方便,还是从“外部”实现比较方便。
相较于“外部拦截法”,“内部拦截法”并没有减少事件分发的层级,因此看起来可能会更加复杂一些。并且也需要注意requestDisallowInterceptTouchEvent方法具体在哪个方法中使用。若两个方法都能实现最终的效果,建议优先使用“外部拦截法”。
**深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**



**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.youkuaiyun.com/topics/618636735)**
资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.youkuaiyun.com/topics/618636735)**