34

I am trying to use android-navigation lib in my APP and I do the things as the tutorial said. I just wanna use one single activity in my APP. I am confused about one question. some fragments that just don't want the BottomNavigationView, how can I hide it. here is my main_layout

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:elevation="0dp"
        android:theme="@style/AppTheme.AppBarOverlay">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:minHeight="?attr/actionBarSize"
            android:fitsSystemWindows="true"
            app:popupTheme="@style/AppTheme.PopupOverlay" >

        </androidx.appcompat.widget.Toolbar>

    </com.google.android.material.appbar.AppBarLayout>

    <fragment
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:defaultNavHost="true"
        app:navGraph="@navigation/carkeeper_navigation"/>

<com.google.android.material.bottomnavigation.BottomNavigationView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/menu_bottom_nav"
    app:itemTextColor="@color/bottom_nav_title_color_selector"
    app:itemIconSize="@dimen/x40"
    app:menu="@menu/menu_main_bottom_nav"
    app:labelVisibilityMode="labeled">

</com.google.android.material.bottomnavigation.BottomNavigationView>

here is my mainActivity

class MainActivity : BaseActivity() {

private lateinit var navController: NavController
private lateinit var bottomNavigationView: BottomNavigationView
private lateinit var appBarConfiguration: AppBarConfiguration

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.home_activity)

    navController = Navigation.findNavController(this, R.id.nav_host_fragment)
    appBarConfiguration = AppBarConfiguration(navController.graph, null)
    setSupportActionBar(findViewById(R.id.toolbar))

    bottomNavigationView = findViewById(R.id.menu_bottom_nav)
    bottomNavigationView.setupWithNavController(navController)
    bottomNavigationView.itemIconTintList = null
}}

then the navigaton_graph

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/carkeeper_navigation"
app:startDestination="@id/mainFragment">

<fragment
    android:id="@+id/mainFragment"
    android:name="com.saicfinance.carkeeper.func.main.MainFragment"
    android:label="MainFragment"
    tools:layout="@layout/home_fragment">
</fragment>

<fragment
    android:id="@+id/mineFragment"
    android:name="com.saicfinance.carkeeper.func.mine.MineFragment"
    android:label="@string/mine_title"
    tools:layout="@layout/mine_fragment" >

    <action android:id="@+id/action_mine_fragment_to_setting_fragment"
        app:destination="@id/settingFragment"
        app:enterAnim="@anim/slide_in_right"
        app:exitAnim="@anim/slide_out_left"
        app:popEnterAnim="@anim/slide_in_left"
        app:popExitAnim="@anim/slide_out_right"/>
</fragment>

<fragment
    android:id="@+id/settingFragment"
    android:name="com.freddy.func.setting.SettingFragment"
    android:label="setting_fragment"
    tools:layout="@layout/setting_fragment" />

I know I can set BottomNavigationView gone when navigating to settingFragment. then set BottomNavigationView visible when back to mine fragment. But that is strange. anyone who can help me, thanks in advance.

Rahul
  • 3,293
  • 2
  • 31
  • 43
Freddy
  • 764
  • 2
  • 6
  • 21

10 Answers10

82

You could do something like this in your activity's onCreate. When ever an item in the nav bar is selected it will show or hide the nav based on the fragment id's.

private fun setupNav() {
    val navController = findNavController(R.id.nav_host_fragment)
    findViewById<BottomNavigationView>(R.id.bottomNav)
        .setupWithNavController(navController)

    navController.addOnDestinationChangedListener { _, destination, _ ->
        when (destination.id) {
            R.id.mainFragment -> showBottomNav()
            R.id.mineFragment -> showBottomNav()
            else -> hideBottomNav()
        }
    }
}

private fun showBottomNav() {
    bottomNav.visibility = View.VISIBLE
    
}

private fun hideBottomNav() {
    bottomNav.visibility = View.GONE
   
}
Samuel Grogan
  • 1,009
  • 10
  • 18
  • 16
    Works well except it causes a flicking effect before the Fragment is even detached. In other words, before changing the fragment to the destination one the bottom bar will disappear. These UI activity changes won't be synchronised with the lifecycle of the fragment. Am I not right? – GoRoS Aug 03 '19 at 12:01
  • I was looking for something else. and I found this by accident. thank you for this. I needed this function badly. but I couldn't find anything useful. good job – A.R.B.N Oct 02 '19 at 12:16
  • I'm having the same flicker issue as @GoRoS , any solution to it? It seems that at the first launch of the fragment is the flicker, but after one time opened it works properly – Gastón Saillén Mar 03 '20 at 18:24
  • 1
    This is not working at all. Bottom Nav View and Toolbar force fragments to resize when they are hidden or shown. – StayCool Jun 11 '20 at 13:32
  • 1
    @Samuel Grogan is there any way to do it without using the visibility option using destination, i'm also have the same issue in my application i have lot of fragments which don't need bottomNav. – YBDevi Jul 02 '20 at 13:13
  • Try using View.INVISIBLE instead of View.GONE because it'll hide it instead of removing it. – RCB Feb 11 '21 at 07:39
  • @GoRoS can you suggest any other solutions? – user924 Sep 15 '21 at 09:10
  • isn't navController.addOnDestinationChangedListener costly opertion? because every fragment transaction this listener will call. – Ali Azaz Alam Sep 21 '21 at 12:21
21

The accepted answer works, and it's the one recommended in the official documentation, but as stated on comments, it does cause some flickering, as the callback is executed before the fragment is attached.

I find the below answer more flexible, and handles animations better:

supportFragmentManager.registerFragmentLifecycleCallbacks(object : FragmentManager.FragmentLifecycleCallbacks() {
        override fun onFragmentViewCreated(fm: FragmentManager, f: Fragment, v: View, savedInstanceState: Bundle?) {
            TransitionManager.beginDelayedTransition(binding.root, Slide(Gravity.BOTTOM).excludeTarget(R.id.nav_host_fragment, true))
            when (f) {
                is ModalFragment -> {
                    binding.bottomNavigation.visibility = View.GONE
                }
                else -> {
                    binding.bottomNavigation.visibility = View.VISIBLE
                }
            }
        }
    }, true)

You can customize it depending on the transitions between your fragments, by choosing different animation (on my example it's a Slide), or by making the call at another lifecycle callback.

Hicham
  • 720
  • 6
  • 9
  • binding.root is .... fragmentView in main activity? I have single activity application, and it feels like my root is R.id.nav_host_fragment. Should I use R.id.nav_host_fragment? Also, how to do it without Slide anim? I want to just hide bottom nav without flickering – StayCool Jun 08 '20 at 12:09
  • I'm using ViewBinding, binding.root is the root of my layout (ConstraintLayout on my side), it's the layout containing your nav_host_fragment and the bottom navigation view. If you don't want the Slide, you can replace it with Fade to have a fade in/fade out effect. – Hicham Jun 09 '20 at 13:18
  • actually I don't want anything. I just want my fragment to change all at once, but now with OP implementation I have bottom nav view dissapear before fragment changes, and I even can see other fragments views, before fragment changes. I read that Transaction can be null, but by default it's using Slide. So... i dunno – StayCool Jun 09 '20 at 17:14
  • If your goal is just deleting the animation completely, just remove the whole TransitionManager.beginDelayedTransition line. – Hicham Jun 10 '20 at 18:54
  • 1
    Nop. My goal is not to make fragment resize if bottom nav view or toolbar is shown or hidden. This solution is not working, adroid developes guide is not working. The only solution I see is not to use toolbar and bottom nav bar with navigation component, and use them in each fragment separated – StayCool Jun 11 '20 at 13:33
  • It depends on your root layout, if the fragment layout depends on bottomNavigationView (for example in a ConstraintLayout, the fragment bottom is linked to the top of the bottomNavigationView), the fragment will resize, when you call "bottomNavigationView.visibility = View.GONE". – Hicham Jun 12 '20 at 17:36
  • I have single activity with layout that contains toolbar, fragmentView for placing fragments, bottom nav view (following example on android.developers), so, i listen to onDestinationChanged and set bottom nav or toolbar to Gone, and fragmentView between them is being resized. I understood that I need nested nav graphs, so I could have some layout with bottom nav bar, and some without. – StayCool Jun 14 '20 at 10:07
  • Ahh, much better than the destination changed listener. Thank you very much! – Florian Walther Apr 06 '21 at 14:48
  • This works so much better then the official solution!! Thanks – w0wka91 Jul 08 '22 at 09:45
14

There’s a solution to this on the official Android site:

https://developer.android.com/guide/navigation/navigation-ui

navController.addOnDestinationChangedListener { _, destination, _ ->
   if(destination.id == R.id.full_screen_destination) {
      toolbar.visibility = View.GONE
      bottomNavigationView.visibility = View.GONE
   } else {
      toolbar.visibility = View.VISIBLE
      bottomNavigationView.visibility = View.VISIBLE
   }
}
Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
Tom
  • 161
  • 2
  • 5
  • 1
    This is not working at all. Bottom Nav View and Toolbar force fragments to resize when they are hidden or shown. – StayCool Jun 11 '20 at 13:32
  • 3
    I'm surprised this is suggested in the docs because it actually looks pretty terrible since the listener fires before the new fragment is placed on the screen, causing a visible flicker (that's sometimes there, sometimes not) – Florian Walther Apr 06 '21 at 14:44
8

@Samuel Grogan answer is correct, but if you have a problem that bottom navigation shows/hides unexpected when the device is rotated, you can use ViewModel to solve this issue:

MainViewModel

class MainViewModel : ViewModel() {

    private val _bottomNavigationVisibility = MutableLiveData<Int>()
    val bottomNavigationVisibility: LiveData<Int>
        get() = _bottomNavigationVisibility

    init {
        showBottomNav()
    }

    fun showBottomNav() {
        _bottomNavigationVisibility.postValue(View.VISIBLE)
    }

    fun hideBottomNav() {
        _bottomNavigationVisibility.postValue(View.GONE)
    }
}

MainActivity

class MainActivity : AppCompatActivity() {
    private lateinit var mainViewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        val bottomNav: BottomNavigationView = findViewById(R.id.nav_view)

        val navController = findNavController(R.id.nav_host_fragment)
        bottomNav.setupWithNavController(navController)

        mainViewModel.bottomNavigationVisibility.observe(this, Observer { navVisibility ->
            bottomNav.visibility = navVisibility
        })

        navController.addOnDestinationChangedListener { _, destination, _ ->
            when (destination.id) {
                R.id.comment_fragment -> mainViewModel.hideBottomNav()
                else -> mainViewModel.showBottomNav()
            }
        }
    }
}
kojot
  • 1,634
  • 2
  • 16
  • 32
4

This works for me

    navController.addOnDestinationChangedListener { _, destination, _ ->
        TransitionManager.beginDelayedTransition(bottomNavigationView, Fade())
        if (destination.id == R.id.addExpensesFragment) {
            bottomNavigationView.visibility = View.GONE
        } else {
            bottomNavigationView.visibility = View.VISIBLE
        }
    }
w0wka91
  • 189
  • 2
  • 9
2

the best way to handle this without the refreshing glitches and also without a ViewModel is to call setupWithNavController method after your when condition.

navController.addOnDestinationChangedListener { _, destination, _ ->
    when (destination.id) {
        R.id.dashboardFragment, R.id.globalParamsFragment, R.id.managementFragment
        -> {
            binding.bottomNavigation.visible()
            binding.fabAdd.visible()
        }
        else -> {
            binding.bottomNavigation.gone()
            binding.fabAdd.gone()
        }
    }
}
NavigationUI.setupWithNavController(binding.bottomNavigation, navController)

I've done it this way and it works perfectly.

Tyler2P
  • 2,324
  • 26
  • 22
  • 31
2

Expand on other's answer here, the flickering issue and first fragment resizes issue both seems to be solved for me by using Handler.post to push the execution of show/hide of the nav view to the end of the UI queue. Seem to work pretty well.

I didn't use beginDelayedTransition as I just want it to appear and disappear immediately.

navController.addOnDestinationChangedListener { _, destination, _ ->
    Handler(Looper.getMainLooper()).post {
        when (destination.id) {
            R.id.abcFragment, R.id.defFragment -> {
                bottomNavigationView.visibility = View.GONE
            }
            else -> {
                bottomNavigationView.visibility = View.VISIBLE
            }
        }
    }
}
Bruce
  • 2,357
  • 5
  • 29
  • 50
0

Yes, it depends. I did it like guide on android.dev says. Main activity layout has toolbar on top, fragment root container in the middle and bottom nav view in the bottom. And it resizes everytime on any fragment when I hide bottom nav. Now I understand that I need multiple nav graphs, I need different root containers for fragments with or without navigation/toolbar

StayCool
  • 421
  • 1
  • 9
  • 23
0

Late Reply, But I think this works fine.

import android.animation.ValueAnimator
import android.graphics.drawable.BitmapDrawable
import android.view.View
import android.view.ViewGroup
import android.view.animation.AnimationUtils
import androidx.core.animation.doOnEnd
import androidx.core.view.drawToBitmap
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.snackbar.Snackbar

/**
 * Potentially animate showing a [BottomNavigationView].
 *
 * Abruptly changing the visibility leads to a re-layout of main content, animating
 * `translationY` leaves a gap where the view was that content does not fill.
 *
 * Instead, take a snapshot of the view, and animate this in, only changing the visibility (and
 * thus layout) when the animation completes.
 */
fun BottomNavigationView.show() {
    if (visibility == View.VISIBLE) return

    val parent = parent as ViewGroup
    // View needs to be laid out to create a snapshot & know position to animate. If view isn't
    // laid out yet, need to do this manually.
    if (!isLaidOut) {
        measure(
            View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY),
            View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.AT_MOST)
        )
        layout(parent.left, parent.height - measuredHeight, parent.right, parent.height)
    }

    val drawable = BitmapDrawable(context.resources, drawToBitmap())
    drawable.setBounds(left, parent.height, right, parent.height + height)
    parent.overlay.add(drawable)
    ValueAnimator.ofInt(parent.height, top).apply {
        startDelay = 100L
        duration = 300L
        interpolator = AnimationUtils.loadInterpolator(
            context,
            android.R.interpolator.linear_out_slow_in
        )
        addUpdateListener {
            val newTop = it.animatedValue as Int
            drawable.setBounds(left, newTop, right, newTop + height)
        }
        doOnEnd {
            parent.overlay.remove(drawable)
            visibility = View.VISIBLE
        }
        start()
    }
}

/**
 * Potentially animate hiding a [BottomNavigationView].
 *
 * Abruptly changing the visibility leads to a re-layout of main content, animating
 * `translationY` leaves a gap where the view was that content does not fill.
 *
 * Instead, take a snapshot, instantly hide the view (so content lays out to fill), then animate
 * out the snapshot.
 */
fun BottomNavigationView.hide() {
    if (visibility == View.GONE) return

    val drawable = BitmapDrawable(context.resources, drawToBitmap())
    val parent = parent as ViewGroup
    drawable.setBounds(left, top, right, bottom)
    parent.overlay.add(drawable)
    visibility = View.GONE
    ValueAnimator.ofInt(top, parent.height).apply {
        startDelay = 100L
        duration = 200L
        interpolator = AnimationUtils.loadInterpolator(
            context,
            android.R.interpolator.fast_out_linear_in
        )
        addUpdateListener {
            val newTop = it.animatedValue as Int
            drawable.setBounds(left, newTop, right, newTop + height)
        }
        doOnEnd {
            parent.overlay.remove(drawable)
        }
        start()
    }
}

Now We just need to use lifecycleScope.

lifecycleScope.launchWhenResumed {
                navController.addOnDestinationChangedListener { _, destination, _ ->
                    when (destination.id) {
                        R.id.home, R.id.booking, R.id.profile -> navigationView.show()
                        else -> navigationView.hide()
                    }
                }
            }
Shvet
  • 1,159
  • 1
  • 18
  • 30
  • Nah, this doesnt work. HomeFragment still collapses – Andrew Sep 02 '20 at 15:18
  • @Andrew you need to keep visibility gone from starting and it wont crash on it and also you can put above code in `oncreate` method – Shvet Sep 02 '20 at 17:18
  • My problem is that when using this method, the first fragment resizes and it looks awful. This doesn't work – Andrew Sep 02 '20 at 17:25
0

PreferenceFragmentCompat
https://stackoverflow.com/a/62552740/18606060
Override the LifeCycle methods of PreferenceFragmentCompat.

class MySettingsFragment : PreferenceFragmentCompat() {

    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.root_preferences, rootKey)
    }

    // Called when the Fragment is visible to the user. 
    override fun onStart() {
        super.onStart()
        val mainActivity = activity as MainActivity
        mainActivity.setBottomNavigationVisibility(View.GONE)
    }
    // Called when the Fragment is no longer started. 
    override fun onStop() {
        super.onStop()
        val mainActivity = activity as MainActivity
        mainActivity.setBottomNavigationVisibility(View.VISIBLE)
    }
}


// Toggle the visibility of BottomNavigationView in MainActivity.kt.
fun setBottomNavigationVisibility(visibility: Int) {
    binding.bottomNav.visibility = visibility
}
Mark
  • 1
  • 2