说到Android中的Touch事件,对于绝大多数初级Android开发者来说都是一知半解。但是彻底弄清楚Touch事件的分发机制又是一件势在必行的事,因为在实际开发中有太多的滑动冲突、点击事件的冲突等等需要我们去解决。比如:我们都遇到过ViewPager里面嵌套ListView,当我们手指滑动时是该左右滑动ViewPager还是上下滑动ListView呢? ViewPager里面再嵌套一个ViewPager,那我们怎么来控制啥时候是内部的ViewPager滑动啥时候是外部的ViewPager滑动呢? 又比如,我们经常听到onTouch、onTouchEvent、onClick,这三者之间又有什么区别和联系呢? 为了弄清楚这些问题,为了彻底搞懂事件分发机制,接下来我们就从源码和小demo入手来探索Touch事件这片神秘的地界。
首先,说到Touch事件分发机制,那我们就要弄清楚什么事Touch事件?我们通常说的Touch事件,它其实并不简简单单是一个事件,它是由1个DOWN事件,n个MOVE事件,1个UP事件构成的,当然在这里n可以为零。也就是说,一个完整的Touch事件,必须有且只有1个DOWN事件和1个UP事件,中间还可以穿插若干个MOVE 事件。
那接下来就来弄清楚我们经常听到的onTouch、onTouchEvent、onClick,这三者之间又有什么区别和联系呢?要弄清楚这个问题我们还是要借助源码和一个非常简单的小demo,这个demo中只有一个MainActivity,MainActivity中只有一个继承Button的自定义View——TestView,我们给该TestView设置onTouch监听,下面是相关代码:
/**
* Created by leevi on 16/9/1.
*/
public class TestView extends Button{
private Context mContext;
public TestView(Context context) {
this(context,null);
mContext = context;
}
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("TestView", "onTouchEvent!!!!!!");
return super.onTouchEvent(event);
}
}
public class MainActivity extends AppCompatActivity implements View.OnTouchListener {
private TestView mTestView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTestView = (TestView) findViewById(R.id.test);
mTestView.setOnTouchListener(this);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("MainActivity","onTouch!!!!!!");
return false;
}
}
1、我们先来探究一下onTouch方法和onTouchEvent方法的关联,首先是当onTouch方法返回false的情况,我们在log中能得到如下信息:
【一脸懵逼状!!!】先打印onTouch后打印onTouchEvent,而且还分别打印了两次??? 首先我们要知道,这是一个完整的touch事件,所以必定有DOWN和UP事件,那么打印两次也就不足为奇了,至于为什么先打印onTouch后打印onTouchEvent呢? 我们暂且猜测onTouch的优先级高于onTouchEvent,究竟是不是这样呢? 那就让我们将onTouch的返回值改成true再来验证一下。
当我们将onTouch的返回值改成true之后出现了如下情况:
果然不出我们所料,只打印了onTouch方法,因为onTouch方法中返回true将该事件消费了所以就不会走onTouchEvent方法了。
2、接下来我们就来探究onTouch方法和和onClick方法的区别和联系。还是一样的,我们先给TestView注册点击事件,然后先让onTouch方法返回false,当然这一切的前提都是onTouchEvent返回super.onTouchEvent(MotionEvent event)的基础上。
public class MainActivity extends AppCompatActivity implements View.OnTouchListener, View.OnClickListener {
private TestView mTestView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTestView = (TestView) findViewById(R.id.test);
mTestView.setOnTouchListener(this);
mTestView.setOnClickListener(this);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("MainActivity","onTouch!!!!!!");
return false;
}
@Override
public void onClick(View v) {
Log.d("MainActivity","onClick!!!!!!");
}
}
onTouch返回false的情况下,会打印出如下log:
我们可以看出,先打印了两次onTouch和onTouchEvent方法,最后打印了一次onClick。这里说明了两个问题:1、onClick方法只在UP事件中才能触发。 2、onClick方法的优先级比onTouch方法低。 第一个结论当然不用去验证,我们将onTouch中的返回值改成true验证一下第二个结论。可以看到如下log:
很明显,只打印了两次onTouch,没有打印onClick。所以我们可以得出结论,onClick方法o的优先级比onTouch方法低
3、最后咱们再来探究一下onTouchEvent和onClick方法的关系。上面我们比较onTouch方法和onClick方法的时候,onTouchEvent方法都是返回super.onTouchEvent(MotionEvent event);那我们先将onTouchEvent的返回值修改成true和false看看会发生什么情况:
首先是改成返回值为true,会得到如下的log:
沃肏!!!!!!什么情况?居然只打印两次onTouch和onTouchEvent,没有打印onClick了。。。。。一脸懵逼!那我们再看看返回值为false的情况:
沃肏TOO!!!!只打印一次onTouch和onTouchEvent(这个问题咱们稍后再议)?? 居然也没有打印onClick。那就说明onClick方法和那个super.onTouchEvent(MotionEvent event)有着密不可分的关系。Button的父类是TextView,我看了TextView的onTouchEvent方法,并没有什么发现,那么我们就只能去看TextView的父类——View的onTouchEvent方法。
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
.............
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}//<span style="color:#ff0000;">注意这里,performClick方法。 翻译过来是执行click的意思</span>
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
..........
}
return true;
}
return false;
}
大家注意我红色注释的部分,有个performClick方法,那我们就能猜测是在这里执行的click事件。那接下来我们就看看performClick方法的源码:
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);//<span style="color:#ff0000;">在这里调用onClick方法。</span>
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
我们可以发现是在我做红色注释的地方调用onClick方法。 马丹!找了这么久终于找到源头了,我们也能得到结论,继承View的子类自由党onTouchEvent返回super.onTouchEvent时才能响应点击事件。
经过以上的小demo及对源码的追踪,我们总算是弄清楚了onTouch、onTouchEvent、onClick这三者的区别和联系。
接下来我们再来看看,整个Touch事件的传递流程。
从上图我们可以看出,Touch事件最先传递给Activity,再由Activity交给Window,再交给顶级View(DecorView),再由DecorView交给容器ViewGroup最后再交给最底层的View。
在这个流程中我们会看到Touch事件传递过程中的三个最重要的方法:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。这三个方法的作用分别是用来分发、拦截和处理Touch事件。在接下来的分析中,我们会详细介绍这三个方法。
首先是Activity得到Touch事件,我们看一下Activity的源码,看它接收到Touch事件后是怎么处理的?
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
从以上源码我们可以看到,首先事件交给Activity附属的window进行分发,如果返回true整个事件结束。如果返回false,意味着没人处理,所有View的onTouchEvent都返回false,那么Activity的onTouchEvent会被调用。
接下来,咱们看Window怎么处理Touch事件,通过源码我们可以发现,Window是一个抽象类,他的superDispatchTouchEvent()方法也是个抽象方法,那么我们必须去看Window的唯一实现类,PhoneWindow的源码:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
源码实现非常简单,就是将Touch事件交给DecorView的superDispatchTouchEvent方法来处理。那咱么就来看看DecorView的superDispatchTouchEvent方法里面的具体实现吧。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
里面直接是返回super.dispatchTouchEvent(event),我们通过看DecorView的源码可以知道,DecorView的父类是ViewGroup,那么就是按照ViewGroup的原则来处理Touch事件。
在下一篇《深入源码理解AndroidTouch事件分发机制(下篇)》我们会深入ViewGroup和View的dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent方法的源码来分析Touch事件的传递。