以一个简单的播控页面(机顶盒上的播控页面)为例来探讨页面的按键管理方式(没有鼠标,不考虑组合键)。
简单的播控页面:
这个播控页面由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很类似,请不要对语言的优劣性有任何微词;