DEFAULT_KEYS_SHORTCUT 功能的验证 及其 源码实现分析

揭秘Android平台的DEFAULT_KEYS_SHORTCUT模式实现
本文深入解析Android平台下DEFAULT_KEYS_SHORTCUT模式的工作原理,通过示例程序验证其功能,并详细剖析实现机制。了解如何在不激活菜单的情况下直接触发菜单快捷键,体验Android平台强大的定制能力。

Activity的setDefaultKeyMode (int mode) 方法用来设置一个Activity的默认的按键模式。

具体介绍可以参见我写的 setDefaultKeyMode 用法介绍   一文。

地址是:   http://blog.youkuaiyun.com/silenceburn/archive/2010/12/11/6069645.aspx

 

其中有一种模式是 DEFAULT_KEYS_SHORTCUT ,本文从API文档对此模式的介绍出发,

首先通过编写示例代码,介绍其功能用法,然后通过追踪源码,简单介绍此模式在android源码中是如何实现的。

 


1.  关于 DEFAULT_KEYS_SHORTCUT  的 API文档介绍

Use with setDefaultKeyMode(int) to execute a menu shortcut in default key handling.

That is, the user does not need to hold down the menu key to execute menu shortcuts.

 

从字面上看,其含义是指,将默认的按键输入作为菜单快捷键进行处理。

也就是说,用户不需要按下menu按键,就可以处理菜单快捷键,听起来非常神奇,究竟是不是这样呢?

 

 


2.编写示例程序

我们编写一个程序验证一下其功能,首先新建一个工程,并设置默认按键模式为 DEFAULT_KEYS_SHORTCUT

[java]  view plain copy
  1. package com.silenceburn;  
  2.   
  3. import android.app.Activity;  
  4. import android.os.Bundle;  
  5.   
  6. public class MenuShortCutTester extends Activity {  
  7.     /** Called when the activity is first created. */  
  8.     @Override  
  9.     public void onCreate(Bundle savedInstanceState) {  
  10.         super.onCreate(savedInstanceState);  
  11.         setContentView(R.layout.main);  
  12.           
  13.         setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);  
  14.     }  
  15. }  

 

 

为默认的main.xml中的TextView增加一个id属性,之后我们会用菜单选项控制这行字的颜色

[xhtml]  view plain copy
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:orientation="vertical"  
  4.     android:layout_width="fill_parent"  
  5.     android:layout_height="fill_parent"  
  6.     >  
  7. <TextView   
  8.     android:id="@+id/myText"   
  9.     android:layout_width="fill_parent"   
  10.     android:layout_height="wrap_content"   
  11.     android:text="@string/hello"  
  12.     />  
  13. </LinearLayout>  

 

 

使用findViewById获取上一步中定义了id的文本对象,将其引用保存在成员变量b中。

重写onPrepareOptionsMenu方法,增加我们自己的菜单项,并注册快捷键,同时增加菜单点击的响应事件。

[java]  view plain copy
  1. package com.silenceburn;  
  2.   
  3. import android.app.Activity;  
  4. import android.os.Bundle;  
  5. import android.view.Menu;  
  6. import android.view.MenuItem;  
  7. import android.view.MenuItem.OnMenuItemClickListener;  
  8. import android.widget.TextView;  
  9.   
  10. public class MenuShortCutTester extends Activity {  
  11.     /** Called when the activity is first created. */  
  12.     TextView b;  
  13.       
  14.     @Override  
  15.     public void onCreate(Bundle savedInstanceState) {  
  16.         super.onCreate(savedInstanceState);  
  17.         setContentView(R.layout.main);  
  18.           
  19.         b = (TextView) this.findViewById(R.id.myText);  
  20.           
  21.         setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);  
  22.     }  
  23.       
  24.     @Override  
  25.     public boolean onPrepareOptionsMenu(Menu menu) {  
  26.         // TODO Auto-generated method stub  
  27.         super.onPrepareOptionsMenu(menu);  
  28.           
  29.         menu.removeItem(0);  
  30.         menu.removeItem(1);  
  31.         menu.add( 000"One").setShortcut('0''0').setOnMenuItemClickListener(new OnMenuItemClickListener(){  
  32.   
  33.             @Override  
  34.             public boolean onMenuItemClick(MenuItem item) {  
  35.                 // TODO Auto-generated method stub  
  36.                 b.setBackgroundColor(android.graphics.Color.RED);  
  37.                 return true;  
  38.             }});  
  39.         menu.add( 010"Two").setShortcut('1''1').setOnMenuItemClickListener(new OnMenuItemClickListener(){  
  40.   
  41.             @Override  
  42.             public boolean onMenuItemClick(MenuItem item) {  
  43.                 // TODO Auto-generated method stub  
  44.                 b.setBackgroundColor(android.graphics.Color.GREEN);  
  45.                 return true;  
  46.             }});   
  47.           
  48.         return true;  
  49.     }  
  50. }  

 

 

注意我们一共注册了两个菜单项,

一个叫“One”,点击时将文本对象 b 的背景颜色改为红色,同时定义其快捷键为0

一个叫“Two”,点击时将文本对象 b 的背景颜色改为绿色,同时定义其快捷键为1

 

至此示例程序完成。

 


3.验证使用示例程序

启动AVD,运行上述程序,程序启动后,我们应当看到是黑底灰字,点击menu按钮,可以看到One和Two两个菜单选项。

如下图所示:

 

目前Menu是打开状态,

点击One ,将把“helloworld...”字样的背景色变为红色,

点击Two ,将把“helloworld...”字样的背景色变为红绿色。

或者我们点设置好的快捷键 0 和 1,发现可以直接调用菜单选项控制颜色变化。 

 

到目前为止一切都很正常,不过,神奇的现在来了! 

 

我们首先关闭菜单,

然后直接点键盘键"0“,看看会发生什么。再直接点键盘键"1" ,看看会发生什么。

哈哈,在没有激活菜单的情况下,菜单项快捷键被直接调用了!根本不需要打开菜单,就可以用激活菜单快捷键!

 

什么?有位同学说快捷键就应该是这样子把,那好,请你把 onCreate 里面的

setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT); 改为 setDefaultKeyMode(DEFAULT_KEYS_DISABLE);

然后再运行试试,在不打开菜单的情况下,你就是把 0 和 1 按坏,系统也不会理你的呵呵

 


4.浅析实现原理

那么这神奇的功能是如何实现的呢?我们试着通过分析android源码找到答案。

 

首先顺藤摸瓜,我们找一找系统是如何处理 DEFAULT_KEYS_SHORTCUT 关键字的,

在Activity.java中可以找到如下代码片段:

 

[java]  view plain copy
  1. if (mDefaultKeyMode == DEFAULT_KEYS_DISABLE) {  
  2.     return false;  
  3. else if (mDefaultKeyMode == DEFAULT_KEYS_SHORTCUT) {  
  4.     if (getWindow().performPanelShortcut(Window.FEATURE_OPTIONS_PANEL,   
  5.             keyCode, event, Menu.FLAG_ALWAYS_PERFORM_CLOSE)) {  
  6.         return true;  
  7.     }  
  8.     return false;  
  9. }  

 

由此可知,当系统检测到 DEFAULT_KEYS_SHORTCUT 关键字时,实际调用了

getWindow().performPanelShortcut(Window.FEATURE_OPTIONS_PANEL,

keyCode, event,Menu.FLAG_ALWAYS_PERFORM_CLOSE)

 

我们继续追寻,但是这里会遇到一个困难,就是查阅API文档你会发现,performPanelShortcut函数是个纯虚函数!

接下来该怎么办呢?既然功能顺利执行了,那么这个纯虚函数一定会有一个实现的。这个实现类必然是window类的子类。

所以我们在OnCreate里面加上一行代码 Window w =  this.getWindow();

然后通过Eclipse的调试器,利用RTTI查看其实现类,结果如下图:

 

 

可以看的很清楚,实现类是 PhoneWindow ,

这样我们就可以到 PhoneWindow 的源码中去查找performPanelShortcut的实现了。

 

在PhoneWindow.java中我们可以看到如下代码片段:

 

[java]  view plain copy
  1. // Only try to perform menu shortcuts if preparePanel returned true (possible false  
  2. // return value from application not wanting to show the menu).  
  3. if ((st.isPrepared || preparePanel(st, event)) && st.menu != null) {  
  4.     // The menu is prepared now, perform the shortcut on it  
  5.     handled = st.menu.performShortcut(keyCode, event, flags);  
  6. }  

 

终于看到menu字样了,这里我们可以看到 if 里面描述的调用条件,

首先当前panel必须已经准备好了(你可以用 onPreparePanel 截获到准备请求),

其次,当前panel必须是有Menu的!(st.menu != null),

从这里我们可以明白DEFAULT_KEYS_SHORTCUT对于没有menu的应用是没有任何效果的。

而且在另一处代码我们会看到还要进行 isShortCut 的判断,所以对于没有快捷键的菜单也是没有任何效果的。

 

那么我们再看看 preparePanel 里面是如何实现的,在其实现中可以找到如下代码片段:

[java]  view plain copy
  1. // Callback and return if the callback does not want to show the menu  
  2. if (!cb.onPreparePanel(st.featureId, st.createdPanelView, st.menu)) {  
  3.     return false;  
  4. }  

 

至此,就完全明白了!代码在这里回调了 onPreparePanel ,而 onPreparePanel  中会回调  onPrepareOptionsMenu ,

而onPrepareOptionsMenu ,就是我们自己写实现自定义菜单的地方了。

 

为了验证上述推导,我们在onPrepareOptionsMenu 中放入断点,然后在菜单关闭的情况下,输入快捷键,

运行到断点后查看调用堆栈,入下图所示:

 

 

 

堆栈调用顺序可以很清楚的看出我们的推导过程是正确的。至此 DEFAULT_KEYS_SHORTCUT 的实现分析完毕。

 


 

总结:

 

我之所以非常喜爱和看好android平台,就是因为她是开源的,

当我们对任何一个问题有疑问时,都可以把她扒光了细看,呵呵。

而apple的IOS,尽管你很美,但是你的内心实在太难捉摸了。

 

从本文的分析过程可以看出,平台任何一个看似神奇的功能的实现,

背后都有安卓源码开发者们大巧不工重剑无锋的的实现。

 

本文对DEFAULT_KEYS_SHORTCUT的分析实际上是很浅显的,

如果细看源码,会发现更多的有意思的地方的。

 

你还是把这个改好了 发完整版给我吧 你的修复我看不懂 弄得我不知道是直接替换 还是让我干什么“# E:\AI_System\core\config.py import os import sys import json import logging from pathlib import Path from dotenv import load_dotenv from prettytable import PrettyTable # 临时添加项目根目录到Python路径 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) class CoreConfig: _instance = None @classmethod def get_instance(cls): """获取单例实例""" if cls._instance is None: cls._instance = cls() return cls._instance def __init__(self): """初始化配置系统""" # 设置日志 self.logger = logging.getLogger('CoreConfig') self.logger.setLevel(logging.INFO) # 确保有基本日志处理器 if not self.logger.handlers: handler = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) self.logger.addHandler(handler) # 设置基础目录 self.base_dir = Path(__file__).resolve().parent.parent self.env_prefix = "AI_SYSTEM" self.config = {} # 敏感字段列表(在日志和输出中掩码) self.sensitive_fields = ["DB_PASSWORD", "SECRET_KEY", "API_KEY", "ACCESS_TOKEN"] # 加载配置 self._load_config() # 直接调用方法,不要嵌套定义 self.logger.info("✅ 配置系统初始化完成") # 注意:所有方法都在类作用域内,不要嵌套定义 def _load_config(self): """加载所有配置""" # 1. 设置默认值 self._set_defaults() # 2. 加载配置文件 self._load_config_files() # 3. 加载环境变量 self._load_environment() # 4. 验证关键路径 self.validate_model_paths() def _mask_sensitive_value(self, key, value): """对敏感信息进行掩码处理""" if value and key in self.sensitive_fields: return "******" return value # ... 其他方法保持不变 ... def _log_sensitive_value(self, key, value): """在日志中安全地记录敏感信息""" if key in self.sensitive_fields: self.logger.info(f"🔄 环境变量覆盖: {key}=******") else: self.logger.info(f"🔄 环境变量覆盖: {key}={value}") def _set_defaults(self): """设置默认配置值""" # 系统路径配置 defaults = { "LOG_DIR": str(self.base_dir / "logs"), "CONFIG_DIR": str(self.base_dir / "config"), "MODEL_CACHE_DIR": str(self.base_dir / "model_cache"), "AGENT_NAME": "小蓝", "DEFAULT_USER": "管理员", "MAX_WORKERS": 4, "AGENT_RESPONSE_TIMEOUT": 30.0, # 模型路径配置 "MODEL_BASE_PATH": "E:/AI_Models", "TEXT_BASE": "E:/AI_Models/Qwen2-7B", "TEXT_CHAT": "E:/AI_Models/deepseek-7b-chat", "MULTIMODAL": "E:/AI_Models/deepseek-vl2", "IMAGE_GEN": "E:/AI_Models/sdxl", "YI_VL": "E:/AI_Models/yi-vl", "STABLE_DIFFUSION": "E:/AI_Models/stable-diffusion-xl-base-1", # 系统路径配置 "SYSTEM_ROOT": str(self.base_dir), "AGENT_DIR": str(self.base_dir / "agent"), "WEB_UI_DIR": str(self.base_dir / "web_ui"), "CORE_DIR": str(self.base_dir / "core"), "MODELS_DIR": str(self.base_dir / "models"), "LOGS_DIR": str(self.base_dir / "logs"), # 服务器配置 "HOST": "0.0.0.0", "FLASK_PORT": 8000, "GRADIO_PORT": 7860, # 数据库配置 "DB_HOST": "localhost", "DB_PORT": 5432, "DB_NAME": "ai_system", "DB_USER": "ai_user", "DB_PASSWORD": "", # 安全配置 "SECRET_KEY": "default-secret-key" } for key, value in defaults.items(): self.config[key] = value self.logger.debug(f"设置默认值: {key}={self._mask_sensitive_value(key, value)}") def _load_config_files(self): """加载配置文件""" # 确保配置目录存在 config_dir = self.base_dir / "config" config_dir.mkdir(exist_ok=True, parents=True) # 配置加载顺序 config_files = [ config_dir / 'default.json', config_dir / 'local.json' ] for config_file in config_files: if config_file.exists(): try: with open(config_file, 'r', encoding='utf-8') as f: config_data = json.load(f) # 掩码敏感信息 masked_data = {k: self._mask_sensitive_value(k, v) for k, v in config_data.items()} self.config.update(config_data) self.logger.info(f"📂 从 {config_file} 加载配置: {masked_data}") except Exception as e: self.logger.error(f"❌ 加载配置文件 {config_file} 错误: {str(e)}") else: self.logger.info(f"ℹ️ 配置文件不存在: {config_file},跳过") def _load_environment(self): """加载环境变量""" # 加载.env文件 env_file = self.base_dir / '.env' if env_file.exists(): try: # 加载.env文件 load_dotenv(dotenv_path=str(env_file), override=True) self.logger.info(f"🌐 从 {env_file} 加载环境变量") except Exception as e: self.logger.error(f"❌ 加载环境变量失败: {str(e)}") # 覆盖环境变量中的配置 for key in list(self.config.keys()): # 先尝试带前缀的环境变量 prefixed_key = f"{self.env_prefix}_{key}" env_value = os.getenv(prefixed_key) # 如果带前缀的环境变量不存在,尝试直接使用key if env_value is None: env_value = os.getenv(key) if env_value is not None: # 尝试转换数据类型 if env_value.lower() in ['true', 'false']: env_value = env_value.lower() == 'true' elif env_value.isdigit(): env_value = int(env_value) elif env_value.replace('.', '', 1).isdigit(): try: env_value = float(env_value) except ValueError: pass # 保持字符串 self.config[key] = env_value self._log_sensitive_value(key, env_value) def validate_model_paths(self): """验证所有模型路径是否存在""" model_keys = ["TEXT_BASE", "TEXT_CHAT", "MULTIMODAL", "IMAGE_GEN", "YI_VL", "STABLE_DIFFUSION"] results = {} for key in model_keys: path = self.get(key, "") if path: path_obj = Path(path) exists = path_obj.exists() results[key] = { "path": str(path_obj), "exists": exists } if not exists: self.logger.warning(f"⚠️ 模型路径不存在: {key} = {path}") else: results[key] = { "path": "", "exists": False } self.logger.warning(f"⚠️ 模型路径未配置: {key}") return results def get(self, key, default=None): """获取配置值""" return self.config.get(key, default) def __getitem__(self, key): """通过键访问配置值""" return self.config[key] def __contains__(self, key): """检查键是否存在""" return key in self.config def to_dict(self, mask_sensitive=True): """返回当前配置的字典表示""" if mask_sensitive: return {k: self._mask_sensitive_value(k, v) for k, v in self.config.items()} return self.config.copy() # 创建全局配置实例 config = CoreConfig.get_instance() # 测试代码 if __name__ == "__main__": # 设置日志 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) print("=" * 50) print("配置系统测试") print("=" * 50) # 获取配置实例 config = CoreConfig.get_instance() # 打印基本配置 print(f"AGENT_NAME: {config.get('AGENT_NAME')}") print(f"SYSTEM_ROOT: {config.get('SYSTEM_ROOT')}") print(f"LOG_DIR: {config.get('LOG_DIR')}") print(f"AGENT_DIR: {config.get('AGENT_DIR')}") print(f"WEB_UI_DIR: {config.get('WEB_UI_DIR')}") print(f"DB_HOST: {config.get('DB_HOST')}") # 验证模型路径 print("\n模型路径验证结果:") for model, info in config.validate_model_paths().items(): status = "存在 ✅" if info["exists"] else "不存在 ❌" print(f"{model:20} {status} ({info['path']})") # 使用表格显示所有配置(美化输出) print("\n当前所有配置:") table = PrettyTable() table.field_names = ["配置项", "值"] table.align["配置项"] = "l" table.align["值"] = "l" # 获取掩码后的配置 masked_config = config.to_dict(mask_sensitive=True) for key, value in masked_config.items(): table.add_row([key, value]) print(table) print("\n测试完成!") ”还有这个 我不知道怎么添加,你下次最好把需要更改的文件 都弄成能直接复制粘贴的那种 你这么发 我看不明白 更改不明白 懂吗?“{ "ENV": "dev", "LOG_LEVEL": "DEBUG", "USE_GPU": false, "DEFAULT_MODEL": "minimal-model", "HOST": "0.0.0.0", "FLASK_PORT": 8000, "GRADIO_PORT": 7860, "DB_HOST": "localhost", "DB_PORT": 5432, "DB_NAME": "ai_system", "DB_USER": "ai_user" }”
最新发布
08-24
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值