Android 7.0 虛擬按鍵(NavigationBar)源碼分析 (二)之 點擊事件的實現流程

轉自:http://blog.csdn.net/kuaiguixs/article/details/78330982

第二部分: Let's go!!!

【點擊事件的實現流程】

1、初始化

    虛擬按鍵點擊效果的實現和實體按鍵相似,也是通過上報一個keyCode值,來判斷哪個按鈕被點擊。不同的是,實體按鍵的keyCode值是硬件驅動層傳遞到上層的。而虛擬按鍵的keyCode值是應用層自己定義的。
    首先來看KeyButtonView的構造函數。由此可見,最終都會調用到有三個參數的構造方法。最重要的是變量 mCode,它接收了在佈局文件中定義的 keyCode 值。

[java] view plain copy
  1. public KeyButtonView(Context context, AttributeSet attrs) {  
  2.     this(context, attrs, 0);  
  3. }  
  4.   
  5. public KeyButtonView(Context context, AttributeSet attrs, int defStyle) {  
  6.     super(context, attrs);  
  7.   
  8.     TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.KeyButtonView,  
  9.             defStyle, 0);  
  10.     //在佈局xml文件中定義的keyCode值,用於分發點擊事件時唯一標記一個按鍵  
  11.     mCode = a.getInteger(R.styleable.KeyButtonView_keyCode, 0);  
  12.     //在佈局xml文件中定義的值,定義該按鈕是否支持長按。  
  13.     mSupportsLongpress = a.getBoolean(R.styleable.KeyButtonView_keyRepeat, true);  
  14.   
  15.     TypedValue value = new TypedValue();  
  16.     //如果定義了android:contentDescription屬性,則給該按鈕添加描述  
  17.     if (a.getValue(R.styleable.KeyButtonView_android_contentDescription, value)) {  
  18.         mContentDescriptionRes = value.resourceId;  
  19.     }  
  20.   
  21.     a.recycle();  
  22.   
  23.   
  24.     setClickable(true); //因爲繼承的ImageView,所以設置下它的Clickable爲true,不然不能點擊  
  25.     mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); //該變量控制虛擬按鍵的可點擊區域  
  26.     mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); //獲取音頻服務,用於播放按鍵音  
  27.     setBackground(new KeyButtonRipple(context, this)); //設置背景  
  28. }  

2、事件的發送    

    之前說過KeyButtonView繼承自ImageView,間接父類是View類,所以它的觸摸事件可以通過 onTouchEvent() 回調方法來接收,單擊和長按事件的發送,也是通過重寫該方法實現的。

    最重要的MotionEvent就是ACTION_DOWN事件,單擊和長按事件主要是在這裏處理的。

    首先是單擊事件。首先判斷當前按鈕的mCode,即keyCode的值。如果不爲0,則通過 sendEvent() 發送ACTION_DOWN的事件。

    然後把一個Runnable:mCheckLongPress 放入隊列,延時0.5s執行,用與檢查是否滿足長按的條件。

注:ViewConfiguration.getLongPressTimeout() 的值爲500ms,即0.5s。

    其他MotionEvent就不細說了,代碼裏都寫了註釋。

[java] view plain copy
  1. public boolean onTouchEvent(MotionEvent ev) {  
  2.     final int action = ev.getAction();  
  3.     int x, y;  
  4.     if (action == MotionEvent.ACTION_DOWN) {  
  5.         mGestureAborted = false;  
  6.     }  
  7.     if (mGestureAborted) {  
  8.         return false;  
  9.     }  
  10.   
  11.     switch (action) {  
  12.         case MotionEvent.ACTION_DOWN:  
  13.             mDownTime = SystemClock.uptimeMillis();//記錄按下的時間  
  14.             mLongClicked = false;  
  15.             setPressed(true); //設置當前按鈕爲按下的狀態  
  16.             if (mCode != 0) {  
  17.                 //如果mCode不爲零,則發送一個ACTION_DOWN類型的點擊事件  
  18.                 sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);  
  19.             } else {  
  20.                 // Provide the same haptic feedback that the system offers for virtual keys.  
  21.                 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);  
  22.             }  
  23.             //再次進入MotionEvent.ACTION_DOWN時,移除檢查長按狀態的的Runnable  
  24.             removeCallbacks(mCheckLongPress);  
  25.             //發送一個延時0.5s的Runnable。用於檢查當前按鈕是否滿足長按條件  
  26.             postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());  
  27.             break;  
  28.         case MotionEvent.ACTION_MOVE:  
  29.             x = (int)ev.getX();  
  30.             y = (int)ev.getY();  
  31.             //獲取當前觸屏座標,當手指移動出按鍵範圍,將Pressed狀態設爲false  
  32.             setPressed(x >= -mTouchSlop  
  33.                     && x < getWidth() + mTouchSlop  
  34.                     && y >= -mTouchSlop  
  35.                     && y < getHeight() + mTouchSlop);  
  36.             break;  
  37.         case MotionEvent.ACTION_CANCEL:  
  38.             setPressed(false);  
  39.             //發送CANCELED類型的點擊事件  
  40.             if (mCode != 0) {  
  41.                 sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);  
  42.             }  
  43.             removeCallbacks(mCheckLongPress);  
  44.             break;  
  45.         case MotionEvent.ACTION_UP:  
  46.             final boolean doIt = isPressed() && !mLongClicked;  
  47.             setPressed(false);  
  48.             if (mCode != 0) {  
  49.                 if (doIt) {  
  50.                     sendEvent(KeyEvent.ACTION_UP, 0); //發送ACTION_UP事件  
  51.                     sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);  
  52.                     playSoundEffect(SoundEffectConstants.CLICK); //播放按鍵音  
  53.                 } else {  
  54.                     sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);  
  55.                 }  
  56.             } else {  
  57.                 // no key code, just a regular ImageView  
  58.                 if (doIt) {  
  59.                     performClick();  
  60.                 }  
  61.             }  
  62.             removeCallbacks(mCheckLongPress);  
  63.             break;  
  64.     }  
  65.   
  66.     return true;  
  67. }  

    來看看這個mCheckLongPress的實現。當接收到 MotionEvent.ACTION_DOWN 事件0.5s後,run()就會被執行。但前提是,在這期間,沒有再次接收到 ACTION_DOWN,ACTION_CANCEL,ACTION_UP 其中的任一事件,否則 mCheckLongPress 會被移除。

    如果執行到了run(),判斷當前按鈕是否仍然爲按下的狀態,如果爲true,表示滿足長按的條件,因爲從接收到ACTION_DOWN到現在一共0.5s,按鈕一直處於pressed的狀態。由此可見,系統默認按下按鍵持續0.5s即爲長按動作。

    通過isLongClickable()判斷當前按鈕是否支持長按,如果爲true,則通過父類View的方法performLongClick()去發送一個長按的事件。

    變量mSupportsLongpress默認值爲true。用於確保當isLongClickable()爲false時,也能發送出長按事件。

[java] view plain copy
  1. private final Runnable mCheckLongPress = new Runnable() {  
  2.     public void run() {  
  3.         if (isPressed()) { //判斷當前按鈕是否仍爲按下的狀態  
  4.             if (isLongClickable()) { //判斷是否支持長按  
  5.                 // Just an old-fashioned ImageView  
  6.                 performLongClick(); //發送長按事件  
  7.                 mLongClicked = true;  
  8.             } else if (mSupportsLongpress) {  
  9.                 sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS);  
  10.                 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);  
  11.                 mLongClicked = true;  
  12.             }  
  13.         }  
  14.     }  
  15. };  

細心的筒子們可能發現了。發送事件大部分都是通過 sendEvent() 來實現的。看下它的源碼。

它將包含了keyCode,action和repeatCount等數據的KeyEvent,通過系統服務類InputManager,把事件發送了出去。

事件發送出去了,在哪處理呢?往下看。

[java] view plain copy
  1. public void sendEvent(int action, int flags) {  
  2.     sendEvent(action, flags, SystemClock.uptimeMillis());  
  3. }  
  4.   
  5. void sendEvent(int action, int flags, long when) {  
  6.     final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;  
  7.     final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount,  
  8.             0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,  
  9.             flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,  
  10.             InputDevice.SOURCE_KEYBOARD);  
  11.     InputManager.getInstance().injectInputEvent(ev,  
  12.             InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);  
  13. }  

3、事件的處理

    由於虛擬按鍵需要在系統所有界面都能響應,所以點擊事件也跟一般View的處理不太一樣。我們知道,一個界面的點擊事件發生時,是由當前Activity的dispatchTouchEvent()去分發,但具體的工作是由其內部的Window去完成的。所以要想在所有界面中都響應某個按鍵,則必須在Window的管理類中去處理。

    路徑是 frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java

    當有點擊事件發生時,首先都會在該類中進行處理,然後向下分發。來看看interceptKeyBeforeDispatching()。光看方法的名字,都可以推測,這個方法會在key事件被分發前被調用。

    到這裏,之前設置的keyCode就派上用場了。首先來看HOME鍵,通過keyCode確定當前按下了虛擬按鍵的HOME鍵。

先處理單擊事件,把除了 KeyEvent.ACTION_DOWN 之外的key類型,作爲單擊事件結束的標誌。中間加了一些條件,在某些條件下,不響應HOME鍵的點擊操作。

    關鍵方法是 handleShortPressOnHome(),下面細說。

    接着是長按事件。 如果 repeatCount > 0 ,且事件裏包含了 KeyEvent.FLAG_LONG_PRESS 這個FLAG,則說明是長按事件。在 handleLongPressOnHome(event.getDeviceId()) 中去處理。詳情往下滑。

    這裏還有個雙擊事件,就不多說了,因爲一般不用雙擊這個效果,而且原理也差不多。

[java] view plain copy
  1. @Override  
  2. public long interceptKeyBeforeDispatching(WindowState win, KeyEvent event, int policyFlags) {  
  3.     final boolean keyguardOn = keyguardOn();  
  4.     final int keyCode = event.getKeyCode();  
  5.     final int repeatCount = event.getRepeatCount();  
  6.     final int metaState = event.getMetaState();  
  7.     final int flags = event.getFlags();  
  8.     final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;  
  9.     final boolean canceled = event.isCanceled();  
  10.     ...  
  11.   
  12.     // First we always handle the home key here, so applications  
  13.     // can never break it, although if keyguard is on, we do let  
  14.     // it handle it, because that gives us the correct 5 second  
  15.     // timeout.  
  16.     if (keyCode == KeyEvent.KEYCODE_HOME) {  
  17.   
  18.         // If we have released the home key, and didn't do anything else  
  19.         // while it was pressed, then it is time to go home!  
  20.         if (!down) {  
  21.             cancelPreloadRecentApps(); //如果當前爲顯示最近使用APP列表界面,則隱藏掉  
  22.   
  23.             mHomePressed = false;  
  24.             if (mHomeConsumed) {  
  25.                 mHomeConsumed = false;  
  26.                 return -1;  
  27.             }  
  28.   
  29.             if (canceled) {  
  30.                 Log.i(TAG, "Ignoring HOME; event canceled.");  
  31.                 return -1;  
  32.             }  
  33.   
  34.             // If an incoming call is ringing, HOME is totally disabled.  
  35.             // (The user is already on the InCallUI at this point,  
  36.             // and his ONLY options are to answer or reject the call.)  
  37.             TelecomManager telecomManager = getTelecommService();  
  38.             if (telecomManager != null && telecomManager.isRinging()) {  
  39.                 Log.i(TAG, "Ignoring HOME; there's a ringing incoming call.");  
  40.                 return -1;  
  41.             }  
  42.   
  43.             // Delay handling home if a double-tap is possible.  
  44.             if (mDoubleTapOnHomeBehavior != DOUBLE_TAP_HOME_NOTHING) {  
  45.                 mHandler.removeCallbacks(mHomeDoubleTapTimeoutRunnable); // just in case  
  46.                 mHomeDoubleTapPending = true;  
  47.                 mHandler.postDelayed(mHomeDoubleTapTimeoutRunnable,  
  48.                         ViewConfiguration.getDoubleTapTimeout());  
  49.                 return -1;  
  50.             }  
  51.   
  52.             handleShortPressOnHome();  
  53.             return -1;  
  54.         }  
  55.   
  56.         // Remember that home is pressed and handle special actions.  
  57.         if (repeatCount == 0) {  
  58.             mHomePressed = true;  
  59.             if (mHomeDoubleTapPending) {  
  60.                 mHomeDoubleTapPending = false;  
  61.                 mHandler.removeCallbacks(mHomeDoubleTapTimeoutRunnable);  
  62.                 handleDoubleTapOnHome();  
  63.             } else if (mLongPressOnHomeBehavior == LONG_PRESS_HOME_RECENT_SYSTEM_UI  
  64.                     || mDoubleTapOnHomeBehavior == DOUBLE_TAP_HOME_RECENT_SYSTEM_UI) {  
  65.                 preloadRecentApps();  
  66.             }  
  67.         } else if ((event.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0) {  
  68.             if (!keyguardOn) {  
  69.                 handleLongPressOnHome(event.getDeviceId());  
  70.             }  
  71.         }  
  72.         return -1;  
  73.     }  
  74. ...  
  75. }  

先講單擊的具體邏輯。一步步調用,來到了帶兩個參數的 launchHomeFromHotKey

這裏分了兩種情況。鎖屏狀態和非鎖屏狀態。

在鎖屏狀態下,不響應HOME鍵的點擊操作,直接返回。

只有在非鎖屏狀態下,才能響應HOME鍵的操作。關鍵是 startDockOrHome(true, awakenFromDreams);

[java] view plain copy
  1. private void handleShortPressOnHome() {  
  2.     ...  
  3.     // Go home!  
  4.     launchHomeFromHotKey();  
  5. }  
  6.   
  7. void launchHomeFromHotKey() {  
  8.     launchHomeFromHotKey(true /* awakenFromDreams */true /*respectKeyguard*/);  
  9. }  
  10.   
  11. /** 
  12.  * A home key -> launch home action was detected.  Take the appropriate action 
  13.  * given the situation with the keyguard. 
  14.  */  
  15. void launchHomeFromHotKey(final boolean awakenFromDreams, final boolean respectKeyguard) {  
  16.     if (respectKeyguard) {  
  17.         if (isKeyguardShowingAndNotOccluded()) {  
  18.             // don't launch home if keyguard showing  
  19.             return;  
  20.         }  
  21.   
  22.         if (!mHideLockScreen && mKeyguardDelegate.isInputRestricted()) {  
  23.             // when in keyguard restricted mode, must first verify unlock  
  24.             // before launching home  
  25.             mKeyguardDelegate.verifyUnlock(new OnKeyguardExitResult() {  
  26.                 @Override  
  27.                 public void onKeyguardExitResult(boolean success) {  
  28.                     if (success) {  
  29.                         try {  
  30.                             ActivityManagerNative.getDefault().stopAppSwitches();  
  31.                         } catch (RemoteException e) {  
  32.                         }  
  33.                         sendCloseSystemWindows(SYSTEM_DIALOG_REASON_HOME_KEY);  
  34.                         startDockOrHome(true /*fromHomeKey*/, awakenFromDreams);  
  35.                     }  
  36.                 }  
  37.             });  
  38.             return;  
  39.         }  
  40.     }  
  41.   
  42.     // no keyguard stuff to worry about, just launch home!  
  43.     try {  
  44.         ActivityManagerNative.getDefault().stopAppSwitches();  
  45.     } catch (RemoteException e) {  
  46.     }  
  47.     if (mRecentsVisible) {  
  48.         // Hide Recents and notify it to launch Home  
  49.         if (awakenFromDreams) {  
  50.             awakenDreams();  
  51.         }  
  52.         hideRecentApps(falsetrue);  
  53.     } else {  
  54.         // Otherwise, just launch Home  
  55.         sendCloseSystemWindows(SYSTEM_DIALOG_REASON_HOME_KEY);  
  56.         startDockOrHome(true /*fromHomeKey*/, awakenFromDreams);  
  57.     }  
  58. }  

startDockOrHome(true, awakenFromDreams) 完成了界面的切換,從當前界面跳轉到桌面。

每個桌面應用的主Activity會在AndroidManifest文件中設置一個 Intent.CATEGORY_HOME 的標籤,通過這個標籤,就可以通過intent匹配跳轉到到桌面主界面。

[java] view plain copy
  1. mHomeIntent =  new Intent(Intent.ACTION_MAIN, null);  
  2. mHomeIntent.addCategory(Intent.CATEGORY_HOME);  
  3. mHomeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK  
  4.         | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);  
  5.   
  6. void startDockOrHome(boolean fromHomeKey, boolean awakenFromDreams) {  
  7.     if (awakenFromDreams) {  
  8.         awakenDreams();  
  9.     }  
  10.   
  11.     Intent dock = createHomeDockIntent();  
  12.     if (dock != null) {  
  13.         try {  
  14.             if (fromHomeKey) {  
  15.                 dock.putExtra(WindowManagerPolicy.EXTRA_FROM_HOME_KEY, fromHomeKey);  
  16.             }  
  17.             startActivityAsUser(dock, UserHandle.CURRENT);  
  18.             return;  
  19.         } catch (ActivityNotFoundException e) {  
  20.         }  
  21.     }  
  22.   
  23.     Intent intent;  
  24.   
  25.     if (fromHomeKey) {  
  26.         intent = new Intent(mHomeIntent);  
  27.         intent.putExtra(WindowManagerPolicy.EXTRA_FROM_HOME_KEY, fromHomeKey);  
  28.     } else {  
  29.         intent = mHomeIntent;  
  30.     }  
  31.   
  32.     startActivityAsUser(intent, UserHandle.CURRENT);  
  33. }  

到此,單擊事件告一段落。下面是長按事件。

關鍵方法 handleLongPressOnHome。

這裏有個變量 mLongPressOnHomeBehavior,作用是控制按鍵長按所需要進行的操作。如果需要客製化,則改動mLongPressOnHomeBehavior的值,並在對應的值下進行響應的處理即可。

[java] view plain copy
  1. private void handleLongPressOnHome(int deviceId) {  
  2.     if (mLongPressOnHomeBehavior == LONG_PRESS_HOME_NOTHING) {  
  3.         return;  
  4.     }  
  5.     mHomeConsumed = true;  
  6.     //振動反饋  
  7.     performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false);  
  8.   
  9.     switch (mLongPressOnHomeBehavior) {  
  10.         case LONG_PRESS_HOME_RECENT_SYSTEM_UI:  
  11.             toggleRecentApps(); //啓動最近打開過的App列表界面  
  12.             break;  
  13.         case LONG_PRESS_HOME_ASSIST:  
  14.             launchAssistAction(null, deviceId); //啓動助手類應用  
  15.             break;  
  16.         default:  
  17.             Log.w(TAG, "Undefined home long press behavior: " + mLongPressOnHomeBehavior);  
  18.             break;  
  19.     }  
  20. }  


OK。到此虛擬按鍵事件的發送和處理都已經完成了。

下面準備分享一個客製化修改NavigationBar的例子,並進行總結。

發佈了16 篇原創文章 · 獲贊 21 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章