Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android事件處理分發機制的總結:一(事件處理)

Android事件處理分發機制的總結:一(事件處理)

編輯:關於Android編程

從View的dispatchTouchEvent可以看出,事件最終的處理無非是交給TouchListener的onTouch方法或者是交由onTouchEvent處理,由於onTouch默認是空實現,由程序員來編寫邏輯,那麼我們來看看onTouchEvent事件。
首先我們來看一個比較簡單的onTouchEvent的處理,那就是View,我們知道,View只能響應click和longclick,不具備滑動等特性。

public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;
    //先判斷標示位是否為disable,也就是無法處理事件。
    if ((viewFlags&ENABLED_MASK)==DISABLED) {
        if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }//如果是UP事件,並且狀態為按壓,取消按壓。
        //系統源碼解釋:雖然是disable,但是還是可以消費掉觸摸事件,只是不觸發任何click或者longclick事件。
        //根據是否可點擊,可長按來決定是否消費點擊事件。
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }
    if (mTouchDelegate != null) {
        //先檢查觸摸的代理對象是否存在,如果存在,就交由代理對象處理。
       // 觸摸代理對象是可以進行設置的,一般用於當我們手指在某個View上,而讓另外一個View響應事件,另外一個View就是該View的事件代理對象。
        if (mTouchDelegate.onTouchEvent(event)) {//如果代理對象消費了,則返回true消費該事件
            return true;
        }
    }
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            //如果是可點擊或者長按的標識位執行下面的邏輯,這些標志位可以設置,也可以設置了對應的listener後自動添加
            //因為作為一個View,它只能單純的接受處理點擊事件,像滑動之類的復雜事件普通View是不具備的。
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP://處理Up事件
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;//是否包含臨時按壓狀態
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {//如果本身處於被按壓狀態或者臨時按壓狀態
                    //臨時按壓狀態會在下面的Move事件中說明
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        //如果它可以獲取焦點,並且可以通過觸摸來獲取焦點,並且現在不是焦點,則請求獲取焦點,因為一個被按壓的View理論上應該獲取焦點
                        focusTaken = requestFocus();
                    }
                    if (prepressed) {
                    //如果是臨時按壓,則設置為按壓狀態,PFLAG_PREPRESSED是一個非常短暫的狀態,用於在某些時候短時間內表示Pressed狀態,但不需要繪制
                        setPressed(true);//設置為按壓狀態,是因為臨時按壓不會繪制,這個時候強制繪制一次,確保用戶能夠看見按壓狀態
                   }
                    if (!mHasPerformedLongPress) {
                        //是否執行了長按事件,還沒有的話,這個時候可以移除長按的回調了,因為UP都已經觸發,說明從按下到UP的時間不足以觸發longPress
                        //至於longPress,會在Down事件中說明
                        removeLongPressCallback();
                        if (!focusTaken) {//如果是焦點狀態,就不會觸摸click,這是為什麼呢?因為焦點狀態一般是交給按鍵處理的,
                        //pressed狀態才是交給觸摸處理,如果它是焦點,那麼它的click事件應該由按鍵來觸發
                            if (mPerformClick == null) {    //封裝一個Runnable對象,這個對象中實際就調用了performClick();
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {//向消息隊列發生該runnabel,如果發送不成功,則直接執行該方法。
                                performClick();//這個方法內部會調用clickListner
                            }
                            //為什麼不直接執行呢?如果這個時候直接執行,UP事件還沒執行完,發送post,可以保障在這個代碼塊執行完畢之後才執行
                        }
                    } (mUnsetPressedState == null) {//仍舊是創建一個Runnabel對象,執行setPressed(false)
                        mUnsetPressedState = new UnsetPressedState();
                    }
                    if (prepressed) {
//如果是臨時按壓狀態,之前的Down和move都還未觸發按壓狀態,只在up時設置了,這個狀態才剛剛繪制,為了保證用戶能看到,發生一個64秒的延遲消息,來取消按壓狀態。                        
                        postDelayed(mUnsetPressedState,
                            ViewConfiguration.getPressedStateDuration());
                        //這是一個64毫秒的短暫時間,這是為了讓這個按壓狀態持續一小段時間,以便手指離開時候,還能看見View的按壓狀態
                    } else if (!post(mUnsetPressedState)) {//如果不是臨時按壓,則直接發送,發送失敗,則直接執行
                        mUnsetPressedState.run();
                    if
                    }
                    removeTapCallback();
                    //移除這個callBack,這個callBack內部就是把臨時按壓狀態設置成按壓狀態,因為這個已經沒必要了,手指已經up了
                }
                break;
            case MotionEvent.ACTION_DOWN:
                mHasPerformedLongPress = false;
                //按下事件把長按事件執行的變量設置為false,代表還沒執行長按,因為才按下,表示新的一個長按事件可以開始計算了
                if (performButtonActionOnTouchDown(event)) {
                    //先把這個事件交由該方法,該方法內部會判斷是否為上下文的菜單按鈕,或者是否為鼠標右鍵,如果是就彈出上下文菜單。
                    //現在有些手機的上下文菜單按鈕也是在屏幕觸屏上的
                    break;
                }
                //這個方法會一直往上找父View,判斷自身是否在一個可以滾動的容器中
                boolean isInScrollingContainer = isInScrollingContainer();
                //如果是在一個滾動的容器中,那麼按壓事件將會被推遲一段時間,如果這段時間內,發生了Move,那麼按壓狀態講不會被顯示,直接滾動父視圖
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED; //先添加臨時的按壓狀態,該狀態表示按壓,但不會繪制
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                        //創建一個runnable對象,這個runnable內部會取消臨時按壓狀態,設置為按壓狀態,並啟動長按的延遲事件
                    }
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    //向消息機制發生一個64毫秒的延遲時間,該事件會取消臨時按壓狀態,設置為直接按壓,並啟動長按時間的計時
                } else {
                    //如果不在一個滾動的容器中,則直接設置按壓狀態,並啟動長按計時
                    setPressed(true);
                    checkForLongClick(0);
                    //長按事件就是向消息機制發送一個runnable對象,封裝的就是我們在lisner中的代碼,延遲500毫秒執行,也就是說長按事件在我們按下的時候發送,在up的時候檢查一下執行了嗎?如果沒執行,就取消,並執行click
                }
                break;
            case MotionEvent.ACTION_CANCEL: //如果是取消事件,那就好辦了,把我們之前發送的幾個延遲runnable對象給取消掉
                setPressed(false);      //設置為非按壓狀態
                removeTapCallback();    //取消mPendingCheckForTap,也就是不用再把臨時按壓設置為按壓了
                removeLongPressCallback();    //取消長按事件的延遲回調
                break;
            case MotionEvent.ACTION_MOVE:    //move事件
                final int x = (int) event.getX();    //取觸摸點坐標
                final int y = (int) event.getY();
                // 用於判斷是否在View中,為什麼還要判斷呢?
                //這是因為父View是在Down事件中判斷是否在該View中的,如果在,以後的Move和up都會傳遞過來,不再進行范圍判斷
                if (!pointInView(x, y, mTouchSlop)) {
                //mTouchSlop是一個常量,不同的手機值不一樣,dpi越高,值大,一般數值為8,也就是說,就算你的落點超出了View的8像素位置,也算在View中。
                //是因為人的手指觸摸點比較大,有可能你感覺點在某個控件的邊緣,但是實際落點已經超出這個View,所以這裡給了8像素的范圍
                    removeTapCallback();//如果在范圍外,就移除這些runnable回調
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                //如果是按壓狀態,就取消長按,設置為非按壓狀態,為什麼這個時候取消呢,因為在Down的時候,我們可以知道,只有是按壓狀態,才會設置長按
                        removeLongPressCallback();
                        setPressed(false);
                    }
                }
                break;
        }
        return true;    //至此,可以返回true,消費該事件
    }
    return false;    //如果不可點擊,也不可長按,則返回false,因為View只具備消費點擊事件
}

從上面的代碼我們總結一下View對觸摸事件的處理:
**1、是否為diabale,如果是,直接根據是否設置了click和longclick來返回。
2、是否設置了觸摸代理對象,如果有,把事件傳遞給觸摸代理對象,交由其處理,如果消費了,直接返回
3、是否為click或者longclick的,如果是,返回true,不是返回false。
而View對click和longclick的處理如下:
1、Down:
1.判斷是否可以觸摸上下文菜單。
2.是否在可以滑動的容器中,如果是先設置臨時按壓,再發送一個延遲消息把臨時按壓改為按壓,並發送一個延遲500毫秒的事件去執行長按代碼
3.如果不在滾動容器中,直接設置按壓狀態,並發送一個延遲500毫秒的事件去執行長按代碼。
2、Move:
1、取觸摸點坐標判斷是否在View中(額外增加了8像素的范圍)
2、如果在,不用做任何事。
3、如果不在,取消臨時按壓到按壓回調,取消長按延遲回調,設置為非按壓狀態
3、Up
1、判斷是否為按壓或者臨時按壓狀態
2、如果不是,不做任何處理
3、如果是先判斷其是否可以獲取焦點,然後請求焦點。
4、如果是臨時按壓狀態,設置臨時按壓狀態為按壓狀態。保證界面被繪制成按壓狀態,讓用戶可以看見。
5、如果長按回調還未觸發,取消長按回調,如果不是焦點狀態,觸發click事件。
6、如果是臨時按壓狀態,發送一個延遲取消按壓狀態的,保證按壓狀態持續一段時間,讓用戶可見。
7、如果不是臨時按壓狀態,直接發送消息取消按壓狀態。發送失敗,直接取消按壓狀態。
8、取消把臨時按壓設置按壓的回調。**
從中我們知道View的onTouchEvent主要處理了click和longclick事件,當按下時,向消息機制發送一個延遲500毫秒的長按回調事件,當移動時候判斷是否移出了View的范圍,超出則取消事件。當離開時,判斷長按事件是否觸發了,如果沒觸發且不是焦點,就觸發click事件。
在這裡最繞的就是臨時按壓和按壓狀態,臨時按壓是為了處理滑動容器的,讓處於滑動容器中,按下時,我們先設置的是臨時按壓,持續64毫秒,是為了判斷接下來的時間內是否發生了move事件,如果發生了,將不會再出發按壓狀態,這樣不會讓用戶看到listView滾動時,item還處於按壓狀態。在離開時,我們再次判斷是否處於臨時按壓,如果是在64毫秒內觸發了down和up,說明按壓狀態還沒來得急繪制,則強制設置為按壓狀態,保證用戶能看到,並在取消回調的方法上加上64毫秒的延遲
之前看了View的onTouch,知道裡面處理了點擊事件,那麼我們再來看看ScrollView來看看滑動事件的處理:

public boolean onTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
        //如果是down事件,並且觸摸到邊緣,就不處理EdgeFlags代表是否為邊緣,其值是1/2/4/8。代表上下左右
        return false;
    }
    if (mVelocityTracker == null) {
    //這是一個追蹤觸摸事件,並計算速度的幫助類,實現原理就是用三個數組分別記錄每次觸摸的x/y和時間
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(ev);

    final int action = ev.getAction();
    switch (action & MotionEvent.ACTION_MASK) {//與上ff,去掉高位有關多點的信息
        case MotionEvent.ACTION_DOWN: {//如果是down
            final float y = ev.getY();//獲取y坐標
            if (!(mIsBeingDragged = inChild((int) ev.getX(), (int) y))) {//判斷是否開始拖動
            //原理就是判斷落點是否在child中,ScrollView只能由一個child,如果在,返回true,反之false
            //也就是說落點在child中,就是准備開始拖動,不在,就直接返回,這可能是因為設置了padding之類的緣故造成的
                return false;
            }
            if (!mScroller.isFinished()) {//判斷滾動是否完成
                mScroller.abortAnimation();//如果沒完成,停止滾動
                //對應上一次用戶手指離開時候處理fling狀態,這次按下手指,直接停止滾動
            }
            //記錄y坐標,以便下次事件來對比
            mLastMotionY = y;
            mActivePointerId = ev.getPointerId(0);//記住多點的id,下次取值時只取該點的
            break;
        }
        case MotionEvent.ACTION_MOVE:
            if (mIsBeingDragged) {//可以看出,如果down的時候落點在child外,則以後就算滑進了child也不處理
                //根據上次記錄的多點id,找到對應的點,取y值
                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                final float y = ev.getY(activePointerIndex);
                final int deltaY = (int) (mLastMotionY - y);//計算位移
                mLastMotionY = y;//重新記錄y值
                scrollBy(0, deltaY);//滾動指定的距離,這也說明了ScrollView只具備縱向滑動
            }
            break;
        case MotionEvent.ACTION_UP:
            if (mIsBeingDragged) {//如果是離開事件
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);//計算最後1秒鐘內的速度,並給定一個最大速度進行限制
                //這個最大速度是根據屏幕密度不同而不同的,所以大家也沒事別使勁滑動屏幕,因為有這個最大速度限制
                //獲取y方向的速度
                int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
                if (getChildCount() > 0 && Math.abs(initialVelocity) > mMinimumVelocity) {
                //如果有子View,並且計算出來的y的速度比最小速度要大,執行fling狀態
                //手指滑動的方向和屏幕移動的方向是相反的,所以這裡加-
                    fling(-initialVelocity);
                }
                mActivePointerId = INVALID_POINTER;//給mActivePointerId重新賦值為-1,防止下次事件找到了錯誤的點
                mIsBeingDragged = false;//恢復默認值
                if (mVelocityTracker != null) {//清空速度計算幫助類
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
            }
            break;
        case MotionEvent.ACTION_CANCEL:
            if (mIsBeingDragged && getChildCount() > 0) {//判斷條件,只有這2個條件成立,才會發生滾動事件,下面的值才會被改變,才需要恢復默認
                mActivePointerId = INVALID_POINTER;
                mIsBeingDragged = false;
                if (mVelocityTracker != null) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
            }
            break;
        case MotionEvent.ACTION_POINTER_UP://多點觸摸時,不是最後一個點離開
            onSecondaryPointerUp(ev);
            break;
    }
    return true;
}
//用於應對先按下1點,然後按下2點,1點離開後,2點仍能繼續滑動的邏輯
private void onSecondaryPointerUp(MotionEvent ev) {
    final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
            MotionEvent.ACTION_POINTER_INDEX_SHIFT;//首先對高位進行與操作,然後右移8位,獲取其高位代表index的值
    final int pointerId = ev.getPointerId(pointerIndex);//取出該點的id
    if (pointerId == mActivePointerId) {//如果這個id對應的就是第一個按下的點
    //理論上pointerIndex應該是0,所以用第二個按下的點,即1index的點代替
        final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
        mLastMotionY = ev.getY(newPointerIndex);//取出新點的y坐標
        mActivePointerId = ev.getPointerId(newPointerIndex);//記錄新點的id
        if (mVelocityTracker != null) {//清空之前存入的MotionEvent,也就是說最後的速度只計算該點產生的
            mVelocityTracker.clear();
        }
    }
}

通過以上分析,我們得出以下知識:
**1.在down事件的時候先判斷觸摸是否處於邊緣,如果是,則不處理
2.在down事件中判斷落點是否在子View中,如果不在,不處理
3.在down事件中判斷是否仍在滑動,如果是,先停止
4.記錄第一個按下點的索引值
5.每次事件都記錄住當前的y值
6.在move事件中通過記錄的索引值找到對應的點,獲取y坐標
7.與上一次y坐標進行比對,scrollBy兩次的差值
8.在up事件的時候計算每秒的速度,並且有最大速度進行限制,當計算的速度大於系統默認的最小速度時,只想fling
9.up和cancel事件還原變量為默認值
10.如果為多點離開,進行多點離開的處理
11.該處理方式時:如果離開的是第一個按下的點,那麼由第二個按下的點代替其進行y值偏移計算的基點,並清空速度計算的幫助類,重新記錄MotionEvnet**

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved