本文基于Android事件分发机制完全解析,带你从源码的角度彻底理解和从Android源码的角度理解应用开发(1)-Touch机制进行编写的,加入自己的理解。方便自己理清思路和便于以后的查看。
①首先我们先写一个小Demo,Demo源码下载, 如下图所示
布局代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:lingchen="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.yitong.mytouchevent.MainActivity">
<com.yitong.mytouchevent.view.TouchView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_blue_dark"
android:gravity="center"
lingchen:viewName="Out"
>
<com.yitong.mytouchevent.view.TouchView
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@android:color/holo_green_dark"
android:gravity="center"
lingchen:viewName="Center">
<com.yitong.mytouchevent.view.TouchView
android:id="@+id/main_in"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_red_dark"
lingchen:viewName="In"/>
</com.yitong.mytouchevent.view.TouchView>
</com.yitong.mytouchevent.view.TouchView>
</RelativeLayout>
当In_View设置了android:clickable=”true”时,当点击了In_View时。事件处理经过如下:
=============================================
ACTION_DOWN: Out dispatchTouchEvent
ACTION_DOWN: Center dispatchTouchEvent
ACTION_DOWN: In dispatchTouchEvent
ACTION_DOWN: In onTouchEvent
=============================================
ACTION_MOVE: Out dispatchTouchEvent
ACTION_MOVE: Center dispatchTouchEvent
ACTION_MOVE: In dispatchTouchEvent
ACTION_MOVE: In onTouchEvent
=============================================
ACTION_UP: Out dispatchTouchEvent
ACTION_UP: Center dispatchTouchEvent
ACTION_UP: In dispatchTouchEvent
ACTION_UP: In onTouchEvent
=============================================
当In_View没有设置android:clickable=”true”时,当点击了In_View时。事件处理经过如下:
=============================================
ACTION_DOWN: Out dispatchTouchEvent
ACTION_DOWN: Center dispatchTouchEvent
ACTION_DOWN: In dispatchTouchEvent
=======================
ACTION_DOWN: In onTouchEvent ACTION_DOWN: Center onTouchEvent
ACTION_DOWN: Out onTouchEvent =============================================
②接着我们测试下当view的touch和onClick事件的关系,比较触发点击事件时,那个事件先执行
mainIn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(Constants.name + TAG, "onTouch_ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(Constants.name + TAG, "onTouch_ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(Constants.name + TAG, "onTouch_ACTION_UP");
break;
}
return false;
}
});
mainIn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(Constants.name + TAG, "onClick");
}
});
当In_View设置了android:clickable=”true”时,当点击了In_View时。会先执行onTouch方法,再执行onClick
=============================================
MainActivity: onTouch_ACTION_DOWN
MainActivity: onTouch_ACTION_MOVE
MainActivity: onTouch_ACTION_UP
=======================
MainActivity: onClick =============================================
当In_View的onTouch的返回值返回为true,则不会执行onClick事件了
mainIn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
...
return false;
}
});
mainIn.setOnClickListener(new View.OnClickListener() {
...
});
=============================================
MainActivity: onTouch_ACTION_DOWN
MainActivity: onTouch_ACTION_MOVE
MainActivity: onTouch_ACTION_UP
=============================================
几点重要总结:
只要你触摸到了任何一个控件,就一定会调用该控件的dispatchTouchEvent方法。首先在dispatchTouchEvent中最先执行的就是onTouch方法,因此onTouch肯定是要优先于onClick执行的。如果在onTouch方法里返回了true,就会让dispatchTouchEvent方法直接返回true,不会再继续往下执行。onClick的调用是在onTouchEvent(event)方法中。
如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。
当In_View的onTouch的返回值返回为false,并且In_View没有设置了android:clickable=”true”时,只会去执行onTouch中的ACTION_DOWN方法
MainActivity: onTouch_ACTION_DOWN
onTouch和onTouchEvent区别:这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。
==================至此关于事件分发机制介绍完毕=====================
接下几种情况带你回顾事件分发机制:
①为什么图片轮播器里的图片使用Button而不用ImageView?
②为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?
③touch监听器没被调用到?
④设置了onClickListener后,点击View没有反应?
⑤点击两下View才调用onClickListener的bug?
=========让我们梳理一下============
1. Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View的。
2. 在ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。
3. 子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。
===============又是一个分水岭=======================
接下来我们就实战一下:
①写一个不能左右滑动的ViewPager,我们知道ViewPager是可以左右滑动的,当我们的ViewPager中嵌套一个轮播图(又一个ViewPager)。这时如果我们滑动轮播图,但是ViewPager把事件给拦截了。
public class NoScrollViewPager extends ViewPager {
public NoScrollViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NoScrollViewPager(Context context) {
super(context);
}
/** * 表示事件是否拦截, 返回false表示不拦截, 可以让嵌套在内部的viewpager相应左右划的事件 */
@Override
public boolean onInterceptTouchEvent(MotionEvent arg0) {
return false;
}
/** * 重写onTouchEvent事件,什么都不用做 */
@Override
public boolean onTouchEvent(MotionEvent arg0) {
return false;
}
}
②我们使用手机的新闻客户端,在主页面总体用的是一个ViewPager,可以查看娱乐、社会、实时…,当滑动到第一个主题(最左边)时,我们想再次滑动就可以打开菜单栏。这时单纯的ViewPager就不能满足。
public class HorizontalViewPager extends ViewPager {
public HorizontalViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
public HorizontalViewPager(Context context) {
super(context);
}
/** * 事件分发, 请求父控件及祖宗控件是否拦截事件 */
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (getCurrentItem() == 0) {// 第一个主题,再次滑动打开菜单栏
getParent().requestDisallowInterceptTouchEvent(false);// 请求父控件拦截我的事件,让父控件去处理该事件
} else {// 如果不是第一个主题,ViewPager自身处理
getParent().requestDisallowInterceptTouchEvent(true);// 请求父控件不要拦截我的事件
}
return super.dispatchTouchEvent(ev);
}
}
③上面第一个不能左右滑动的NoScrollViewPager显然功能太弱,我们想要当里面的ViewPager滑动到第一个主题和最后一个主题的时候能够触发外面的ViewPager。
public class TopNewsViewPager extends ViewPager {
int startX;
int startY;
public TopNewsViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TopNewsViewPager(Context context) {
super(context);
}
/** * 事件分发, 请求父控件及祖宗控件是否拦截事件 1. 右划, 而且是第一个页面, 需要父控件拦截 2. 左划, 而且是最后一个页面, 需要父控件拦截 * 3. 上下滑动, 需要父控件拦截 */
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);// 不要拦截,
// 这样是为了保证ACTION_MOVE调用
startX = (int) ev.getRawX();
startY = (int) ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int endX = (int) ev.getRawX();
int endY = (int) ev.getRawY();
if (Math.abs(endX - startX) > Math.abs(endY - startY)) {// 左右滑动
if (endX > startX) {// 右划
if (getCurrentItem() == 0) {// 第一个页面, 需要父控件拦截
getParent().requestDisallowInterceptTouchEvent(false);
}
} else {// 左划
if (getCurrentItem() == getAdapter().getCount() - 1) {// 最后一个页面,
// 需要拦截
getParent().requestDisallowInterceptTouchEvent(false);
}
}
} else {// 上下滑动
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
}