I have a single Activity app where I put a SearchView on the topbar. I am struggling since there are like thousands of manuals/tutorials on implementing search online, but all of them seem to be outdated somehow. Even the official documentation does not make it clear for me.
For one reason or the other, I have to use a single Activity in my app, and handle the whole interaction with Fragment.
I am struggling on how to make the SearchView behave like I want to: I want the menu item to show the search bar at the top when i click it, then offer history and suggestions, then whenever something is searched, open a different Fragment with the results (actually, a TabLayout with three different result types) -- this is the way Youtube does I think.
I got to the point where searching for something brings another Fragment to the screen, but then I want that when the user clicks on the back arrow, the user is brought back to the previous Fragment (without it having to reload again the info -- there is endless scrolling so the user might have loaded tons of data): Whenever I click, first the action view for search disappears, then if I hit back again, the previous Fragment shows up but it's reloading its content.
this is my setupSearch() method:
private fun setupSearch(menu: Menu) {
// Get the SearchView and set the searchable configuration
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
val searchItem = menu.findItem(R.id.app_search)
val searchView = searchItem.actionView as SearchView
// Assumes current activity is the searchable activity
searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName))
var searchEditTextId = R.id.search_src_text;
var searchEditText = searchView.findViewById<AutoCompleteTextView>(searchEditTextId)
var dropDownAnchor = searchView.findViewById<View>(searchEditText.dropDownAnchor)
if (dropDownAnchor != null) {
dropDownAnchor.addOnLayoutChangeListener { p0, p1, p2, p3, p4, p5, p6, p7, p8 ->
// screen width
var screenWidthPixel = this#DashboardListActivity.resources.displayMetrics.widthPixels
searchEditText.dropDownWidth = screenWidthPixel
}
}
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
var searchSuggestion = SearchRecentSuggestions(this#DashboardListActivity, SearchHistoryProvider.AUTHORITY, SearchHistoryProvider.MODE)
searchSuggestion.saveRecentQuery(query, null)
supportFragmentManager
.beginTransaction()
.replace(R.id.fragment_container, SearchResultsContainerFragment.newInstance())
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.addToBackStack("SEARCH")
.commit()
return false
}
override fun onQueryTextChange(newText: String): Boolean {
return false
}
})
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener{
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
return true
}
})
searchView.setOnSuggestionListener(object: SearchView.OnSuggestionListener {
override fun onSuggestionSelect(position: Int): Boolean {
return false
}
override fun onSuggestionClick(position: Int): Boolean {
return false
}
})
}
Also, when displaying the suggestion list, it seems the Activity is somehow paused and resumed (I guess because it is implemented as a dialog?). This would not be a problem but I am keeping a video window always on top of all views (hence my need to do everything with Fragment) and the video stops for a split sec then continues. Is it possible to prevent that?
Search seemed like an easy task but its becoming kind of a nightmare. The official documentation says that i must create a different Activity that is "Searchable" but I cannot do that.
If you do not want to reload the fragment(that contains the search) when you go back you have to change the replace fragment with add. That is because replace does a remove (of any fragment) followed by an add(the new fragment).
You can use any of these library as per your requirements :
https://github.com/MiguelCatalan/MaterialSearchView
https://github.com/arimorty/floatingsearchview
https://github.com/lapism/SearchView
Related
I have the activity for example: FoodActivity in this activity I created the grid of the type of Food, I used the RecyclerView for this propose. The FoodActivity has the back-button in the title bar. I'm using setDisplayHomeAsUpEnabled to put a back mark at icon in title bar.
supportActionBar?.setDisplayHomeAsUpEnabled(true)
The issue: when user clicked on type of Food item the new fragment is opened. But when user now click the Back button he return not to the previews FoodActivity with the RecyclerView items grid. The user returns to the MainActivity. It's not usable. I need the Back button just close the fragment, and return to the prev activity not to to the "prev-prev".
I found this chunk of code:
override fun onAttach(context: Context) {
super.onAttach(context)
val callback: OnBackPressedCallback =
object : OnBackPressedCallback(true)
{
override fun handleOnBackPressed() {
// Leave empty do disable back press or
// write your code which you want
}
}
requireActivity().onBackPressedDispatcher.addCallback(
this,
callback
)
}
And it can help but it does not work with the back-button in the title bar. This code works only with the default back button of the device. Is there a way to solve my issue?
Add below code in the onCreate method of your fragment:
setHasOptionsMenu(true)
This method call will allow your fragment to populate the options menu including the back icon in the toolbar.
Now you can override the onOptionsItemSelected method to perform actions whenever any of the buttons is pressed, in your case the top back arrow.
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
// top back arrow is mapped to android.R.id.home
android.R.id.home -> {
//Close the fragment and navigate back to Recyclerview
}
}
I have an app bar defined from my fragment rather than activity by using
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.apply {
//add menu
inflateMenu(R.menu.menu_fragment)
//setup with navcontroller/navgraph
setupWithNavController(findNavController())
}
}
The problem I'm facing is trying to implement a warning message when a user clicks the Navigate Up button using the app bar. I want this behaviour only in one fragment.
I've found solutions online pertaining to app bars defined in an activity but they don't seem to work for me (such as using override fun onSupportNavigateUp().
Any ideas if I may be able to accomplish this?
Update
Initially, I implemented the chosen answer which worked but was causing some memory leaks. The kind individual who answered this question also found a workaround for the memory leaks here . Unfortunately, it didn't work so great for me (I believe because I am using navigation components) but it may work for you.
I later realized that I could easily override the navigate up default behaviour by adding this piece of line to my toolbar code:
binding.toolbar.apply {
//add menu
inflateMenu(R.menu.menu_fragment)
//setup with navcontroller/navgraph
setupWithNavController(findNavController())
//****************ADD THIS******************
setNavigationOnClickListener { view ->
//do what you want after user clicks navigate up button
}
}
The problem I'm facing is trying to implement a warning message when a user clicks the Navigate Up button using the app bar. I want this behaviour only in one fragment.
So, you just need to catch the event of hitting the UP button of the app bar for that particular fragment.
You can enable the options menu for that fragment:
setHasOptionsMenu(true)
And override onOptionsItemSelected to catch the UP button id:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
// Handle the UP button here
Toast.makeText(requireContext(), "UP button clicked", Toast.LENGTH_SHORT).show()
return true
}
return super.onOptionsItemSelected(item)
}
Note: if you want to use a unique toolbar for that fragment other than the default one, check this answer.
now I am unable to inflate my menu using inflateMenu(R.menu.menu_fragment). Any ideas?
You can remove this inflation, and instead override onCreateOptionsMenu for that:
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_fragment, menu)
}
onCreateOptionsMenu() didn't work for me,
I have write this code part in onCreate() for Activity; (navigationView is id my NavigationView)
for (i in 0 until navigationView.menu!!.size()) {
val item = navigationView.menu.getItem(i)
val s = SpannableString(item.title)
s.setSpan(AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), 0, s.length, 0)
item.title = s
}
How can I get notified at the moment when the selection menu appears for TextView?
See the screen recording below. If you select some text, context menu (copy/share/select all) appears. If you begin to drag the blue drag handle, the context menu disappears, and once you release the handle, the menu appears again. So, basically, the menu appears only when selection is done, not whilst you are still selecting the text.
I want to get notified when selection is done (i.e., the same time as the menu appears). I thought that onPrepareActionMode would be called when the selection is done and the menu appears, but after testing with the code below, it seemed that onPrepareActionMode is continuously called whilst I am dragging the handle, even when the selection menu is not visible. Also, it often got called twice for a single dragging. So onPrepareActionMode does not seem to be then answer. Then what is?
object: ActionMode.Callback{
#SuppressLint("ResourceType")
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean
{
Log.d("test", "onCreateActionMode");
return true;
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean
{
Log.d("test", "onPrepareActionMode");
return false
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean
{
Log.d("test", "onActionItemClicked");
return false;
}
override fun onDestroyActionMode(mode: ActionMode?)
{
Log.d("test", "onDestroyActionMode");
}
};
No answer? I thought that this would be a common requirement to do something automatically when text is selected. Anyway, I spent a lot of time trying to find a way and it all failed. But the hint came from a random thing: I have noticed that whenever the text selection is done and the menu appears,
W/androidtc: TextClassifier called on main thread
is printed in the Logcat. That is how I found that there is TextClassifier in TextView. So, I tried the following code
val tc = object:TextClassifier{
override fun classifyText(request: TextClassification.Request): TextClassification
{
Log.d("test", "classifyText")
return super.classifyText(request)
}
}
text2.setTextClassifier(tc);
, and as I suspected, classifyText was called when the selection is done, not during I was dragging the selection handle. This is also called when the selection is closed (tap elsewhere), but I guess I can probably filter that out by checking if the selected text length is 0. I will use this workaround until someone who knows better posts a more elegant and correct solution.
PS: The code above works on Android 10/11/12, but caused a runtime exception on Android 8.1. I guess that it is because there are no default implementations for methods on Android 8.1.
For Android 8.1, I have tried the following and it worked. If you do not care about classifier itself, I guess you do not have to pass the default classifier and use the commented-out dummy return values.
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textClassificationManager = getSystemService(Context.TEXT_CLASSIFICATION_SERVICE) as TextClassificationManager;
val defaultOne = textClassificationManager.textClassifier;
val txt = findViewById<TextView>(R.id.textview1);
txt.setTextClassifier(MyTextClassfier(defaultOne));
}
inner class MyTextClassfier(private val fallback:TextClassifier) : TextClassifier by fallback
{
override fun suggestSelection(
text: CharSequence,
selectionStartIndex: Int,
selectionEndIndex: Int,
defaultLocales: LocaleList?
): TextSelection
{
return fallback.suggestSelection(text, selectionStartIndex, selectionEndIndex, defaultLocales);
//return TextSelection.Builder(selectionStartIndex, request.getEndIndex()).build();
}
override fun classifyText(
text: CharSequence,
startIndex: Int,
endIndex: Int,
defaultLocales: LocaleList?
): TextClassification
{
//Selection ended. User has lifted his finger.
return fallback.classifyText(text, startIndex, endIndex, defaultLocales);
//return TextClassification.Builder().build();
}
}
I have a single activity app using the androidx navigation library. For one of the menu destinations I effectively have a fragment as destination with no view whatsoever that depending on the state of the user provided configuration either redirects to the real destination that should be there or to one of currently two different views that tell the user that either he needs to setup a configuration first or that there currently is no active configuration (deleted?) and he needs to select one of the available configurations.
Now, functionally this approach works perfectly fine. However, since androidx navigation ties menu items to destinations by id the menu item that gets you to that view is never selected as it matches the fragment destination with no view in it.
I tried to add a NavController.OnDestinationChangedListener to my Activity and added it to the navController navController.addOnDestinationChangedListener(this). But it seems to get overwritten by the navigation afterwards.
override fun onDestinationChanged(controller: NavController, destination: NavDestination, arguments: Bundle?) {
val destinations = listOf(R.id.destinationA, R.id.destinationB, R.id.destinationC)
if(destinations.contains(destination.id)) {
nav_view.menu.getItem(0).isChecked = true
}
}
It is deffinitely the right menu item. As when I change isChecked = true to isEnabled = false I can no longer click on it.
Also when I do this odd hack it works
GlobalScope.launch(Dispatchers.Main) {
delay(1000)
nav_view.menu.getItem(0).isChecked = true
}
Needless to say this is not a very good solution.
Anyone here knows how to overwride the default behaviour of androidx navigation in this regard?
I´ll come back to this later and report back if I find a proper solution to this.
Adding a listener to the drawer opening and setting the selected menu item then might be a good workaround for this if it is not possible to do currently.
Instead of using setupWithNavController(), as mentioned in the documentation, setup it up yourself.
As mentioned here, onNavDestinationSelected() helper method in NavigationUI is called when the menu item is clicked when you set it up using setupWithNavController(). So you could try something like this:
yourNavigationView.setNavigationItemSelectedListener { item: MenuItem ->
if(item.itemId == R.id.noViewFragmentId) {
val isConfigurationProvided = ...
if(!isConfigurationProvided) {
//Perform your actions (navigate to either of the two alternate views)
return#setNavigationItemSelectedListener true
}
}
val success = NavigationUI.onNavDestinationSelected(item, navController)
if(success) {
drawerLayout.closeDrawer(GravityCompat.START)
item.isChecked = true
}
success
}
I´ll add this as a possible solution and stick with it for the time being. I still feel like there should be a better way to do this, so I will not accept it as an awnswer.
It´s essentially the idea I got at the end of writing the question
Adding a listener to the drawer opening and setting the selected menu item then might be a good workaround for this if it is not possible to do currently.
class SetActiveMenuDrawerListener(
private val navController: NavController,
navigationView: NavigationView) : DrawerLayout.DrawerListener {
private var checked = false
private val destinations = listOf(R.id.destinationA, R.id.destinationB, R.id.destinationC)
private val menu = navigationView.menu.getItem(0)
init {
navController.addOnDestinationChangedListener { _, _, _ -> checked = false }
}
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
}
override fun onDrawerOpened(drawerView: View) {
}
override fun onDrawerClosed(drawerView: View) {
}
override fun onDrawerStateChanged(newState: Int) {
if(checked) return
val currentDestination = navController.currentDestination ?: return
if(destinations.contains(currentDestination.id)) {
menu.isChecked = true
}
checked = true
}
}
Then add this to the DrawerLayout
drawer_layout.addDrawerListener(SetActiveMenuDrawerListener(navController, nav_view))
I did add the code into the onDrawerStateChanged instead onDrawerOpened, because onDrawerOpened gets called a bit late if clicking the drawer and not at all while dragging it.
It´s not the pretties thing to look at, but it gets the job done.
I'm trying to implement the android library SelectionTracker which allows to select items in a recyclerView.
Everything works fine except that when I click outside of an
item (which is in a grid layout), the all selection is cleared.
I actually have found the code which calls the clearSelection(). It's on the line 78 of the class TouchInputHandler.
It then calls the line 64 of ItemDetailsLookup which returns false because the touch event didn't occurred on an item.
I was wondering if anyone have found a workaround to prevent this behavior, because I didn't found any option in the documentation.
It's a gridLayout so it is quite "normal" to have space between items and I don't want my users to clear the selection because they have touch the side of an item.
This is my solution, based on that if we have predefined ItemDetail that will be used as "this is not the view you can select".
First, inside your ItemDetailsLookup instead of returning null you can pass single item with distinguish data that will make sure there is no name/position collision with any other data you can have
class AppItemDetailsLookup(private val rv: RecyclerView) : ItemDetailsLookup<String>() {
override fun getItemDetails(e: MotionEvent): ItemDetails<String>? {
val view = rv.findChildViewUnder(e.x, e.y) ?: return EMPTY_ITEM
return (rv.getChildViewHolder(view) as AppItemViewHolder).getItemDetails()
}
object EMPTY_ITEM : ItemDetails<String>() {
override fun getSelectionKey(): String? = "empty_item_selection_key_that_should_be_unique_somehow_that_is_why_i_made_it_so_long"
override fun getPosition(): Int = Integer.MAX_VALUE
}
}
And then when you are creating SelectionTracker with builder, instead of using standard predicate (default is SelectionPredicates.createSelectAnything()) you make your own that will notify that this EMPTY_ITEM cannot be selected
.withSelectionPredicate(object : SelectionTracker.SelectionPredicate<String>() {
override fun canSelectMultiple(): Boolean = true
override fun canSetStateForKey(key: String, nextState: Boolean): Boolean =
key != AppItemDetailsLookup.EMPTY_ITEM.selectionKey
override fun canSetStateAtPosition(position: Int, nextState: Boolean): Boolean =
position != AppItemDetailsLookup.EMPTY_ITEM.position
})
I tested it with LinearLayoutManger, the selection was deselecting all items once i clicked outside any of them (my items did not had spacing decoration, but there were so few of them that i was seeing empty under last item)