深入聊聊Android事件分发机制

在Android开发的过程中,自定义控件一直是我们绕不开的话题。而在这个话题中事件分发机制也是其中的重点和疑点,特别是当我们处理控件嵌套滑动事件时,正确的处理各个控件间事件分发拦截状态,可以实现更炫酷的控件动画效果。

一、事件分发机制介绍

关于Android事件分发,我们主要分ViewGroup和View两个事件处理部分进行介绍,主要研究在处理事件过程中关注最多的三个方法dispatchTouchEventonInterceptTouchEventonTouchEvent,在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;
    }
事件分发

先说结论再细分析:

  1. 事件是由其父视图向子视图传递,如图为A->B->C
  2. 如果当前控件需要拦截该事件,则在onInterceptTouchEvent方法中返回true,但真正决定是否处理事件是在onTouchEvent方法中,也就是说如果此时onTouchEvent方法返回了false,则此控件也表示不处理该事件,交由父控件的onTouchEvent方法来判断处理。如图:当事件由A分发至B,B在其onInterceptTouchEvent方法中返回true表示要拦截该事件,此时事件将不会再传给C,但在B的onTouchEvent方法中返回了false,表示不处理该事件,则事件以此向上传递交由A控件的onTouchEvent方法处理。即onInterceptTouchEvent负责对事件进行拦截,拦截成功后交给最先遇到onTouchEvent返回true的那个view进行处理。
  3. 一旦控件确定处理该事件,则后续事件序列也会交由该控件处理,同时该控件的onInterceptTouchEvent方法将不再调用。
  4. 由于View没有onInterceptTouchEvent方法,在其dispatchTouchEvent方法中调用onTouchEvent方法处理事件,如果返回false则表示事件不作处理。同时其ACTION_MOVE、ACTION_UP不会得到响应。
  5. 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 onInterceptTouchEventonTouchEvent方法返回true

事件种类 A(ViewGroup) B(ViewGroup) C(View)
onInterceptTouchEvent false true
onTouchEvent false true false

当BonInterceptTouchEventonTouchEvent返回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());

    }

重点在对onInterceptTouchEventonTouchEvent方法的处理,我们在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从而直到滑动结束。

好了,总的实现就这么多,希望可以加深对事件分发机制的理解~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容