19

I'm trying to use a CoordinatorLayout with a BottomNavigationView, an AppBarLayout, and a ViewPager. Here is my layout:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".MainActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:layout_scrollFlags="enterAlways|scroll"
            app:popupTheme="@style/AppTheme.PopupOverlay"/>

    </android.support.design.widget.AppBarLayout>

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

    <android.support.design.widget.BottomNavigationView
        android:id="@+id/navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="?android:attr/windowBackground"
        app:itemIconTint="?colorPrimaryDark"
        app:itemTextColor="?colorPrimaryDark"
        app:menu="@menu/navigation"/>
</android.support.design.widget.CoordinatorLayout>

The problem is that the CoordinatorLayout places the ViewPager to extend to the bottom of the screen, so the bottom is obscured by the BottomNavigationView, like this:

bounds of ViewPager

This happens even though the CoordinatorLayout itself doesn't extend down so far:

bounds of CoordinatorLayout

I've tried adding app:layout_insetEdge="bottom" to the BottomNavigationView and app:layout_dodgeInsetEdges="bottom" to the ViewPager, but that has a different problem: it shifts the bottom of the ViewPager up, but it keeps the same height, so the top is now chopped off:

shifted ViewPager bounds

I tried two other experiments. First, I tried removing the BottomNavigationView from the CoordinatorLayout and making them siblings under a vertical LinearLayout. Second, I put the ViewPager and BottomNavigationView together under a LinearLayout, hoping they would layout out correctly. Neither helped: in the first case, the CoordinatorLayout still sized the ViewPager with respect to the entire screen, either hiding part of it behind the BottomNavigationView or chopping off the top. In the second case, the user needs to scroll to see the BottomNavigationView.

How do I get the layout right?

P.S. When I tried the layout suggested by @Anoop S S (putting the CoordinatorLayout and the BottomNavigationView as siblings under a RelativeLayout), I get the following (with the ViewPager still extending down behind the BottomNavigationView):

result of Anoop's layout

As before, the CoordinatorView itself only extends down to the top of the BottomNavigationView.

Ted Hopp
  • 222,293
  • 47
  • 371
  • 489

7 Answers7

10

I came up with a different approach (not battle tested yet though):

I subclassed AppBarLayout.ScrollingViewBehavior to adjust the bottom margin of the content view based on the height of the BottomNavigationView (if present). This way it should be future proof (hopefully) if the height of the BottomNavigationView changes for any reason.

The subclass (Kotlin):

class ScrollingViewWithBottomNavigationBehavior(context: Context, attrs: AttributeSet) : AppBarLayout.ScrollingViewBehavior(context, attrs) {
    // We add a bottom margin to avoid the bottom navigation bar
    private var bottomMargin = 0

    override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
        return super.layoutDependsOn(parent, child, dependency) || dependency is BottomNavigationView
    }

    override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
        val result = super.onDependentViewChanged(parent, child, dependency)

        if(dependency is BottomNavigationView && dependency.height != bottomMargin) {
            bottomMargin = dependency.height
            val layout = child.layoutParams as CoordinatorLayout.LayoutParams
            layout.bottomMargin = bottomMargin
            child.requestLayout()
            return true
        } else {
            return result
        }
    }
}

And then in the layout XML you put:

app:layout_behavior=".ScrollingViewWithBottomNavigationBehavior"

instead of

app:layout_behavior="@string/appbar_scrolling_view_behavior"
James
  • 3,119
  • 1
  • 30
  • 37
  • Great solution, thanks! The only solution I found that takes advantage of CoordinatorLayout's powerful Behavior system. Note: My child views are Fragment/FrameLayout so I had to update it to use padding instead of margin, and it works wonderfully. – Luke S. May 22 '19 at 17:51
  • Aw, man. Finding this earlier could have saved me hours of trying :) Thx! – kazume May 31 '19 at 09:15
1

Basically what you have to do is create a Relativelayout as parent and put BottomNavigationView and CoordinatorLayout as children. Then align BottomNavigationView at the bottom and set CoordinatorLayout above that. Please try the below code. It might have few attribute erros, because I wrote it here itself. And sorry for the messed up indentation.

<?xml version="1.0" encoding="utf-8"?>

    <RelativeLayout
        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:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <android.support.design.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/navigation"
        >

        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true"
            android:theme="@style/AppTheme.AppBarOverlay">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_scrollFlags="enterAlways|scroll"
                app:popupTheme="@style/AppTheme.PopupOverlay"/>

        </android.support.design.widget.AppBarLayout>

        <android.support.v4.view.ViewPager
            android:id="@+id/pager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

    </android.support.design.widget.CoordinatorLayout>

<android.support.design.widget.BottomNavigationView
            android:id="@+id/navigation"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:background="?android:attr/windowBackground"
            app:itemIconTint="?colorPrimaryDark"
            app:itemTextColor="?colorPrimaryDark"
            app:menu="@menu/navigation"/>

    </RelativeLayout>
Anoop
  • 963
  • 5
  • 16
  • I had to move the `BottomNavigationView` to be a sibling of `CoordinatorLayout` instead of a child. This still didn't fix things, although it improved them slightly. The `ViewPager` still extends behind the `BottomNavigationView`, although it now doesn't go behind the system navigation bar. (The `CoordinatorLayout` itself doesn't extend below the top of the `BottomNavigationView`, but, as before, it lays out the `ViewPager` to be larger than it is itself.) – Ted Hopp Dec 21 '17 at 04:44
  • "I had to move the BottomNavigationView to be a sibling of CoordinatorLayout instead of a child." - Yeah, sorry that is what I intended but placed it wrongly. Updated the answer. What is the issue you are facing now, I don't get it correctly. If possible post a screenshot of your layout design and update layout code on question – Anoop Dec 21 '17 at 07:04
  • I updated my answer with a screen shot of the result. – Ted Hopp Dec 21 '17 at 07:21
  • have you added android:layout_above="@+id/navigation" in the CoordinatorLayout? Can you check that. Then pager shoud reach only till top of BottomNavigationView – Anoop Dec 21 '17 at 08:22
  • Yes, that's part of the layout. The `CoordinatorLayout` itself has the correct dimensions. It just places its child outside its own bounds. – Ted Hopp Dec 21 '17 at 15:25
  • That's strange. how can a ViewGroup place its children outside the bound? – Anoop Dec 22 '17 at 07:19
  • That happens all the time. Normally, the child view just gets clipped at the parent view's bounds when it is rendered, although that's not required. See, for instance the [docs on `android:clipChildren`](https://developer.android.com/reference/android/view/ViewGroup.html#attr_android:clipChildren). – Ted Hopp Dec 22 '17 at 13:57
1

This is caused by app:layout_behavior="@string/appbar_scrolling_view_behavior" in your ViewPager. If you remove this line, you will see now it fits the CoordinatorLayout container (unfortunately, this includes now being underneath the Toolbar).

I found it helped to treat CoordinatorLayout as just a FrameLayout, with a few extra tricks. The app:layout_behavior attribute above is necessary to allow the toolbar to appear to scroll in and out... in reality, the layout is doing this by having the view linked to the collapsing toolbar (in your case, your ViewPager) be exactly a toolbar's height larger than the bounds. Scrolling up brings the view up to the bottom within the bounds, and pushes the toolbar up extending beyond the bounds. Scrolling down, vice versa.

Now, onto the BottomNavigationView! If, as I did, you want the BottomNavigationView visible the whole time, then move it outside the CoordinatorLayout, as Anoop said. Use CoordinatorLayout only for things that need to coordinate, everything else outside. I happened to use a ConstraintLayout for my parent view (you could use RelativeLayout or whatever works for you though). With ConstraintLayout, for you it would look like this:

 <android.support.constraint.ConstraintLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">


<android.support.design.widget.CoordinatorLayout
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@id/navigation"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    tools:context=".MainActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:layout_scrollFlags="enterAlways|scroll"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</android.support.design.widget.CoordinatorLayout>

<android.support.design.widget.BottomNavigationView
    android:id="@+id/navigation"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:background="?android:attr/windowBackground"
    app:itemIconTint="?colorPrimaryDark"
    app:itemTextColor="?colorPrimaryDark"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:menu="@menu/navigation" />

 </android.support.constraint.ConstraintLayout>

In Android Studio design view, you're still going to see the ViewPager appear to be larger than the container (probably looks like it's behind the Bottom Nav still). But that's ok, when you get to the bottom of the ViewPager's content, it will show (i.e. won't be behind the bottom navigation). This quirk in the design view is just the way the CoordinatorLayout makes the toolbar show/hide, as mentioned earlier.

Vin Norman
  • 1,848
  • 1
  • 12
  • 30
  • The problem is that I need the ViewPager's content to be entirely visible without scrolling, so this doesn't work. I was hoping to find a way for the CoordinatorLayout to dynamically change the size of the ViewPager as it shrank the toolbar. I looked at developing my own Behavior, but I didn't see a way to get what I want using that. – Ted Hopp Dec 24 '17 at 14:20
  • The Coordinator isn't shrinking the toolbar, it's moving it out of view (above the bounds). You would need to convert a vertical scroll on the viewpager to an action of making the viewPager larger, to have the same effect of pushing the toolbar out of view... not sure how that would work. Out of interest, is the content in your ViewPager pages/fragments mostly visible completely whether the toolbar shows or not? I'm curious as to why you need the behavior of a collapsing toolbar AND all the content visible. – Vin Norman Dec 24 '17 at 14:27
  • The ViewPager contents are mostly visible. However, some of the fragments (not all) need a floating action button or other widget, anchored near the bottom, that must be always visible. I don't think I can just make a top-level fab and share it among fragments because the fabs needs to be anchored to different views, and have different appearance, depending on what fragment is currently on display in the ViewPager. – Ted Hopp Dec 24 '17 at 14:39
  • that makes sense, I had this issue with the Floating Action Button too, and as you mentioned my solution was a top level fab (e.g. in your case, a sibling of the ViewPager, in the Coordinator layout). I did as you mentioned, shared this fab with the child fragments. I am able to change the appearance of the fabs, different icons, etc. with fab.setImageResource(R.drawable.my_drawable), but would probably become stuck with anchoring to views in the view pager! – Vin Norman Dec 24 '17 at 14:42
  • the only other thing I can think of, and I haven't tested a working solution with this, would be to play around with putting 'app:layout_behavior="@string/appbar_scrolling_view_behavior" in different views in the pager fragments, and take it out the viewPager. I think you'll run into issues with the ViewPager appearing under the toolbar, not sure if setting margins will solve that, and if it'll play nice when the toolbar collapses though... – Vin Norman Dec 24 '17 at 14:46
  • I'm pretty sure that the layout behavior needs to be attached to a sibling of the app bar (a direct child of the `CoordinatorLayout`) for it to have any effect. (Otherwise, how would the `CoordinatorLayout` find the behaviors?) From the [docs for `AppBarLayout.ScrollingViewBehavior`](https://developer.android.com/reference/android/support/design/widget/AppBarLayout.ScrollingViewBehavior.html) (emphasis added): "Behavior which should be used by `Views` which can scroll vertically and support nested scrolling to automatically scroll **any `AppBarLayout` siblings**." – Ted Hopp Dec 24 '17 at 15:43
  • Have you tried just removing the line from the ViewPager? When I do this, the effect I get is the toolbar appears and disappears fine on a vertical scroll (as long as it's some sort of nestedscrollview/recyclerview etc. it seems to work), and just overlays the ViewPager. I've tested with my child fragment being a coordinator layout, with floating action button, works fine. The only thing you 'lose' is the viewpager doesn't push down when the toolbar reappears. – Vin Norman Dec 24 '17 at 16:02
1

I had a similar problem with a layout very close to OP's and a ViewPager with 3 pages but only page 2 and 3 which should be affected by appbar_scrolling_view_behavior.

After struggling for hours exploring dead-end possible solutions (layout_dodgeInsetEdges, Window insets, attempting to modify ViewPager's page measured size, android:clipChildren, fitSystemWindows, ...), I finally found an easy solution detailed below.

As Vin Norman explained, ViewPager overlapping BottomNavigation is entirely caused by appbar_scrolling_view_behavior set on the ViewPager. AppBarLayout will just make fullscreen the sibling that has appbar_scrolling_view_behavior. That's how it works.

If you only need this behavior on certain ViewPager pages, there is a simple fix than you can apply on the ViewPager's OnPageChangeListener to dynamically change the Behavior and add/remove required padding:

public class MyOnPageChangeListener extends  ViewPager.SimpleOnPageChangeListener {

        @Override
        public void onPageSelected(int position) {

           ...

           CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) _viewPager.getLayoutParams();

        if(position == 0) {
            params.setBehavior(null);
            params.setMargins(params.leftMargin, _appBarLayoutViewPagerMarginTopPx, 
                    params.rightMargin, _appBarLayoutViewPagerMarginBottomPx);
        } else {
            params.setBehavior(_appBarLayoutViewPagerBehavior);
            params.setMargins(params.leftMargin, 0, params.rightMargin, 0);
        }

        _viewPager.requestLayout();

        }

}

For page at position 0 (the one we want the ViewPager to extend exactly below the Toolbar and above the BottomNavigationView), it removes the behavior and adds top and bottom padding, respectively _appBarLayoutViewPagerMarginTopPx and _appBarLayoutViewPagerMarginBottomPx that are constants easy to compute beforehand (respectively the value in pixel for R.attr.actionbarSize and the height for the NavigationBottomView. Usually both are 56dp)

For all other pages needing appbar_scrolling_view_behavior we restore the associated scrolling behavior (stored beforehand in _appBarLayoutViewPagerBehavior) and remove top and bottom padding.

I tested this solution and it works fine without caveat.

b0b
  • 444
  • 3
  • 5
0

If it still matters to someone:

In the answer of Anoop SS above, trying replacing the RelativeLayout with LinearLayout. Also set layout_height of CoordinatorLayout to 0dp and set layout_weight to 1.

I had almost the same problem....just that i wanted to have a static AdView at the bottom instead of the BottomNavigationView. Trying Anoop SS suggestions, at first, I got the same behaviour as OP: ViewPager extended behind the AdView. But then I did what I suggested about and everything worked fine.

Android layouts behave in weird manner or may be it is the lack of good documentation or the lack of knowledge on our part....but making a layout is just too annoying most of the time.

Shahood ul Hassan
  • 470
  • 2
  • 5
  • 16
0

In case anyone is still searching for a solution of this problem:

Cause of the problem is that CoordinatorLayout is not calculating correctly size of AppBarLayout because it has Toolbar with app:layout_scrollFlags="enterAlways|scroll" setting. It thinks that Toolbar will hide when scrolling so it leaves all available space to ViewPager, but actually what happens is that toolbar shows so ViewPager moves down, behind NavigationBar.

Easiest way to solve this is just to add android:minHeight="?attr/actionBarSize" (or whatever toolbar height you are using) to AppBarLayout. This way CoordinatorLayout will know properly how much space it needs to leave for ViewPager.

MarkoR
  • 383
  • 1
  • 11
-1

If you are using Androidx try this

<RelativeLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".MainActivity">

<androidx.coordinatorlayout.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:layout_above="@+id/bottomNavView">

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

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|enterAlways"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

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

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <fragment
            android:id="@+id/nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:navGraph="@navigation/mobile_navigation" />
    </FrameLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

<com.google.android.material.bottomnavigation.BottomNavigationView
    android:id="@+id/bottomNavView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:background="?android:attr/windowBackground"
    app:menu="@menu/bottom_nav" />

Jimale Abdi
  • 1,855
  • 1
  • 16
  • 27