在SearchView中对onQueryTextChange进行限制

16

如何最好地“节流”onQueryTextChange,使得我的performSearch()方法只在用户输入之后的每秒调用一次而不是每次用户键入时都调用?

public boolean onQueryTextChange(final String newText) {
    if (newText.length() > 3) {
        // throttle to call performSearch once every second
        performSearch(nextText);
    }
    return false;
}
9个回答

29

如果您正在使用 Kotlin 和协程,您可以执行以下操作:

var queryTextChangedJob: Job? = null

...

fun onQueryTextChange(query: String) {

    queryTextChangedJob?.cancel()
    
    queryTextChangedJob = launch(Dispatchers.Main) {
        delay(500)
        performSearch(query)
    }
}

3
协程是未来的发展方向!谢谢您的回答。 - Dhaval Patel
谢谢,@jc12,我的代码大部分都被简化了。 - newbie

25

在aherrick的代码基础上,我有一个更好的解决方案。不使用布尔型的'canRun',而是声明一个可运行的变量,并在每次查询文本更改时清空处理程序上的回调队列。这是我最终使用的代码:

@Override
public boolean onQueryTextChange(final String newText) {
    searchText = newText;

    // Remove all previous callbacks.
    handler.removeCallbacks(runnable);

    runnable = new Runnable() {
        @Override
        public void run() {
            // Your code here.
        }
    };
    handler.postDelayed(runnable, 500);

    return false;
}

9

我已经想出了一个解决方案,使用RxJava,特别是它的debounce操作符。

有了Jake Wharton的方便的RxBinding,我们将得到以下内容:

RxSearchView.queryTextChanges(searchView)
        .debounce(1, TimeUnit.SECONDS) // stream will go down after 1 second inactivity of user
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Consumer<CharSequence>() {
            @Override
            public void accept(@NonNull CharSequence charSequence) throws Exception {
                // perform necessary operation with `charSequence`
            }
        });

4
  1. Create abstract class:

    public abstract class DelayedOnQueryTextListener implements SearchView.OnQueryTextListener {
    
     private Handler handler = new Handler();
     private Runnable runnable;
    
     @Override
     public boolean onQueryTextSubmit(String s) {
         return false;
     }
    
     @Override
     public boolean onQueryTextChange(String s) {
         handler.removeCallbacks(runnable);
         runnable = () -> onDelayerQueryTextChange(s);
         handler.postDelayed(runnable, 400);
         return true;
     }
    
     public abstract void onDelayedQueryTextChange(String query);
    

    }

  2. Set it like this:

    searchView.setOnQueryTextListener(new DelayedOnQueryTextListener() {
         @Override
         public void onDelayedQueryTextChange(String query) {
             // Handle query
         }
     });
    

3

针对Kotlin

在使用 coroutineScope.launch(Dispatchers.Main) {} 时,你可能会遇到问题:Suspend function '...' should be called only from a coroutine or another suspend function

我找到了以下解决方法:

private var queryTextChangedJob: Job? = null
private lateinit var searchText: String

下一步,不要忘记使用 implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha05"
override fun onQueryTextChange(newText: String?): Boolean {

    val text = newText ?: return false
    searchText = text

    queryTextChangedJob?.cancel()
    queryTextChangedJob = lifecycleScope.launch(Dispatchers.Main) {
        println("async work started...")
        delay(2000)
        doSearch()
        println("async work done!")
    }

    return false
}

如果您想在ViewModel中使用`launch`,请使用`implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-alpha05"`。
queryTextChangedJob = viewModelScope.launch(Dispatchers.Main) {
        //...
}

2
你可以使用RxJava轻松实现它。同时,你还需要RxAndroidRxBinding(如果你正在使用RxJava,那么你的项目中可能已经包含它们了)。
RxTextView.textChangeEvents(yourEditText)
          .debounce(1, TimeUnit.SECONDS)
          .observeOn(AndroidSchedulers.mainThread())
          .subscribe(performSearch());

这里是Kaushik Gopal提供的完整示例。


谢谢!但是我目前并不感兴趣引入RxJava。能否本地实现? - aherrick
很遗憾,我不知道任何常见的干净解决方案来解决这个问题。但是可以通过使用Handler延迟或Timer和TimerTasks来实现。 - Geralt_Encore

1

我发现没有一个解决方案可以让我使用一些流操作符,比如distinctUntilChanged,直到我尝试了这种方法,也许有人需要它:

首先从SearchView创建一个扩展函数:

private fun SearchView.getQueryTextChangeStateFlow(): StateFlow<String> {
    val query = MutableStateFlow("")
    
    setOnQueryTextListener(object : SearchView.OnQueryTextListener {
        override fun onQueryTextSubmit(query: String?): Boolean {
            return true
        }
        override fun onQueryTextChange(newText: String): Boolean {
            query.value = newText
            return true
        }
    })

    return query
}

那么

 private fun setUpSearchStateFlow(searchView: SearchView) {
    lifecycleScope.launch {
        searchView.getQueryTextChangeStateFlow()
            .debounce(300)
            .distinctUntilChanged()
            .map { query ->
                startSearchRequestHere(query)
            }
            .flowOn(Dispatchers.Default)
            .collect {}
    }
}

Reference


0

使用协程和流:

private fun SearchView.onQueryTextChanged(): ReceiveChannel<String> =
    Channel<String>(capacity = Channel.UNLIMITED).also { channel ->
        setOnQueryTextListener(object : SearchView.OnQueryTextListener{
            override fun onQueryTextSubmit(query: String?): Boolean {
                return false
            }

            override fun onQueryTextChange(newText: String?): Boolean {
                newText.orEmpty().let(channel::offer)
                return true
            }
        })
    }

@ExperimentalCoroutinesApi
fun <T> ReceiveChannel<T>.debounce(time: Long, unit: TimeUnit = TimeUnit.MILLISECONDS, scope: CoroutineScope): ReceiveChannel<T> =
    Channel<T>(capacity = Channel.CONFLATED).also { channel ->
        scope.launch {
            var value = receive()
            whileSelect {
                onTimeout(time) {
                    channel.offer(value)
                    value = receive()
                    true
                }
                onReceive {
                    value = it
                    true
                }
            }
        }
    }

然后像这样添加到您的搜索视图中:

lifecycleScope.launch {
   searchView.onQueryTextChanged().debounce(time = 500, scope = lifecycleScope).consumeEach { newText ->
          //Use debounced query
   }
}

-1
我最终得到了一种与下面类似的解决方案。这样它应该每半秒钟触发一次。
        public boolean onQueryTextChange(final String newText) {

            if (newText.length() > 3) {

                if (canRun) {
                    canRun = false;
                    handler.postDelayed(new Runnable() {
                        @Override
                        public void run() {

                            canRun = true;
                            handleLocationSearch(newText);
                        }
                    }, 500);
                }
            }

            return false;
        }

这是一个错误的解决方案。请查看 https://proandroiddev.com/implementing-search-on-type-in-android-with-rxjava-9ece00f4e266 - tim4dev
此解决方案缺少取消功能。如果您不再需要观察查询文本更改,应记得从处理程序中删除回调。 - Lukas

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