一步步自定义下拉刷新上拉加载——事件分发与滑动冲突

自定义下拉刷新组件之前,必须要了解什么是事件分发机制。
按照顺序阅读,看完郭霖 事件分发机制上郭霖 事件分发机制下,和鸿洋 事件分发机制上鸿洋 事件分发机制下以后结合Android开发艺术探索和源码,然后看事件分发机制总结这篇,基本能够掌握事件分发机制了。

事件分发机制的内容对于初学者而言,是反复看,实践过后再理解便能掌握其原理。下面我们简要介绍下事件分发机制,具体内容,请按顺序反复阅读上面提及的几篇文章。

事件分发机制

什么是事件分发机制?
事件分发机制就是,当一个点击事件发生后,系统将这个事件按照Activity,ViewGroup,View传递给具体的View去处理。

事件分发机制涉及以下三个方法

  • dispatchTouchEvent 分发点击事件
  • onInterceptTouchEvent 拦截点击事件
  • onTouchEvent 处理点击事件

记住View没有onInterceptTouchEvent这个方法。

小例子

我们通过一个小例子来大致了解下事件分发机制的流程,这里我们使用系统默认的返回方式。

步骤:

  1. 创建TestLinearLayout 继承 Linearlayout, 重写 dispatchTouchEvent, onInterceptTouchEvent,onTouchEvent
  2. 创建TestButton 继承 Button,重写onInterceptTouchEvent,onTouchEvent
  3. 创建布局
  4. 重写Activity的dispatchTouchEvent,onTouchEvent

TestLinearLayout

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
54
55
public class TestLinearLayout extends LinearLayout {
public String TAG = "TestLinearLayout";

public TestLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "onTouchEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "onTouchEvent: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "onTouchEvent: ACTION_UP");
break;
}
return super.onTouchEvent(event);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "onInterceptTouchEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "onInterceptTouchEvent: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "onInterceptTouchEvent: ACTION_UP");
break;
}
return super.onInterceptTouchEvent(ev);
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "dispatchTouchEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "dispatchTouchEvent: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "dispatchTouchEvent: ACTION_UP");
break;
}
return super.dispatchTouchEvent(ev);
}
}

TestButton中的内容和TestLinearLayout完全一样,只是少了OnInterceptTouchEvent不贴了。

布局

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<com.example.com.myapplication.test.TestLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.example.com.myapplication.TestButton
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

</com.example.com.myapplication.test.TestLinearLayout>

在MainActivity中按类似的方式重写dispatchTouchEvent和OnTouchEvent。

点击空白处打印如下

通过这张图片我们可以看到事件确实是从Activity传递到ViewGroup,由于Activity没有OnInterceptTouchEvent,所以调用完dispatchTouchEvent后,调用ViewGroup的dispatchTouchEvent。

我们还可以发现ViewGroup的onTouchEvent 比Activity先调用。

点击Button打印如下

在这里我们可以清晰的看到
Activity 调用dispatchTouchEvent
ViewGroup 调用dispatchTouchEvent
ViewGroup 调用onInterceptTouchEvent
View 调用dispatchTouchEvent
VIew 调用onTouchEvent

此时我们看到了当View调用了onTouchEvent后,ViewGroup和MainActivity中的onTouchEvent不被调用。
这是为什么呢?
由于Button是可点击的,一旦触发了Button的onTouchEvent,返回true,表示消费了该事件,就不向上传递onTouchEvent事件。

如果在ViewGroup中的OnTouchEvent也返回true,那么Activity中的OnTouchEvent也不会调用。

上述内容可以被表达为:
领导下发了一个事件给 经理,经理把事件 交给你处理,如果你有能力处理 就消费掉这个事件。 没有能力处理,就退回给经理,经理也没能力处理 就退回给领导。

拦截事件可以表达为:
领导下发事件给 经理, 经理觉得太简单了 直接拦截处理。

源码解析

Activity源码

1
2
3
4
5
6
7
8
9
10
11
12
public boolean dispatchTouchEvent(MotionEvent ev) {

//一般事件列开始都是DOWN,所以这里基本是true
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}

if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}

我们主要关注 getWindow().superDispatchTouchEvent(ev)) ,这句话接调用的内容

1
2
3
4
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}

再来看看mDecor调用的内容

1
2
3
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}

这里的super.dispatchTouchEvent()调用的就是ViewGroup的dispatchTouchEvent。
所以在 Activity调用完dispatchTouchEvent之后会调用ViewGroup的相同的方法。

ViewGroup的源码 伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public boolean dispatchTouchEvent(MotionEvent ev) {

//代表是否消耗事件
boolean consume = false;

if (onInterceptTouchEvent(ev)) {

consume = onTouchEvent (ev) ;

} else {

consume = child.dispatchTouchEvent (ev) ;
}

return consume;
}

通过这段代码,我们可以很清楚的看到,ViewGroup调用dispatchTouchEvent后,
调用onInterceptTouchEvent,判断是否拦截,如果返回true,表示拦截。那么接下来就调用ViewGroup自身的onTouchEvent来处理时间,不会传递给View。
如果返回false,表示不拦截,接下来就调用View的dispatchTouchEvent方法,触发View相关的事件。

View的源码

1
2
3
4
5
6
7
public boolean dispatchTouchEvent(MotionEvent event) {  
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}

这段源码涉及到了 setOnTouchListener事件。当我们给View设置 setOnTouchListener

1
2
3
public boolean onTouch(View v, MotionEvent event) {
return false;
}

默认返回false。 结合上面的代码,如果我们设置了setOnTouchListener,
即mOnTouchListener!=null
(mViewFlags & ENABLED_MASK) == ENABLED表示View可使用
mOnTouchListener.onTouch(this, event) 中将默认的false 改为true, 就直接返回true,不会执行onTouchEvent。
否则,执行 onTouchEvent。

接下来我们看onTouchEvent,源码很长,我们截取一部分看,

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
public boolean onTouchEvent(MotionEvent event) {  
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//如果该控件是可以点击的就会进入到下两行的switch判断中去;

if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
//如果当前的事件是抬起手指,则会进入到MotionEvent.ACTION_UP这个case当中。

switch (event.getAction()) {
case MotionEvent.ACTION_UP:
......
if (!focusTaken) {

if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {

//performClick会调用 onClick
performClick();
}
}
}
.....
break;
case MotionEvent.ACTION_DOWN:
....
break;
case MotionEvent.ACTION_CANCEL:
......
break;
case MotionEvent.ACTION_MOVE:
.....
break;
}
//如果该控件是可以点击的,就一定会返回true
return true;
}
return false;
}

这段代码中我们可以发现 只要进入 if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) ,返回值就为true,表明了只要View为可点击的,那么onTouchEvent 一定会 返回true,也就是一定会 消费事件。这也就解释了上面 点击Button 触发onTouchEvent 不调用 ViewGroup和Activity onTouchEvent的原因了。

注意事项

我们知道MotionEvent 主要有以下三个事件

  • ACTION_DOWN
  • ACTION_MOVE
  • ACTION_UP

我们每次点击屏幕,最先触发的就是ACTION_DOWN事件。在ViewGroup的拦截事件中,
面对ACTION_DOWN ,ViewGroup总会调用自己的onInterceptTouchEvent来询问是否拦截事件。
一旦ViewGroup在ACTION_DOWN 拦截,onInterceptTouchEvent 返回true,表明要自己处理所有点击事件,也就是不会传递给View,后续的ACTION_MOVE,ACTION_UP 同样不能。

系统还给View提供了改变拦截状态的方法
requestDisallowInterceptTouchEvent,来修改处ACTION_DOWN以外的拦截事件。

综上两点,我们在处理滑动冲突的时候,在ViewGroup的ACTION_DOWN中 onIntercept 设置为false。下面就来介绍滑动冲突的相关内容。

滑动冲突

这篇文章介绍了滑动冲突解决
滑动冲突分为三种

  • 内布局横向滑动,外布局纵向滑动,或者相反。也就是内外滑动的方向不同
  • 内外布局同时横向滑动,或者纵向滑动。也就是内外滑动方向相同
  • 前两者的多重组合

滑动冲突小例子

滑动冲突的解决方案有两种,

  • 外部拦截法
    父View根据需要对事件进行拦截。逻辑处理放在父View的onInterceptTouchEvent方法中。我们只需要重写父View的onInterceptTouchEvent方法,并根据逻辑需要做相应的拦截即可。

  • 内部拦截法
    父View不拦截任何事件,所有事件都传递给子View,子View根据需要决定是自己消费事件还是给父View处理。这需要子View使用requestDisallowInterceptTouchEvent方法才能正常工作。

为了更容易理解滑动冲突,这里的小例子 创建一个 BadViewPager继承自ViewPager,改写onInterceptTouchEvent

父类不拦截

1
2
3
4
5
6
7
8
9
10
public class BadViewPager extends ViewPager{
public BadViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;//不拦截
}
}

布局文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.com.myapplication.MainActivity">

<com.example.com.myapplication.test.BadViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

list_view_layout的布局

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

</LinearLayout>

主代码如下,在这里给ViewPager 添加了三个ListView布局。

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
54
public class ScrollerActivity extends AppCompatActivity {
private BadViewPager badViewPager;
private List<String> data = new ArrayList<>();
private List<View> viewList = new ArrayList<>();

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
badViewPager = findViewById(R.id.view_pager);

initListView();

badViewPager.setAdapter(new PagerAdapter() {
@Override
public int getCount() {
return viewList.size();
}

@Override
public Object instantiateItem(ViewGroup container, int position) {
container.addView(viewList.get(position));
return viewList.get(position);
}

@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView(viewList.get(position));
}
});
}

private void initListView() {
initData();
for (int i = 0; i < 3; i++) {
View view = LayoutInflater.from(this).inflate(R.layout.list_view_layout, null);
ListView listView = view.findViewById(R.id.list_view);
ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_expandable_list_item_1, data);
listView.setAdapter(arrayAdapter);
viewList.add(view);
}
}

private void initData() {
for (int i = 0; i < 20; i++) {
data.add("hehe->" + i);
}
}
}

我们在父布局中设置不拦截,所以ListView 消费点击触摸事件,效果如图所示,只有ListView可以滑动,左右不能滑动。

父类拦截

1
2
3
4
5
6
7
8
9
10
public class BadViewPager extends ViewPager{
public BadViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;//拦截
}
}

效果如图,我们能够左右滑动,ListView不能上下滑动

外部拦截方法

套路伪代码

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
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.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;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}

注意
根据我们已经了解到的事件分发机制,在这里
ACTION_DOWN 一定返回false,不要拦截它,否则后续ACTION_MOVE 与 ACTION_UP事件都将默认交给父View去处理。

当我们的滑动方向呈一定角度,如图

当横向滑动的距离大于纵向距离,我们就认为是ViewPager的滑动

1
2
3
4
5
6
7
8
9
10
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastInterceptX;
int deltaY = y - mLastInterceptY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {//横向滑动距离大时,拦截
intercept = true;
} else {
intercept = false;
}

break;

重写后的代码如下

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
public class BadViewPager extends ViewPager {
private int mLastInterceptX;
private int mLastInterceptY;

public BadViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}


@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercept = false;
super.onInterceptTouchEvent(ev);//初始化ViewPager的成员变量mActivePointerId
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastInterceptX;
int deltaY = y - mLastInterceptY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {//横向滑动距离大时,拦截
intercept = true;
} else {
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
}
mLastInterceptX = x;
mLastInterceptY = y;
return intercept;
}
}

在ACTION_DOWN中调用了 super.onInterceptTouchEvent(ev);
目的是为了初始化ViewPager的成员变量mActivePointerId。mActivePointerId默认值为-1,只有初始化后,MOVE时才能控制界面滑动,否则ViewPager会认为这个事件已经被子View给消费了,然后break掉,接下来的滑动操作也就不执行了。

在ViewPager中的OnTouchEvent的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public boolean onTouchEvent(MotionEvent ev) {
...
switch (action & MotionEventCompat.ACTION_MASK) {
case MotionEvent.ACTION_MOVE:
if (!mIsBeingDragged) {
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) {
// A child has consumed some touch events and put us into an inconsistent
// state.
needsInvalidate = resetTouch();
break;
}
//具体的滑动操作...
}
...
break;
...
}
...
}

效果如图

内部拦截方法

套路伪代码
子View的dispatchTouchEvent方法的伪代码:

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
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此类点击事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}

mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}

父类重写onInterceptTouchEvent,不拦截ACTION_DOWN

1
2
3
4
5
6
7
8
9
public boolean onInterceptTouchEvent(MotionEvent event) {

int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}

实际代码如下,自定义子类继承ListView。

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
public class SubListView extends ListView {
private int mLastX;
private int mLastY;

public SubListView(Context context, AttributeSet attrs) {
super(context, attrs);
}

@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:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {//横向距离大,父类拦截
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
}

修改布局list_view_layout

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.com.myapplication.test.SubListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>

重写BadViewPager的onInterceptTouchEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class BadViewPager extends ViewPager {

public BadViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}


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

效果如图

外部拦截方法比内部拦截更加简单,建议使用外部拦截方法来解决滑动冲突的问题。

懂得了什么是事件分发机制和如何解决滑动冲突,下一篇,我们开始自定义下拉刷新组件。

0%