Android滑动冲突解决方法

滑动冲突

首先讲解一下什么是滑动冲突。当你需要在一个ScrollView中嵌套使用ListView或者RecyclerView的时候你会发现只有ScrollView能够滑动,而ListView或RecyclerView不能滑动,这个就违背了我们写这段代码的意愿。我们想要的结果是当我们滑动ListView的时候ListView滑动,滑动ListView以外的地方的时候ScrollView滑动。这时候滑动冲突就产生了,我们需要想办法解决这个冲突。

你可以在这里看到这个引文的demo:https://github.com/onlynight/SlidingConfict

View Touch事件分发

首先我们了解下Android的控件组织结构。View是显示组件的基类,ViewGroup继承自View是布局的基类。ViewGroup中可包含View和ViewGroup,这样就形成了View树。View的Touch事件总是从View根节点开始向下传递的,根据点击的位置判断该传递给哪个子View,直到子节点再没有子节点这时候,如果这个事件被该View消耗那么事件的传递就此结束,如果该View没有使用这个事件那么这个事件会依次向上传递直到有View消耗了这个事件,如果没有View消耗这个事件,那么该事件就会被传递给Activity处理。以上就是Vieww Touch事件传递的过程。

我们来看View的dispatchEvent方法:

//View.java
/*** Pass the touch screen motion event down to the target view, or this* view if it is the target.** @param event The motion event to be dispatched.* @return True if the event was handled by the view, false otherwise.*/
public boolean dispatchTouchEvent(MotionEvent event) {boolean result = false;if (onFilterTouchEventForSecurity(event)) {//noinspection SimplifiableIfStatementListenerInfo li = mListenerInfo;if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event)) {result = true;}if (!result && onTouchEvent(event)) {result = true;}}return result;
}

从这段代码我们可以看出OnTouchListener的优先级高于onTouchEvent。
下面我们再来看看ViewGroup的dispatchTouchEvent方法:

//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {...boolean handled = false;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;}// Check for cancelation.final boolean canceled = resetCancelNextUpFlag(this)|| actionMasked == MotionEvent.ACTION_CANCEL;//如果没有拦截再分发下去处理if (!canceled && !intercepted) {...if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {...}...}return handled;
}/*** Transforms a motion event into the coordinate space of a particular child view,* filters out irrelevant pointer ids, and overrides its action if necessary.* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {final boolean handled;// Canceling motions is a special case.  We don't need to perform any transformations// or filtering.  The important part is the action, not the contents.final int oldAction = event.getAction();if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {event.setAction(MotionEvent.ACTION_CANCEL);if (child == null) {handled = super.dispatchTouchEvent(event);} else {handled = child.dispatchTouchEvent(event);}event.setAction(oldAction);return handled;}...
}

可以看到ViewGroup在处理事件前有一个touch事件是否被拦截onInterceptTouchEvent的判断,如果被拦截则不再向下一级分发;如果没有拦截则向下分发,处理方式会根据ViewGroup中是否包含子元素来判断,如果包含子元素则将事件交由子元素处理touch事件handled = child.dispatchTouchEvent(event);,如果不包含子元素则由自身处理handled = child.dispatchTouchEvent(event);处理流程和View相同。

实线箭头为touch事件正向传递,虚线为向上传递touch事件。

通过上面的分发的逻辑我们可以知道父控件有能力把事件不传递给子View,从而不让子控件接收Touch事件,那么子控件有没有能力让父控件失去响应Touch事件的能力呢,下面我们来看看具体的源码,看源码的顺序是由下而上的,这回我们反其道而行,我们知道事件的入口然后依次向下找。

Activity分发事件到ViewGroup

根据上面的图我们知道View的touch事件是由Activity传递过来的,那么我们先看看Activity有没有类似的方法,正如我们所料,Activity的dispatchTouchEvent函数如下:

/*** Called to process touch screen events.  You can override this to* intercept all touch screen events before they are dispatched to the* window.  Be sure to call this implementation for touch screen events* that should be handled normally.** @param ev The touch screen event.** @return boolean Return true if this event was consumed.*/
public boolean dispatchTouchEvent(MotionEvent ev) {if (ev.getAction() == MotionEvent.ACTION_DOWN) {onUserInteraction();}if (getWindow().superDispatchTouchEvent(ev)) {return true;}return onTouchEvent(ev);
}

显而易见我们要看的是getWindow().superDispatchTouchEvent(ev),我们深入进去看到Window类中的这个方法:

//Window.java
/*** Used by custom windows, such as Dialog, to pass the touch screen event* further down the view hierarchy. Application developers should* not need to implement or call this.**/
public abstract boolean superDispatchTouchEvent(MotionEvent event);

Window类是个抽象类,它的唯一实现类是PhoneWindow,PhoneWindow类中的实现如下:

//PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {return mDecor.superDispatchTouchEvent(event);
}

mDecor是DecorView,我们看看这个DectorView是从哪里来的:

//PhoneWindow.java
private void installDecor() {if (mDecor == null) {mDecor = generateDecor();...}...if (mContentParent == null) {mContentParent = generateLayout(mDecor);}...
}protected DecorView generateDecor() {return new DecorView(getContext(), -1);
}@Override
public void setContentView(int layoutResID) {// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window// decor, when theme attributes and the like are crystalized. Do not check the feature// before this happens.if (mContentParent == null) {installDecor();} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {mContentParent.removeAllViews();}if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,getContext());transitionTo(newScene);} else {mLayoutInflater.inflate(layoutResID, mContentParent);}final Callback cb = getCallback();if (cb != null && !isDestroyed()) {cb.onContentChanged();}
}protected ViewGroup generateLayout(DecorView decor) {...View in = mLayoutInflater.inflate(layoutResource, null);decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));mContentRoot = (ViewGroup) in;ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);if (contentParent == null) {throw new RuntimeException("Window couldn't find content container view");}...return contentParent;
}//Activity.java
/*** Set the activity content from a layout resource.  The resource will be* inflated, adding all top-level views to the activity.** @param layoutResID Resource ID to be inflated.** @see #setContentView(android.view.View)* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)*/
public void setContentView(int layoutResID) {getWindow().setContentView(layoutResID);initWindowDecorActionBar();
}

这里我们就看出了mDecorView中包含了mContentParent,并且DecorView继承自FramLayout,所以touch事件的分发也符合View的事件分发,mDecorView之后会添加到Activity关联的Window上(这里我们不再深究),下面我们来看DecorView的superDispatchTouchEvent:

//PhoneWindow.java#DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {return super.dispatchTouchEvent(event);
}

至此,activity的dispatchTouchEvent方法就最终分发到了我们的布局上,最后总结一下:

Activity#dispatchTouchEvent -> PhoneWindow#superDispatchTouchEvent ->
DecorView#superDispatchTouchEvent -> ViewGroup#dispatchTouchEvent -> View#dispatchTouchEvent

解决滑动冲突的原理

看了上面的源码解析,我们知道Viewtouch事件分发过程中重要的三个函数:

  • dispatchTouchEvent 负责touch事件的分发
  • onInterceptTouchEvent 负责拦截touch事件
  • onTouchEvent 最终处理touch事件

其中dispatchTouchEvent和onInterceptTouchEvent可以控制touch事件流不传递给子控件,这两个方法中可以控制事件流的向下分发,那么是不是有方法控制事件流向上分发呢?我们找到ViewGroup中有这样一个函数:

//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {...boolean handled = false;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;}// Check for cancelation.final boolean canceled = resetCancelNextUpFlag(this)|| actionMasked == MotionEvent.ACTION_CANCEL;//如果没有拦截再分发下去处理if (!canceled && !intercepted) {...if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {...}...}return handled;
}/*** {@inheritDoc}*/
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {// We're already in this state, assume our ancestors are tooreturn;}if (disallowIntercept) {mGroupFlags |= FLAG_DISALLOW_INTERCEPT;} else {mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;}// Pass it up to our parentif (mParent != null) {mParent.requestDisallowInterceptTouchEvent(disallowIntercept);}
}

实际上requestDisallowInterceptTouchEvent是修改了disallowIntercept的状态,再结合ViewGroup的dispatchTouchEvent方法查看,我们就明白这个方法的最终意义。ViewGroup的子元素可以通过调用这个方法禁止ViewGroup拦截touch事件。到这里我们就找到了自下而上的touch事件的拦截方法。

滑动冲突两种解决办法

1. 外部拦截法

通过上面的原理分析我们知道我们可以在dispatchTouchEvent的时候不分发事件或者onInterceptTouchEvent时候拦截事件,实际上onInterceptTouchEvent方法是一个空方法,是android专门提供给我们处理touch事件拦截的方法,所以这里我们在onInterceptTouchEvent方法中拦截touch事件。

具体做法就是当你不想把事件传递给子控件的时候在onInterceptTouchEvent方法中返回true即可拦截事件,这时候子控件将不会再接收到这一次的touch事件流(所谓touch事件流是以ACTION_DOWN开始,中间包含若干个ACTION_MOVE,以ACTION_UP结束的一连串事件)。伪代码如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {if ( condition ) {return true;}return false;
}

这里的condition将会再下一章节中具体讲解。

2. 内部拦截法

首先,我们让父控件拦截除了ACTION_DOWN以外的所有事件,如果连ACTION_DOWN都拦截那么子控件将无法收到任何touch事件:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {int action = event.getAction();if (action == MotionEvent.ACTION_DOWN) {return false;} else {return true;}
}

然后,在控件的内部分发事件的时候请求需要的事件(实际上就是禁止父控件拦截事件):

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {switch (ev.getAction()){case MotionEvent.ACTION_DOWN://通知父容器不要拦截事件parent.requestDisallowInterceptTouchEvent(true);break;case MotionEvent.ACTION_MOVE:if ( <condition> ){//通知父容器拦截此事件parent.requestDisallowInterceptTouchEvent(false);}break;case MotionEvent.ACTION_UP:parent.requestDisallowInterceptTouchEvent(false);break;default:break;}return super.dispatchTouchEvent(ev);
}

这样,就可以解决touch事件的冲突问题,从控件本身解决。内部拦截法使用起来稍显复杂,需要修改两个控件,一般情况下我们都通过外部拦截法解决滑动冲突,如果有特殊情况需要使用内部拦截法才会使用内部拦截法。

事件拦截Condition

试想以下情况:

MapView的功能是内部可以任意滑动(包括上下,左右以及任意方向滑动),ScrollView需要上下滑动。这时候我们在MapView内部上下滑动时会出现什么结果?我们期望的结果是MapView内部滑动,但是我们看到的实际情况却是ScrollView在上下滑动,滑动冲突就产生了,解决这个滑动冲突的方法很简单,直接上代码:

@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {if (isMapViewTouched(ev)) {return false;} else {return super.onInterceptTouchEvent(ev);}}private boolean isMapViewTouched(MotionEvent ev) {if (getChildCount() == 1) {float touchX = ev.getX();float touchY = ev.getY() + getScrollY();LinearLayout baseLinear = (LinearLayout) getChildAt(0);for (int i = 0; i < baseLinear.getChildCount(); i++) {View child = baseLinear.getChildAt(i);// add map view you want ignoreif (isMapView(child)) {if (touchX < child.getRight() && touchX > child.getLeft() &&touchY < child.getBottom() && touchY > child.getTop()) {return true;}}}}return false;
}private boolean isMapView(View child) {return child instanceof MapView ||child instanceof com.google.android.gms.maps.MapView;
}

isMapViewTouched这个函数就是我们这个情况下的condition,具体的含义就是当前点击的是MapView那么所有的touch事件都不允许拦截,交由MapView处理。

这是一种很简单的滑动冲突情况,没有判断滑动的方向以及速度等因素,一般的我们通过判断滑动的方向作为判断条件,下面我们再来看一种情况:

ViewPager需要左右滑动,ListView需要上下滑动,当我们斜向滑动时就出现了滑动冲突。实际上ViewPage已经解决了这种滑动冲突,这里我们假定它没有解决这种滑动冲突,我们自己来解决这个滑动冲突。当我们斜向滑动时候示意图如下:

当我们从start滑动到end时,x方向的坐标变化我们称之为dx,y方向的坐标变化我们称之为dy。

  1. 当dx > dy时我们视其为水平滑动
  2. 当dx < dy时我们视其为竖直滑动

通过外部拦截法的代码如下:

//ViewPager.java
int lastX = -1;
int lastY = -1;
boolean isHorizontal = false;
boolean hasDirection = false;@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;int currentX = ev.getX();int currentY = ev.getY();switch( action ){case MotionEvent.ACTION_DOWN:break;case MotionEvent.ACTION_MOVE:int dx = Math.abs(currentX - lastX);int dy = Math.abs(currentY - lastY);// 这里为了保证用户体验,当我们第一次滑动的方向即为这次touch事件流的滑动方向if ( hasDirection ) {return isHorizontal;} else {if ( dx > dy ) { // 水平滑动isHorizontal = true;return true;} else { // 竖直滑动isHorizontal = false;return false;}}hasDirection = true;lastX = currentX;lastY = currentY;break;case MotionEvent.ACTION_UP:hasDirection = false;break;}return super.onInterceptTouchEvent(ev);
}

滑动冲突解决拓展

滑动冲突的解决方法我们已经知道了,以后无论遇到多么复杂的情况解决滑动冲突的原则都是不变的,根据你的业务需求进行不同的事件拦截即可。

Touch事件的来源深入篇

如果你想知道Activity中的Touch事件是从哪来的,你可以查看任玉刚大神的这篇文章:http://blog.csdn.net/singwhatiwanna/article/details/50775201

Android滑动冲突解决方法相关推荐

  1. Android滑动冲突解决方法(二)

    之前的一遍学习笔记主要就Android滑动冲突中,在不同方向的滑动所造成冲突进行了了解,这种冲突很容易理解,当然也很容易解决.今天,就同方向的滑动所造成的冲突进行一下了解,这里就先以垂直方向的滑动冲突 ...

  2. Android滑动冲突解决方式(下拉刷新上拉加载更多,适配RecyclerView/ListView/ScrollView)

    一.Android事件的分发机制 这里需要了解下Andorid事件的分发机制.事件分发一般是针对一组事件,即ACTION_DOWN > ACTION_UP 或 ACTION_DOWN > ...

  3. 关于ViewPager嵌套ViewPager左右滑动冲突解决方法

    情况如下,TabLayout+ViewPager+Fragment,其中一个Fragment里面又嵌套了一层TabLayout+ViewPager+Fragment,运行的时候出现了两个情况. 1.子 ...

  4. ScrollView嵌套ViewPager,ViewPage动态设置高度,嵌套事件冲突——滑动冲突解决方法

    1.创建自定义ViewPager public class ViewPagerForScrollView extends ViewPager {int myh=0 ;boolean ifme =fal ...

  5. Android滑动冲突的完美解决方案

    一.Android滑动冲突的完美解决方案 在Android开发中滑动冲突可以说是比较常见的一类问题,也是比较让人头疼的一类问题,两个原本完美的控件,组合在一起之后,忽然发现整个世界都不好了.滑动冲突主 ...

  6. 关于Android滑动冲突的解决方法(二)

    之前的一遍学习笔记主要就Android滑动冲突中,在不同方向的滑动所造成冲突进行了了解,这种冲突很容易理解,当然也很容易解决.今天,就同方向的滑动所造成的冲突进行一下了解,这里就先以垂直方向的滑动冲突 ...

  7. Android进阶必备:滑动冲突解决与事件分发机制(附视频讲解)这篇看完还不懂请寄刀片

    1.前言 Android学习一段时间,需求做多了必然会遇到滑动冲突问题,比如在一个ScrollView中要嵌套一个地图View,这时候触摸移动地图或者放大缩小地图就会变得不太准确甚至没有反应,这就是遇 ...

  8. Android Studio androidx 包冲突解决方法

    Android Studio androidx 包冲突解决方法 参考文章: (1)Android Studio androidx 包冲突解决方法 (2)https://www.cnblogs.com/ ...

  9. android studio 导入模块SDK 以及该模块中继承application的类与原工程继承application的类存在冲突 解决方法

    android studio 导入模块SDK 以及该模块中继承application的类与原工程继承application的类存在冲突 解决方法 导入某修改后的海康视频SDK(老师给的),并修改相关文 ...

最新文章

  1. 芯片IP,SOC,FPGA智能卡
  2. too many levels of symbolic links的错误
  3. 数据库中char varchar nchar nvarchar的区别
  4. 用Go重构WEB请求分析跟踪服务
  5. [云炬创业基础笔记]第十一章创业计划书测试8
  6. clickhouse 航空数据_ClickHouse空间分析运用
  7. Spring Boot——获取上传文件的MD5值解决方案
  8. 获取linux详细信息,Linux 获取网口详细信息
  9. 简单的Jquery轮播
  10. JavaScript服务器端开发(函数实参对象arguments使用的几个注意事项)
  11. php毕业设计和毕业论文怎么写,答辩流程是怎样的?
  12. 富士胶片滤镜人像调色预设PS插件
  13. 配置mysql环境变量之后,仍然无法打开mysql的解决方法
  14. ps出现标尺的快捷键,隐藏参考线,把隐藏的参考线显示出来的快捷键。
  15. LeetCode K站中转内最便宜的航班(回溯法、动态规划)
  16. linux 内核 禁止抢占,内核抢占实现(preempt)
  17. 非视距微波传输抗干扰特性
  18. php微信聊天带语音,使用Html5多媒体实现微信语音功能
  19. 一个简单的ASP登录页面
  20. 数学模型课程期末复习提纲(上)

热门文章

  1. 工信部:今年将发放5G临时牌照,预计下半年5G手机投放市场
  2. J0ker的CISSP之路:复习Access Control(10)
  3. JavaScript设计模式--简单工厂模式例子---XHR工厂
  4. UVALive5389 UVA414 POJ1493 ZOJ1339 Machined Surfaces
  5. vs2013 类名颜色显示黑色,无法修改
  6. 后面一次上传对linux kernel 的分析
  7. Mysql与sqlite注意
  8. windows下scrapy安装问题,以及Twisted安装报错(error: Microsoft Visual C++ 14.0 is required.)完美解决办法...
  9. 208. Implement Trie (Prefix Tree)
  10. [Android Studio] Android Studio常用快捷键