* Add onPause\onResume persistence to searchView. Fixes issue #3627 * New controller subclass with built-in SearchView support * Implement new SearchableNucleusController in SourceController * Add query to BasePresenter (for one field it is not worth create a subclass in my opinion), convert BrowseSourceController to inherit from SearchableNucleusController * move to flows to fix an issue in GlobalSearch where it would trigger the search multiple times * Continue conversion to SearchableNucleusController * Convert LibraryController, convert to flows, Known ISSUE with empty string being posted after setting the query upon creation of UI * Fix issues with the post being tide to the SearchView queue which is not processed until shown. Add COLLAPSING state capture which should wrap this up. * refactoring & enforce @StringRes for queryHint
This commit is contained in:
committed by
GitHub
parent
14c114756d
commit
2911fe7a1a
@@ -121,7 +121,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
||||
* [expandActionViewFromInteraction] should be set to true in [onOptionsItemSelected] when the expandable item is selected
|
||||
* This method should be called as part of [MenuItem.OnActionExpandListener.onMenuItemActionExpand]
|
||||
*/
|
||||
fun invalidateMenuOnExpand(): Boolean {
|
||||
open fun invalidateMenuOnExpand(): Boolean {
|
||||
return if (expandActionViewFromInteraction) {
|
||||
activity?.invalidateOptionsMenu()
|
||||
false
|
||||
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.appcompat.QueryTextEvent
|
||||
import reactivecircus.flowbinding.appcompat.queryTextEvents
|
||||
|
||||
/**
|
||||
* Implementation of the NucleusController that has a built-in ViewSearch
|
||||
*/
|
||||
abstract class SearchableNucleusController<VB : ViewBinding, P : BasePresenter<*>>
|
||||
(bundle: Bundle? = null) : NucleusController<VB, P>(bundle) {
|
||||
|
||||
enum class SearchViewState { LOADING, LOADED, COLLAPSING, FOCUSED }
|
||||
|
||||
/**
|
||||
* Used to bypass the initial searchView being set to empty string after an onResume
|
||||
*/
|
||||
private var currentSearchViewState: SearchViewState = SearchViewState.LOADING
|
||||
|
||||
/**
|
||||
* Store the query text that has not been submitted to reassign it after an onResume, UI-only
|
||||
*/
|
||||
protected var nonSubmittedQuery: String = ""
|
||||
|
||||
/**
|
||||
* To be called by classes that extend this subclass in onCreateOptionsMenu
|
||||
*/
|
||||
protected fun createOptionsMenu(
|
||||
menu: Menu,
|
||||
inflater: MenuInflater,
|
||||
menuId: Int,
|
||||
searchItemId: Int,
|
||||
@StringRes queryHint: Int? = null,
|
||||
restoreCurrentQuery: Boolean = true
|
||||
) {
|
||||
// Inflate menu
|
||||
inflater.inflate(menuId, menu)
|
||||
|
||||
// Initialize search option.
|
||||
val searchItem = menu.findItem(searchItemId)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
|
||||
searchView.maxWidth = Int.MAX_VALUE
|
||||
|
||||
searchView.queryTextEvents()
|
||||
.onEach {
|
||||
val newText = it.queryText.toString()
|
||||
|
||||
if (newText.isNotBlank() or acceptEmptyQuery()) {
|
||||
if (it is QueryTextEvent.QuerySubmitted) {
|
||||
// Abstract function for implementation
|
||||
// Run it first in case the old query data is needed (like BrowseSourceController)
|
||||
onSearchViewQueryTextSubmit(newText)
|
||||
presenter.query = newText
|
||||
nonSubmittedQuery = ""
|
||||
} else if ((it is QueryTextEvent.QueryChanged) && (presenter.query != newText)) {
|
||||
nonSubmittedQuery = newText
|
||||
|
||||
// Abstract function for implementation
|
||||
onSearchViewQueryTextChange(newText)
|
||||
}
|
||||
}
|
||||
// clear the collapsing flag
|
||||
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.COLLAPSING)
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
|
||||
val query = presenter.query
|
||||
|
||||
// Restoring a query the user had not submitted
|
||||
if (nonSubmittedQuery.isNotBlank() and (nonSubmittedQuery != query)) {
|
||||
searchItem.expandActionView()
|
||||
searchView.setQuery(nonSubmittedQuery, false)
|
||||
onSearchViewQueryTextChange(nonSubmittedQuery)
|
||||
} else {
|
||||
if (queryHint != null) {
|
||||
searchView.queryHint = applicationContext?.getString(queryHint)
|
||||
}
|
||||
|
||||
if (restoreCurrentQuery) {
|
||||
// Restoring a query the user had submitted
|
||||
if (query.isNotBlank()) {
|
||||
searchItem.expandActionView()
|
||||
searchView.setQuery(query, true)
|
||||
searchView.clearFocus()
|
||||
onSearchViewQueryTextChange(query)
|
||||
onSearchViewQueryTextSubmit(query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for weird behavior where searchView gets empty text change despite
|
||||
// query being set already, prevents the query from being cleared
|
||||
binding.root.post {
|
||||
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.LOADING)
|
||||
}
|
||||
|
||||
searchView.setOnQueryTextFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
setCurrentSearchViewState(SearchViewState.FOCUSED)
|
||||
} else {
|
||||
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.FOCUSED)
|
||||
}
|
||||
}
|
||||
|
||||
searchItem.setOnActionExpandListener(
|
||||
object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
||||
onSearchMenuItemActionExpand(item)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
||||
val localSearchView = searchItem.actionView as SearchView
|
||||
|
||||
// if it is blank the flow event won't trigger so we would stay in a COLLAPSING state
|
||||
if (localSearchView.toString().isNotBlank()) {
|
||||
setCurrentSearchViewState(SearchViewState.COLLAPSING)
|
||||
}
|
||||
|
||||
onSearchMenuItemActionCollapse(item)
|
||||
return true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
super.onActivityResumed(activity)
|
||||
// Until everything is up and running don't accept empty queries
|
||||
setCurrentSearchViewState(SearchViewState.LOADING)
|
||||
}
|
||||
|
||||
private fun acceptEmptyQuery(): Boolean {
|
||||
return when (currentSearchViewState) {
|
||||
SearchViewState.COLLAPSING, SearchViewState.FOCUSED -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCurrentSearchViewState(to: SearchViewState, from: SearchViewState? = null) {
|
||||
// When loading ignore all requests other than loaded
|
||||
if ((currentSearchViewState == SearchViewState.LOADING) && (to != SearchViewState.LOADED)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent changing back to an unwanted state when using async flows (ie onFocus event doing
|
||||
// COLLAPSING -> LOADED)
|
||||
if ((from != null) && (currentSearchViewState != from)) {
|
||||
return
|
||||
}
|
||||
|
||||
currentSearchViewState = to
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the SearchView since since the implementation of these can vary in subclasses
|
||||
* Not abstract as they are optional
|
||||
*/
|
||||
protected open fun onSearchViewQueryTextChange(newText: String?) {
|
||||
}
|
||||
|
||||
protected open fun onSearchViewQueryTextSubmit(query: String?) {
|
||||
}
|
||||
|
||||
protected open fun onSearchMenuItemActionExpand(item: MenuItem?) {
|
||||
}
|
||||
|
||||
protected open fun onSearchMenuItemActionCollapse(item: MenuItem?) {
|
||||
}
|
||||
|
||||
/**
|
||||
* During the conversion to SearchableNucleusController (after which I plan to merge its code
|
||||
* into BaseController) this addresses an issue where the searchView.onTextFocus event is not
|
||||
* triggered
|
||||
*/
|
||||
override fun invalidateMenuOnExpand(): Boolean {
|
||||
return if (expandActionViewFromInteraction) {
|
||||
activity?.invalidateOptionsMenu()
|
||||
setCurrentSearchViewState(SearchViewState.FOCUSED) // we are technically focused here
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,11 @@ open class BasePresenter<V> : RxPresenter<V>() {
|
||||
|
||||
lateinit var presenterScope: CoroutineScope
|
||||
|
||||
/**
|
||||
* Query from the view where applicable
|
||||
*/
|
||||
var query: String = ""
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
try {
|
||||
super.onCreate(savedState)
|
||||
|
||||
Reference in New Issue
Block a user