深入聊聊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從而直到滑動結束。

好了,總的實現就這麼多,希望可以加深對事件分發機制的理解~

測試Demo下載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章