Android事件冲突与解决方案

摘要:事件冲突解决思路与方案目录介绍
1.事件机制简单介绍 1.1触摸事件1.2分发事件1.3拦截事件
2.解决滑动冲突的思路及方法 2.1第一种情况,滑动方向不同2.2第二种情况,滑动方法相同2.3第三种情况,以上两种情况嵌套
3.案例解决方法 3.1针对2问题的解决思路3.2滑动方向不同,解决冲突的外部解决法3.3滑动方向不同,解决冲突的内部解决法3.4ViewPager嵌套ViewPager内部解决法3.5滑动方向相同,解决冲突的外部解决法3.6解决ScrollView和ViewPage
事件冲突解决思路与方案
目录介绍
1.事件机制简单介绍
1.1 触摸事件
1.2 分发事件
1.3 拦截事件

2.解决滑动冲突的思路及方法
2.1 第一种情况,滑动方向不同
2.2 第二种情况,滑动方法相同
2.3 第三种情况,以上两种情况嵌套

3.案例解决方法
3.1 针对2问题的解决思路
3.2 滑动方向不同,解决冲突的外部解决法
3.3 滑动方向不同,解决冲突的内部解决法
3.4 ViewPager嵌套ViewPager内部解决法
3.5 滑动方向相同,解决冲突的外部解决法
3.6 解决ScrollView和ViewPager,RecycleView滑动冲突

1.事件机制简单介绍
1.1 触摸事件
/**

  • 触摸事件
  • 如果返回结果为false表示不消费该事件,并且也不会截获接下来的事件序列,事件会继续传递
  • 如果返回为true表示当前View消费该事件,阻止事件继续传递
  • 在这里要强调View的OnTouchListener。如果View设置了该监听,那么OnTouch()将会回调。
  • 如果返回为true那么该View的OnTouchEvent将不会在执行 这是因为设置的OnTouchListener执行时的优先级要比onTouchEvent高。
  • 优先级:OnTouchListener > onTouchEvent > onClickListener
  • @param event
  • @return
    */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    Log.e("onEvent","MyLinearLayout onTouchEvent");
    return super.onTouchEvent(event);
    }
    1.2 分发事件

/**

  • 分发事件
  • 根据内部拦截状态,向其child或者自己分发事件
  • @param ev
  • @return
    */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    Log.e("onEvent","MyLinearLayout dispatchTouchEvent");
    return super.dispatchTouchEvent(ev);
    }
    1.3拦截事件

/**

  • 拦截事件
  • 默认实现是返回false,也就是默认不拦截任何事件
  • 判断自己是否需要截取事件
  • 如果该方法返回为true,那么View将消费该事件,即会调用onTouchEvent()方法
  • 如果返回false,那么通过调用子View的dispatchTouchEvent()将事件交由子View来处理
  • @param ev
  • @return
    */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
    Log.e("onEvent","MyLinearLayout onInterceptTouchEvent");
    return super.onInterceptTouchEvent(ev);
    }
    2.解决滑动冲突的思路及方法
    2.1 第一种情况,滑动方向不同
ffd903a6d3305e66137dd3479e0884ba.jpg

2.2 第二种情况,滑动方法相同


a4c39744e199d7eef63b7b89877f992b.jpg

2.3 第三种情况,以上两种情况嵌套


0441f628d1372efeaaffd4b7b9b019a4.jpg

3.案例解决方法
3.1 针对2问题的解决思路

看了上面三种情况,我们知道他们的共同特点是 父View 和 子View 都想争着响应我们的触摸事件,但遗憾的是我们的触摸事件 同一时刻 只能被某一个View或者ViewGroup拦截消费,所以就产生了滑动冲突?那既然同一时刻只能由某一个View或者ViewGroup消费拦截,那我们就只需要 决定在某个时刻由这个View或者ViewGroup拦截事件,另外的 某个时刻由 另外一个View或者ViewGroup拦截事件不就OK了吗?综上,正如 在 《Android开发艺术》 一书提出的,总共 有两种解决方案
3.2 滑动方向不同,解决冲突的外部解决法【以ScrollView与ViewPager为例 】

举例子:以ScrollView与ViewPager为例
从 父View 着手,重写 onInterceptTouchEvent 方法,在 父View 需要拦截的时候拦截,不要的时候返回false,代码大概如下

public class MyScrollView extends ScrollView { 
public MyScrollView(Context context) { 
super(context); 
} 
public MyScrollView(Context context, AttributeSet attrs) { 
super(context, attrs); 
} 
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) { 
super(context, attrs, defStyleAttr); 
} 
@TargetApi(21) 
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 
super(context, attrs, defStyleAttr, defStyleRes); 
} 
private float mDownPosX = 0; 
private float mDownPosY = 0; 
@Override 
public boolean onInterceptTouchEvent(MotionEvent ev) { 
final float x = ev.getX(); 
final float y = ev.getY(); 
final int action = ev.getAction(); 
switch (action) { 
case MotionEvent.ACTION_DOWN: 
mDownPosX = x; 
mDownPosY = y; 
break; 
case MotionEvent.ACTION_MOVE: 
final float deltaX = Math.abs(x - mDownPosX); 
final float deltaY = Math.abs(y - mDownPosY); 
// 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截 
if (deltaX > deltaY) { 
return false; 
} 
} 
return super.onInterceptTouchEvent(ev); 
} 
} 

3.3 滑动方向不同,解决冲突的内部解决法【以ScrollView与ViewPager为例 】

从子View着手,父View 先不要拦截任何事件,所有的 事件传递给 子View,如果 子View 需要此事件就消费掉,不需要此事件的话就交给 父View 处理。
实现思路 如下,重写 子View 的 dispatchTouchEvent 方法,在 Action_down 动作中通过方法 requestDisallowInterceptTouchEvent(true) 先请求 父View 不要拦截事件,这样保证 子View 能够接受到Action_move事件,再在Action_move动作中根据 自己的逻辑是否要拦截事件,不要的话再交给 父View 处理

public class MyViewPager extends ViewPager { 
private static final String TAG = "yc"; 
int lastX = -1; 
int lastY = -1; 
public MyViewPager(Context context) { 
super(context); 
} 
public MyViewPager(Context context, AttributeSet attrs) { 
super(context, attrs); 
} 
@Override 
public boolean dispatchTouchEvent(MotionEvent ev) { 
int x = (int) ev.getRawX(); 
int y = (int) ev.getRawY(); 
int dealtX = 0; 
int dealtY = 0; 
switch (ev.getAction()) { 
case MotionEvent.ACTION_DOWN: 
dealtX = 0; 
dealtY = 0; 
// 保证子View能够接收到Action_move事件 
getParent().requestDisallowInterceptTouchEvent(true); 
break; 
case MotionEvent.ACTION_MOVE: 
dealtX += Math.abs(x - lastX); 
dealtY += Math.abs(y - lastY); 
Log.i(TAG, "dealtX:=" + dealtX); 
Log.i(TAG, "dealtY:=" + dealtY); 
// 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截 
if (dealtX >= dealtY) { 
getParent().requestDisallowInterceptTouchEvent(true); 
} else { 
getParent().requestDisallowInterceptTouchEvent(false); 
} 
lastX = x; 
lastY = y; 
break; 
case MotionEvent.ACTION_CANCEL: 
break; 
case MotionEvent.ACTION_UP: 
break; 
} 
return super.dispatchTouchEvent(ev); 
} 
} 

3.4 ViewPager嵌套ViewPager内部解决法

从 子View ViewPager着手,重写 子View 的 dispatchTouchEvent方法,在 子View 需要拦截的时候进行拦截,否则交给 父View 处理,代码如下

public class ChildViewPager extends ViewPager { 
private static final String TAG = "yc"; 
public ChildViewPager(Context context) { 
super(context); 
} 
public ChildViewPager(Context context, AttributeSet attrs) { 
super(context, attrs); 
} 
@Override 
public boolean dispatchTouchEvent(MotionEvent ev) { 
int curPosition; 
switch (ev.getAction()) { 
case MotionEvent.ACTION_DOWN: 
getParent().requestDisallowInterceptTouchEvent(true); 
break; 
case MotionEvent.ACTION_MOVE: 
curPosition = this.getCurrentItem(); 
int count = this.getAdapter().getCount(); 
Log.i(TAG, "curPosition:=" +curPosition); 
// 当当前页面在最后一页和第0页的时候,由父亲拦截触摸事件 
if (curPosition == count - 1|| curPosition==0) { 
getParent().requestDisallowInterceptTouchEvent(false); 
} else {//其他情况,由孩子拦截触摸事件 
getParent().requestDisallowInterceptTouchEvent(true); 
} 
} 
return super.dispatchTouchEvent(ev); 
} 
} 

3.5 滑动方向相同,解决冲突的外部解决法【解决ScrollView和RecycleView滑动冲突】

public class RecyclerScrollview extends ScrollView { 
private int downY; 
private int mTouchSlop; 
public RecyclerScrollview(Context context) { 
super(context); 
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 
} 
public RecyclerScrollview(Context context, AttributeSet attrs) { 
super(context, attrs); 
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 
} 
public RecyclerScrollview(Context context, AttributeSet attrs, int defStyleAttr) { 
super(context, attrs, defStyleAttr); 
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 
} 
@Override 
public boolean onInterceptTouchEvent(MotionEvent e) { 
int action = e.getAction(); 
switch (action) { 
case MotionEvent.ACTION_DOWN: 
downY = (int) e.getRawY(); 
break; 
case MotionEvent.ACTION_MOVE: 
int moveY = (int) e.getRawY(); 
if (Math.abs(moveY - downY) > mTouchSlop) { 
return true; 
} 
} 
return super.onInterceptTouchEvent(e); 
} 
} 
**注意:RecycleView一定要被嵌套里面** 

android:layout_width="match_parent" 
android:layout_height="wrap_content" 
android:descendantFocusability="blocksDescendants"> 
android:id="@+id/recyclerView" 
android:layout_width="match_parent" 
android:layout_height="match_parent"/>

3.6 解决ScrollView和ViewPager,RecycleView滑动冲突
对于ScrollView

public class BottomScrollView extends ScrollView { 
private OnScrollToBottomListener mOnScrollToBottomListener; 
public BottomScrollView(Context context) { 
super(context); 
} 
public BottomScrollView(Context context, AttributeSet attrs) { 
super(context, attrs); 
} 
public BottomScrollView(Context context, AttributeSet attrs, int defStyleAttr) { 
super(context, attrs, defStyleAttr); 
} 
@TargetApi(Build.VERSION_CODES.LOLLIPOP) 
public BottomScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 
super(context, attrs, defStyleAttr, defStyleRes); 
} 
@Override 
protected void onScrollChanged(int l, int t, int oldl, int oldt){ 
super.onScrollChanged(l,t,oldl,oldt); 
// 滑动的距离加上本身的高度与子View的高度对比 
if(t + getHeight() >=getChildAt(0).getMeasuredHeight()){ 
// ScrollView滑动到底部 
if(mOnScrollToBottomListener != null) { 
mOnScrollToBottomListener.onScrollToBottom(); 
} 
} else { 
if(mOnScrollToBottomListener != null) { 
mOnScrollToBottomListener.onNotScrollToBottom(); 
} 
} 
} 
public void setScrollToBottomListener(OnScrollToBottomListener listener) { 
this.mOnScrollToBottomListener = listener; 
} 
public interface OnScrollToBottomListener { 
void onScrollToBottom(); 
void onNotScrollToBottom(); 
} 
} 
**// ViewPager滑动冲突解决** 
mViewPager.setOnTouchListener(new View.OnTouchListener() { 
@Override 
public boolean onTouch(View v, MotionEvent event) { 
int action = event.getAction(); 
if(action == MotionEvent.ACTION_DOWN) { 
// 记录点击到ViewPager时候,手指的X坐标 
mLastX = event.getX(); 
} 
if(action == MotionEvent.ACTION_MOVE) { 
// 超过阈值,禁止SwipeRefreshLayout下拉刷新,禁止ScrollView截断点击事件 
if(Math.abs(event.getX() - mLastX) > THRESHOLD_X_VIEW_PAGER) { 
mRefreshLayout.setEnabled(false); 
mScrollView.requestDisallowInterceptTouchEvent(true); 
} 
} 
// 用户抬起手指,恢复父布局状态 
if(action == MotionEvent.ACTION_UP) { 
mRefreshLayout.setEnabled(true); 
mScrollView.requestDisallowInterceptTouchEvent(false); 
} 
return false; 
} 
}); 
**// ListView滑动冲突解决** 
mListView.setOnTouchListener(new View.OnTouchListener() { 
@Override 
public boolean onTouch(View v, MotionEvent event) { 
int action = event.getAction(); 
if(action == MotionEvent.ACTION_DOWN) { 
mLastY = event.getY(); 
} 
if(action == MotionEvent.ACTION_MOVE) { 
int top = mListView.getChildAt(0).getTop(); 
float nowY = event.getY(); 
if(!isSvToBottom) { 
// 允许scrollview拦截点击事件, scrollView滑动 
mScrollView.requestDisallowInterceptTouchEvent(false); 
} else if(top == 0 &;&; nowY - mLastY > THRESHOLD_Y_LIST_VIEW) { 
// 允许scrollview拦截点击事件, scrollView滑动 
mScrollView.requestDisallowInterceptTouchEvent(false); 
} else { 
// 不允许scrollview拦截点击事件, listView滑动 
mScrollView.requestDisallowInterceptTouchEvent(true); 
} 
} 
return false; 
} 
}); 

此篇文章来源于:https://www.aliyun.com/jiaocheng/4207.html
或者为了加深理解,下面这篇博文讲解的很好
ScrollView嵌套RecyclerView滑动冲突相关问题
一、应用场景
在解决具体问题之前,先介绍下实际应用场景及问题状况。

3985563-f3d215ecdb5c06b2.png

从图中可以看出,一个ScrollView内部嵌套三个RecyclerView,其中两个RecyclerView是横向,一个RecyclerView是纵向。
在这个场景下,出现了滑动冲突问题,主要表现为横向RecyclerView滑动不灵敏,纵向RecyclerView滑动卡顿。
二、问题分析
1.横向RecyclerView滑动不灵敏


3985563-d28a60f9b90db420.png

该问题所产生的滑动冲突如上图所示。
针对该问题,解决的方案是根据当前滑动方向,水平还是垂直来判断这个事件到底该交给谁来处理。
一般情况下根据滑动路径形成的夹角(或者说是斜率如下图)、水平和竖直方向滑动速度差来判断。

3985563-e8f50b90995abc06.png

2.纵向RecyclerView滑动卡顿

3985563-4d720b11b84bb15d.png

该问题所产生的滑动冲突如上图所示。
针对该问题,一般情况下必需通过业务逻辑来进行判断,决定到底谁来处理该事件。
三、滑动冲突解决方法
针对滑动冲突,一般有两个解决方法。
1.外部拦截法
事件都先经过父容器的拦截处理,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题。外部拦截法需要重写父容器的onInterceptTouchEvent()方法,在内部完成相应的拦截即可

@Override public boolean onInterceptTouchEvent(MotionEvent ev) {     boolean intercepted = false; 
 int x = (int) ev.getX();
 int y = (int) ev.getY(); 
 switch (ev.getAction()) { 
 case MotionEvent.ACTION_DOWN: 
 intercepted = false;
 break; 
 case MotionEvent.ACTION_MOVE: {
 if (父容器需要事件) { 
 intercepted = true; 
 } else { 
 intercepted = false;
 } 
 break; 
}
 case MotionEvent.ACTION_UP: 
{ 
 intercepted = false; 
 break;
 } 
} 
return intercepted; 
} 

ACTION_DOWN 这个事件里父容器必须返回 false,即不拦截ACTION_DOWN事件,因为一旦拦截了那么后续的 ACTION_MOVE、ACTION_UP都由父容器去处理,事件就无法传到子view了
ACTION_MOVE 事件可以根据需要来进行拦截或者不拦截
ACTION_UP 这个事件必须返回false,就会导致子View无法接受到UP事件,这个时候子元素中的onClick()事件就无法处触发。
2.内部拦截法
父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理。这种方法需要配合requestDisallowInterceptTouchEvent()方法才能正常工作。
主要是修改子view的dispatchTouchEvent()方法

@Override 
public boolean dispatchTouchEvent(MotionEvent ev) {
 int x = (int) ev.getX(); 
 int y = (int) ev.getY(); 
 switch (ev.getAction()) { 
 case MotionEvent.ACTION_DOWN:
 { 
 getParent().requestDisallowInterceptTouchEvent(true);
 break; 
} 
 case MotionEvent.ACTION_MOVE: 
 { if (父容器需要此类事件) { 
 getParent().requestDisallowInterceptTouchEvent(false);
 } 
 break; 
 } 
 case MotionEvent.ACTION_UP: 
{
 break; 
} 
} 
return super.dispatchTouchEvent(ev); 
}

父容器需要重写onInterceptTouchEvent()方法

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

父容器拦截ACTION_DOWN以外的其他事件,因为ACTION_DOWN 事件不受 FLAG_DISALLOW_INTERCEPT这个标记的控制,所以一旦父容器拦截了ACTION_DOWN 事件那么所有的事件都无法传到子view中去了,这样内部拦截法就不起作用了。
四、问题解决
下面就来实际解决本文中遇到的滑动冲突问题。通过上述分析可知,本文所遇到的问题通过外部拦截法,重写ScrollView的onInterceptTouchEvent()方法即可快速简单的解决。

public class FScrollView extends ScrollView { 
   private float mLastXIntercept = 0f; 
   private float mLastYIntercept = 0f; 
public FScrollView(Context context, AttributeSet attrs) { 
   super(context, attrs);
 } 
@Override 
public boolean onInterceptTouchEvent(MotionEvent ev) {
 boolean intercepted = false; 
float x = ev.getX(); 
float y = ev.getY(); 
int action = ev.getAction() & MotionEvent.ACTION_MASK; 
switch (action) { 
case MotionEvent.ACTION_DOWN: {
 intercepted = false; //初始化mActivePointerId super.onInterceptTouchEvent(ev); 
break; 
} 
case MotionEvent.ACTION_MOVE: { 
//横坐标位移增量 
float deltaX = x - mLastXIntercept;
 //纵坐标位移增量 
float deltaY = y - mLastYIntercept; 
if (Math.abs(deltaX) < Math.abs(deltaY)) {
 intercepted = true;
 } else { 
intercepted = false; 
}
 break;
 } 
case MotionEvent.ACTION_UP: {
 intercepted = false; 
break; 
} 
} 
 mLastXIntercept = x; 
 mLastYIntercept = y; 
return intercepted; 
 } 
} 

Math.abs(deltaX) < Math.abs(deltaY)表示横向位移增量小于于竖向位移增量,即竖直滑动,则ScrollView 拦截事件。
super.onInterceptTouchEvent(ev),初始化mActivePointerId,避免出现Invalid pointerId=-1 in onTouchEvent问题。
纵向RecyclerView的滑动被拦截,交给ScrollView处理,需要测量高度,会默认加载所有item,相当于LinearLayout,从而导致复用效率大大降低。所以如果情况复杂,建议采用头布局。
此篇文章来源于:https://www.jianshu.com/p/d456f4e88697

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容