Android 事件小結

這篇文章不適合小白直接來閱讀
原創文章,轉載須要本人贊成android

背景

我以前一直從事 Android App 開發,如今跑去作 APM 了,在公司悠閒了好一段時間,後來發現本身對 Android 本來很瞭解的一些東西都遺忘掉了,意識到仍是以前沒有寫博客致使的,因此如今想把本身回憶的一些東西記錄整理下來。 寫的時候參考了一些書籍和一些開源框架(主要是 ObservableScrollView),同時也動手寫了一個封裝了事件處理細節的框架來方便工做和學習。git

談我對 Android 事件機制的理解

  1. 事件從 Activity 開始進入,經過頂級 View DecorView 依次遞歸向子 View 分發
  2. 若是 down 事件被 View 消耗,那麼後續事件會一直傳遞到該 View, 若是該 View 不處理後續事件,那麼後續事件依然會傳遞進來,只是事件會往父容器拋 ( 這裏的 View 若是是 ViewGroup,ViewGroup 的子 View 消耗了 down,能夠等價的認爲事件被該 ViewGroup 消耗了,也就是後續的事件會傳遞給該 ViewGroup, 由該 ViewGroup 的事件分發邏輯處理 ), 反過來,若是 View 不消耗 down 事件,那麼後續事件壓根不會傳進來。 ( 因此自定義View 的時候,若是須要處理事件,down 事件要消耗,同時自定義的 ViewGroup 若是想將後續的事件分發給子 View 處理,那麼 down 事件不能攔截)
  3. ViewGroup 若是決定攔截事件,那麼後續事件不會再通過 onInterceptTouchEvent 方法,只要事件通過子 View 而且子 View 沒有調用 requestDisallowInterceptTouchEvent(true) 方法, 那麼每次都會調用 onInterceptTouchEvent 方法,換句話說,若是有事件衝突,咱們的子 View 不想讓父容器處理事件,那麼能夠在 子 View 中調用這個方法,阻止父容器攔截事件
  4. 子容器不處理事件,那麼事件會繼續交給父容器的 onTouchEvent 處理(2 補充)
  5. View、ViewGroup 雖然機制有所不一樣,可是事件分發上邏輯等價,整個分發過程邏輯遞歸 (ViewGroup 子 View 消耗了事件,邏輯上等價於該 ViewGroup 消耗了事件,只是調用誰的 onTouchEvent 問題)

滑動事件衝突

這裏的事件衝突表示爲整個完整的事件本應該交給父容器處理確被容器裏面的 View 消費了,反過來同樣。注意完整的事件說明不純在整個事件流不純在一會是父容器處理一會是子 view 處理的狀況,這種狀況後面細說,也就是說肯定是誰消耗事件了,那麼後續的事件就只由他處理(若是他不處理不了,那麼事件會往上拋,可是事件流仍是會通過他)github

套路框架

父容器處理事件

public boolean onInterceptTouchEvent(MotionEvent ev){
   switch (ev.getActionMasked()){

        case MotionEvent.ACTION_DOWN:
            return false; // 讀者想一想這裏若是返回 true 會怎麼樣,想不起來再往上看
        case MotionEvent.ACTION_MOVE:
            if (事件交給我處理){
                return true;
            }
             break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP: // 想一想爲啥不對這兩個事件進行攔截
             break; 

   }

   return super.onInterceptTouchEvent(ev);
}

子 View 處理事件

public boolean dispatchTouchEvent(MotionEvent ev){ //建議在這裏處理,若是該 View 是個 ViewGroup,可能不會調用到 onTouchEvent() 方法。該方法保證能在事件傳遞進來後都能接收到。
    switch (ev.getActionMasked()){
        case MotionEvent.ACTION_DOWN:
               parent.requestDisallowInterceptTouchEvent(true);
           break;
        case MotionEvent.ACTION_MOVE:
            if (處理事件){
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;

    }
    return super.dispatchTouchEvent(ev);
}

複雜場景的自定義分發事件

問題分析

考慮這種狀況,ViewGroup 嵌套 View, 根據手勢的滑動,整個事件流,在不一樣狀況下回交給 ViewGroup 或者 View 處理,系統的 Android 事件分發機制表名,若是 View 的事件被攔截,那麼就不會再調用 onInterceptTouchEvent 方法,後續全部事件會交給 ViewGroup 處理,這時候問題就來了,若是後續的 move 事件知足某個條件,須要交給 view 處理,那這樣就無法實現了。ide

問題解決

上面這種狀況的解決,靠 Android 系統默認的分發機制是實現不了的,解決方案就只有本身來給 View 分發事件,當 ViewGroup 攔截事件後,後續的事件會交給 ViewGroup 的 onTouchEvent 處理, 因此咱們要在 onTouchEvent 事件裏面在特定條件下向 View 手動調用 dispatchTouchEvent 方法分發事件。學習

一個簡單的封裝了事件分發的框架

思路
  1. 一個事件流由 down 事件開始,接着若干 move 事件,最後以 cancel 或者 up 事件結束。
  2. 一個事件流由 viewGroup 來進行分發 (onTouchEvent)。
  3. 分發給 ViewGroup 處理和 View 處理的事件是一個完整的事件,也就說因爲整個事件流分發給 ViewGroup 或者 View 處理的時候可能只是一個事件流的一部分(沒有從 down 事件以 cancel 或者 up 事件結束),因此在分發開始的時候咱們要本身建立 down 事件和 cancel 事件來讓事件流完整。
  4. 後續事件再也不交給 View 處理,咱們以 cancel 事件結束( 沒用 up)
代碼
// ViewGroup 事件處理回調
    public interface LinkageListener {
        public boolean shouldIntercept(MotionEvent event, boolean isDown, float diffx, float diffy);
        public void eventDown(MotionEvent event);
        public void eventMove(MotionEvent event, float diffx, float diffy);
        public void eventCancelOrUp(MotionEvent event);
    }

    //事件分發處理類
    public abstract class LinkageBaseFrameLayout extends FrameLayout{
    public LinkageBaseFrameLayout(Context context) {
        super(context);
    }

    public LinkageBaseFrameLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public LinkageBaseFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    boolean intercepting;
    PointF firstPoint;
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (getLinkageListener() != null){
            switch (ev.getActionMasked()){
                case MotionEvent.ACTION_DOWN://若是攔截,那麼後續事件不會傳遞給 子view,而且不會子啊調用這個方法
                    firstPoint = new PointF(ev.getX(), getY());
                    intercepting = getLinkageListener().shouldIntercept(ev, true, 0, 0);
                    dispatchChildDownEvent = !intercepting;
                    dispatchThisDownEvent = intercepting;
                    cancelChildEvent = false;
                    cancelThisEvent = false;
                    firstPoint = new PointF(ev.getX(), ev.getY());
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (firstPoint == null){// 防護代碼, 可能父容器是自定義的 View,好比本框架(QAQ) down 事件別攔截,其餘事件被分發過來了
                        firstPoint = new PointF(ev.getX(), ev.getY());
                    }
                    float diffx = ev.getX() - firstPoint.x; //這裏 diff 採用上一次和這一次的偏移或者到起始點的偏移, 這裏第二種
                    float diffy = ev.getY() - firstPoint.y;
                    intercepting = getLinkageListener().shouldIntercept(ev, false, diffx, diffy);
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    break;
            }

            return intercepting;
        }
        return super.onInterceptTouchEvent(ev);
    }


    boolean dispatchChildDownEvent;
    boolean dispatchThisDownEvent;
    boolean cancelChildEvent;
    boolean cancelThisEvent;
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (getLinkageListener() != null){
            switch (ev.getActionMasked()){
                case MotionEvent.ACTION_DOWN:
                    if (intercepting){// 事件只給 viewGroup 處理
                        getLinkageListener().eventDown(ev);
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (firstPoint == null){// 防護代碼
                        firstPoint = new PointF(ev.getX(), ev.getY());
                    }
                    float diffx = ev.getX() - firstPoint.x;// 這裏是事件開始,到當前事件座標的偏移,固然也能夠改爲上一個事件到當前事件的偏移
                    float diffy = ev.getY() - firstPoint.y;
                    intercepting = getLinkageListener().shouldIntercept(ev, false, diffx, diffy);
                    if (intercepting){ // 攔截,說明接下來的事件給本 viewgrop 處理
                        // 1. 給前面分發給子 View 的事件建立 cancel 事件,使事件完整
                        if (!cancelChildEvent && dispatchChildDownEvent){ // 上一個事件是給子 View 處理,而且沒有傳遞結束事件
                            MotionEvent cancelEvent = obtainMotionEvent(ev, MotionEvent.ACTION_CANCEL);
                            dispathEvent(ev);
                            dispathEvent(cancelEvent);
                            cancelChildEvent = true;
                        }
                        dispatchChildDownEvent = false;
                        cancelThisEvent = false;


                        //2. 當前分配給 ViewGroup 的事件,必需要 down 事件開頭(注意偏移是從 down 事件開始)
                        if (!dispatchThisDownEvent){
                            dispatchThisDownEvent = true;
                            MotionEvent downEvent = obtainMotionEvent(ev, MotionEvent.ACTION_DOWN);
                            getLinkageListener().eventDown(downEvent);
                            firstPoint.set(ev.getX(), ev.getY());
                            diffx = 0;
                            diffy = 0;

                        }else {

                            getLinkageListener().eventMove(ev, diffx, diffy);
                        }


                    }else{// 不攔截,事件分發給子 View
                        //1. 取消 ViewGroup 事件, 能夠思考不要這段代碼怎麼樣
                        if (!cancelThisEvent && dispatchThisDownEvent){
                            MotionEvent cancelEvent = obtainMotionEvent(ev, MotionEvent.ACTION_CANCEL);
                            getLinkageListener().eventMove(ev, diffx, diffy);
                            getLinkageListener().eventCancelOrUp(cancelEvent);
                            cancelThisEvent = true;
                        }

                        cancelChildEvent = false;
                        dispatchThisDownEvent = false;
                        //2. 子 View 沒有分發 down 事件,要補充 down 事件
                        if (!dispatchChildDownEvent){//沒有分發 down 事件
                            dispatchChildDownEvent = true;
                            MotionEvent downEvent = obtainMotionEvent(ev, MotionEvent.ACTION_DOWN);
                            dispathEvent(downEvent);
                        }else {

                            dispathEvent(ev);
                        }

                    }

                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                     if (!cancelChildEvent && dispatchChildDownEvent){
                         dispathEvent(ev);
                         cancelChildEvent = true;
                     }

                    if (intercepting){
                        if (!dispatchThisDownEvent){
                            dispatchThisDownEvent = true;
                            MotionEvent downEvent = obtainMotionEvent(ev, MotionEvent.ACTION_DOWN);
                            getLinkageListener().eventDown(downEvent);
                        }
                        getLinkageListener().eventCancelOrUp(ev);
                    }

                    break;
            }
            return true;
        }
        return super.onTouchEvent(ev);
    }

    private MotionEvent obtainMotionEvent(MotionEvent ev, int action) {
        MotionEvent downEvent = MotionEvent.obtain(ev);
        downEvent.setAction(action);
        return downEvent;
    }

    Rect hintRect = new Rect();

    /**
     * 向子 View 分發事件,子 View 不必定能收到完整的事件流,所以最好手指滑動的時候,要在同一個 View 上
     *
     * @param event
     */
    protected void dispathEvent(MotionEvent event){
        View child = null;
        boolean consume = false;
        MotionEvent temp = null;
        for (int i = 0; i < getChildCount(); i++){
            child = getChildAt(i);
            child.getHitRect(hintRect);
            if (hintRect.contains((int)event.getX(), (int)event.getY())){ // 判斷點擊點的座標是否在該 view 上
                temp = MotionEvent.obtain(event);
                temp.offsetLocation(-hintRect.left, -hintRect.top); // event 座標修改成相對 子 view
                consume |= child.dispatchTouchEvent(temp); //分發事件
                if (consume) break;
            };
           }
    };


    /**
     * 本 viewgroup 處理的事件
     *
     * @return
     */
    public abstract LinkageListener getLinkageListener();
}

結尾

經過這種事件分發邏輯,我寫了一個下拉放大的組件分享給你們GitHub:EventDispatchspa

項目效果
code

相關文章
相關標籤/搜索