Android: Override Navigate Up in App Bar Defined in Fragment - android

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
}

Related

How to use Back-Button in the title bar for closing the fragment?

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
}
}

androidx.navigation multiple destinations for menu item

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.

Prevent views from being clicked until animation added to action via navigation component completes

Given 2 Fragments A and B, A moves to B (so A -> B), via navigation component action with enter animation has been added. How to prevent views in Fragment B being clickable while enter animation is running? I've found this question How to add listener to android Navigation Architecture Component action animation but unfortunately there're no answers.
What I found in the documentation is that I could get resource ID of that animation through NavOptions object hooked onto the NavAction, but not the Animation object itself.
You can start by having your views as disabled in xml android:enabled="false" then in your fragment's onViewCreated you can set a delay with the animation duration using coroutines:
override fun onViewCreated(view: View, savedState: Bundle?) {
super.onViewCreated(view, savedState)
// Initialize views here.
lifecycleScope.launch {
delay(resources.getInteger(R.integer.anim_duration).toLong())
// Enable views here
myView.isEnabled = true
}
}
While I originally solved the problem using coroutines I faced the same problem
once again :] so I investigated a bit and stumbled upon this topic Disable clicks when fragment adding animation playing that helped me to figure out the right solution.
Apparently those action animations added through the navigation graph are
set by FragmentTransaction.setCustomAnimation(enter, exit, popEnter, popExit)
and these can be accessed by overriding onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int). Where nextAnim actually represents the action animations we added. For the fragment A it would be either exit or popEnter and for the fragment B it would be either enter or popExit.
The problem of views being clicked happens when fragment is entering (either enter or popEnter) so one can use an if statement to check enter and if true create Animation based on the nextAnim and then one can set listener to it. In case of home (starting) fragment one should exclude the case of nextAnim = 0 since it's also entering animation.
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
if (nextAnim == 0 || !enter) return super.onCreateAnimation(transit, enter, nextAnim)
else {
return AnimationUtils.loadAnimation(requireContext(), nextAnim).apply {
setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation?) {
disableClicking()
}
override fun onAnimationEnd(animation: Animation?) {
enableClicking()
}
override fun onAnimationRepeat(animation: Animation?) {
}
})
}
}
}
EDIT: For non-home fragments to avoid disabling clicks at the start of the animation, we can start with views being unclickable in xml layout and only enable clicking when the animation ends. To remove a bug where views remain unclickable if a device rotation happens we can introduce a boolean variable that we will set to true when animation ends and preserve it by overriding onSaveInstanceState(outState: Bundle) and reinstating it in onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) and check if it was true before device rotation to re-enable clicking once again.

Leanback Search fragment closing after gaining focus on results

In the leanback search fragment when we have committed a search with the keyboard and we have search results the fragment is closing when the search query is submitted with the back button. We can see that on action down of the back button the results are gaining focus and the keyboard is hiding(as expected) but on action up the screen is closing.
I believe this is a bug in the leanback framework since this is reproducing in the leanback showcase.
I have also posted an issue https://github.com/googlesamples/leanback-showcase/issues/58
Is there a workaround to disable the closing of the screen?
I have found the source of the bug.
In the SearchSupportFragment there is a searchBarListener that is giving focus on results in onKeyboardDismiss, but this is called before dispatchKeyEvent and when the back button is handled the results are already focused and the screen is closing.
I have found a hacky sololution of this until the Leanback team fix it. In onViewCreated I have set my own searchBarListener and I have delayed the focus change
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//this is overriding the default searchResultProvider, because of a bug in it
view.findViewById<SearchBar>(R.id.lb_search_bar).setSearchBarListener(object : SearchBar.SearchBarListener {
override fun onSearchQueryChange(query: String?) {
onQueryTextChange(query)
}
override fun onSearchQuerySubmit(query: String?) {
onQueryTextSubmit(query)
}
override fun onKeyboardDismiss(query: String?) {
searchHandler.postDelayed({ focusOnResults() }, 200)
}
})
}

SearchView with fragments

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

Categories

Resources