我的EditText控件文本更改后0.5秒如何实现某项操作?

78

我正在使用EditText控件过滤我的列表。我希望在用户完成在EditText中输入后0.5秒后再过滤列表。我使用了TextWatcher的afterTextChanged事件来实现这个目的。但是这个事件会在EditText中每次字符改变时触发。

我该怎么办?

17个回答

173

使用:

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

        private Timer timer = new Timer();
        private final long DELAY = 1000; // Milliseconds

        @Override
        public void afterTextChanged(final Editable s) {
            timer.cancel();
            timer = new Timer();
            timer.schedule(
                new TimerTask() {
                    @Override
                    public void run() {
                        // TODO: Do what you need here (refresh list).
                        // You will probably need to use
                        // runOnUiThread(Runnable action) for some
                        // specific actions (e.g., manipulating views).
                    }
                },
                DELAY
            );
        }
    }
);

每次文本框EditText中的文本发生变化时,关键是取消和重新安排Timer

要设置延迟多长时间,请参见此帖子


3
请检查一下这个链接,https://dev59.com/rmkv5IYBdhLWcg3w_Vmn。该链接讨论了如何在用户输入时避免多次触发EditText的事件。 - Saurabh
delayInMillis 定义在哪里?它能以某种方式传递吗? - gonzobrains
当然,delayInMillis是任何代表延迟的long变量(在这种情况下为0.5秒)。您也可以使用500代替它。请参阅我的更新答案。 - Berťák
1
对于那些想要在UI线程上运行的人...可以使用((Activity)getContext()).runOnUiThread()或者getActivity().runOnUiThread()。 - Ken Ratanachai S.
如果TimerTask的代码很大,那么每次都创建它是否更好(因为每次按键都会调用schedule),还是重复使用一个对象?我也尝试过不再创建时间,但似乎当计时器被取消一次后,它就不能被重用于新的计划了。 - Damn Vegetables
显示剩余2条评论

56

最好使用Handler与postDelayed()方法。在Android的实现中,Timer每次运行任务都会创建一个新线程。Handler有自己的Looper,可以附加到我们希望的任何线程,因此我们不需要额外支付创建线程的成本。

示例

 Handler handler = new Handler(Looper.getMainLooper() /*UI thread*/);
 Runnable workRunnable;
 @Override public void afterTextChanged(Editable s) {
    handler.removeCallbacks(workRunnable);
    workRunnable = () -> doSmth(s.toString());
    handler.postDelayed(workRunnable, 500 /*delay*/);
 }

 private final void doSmth(String str) {
    //
 }

1
为什么需要移除回调函数? - TheLearner
@TheLearner 所以它不会被多次调用。如果您不取消之前的调用,它仍然会被执行。这就让我们回到了这个问题的核心,即要对其进行去抖处理。 - Denny
我知道这是一个旧答案,但是如果您安排了这个Runnable在一段时间后进行调用,并且上下文被删除,例如因为用户离开了此屏幕,您认为会发生什么? - Darwind
在Fragment/Activity的onPause中应该另外调用handler.removeCallbacks(workRunnable);以防止在UI不可见时运行。 - MatPag

17

你可以使用RxBindings;它是最佳解决方案。请参阅RxJava运算符指南中的debounce。我相信这对你的情况非常有用。

RxTextView.textChanges(editTextVariableName)
            .debounce(500, TimeUnit.MILLISECONDS)
            .subscribe(new Action1<String>() {
                @Override
                public void call(String value) {
                    // Do some work with the updated text
                }
            });

你可能还需要.observeOn(AndroidSchedulers.mainThread())才能使其正常工作。 - Isen Ng

17

使用Kotlin扩展函数和协程:

fun AppCompatEditText.afterTextChangedDebounce(delayMillis: Long, input: (String) -> Unit) {
    var lastInput = ""
    var debounceJob: Job? = null
    val uiScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
    this.addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(editable: Editable?) {
            if (editable != null) {
                val newtInput = editable.toString()
                debounceJob?.cancel()
                if (lastInput != newtInput) {
                    lastInput = newtInput
                    debounceJob = uiScope.launch {
                        delay(delayMillis)
                        if (lastInput == newtInput) {
                            input(newtInput)
                        }
                    }
                }
            }
        }

        override fun beforeTextChanged(cs: CharSequence?, start: Int, count: Int, after: Int) {}
        override fun onTextChanged(cs: CharSequence?, start: Int, before: Int, count: Int) {}
})}

有人能解释一下为什么在UI范围内添加延迟吗?这不会挂起主线程吗? - Zeus Almighty

13

以上解决方法对我都没用。

我需要一种方法让TextWatcher不会在我输入搜索关键词时每个字符都触发,并且在搜索过程中显示进度,这意味着我需要访问UI线程。

private final TextWatcher textWatcherSearchListener = new TextWatcher() {
    final android.os.Handler handler = new android.os.Handler();
    Runnable runnable;

    public void onTextChanged(final CharSequence s, int start, final int before, int count) {
        handler.removeCallbacks(runnable);
    }

    @Override
    public void afterTextChanged(final Editable s) {
        // Show some progress, because you can access UI here
        runnable = new Runnable() {
            @Override
            public void run() {
                // Do some work with s.toString()
            }
        };
        handler.postDelayed(runnable, 500);
    }

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

在每次onTextChanged(当用户输入新字符时调用)中删除Handler。afterTextChanged在输入字段内的文本已更改后被调用,我们可以在其中启动一个新的Runnable,但如果用户键入更多字符,则会取消它(有关这些回调何时被调用的更多信息,请参见此处)。如果用户不再输入任何字符,则该时间间隔将在postDelayed中过去,并且它将使用该文本调用应进行的工作。

此代码每个间隔只运行一次,而不是为每个用户输入的按键运行。


你能解释一下为什么我们需要移除回调函数以及在这种情况下 Handler 是如何工作的吗?它是否在主线程中?我试图理解所有这些是如何工作的。 - TheLearner

7
在 Kotlin 语言中,您可以这样做:
tv_search.addTextChangedListener(mTextWatcher)

private val mTextWatcher: TextWatcher = object : TextWatcher {
    private var timer = Timer()
    private val DELAY: Long = 1000L

    override fun afterTextChanged(s: Editable?) {
        timer.cancel()
        timer = Timer()
        timer.schedule(object : TimerTask() {
            override fun run() {

                // Do your stuff here
            }
        }, DELAY)
    }

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

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

}

1
考虑到每个 Timer 实例化都会生成一个后台线程,这种方式的效率如何? - YaMiN

5

观察文本变化事件的另一种方式是使用协程通道。

lifecycleScope.launchWhenCreated {
            editText.afterTextChanged {
                // do something
            }
        }

创建一个扩展函数,以从Flow收集数据。
suspend fun EditText.afterTextChanged(afterTextChanged: suspend (String) -> Unit) {
    val watcher = Watcher()
    this.addTextChangedListener(watcher)

    watcher.asFlow()
        .debounce(500)
        .collect { afterTextChanged(it) }
}

创建一个Watcher类,在文本更改后提供文本。
class Watcher : TextWatcher {

    private val channel = ConflatedBroadcastChannel<String>()

    override fun afterTextChanged(editable: Editable?) {
        channel.offer(editable.toString())
    }

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

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

    fun asFlow(): Flow<String> {
        return channel.asFlow()
    }
}

4
我要如何确定他们已经完成了写作?编辑文本框失去焦点吗?那么就需要使用setOnFocusChangedListener
回应问题中的最新编辑:如果您想在最后一个按键后等待一段特定的时间,那么您必须在第一次按键时启动一个线程(使用TextWatcher)。不断注册最新按键的时间。让线程休眠到最新按键的时间+0.5秒。如果最新按键的时间戳没有更新,则执行您想要的操作。

用户完成输入后0.5秒内返回结果,我该如何实现? - Bobs

3
你可以使用 TextWatcher 接口并创建自定义类来实现它,以便多次重用你的 CustomTextWatcher ,并且你还可以通过其构造函数传递视图或其他所需内容:
public abstract class CustomTextWatcher implements TextWatcher { // Notice abstract class so we leave abstract method textWasChanged() for implementing class to define it

    private final TextView myTextView; // Remember EditText is a TextView, so this works for EditText also


    public AddressTextWatcher(TextView tView) { // Notice I'm passing a view at the constructor, but you can pass other variables or whatever you need
        myTextView = tView;
    }

    private Timer timer = new Timer();
    private final int DELAY = 500; // Milliseconds of delay for timer

    @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(final Editable s) {
        timer.cancel();
        timer = new Timer();

        timer.schedule(

            new TimerTask() {
                @Override
                public void run() {
                    textWasChanged();
                }
            },
            DELAY

        );
    }

    public abstract void textWasChanged(); // Notice the abstract method to leave the
                                           // implementation to the implementing class

}

现在在您的活动中,您可以像这样使用它:
// Notice I'm passing in the constructor of CustomTextWatcher
// myEditText I needed to use
myEditText.addTextChangedListener(new CustomTextWatcher(myEditText) {
    @Override
    public void textWasChanged() {
        //doSomething(); This is method inside your activity
    }
});

我需要在textWasChanged方法内调用runOnUIThread来进行任何UI更改/更新吗? - OnePunchMan

2
你可以使用计时器。在输入文本后,它会等待600毫秒。通过使用600毫秒的延迟,在afterTextChanged()中放置代码即可。"Original Answer"翻译成"最初的回答"。
@Override
    public void afterTextChanged(Editable arg0) {
        // The user typed: start the timer
        timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                // Do your actual work here
               editText.setText(et.getText().toString());
            }
        }, 600); // 600 ms delay before the timer executes the „run“ method from TimerTask
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        // Nothing to do here
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        // The user is typing: reset already started timer (if existing)
        if (timer != null) {
            timer.cancel();
        }
    }
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接