轉自:http://blog.csdn.net/kuaiguixs/article/details/78330982
第二部分: Let's go!!!
【點擊事件的實現流程】
1、初始化
虛擬按鍵點擊效果的實現和實體按鍵相似,也是通過上報一個keyCode值,來判斷哪個按鈕被點擊。不同的是,實體按鍵的keyCode值是硬件驅動層傳遞到上層的。而虛擬按鍵的keyCode值是應用層自己定義的。
首先來看KeyButtonView的構造函數。由此可見,最終都會調用到有三個參數的構造方法。最重要的是變量 mCode,它接收了在佈局文件中定義的 keyCode 值。
- public KeyButtonView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
- public KeyButtonView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs);
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.KeyButtonView,
- defStyle, 0);
- //在佈局xml文件中定義的keyCode值,用於分發點擊事件時唯一標記一個按鍵
- mCode = a.getInteger(R.styleable.KeyButtonView_keyCode, 0);
- //在佈局xml文件中定義的值,定義該按鈕是否支持長按。
- mSupportsLongpress = a.getBoolean(R.styleable.KeyButtonView_keyRepeat, true);
- TypedValue value = new TypedValue();
- //如果定義了android:contentDescription屬性,則給該按鈕添加描述
- if (a.getValue(R.styleable.KeyButtonView_android_contentDescription, value)) {
- mContentDescriptionRes = value.resourceId;
- }
- a.recycle();
- setClickable(true); //因爲繼承的ImageView,所以設置下它的Clickable爲true,不然不能點擊
- mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); //該變量控制虛擬按鍵的可點擊區域
- mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); //獲取音頻服務,用於播放按鍵音
- setBackground(new KeyButtonRipple(context, this)); //設置背景
- }
2、事件的發送
之前說過KeyButtonView繼承自ImageView,間接父類是View類,所以它的觸摸事件可以通過 onTouchEvent() 回調方法來接收,單擊和長按事件的發送,也是通過重寫該方法實現的。
最重要的MotionEvent就是ACTION_DOWN事件,單擊和長按事件主要是在這裏處理的。
首先是單擊事件。首先判斷當前按鈕的mCode,即keyCode的值。如果不爲0,則通過 sendEvent() 發送ACTION_DOWN的事件。
然後把一個Runnable:mCheckLongPress 放入隊列,延時0.5s執行,用與檢查是否滿足長按的條件。
注:ViewConfiguration.getLongPressTimeout() 的值爲500ms,即0.5s。
其他MotionEvent就不細說了,代碼裏都寫了註釋。
- public boolean onTouchEvent(MotionEvent ev) {
- final int action = ev.getAction();
- int x, y;
- if (action == MotionEvent.ACTION_DOWN) {
- mGestureAborted = false;
- }
- if (mGestureAborted) {
- return false;
- }
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- mDownTime = SystemClock.uptimeMillis();//記錄按下的時間
- mLongClicked = false;
- setPressed(true); //設置當前按鈕爲按下的狀態
- if (mCode != 0) {
- //如果mCode不爲零,則發送一個ACTION_DOWN類型的點擊事件
- sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);
- } else {
- // Provide the same haptic feedback that the system offers for virtual keys.
- performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
- }
- //再次進入MotionEvent.ACTION_DOWN時,移除檢查長按狀態的的Runnable
- removeCallbacks(mCheckLongPress);
- //發送一個延時0.5s的Runnable。用於檢查當前按鈕是否滿足長按條件
- postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
- break;
- case MotionEvent.ACTION_MOVE:
- x = (int)ev.getX();
- y = (int)ev.getY();
- //獲取當前觸屏座標,當手指移動出按鍵範圍,將Pressed狀態設爲false
- setPressed(x >= -mTouchSlop
- && x < getWidth() + mTouchSlop
- && y >= -mTouchSlop
- && y < getHeight() + mTouchSlop);
- break;
- case MotionEvent.ACTION_CANCEL:
- setPressed(false);
- //發送CANCELED類型的點擊事件
- if (mCode != 0) {
- sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
- }
- removeCallbacks(mCheckLongPress);
- break;
- case MotionEvent.ACTION_UP:
- final boolean doIt = isPressed() && !mLongClicked;
- setPressed(false);
- if (mCode != 0) {
- if (doIt) {
- sendEvent(KeyEvent.ACTION_UP, 0); //發送ACTION_UP事件
- sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
- playSoundEffect(SoundEffectConstants.CLICK); //播放按鍵音
- } else {
- sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
- }
- } else {
- // no key code, just a regular ImageView
- if (doIt) {
- performClick();
- }
- }
- removeCallbacks(mCheckLongPress);
- break;
- }
- return true;
- }
來看看這個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時,也能發送出長按事件。
- private final Runnable mCheckLongPress = new Runnable() {
- public void run() {
- if (isPressed()) { //判斷當前按鈕是否仍爲按下的狀態
- if (isLongClickable()) { //判斷是否支持長按
- // Just an old-fashioned ImageView
- performLongClick(); //發送長按事件
- mLongClicked = true;
- } else if (mSupportsLongpress) {
- sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS);
- sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
- mLongClicked = true;
- }
- }
- }
- };
細心的筒子們可能發現了。發送事件大部分都是通過 sendEvent() 來實現的。看下它的源碼。
它將包含了keyCode,action和repeatCount等數據的KeyEvent,通過系統服務類InputManager,把事件發送了出去。
事件發送出去了,在哪處理呢?往下看。
- public void sendEvent(int action, int flags) {
- sendEvent(action, flags, SystemClock.uptimeMillis());
- }
- void sendEvent(int action, int flags, long when) {
- final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
- final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount,
- 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
- flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
- InputDevice.SOURCE_KEYBOARD);
- InputManager.getInstance().injectInputEvent(ev,
- InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
- }
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()) 中去處理。詳情往下滑。
這裏還有個雙擊事件,就不多說了,因爲一般不用雙擊這個效果,而且原理也差不多。
- @Override
- public long interceptKeyBeforeDispatching(WindowState win, KeyEvent event, int policyFlags) {
- final boolean keyguardOn = keyguardOn();
- final int keyCode = event.getKeyCode();
- final int repeatCount = event.getRepeatCount();
- final int metaState = event.getMetaState();
- final int flags = event.getFlags();
- final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
- final boolean canceled = event.isCanceled();
- ...
- // First we always handle the home key here, so applications
- // can never break it, although if keyguard is on, we do let
- // it handle it, because that gives us the correct 5 second
- // timeout.
- if (keyCode == KeyEvent.KEYCODE_HOME) {
- // If we have released the home key, and didn't do anything else
- // while it was pressed, then it is time to go home!
- if (!down) {
- cancelPreloadRecentApps(); //如果當前爲顯示最近使用APP列表界面,則隱藏掉
- mHomePressed = false;
- if (mHomeConsumed) {
- mHomeConsumed = false;
- return -1;
- }
- if (canceled) {
- Log.i(TAG, "Ignoring HOME; event canceled.");
- return -1;
- }
- // If an incoming call is ringing, HOME is totally disabled.
- // (The user is already on the InCallUI at this point,
- // and his ONLY options are to answer or reject the call.)
- TelecomManager telecomManager = getTelecommService();
- if (telecomManager != null && telecomManager.isRinging()) {
- Log.i(TAG, "Ignoring HOME; there's a ringing incoming call.");
- return -1;
- }
- // Delay handling home if a double-tap is possible.
- if (mDoubleTapOnHomeBehavior != DOUBLE_TAP_HOME_NOTHING) {
- mHandler.removeCallbacks(mHomeDoubleTapTimeoutRunnable); // just in case
- mHomeDoubleTapPending = true;
- mHandler.postDelayed(mHomeDoubleTapTimeoutRunnable,
- ViewConfiguration.getDoubleTapTimeout());
- return -1;
- }
- handleShortPressOnHome();
- return -1;
- }
- // Remember that home is pressed and handle special actions.
- if (repeatCount == 0) {
- mHomePressed = true;
- if (mHomeDoubleTapPending) {
- mHomeDoubleTapPending = false;
- mHandler.removeCallbacks(mHomeDoubleTapTimeoutRunnable);
- handleDoubleTapOnHome();
- } else if (mLongPressOnHomeBehavior == LONG_PRESS_HOME_RECENT_SYSTEM_UI
- || mDoubleTapOnHomeBehavior == DOUBLE_TAP_HOME_RECENT_SYSTEM_UI) {
- preloadRecentApps();
- }
- } else if ((event.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0) {
- if (!keyguardOn) {
- handleLongPressOnHome(event.getDeviceId());
- }
- }
- return -1;
- }
- ...
- }
先講單擊的具體邏輯。一步步調用,來到了帶兩個參數的 launchHomeFromHotKey
這裏分了兩種情況。鎖屏狀態和非鎖屏狀態。
在鎖屏狀態下,不響應HOME鍵的點擊操作,直接返回。
只有在非鎖屏狀態下,才能響應HOME鍵的操作。關鍵是 startDockOrHome(true, awakenFromDreams);
- private void handleShortPressOnHome() {
- ...
- // Go home!
- launchHomeFromHotKey();
- }
- void launchHomeFromHotKey() {
- launchHomeFromHotKey(true /* awakenFromDreams */, true /*respectKeyguard*/);
- }
- /**
- * A home key -> launch home action was detected. Take the appropriate action
- * given the situation with the keyguard.
- */
- void launchHomeFromHotKey(final boolean awakenFromDreams, final boolean respectKeyguard) {
- if (respectKeyguard) {
- if (isKeyguardShowingAndNotOccluded()) {
- // don't launch home if keyguard showing
- return;
- }
- if (!mHideLockScreen && mKeyguardDelegate.isInputRestricted()) {
- // when in keyguard restricted mode, must first verify unlock
- // before launching home
- mKeyguardDelegate.verifyUnlock(new OnKeyguardExitResult() {
- @Override
- public void onKeyguardExitResult(boolean success) {
- if (success) {
- try {
- ActivityManagerNative.getDefault().stopAppSwitches();
- } catch (RemoteException e) {
- }
- sendCloseSystemWindows(SYSTEM_DIALOG_REASON_HOME_KEY);
- startDockOrHome(true /*fromHomeKey*/, awakenFromDreams);
- }
- }
- });
- return;
- }
- }
- // no keyguard stuff to worry about, just launch home!
- try {
- ActivityManagerNative.getDefault().stopAppSwitches();
- } catch (RemoteException e) {
- }
- if (mRecentsVisible) {
- // Hide Recents and notify it to launch Home
- if (awakenFromDreams) {
- awakenDreams();
- }
- hideRecentApps(false, true);
- } else {
- // Otherwise, just launch Home
- sendCloseSystemWindows(SYSTEM_DIALOG_REASON_HOME_KEY);
- startDockOrHome(true /*fromHomeKey*/, awakenFromDreams);
- }
- }
startDockOrHome(true, awakenFromDreams) 完成了界面的切換,從當前界面跳轉到桌面。
每個桌面應用的主Activity會在AndroidManifest文件中設置一個 Intent.CATEGORY_HOME 的標籤,通過這個標籤,就可以通過intent匹配跳轉到到桌面主界面。
- mHomeIntent = new Intent(Intent.ACTION_MAIN, null);
- mHomeIntent.addCategory(Intent.CATEGORY_HOME);
- mHomeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
- void startDockOrHome(boolean fromHomeKey, boolean awakenFromDreams) {
- if (awakenFromDreams) {
- awakenDreams();
- }
- Intent dock = createHomeDockIntent();
- if (dock != null) {
- try {
- if (fromHomeKey) {
- dock.putExtra(WindowManagerPolicy.EXTRA_FROM_HOME_KEY, fromHomeKey);
- }
- startActivityAsUser(dock, UserHandle.CURRENT);
- return;
- } catch (ActivityNotFoundException e) {
- }
- }
- Intent intent;
- if (fromHomeKey) {
- intent = new Intent(mHomeIntent);
- intent.putExtra(WindowManagerPolicy.EXTRA_FROM_HOME_KEY, fromHomeKey);
- } else {
- intent = mHomeIntent;
- }
- startActivityAsUser(intent, UserHandle.CURRENT);
- }
到此,單擊事件告一段落。下面是長按事件。
關鍵方法 handleLongPressOnHome。
這裏有個變量 mLongPressOnHomeBehavior,作用是控制按鍵長按所需要進行的操作。如果需要客製化,則改動mLongPressOnHomeBehavior的值,並在對應的值下進行響應的處理即可。
- private void handleLongPressOnHome(int deviceId) {
- if (mLongPressOnHomeBehavior == LONG_PRESS_HOME_NOTHING) {
- return;
- }
- mHomeConsumed = true;
- //振動反饋
- performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false);
- switch (mLongPressOnHomeBehavior) {
- case LONG_PRESS_HOME_RECENT_SYSTEM_UI:
- toggleRecentApps(); //啓動最近打開過的App列表界面
- break;
- case LONG_PRESS_HOME_ASSIST:
- launchAssistAction(null, deviceId); //啓動助手類應用
- break;
- default:
- Log.w(TAG, "Undefined home long press behavior: " + mLongPressOnHomeBehavior);
- break;
- }
- }
OK。到此虛擬按鍵事件的發送和處理都已經完成了。
下面準備分享一個客製化修改NavigationBar的例子,並進行總結。