以一个简单的播控页面(机顶盒上的播控页面)为例来探讨页面的按键管理方式(没有鼠标,不考虑组合键)。
简单的播控页面:
这个播控页面由2个子页面组成:
1)播放页面
2)信息面板MINIEPG
这2个页面的按键责任如下:
1)播放页面:
·up:调出MINIEPG,在其中显示下一个频道的信息;
·down:调出MINIEPG,在其中显示上一个频道的信息;
·CH+:切台,播放下一个频道,并显示当前频道当前节目单的MINIEPG
·CH-:切台,播放上一个频道,并显示当前频道当前节目单的MINIEPG
·OK:显示miniepg
·vol+/-:音量加减
2)MINIEPG:
·up:在其中显示下一个频道的信息;
·down:在其中显示上一个频道的信息;
·CH+:切台,播放下一个频道,并展示当前播放的频道和节目单的信息
·CH-:切台,播放上一个频道,并展示当前播放的频道和节目单的信息
·OK:隐藏MINIEPG,焦点到了“播放页面”
·vol+/-:音量加减
//====页面层次关系
可以有两种看法:
·MINIEPG是“播放页面”的子页面;
·MINIEPG和“播放页面”的同等级页面;
//====按键责任分配
情景介绍完了,下面就是对每个页面实际的按键分工进行分析了:
1)“MINIEPG”上的up/down、CH+/-、vol+/-其实是“播放页面”在实际起作用,也就是说,这些按键是传递给“播放页面”来执行的;
//同样的道理:“SUBMENU”上的up、down、CH+/-、left/right、vol+/-也是最终传递给“播放页面”来执行的;
当然,也可以换一个角度来分析。
up/down、CH+/-实际是“MINIEPG”页面在起作用:“播放页面”实际只是调出了“MINIEPG”,然后由“MINIEPG”来真正执行;
2)“MINIEPG”上的OK等按键却将“播放页面”的OK给屏蔽了;
同理如“SUBMENU”对“MINIEPG”;
3)在执行CH+/-的时候,“播放页面”需要播放新频道,同一时刻,“MINIEPG”需要展示新频道的信息;
//====按键责任分配总结
到目前为止就可以总结出以下几种页面按键责任分配的情况:
1、焦点在某个页面上,并响应某个按键;
2、两个页面要同时响应某个按键;
3、两个页面都有需要对某个按键有响应,但是同一时刻却只能在当前页面有响应;
4、某一个按键只有某个页面有响应,但是当前焦点在别的页面上;
又有两种情况:
1)响应了这个按键之后,焦点会移动到这个响应焦点的页面
2)响应了这个按键之后,焦点不变
这其中,比较困难的是后面3种情况;
//====按键管理方式
我在实际中总结出以下几种按键管理的方式:
1、状态模式;----按键独占方式
2、责任链模式;----按键捕获和透传方式
3、观察者模式;----按键广播方式
//====状态模式按键管理方式的代码演示
页面层次关系:MINIEPG和“播放页面”的同等级页面;
可以解决所有的情况;
interface org.ideamarker.as2.keyhandle.example.state.IKeyCodeHandle {
function onUp():Void;
function onDown():Void;
function onChanUp():Void;
function onChanDown():Void;
function onOk():Void;
function onVolUp():Void;
}
class org.ideamarker.as2.keyhandle.example.state.LiveContext { private var m_currFocus:IKeyCodeHandle; private var m_livePlayUI:LivePlayUI; private var m_liveMiniEpgUI:LiveMiniEpgUI; public function getCurrFocus():IKeyCodeHandle { return m_currFocus; } public function getLivePlayUI():IKeyCodeHandle { return m_livePlayUI; } public function getLiveMiniEpgUI():IKeyCodeHandle { return m_liveMiniEpgUI; } public function setCurrFocus(_focusUI:IKeyCodeHandle):Void { m_currFocus = _focusUI; } public function onOk():Void { m_currFocus.onOk(); } public function onUp():Void { m_currFocus.onUp(); } public function onDown():Void { m_currFocus.onDown(); } public function onChanUp():Void { // TODO:“‘按键责任分配总结’中的第1种情况”; m_livePlayUI.onChanUp(); m_liveMiniEpgUI.onChanUp(); // 焦点移动到了liveMiniEpgUI上; m_currFocus = m_liveMiniEpgUI; } public function onChanDown():Void { // TODO:“‘按键责任分配总结’中的第1种情况”; m_livePlayUI.onChanDown(); m_liveMiniEpgUI.onChanDown(); // 焦点移动到了liveMiniEpgUI上; m_currFocus = m_liveMiniEpgUI; } public function onVolUp():Void { m_currFocus.onVolUp(); // 或者直接在此处调用m_livePlayUI的onVolUp(); // m_livePlayUI.onVolUp(); } }
class org.ideamarker.as2.keyhandle.example.state.LivePlayUI implements IKeyCodeHandle {
private var m_context:LiveContext; private var m_currPlayChannel:String; public function LivePlayUI(_context:LiveContext) { m_context = _context; } public function onUp():Void { // 调出MINIEPG,在其中显示下一个频道的信息; // TODO:“‘按键责任分配总结’的第4种的第1种情况”,并且当前焦点被移动到LiveMiniEpgUI上 m_context.setCurrFocus(m_context.getLiveMiniEpgUI()); m_context.getCurrFocus().onUp(); } public function onDown():Void { // 调出MINIEPG,在其中显示上一个频道的信息; // TODO:“‘按键责任分配总结’的第4种的第1种情况”,并且当前焦点被移动到LiveMiniEpgUI上 m_context.setCurrFocus(m_context.getLiveMiniEpgUI()); m_context.getCurrFocus().onDown(); } public function onChanUp():Void { // 切台,播放下一个频道; m_currPlayChannel = "play the next channel"; } public function onChanDown():Void { // 切台,播放上一个频道; m_currPlayChannel = "play the pervious channel"; } public function onOk():Void { // 显示miniepg // TODO:“‘按键责任分配总结’中的第4种的第1种情况”; m_context.setCurrFocus(m_context.getLiveMiniEpgUI()); } public function onInfo():Void { // 显示miniepg, 同onOk() // TODO:“‘按键责任分配总结’中的第4种的第1种情况”; m_context.setCurrFocus(m_context.getLiveMiniEpgUI()); } public function onVolUp():Void { // 音量加 } }
class org.ideamarker.as2.keyhandle.example.state.LiveMiniEpgUI implements IKeyCodeHandle {
private var m_context:LiveContext; private var m_info:String; public function LiveMiniEpgUI(_context:LiveContext) { m_context = _context; } public function onUp():Void { // 在其中显示下一个频道的信息; m_info = "the next channel info"; } public function onDown():Void { // 在其中显示上一个频道的信息; m_info = "the previous channel info"; } public function onChanUp():Void { // 展示下一个播放的频道和节目单的信息; m_info = "the next channel info"; } public function onChanDown():Void { // 展示上一个播放的频道和节目单的信息; m_info = "the next channel info"; } public function onOk():Void { // ·OK/Info/back:隐藏MINIEPG,焦点到了“播放页面” // TODO:“‘按键责任分配总结’中的第2种情况”; m_context.setCurrFocus(m_context.getLivePlayUI()); } public function onVolUp():Void { // 音量加,按键责任要传递给LivePlayUI // TODO:“‘按键责任分配总结’中的第4种的第2种情况”; // 其实在这种情况,完全无需要抽象出接口方法,只需要直接在LiveContext中调用LivePlayUI的onVolUp()方法即可 m_context.getLivePlayUI().onVolUp(); } }
优点:
·状态模式可以解决“按键责任分配总结”中的所有情况。但是入门有门槛,没有明显的固定的套路,在情况较为复杂的情况必须要coder有一定的分析能力;
缺点:
·所有具有对相同按键有响应的方法都需要抽象成相同的接口名称;
·如果可以将状态模式再进一步抽象,总结出一个工具类来,那就更加好了----这是我一直在思考的问题;
//====责任链模式按键管理方式的代码演示
页面层次关系:MINIEPG是“播放页面”的子页面;
interface org.ideamarker.as2.keyhandle.example.cor.IKeyControlItem { /** * 该受控单元的按键处理函数; */ public function keyHandle(_keyCode:String):Void; /** * 获取该受控单元需要处理的按键列表; */ public function getInterestingKeys():Array; }
class org.ideamarker.as2.keyhandle.example.cor.KeyControlManager {
private var itemList:Array; public function KeyControlManager() { itemList = new Array(); } /** * 向按键状态管理器里面添加一个受控单元; * 如果一个受控单元被重复添加,则其优先级以最后一次添加的为准; * @param item 要添加的按键受控单元; */ public function regItem(_item:IKeyControlItem):Void { unRegItem(_item); itemList.unshift(_item); } /** * 从按键状态管理器里面移除一个受控单元; * @param item 要移除的按键受控单元; */ public function unRegItem(_item:IKeyControlItem):Void { for(var i:Number = 0;i < itemList.length;i++) { if(itemList[i] == _item) { itemList.splice(i, 1); return ; } } } /** * 按键按下的事件; */ public function onKeyCodeHandle(_keyCode:String):Void { for(var i:Number = 0;i < itemList.length;i++) { var item:IKeyControlItem = IKeyControlItem(itemList[i]); var interestKeys:Array = item.getInterestingKeys(); for(var j:Number = 0;j < interestKeys.length;j++) { if(_keyCode == interestKeys[j]) { item.keyHandle(_keyCode); return ; // 因为这个return,让这个例子是责任链模式,而不是观察者模式 } } } } }
class org.ideamarker.as2.keyhandle.example.cor.LivePlayUI implements IKeyControlItem { // 焦点管理工具 private var m_keyControlManager:KeyControlManager; // keyCode组的集合 private var m_keyCodeGroup:Array; // 当前key组的index private var m_nowGroupIndex:Number; // 子页面 miniepg private var m_liveMiniEpgUI:LiveMiniEpgUI; // private var m_currChannel:Object; public function LivePlayUI() { m_keyControlManager = new KeyControlManager(); this.regKeyItem(); this.regKeyCodeGroup(); m_liveMiniEpgUI.setKeyControlManager(m_keyControlManager); } /** * 注册本页面 */ private function regKeyItem():Void { m_keyControlManager.regItem(this); } /** * 注册本页面感兴趣的键值 */ private function regKeyCodeGroup():Void { // 注册键值 m_keyCodeGroup.push([Const.UP, Const.DOWN, Const.CHAN_UP, Const.CHAN_DOWN , Const.OK, Const.BACK, Const.VOL_UP, Const.VOL_DOWN]); // 当此例子改成观察者模式的时候放开注释 // m_keyCodeGroup.push([Const.CHAN_UP, Const.CHAN_DOWN, Const.VOL_UP, Const.VOL_DOWN]); m_nowGroupIndex = 0; } public function keyHandle(_keyCode:String):Void { switch (_keyCode) { case (Const.UP): this.onUp(); break; case (Const.DOWN): this.onDown(); break; case (Const.CHAN_UP): this.onChanUp(); break; case (Const.CHAN_DOWN): this.onChanDown(); break; case (Const.OK): this.onOk(); break; case (Const.VOL_UP): this.onVolUp(); break; } } public function getInterestingKeys():Array { return m_keyCodeGroup[m_nowGroupIndex]; } private function onUp():Void { // 1.获取下一个频道信息,在miniepg上展示 // 2.展示miniepg m_liveMiniEpgUI.show("the next channel info"); } private function onDown():Void { // 1.获取上一个频道信息,在miniepg上展示 // 2.展示miniepg m_liveMiniEpgUI.show("the previous channel info"); } private function onChanUp():Void { // 1.播放下一个频道 // 2.展示miniepg m_liveMiniEpgUI.show("the next channel info"); } private function onChanDown():Void { // 1.播放上一个频道 // 2.展示miniepg m_liveMiniEpgUI.show("the previous channel info"); } private function onOk():Void { // 1.展示miniepg m_liveMiniEpgUI.show("the current channel info"); } private function onVolUp():Void { // } }
class org.ideamarker.as2.keyhandle.example.cor.LiveMiniEpgUI implements IKeyControlItem { private var m_keyControlManager:KeyControlManager; // keyCode组的集合 private var m_keyCodeGroup:Array; // 当前key组的index private var m_nowGroupIndex:Number; public function LiveMiniEpgUI() { } /** * 注册本页面 */ private function regKeyItem():Void { m_keyControlManager.regItem(this); } /** * 注册本页面感兴趣的键值 */ private function regKeyCodeGroup():Void { // 注册键值 m_keyCodeGroup.push([Const.UP, Const.DOWN, Const.OK]); // 当此例子改成观察者模式的时候放开注释 // m_keyCodeGroup.push([Const.UP, Const.DOWN]); m_nowGroupIndex = 0; } public function setKeyControlManager(_keyControlManager:KeyControlManager):Void { m_keyControlManager = _keyControlManager; regKeyItem(); regKeyCodeGroup(); } public function show(_chanInfo:String):Void { } public function keyHandle(_keyCode:String):Void { switch(_keyCode) { case (Const.UP): this.onUp(); break; case (Const.DOWN): this.onDown(); break; case (Const.OK): this.onOk(); break; } } public function getInterestingKeys():Array { return m_keyCodeGroup[m_nowGroupIndex]; } private function onUp():Void { // do sth. } private function onDown():Void { // do sth. } private function onOk():Void { // 隐藏miniepg m_keyControlManager.unRegItem(this); } }
优点:
·抽象出了一个工具类,方便使用,易于理解;
缺点:
·无法实现“按键责任分配总结”中的第2种情况;
//====观察者模式按键管理方式的代码演示
因为多了一个return,上个例子就是责任链模式;
如果将这个return去掉,上个例子就可以变成观察者模式;但是,需要做点修改:
1)需要添加更多的可能情况的key组;
2)m_nowGroupIndex需要随着页面的跳转而改变;
疑惑:
1、总是觉得现在这样的状态模式还是不够抽象,能够抽象出像责任链模式例子中的KeyControlManager类就更加好了;
2、总是觉得这个责任链模式例子中的代码,不是那么和谐,尤其是keyHandle()方法中的内容(虽然可以使用map来关联keyCode和Function);
有没有更加好的改进方法?
声明:
1、本文属于原创文章,历时一周思考得出的结论;
2、由于工作原因,本文代码由Actionscript2实现,语法和Java很类似,请不要对语言的优劣性有任何微词;