事件,如:onTouchEvent、onClick、onLongClick等。
事件通常重要的有三种:MotionEvent.ACTION_DOWN / ACTION_MOVE / ACTION_UP
事件的响应原理:最广泛应用的就是监听、回调,进而形成了事件响应的过程。
要了解View的事件分发机制,首先,我们要熟悉dispatchTouchEvent()和onTouchEvent()两个函数,这两个函数都是View的函数,要理解View事件的分发机制,只要清楚这两个函数就基本上清楚了。
- dispatchTouchEvent(): 此函数负责事件的分发,你只需要记住当触摸一个View控件,首先会调用这个函数就行,在这个函数体里决定将事件分发给谁来处理。
- onTouchEvent(MotionEvent event): 此函数负责执行事件的处理,负责处理事件。
参数event为手机屏幕触摸事件封装类的对象,其中封装了该事件的所有信息,例如触摸的位置、触摸的类型以及触摸的时间等。该对象会在用户触摸手机屏幕时被创建。
1 View的事件分发(以Button为例)
我们知道,View做为所有控件的父类,它本身定义了很多接口来监听触摸在View上的事件,那么当手指触摸到View时候,该响应“点击”还是”触摸”呢,就是根据dispatchTouchEvent和onTouchEvent这两个函数组合实现的,我们之下的讨论,仅对常用的“点击OnClick”和“触摸onTouch”来讨论,顺藤摸瓜,找出主线,进而搞清楚View的事件分发机制。
对于按钮,点击它一下,我们期望2种结果,第一种:它响应一个点击事件。第二种:不响应点击事件。
第一种源码: public class MainActivity extends Activity implements OnClickListener ,OnTouchListener{ private Button btnButton; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnButton=(Button) findViewById(R.id.btn); btnButton.setOnClickListener(this); btnButton.setOnTouchListener(this); } public void onClick(View v) { Log.e("View", "onClick===========>"); } public boolean onTouch(View v, MotionEvent event) { Log.e("View", "onTouch.................................."); return false; } }
第二种源码: …… // 同上 public boolean onTouch(View v, MotionEvent event) { Log.e("View", "onTouch.................................."); return true; }
结果分析:上面两处代码,第一种执行了OnClick函数和OnTouch函数,第二种执行了OnTouch函数,并没有执行OnClick函数,而且对两处代码进行比较,发现只有在onTouch处返回值true和false不同。当onTouch返回false,onClick被执行了,返回true,onClick未被执行。
为什么会这样呢?我们只有深入源码才能分析出来。
前面提到,触摸一个View就会执行dispatchTouchEvent方法去“分发”事件,既然触摸的是按钮Button,那么我们就查看Button的源码,寻找dispatchTouchEvent方法,Button源码中没有dispatchTouchEvent方法,但知道Button继承自TextView,寻找TextView,发现它也没有dispatchTouchEvent方法,继续查找TextView的父类View,发现View有dispatchTouchEvent方法,那我们就分析dispatchTouchEvent方法。主要代码如下:
public boolean dispatchTouchEvent(MotionEvent event) { if (onFilterTouchEventForSecurity(event)) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&mOnTouchListener.onTouch(this, event)) return true; // 截断,跳出 if (onTouchEvent(event)) return true; } return false; }
分析:先来看dispatchTouchEvent函数返回值,如果返回true,表明事件被处理了,反之,表明事件未被处理。
- mOnTouchListener != null,判断该控件是否注册了OnTouchListener对象的监听,
- (mViewFlags & ENABLED_MASK) == ENABLED,判断当前的控件是否能被点击(比如Button默认可以点击,ImageView默认不许点击,看到这里就了然了),
- mOnTouchListener.onTouch(this, event)这个是关键,这个调用,就是回调你注册在这个View上的mOnTouchListener对象的onTouch方法。如果你在onTouch方法里返回false,那么这个判断语句就跳出,去执行下面的程序,否则,条件成立,就直接返回了,不再执行下面的程序。
- if (onTouchEvent(event)) 这个判断很重要,能否回调OnClickListener接口的onClick函数,关键在于此,可以肯定的是,如果上面if返回true,那么就不会执行并回调OnClickListener接口的onClick函数。
接下来,我们看onTouchEvent这个函数,看它是如何响应点击事件的。主要代码如下:
public boolean onTouchEvent(MotionEvent event) { //…… // 可以点击或长按 if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { switch (event.getAction()) { case MotionEvent.ACTION_UP: if (!focusTaken) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); // 这里就是去执行OnClick()回调函数,实现点击 } } break; case MotionEvent.ACTION_DOWN: …… } return true; } return false; }
// 如果Button注册了OnClickListener protected OnClickListener mOnClickListener; public interface OnClickListener { void onClick(View v); } public void setOnClickListener(OnClickListener l) { if (!isClickable()) { setClickable(true); } mOnClickListener = l; } public boolean performClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; } return false; }
从上面主要代码可以看出onTouchEvent传参MotionEvent类型,它封装了触摸的活动事件,其中就有ACTION_DOWN、ACTION_MOVE、ACTION_UP三个事件。
我们在来看看onTouchEvent的返回值,因为onTouchEvent是在dispatchTouchEvent事件分发处理中调用的,如果onTouchEvent返回true,dispatchTouchEvent就返回true,表明事件被处理了,反之,事件未被处理。 从上面主要代码可以看出onTouchEvent传参MotionEvent类型,它封装了触摸的活动事件,其中就有ACTION_DOWN、ACTION_MOVE、ACTION_UP三个事件。
程序的关键在那个长长的if的判断里,我们发现无论switch的分支在什么地方跳出,返回都是true。这就表明,无论是三个事件中的哪一个,都会返回true。参照右图,结合上述,不难理解View的分发机制了。
2 ViewGroup的事件分发
ViewGroup事件分发机制较View的稍微复杂一些,不过对View的机制只要精确的理解后,仔细看过这一节,睡几觉起来,估计也就悟出来了,学习就是这么奇怪,当下理解不了或模糊的地方,只要脑子有印象,忽然一夜好像就懂了。先来看下面的一个简单布局,我们将通过例子,了解ViewGroup+View的android事件处理机制。
上图所示:黑色为线性布局LinearLayout,深灰色为相对布局RelativeLayout,按钮Button三部分组成。RelativeLayout为LinearLayout的子布局,Button为RelativeLayout的子布局。以下RelativeLayout简称(R),LinearLayout简称(L),Button简称(B)。
经过前面讲解,我们首先知道这样两件事情:
A、(R)和(L)的父类是ViewGroup,(B)的父类是View。
B、dispatchTouchEvent这个函数很重要,不论是ViewGroup还是View,都由它来处理事件的消费和传递。
分析:当手指点击按钮B时,事件传递的顺序是从底向上(从外向内)传递的,也就是按照L->R->B的顺序由下往上逐层传递,响应正好相反,是自上而下。
1) L首先接收到点击事件,L的父类是ViewGroup类,并将事件传递给dispatchTouchEvent方法,dispatchTouchEvent函数中判断该控件L是否重载了onInterceptTouchEvent方法进行事件拦截:
-
- 默认返回false不拦截,那么dispatchTouchEvent方法将事件传递给R去处理(进入第2流程处理),
- 如果返回true表示当前L控件拦截了事件向其它控件的传递,交给它自己父类View的dispatchTouchEvent去处理,在父方法的dispatchTouchEvent中,将会按照前面讲的View的事件处理机制去判断,比如判断L是否重载了onTouch方法,是否可点击,是否做了监听等事件。
2) R也是ViewGroup的子类,因此与第1流程基本相似,如果onInterceptTouchEvent返回了false,表示事件将不拦截继续传递给B。
3) B是View的子类,它没有onInterceptTouchEvent方法,直接交给自己父类View的dispatchTouchEvent去处理。
总结:onInterceptTouchEvent只有ViewGroup才有,当一个控件是继承自ViewGroup而来的,那么它就可能会有子控件,因此,才有可能传递给子控件,而继承自View的控件不会有子控件,也就没有onInterceptTouchEvent函数了。通过dispatchTouchEvent分发的控件返回值True和false,如果消费了,返回True,就不再继续传递了;反之false,没有消费,如果有子控件将继续传递。
A、如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发;
B、可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法
C、子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup对其MOVE或者UP事件进行拦截;
实际应用中能解决哪些问题呢?
比如你需要写一个左侧隐藏menu,主Activity上有个Button、ListView或者任何可以响应点击的View,你在当前View上死命的滑动,菜单栏也出不来;因为MOVE事件被子View处理了~
你需要这么做:在ViewGroup的dispatchTouchEvent中判断用户是不是想显示菜单,如果是,则在onInterceptTouchEvent(ev)拦截子View的事件;自己进行处理,这样onTouchEvent就可以顺利展现出菜单栏了。
3 实例:ViewPager+ListView滑动事件拦截
在Android开发过程中,你一定会用到ViewPager这个控件,最让人头疼的就是各种滑动冲突,比如说:在ListView,SrollView中嵌套ViewPager,在作侧边栏滑动时和ViewPager的冲突,甚至还有ViewPager嵌套ViewPager的情况等等,解决起来很麻烦。这些冲突无非就是横向滑动和纵向滑动的一个冲突,而我们要解决的就是要判断将事件给父控件还是子控件处理。
import android.content.Context; import android.support.v4.view.ViewPager; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; public class ChildViewPager extends ViewPager { public ChildViewPager(Context context, AttributeSet attrs) { super(context, attrs); } public ChildViewPager(Context context) { super(context); } private float xDistance, yDistance, xLast, yLast,xDown, mLeft; // 滑动距离及坐标 归还父控件焦点 @Override public boolean dispatchTouchEvent(MotionEvent ev) { getParent().requestDisallowInterceptTouchEvent(true); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: Log.d("touch", "ACTION_DOWN"); xDistance = yDistance = 0f; xLast = ev.getX(); yLast = ev.getY(); xDown = ev.getX(); mLeft = ev.getX(); // 解决与侧边栏滑动冲突 break; case MotionEvent.ACTION_MOVE: final float curX = ev.getX(); final float curY = ev.getY(); xDistance += Math.abs(curX - xLast); yDistance += Math.abs(curY - yLast); xLast = curX; yLast = curY; if (mLeft < 100 || xDistance < yDistance) { getParent().requestDisallowInterceptTouchEvent(false); } else { if (getCurrentItem() == 0) { if (curX < xDown) { getParent().requestDisallowInterceptTouchEvent(true); } else { getParent().requestDisallowInterceptTouchEvent(false); } } else if (getCurrentItem() == (getAdapter().getCount()-1)) { if (curX > xDown) { getParent().requestDisallowInterceptTouchEvent(true); } else { getParent().requestDisallowInterceptTouchEvent(false); } } else { getParent().requestDisallowInterceptTouchEvent(true); } } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: break; } return super.dispatchTouchEvent(ev); } }
核心代码就是 getParent().requestDisallowInterceptTouchEvent(false);这句;当为false则通知父view可以拦截touch事件,由父view处理,而当为true时,则会阻止父层的View截获touch事件,这样就会返回个子view处理。
那么好,解决了这个关键性问题接下来的问题,就是要解决什么时候交给子view或是父view的问题了。当ScrollView和ListView中嵌套ViewPager的时候,多数是在作轮播的幻灯片,冲突无非就是能左右滑动,不能上下滑动的问题;而ViewPager内嵌套ViewPager这是子view不能滑动,一划就是父ViewPager滑动的问题;这两个问题上面的自定义ViewPager都能解决,解决思路是在事件分发down的时候记录xLast和yLast,然后在move的时候比较xDistance和yDistance即x轴差和y轴差,如果x轴差小于y轴差,则说明是上下滑动,此时将事件还给父view,反之左右滑动把事件交给子view。
那mLeft是干嘛用的啊?是这样的,可能我在项目中会用到侧边栏,我的用的是SlidingMenuLibrary这个开源组件,可以设置将侧边栏划出的区域,这时就会跟ViewPager冲突,都是左右滑,事件被子view吃掉了,侧边栏画不出来,所以我就加个mLeft当点击屏幕左侧边缘(即down时x<100)时,将事件还给父view这样既不影响侧边栏划出,也不影响ViewPager的滑动。同样你可以举一反三,灵活控制事件响应区域。
再给大家说一个扩展的,如果用过ViewPagerIndicatorLibrary朋友,可能会遇到,ViewPager内套ViewPager,这时我们就想能不能在内部ViewPager滑到最后一页时,父ViewPager在切换到下一页,提供一个思路:就是在事件分发时判断当前页是不是第一页或最后一页,如果是最后一页且向左滑将事件交给父view即可,以此类推。
ScrollView嵌套ListView正常分页加载显示解决方案