Android 7.0 虚拟按键(NavigationBar)源码分析 之 点击事件的实现流程

第二部分: 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的例子,并进行总结。

    原文作者:Android源码分析
    原文地址: https://blog.csdn.net/kuaiguixs/article/details/78330982
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞