[Из песочницы] Поле ввода числовых значений в Android

В этой статье я бы хотел осветить вопрос ввода пользователем чисел с заданной точностью.
Давайте посмотрим что имеется в арсенале Android и решим эту задачу.

Начнем с требований


Основные требования:

  • Реализовать поле ввода, позволяющее вводить только числа с задаваемой точностью.
  • Точность задается количеством значимых цифр перед и после десятичной запятой.


Дополнительные требования:

  • Поддержка курсора ввода.
  • Стандартные опции редактирования: копировать, вырезать и вставить.


Анализ


Из всего спектра элементов управления в Android нас интересует виджет EditText.
Обратимся к документации и посмотрим, что нам предлагает Android SDK.

EditText наследуется от TextView, который, в свою очередь, обладает свойствами:
XML-разметка
digits— позволяет установить набор специальных символов, которые может принимать поле и автоматически включает режим ввода чисел.
numeric — задает обработчик ввода чисел.
inputType — с помощью набора константных значений позволяет сгенерировать требуемый обработчик ввода.

Публичные методы класса
setFilters — позволяет задать набор фильтров, которые будут применяться при вводе значений в поле.

digits, numeric и inputType позволяют ограничить набор вводимых символов, но никак не влияют на точность числа, а вот setFilters, как раз то, что нам нужно.

Реализация


Перед созданием собственного фильтра, сделаем так, чтобы в поле можно было вводить только числа и десятичный разделитель.

numberEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);


Теперь зададим собственный фильтр для поля ввода, чтобы принимать только значения соответствующие заданной точности.
Нам потребуется реализовать интерфейс InputFilter, а конкретно переопределить метод filter.

Согласно документации, этот метод вызывается при замене значения в поле ввода (dest) в диапазоне от dstart до dend на текст из буфера (source) в диапазоне от start до end.

Пример, для понимания написанного выше

Поле ввода содержит 123456 (dest), курсор находится в 1[23]456 (dstart = 1, dend = 3), из буфера вставляется значение 789 (source = 789, start = 0, end 3).


Метод возвращает объект класса CharSequence, который будет установлен в поле ввода взамен текущему значению. Если заменять значение поля ввода не требуется, то в методе следует вернуть null.

В конструктор нашего фильтра будем передавать количество символов до и после десятичного разделителя.

Конструктор фильтра
public NumberInputFilter(int digsBeforeDot, int digsAfterDot) {
    this.digsBeforeDot = digsBeforeDot;
    this.digsAfterDot = digsAfterDot;
}



Переопределяем метод filter. Алгоритм проверки вводимых значений следующий:

  1. До помещения буферного значения в поле ввода мы будем вставлять это значение в некоторую переменную.
  2. Если полученное значение удовлетворяет нашим требованиям точности, то мы разрешаем ввод, вернув null.
  3. В противном случае возвращаем пустую строку, тем самым отказываясь от вводимого значения.


Формируем будущее значение после ввода пользователя:
Для удобства манипуляция со строкой, создадим экземпляр класса StringBuilder на основе исходного значения поля ввода и произведем необходимые замены с учетом вводимого значения.

StringBuilder newText = new StringBuilder(dest).replace(dstart, dend, source.toString());


В результате в переменной newText будет содержаться будущее значения текстового поля.

Теперь нам следует проверить корректность нового значения.
Оно должно удовлетворять следующим условиям:

  1. Количество десятичных разделителей не должно превышать 1.
    123, 123.45 — верно, 12.3.4 — неверно.
  2. Количество символов целой и дробной частей числа должно удовлетворять заданной точности.


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

поиск десятичного разделителя
int size = newText.length();
int decInd = -1; // индекс десятичного разделителя
// проверяем десятичный разделитель
// количество разделителей не должно превышать 1
for (int i = 0; i < size; i++) {
    if (newText.charAt(i) == '.') {
        if (decInd < 0) {
            decInd = i; // запоминаем индекс разделителя
        } else { // разделителей более 1, некорректный ввод
            isValid = false;
            break;
        }
    }
}



Проверим корректность самого числа. Нам уже известен индекс десятичного разделителя, либо он отсутствует.
Остается только сравнить длину всего числа относительно индекса разделителя.

Проверка точности числа
if (decInd < 0) { // случай когда разделителя нет
    if (size > integerSize) { // проверяем длину всего числа
        isValid = false;
    }
} else if (decInd > digsBeforeDot) {// проверяем длину целой части
    isValid = false;
} else if (size - decInd - 1 > digsAfterDot) { // проверяем длину дробной части
    isValid = false;
}



В завершении возвращаем результат.
Если ввод корректный, то возвращаем null, тем самым принимая вводимые значения,
в противном случае возвращаем пустую строку, чтобы не передавать в поле ввода значения.

if (isValid) {
    return null;
} else {
    return "";
}


Полный исходный код@github

Отладка


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

Видео
e016fb223cfe4c85b7b199d1e36ab03a.gif


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

Об этом упоминается в документации

Be careful to not to reject 0-length replacements, as this is what happens when you delete text.


Нужно быть осторожным с заменами нулевой длины, как это происходит при удалении текста.
Что же произошло на самом деле?

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

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

Обработка удаления символов
if (isValid) {
    return null;
} else if (source.equals("")) { // обрабатываем удаление
    return dest.subSequence(dstart, dend); // возвращаем удаленные символы
} else {
    return "";
}



Запускаем проект и проверяем.

Видео
207107fdb07b4d15a83e408148d43ca8.gif


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

Библиотека успешно используется в продакшене.
Подключить её можно с помощью Gradle

repositories {
        maven { url "https://raw.githubusercontent.com/hyperax/Android-NumberEditText/master/maven-repo" }
}
compile 'ru.softbalance.widgets:NumberEditText:1.1.2'

Полный исходный код проекта@github.

Бонус


Часто бывает необходимым запретить отображение системной клавиатуры для поля ввода.
Например, если используется клавиатура из приложения.

На помощь приходит метод класса EditText, появившийся в Android версии 21 и выше:
setShowSoftInputOnFocus (boolean show)

Вообще-то, этот метод был и в ранних версиях, но был приватным.

добавим свой метод для поля ввода showSoftInputOnFocusCompat
public void showSoftInputOnFocusCompat (boolean isShow) {
        showSoftInputOnFocus = isShow;
        if (Build.VERSION.SDK_INT >= 21) {
            setShowSoftInputOnFocus(showSoftInputOnFocus);
        } else {
            try {
                final Method method = EditText.class.getMethod("setShowSoftInputOnFocus", boolean.class);
                method.setAccessible(true);
                method.invoke(this, showSoftInputOnFocus);
            } catch (Exception e) {
                // ignore
            }
        }
    }



Да это «хак», однако это наиболее простой и короткий способ скрыть системную клавиатуру для поля ввода.

© Habrahabr.ru