Как стать автором
Обновить

Chrome / Chromium и клавиатура

Время на прочтение10 мин
Количество просмотров2.3K
В замечательном браузере Chromium (я думаю, это так же относится и к Google Chrome) события клавиатуры, которые можно обрабатывать JavaScript'ом, почему-то ведут себя очень странным, как по мне, образом. Я столкнулся с двумя интересными его проявлениями:

1. Событие onkeyup генерируется сразу же после события onkeydown, при этом при «зажатии» клавиши эта пара событий начинает генерироваться с завидной частотой в 25 миллисекунд, однако с некоторой паузой в начале.
2. Если после нажатия и удержания одной клавиши нажать следующую — происходит пауза ~500мс, и затем пара onkeydown/onkeyup уже генерируется для новой клавиши.

Ниже приведу свой способ обходить такие «милые» баги реализации.
CAUTION! Способ не идеален, и по большей части это dirty hack, чем работающее решение.



Итак, приступим.

Шаг 1. Мы знаем, что связка onkeydown/onkeyup у нас генерируется примерно каждые 25 мс (выведено экспериментальным путем). Первое, что нужно сделать — назначить этим событиям обработчики. Для этого я предварительно создаю объект с обработчиками и по событию onDomReady назначаю их событиям документа.

Для onDomReady я использовал метод из jQuery, однако он с успехом может быть заменен любым другим.

KeyHandler = {<br/>
    // Обработчик нажатия клавиши<br/>
    keyDown: function(e) {<br/>
    },<br/>
 <br/>
    // Обработчик "отпускания" клавиши<br/>
    keyUp: function(e) {<br/>
    },<br/>
 <br/>
    // Метод, вызываемый из onDomReady события и назначающий кастомные обработчики.<br/>
    init: function() {<br/>
        document.onkeydown = KeyHandler.keyDown;<br/>
        document.onkeyup = KeyHandler.keyUp;<br/>
    }<br/>
}<br/>
 <br/>
$(document).ready(function(){<br/>
    KeyHandler.init();<br/>
})

Шаг 2. Чтобы избежать «потерю» событий при нажатии нескольких клавиш подряд было бы неплохо организовать какую-то структуру для хранения хотя-бы идентификаторов того, что произошло событие при нажатии на какую-либо клавишу.

Так же все события клавиатуры в дальнейшем мы будем выполнять из метода-наблюдателя, старт которого будет вызываться при событии onkeydown, и который будет сам же завершаться при отсутствии в очереди событий на выполнение.

Изменим наши обработчики событий так, чтобы они занимались полезной работой и добавим «наблюдателя»:
KeyHandler = {<br/>
 <br/>
    // Добавляем к нашему KeyHandler новый объект, в котором мы будем обозначать, <br/>
    // какие клавиши у нас "зажаты" в текущий момент.<br/>
    pressed: {},<br/>
    // Флаг, показывающий, выполняется ли сейчас обработчик событий, или нет<br/>
    observed: false,<br/>
 <br/>
    // Метод, который отвечает за выполнение обработчиков, когда клавиша зажата<br/>
    observe: function() {<br/>
 <br/>
        // Объявляем новый счетчик, чтобы посчитать, много ли у нас выполнилост событий.<br/>
        var count = 0;<br/>
 <br/>
        // Перевариваем очередь,пытаясь, по возможности, запустить все события "одновременно". <br/>
        // Т.е. настолько быстро, насколько это возможно.<br/>
        for ( a in KeyHandler.pressed ) {<br/>
            //<br/>
            // Наш какой-либо полезный код<br/>
            //<br/>
            count++;<br/>
        }<br/>
 <br/>
        // Если наши события не убились при вызове onkeyup - через 25 миллисеунд вызываем этот же метод.<br/>
        // Иначе просто умираем, указав, что клавиатурные события не наблюдаются.<br/>
        if(count > 0) {<br/>
            setTimeout(KeyHandler.observe, 25);<br/>
        } else {<br/>
            KeyHandler.observed = false;<br/>
        }<br/>
    },<br/>
 <br/>
    // Метод, вызываемый при получении нового события onkeydown<br/>
    startObserve: function() {<br/>
        // Запускаемся, только если нет уже назначенного обработчика и метод observe не выполняется<br/>
        if(KeyHandler.observed == false) {<br/>
            // Указываем в специальном флаге, что события клавиатуры уже находятся под нашим наблюдением<br/>
            KeyHandler.observed = true;<br/>
            // Пытаемся имитировать нормальную работу браузера, выполняя только одно событие <br/>
            // (а не пару) раз в 25 миллисекунд<br/>
            setTimeout(KeyHandler.observe, 25);<br/>
        }<br/>
    },<br/>
 <br/>
    keyDown: function(e) {<br/>
        // Добавляем новое клавиатурное событие в нашу импровизированную "очередь".<br/>
        KeyHandler.pressed[e.keyCode] = true;<br/>
        //Запускаем обработчик.<br/>
        KeyHandler.startObserve();<br/>
    },<br/>
 <br/>
    keyUp: function(e) {<br/>
        // Удаляем событие из очереди при "отпуске" клавиши<br/>
        delete KeyHandler.pressed[e.keyCode];<br/>
    },<br/>
}

В общем и целом весь алгоритм обхода багов весьма прост — мы складываем все события в очередь, и если есть на входе событие нажатия клавиши — пытаемся запустить в вечность обрабатываться каждые 25 миллисекунд обработчик для этого события.

Но поскольку так же мы каждые 25 миллисекунд удаляем это событие из очереди — уйти в бесконечность обработчику не получится. При этом мы получаем один неоценимый плюс — событие для уже нажатой клавиши при нажатии второй клавиши сохранится (т.к. onkeyup при нажатии на вторую клавишу не выполнится ), т.е. в нашем случае будут выполняться два параллельных события для уже нажатой клавиши, для которой браузер не генерирует пару событий onkeydown/onkeyup и для второй нажатой клавиши.

Так же таким методом мы избавляемся от паузы в событиях между моментом нажатия второй клавиши и, собственно, началом генерирования наших пресловутых пар onkeydown/onkeyup каждые ~25 миллисекунд.

Шаг 3. Теперь добавим то, ради чего мы так старались — непосредственно методы, которые должны вызываться при нажатии (и зажатии) клавиши.

KeyHandler = {<br/>
 <br/>
    // Объект, содержащий все обработчики нажатия клавиш<br/>
    keyBinds: {<br/>
        // В нашем примере мы будем отлавливать события нажатия стрелок на клавиатуре<br/>
        // и будем выводить сообщения об этом в дебаг-консоль Chromium'a<br/>
        40: function() { // DOWN<br/>
            console.log("DOWN key fired");<br/>
        },<br/>
        38: function() { // UP<br/>
            console.log("UP key fired");<br/>
        },<br/>
        39: function() { // RIGHT<br/>
            console.log("RIGHT key fired");<br/>
        },<br/>
        37: function() { // LEFT<br/>
            console.log("LEFT key fired");<br/>
        }<br/>
    },<br/>
 <br/>
    observe: function() {<br/>
 <br/>
        var count = 0;<br/>
        for ( a in KeyHandler.pressed ) {<br/>
 <br/>
            // Наш полезный код<br/>
            // Выносим все полезные функции в отдельный объект<br/>
            // И проверяем существование методов для конкретных клавиш<br/>
            // А так же запускаем наши события в несколько потоков<br/>
            if(typeof KeyHandler.keyBinds[a] == "function") {<br/>
                setTimeout(KeyHandler.keyBinds[a], 0);<br/>
            }<br/>
            count++;<br/>
        }<br/>
 <br/>
        if(count > 0) {<br/>
            setTimeout(KeyHandler.observe, 25);<br/>
        } else {<br/>
            KeyHandler.observed = false;<br/>
        }<br/>
    },<br/>
}

Ну и несколько последних штрихов. Наш обработчик может повиснуть навечно, если вдруг неожиданно окно потеряло фокус при зажатой клавише. Добиться этого можно кликнув на другую вкладку браузера, не отжимая клавишу. Чтобы избежать такого поворота событий, мы будем дополнительно обрабатывать событие onblur и вычищать очередь обработки, если таковое случилось.

KeyHandler = {<br/>
    // Если окно потеряло фокус<br/>
    blur: function() {<br/>
        for(in KeyHandler.pressed) {<br/>
            delete KeyHandler.pressed[a];<br/>
        }<br/>
    },<br/>
 <br/>
    init:function() {<br/>
        window.onkeydown    = KeyHandler.keyDown;<br/>
        window.onkeyup      = KeyHandler.keyUp;<br/>
        window.onblur       = KeyHandler.blur;<br/>
    },<br/>
}

Вот, собственно, и все. Теперь нажатия на клавиши будут обрабатываться плавно, и, главное, все вместе. В качестве развития можно добавить определение нажатия комбинаций клавиш.
Надеюсь, мой пример был вам полезен.

P.S. Полный код примера можно посмотреть здесь: http://elhsmart.net.ru/chrome-keyboard.html
P.P.S. Код тестировался только в Chromium. Моя текущая версия 7.0.505.0. Если что-то не так — пишите, попытаюсь исправить.
Теги:
Хабы:
Всего голосов 11: ↑8 и ↓3+5
Комментарии3

Публикации

Истории

Работа

Ближайшие события