четверг, 19 апреля 2018 г.

Форматируемый EditText для номеров телефона или email'ов

Как-то на одном проекте нужно было сделать формат ввода телефона прям очень жестким, что бы оно не давало пользователю двинуться ни вправо ни влево. Полазив по интернету нашел кучу либ которые умеют красивенько это делать, но ради одной строчки не хотелось подключать целую библиотеку и начал я еще по интернету лазить и набрел на какой-то репозиторий на котором я отрыл прям кусок кода — кастомного EditText который умеет все тоже самое что и либы, только это все в одном файле и его можно редактировать как душе угодно!

image

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

Сама вьюха является наследником EditText'a, соответственно все методы что доступны у EditText'a будут доступны и этой вьюхе, только она еще будет это красивенько форматировать под стиль который мы укажем в верстке этой вьюхи.

MaskedEditText.java
import android.content.Context;
import android.content.res.TypedArray;
import android.text.Editable;
import android.text.InputFilter;
import android.text.Selection;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextWatcher;
import android.util.AttributeSet;

import project.dajver.com.maskedittext.R;

public class MaskedEditText extends android.support.v7.widget.AppCompatEditText {

    private static final char NUMBER_MASK = '9';
    private static final char ALPHA_MASK = 'A';
    private static final char ALPHANUMERIC_MASK = '*';
    private static final char CHARACTER_MASK = '?';
    private static final char ESCAPE_CHAR = '\\';

    private String mask;
    private String placeholder;

    public MaskedEditText(Context context) {
        this(context, "");
    }

    public MaskedEditText(Context context, String mask) {
        this(context, mask, ' ');
    }

    public MaskedEditText(Context context, String mask, char placeholder) {
        this(context, null, mask, placeholder);
    }

    public MaskedEditText(Context context, AttributeSet attr) {
        this(context, attr, "");
    }

    public MaskedEditText(Context context, AttributeSet attr, String mask) {
        this(context, attr, "", ' ');
    }

    public MaskedEditText(Context context, AttributeSet attr, String mask, char placeholder) {
        super(context, attr);

        TypedArray a = context.obtainStyledAttributes(attr, R.styleable.MaskedEditText);
        final int N = a.getIndexCount();

        for (int i = 0; i < N; ++i)
        {
            int at = a.getIndex(i);
            switch (at)
            {
                case R.styleable.MaskedEditText_mask:
                    mask = (mask.length() > 0 ? mask : a.getString(at));
                    break;
                case R.styleable.MaskedEditText_placeholder:
                    placeholder = (a.getString(at).length() > 0 && placeholder == ' ' ? a.getString(at).charAt(0) : placeholder);
                    break;
            }
        }

        a.recycle();

        this.mask = mask;
        this.placeholder = String.valueOf(placeholder);
        addTextChangedListener(new MaskTextWatcher());

        if (mask.length() > 0)
            setText(getText()); // sets the text to create the mask
    }

    public String getMask() {
        return mask;
    }

    public void setMask(String mask) {
        this.mask = mask;
        setText(getText());
    }

    public char getPlaceholder() {
        return placeholder.charAt(0);
    }

    public void setPlaceholder(char placeholder) {
        this.placeholder = String.valueOf(placeholder);
        setText(getText());
    }

    public Editable getText(boolean removeMask) {
        if (!removeMask) {
            return getText();
        } else {
            SpannableStringBuilder value = new SpannableStringBuilder(getText());
            stripMaskChars(value);

            return value;
        }
    }

    private void formatMask(Editable value) {
        InputFilter[] inputFilters = value.getFilters();
        value.setFilters(new InputFilter[0]);

        int i = 0;
        int j = 0;
        int maskLength = 0;
        boolean treatNextCharAsLiteral = false;

        Object selection = new Object();
        value.setSpan(selection, Selection.getSelectionStart(value), Selection.getSelectionEnd(value), Spanned.SPAN_MARK_MARK);

        while (i < mask.length()) {
            if (!treatNextCharAsLiteral && isMaskChar(mask.charAt(i))) {
                if (j >= value.length()) {
                    value.insert(j, placeholder);
                    value.setSpan(new PlaceholderSpan(), j, j + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                    j++;
                } else if (!matchMask(mask.charAt(i), value.charAt(j))) {
                    value.delete(j, j + 1);
                    i--;
                    maskLength--;
                } else {
                    j++;
                }

                maskLength++;
            } else if (!treatNextCharAsLiteral && mask.charAt(i) == ESCAPE_CHAR) {
                treatNextCharAsLiteral = true;
            } else {
                value.insert(j, String.valueOf(mask.charAt(i)));
                value.setSpan(new LiteralSpan(), j, j + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                treatNextCharAsLiteral = false;

                j++;
                maskLength++;
            }

            i++;
        }

        while (value.length() > maskLength) {
            int pos = value.length() - 1;
            value.delete(pos, pos + 1);
        }

        Selection.setSelection(value, value.getSpanStart(selection), value.getSpanEnd(selection));
        value.removeSpan(selection);

        value.setFilters(inputFilters);
    }

    private void stripMaskChars(Editable value) {
        PlaceholderSpan[] pspans = value.getSpans(0, value.length(), PlaceholderSpan.class);
        LiteralSpan[] lspans = value.getSpans(0, value.length(), LiteralSpan.class);

        for (int k = 0; k < pspans.length; k++) {
            value.delete(value.getSpanStart(pspans[k]), value.getSpanEnd(pspans[k]));
        }

        for (int k = 0; k < lspans.length; k++) {
            value.delete(value.getSpanStart(lspans[k]), value.getSpanEnd(lspans[k]));
        }
    }

    private boolean matchMask(char mask, char value) {
        boolean ret = (mask == NUMBER_MASK && Character.isDigit(value));
        ret = ret || (mask == ALPHA_MASK && Character.isLetter(value));
        ret = ret || (mask == ALPHANUMERIC_MASK && (Character.isDigit(value) || Character.isLetter(value)));
        ret = ret || mask == CHARACTER_MASK;

        return ret;
    }

    private boolean isMaskChar(char mask) {
        switch (mask) {
            case NUMBER_MASK:
            case ALPHA_MASK:
            case ALPHANUMERIC_MASK:
            case CHARACTER_MASK:
                return true;
        }

        return false;
    }

    private class MaskTextWatcher implements TextWatcher {
        private boolean updating = false;

        @Override
        public void afterTextChanged(Editable s) {
            if (updating || mask.length() == 0)
                return;

            if (!updating) {
                updating = true;

                stripMaskChars(s);
                formatMask(s);

                updating = false;
            }
        }

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
        }
    }

    private class PlaceholderSpan {
        // this class is used just to keep track of placeholders in the text
    }

    private class LiteralSpan {
        // this class is used just to keep track of literal chars in the text
    }
}

Кода много, делает он многое, но вся настройка происходит именно тут. Маска у нас вся будет настраивается из xml, соответственно нам нужен файл атрибутов для того что бы эти параметры передавать из xml во вьюху, для этого нам нужну создать файл attrs.xml и в него вписать нужные нам атрибуты.

attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MaskedEditText">
        <attr name="mask" format="string" />
        <attr name="placeholder" format="string" />
    </declare-styleable>
</resources>

А дальше смотрим в констркутор который у нас самый последний, и в нем у нас идет настройка, в нем задается сама маска в виде 99 (999) 99-99-999, и задается разделитель который будет внизу под цифрами, остальной код у нас добавляет этот фильтр на текст и все. Я, честно, весь код рассматривать не буду, его тут много, часть сетит текст в вьюху, часть изменяет его, и несколько методов которые возвращают текст отформатированный или же пустой, то есть чисто введенные цифры.

Дальше нам нужно это как-то использовать, а как? Просто. Достаточно добавить в наш activity_main нашу вьюху, и дальше настроить ее на наш лад.

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="20dp">

    <project.dajver.com.maskedittext.view.MaskedEditText
        android:id="@+id/phone"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="phone"
        app:mask="+99 (999) 999-99-99"
        app:placeholder="_" />

</LinearLayout>

Мы задали формат маски — "+99 (999) 999-99-99", и задали что бы разделителем был "_", дальше поставили тип ввода что бы были цифры без точек и запятых, только цифры, и на этом все. После запуска, можно будет проверить как оно будет работать. Очень удобно и просто в использовании, спасибо тому доброму человеку который это сделал :)

Исходники:
GitHub

3 комментария:

  1. Приложение на HTC U11+ умирает при использовании MaskedEditText. К сожалению stacktrace ничего не говорит, кроме этого:
    DeadSystemException: The system died; earlier logs will point to the root cause

    ОтветитьУдалить
    Ответы
    1. Это не в коде проблема, а в самой системе. Вот объяснение на stackoverflow https://stackoverflow.com/a/44395215

      Удалить
  2. Этот комментарий был удален автором.

    ОтветитьУдалить