分享一个小巧的快捷键管理工具类

最近要完成软件中的快捷键模块。基本需求就是能做到让用户自由设置。基本之前的所有按键都是写死在代码中的,就像下面这样糟糕:

this.addEventListener(KeyboardEvent.KEY_DOWN,onKeyDown);

private function onKeyDown(event:KeyboardEvent):void
{
    if(event.keyCode==Keyboard.S&&event.ctrlKey)
    {
        ...dosomething
    }
}

这样写是无法做到在后期维护中把快捷键分离出来让用户自由设置的。而且会造成代码冗余,比如这个面板需要Ctrl+S执行保存操作,那个面板也需要Ctrl+S执行那个面板的保存操作。这样Ctrl+S这种组合的快捷键每个面板都需要判断一下,这样相同的逻辑判断会多次出现。还有一种比较严格的情况,比如上面的那种判断严格意义上来讲是不能判断Ctrl+S的,因为用户可能同时也按下了Shift键或者Alt键,那这样这个判断要完全写对,代码量又要多不少。

比较好的解决方式是使用一个快捷键管理类Shortcut,统一管理这些操作。在程序初始化的时候注入快捷键的映射,然后在各个面板注册需要响应的事件类型以及对应的函数。比如一个Ctrl+S的操作可以这样表示:

程序初始化的时候,添加快捷键映射

Shortcut.addBinding("save" , [Keyboard.S , Keyboard.CONTROL]);

具体的面板注册监听事件

Shortcut.addRegister(this , “save” , onShortcut);

addBinding这个方法第一个参数就是事件类型,第二个类型就是事件对应的按键,用一个数组表示,可以是多个具体的解析交由Shortcut实现。

addRegister这个方法第一个参数就是要监听这个事件的对象,第二个参数是监听的类型,第三个参数是对应的回调函数。当然只有满足了按键条件才会触发这个回调函数。

这样具体的按键事件监听,按键逻辑都交由Shortcut这个管理类去实现。要响应事件的对象不再需要去写繁琐的事件监听与逻辑判断了。这个面板只关心save这个事件,而不关心按了什么键,这样解耦了按键事件与面板的关系。用户如果需要改变快捷键,只需要改变那个binding的映射表就行了,具体的面板不需要任何改变。

当然基于以上两个方法我们可以继续添加扩展,比如批量初始化快捷键映射

Shortcut.addBindindBatch(config);

这种方式可以用于初始化的时候或者用户修改了快捷键的时候,传入一个映射表配置就行了。

事件的类型也可以从简单的字符串变为一个静态常量,比如可以定义一个ShortcutType类存放这些事件:

public class ShortcutType
{
    /**
     * 保存
     */
    public static const SAVE:String = "save";
}

基本上API就是这样,使用很简单,也很灵活。然后是重点No Code You Say A Diao

package
{
    import flash.display.InteractiveObject;
    import flash.display.Stage;
    import flash.events.Event;
    import flash.events.FocusEvent;
    import flash.events.KeyboardEvent;
    import flash.ui.Keyboard;
    import flash.utils.Dictionary;

    /**
     * 快捷键管理类, 执行initialize初始化一个全局管理。
     * <br>使用 addBinding 添加一个事件类型 与 按键的绑定
     * <br>使用 addRegister 注册一个监听对象
     * <br>下面是一个简单的使用例子
     * <code>
     *    <br>Shortcut.addBinding("example" , [Keyboard.S , Keyboard.CONTROL]);
     *    <br>Shortcut.addRegister(this , "example" , onSave);
     * </code>
     */
    public class Shortcut
    {
        public function Shortcut(stage:Stage)
        {
            stage.addEventListener(KeyboardEvent.KEY_DOWN,onStageKeyDown,true,2000);
            stage.addEventListener(KeyboardEvent.KEY_UP,onStageKeyUp,true,2000);
            stage.addEventListener(FocusEvent.FOCUS_OUT,onDeactive);
        }

        private static var instance:Shortcut;
        /**
         * 初始化
         */        
        public static function initialize(stage:Stage):void
        {
            if(instance)
                return;
            instance = new Shortcut(stage);
        }

        /**
         * @copy  Shortcut#addRegister()
         */
        public static function addRegister(target:InteractiveObject , type:String , callBack:Function):void
        {
            instance.addRegister(target , type , callBack);
        }

        /**
         * @copy  Shortcut#removeRegister()
         */
        public static function removeRegister(target:InteractiveObject , type:String , callBack:Function):void
        {
            instance.removeRegister(target , type , callBack);
        }

        /**
         * 移除一个对象所有注册的快捷键
         */
        public static function removeTargetRegister(target:InteractiveObject):void
        {
            if(instance.targetDic[target])
            {
                var shortcutMapList:Array = instance.targetDic[target];
                for each (var shortcutMap:ShortcutMap in shortcutMapList) 
                {
                    instance.removeRegister(target , shortcutMap.type , shortcutMap.callBack);
                }
            }
        }

        /**
         * @copy  Shortcut#addBinding()
         */
        public static function addBinding(type:String , keyCodeValue:Array):void
        {
            instance.addBinding(type , keyCodeValue);
        }

        /**
         * 批量添加绑定事件类型 与 按键值的映射
         * @param data 数据中的每一项格式参照addBinding方法中的参数
         */
        public static function addBindindBatch(data:Object):void
        {
            for (var key:String in data) 
            {
                instance.addBinding(key , data[key]);
            }
        }

        /**
         * @copy  Shortcut#removeBinding()
         */
        public static function removeBinding(type:String):void
        {
            instance.removeBinding(type);
        }

        /**
         * 移除所有的事件类型
         */
        public static function removeAllBinding():void
        {
            instance.bindingDic = new Dictionary();
        }

        /**
         * 判断指定键是否按下
         */
        public static function isKeyDown(keyCode:uint):Boolean
        {
            return instance.isKeyDown(keyCode);
        }


        //======================== 快捷键相关=====================start=======================

        /**
         * 监听快捷键的对象 与 持有的注册列表字典
         */
        private var targetDic:Dictionary = new Dictionary(true);

        /**
         * 事件类型 与 具体按键值的关系映射表
         */
        private var bindingDic:Dictionary = new Dictionary();

        /**
         * 注册一个监听对象
         * @param target 监听事件的目标对象
         * @param type 按键要触发的事件类型
         * @param callBack 回调函数示例:不含参数 callBack():void
         * 带一个参数callBack(type:String):void , 带两个参数callBack(type:String , event:KeyboardEvent):void
         */
        public function addRegister(target:InteractiveObject , type:String , callBack:Function):void
        {
            if(getRegisterIndex(target , type , callBack) >= 0)
            {
                return;
            }
            if(targetDic[target] === undefined)
            {
                targetDic[target] = [];
                target.addEventListener(KeyboardEvent.KEY_DOWN , onKeyDown , false ,0 , true);
            }
            var map:ShortcutMap = new ShortcutMap();
            map.type = type;
            map.callBack = callBack; 
            targetDic[target].push(map);
        }

        /**
         * 移除一个监听对象
         * @param target 监听事件的目标对象
         * @param type 按键要触发的事件类型
         * @param callBack 回调函数
         */
        public function removeRegister(target:InteractiveObject , type:String , callBack:Function):void
        {
            var registerIndex:int = getRegisterIndex(target , type , callBack);
            if(registerIndex >= 0)
            {
                var shortcutMapList:Array = targetDic[target];
                shortcutMapList.splice(registerIndex , 1);
                if(shortcutMapList.length == 0)
                {
                    target.removeEventListener(KeyboardEvent.KEY_DOWN , onKeyDown);
                    delete targetDic[target];
                }
            }
        }

        /**
         * 测试是否注册了指定类型的指定方法,并返回注册的索引
         */
        private function getRegisterIndex(target:InteractiveObject , type:String , callBack:Function):int
        {
            if(targetDic[target])
            {
                var shortcutMapList:Array = targetDic[target];
                var shortcutMap:ShortcutMap;
                for (var i:int = 0; i < shortcutMapList.length; i++) 
                {
                    shortcutMap = shortcutMapList[i] as ShortcutMap;
                    if(shortcutMap.type == type && shortcutMap.callBack == callBack)
                    {
                        return i;
                    }
                }
            }
            return -1;
        }

        /**
         * 绑定一个事件类型 与 按键值的映射
         * @param type  事件类型
         * @param keyCodeValue 按下的键的对象, 可以是一个数组也可以每一项是一个数组,数组中的元素对应Keyboard中的常量
         *     <br/>如果数组的长度等于1,则 按照对应的键值触发事件
         * <br/>如果数组的长度大于1,则 按照数组中的第一项对应的键值触发事件,其他的键为触发事件需要按下的键
         * <br/>下面是一些例子
         * <code>
         *    <br>[Keyboard.S , Keyboard.CONTROL] 表示 Ctrl+S
         *    <br>[[Keyboard.BACKSPACE] , [Keyboard.DELETE]] 表示退格或者删除
         * </code>
         */
        public function addBinding(type:String , keyCodeValue:Array):void
        {
            bindingDic[type] = keyCodeValue;
        }

        /**
         * 移除一个事件类型
         */
        public function removeBinding(type:String):void
        {
            if(bindingDic[type])
            {
                delete bindingDic[type];
            }
        }

        private function onKeyDown(event:KeyboardEvent):void
        {
            var shortcutMapList:Array = targetDic[event.currentTarget];
            if(!shortcutMapList || shortcutMapList.length < 1)
                return;

            var shortcutMap:ShortcutMap;
            for (var i:int = 0; i < shortcutMapList.length; i++) 
            {
                shortcutMap = shortcutMapList[i] as ShortcutMap;
                var keyCodeValue:Array = bindingDic[shortcutMap.type];
                var result:Boolean = false;
                if(keyCodeValue[0] is Array)
                {
                    for each (var keyCodeArray:* in keyCodeValue) 
                    {
                        if(keyCodeArray is uint)
                            result = check(event , [keyCodeArray]);
                        else
                            result = check(event , keyCodeArray);
                        if(result)
                            break;
                    }
                }
                else
                {
                    result = check(event , keyCodeValue);
                }
                if(result)
                {
                    if(shortcutMap.callBack.length == 0)
                        shortcutMap.callBack();
                    else if(shortcutMap.callBack.length == 1)
                        shortcutMap.callBack(shortcutMap.type);
                    else if(shortcutMap.callBack.length == 2)
                        shortcutMap.callBack(shortcutMap.type , event);
                    else
                        shortcutMap.callBack();
                }
            }
        }

        /**
         * 检查事件是否符合指定按键
         */
        private function check(event:KeyboardEvent , keyCodeArray:Array):Boolean
        {
            if(!keyCodeArray || keyCodeArray.length < 0)
                return false;
            if(event.keyCode == keyCodeArray[0])
            {
                if(keyCodeArray.length == 1)
                {
                    return true;
                }
                else
                {
                    var firstKey:uint = keyCodeArray[0];
                    var hopeKeys:Array = keyCodeArray.concat();
                    var allDownkeyCodes:Array = compositeKeys(event);
                    if(allDownkeyCodes.indexOf(firstKey)<0)
                        allDownkeyCodes.push(firstKey);

                    if(hopeKeys.length != allDownkeyCodes.length)
                        return false;

                    hopeKeys.sort();
                    allDownkeyCodes.sort();

                    for (var i:int = 0; i < hopeKeys.length; i++) 
                    {
                        if(hopeKeys[i] != allDownkeyCodes[i])
                        {
                            return false;
                        }
                    }
                    return true;
                }
            }
            return false;
        }

        /**
         * 获取按下的组合键
         */
        private function compositeKeys(event:KeyboardEvent):Array
        {
            var keys:Array = [];
            if(event.altKey)
                keys.push(Keyboard.ALTERNATE);
            if(event.shiftKey)
                keys.push(Keyboard.SHIFT);
            if(event.controlKey)
                keys.push(Keyboard.CONTROL);
            if(event.commandKey)
                keys.push(Keyboard.COMMAND);
            return keys;
        }

        //======================== 快捷键相关=====================end=======================

        //======================== 全局舞台按键 =====================start=======================

        private function onDeactive(event:Event):void
        {
            downKeyCodes = new Dictionary();
        }
        /**
         * 按下的键代码列表
         */        
        private var downKeyCodes:Dictionary = new Dictionary();

        private function onStageKeyDown(event:KeyboardEvent):void
        {
            var keyCode:uint = event.keyCode;
            downKeyCodes[keyCode] = true;
        }

        private function onStageKeyUp(event:KeyboardEvent):void
        {
            var keyCode:uint = event.keyCode;
            if(downKeyCodes[keyCode])
                delete downKeyCodes[keyCode];
        }

        /**
         * 是否按下了指定键
         */
        public function isKeyDown(keyCode:uint):Boolean
        {
            return Boolean(downKeyCodes[keyCode]);
        }

        //======================== 全局舞台按键 =====================end=======================

    }
}

class ShortcutMap
{
    /**
     * 事件类型
     */
    public var type:String;

    /**
     * 回调函数
     */
    public var callBack:Function;
}

基本上代码已经说明一切了。主要思路就是给注册的对象监听一个KeyDown事件,在事件内部判断有哪些键按下了,是否符合这个对象某一个注册了的事件对应的按键映射,如果有就执行回调。

有一个关键点,给对象监听的按键事件useWeakReference = true 以及 targetDic也是使用了弱引用。只有这样我们无须手动调用removeRegister,当对象的引用计数为0时,就会被垃圾回收器GC自动回收了,不用担心内存泄露。

另外,这个工具类一开始需要传入一个舞台对象stage进行初始化,这个主要是用于保存一个全局有那些键是被按下了,就是后面那一段代码。这一段可有可无,主要是提供一个公共方法外界可以获取哪些键按下了。

基本上这个Shortcut已经能够满足大多数需求了。之前看过几个快捷键管理的工具类都是直接监听舞台上的事件,这个是有问题的。因为有些情况下,快捷键是针对指定面板的,当焦点不在面板上时快捷键是不起作用的,所以removeRegister这个方法的第一个参数就是设置快捷键应用的对象。
这个也能实现同一个事件可以注册给不同的对象,也可以一个对象注册同一个事件但是有不同的回调。就是说同一个快捷键可以给不同面板,同一个面板同一个快捷键也可以响应多个回调。

另外还支持一个事件多种快捷键,比如一个type为delete的事件,可以这样定义

Shortcut.binding("delete" , [[Keyboard.BACKSPACE] , [Keyboard.DELETE]])

使用一个二维数组,每一个数组表示一组快捷键。delete事件既可以是退格键也可以是删除键。

再一个就是快捷键冲突的问题,同一个快捷键可以派发不同的事件。这种设计也是允许冲突的,因为有可能事件的对象不同,比如面板1里面Ctrl+S是保存,面板2里面Ctrl+S是删除一行,也就不冲突了。

差不多就这些啦。这个类可以拿来直接用,没有和其他的类耦合,也不需要定义接口啥的。另外,这些东西真的要到项目前期规划好,不然到了后面想改都蛋疼。