简单的View冲突事件处理

前几天我们App上面有这么一个需求,就是在我们发照片的地方会贴上标签,但是这个标签可以点击删除,可以长按翻转,可以移动地方,那就会涉及到事件的冲突处理,这地方会有onClick,onLongClick,onTouch,总的来说,所有的事件都由如下三个部分作为基础:

  • 按下(ACTION_DOWN)
  • 移动(ACTION_MOVE)
  • 抬起(ACTION_UP)

下面我们先从事件的实现开始来看看,最后我们一起来实现需求。
所有的操作事件首先会执行按下操作,之后的所有的操作都是以按下操作作为前提,当按下完成后,接下来可能是移动然后抬起,或者按下之后直接抬起,这一系列都是可以进行控制的。

我们知道,这些所有的触摸操作都是发生在触摸屏上,而在屏幕上与我们交互的是各种各样的试图组件View,所有的视图都继承于View,另外通过各种布局组件(ViewGroup)来对View进行布局,ViewGroup也继承于View。那么View和ViewGroup中主要有哪些方法来对这些事件进行响应哪?查看View和ViewGroup源码可知:
View.java

1
2
public boolean dispatchTouchEvent(MotionEventevent)
public boolean onTouchEvent(MotionEvent event)

ViewGroup.java

1
2
3
public boolean dispatchTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event)
public boolean onInterceptTouchEvent(MotionEvent event)

通过比对上面这几个方法可以看出,这里面有两个方法是公有的,dispatchTouchEventonTouchEvent,那么ViewGroup的第三个方法是干啥的那,不要着急,慢慢往下看。还有一个共同的地方就是他们的返回值竟然都是boolean类型,为什么会都是boolean类型,想想我们写这个博文的初衷,“事件传递”,传递嘛,就是一个接一个,那总要有结束的时候啊,这个boolean就是控制这个的,到了某一个点后是否继续往下传递,这个返回值很重要。所有的事件都是从开始传递到最后事件的消费,那么这个返回值就决定了是否继续传递,还是被拦截,或者消费了。
介绍完了返回值,接下来我们来看看这些方法的参数,都接收一个MotionEvent参数,这个参数继承于InputEvent,作用就是标记各种动作事件,刚开始说到的ACTION_DOWN,ACTION_MOVE,ACTION_UP,都是MotionEvent中定义的常量,我们就是通过这个常量来知道用户的具体操作事件。现在,我们对返回值和参数都有一定的概念了,下面来看一下这三个方法分别在什么时候处理事件。

  • dispatchTouchEvent()dispatch派遣,分发的意思,很明显这个方法就是用于事件的分发,Android中所有事件都必须经过这个方法来处理,决定是自身消费当前事件,还是继续分发给子View处理。返回true表示不继续分发,事件没有得到消费。反之,继续分发,如果是ViewGroup则分发给onInterceptTouchEvent进行判断是否拦截该事件。

  • onTouchEvent()主要用于事件的处理,返回true,则消费此次事件,返回false,则不处理,交给子View处理。

  • onInterceptTouchEvent()这是ViewGroup中独有的方法,作用就是事件的拦截,返回true,拦截当前事件,不继续分发,交给自身的onTouchEvent处理。返回false,不拦截,继续传递。为什么View没有这个方法,因为ViewGroup中可能还会有子View,而在Android中View是不可能再有View的(iOS是可以的)。

实践这三个方法的实例的代码这里就不贴了,自己写一个demo,然后重写以上方法,各种手势,打Log。看下图,一般都是按照这个流程的

画的有点丑,在sketch上面自己草草画的。
下面我们来看看多布局事件传递
多布局,也就是ViewGroup,这里要比上面多写一个onInterceptTouchEvent方法,ViewGroup中独有的,作用就是控制事件是否需要拦截。
这里就出来一个问题,一个事件是先传递到View,还是ViewGroup呐?最后直接上答案,Android中事件的传递,是从ViewGroup传递到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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
private ClickEvent mEvent;
private long pressStartTime;
private boolean isMove = false;
private Handler outerHandler;
/**
* 当前触摸点相对于屏幕的坐标
*/

private int mCurrentInScreenX;

private int mCurrentInScreenY;


/**
* 触摸点按下时的相对于屏幕的坐标
*/

int prevRawX, prevRawY;
private static final long LONG_PRESS_TIME = 500;
/**
* 长按线程
*/

private LongPressedThread mLongPressedThread;
this.setOnTouchListener(new OnTouchListener() {
float prevX, prevY;
@Override
public boolean onTouch(final View v, final MotionEvent event) <
//获取相对屏幕的坐标,即以屏幕左上角为原点
mCurrentInScreenX = (int) event.getRawX();
mCurrentInScreenY = (int) event.getRawY();
switch (event.getAction() & MotionEvent.ACTION_MASK) <
case MotionEvent.ACTION_MOVE: <
//取消注册的长按事件
outerHandler.removeCallbacks(mLongPressedThread);
//处理移动逻辑
return true;
&#125;
case MotionEvent.ACTION_UP: <
if (!isMove()) <
if (Calendar.getInstance().getTimeInMillis() - pressStartTime <= LONG_PRESS_TIME) <
outerHandler.removeCallbacks(mLongPressedThread);
if (mEvent != null) <
//点击事件
mEvent.onClick();
&#125;
&#125;
&#125;
return true;
&#125;
case MotionEvent.ACTION_DOWN: <
pressStartTime = System.currentTimeMillis();
prevRawX = (int) event.getRawX();
prevRawY = (int) event.getRawY();
mLongPressedThread = new LongPressedThread();
outerHandler.postDelayed(mLongPressedThread, LONG_PRESS_TIME);

&#125;
&#125;
return true;
&#125;
&#125;);

public class LongPressedThread implements Runnable <
@Override
public void run() <
//长按事件
mEvent.onLongClick();
&#125;
&#125;
public void setClickEvent(ClickEvent event) <
this.mEvent = event;
&#125;
private boolean isMove() <
if (Math.abs(prevRawX - mCurrentInScreenX) <= 5 && Math.abs(prevRawY - mCurrentInScreenY) <= 5) <
isMove = false;
&#125; else <
isMove = true;
&#125;
return isMove;
&#125;
public interface ClickEvent <
void onClick();

void onLongClick();
&#125;

首先我们添加了onTouchListener事件,重写onTouch事件,上面已经说过,任何事件都是从Down开始的,源码中已经告诉我们,如果没有down,后续的所有事件将毫无意义。
上面我们在Down中纪录了按下的时间,开启一个线程然后延迟500ms(长按触发的时间)后执行,然后再UP的时候我判断如果按住的时间没有超过了500ms那么代码长按事件没有触发,然后执行点击事件的逻辑,同时删除掉Handler队列中的处理长按事件的线程,如果执行了Move事件也需要删除Handler队列里面的长按处理线程。
我们只要在想用的地方这样就好了

1
2
3
4
5
6
7
8
9
10
11
View.setClickEvent(new CustomView.ClickEvent() <
@Override
public void onClick() <
//处理你的点击逻辑
&#125;

@Override
public void onLongClick() <
//处理你的长按逻辑
&#125;
&#125;);

综上所述
我上高中的时候,我数学老师最爱说的一句话就是综上所述,直接出答案。

  • Android 中的事件传递展现的是一种层级关系,从上往下,从ViewGroup到View。
  • 事件传递有三个重要的方法,dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent,前两个是View和ViewGroup共有的,最后一个是ViewGroup特有的。他们分别负责的事情就是,事件的分发,事件的处理,事件的拦截。
  • onTouch事件优先于onClick执行,onTouchEvent要后于dispatchTouchEvent方法调用。因为onTouch在事件分发完在dispatchTouchEvent中调用,而onClick在事件处理方法onTouchEvent中调用。

夜已经深了,今天也比较累了,可能就是让这个搞得吧,但是最终还是solve了,大家晚安。GoodNight。

坚持原创技术分享,您的支持将鼓励我继续创作!