Написать что знаю несколько языков это одно, но действительно знать, понимать, и применять на практике это совсем другое, поэтому плохо зная свой сигнатурный язык мышления, полученный от социальной среды с рождения, буду рассказывать в статье как правильно собирать данные нажатия, виртуальной клавиатуры Android в web проекте, используя javascript. Классика, сам не умею научу других. Для чего это может понадобиться, если у вас возник такой вопрос, то вы явно новичок во всех "этих движения", мой вариант ответа: безопасность, изучение характеристик психомоторики пользователя, дальше фантазия в помощь. Игровая площадка - сообщество.com, к сожалению без рекламы не обойтись, мой, своеобразный сборник различных решений для web проекта. Будьте осторожны заходя на этот ресурс, использую старый протокол http, хакеры из Мали могут украсть ваши персональные данные, предупредил если вдруг кто то решил зарегистрироваться. Пользователь использует интерфейс web проекта, набирает текст для публикации своей мысли и генерации изображения.

Примерный алгоритм:

  1. Регистрация нажатия клавиши

  2. Регистрация отпускания клавиши

  3. Вычисление времени нажатия

  4. При сохранении публикации, отправлять данные в сервер

ВАЖНО! Сохранять данные нажатий клавиш только те которые отображаются в полях набора текста HTML, все события виртуальной клавиатуры меня не интересуют, в этой задачи.

Ожидаемый вид данных: список из словарей, в последовательности получаемых символов из клавиатуры с учетом их последовательности в тексте.

[{"key_name": "в", "time_keyup": 1687158386.862, "time_press": 0.127000093460083, "time_keydown": 1687158386.735}, ... ]

В общем цель задана, план не кажется сложным, приступаю к реализации. Так как я великолепный писатель плагиатор javascript, сразу отправляюсь за советом к поисковику. Нахожу нужные фрагменты кода и формирую из этого своё решение:

var arr_keystroke = [];
var arr_bad_keystroke = [];
var idx_arr_keystroke = 0;
var tKey = {}
var list_exept = ["CapsLock", "Alt", "Control", "Shift", "Insert"]

function static_handle(e, charCode) {
    if (list_exept.indexOf(e.key) == -1) {
        if (e.type == "keydown") {
            if (e.key=="Backspace") {
                if (arr_keystroke.length!=0 && idx_arr_keystroke!=0) {
                    idx_arr_keystroke--;
                    arr_keystroke.splice(idx_arr_keystroke, 1);
                }
            } else if (e.key == "ArrowRight") {   
            } else if (e.key == "ArrowLeft") {  
            } else if (e.key == "ArrowUp") {  
            } else if (e.key == "ArrowDown") {   
            } else {
                var keyTimes = {};
                let K;
                if (e.key =="Enter") { 
                    K = String.fromCharCode(charCode); //String.fromCodePoint
                    console.log(".....", K);
                } else { K = e.key };
                keyTimes["key_name"] = K;
                keyTimes["time_keydown"] = new Date().getTime()/1000.0;
                keyTimes["time_keyup"] = new Date().getTime()/1000.0;
                arr_keystroke.splice(idx_arr_keystroke, 0, keyTimes);
                if (!tKey[e.key]) {
                    tKey[e.key] = [idx_arr_keystroke];
                } else {
                    tKey[e.key].push(idx_arr_keystroke);
                }
            }
        }
        if (e.type == "keyup") {
            if (arr_keystroke.length>0) {
                let time_up = new Date().getTime()/1000.0;
                if (tKey[e.key]) {
                    for (var i = 0; i < tKey[e.key].length; i++) {
                        let rev_idx = (tKey[e.key].length-1)-i;
                        arr_keystroke[tKey[e.key][i]]["time_keyup"] = time_up;
                        arr_keystroke[tKey[e.key][i]]["time_press"] = time_up - arr_keystroke[tKey[e.key][rev_idx]]["time_keydown"];
                    }
                    delete tKey[e.key];
                }
            }
        }
    } else {
        if (e.type == "keydown") {
            arr_bad_keystroke.push([e.key, idx_arr_keystroke]);
        }
    } 
}

Полученный сборник чужих идей, проверяю в Chromium, Firefox, операционной системы Ubuntu, работает так как ожидалось. А как дела в android? Сначала выясняю что не работает. Дальше узнаю что просто так взять и посмотреть что там происходит в log браузера телефона не могу, без дополнительных манипуляций, которые к слову не принесут должного комфорта, итог: добро пожаловать, мне, в мир эмулятора. Умные люди напишут что можно установить плагины, всякие экранные клавиатуры и отслеживать нужное без всяких эмуляторов, этим замечательным удачи. А я не умный, поэтому трачу сутки на установку Android SDK в Ubuntu. Почему так долго? Живу в новом Донецке, ошибка 403, лучшее объяснение от github:

так со многими ресурсами на протяжении нескольких лет
так со многими ресурсами на протяжении нескольких лет

Так что да здравствует бесплатный vpn и 104 кб/с. Проверил два варианта установки:

  • бесплатным prox, конфигурирую SDK под свою задачу, дальше в браузере с бесплатным vpn выкачиваю нужные компоненты вручную и сортирую по папкам

  • ноутбук с Windows и бесплатным адекватным vpn, раздаю интернет серверу с Ubuntu

Принципиально не хотел использовать платный промежуточный пк для скачивания бесплатного программного обеспечения. Вопрос живущим на материке: доступ к unreal engine открыт?

первый запуск эмулятора
первый запуск эмулятора

инструкция и совет установки android SDK

необходимые компоненты для работы android SDK

Запуск эмулятора в terminal:

avdmanager list avd
sdkmanager --list
avdmanager create avd -n android_temp -k "system-images;android-28;android-automotive-playstore;x86"
sudo /usr/lib/android-sdk/emulator/emulator_original/emulator -avd Pixel_2_API_27

ВАЖНО! Использовать браузер Chrome или Chromium

Подключаться к отладке chrome://inspect/#devices
Подключаться к отладке chrome://inspect/#devices
пример настроенного инструмента для работы
пример настроенного инструмента для работы

Эмулятор установлен, продолжаю... Подключаюсь к браузеру, вижу ошибку 229 при нажатии с виртуальной клавиатуры. Снова как продвинутый программист обращаюсь за советом к накопленным человеческим знаниям. Выясняю что это вовсе не проблема, так задумано(https://www.w3.org/TR/uievents/#determine-keydown-keyup-keyCode, https://www.w3.org/TR/uievents/#keys-IME), в силу нескольких обстоятельств :

  • безопасность, кстати в ios код ПК версии работает без проблем

  • мультиязычность

Обходных API на этот случай не предусмотрено, решение: отслеживать получаемый символ в полях набора текста HTML. Для соблюдения требования "сохранять данные нажатий клавиш только те которые отображаются..." , в версии для android, вынужден использовать Patience Diff алгоритм, а зачем дайте ответ самостоятельно (домашнее задание), подсказка автодополнение и перемещение каретки по тексту. Собственно android версия:

function android_keydown(keyTimes) {
    prev_idx = idx_arr_keystroke;
    keyTimes["key_name"] = idx_arr_keystroke;
    keyTimes["time_keydown"] = new Date().getTime()/1000.0;
    keyTimes["time_keyup"] = new Date().getTime()/1000.0;
    arr_keystroke.splice(idx_arr_keystroke, 0, keyTimes);
    if (!tKey[idx_arr_keystroke]) {
        tKey[idx_arr_keystroke] = [idx_arr_keystroke];
    } else {
        tKey[idx_arr_keystroke].push(idx_arr_keystroke);
    }   
}

Array.prototype.max = function() {
    return Math.max.apply(null, this);
};

Array.prototype.min = function() {
    return Math.min.apply(null, this);
};

function android_keyup(e, keyTimes) {
    let strrr = "";
    let time_up = new Date().getTime()/1000.0;
    if (tKey[prev_idx] && typeof e.target.value[prev_idx] != "undefined") {
        arr_keystroke[prev_idx]["time_keyup"] = time_up;
    }
    for (var i = 0; i<e.target.value.length; i++) {
        let T = arr_keystroke[i];
        if (typeof T != "undefined") {
            let G = {"key_name":e.target.value[i],
             "time_keydown":T["time_keydown"],
             "time_keyup":T["time_keyup"],
             "time_press":T["time_keyup"]-T["time_keydown"]
            }
            arr_keystroke.splice(i, 1, G); 
            strrr += e.target.value[i];
        } 
    }  
    for (var i = 0; i<arr_keystroke.length; i++) {
        if (typeof arr_keystroke[i]["time_press"] == "undefined") {
            arr_keystroke.splice(i, 1);
            arr_keystroke.splice(prev_idx, 1);
            arr_keystroke.splice(idx_arr_keystroke, 1);
        }
    }   
    const diff = patienceDiff(e.target.value, strrr)
    let toUpdate = []
    diff.lines.forEach((line) => {
        if (line.aIndex < 0 || line.bIndex < 0) {
            toUpdate.push(line.aIndex + line.bIndex + 1)
        }
    });
    console.log(diff.lines, toUpdate)    
    let toFin = [];
    if (toUpdate.length != 0) {
        let temp_data = arr_keystroke[prev_idx];  
        for (var i=0; i<diff.lines.length; i++) {
            if (diff.lines[i].bIndex == -1) {
                toFin.push({"key_name":diff.lines[i].line,
                            "time_keydown":temp_data["time_keydown"],
                            "time_keyup":temp_data["time_keyup"],
                            "time_press":temp_data["time_keyup"]-temp_data["time_keydown"]
                            })
            }
        }
        arr_keystroke.splice.apply(arr_keystroke, [toUpdate.min(), toUpdate.max()].concat(toFin));         
    } else {
        for (var i=0; i<diff.lines.length; i++) {
            let temp_data = arr_keystroke[i];
            toFin.push({"key_name":diff.lines[i].line,
                        "time_keydown":temp_data["time_keydown"],
                        "time_keyup":temp_data["time_keyup"],
                        "time_press":temp_data["time_keyup"]-temp_data["time_keydown"]
                        })
        }
        arr_keystroke = toFin;
    }
      
    console.log(`символы в строке: ${e.target.value.length}, 
                 размер массива: ${arr_keystroke.length}, 
                 позиции каретки: ${idx_arr_keystroke}, 
                 предыдущий индекс: ${prev_idx}, 
                 слово из массива: "${strrr}", 
                 слово из строки: "${e.target.value}"`, 
                 arr_keystroke) 
}
// позиция каретки
function getCaret(node) {
  if (node.selectionStart) {
    return node.selectionStart;
  } else if (!document.selection) {
    return 0;
  }
  var c = "\001",
      sel = document.selection.createRange(),
      dul = sel.duplicate(),
      len = 0;
  dul.moveToElementText(node);
  sel.text = c;
  len = dul.text.indexOf(c);
  sel.moveStart('character',-1);
  sel.text = "";
  return len;
}

var prev_idx = 0;
var prev_char = "";
function handle(e) {
    idx_arr_keystroke = getCaret(e.target);
    var charCode = e.which || e.keyCode;
    if (charCode == 229) {
        var keyTimes = {};
        let K;
        if (e.type == "keydown") {
            android_keydown(keyTimes);
        } else if (e.type == "keyup") { 
            android_keyup(e, keyTimes);
        }            
    } else {
        static_handle(e, charCode);
    }
}

document.addEventListener('click', function (e) {
    if (e.target.id=="id_body") {
        e.target.onkeydown = e.target.onkeyup = e.target.onkeypress = e.target.onclick = handle; 
    }
});

Код уродлив, но точно понятен без объяснения каждой строки. Протестировал, работает. С нынешними вычислительными мощностями мобильных устройств, проблем с производительностью нет. Хорошей практикой будет сбор данных всех действий в вашем web проекте, в дополнение к реализации из статьи. Предложенный способ, поможет избежать вторичной обработки данных, получаемых с android устройств. Рабочий код https://github.com/naturalkind/social-network/blob/v0.5/static/js/keystroke.js

Комментарии (8)


  1. Genrehopper
    15.08.2023 09:03

    Такой себе код на js. Смешаны стили именования, var откуда то всплыл


    1. evilsadko Автор
      15.08.2023 09:03

      Ну почему, мне пришлось разбираться в js что бы сделать то что мне нужно, вместо того что бы скачать ваше решение и не напрягаться?


  1. space2pacman
    15.08.2023 09:03

    Что такое 1000.0?)


    1. evilsadko Автор
      15.08.2023 09:03

      ошибка?


    1. evilsadko Автор
      15.08.2023 09:03

      ваш вариант?


      1. space2pacman
        15.08.2023 09:03

        Просто 1000?


        1. evilsadko Автор
          15.08.2023 09:03

          Влияет на результат или производительность?


          1. space2pacman
            15.08.2023 09:03

            А зачем так писать? Смысл?