Android 手机按键客制化详解

在Android 中会有以下5个按键(Back、Home、Menu、Power、Volume)与用户进行交互,Framework 层中实现按键功能,因此,从手机系统定制的角度,可以满足客户的客制化要求。本文主要从Framework层浅析这些客制化需求的实现。

Back、Home、Menu、Power、Volume 按键图

  1. Android 按键修改相关的类
  2. PhoneWindowManager 简介
  3. 如何打开 或者 关闭 Navigation Bar
  4. 如何长按Home 键启动Google Now
  5. 如何长按实体Menu键进入多窗口模式
  6. 如何点击 Menu键进入调出最近任务列表
  7. 如何让App拿到Power key 值
  8. 如何修Activity启动是的窗口(app启动白屏,黑屏问题)
  9. WindowManagerPolicy 简介

欢迎关注微信公众号:程序员Android
公众号ID:ProgramAndroid
获取更多信息

微信公众号:ProgramAndroid

我们不是牛逼的程序员,我们只是程序开发中的垫脚石。
我们不发送红包,我们只是红包的搬运工。

1. Android 按键修改相关的类

以MTK 平台为例,按键客制化的代码主要存放在以下类中

    1. PhoneWindowManager

PhoneWindowManager 代码路径如下:

\alps\frameworks\base\services\core\java\com\android\server\policy\PhoneWindowManager.java
    1. WindowManagerPolicy

PhoneWindowManager 实现 的接口类WindowManagerPolicy 代码路径如下:

alps\frameworks\base\core\java\android\view\WindowManagerPolicy.java

2. PhoneWindowManager 简介

PhoneWindowManager 类实现接口如下:

java.lang.Object
    ↳  android.view.WindowManagerPolicy.java
         ↳ com.android.server.policy.PhoneWindowManager.java

PhoneWindowManager 类实现关系

PhoneWindowManager主要用于实现各种实体或虚拟按键处理,如需特殊处理按键,请修改源码。

3. 如何打开 或者 关闭 Navigation Bar

虚拟导航栏

解决方法:

  1. 修改config.xml 文件中

搜索关键字 config_showNavigationBar , 查看 config_showNavigationBar 值
true 表示显示,false 表示不显示

  <!-- Whether a software navigation bar should be shown. NOTE: in the future this may be
         autodetected from the Configuration. -->
    <bool name="config_showNavigationBar">true</bool>

参考路径如下:
alps\frameworks\base\core\res\res\values\config.xml

  1. 修改 system.prop 文件

查询关键字 qemu.hw.mainkeys ,并查看值,0 表示关闭 1.表示开启 。

# temporary enables NAV bar (soft keys)
qemu.hw.mainkeys=1

不同项目文件存放地址不一样,可以使用以下命令查找
终端下查找文件方法

find 路径 -name "文件名.java"

或者直接查找文件中的字符串

 find 路径 -type f -name "文件名" | xargs grep "文件中的字符串"

  1. 修改PhoneWindowManager代码

如果上面两个修改都不生效(搜索关键字config_showNavigationBar、qemu.hw.mainkeys),请在PhoneWindowManager 查看setInitialDisplaySize方法中mHasNavigationBar 的值是否被写死,true表示会显示、false 表示不显示导航栏。

 @Override
    public void setInitialDisplaySize(Display display, int width, int height, int density) {
       ...
     // mHasNavigationBar  值控制是否显示虚拟导航栏
        mHasNavigationBar = res.getBoolean(com.android.internal.R.bool.config_showNavigationBar);
     
        ...
      }

4. 如何长按Home 键启动Google Now

  1. 预制 Google Now APK

请自行安装APK

  1. 修改 PhoneWindowManager 代码

长按Home键启动Google Now ,实现方法参考launchAssistLongPressAction 功能实现。

 private void launchAssistAction(String hint, int deviceId) {
        sendCloseSystemWindows(SYSTEM_DIALOG_REASON_ASSIST);
        if (!isUserSetupComplete()) {
            // Disable opening assist window during setup
            return;
        }
        Bundle args = null;
        if (deviceId > Integer.MIN_VALUE) {
            args = new Bundle();
            args.putInt(Intent.EXTRA_ASSIST_INPUT_DEVICE_ID, deviceId);
        }
        if ((mContext.getResources().getConfiguration().uiMode
                & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION) {
            // On TV, use legacy handling until assistants are implemented in the proper way.
            ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE))
                    .launchLegacyAssist(hint, UserHandle.myUserId(), args);
        } else {
            if (hint != null) {
                if (args == null) {
                    args = new Bundle();
                }
                args.putBoolean(hint, true);
            }
            StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
            if (statusbar != null) {
                statusbar.startAssist(args);
            }
        }
    }

自己实现常按Home 键吊起Google Now 方法,供在按键分发处理事件时候调用。

 private void launchAssistLongPressAction() {
        performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false);
        sendCloseSystemWindows(SYSTEM_DIALOG_REASON_ASSIST);

        // launch the search activity
        Intent intent = new Intent(Intent.ACTION_SEARCH_LONG_PRESS);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        try {
            // TODO: This only stops the factory-installed search manager.
            // Need to formalize an API to handle others
            SearchManager searchManager = getSearchManager();
            if (searchManager != null) {
                searchManager.stopSearch();
            }
            startActivityAsUser(intent, UserHandle.CURRENT);
        } catch (ActivityNotFoundException e) {
            Slog.w(TAG, "No activity to handle assist long press action.", e);
        }
    }


  private SearchManager getSearchManager() {
        if (mSearchManager == null) {
            mSearchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
        }
        return mSearchManager;
    }


  private void startActivityAsUser(Intent intent, UserHandle handle) {
        if (isUserSetupComplete()) {
            mContext.startActivityAsUser(intent, handle);
        } else {
            Slog.i(TAG, "Not starting activity because user setup is in progress: " + intent);
        }
    }

  1. 在按键事件分发之前处理

在按键分发处理之前调用自定义长按Home 键的方法

   @Override
    public long interceptKeyBeforeDispatching(WindowState win, KeyEvent event, int policyFlags) {
          ...
 } else if (keyCode == KeyEvent.KEYCODE_ASSIST) {
            if (down) {
                if (repeatCount == 0) {
                    mAssistKeyLongPressed = false;
                } else if (repeatCount == 1) {
                    mAssistKeyLongPressed = true;
                    if (!keyguardOn) {
                         launchAssistLongPressAction();
                    }
                }
            ...
}

注意 双击Home 键调出最近任务列表请用以下方法

双击Home 键调出最近任务列表

在 phoneWindowManager.java 的 interceptKeyBeforeQueueing 方法中修改
修改方法如下:

    int result = 0; // 原为 int result, 请加入初始值.
        // 请在类中补充 boolean homeDownDoubleClick = false; 的定义
        // 请在类中补充 long lastHomeDownTime=0; 的定义
        // 请在类中补充 long lastHomeUpTime=0; 的定义
        // 检测原理: 检测上一次按下的 home key 与本次按下的 home key 时间间隔是否 < 500ms
        // if yes, 则认为是双击 home key

        if (keyCode == KeyEvent.KEYCODE_HOME) {
            if (down) {

                // this is home down
                if (((event.getEventTime() - lastHomeDownTime) < 500)) {
                    homeDownDoubleClick = true;
                } else {
                    homeDownDoubleClick = false;
                }
                lastHomeDownTime = event.getEventTime();
            } else {
                // then home up comes
                Log.d(TAG, "homeDownDoubleClick=" + homeDownDoubleClick
                        + ",lastHomeDownTime=" + lastHomeDownTime
                        + ",lastHomeUpTime=" + lastHomeUpTime
                        + ",this home up=" + event.getEventTime());

                if (homeDownDoubleClick
                        && ((event.getEventTime() - lastHomeUpTime) < 500)) {

                    Log.d(TAG, "double click on home detected");
                    try {
                        IStatusBarService statusbar = getStatusBarService();
                        if (statusbar != null) {
                            // 调出最近任务列表
                            statusbar.preloadRecentApps();
                            statusbar.toggleRecentApps();
                        }
                    } catch (RemoteException e) {
                        Slog.e(TAG,
                                "RemoteException when preloading recent apps",
                                e);
                        mStatusBarService = null;
                    }

                    result |= ACTION_WAKE_UP;
                    return result;
                }
                lastHomeUpTime = event.getEventTime();
            }
        }

 

5. 如何长按实体Menu键进入多窗口模式

Android N上支持Multi-Window,通过recent key 进入多窗口,对于没有打开虚拟导航栏,只有实体menu按键的手机,可以考虑向SystemUI发送广播的形式,进入Android 分屏多任务模式。
解决方案如下:

  1. PhoneStatusBar 里注册广播

PhoneStatusBar 是SystemUI模块的代码,参考路径如下:

alps/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java

自定义广播实现可以参考系统mDemoReceiver 的实现方法
动态注册广播方法如下:

   context.registerReceiverAsUser(mDemoReceiver, UserHandle.ALL, demoFilter,
                android.Manifest.permission.DUMP, null);
                
        context.registerReceiverAsUser(mAppLongSwitchReceiver, UserHandle.ALL, new IntentFilter("广播的Action"),
                android.Manifest.permission.DUMP, null);

自定义接收广播后,onReceive处理事件实现分屏方法如下:

private BroadcastReceiver mAppLongSwitchReceiver = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            if (DEBUG) Log.v(TAG, "onReceive: " + intent);
            toggleSplitScreenMode(MetricsEvent.ACTION_WINDOW_DOCK_LONGPRESS,
                    MetricsEvent.ACTION_WINDOW_UNDOCK_LONGPRESS);
        }
    };
  1. PhoneWindowManager 中发送广播

在 PhoneWindowManager 的interceptKeyBeforeDispatching方法中发送广播

@Override
    public long interceptKeyBeforeDispatching(WindowState win, KeyEvent event, int policyFlags) {
       ...

          if (!keyguardOn) {
            
                if (down && repeatCount == 1){
                    Intent intent = new Intent("com.app_long_switch");
                    mContext.sendBroadcast(intent);
                    return -1;
                }
                
                
                if (down && repeatCount == 0) {
                    preloadRecentApps();
                } else if (!down) {
                    toggleRecentApps();
                }
            }
       ...
     
      }

  1. destory 方法注销广播

再destory 方法中记得一定要注销广播

 mContext.unregisterReceiver(mDemoReceiver);
 mContext.unregisterReceiver(mAppLongSwitchReceiver);

6. 如何点击 Menu键进入调出最近任务列表

如果想调出最近任务列表,需要拦截menu的事件,在PhoneWindowManager的interceptKeyBeforeDispatching 中处理即可

else if (keyCode == KeyEvent.KEYCODE_MENU) {
            // Hijack modified menu keys for debugging features
            final int chordBug = KeyEvent.META_SHIFT_ON;
             this.toggleRecentApps();
             return -1;
            

如果想长按Menu 调出可以使用以下方法

else if (keyCode == KeyEvent.KEYCODE_MENU) {
            // Hijack modified menu keys for debugging features
            final int chordBug = KeyEvent.META_SHIFT_ON;
            if(KeyEvent.ACTION_UP == event.getAction()
              &&event.getEventTime()-event.getDownTime()>500){//long press
             this.toggleRecentApps();
             return -1;
            }

7. 如何让 App 拿到Power key 值

一般情况下App 是拿不到Power的Key值,但通过以下方法可以实现。

  1. 修改PhoneWindowManager 文件实现

在PhoneWindowManager 中修改interceptKeyBeforeQueueing 方法实现让特定的APP拿到Power key 值

 /** {@inheritDoc} */
    @Override
    public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
       ...
         case KeyEvent.KEYCODE_POWER: {
            //com.example.adc为要处理power key的包名
              if(win != null && win.getAttrs() !=null&&win.getOwningPackage().equals("com.example.adc")){
                  return 1;// return 1事件就传给app处理
       }
 
        }
       ... 

}
  1. 如果只想让某个app的某个Activity 处理
 /** {@inheritDoc} */
    @Override
    public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
       ...
         case KeyEvent.KEYCODE_POWER: {
            // 如果只想让power键让某个Activity处理,将以上的if条件改为:
            if(win != null && win.getAttrs() != null&&win.getAttrs().getTitle().equals("xxx.xxx.xxx.xxxActivity")){
                return 1;// return 1 就会传给 xxx.xxx.xxx.xxxActivity处理
}
        }
       ... 

}

8. 如何修Activity启动是的窗口(app启动白屏,黑屏问题)

当用户从主菜单进入其他应用程序例如时钟、联系人、文件管理等时,可能会出现屏幕闪一下黑屏、白屏等问题,这种现象在当前手机主题(Theme)是浅色(例如白色)的情况下比较明显。

此所谓的闪"黑屏",其实是应用程序的启动窗口。
启动窗口出现的条件如下:

  1. 仅在要启动的Activity在新的Task或者新的Process时,才可能显示启动窗口

  2. 启动窗口先于Activity窗口显示,当Activity窗口的内容准备好之后,启动窗口就会被移除掉,show出真正的activity 窗口

  3. 启动窗口和普通的Activity window类似,只是没有画任何内容,默认是一个黑色背景的窗口

正是由于启动窗口默认是黑色背景的,所以在当前的手机主题为浅色调的时候,就比较容易因为颜色的深浅对比而产生一种视觉上的闪动感。

解决方法如下:

1.去掉启动窗口

在 ActivityStack.java中将SHOW_APP_STARTING_PREVIEW 设置为false 既可

  1. 修改启动窗口样式
    在 PhoneWindowManager中的addStartingWindow 方法中添加自定义样式或者背景等
    /** {@inheritDoc} */
    @Override
    public View addStartingWindow(IBinder appToken, String packageName, int theme,
            CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes,
            int icon, int logo, int windowFlags, Configuration overrideConfig) {

   
             ...
             //添加自定义背景
             View.setBackgroundColor(...);    
       
             ...
             
            }

9. WindowManagerPolicy 简介

PhoneWindowManager 实现 的接口类如下:

alps\frameworks\base\core\java\android\view\WindowManagerPolicy.java

WindowManagerPolicy 接口实现

WindowManagerPolicy 是一个接口类,主要对外提供一些接口。
常用接口如下:

 

WindowState 接口

WindowMangerFuncs接口

Screen On 接口

Keyguard 接口

至此,本篇已结束,如有不对的地方,欢迎您的建议与指正。同时期待您的关注,感谢您的阅读,谢谢!

如有侵权,请联系小编,小编对此深感抱歉,届时小编会删除文章,立即停止侵权行为,请您多多包涵。

既然都看到这里,领两个红包在走吧!
以下两个红包每天都可以领取

1.支付宝搜索 522398497,或扫码支付宝红包海报。

支付宝扫一扫,每天领取大红包

2.微信红包,微信扫一扫即可领取红包

 

微信扫一扫,每天领取微信红包

小礼物走一走,来简书关注我

posted @ 2017-11-14 18:30  程序员Android的博客  阅读(306)  评论(0编辑  收藏  举报