Средство форматирования маски Android, которое допускает только целые числа, но с текстом в маске - PullRequest
0 голосов
/ 25 мая 2018

В Android мне нужно создать редактируемый ввод, который будет содержать статические текстовые элементы, которые не меняются, и другие значения, которые должны быть заменены числами, когда пользователь вводит значения, для которых используется символ "#".Замены должны быть только целыми числами от 0 до 9.Например, маска может быть «SERIAL NO #####», где, когда пользователь вводит числа, значения «#» будут заменены, в конечном итоге получая строковый результат «SERIAL NO 12309».

У нас есть существующий код, который использует MaskFormatter, но он исключает синтаксический анализ для масок с любыми символами в них, как описано выше (хотя он отлично работает только с "#").

Кроме того, эта маска может широко варьироваться.От простых масок, таких как "####", до более сложных масок, таких как "### A - ## WHATEVER", до "A # A $ # RRT #", где только "#" должно разрешать числовые значения при наборе текста.

Есть ли простой способ сделать это или мне нужно написать собственный код синтаксического анализа?Является ли MaskFormatter правильным подходом или есть более элегантный механизм?Я почти уверен, что могу написать собственный код для этого, но я бы предпочел стандартное решение.

Вот визуализация поля:

enter image description here

А вот существующий код (я его не писал, был всегда):

    public class MaskedWatcher implements TextWatcher {

    private String mMask;
    String mResult = "";    
    String mPrevResult = "";

    public MaskedWatcher(String mask){
        mMask = mask;
    }

    public void afterTextChanged(Editable s) {

        String mask = mMask;
        String value = s.toString();

        if(value.equals(mResult)) {
            return;
        }

        try {

            // prepare the formatter
            MaskedFormatter formatter = new MaskedFormatter(mask);
            formatter.setValueContainsLiteralCharacters(true);
            formatter.setPlaceholderCharacter((char)1);

            // get a string with applied mask and placeholder chars
            value = formatter.valueToString(value);

            try{
                // find first placeholder
                if ( value.indexOf((char)1) != -1) {
                    value = value.substring(0, value.indexOf((char)1));

                    //process a mask char
                    if(value.charAt(value.length()-1) == mask.charAt(value.length()-1) && ((value.length()-1) >= 0)){
                        value = value.substring(0, value.length() - 1);
                    }
                }
            }
            catch(Exception e){
                Utilities.logException(e);
            }

            // if we are deleting characters reset value and start over
            if(mPrevResult.trim().length() > value.trim().length()) {
                value = "";
            }

            setFieldValue(value);
            mResult = value;
            mPrevResult = value;
            s.replace(0, s.length(), value);
        } 
        catch (ParseException e) {
            //the entered value does not match a mask
            if(mResult.length() >= mMask.length()) {
                if(value.length() > mMask.length()) {
                    value = value.substring(0, mMask.length());
                }
                else {
                    value = "";
                    setFieldValue(value);
                    mPrevResult = value;
                    mResult = value;
                }
            }
            else {
                int offset = e.getErrorOffset();
                value = removeCharAt(value, offset);
            }
            s.replace(0, s.length(), value);
        }
    }

1 Ответ

0 голосов
/ 31 мая 2018

Хорошо, теперь я знаю, почему никто не ответил на это - это противно.Я провел много исследований и не мог найти ничего даже отдаленно похожего - возможно, я не очень хорош в поиске.Сначала немного истории моего исследования.Сначала я думал, что могу просто наблюдать за нажатиями клавиш в поле и реагировать на них.На самом деле, нет.Вы можете сделать это с помощью жесткой клавиатуры, но не мягкой.Я попробовал различные методы против устройства Samsung без успеха.Может быть, кто-то знает фокус, но я не смог его найти.Поэтому я выбрал единственный доступный вариант - TextWatcher.Единственная реальная проблема заключается в том, что вы не можете реально увидеть, какая клавиша была нажата, чтобы среагировать (была добавлена ​​цифра или нажата клавиша удаления?), Поэтому вы должны проверить предыдущую строку с текущей измененной строкой и сделать все возможное, чтобы определитьчто изменилось и что с этим делать.

Просто чтобы помочь, поведение, которое я достиг, было в основном для всех пользователей, чтобы вводить числа (0-9) и НЕ изменять другие элементы маски.Также мне нужно было переместить курсор в правильное положение при вводе или удалении элементов.Кроме того, если мы удалили их, нам нужно было удалить соответствующий элемент и вернуть маску обратно.

Например, если маска «ADX - ### - R», то при вводе произойдет следующее:

Given : "ADX-###-R" Typing: "4" Results: "ADX-4##-R" Cursor at "4"
Given : "ADX-4##-R" Typing: "3" Results: "ADX-43#-R" Cursor at "3"
Given : "ADX-43#-R" Typing: "1" Results: "ADX-431-R" Cursor at end of string
Given : "ADX-431-R" Typing: "Del" Results: "ADX-43#-R" Cursor at "3"

В этом и заключается его суть.У нас также есть требование для значений Hint / Placeholder и Default, которые я оставил. Теперь код.

Вот скриншот того, как это выглядит:

enter image description here

Первый XML:

<LinearLayout 
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:focusable="true"
    android:focusableInTouchMode="true">

    <TextView
        android:id="@+id/name"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:textStyle="normal"
        android:paddingLeft="5dp"
        android:paddingRight="5dp"
        android:text="" />

    <EditText android:id="@+id/entry"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:gravity="right"
        android:singleLine="true"
        android:maxLines="1"
        android:ellipsize="end" /> 

    <View
        android:layout_marginTop="8dp"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="@android:color/darker_gray"
        android:visibility="gone" />

</LinearLayout>

Основной код поля:

public class FormattedInput extends LinearLayout {

    private Context mContext;
    private Field mField;
    private TextView mName;
    private EditText mEntry;
    private Boolean mEnableEvents = true;
    private String mPlaceholderText = "";
    private final static String REPLACE_CHAR = " "; // Replace missing data with blank

    public FormattedInput(Context context, Field field) {
        super(context);

        mContext = context;
        mField = field;

        initialize();
        render(mField);
    }

    private void initialize() {

        LayoutInflater inflater = (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.field_formatted_input, this);

        // setup fields
        mName = (TextView)findViewById(R.id.name);
        mEntry = (EditText)findViewById(R.id.entry);
        mEntry.setFocusable(true);
        mEntry.setRawInputType(Configuration.KEYBOARD_QWERTY);
        mEntry.addTextChangedListener(
                new MaskedWatcher(mField.getDisplayMask())
        );
    }

    public void render(Field field) {
        mName.setText(mField.getFieldName());

        mPlaceholderText = mField.getPlaceholderText();
        if(Utilities.stringIsBlank(mPlaceholderText)) {
            mPlaceholderText = mField.getDisplayMask();
        }
        mEntry.setHint(mPlaceholderText);
        mEntry.setHintTextColor(Color.GRAY);

        if(!Utilities.stringIsBlank(mField.getValue())) {
            mEnableEvents = false;
            String value =  String.valueOf(mField.getValue());
            if (value.equalsIgnoreCase(mField.getDisplayMask()))
                mEntry.setText(mField.getDisplayMask());
            else {
                String val = fillValueWithMask(value, mField.getDisplayMask());
                mEntry.setText(val);
            }
            mEnableEvents = true;
        }
        else if (!Utilities.stringIsBlank(mField.getDefaultValue())) {
            mEnableEvents = false;
            String val = fillValueWithMask(mField.getDefaultValue(), mField.getDisplayMask());
            mEntry.setText(val);
            mEnableEvents = true;
        }
        else {
            mEnableEvents = false;
            mEntry.setText(null);
            mEnableEvents = true;
        }
    }

    public static String fillValueWithMask(String value, String mask) {
        StringBuffer result = new StringBuffer(mask);
        for (int i = 0; i < value.length() && i <= mask.length()-1 ; i++){
            if (mask.charAt(i) == '#' && value.charAt(i) != ' ' && Character.isDigit(value.charAt(i)))
                result.setCharAt(i,value.charAt(i));
        }
        return result.toString();
    }

    public class MaskedWatcher implements TextWatcher {

        private String mMask;
        String mResult = "";    
        String mPrevResult = "";
        int deletePosition = 0;

        public MaskedWatcher(String mask){
            mMask = mask;
        }

        public void afterTextChanged(Editable s) {

            String value = s.toString();

            // No Change, return - or reset of field
            if (value.equals(mPrevResult) && (!Utilities.stringIsBlank(value) && !Utilities.stringIsBlank(mPrevResult))) {
                return;
            }

            String diff = value;
            // First time in and no value, set value to mask
            if (Utilities.stringIsBlank(mPrevResult) && Utilities.stringIsBlank(value)) {
                mPrevResult = mMask;
                mEntry.setText(mPrevResult);
            }
            // If time, but have value
            else if (Utilities.stringIsBlank(mPrevResult) && !Utilities.stringIsBlank(value)) {
                mPrevResult = value;
                mEntry.setText(mPrevResult);
            }
            // Handle other cases of delete and new value, or no more typing allowed
            else {
                // If the new value is larger or equal than the previous value, we have a new value
                if (value.length() >= mPrevResult.length())
                    diff = Utilities.difference(mPrevResult, value);

                // See if new string is smaller, if so it was a delete.
                if (value.length() < mPrevResult.length()) {
                    mPrevResult = removeCharAt(mPrevResult, deletePosition);
                    // Deleted back to mask, reset
                    if (mPrevResult.equalsIgnoreCase(mMask)) {
                        mPrevResult = "";
                        setFieldValue("");
                        mEntry.setText("");
                        mEntry.setHint(mPlaceholderText);
                        return;
                    }
                    // Otherwise set value
                    else
                        setFieldValue(mPrevResult);
                    mEntry.setText(mPrevResult);
                }
                // A new value was added, add to end
                else if (mPrevResult.indexOf('#') != -1) {
                    mPrevResult = mPrevResult.replaceFirst("#", diff);
                    mEntry.setText(mPrevResult);
                    setFieldValue(mPrevResult);
                }
                // Unallowed change, reset the value back
                else {
                    mEntry.setText(mPrevResult);
                }
            }

            // Move cursor to next spot
            int i = mPrevResult.indexOf('#');
            if (i != -1)
                mEntry.setSelection(i);
            else
                mEntry.setSelection(mPrevResult.length());
        }

        private void setFieldValue(String value) {
            //mEnableEvents = false;
            if(mEnableEvents == false) {
                return;
            }
            // Set the value or do whatever you want to do to save or react to the change
        }

        private String replaceMask(String str) {
            return str.replaceAll("#",REPLACE_CHAR);
        }

        private String removeCharAt(String str, int pos) {
            StringBuilder info = new StringBuilder(str);
            // If the position is a mask character, change it, else ignore the change
            if (mMask.charAt(pos) == '#') {
                info.setCharAt(pos, '#');
                return info.toString();
            }
            else {
                Toast.makeText(mContext, "The mask value can't be deleted, only modifiable portion", Toast.LENGTH_SHORT);
                return str;
            }
        }

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

        public void onTextChanged(CharSequence s, int start, int before, int count) {
            deletePosition = start;
        }

    }
}

Код утилиты:

public static boolean stringIsBlank(String stringValue) {
    if (stringValue != null) {
        return stringValue.trim().length() <= 0;
    } else {
        return true;
    }
}

и

public static String difference(String str1, String str2) {
    int at = indexOfDifference(str1, str2);
    if (at == -1) {
        return "";
    }
    return str2.substring(at,at+1);
}

И класс поля ... вам нужно будет добавитьгеттеры и сеттеры:

public class Field {
    private String defaultValue;
    private Object value;
    private String displayMask;
    private String placeholderText;
}

Несколько заключительных мыслей.Основной механизм заключается в сравнении предыдущей строки с текущей строкой.Если новая строка меньше, то мы удаляем и используем deletePosition, пока позиция соответствует «#» в маске, поскольку другие символы не могут быть изменены.Существуют также проблемы с появлением предыдущего значения - и предполагается, что если это значение входит в значения «#», то при отсутствии будет заменено на «» (пробелы).Field не является необходимым, но был вспомогательным классом, который в нашем случае имеет множество других функций.Надеюсь, это кому-нибудь поможет!

...