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.微信红包,微信扫一扫即可领取红包

 

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

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员Android

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值