В этом посте я расскажу, как создать метод ввода жестами для Android. Данный метод ввода можно сделать похожим на рукописный ввод, но есть одно ограничение — нужно рисовать букву (цифру, слово) не отрывая пальца. В реализацию входит метод ввода и утилита для настройки жестов, которая является переделанным «Gesture Builder» из примеров к Android SDK.
Создание «клавиатуры»
Начнём с создания класса клавиатуры. Все методы ввода делаются на основе InputMethodService, а для распознавания жестов мы включили в него OnGesturePerformedListener. Чтобы не было проблем с распознаванием, каждый язык будет иметь отдельный файл. Для этого добавим в класс два свойства: карту языков и название текущего языка.
// HashMap<Название языка, библиотека жестов>
private HashMap<String, GestureLibrary> mGestureLibMap;
// Текущий язык
private String mCurrentGestureLib = null;
Загружать языки будет метод loadLanguages, который будет сканировать директорию файлов приложения и загружать всё содержимое как языки.
private void loadLanguages() {
// Собираем список "языков"
File[] files = getFilesDir().listFiles();
mGestureLibMap = new HashMap<String, GestureLibrary>();
// Загружаем "языки"
for(int i = 0; i < files.length; i++) {
GestureLibrary gestureLib = GestureLibraries.fromFile(files[i]);
if (!gestureLib.load()) {
Toast.makeText(this, "Can't load gestures " + files[i].getName(),
Toast.LENGTH_LONG).show();
continue;
}
// Добавляем язык в список
mGestureLibMap.put(files[i].getName(), gestureLib);
}
if(mCurrentGestureLib == null || !mGestureLibMap.containsKey(mCurrentGestureLib)) {
// Находим первый язык и устанавлием его
mCurrentGestureLib = mGestureLibMap.keySet().toArray(new String[0])[0];
if(mGestureLibMap.containsKey(mCurrentGestureLib.toLowerCase()))
mCurrentGestureLib = mCurrentGestureLib.toLowerCase();
}
}
loadLanguages будет вызываться в методе onStartInputView, который вызывается каждый раз при вызове клавиатуры. Данное решение возможно вызовет тормоза при большом количестве языков, но таким образом всегда будут «свежие» жесты. Можно было бы исхитриться с файлом последнего обновления жестов или static свойством, но я не стал хитрить так как не заметил никаких тормозов при стандартном наборе (en, ru, 09).
Жесты распознаются в методе onGesturePerformed (подробнее о жестах можно узнать тут). Если название жеста начинается с "@@", то будем считать, что за ним идёт keyCode, который будем посылать приложению. Если начинается с чего-то другого, то будем отсылать это как текст. Сам метод выглядит так:
public void onGesturePerformed(GestureOverlayView overlay, Gesture gesture) {
// Распознаём жест
ArrayList<Prediction> predictions = mGestureLibMap.get(mCurrentGestureLib).recognize(gesture);
if (predictions.size() > 0) {
// Выбираем самый подходящий вариант
Prediction prediction = predictions.get(0);
if(prediction.name.startsWith("@@")) {
// Если название начинаеться с @@, то посылаем нажатие кнопки с данным кодом
try {
keyDownUp(Integer.valueOf(prediction.name.substring(2)));
} catch(NumberFormatException e) {
Log.w("GestureKeyboard", "NumberFormatException: " + e.getLocalizedMessage());
}
} else {
// Посылаем название жеста как текст
getCurrentInputConnection().commitText(editString(prediction.name),
prediction.name.length());
}
}
}
В этом методе можно заметить не известный метод editString. Данный метод изменяет регистр в зависимости от состояния кнопок «Upper» и «Lower».
private String editString(String in) {
// Если включен lower, то преобразуем в LowerCase
if(mLowerLock.isChecked()) return in.toLowerCase();
// Если upper, то в UpperCase
if(mUpperLock.isChecked()) return in.toUpperCase();
// Или возвращаем оригинал
return in;
}
А метод keyDownUp посылает два события приложению – keyDown и keyUp. Он был взят из примера SoftKeyboard и выглядит так:
private void keyDownUp(int keyEventCode) {
// Посылаем события keyDown и keyUp
getCurrentInputConnection().sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyEventCode));
getCurrentInputConnection().sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyEventCode));
}
Мы загружали различные языки, значит, их можно менять. Меняются они связкой двух методов — onLangClick и setCurrentLanguage. При клике по кнопке onLangClick мы ищем в списке языков следующий за текущим, или (если такого нет) меняем язык на самый первый. При смене языка так же учитываться состояние кнопки «Caps Lock».
public void onLangClick(View v) {
// Клик по кнопке смены языка
// Получаем список языков
String gestureNames[] = mGestureLibMap.keySet().toArray(new String[0]);
boolean next = false;
for(int i = 0; i < gestureNames.length; i++) {
if(next) {
// Это следующий язык за текущим
if(!mCurrentGestureLib.toLowerCase().equals(gestureNames[i].toLowerCase())) {
// Это не текущий язык в другом регистре
setCurrentLanguage(gestureNames, i);
next = false;
break;
}
continue;
}
if(mCurrentGestureLib.toLowerCase().equals(gestureNames[i].toLowerCase())) {
// Нашли текущий язык в списке
next = true;
}
}
if(next) setCurrentLanguage(gestureNames, 0);
// Устанавливаем язык
mLangSwither.setText(mCurrentGestureLib);
}
private void setCurrentLanguage(String gestureNames[], int index) {
mCurrentGestureLib = gestureNames[index];
// Если есть этот язык в LowerCase, то включаем его
if(mGestureLibMap.containsKey(gestureNames[index].toLowerCase())) {
mCurrentGestureLib = gestureNames[index].toLowerCase();
}
// Если включен капс, то ищем этот язык в UpperCase и включаем если есть
if(mCapsLock.isChecked() && mGestureLibMap.containsKey(gestureNames[index].toUpperCase())) {
mCurrentGestureLib = gestureNames[index].toUpperCase();
}
}
Для смены регистра используются три кнопки: Upper, Lower, Caps. Что-то интересное представляет только капс. Язык может быть в двух регистрах. Регистр можно записать в названия языка (пример: en и EN). А кнопка капс переключает эти регистры или при их отсутствии изменяет состояние кнопок «Upper» и «Lower».
if(isChecked) {
mLowerLock.setChecked(false);
if(mGestureLibMap.containsKey(mCurrentGestureLib.toUpperCase())) {
// Устанавливаем язык в UpperCase
mUpperLock.setChecked(false);
mCurrentGestureLib = mCurrentGestureLib.toUpperCase();
} else {
// Если нет языка в UpperCase, то включаем UpperLock
mUpperLock.setChecked(true);
}
} else {
mUpperLock.setChecked(false);
if(mGestureLibMap.containsKey(mCurrentGestureLib.toLowerCase())) {
// Устанавливаем язык в LowerCase
mLowerLock.setChecked(false);
mCurrentGestureLib = mCurrentGestureLib.toLowerCase();
} else {
// Или включаем LowerLock
mLowerLock.setChecked(true);
}
}
// Устанавливем язык
mLangSwither.setText(mCurrentGestureLib);
Немного приукрасив UI получилось то что показано на скриншоте.
Класс метода ввода уже готов, теперь нужно в AndroidManifest.xml что-то такое:
<service
android:name="GestureKeyboard"
android:permission="android.permission.BIND_INPUT_METHOD">
<intent-filter>
<action android:name="android.view.InputMethod"/>
</intent-filter>
<meta-data android:name="android.view.im" android:resource="@xml/method"/>
</service>
Немного приукрасив UI, получилось то, что показано на скриншоте.
Про настройки расскажу на словах. С начала я взял исходники Gesture Builder из Android SDK, и скопировал в мой проект нужные исходники и ресурсы. Ресурсы и исходники были подчищены от «лишнего». Далее изменил класс GestureBuilderActivity так, чтобы он открывал указанные жесты (а не /sdcard/gestures). И сделал список языков с функциями добавление, удаления и переименования.
Результат
Получившиеся «клавиатура» не плохо распознаёт мой почерк, но пользоваться на постоянной основе её я не стал — клавиши мне удобней. Все исходники можно посмотреть на googlecode.com. Там же скачать apk со встроенным английским, русским, и цифровым (для моего почерка).