第18天 Android Touch事件學習 5 點擊與長按原理

來源:互聯網
上載者:User

 

 

在第一篇文章中又點擊事件的一個例如引入事件的學習,之後第二篇文章尋找一下點擊事件最終是在什麼地方觸發的,發現是在onTouchEvent方法中,第三篇和第四篇總結了一下onTouchEvent的參數MotionEvent對象的常用屬性getAction() 與 getX(), getY()。

前幾篇是打下基礎,現在可以基於這些知識分析下View.onTouchEvent也就是之前第二篇文章中尋找到的發現點擊事件觸發的地方,View類是所有視圖的基類,也就是如果子類不覆寫此方法的話,觸屏事件都是交由View.onTouchEvent處理。

 

分析的源碼是Andorid 4.0 (Android 14)原因之前也解釋過,這種通過的功能因為各種版本變化不會太大,View.onTouchEvent有130行肯定不會直接粘貼到blog上然後一行行的解釋是什麼意思,打算按照先總後分的形式整理。這篇文章並不是我想寫這麼長,實在是方法本身源碼就很多,。

 

一、onTouchEvent整體結構

 

    /**     * Implement this method to handle touch screen motion events.     *     * @param event The motion event.     * @return True if the event was handled, false otherwise.     */    public boolean onTouchEvent(MotionEvent event) {        final int viewFlags = mViewFlags;        // 當前視圖處于禁用狀態        if ((viewFlags & ENABLED_MASK) == DISABLED) {            if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) {                // 如果抬起手指清除掉按下狀態                mPrivateFlags &= ~PRESSED;                // 當前顯示還是按下狀態,所以重刷一下                refreshDrawableState();            }            // 應該是當前視圖處理當前觸摸事件的,但是因為指定為禁用狀態,            // 所以還是消耗當前事件,只是不做任何處理。            // 這樣處理符合邏輯,因為使用者當前觸摸的是這個視圖,雖然被設定為不觸發任何處理。            // A disabled view that is clickable still consumes the touch            // events, it just doesn't respond to them.            return (((viewFlags & CLICKABLE) == CLICKABLE ||                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));        }        // 把當前視圖的事件交由其他視圖處理。        if (mTouchDelegate != null) {            // 事件雖然傳遞給了當前視圖,            // 但是如果其他視圖通過設定mTouchDelegate增大觸摸地區            // 並且當前觸摸點在其他視圖的擴大地區內。            if (mTouchDelegate.onTouchEvent(event)) {                // 交由擴大觸摸地區的那個視圖處理                // 並消耗此次觸摸事件                return true;            }        }        // 如果當前視圖是點擊或者長按狀態        if (((viewFlags & CLICKABLE) == CLICKABLE ||                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {            switch (event.getAction()) {                // 點擊與長按重要處理的地方,之後著重分析這塊                ......            }            // 是點擊或者長按,當前方法反饋與處理使用者操作            return true;        }        // 當前視圖不處理,由事件傳遞機制在找其他匹配的視圖        // 具體如何尋找之後在分析        return false;    }

在看針對各種動作的判斷與處理之前在整理一下以上源碼的結構:

 

1. 當前視圖是否處于禁用狀態(如果是抬起手指,清理掉按下狀態)
2. 是否在其他視圖的擴大範圍內(通過TouchDelegate實現)
3. 如果以上兩者都不成立,並且當前視圖處於點擊或者長按狀態
4. 如果以上3者都不符合條件,返回false表明當前視圖不消耗此次觸摸事件

 

以上源碼會涉及到以下知識,可以查看這些變數的注釋,就不一一貼出源碼了:

1. mViewFlags是全域變數,用於存放檢視狀態資訊。

2. TouchDelegate 設定視圖的點擊地區(增大或者縮小可點擊地區)

3. DISABLED 當前視圖禁用狀態。通過使用View.setEnabled(false)設定視圖為禁用狀態

4. CLICKABLE 可點擊。通過使用View.setClickable(true)設定

5. LONG_CLICKABLE 可長按。通過使用View.setLongClickable(true)設定

 

 

二、點擊與長按手勢處理

在第三篇文章《Android Touch事件學習 3 區分各種手勢基礎知識》中分析過,Android通過各種ACTION(動作)來區分使用者行為,然後現在需要通過系統提供的這些ACTION來判斷是點擊還是長按,根據之前的經驗ACTION的執行是有先後順序的依次是:ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL,且當使用者手指按下時觸發ACTION_DOWN,手指抬起時觸發ACTION_UP,移動時被攔截觸發ACTION_CANCEL,這三個都是僅會觸發依次,手指移動是觸發ACTION_MOVE會根據手指移動事件執行0到多次。 View.onTouchEvent中並不是一這個次序來擺放源碼的,但是下面會依次按照上面的順序分塊進行解釋。

 

三、 ACTION_DOWN

 

case MotionEvent.ACTION_DOWN:mHasPerformedLongPress = false;// 1. 必須先滿足是滑鼠右鍵 或是 手寫筆第一個按鈕,才會返回trueif (performButtonActionOnTouchDown(event)) {break;}// 2. 當前視圖是否可滾動(例如:當前是ScrollView視圖,返回true)// Walk up the hierarchy to determine if we're inside a scrolling container.boolean isInScrollingContainer = isInScrollingContainer();// For views inside a scrolling container, delay the pressed feedback for// a short period in case this is a scroll.if (isInScrollingContainer) {// 滾動視圖內,先不設定為按下狀態,因為使用者之後可能是滾動操作// 不是此次分析的重點,感興趣可以自己瞭解下mPrivateFlags |= PREPRESSED;if (mPendingCheckForTap == null) {    mPendingCheckForTap = new CheckForTap();}postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());} else {// Not inside a scrolling container, so show the feedback right away// 不在滾動視圖內,立即反饋為按下狀態mPrivateFlags |= PRESSED;// 重新整理為按下狀態refreshDrawableState();// 3. 與 4.checkForLongClick(0);}break;


 

以上標註了1,2,3,4,如果感覺注釋已經看懂了可以直接忽略以下關於這幾點的源碼注釋,直接跳到下一個動作ACTION_MOVE

 

1. performButtonActionOnTouchDown方法源碼

 

    /**     * Performs button-related actions during a touch down event.     *     * @param event The event.     * @return True if the down was consumed.     *     * @hide     */    protected boolean performButtonActionOnTouchDown(MotionEvent event) {        // 如果是滑鼠右鍵,手寫筆第一個按鈕(詳見BUTTON_SECONDARY常量注釋)        if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {            if (showContextMenu(event.getX(), event.getY(), event.getMetaState())) {                return true;            }        }        return false;    }

 

 

2. isInScrollingContainer方法源碼
    /**     * @hide     */    public boolean isInScrollingContainer() {        ViewParent p = getParent();        while (p != null && p instanceof ViewGroup) {            if (((ViewGroup) p).shouldDelayChildPressedState()) {                return true;            }            p = p.getParent();        }        return false;    }

是否在滾動容器中,詳見ViewGroup.shouldDelayChildPressedState(),注釋說處於相容問題這個方法預設返回true,但是在所有不可滾動的子類。
例如LinearLayout等所有不會滾動的視圖都會覆寫此方法並返回false。

 

3. checkForLongClick方法源碼

 

    private void checkForLongClick(int delayOffset) {        // 當前視圖可以執行長按操作        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {            mHasPerformedLongPress = false;            if (mPendingCheckForLongPress == null) {                // 4 與 5                mPendingCheckForLongPress = new CheckForLongPress();            }            // 4.             mPendingCheckForLongPress.rememberWindowAttachCount();            // 延遲一段時間把runnable添加到訊息佇列            postDelayed(mPendingCheckForLongPress,                    ViewConfiguration.getLongPressTimeout() - delayOffset);        }    }

 

 

ViewConfiguration.getLongPressTimeout() Android 4.0 源碼值是500

 


4.CheckForLongPress類源碼

 

    class CheckForLongPress implements Runnable {        private int mOriginalWindowAttachCount;        public void run() {            if (isPressed() && (mParent != null)                    && mOriginalWindowAttachCount == mWindowAttachCount) {                // 5. 觸發執行長按事件                if (performLongClick()) {                    mHasPerformedLongPress = true;                }            }        }        public void rememberWindowAttachCount() {            mOriginalWindowAttachCount = mWindowAttachCount;        }    }

 

 

這裡很容易搞不清楚,這裡竟然還判斷
mOriginalWindowAttachCount == mWindowAttachCount
明明在CheckForLongPress.rememberWindowAttachCount()中進行賦值的,解釋一下mOriginalWindowAttachCount是CheckForLongPress的內部變數,而mWindowAttachCount是全域變數,CheckForLongPress的對象是延時發送到訊息佇列的,也就是說如果在延遲期間mWindowAttachCount改變這個判斷條件還是過不了,那這個變數都在哪裡會發生改變呢?不貼源碼了,會在View.dispatchAttachedToWindow方法中進行累加,而此方法會在ViewGroup.addView時調用,也就是再次期間添加視圖的話,不會滿足條件。


5.performLongClick方法源碼

 

    /**     * Call this view's OnLongClickListener, if it is defined. Invokes the context menu if the     * OnLongClickListener did not consume the event.     *     * @return True if one of the above receivers consumed the event, false otherwise.     */    public boolean performLongClick() {        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);        boolean handled = false;        if (mOnLongClickListener != null) {            // 觸發執行長按事件            handled = mOnLongClickListener.onLongClick(View.this);        }        if (!handled) {            handled = showContextMenu();        }        if (handled) {            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);        }        return handled;    }


 

四、ACTION_MOVE

 

 case MotionEvent.ACTION_MOVE:final int x = (int) event.getX();final int y = (int) event.getY();// Be lenient about moving outside of buttons// 1. 觸摸點是否在當前視圖內if (!pointInView(x, y, mTouchSlop)) {// Outside button// 如果手指移出視圖地區// 2. 去除視圖輕觸狀態removeTapCallback();// 如果當前是按下狀態if ((mPrivateFlags & PRESSED) != 0) {// Remove any future long press/tap checks// 3. 移除還沒有執行的長按與輕觸檢測removeLongPressCallback();// Need to switch from pressed to not pressed// 移除按下狀態mPrivateFlags &= ~PRESSED;// 移除後重新整理視圖refreshDrawableState();}}break;

 

 

與之前一樣,以下是上面ACTION_MOVE處理調用的方法,如果已經明白可以直接忽略以下方法與注釋,直接看ACTION_UP的分析



1. pointInView方法源碼

 

    /**     * Utility method to determine whether the given point, in local coordinates,     * is inside the view, where the area of the view is expanded by the slop factor.     * This method is called while processing touch-move events to determine if the event     * is still within the view.     */    private boolean pointInView(float localX, float localY, float slop) {        // 把觸摸點的x,y 與當前視圖的上下左右進行比較,看是否在視圖地區內        // 視圖地區的上下左右都增加slop長度,在視圖外添加的slop地區內也算點擊到視圖內了        // 目的是為了增加當前視圖的可點擊地區,避免在視圖邊界處,即使移動一丁點就會        // 系統可能就會認為是在兩個視圖間切換        return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&                localY < ((mBottom - mTop) + slop);    }    // 在View視圖構造器中對mTouchSlop進行初始化    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();


 

2.removeTapCallback源碼

 

    /**     * Remove the tap detection timer.     */    private void removeTapCallback() {        // 移除輕觸探測定時器        // 此Runnable是在滾動視圖是才會建立        if (mPendingCheckForTap != null) {            // 去除預按下狀態            mPrivateFlags &= ~PREPRESSED;            // 從訊息佇列中刪除mPendingCheckForTap            removeCallbacks(mPendingCheckForTap);        }    }

3.removeLongPressCallback方法源碼

 

 

    /**     * Remove the longpress detection timer.     */    private void removeLongPressCallback() {        // 移除長按探測定時器        if (mPendingCheckForLongPress != null) {          removeCallbacks(mPendingCheckForLongPress);        }    }


 

五、ACTION_UP

 

case MotionEvent.ACTION_UP:boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;if ((mPrivateFlags & PRESSED) != 0 || prepressed) {// 當前視圖處於預按下或者按下狀態// 如果失去焦點,擷取焦點狀態// take focus if we don't have it already and we should in// touch mode.boolean focusTaken = false;if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {focusTaken = requestFocus();}if (prepressed) {// The button is being released before we actually// showed it as pressed.  Make it show the pressed// state now (before scheduling the click) to ensure// the user sees it.// 如果是與按下狀態,抬起手指前設定為按下狀態mPrivateFlags |= PRESSED;refreshDrawableState();   }// 如果沒有執行長按if (!mHasPerformedLongPress) {// 移除長按探測定時器// This is a tap, so remove the longpress checkremoveLongPressCallback();// Only perform take click actions if we were in the pressed state// 只有按下狀態才會執行點擊事件if (!focusTaken) {// Use a Runnable and post this rather than calling// performClick directly. This lets other visual state// of the view update before click actions start.if (mPerformClick == null) {// 用於執行點擊操作mPerformClick = new PerformClick();}// 使用post到runnable發送到訊息佇列的目的是:// 訊息佇列是依次執行,把之前post到隊列的runnable執行完// 才會執行當前runnable,以保證在之前所有狀態都處理完後執行if (!post(mPerformClick)) {// 如果執行不成功,必須保證觸發點擊事件// 所以直接調用PerformClick類內部調用的觸發事件方法performClick();}}}if (mUnsetPressedState == null) {// 1. 清除按下狀態mUnsetPressedState = new UnsetPressedState();}if (prepressed) {// 如果是預按下狀態,過段事件後在發送到訊息佇列postDelayed(mUnsetPressedState,ViewConfiguration.getPressedStateDuration());} else if (!post(mUnsetPressedState)) {// If the post failed, unpress right now// 執行失敗的話,保證視圖不會永遠處於按下狀態// 直接執行一次mUnsetPressedState.run();}// 清除輕觸removeTapCallback();}break;


 

1.UnsetPressedState 類源碼

 

    private final class UnsetPressedState implements Runnable {        public void run() {            setPressed(false);        }    }


 

六、ACTION_CANCEL

 

case MotionEvent.ACTION_CANCEL:// 清理按下狀態mPrivateFlags &= ~PRESSED;// 重新整理一下refreshDrawableState();// 清除輕觸狀態removeTapCallback();break;


 

 

ViewConfiguration是系統配置,各手機廠商可能會根據自身手機特點修改這些參數。

 

七、簡單總結

1. 滿足一些先決條件。例如:當前視圖非禁用狀態、當前視圖允許點擊或者長按(詳見 一、onTouchEvent整體結構)

之後通過系統反饋的動作來進行判斷

2. ACTION_DOWN:當前是否為滾動視圖,如果不是,當前視圖先顯示為按下狀態,且在500毫秒後執行長按操作。(詳見 三、ACTION_DOWN)

3. ACTION_MOVE:如果手指移動出當前視圖範圍內,清理以上設定的所有狀態,並且如果長按還沒有執行不會觸發。(詳見 四、ACTION_MOVE)

4. ACTION_UP:如果MOVE時沒有進行清理,且還沒有執行長按操作,執行點擊操作(詳見 五、ACTION_UP)

5. ACTION_CANCEL:清理所有狀態

 

 

 

 

 

 

 

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.