在Android开发的过程中,自定义控件一直是我们绕不开的话题。而在这个话题中事件分发机制也是其中的重点和疑点,特别是当我们处理控件嵌套滑动事件时,正确的处理各个控件间事件分发拦截状态,可以实现更炫酷的控件动画效果。
一、事件分发机制介绍
关于Android事件分发,我们主要分ViewGroup和View两个事件处理部分进行介绍,主要研究在处理事件过程中关注最多的三个方法dispatchTouchEvent
、onInterceptTouchEvent
、onTouchEvent
,在ViewGroup和View对三个方法的支持如下图所示:
事件种类 | ViewGroup | View |
---|---|---|
dispatchTouchEvent | 有 | 有 |
onInterceptTouchEvent | 有 | 无 |
onTouchEvent | 有 | 有 |
在Android中,当用户触摸界面时系统会把产生一系列的MotionEvent
,通过ViewGroup 的dispatchTouchEvent
方法开始向下分发事件,在dispatchTouchEvent
方法中,会调用onInterceptTouchEvent
方法,如果该方法返回true,表明当前控件拦截了该事件,此后事件交由该控件处理并不再调用该控件的onInterceptTouchEvent
方法。最后交由该控件的onTouchEvent
方法对事件进行处理。如果当前控件在onInterceptTouchEvent
方法中返回false,表示不拦截该控件,之后交由其子控件进行判断是否对事件进行拦截处理。可以用如下伪代码来对其进行处理:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
if (onInterceptTouchEvent(event)) {
consume = onTouchEvent(event);
} else {
consume = child.dispatchTouchEvent(event);
}
return consume;
}
先说结论再细分析:
- 事件是由其父视图向子视图传递,如图为A->B->C
- 如果当前控件需要拦截该事件,则在
onInterceptTouchEvent
方法中返回true,但真正决定是否处理事件是在onTouchEvent
方法中,也就是说如果此时onTouchEvent
方法返回了false,则此控件也表示不处理该事件,交由父控件的onTouchEvent
方法来判断处理。如图:当事件由A分发至B,B在其onInterceptTouchEvent
方法中返回true表示要拦截该事件,此时事件将不会再传给C,但在B的onTouchEvent
方法中返回了false,表示不处理该事件,则事件以此向上传递交由A控件的onTouchEvent
方法处理。即onInterceptTouchEvent
负责对事件进行拦截,拦截成功后交给最先遇到onTouchEvent
返回true的那个view进行处理。- 一旦控件确定处理该事件,则后续事件序列也会交由该控件处理,同时该控件的
onInterceptTouchEvent
方法将不再调用。- 由于View没有
onInterceptTouchEvent
方法,在其dispatchTouchEvent
方法中调用onTouchEvent
方法处理事件,如果返回false则表示事件不作处理。同时其ACTION_MOVE、ACTION_UP不会得到响应。- View的
OnTouchListener
优先于onTouchEvent
方法执行,如果OnTouchListener
方法返回true,那么View的dispatchTouchEvent
方法就返回true。而后则onTouchEvent
方法得不到执行,同时因为onClick
方法在onTouchEvent
方法的ACTION_UP中调用,onClick
方法也得不到执行。
情况一、A\B\C onInterceptTouchEvent
onTouchEvent
均返回false
事件种类 | A(ViewGroup) | B(ViewGroup) | C(View) |
---|---|---|---|
onInterceptTouchEvent | false | false | 无 |
onTouchEvent | false | false | false |
当A、B、C同时返回false时,事件传递为A(onInterceptTouchEvent) -->B(onInterceptTouchEvent) -->C(onTouchEvent)-->B(onTouchEvent) -->A(onTouchEvent),也就是事件从A传至C时,都没有拦截和处理事件,则事件再次向上传递调用B和A的onTouchEvent
方法。
看下打印的结果:
情况二、B onInterceptTouchEvent
方法返回true
事件种类 | A(ViewGroup) | B(ViewGroup) | C(View) |
---|---|---|---|
onInterceptTouchEvent | false | true | 无 |
onTouchEvent | false | false | false |
当BonInterceptTouchEvent
返回true时表示拦截了事件,C控件就无法响应该事件。
情况三、B onInterceptTouchEvent
、 onTouchEvent
方法返回true
事件种类 | A(ViewGroup) | B(ViewGroup) | C(View) |
---|---|---|---|
onInterceptTouchEvent | false | true | 无 |
onTouchEvent | false | true | false |
当BonInterceptTouchEvent
、onTouchEvent
返回true时表示拦截处理了事件,C控件就无法响应该事件,同时事件在B的onTouchEvent
之后将不再向上传递,随后事件将不再调用其onInterceptTouchEvent
方法。
情况四、C onTouchEvent
方法返回true
事件种类 | A(ViewGroup) | B(ViewGroup) | C(View) |
---|---|---|---|
onInterceptTouchEvent | false | false | 无 |
onTouchEvent | false | false | true |
当ConTouchEvent
返回true时表示处理了该事件,之后事件就交由C控件处理,同时事件在C的onTouchEvent
之后将不再向上传递。
情况五、A onInterceptTouchEvent
方法返回true
事件种类 | A(ViewGroup) | B(ViewGroup) | C(View) |
---|---|---|---|
onInterceptTouchEvent | true | false | 无 |
onTouchEvent | false | false | false |
当AonInterceptTouchEvent
返回true时表示拦截了事件,之后事件就交由A的onTouchEvent
方法处理,B、C就无法响应该事件。如果AonTouchEvent
方法返回false,其ACTION_MOVE、ACTION_UP事件不会得到响应。
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "A --- onTouchEvent");
switch (event.getAction()){
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "A --- onTouchEvent :ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "A --- onTouchEvent :ACTION_UP");
break;
}
return false;//super.onTouchEvent(event);
}
二、实现侧滑删除效果
运用上面的知识学习,我们来实现一下简单的侧滑删除效果吧~
其核心代码主要在于对事件的拦截和处理上:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// boolean intercepter = false;
Log.e("TAG", "onInterceptTouchEvent: "+ev.getAction());
boolean intercepter = false;
if (isMoving)
intercepter = true;
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
downX = (int) ev.getX();
downY = (int) ev.getY();
if (mVelocityTracker == null)
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.clear();
break;
case MotionEvent.ACTION_MOVE:
moveX = (int) ev.getX();
moveY = (int) ev.getY();
Log.e("TAG", "getScrollX: "+getScrollX() );
if (Math.abs(moveX - downX) > 0){
intercepter = true;
//Log.e("TAG","onInterceptTouchEvent: ");
//scrollBy(moveX - downX,0);
}else {
intercepter = false;
}
downX = moveX;
downY = moveY;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
intercepter = false;
break;
}
//scrollBy(45,0);
return intercepter;//
//super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.e("TAG", "onTouchEvent: "+ev.getAction() );
mVelocityTracker.addMovement(ev);
switch (ev.getAction()){
case MotionEvent.ACTION_MOVE:
moveX = (int) ev.getX();
moveY = (int) ev.getY();
mVelocityTracker.computeCurrentVelocity(1000);
Log.e("TAG", "getScrollX: "+getScrollX() );
if (getScrollX()+downX - moveX>=0 && getScrollX()+downX - moveX <= view1.getMeasuredWidth()){
scrollBy(downX - moveX,0);
}
isMoving = true;
downX = moveX;
downY = moveY;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
Log.e("TAG1", "getXVelocity: "+mVelocityTracker.getXVelocity() );
Log.e("TAG1", "getYVelocity: "+mVelocityTracker.getYVelocity() );
//
if (getScrollX()>=view1.getMeasuredWidth()/2 || mVelocityTracker.getXVelocity() < -ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity()){
//scrollTo(view1.getMeasuredWidth(),0);
open();
}else {
//scrollTo(0,0);
close();
}
mVelocityTracker.clear();
mVelocityTracker.recycle();
mVelocityTracker = null;
break;
}
return true;//super.onTouchEvent(ev);
}
这里整个父布局继承自ViewGroup
,在onMeasure
中测量子控件大小:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
}
在onFinishInflate
方法中获取各个子控件:
@Override
protected void onFinishInflate() {
super.onFinishInflate();
view = getChildAt(0);
view1 = getChildAt(1);
if (mScroller == null)
mScroller = new Scroller(getContext());
view.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View mViewm, MotionEvent mMotionEventm) {
if (mMotionEventm.getAction() == MotionEvent.ACTION_UP
&& isOpen){
close();
}
if (mMotionEventm.getAction() == MotionEvent.ACTION_DOWN){
if (mOnChangeMenuListener!=null){
mOnChangeMenuListener.onStartTouch();
}
}
return false;
}
});
}
并在onLayout
方法中布局子控件:
@Override
protected void onLayout(boolean mBm, int mIm, int mIm1, int mIm2, int mIm3) {
if (getChildCount()!=2){
throw new IllegalArgumentException("必须包含两个子控件");
}
Log.e("TAG", "onLayout:getWidth "+view.getWidth() );
view.layout(0,0,view.getMeasuredWidth(),view.getMeasuredHeight());
view1.layout(view.getMeasuredWidth(),0,view.getMeasuredWidth()+view1.getMeasuredWidth(),view1.getMeasuredHeight());
}
重点在对onInterceptTouchEvent
和onTouchEvent
方法的处理,我们在onInterceptTouchEvent
中处理是否拦截该事件。如果手指是向左滑动,则表示用户在进行侧滑删除操作,则拦截该事件,需要注意的是,一旦拦截了该事件,之后事件将不调用该控件的onInterceptTouchEvent
方法,所以我们将具体的处理逻辑放在onTouchEvent
方法中,该方法返回true表示处理该事件,此后事件都由dispatchTouchEvent
方法交由onTouchEvent
方法处理。在onTouchEvent
方法中调用scrollBy
方法实现控件左右滑动,从而实现类似侧滑删除效果。
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}else {
isMoving = false;
}
}
为使滑动效果更自然,用Scroller
在手指抬起的时候控制控件打开或者闭合,Scroller
的使用也很简单,抬起时调用其startScroll
方法并刷新界面,在控件computeScroll
方法中判断是否滑动完毕并刷新界面,在invalidate
方法中会调用computeScroll
从而直到滑动结束。
好了,总的实现就这么多,希望可以加深对事件分发机制的理解~