Android View事件机制

1.事件机制代码解释说明

1.1 触摸事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 触摸事件
* 如果返回结果为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 分发事件

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 分发事件
* 根据内部拦截状态,向其child或者自己分发事件
*
* @param ev
* @return
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e("onEvent","MyLinearLayout dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
}

1.3 拦截事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 拦截事件
* 默认实现是返回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.三个事件机制怎么向其调用者传递处理结果

  • 1、事件的分发机制,dispatchTouchEvent。
    • true-事件被以该节点为根节点的View树成功处理,此时该事件就算是处理完成了,事件不会再向上返还给View的父节点(把事件分发过来的那个节点)。
    • false-以该节点为根节点的View树种,没有一个View(包括该View)成功处理了此事件,所以事件会向上返还给View的父节点(把事件分发过来的那个节点)。
  • 2、事件的拦截机制,onInterceptTouchEvent。主要是parent根据它内部的状态、或者child的状态,来把事件拦截下来,阻止其进一步传递到child的机制。
    • true-当前ViewGroup(因为View中没有该方法,而没有child的VIew也不需要有拦截机制)希望该事件不再传递给其child,而是希望自己处理。
    • false-当前ViewGroup不准备拦截该事件,事件正常向下分发给其child。
  • 3、事件的处理机制,onTouchEvent。主要是事件序列的接受者(可以是一个View或者ViewGroup),对事件作出处理,并且向其parent传递处理结果的机制。
    • true-表示该View成功处理了该事件,该处理结果会向上通知给其parent。
    • false-表示该View没有成功处理该事件,那么它的parent会有机会来处理该事件(parent标记为事件序列接受者,parent 的 onTouchEvent 在 Down 事件时返回true)。

3.触摸事件类型【常见】

  • a.1 ACTION_DOWN

    • 用户手指的按下操作,一次按下的操作标志者一次触摸事件的开始。
  • a.2 ACTION_UP

    • 用户手指离开屏幕的操作,一次抬起操作标志者一次触摸事件的结束
  • a.3 ACTION_MOVE

    • 用户手指按压屏幕后,在松开之前,如果距离超过一定得阈值,那么会被判定为ACTION_MOVE。一般情况下,手指的轻微移动都会触发一系列的移动事件。
  • a.4 注意

    • 1.在一次屏幕触摸事件中,ACTION_DOWN和ACTION_UP者两个事件是必须的,而ACTION_MOVE视情况而定,如果用户仅是点击了一下屏幕,那么可能只会监测到按下和抬起的动作。
    • 2.通过MotionEvent可以获得事件发生的x和y坐标,getX和getY返回的是相当于当前View左上角的X和Y坐标,getRawX和getRawY返回的是相当于手机屏幕左上角的X和Y坐标
  • a.5 其他

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Touch触摸事件
    ♦ 在Android中Touch触摸事件主要包括:
    点击(onClick)
    长按(onLongClick)
    拖拽(onDrag)
    滑动(onScroll)
    ♦ 在Android中Touch操作状态主要包括:
    按下(ACTION_DOWN)
    移动(ACTION_MOVE)
    抬起(ACTION_UP)
    取消手势(ACTION_CANCEL)
    划出屏幕(ACTION_OUTSIDE)
  • Android触摸事件流程总结

    • 1.一个事件序列从手指触摸屏幕开始,到触摸结束。同一事件序列是以
      ACTION_DOWN开始,中间有数量不定的ACTION_MOVE事件,最终以ACTION_UP结束
    • 2.事件传递顺序是:Activity——>Window——>View;最后顶级View接收到事件后,就会按照事件分发机制去分发事件
    • 3.事件传递过程是由外向内的,即事件总是有父元素分发给子元素

4.三个重要方法的分布情况

  • 4.1 View
    • 1.分发事件 dispatchTouchEvent
    • 2.处理事件 onTouchEvent
  • 4.2 ViewGroup
    • 1.分发事件 dispatchTouchEvent
    • 2.拦截事件 onInterceptTouchEvent
    • 3.处理事件 onTouchEvent
  • 4.3 Activity
    • 1.分发事件 dispatchTouchEvent
      • 2.处理事件 onTouchEvent

5.滑动冲突的思路及方法

5.1 第一种情况,滑动方向不同

  • 5.1.1 ScrollView里嵌套ViewPager

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

  • 5.2.1 ViewPager嵌套ViewPager
  • 5.2.2 ScrollView嵌套RecyclerView或者GridView

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

  • 5.3.1 比如scrollview嵌套recyclerview,recyclerView复杂条目中又有横向滑动的list

6.滑动冲突解决方案

6.1 滑动方向不同之以ScrollView与ViewPager为例的外部解决法

  • 从 父View 着手,重写 onInterceptTouchEvent 方法,在 父View 需要拦截的时候拦截,不要的时候返回false,代码大概如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
举例子:以ScrollView与ViewPager为例
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);
}
}

6.2 滑动方向不同之以ScrollView与ViewPager为例的内部解决法

  • 从子View着手,父View 先不要拦截任何事件,所有的 事件传递给子View,如果子View需要此事件就消费掉,不需要此事件的话就交给 父View 处理。

  • 实现思路 如下,重写 子View 的dispatchTouchEvent方法,在Action_down动作中通过方法requestDisallowInterceptTouchEvent(true) 先请求 父View 不要拦截事件,这样保证子View能够接受到Action_move事件,再在Action_move动作中根据自己的逻辑是否要拦截事件,不要的话再交给 父View 处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    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);
    }
    }

6.3 滑动方向相同之以ViewPager嵌套ViewPager为例的内部解决法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
从 子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);
}
}

6.4 滑动方向相同之以ScrollView和RecycleView为例的内部解决法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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一定要被嵌套里面
<!-- descendantFocusability该属性是当一个为view获取焦点时,定义viewGroup和其子控件两者之间的关系。
beforeDescendants:viewgroup会优先其子类控件而获取到焦点
afterDescendants:viewgroup只有当其子类控件不需要获取焦点时才获取焦点
blocksDescendants:viewgroup会覆盖子类控件而直接获得焦点-->

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>