Как я могу изменить текст EditText без запуска Text Watcher?

У меня есть EditText поле с наблюдателем текста клиента на нем. В куске кода мне нужно изменить значение в EditText, которое я делаю, используя .setText("whatever"),

Проблема в том, как только я сделаю это изменение afterTextChanged вызывается метод, который создал бесконечный цикл. Как я могу изменить текст без его срабатывания после TextChanged?

Мне нужен текст в методе afterTextChanged, поэтому не рекомендуется удалять TextWatcher,

19 ответов

Решение

Вы можете отменить регистрацию наблюдателя, а затем перерегистрировать его.

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

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

EditText myEditText = (EditText) findViewById(R.id.myEditText);

myEditText.addTextChangedListener(new TextWatcher() {

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        if (getCurrentFocus() == myEditText) {
            // is only executed if the EditText was directly changed by the user
        }
    }

    //...
});

Изменить: как правильно упомянул LairdPleng, это не работает, если myEditText уже имеет фокус, и вы программно измените текст. Итак, перед звонком myEditText.setText(...) ты должен позвонить myEditText.clearFocus() как сказал Chack, что решает и эту проблему.

public class MyTextWatcher implements TextWatcher {
    private EditText et;

    // Pass the EditText instance to TextWatcher by constructor
    public MyTextWatcher(EditText et) {
        this.et = et;
    }

    @Override
    public void afterTextChanged(Editable s) {
        // Unregister self before setText
        et.removeTextChangedListener(this);

        et.setText("text");

        // Re-register self after setText
        et.addTextChangedListener(this);
    }
}

Использование:

et_text.addTextChangedListener(new MyTextWatcher(et_text));

Обновить:

Чтобы обновить текст более плавно, замените

et.setText("text");

с

s.replace(0, s.length(), "text");

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

например,

TextWatcher tw = new TextWatcher() {
  private String lastValue = "";

  @Override
  public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
  }

  @Override
  public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
  }

  @Override
  public void afterTextChanged(Editable editable) {

    // Return value of getNewValue() must only depend
    // on the input and not previous state
    String newValue = getNewValue(editText.getText().toString());
    if (!newValue.equals(lastValue)) {
      lastValue = newValue;

      editText.setText(newValue);
    }
  }
};

Вы можете использовать синтаксис Kotlin DSL, чтобы иметь общее решение для этого:

fun TextView.applyWithDisabledTextWatcher(textWatcher: TextWatcher, codeBlock: TextView.() -> Unit) {
    this.removeTextChangedListener(textWatcher)
    codeBlock()
    this.addTextChangedListener(textWatcher)
}

А внутри вашего TextWatcher вы можете использовать его как:

editText.applyWithDisabledTextWatcher(this) {
    text = formField.name
}

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

Установка текста и тега программно

editText.tag = "dummyTag"
editText.setText("whatever")
editText.tag = null

Проверка на tag в onTextChanged

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
    if (editText.tag == null) {
       // your code
    }
}

Я использую этот способ:

mEditText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

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

            @Override
            public void afterTextChanged(Editable s) {
                if (mEditText.isFocused()) { //<-- check if is focused 
                    mEditText.setTag(true);
                }
            }
        });

И каждый раз, когда вам нужно изменить текст программно, сначала очистите фокус

mEditText.clearFocus();
mEditText.setText(lastAddress.complement);

Это хорошо работает для меня

EditText inputFileName; // = (EditText)findViewbyId(R.id...)
inputFileName.addTextChangedListener(new TextWatcher() {
        public void afterTextChanged(Editable s) {

            //unregistering for event in order to prevent infinity loop
            inputFileName.removeTextChangedListener(this);

            //changing input's text
            String regex = "[^a-z0-9A-Z\\s_\\-]";
            String fileName = s.toString();
            fileName = fileName.replaceAll(regex, "");
            s.replace(0, s.length(), fileName); //here is setting new text

            Log.d("tag", "----> FINAL FILE NAME: " + fileName);

            //registering back for text changes
            inputFileName.addTextChangedListener(this);
        }

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

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

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

if (getCurrentFocus() == editText) {
    editText.clearFocus();
    editText.setText("...");
    editText.requestFocus();
}

Просто сделай это так

editText.addTextChangedListener(object : TextWatcher {

        private var isEditing = false

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {


        }

        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {

        }

        override fun afterTextChanged(s: Editable?) {
            if(!isEditing){
                isEditing = true
                editText.setText("Hello World!")
                isEditing = false
            }

        }
    })

таким образом, он не имеет бесконечного цикла

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

public class EditTexts {
    public final static class EditTextChangeListener implements TextWatcher {
        private final Consumer<String> onEditTextChanged;
        private boolean ignoreNextChange = false;
        public EditTextChangeListener(Consumer<String> onEditTextChanged){
            this.onEditTextChanged = onEditTextChanged;
        }
        public void ignoreNextChange(){
            ignoreNextChange = true;
        }
        @Override public void beforeTextChanged(CharSequence __, int ___, int ____, int _____) { }
        @Override public void onTextChanged(CharSequence __, int ___, int ____, int _____) { }
        @Override public void afterTextChanged(Editable s) {
            if (ignoreNextChange){
                ignoreNextChange = false;
            } else {
                onEditTextChanged.accept(s.toString());
            }
        }
    }
}

Используйте это так:

EditTexts.EditTextChangeListener listener = new EditTexts.EditTextChangeListener(s -> doSomethingWithString(s));
editText.addTextChangedListener(listener);

Всякий раз, когда вы хотите изменить содержимое editText не вызывая каскад рекурсивных изменений, сделайте это:

listener.ignoreNextChange();
editText.setText("whatever"); // this won't trigger the listener

Попробуйте эту логику: я хотел установить текст (""), не переходя в бесконечный цикл, и этот код работает для меня. Я надеюсь, что вы можете изменить это, чтобы соответствовать вашим требованиям

        final EditText text= (EditText)findViewById(R.id.text);
        text.addTextChangedListener(new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        }
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {

        }
        @Override
        public void afterTextChanged(Editable s) {
            if(s.toString().isEmpty())return;
            text.setText("");
            //your code
        }
    });

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

Мой вариант:

public class CustomEditText extends AppCompatEditText{
    TextWatcher l;

    public CustomEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public void setOnTextChangeListener(TextWatcher l) {
        try {
            removeTextChangedListener(this.l);
        } catch (Throwable e) {}
        addTextChangedListener(l);
        this.l = l;
    }

    public void setNewText(CharSequence s) {
        final TextWatcher l = this.l;
        setOnTextChangeListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

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

            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });
        setText(s);
        post(new Runnable() {
            @Override
            public void run() {
                setOnTextChangeListener(l);
            }
        });
    }


}

Установите слушателей только с помощью setOnTextChangeListener() и установите текст только с помощью setNewText (я хотел переопределить setText(), но это окончательно)

Я создал абстрактный класс, который смягчает циклическую проблему, когда изменение в EditText производится через TextWatcher.

/**
 * An extension of TextWatcher which stops further callbacks being called as a result of a change
 * happening within the callbacks themselves.
 */
public abstract class EditableTextWatcher implements TextWatcher {

    private boolean editing;

    @Override
    public final void beforeTextChanged(CharSequence s, int start, int count, int after) {
        if (editing)
            return;

        editing = true;
        try {
            beforeTextChange(s, start, count, after);
        } finally {
            editing = false;
        }
    }

    abstract void beforeTextChange(CharSequence s, int start, int count, int after);

    @Override
    public final void onTextChanged(CharSequence s, int start, int before, int count) {
    if (editing)
        return;

        editing = true;
        try {
            onTextChange(s, start, before, count);
        } finally {
            editing = false;
        }
    }

    abstract void onTextChange(CharSequence s, int start, int before, int count);

    @Override
    public final void afterTextChanged(Editable s) {
        if (editing)
            return;

        editing = true;
        try {
            afterTextChange(s);
        } finally {
            editing = false;
        }
    }    

    public boolean isEditing() {
        return editing;
    }

    abstract void afterTextChange(Editable s);
}

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

Я создал следующий TextWatcher

      class ControlledTextWatcher(
    private val parent: TextView,
    private val onChange: ((text: CharSequence?, start: Int, before: Int, count: Int) -> Unit)?,
    private val beforeChange: ((text: CharSequence?, start: Int, count: Int, after: Int) -> Unit)? = null,
    private val afterChange: ((editable: Editable?) -> Unit)? = null
) : TextWatcher {
    init {
        parent.addTextChangedListener(this)
    }

    private var enabled = true

    var text: String?
        get() = parent.value
        set(value) {
            this.enabled = false
            parent.text = value
            this.enabled = true
        }

    var res: Int
        get() = throw RuntimeException("String resource cannot be retrieved after being set")
        set(value) {
            parent.text = parent.context.getString(value)
        }


    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
        if (enabled)
            beforeChange?.invoke(s, start, count, after)
    }

    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        if (enabled)
            onChange?.invoke(s, start, before, count)
    }

    override fun afterTextChanged(s: Editable?) {
        if (enabled)
            afterChange?.invoke(s)
    }

    fun detach() {
        parent.removeTextChangedListener(this)
    }
}

и я использую его в основном с привязками просмотра, например

class TestActivity: AppCompatActivity () {

      class TestActivity : AppCompatActivity() {
    private lateinit var binding: ActivityTestBinding
    private val edit by lazy { ControlledTextWatcher(binding.text, this::textChanged }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityTestBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

поэтому, когда я хочу внести изменения в фактическое, я использую text или res атрибут подобного так:

      edit.text = "hello world" //this does not trigger the text watcher

но когда пользователь изменяет, он запускает

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

также вы должны быть осторожны при внесении изменений в afterChange потому что изменение опубликовано в TextView так что вы можете получить бесконечный цикл

Очень просто, установите текст с помощью этого метода

void updateText(EditText et, String text) {
   if (!et.getText().toString().equals(text))
       et.setText(text);
}

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

Наиболее распространенной ошибкой является установка нового текста в связанном EditText или Editable, даже если текст фактически не изменялся.

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

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

Мой способ сделать это:

В сегменте записи

        EditText e_q;

        e_q = (EditText) parentView.findViewWithTag("Bla" + i);

        int id=e_q.getId();
        e_q.setId(-1);
        e_q.setText("abcd...");
        e_q.setId(id);

Слушатель

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

        int id = view.getId();
        if(id==-1)return;

        ....

Все равно работает.

Другие вопросы по тегам