1

Background

I wanted to support full screen navigation UI as shown here:

https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1 https://developer.android.com/guide/navigation/gesturenav

The problem

While all Activities of my app worked fine, I suddently reached a problematic one that has a RecyclerView with thumbs.

Here, I got 2 weird issues:

  1. When scrolling to the bottom, the last item/s don't get fully shown.

enter image description here

  1. When in landscape mode, the thumbs go outside of anywhere that I can reach, so they are also not touchable. Not only that, but I can also see the normal scrollbar, and both get to disappear when scrolling to the bottom:

enter image description here

What I've tried

I tried to apply the insets to various views, including both padding and margins, but nothing helped.

Also, in some websites I saw that I should use View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION and in some that I need to also add View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN. Didn't help.

Here's my current code (project available here, as I think this is a bug) :

MainActivity.kt

class MainActivity : AppCompatActivity(R.layout.activity_main) {
    inline fun View.updateMargins(@Px left: Int = marginLeft, @Px top: Int = marginTop, @Px right: Int = marginRight, @Px bottom: Int = marginBottom) {
        updateLayoutParams<ViewGroup.MarginLayoutParams> {
            this.bottomMargin = bottom
            this.topMargin = top
            this.leftMargin = left
            this.rightMargin = right
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setSupportActionBar(toolbar)
        findViewById<View>(android.R.id.content).systemUiVisibility =
            View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
        ViewCompat.setOnApplyWindowInsetsListener(appBarLayout) { _, insets ->
            val systemWindowInsets = insets.systemWindowInsets
            appBarLayout.updateMargins(
                left = systemWindowInsets.left,
                top = systemWindowInsets.top,
                right = systemWindowInsets.right
            )
            insets
        }

        ViewCompat.setOnApplyWindowInsetsListener(recyclerView) { _, insets ->
            val systemWindowInsets = insets.systemWindowInsets
            recyclerView.updatePadding(
                left = systemWindowInsets.left,
                bottom = systemWindowInsets.bottom,
                right = systemWindowInsets.right
            )
            insets
        }

        recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
            init {
                setHasStableIds(true)
            }

            override fun onCreateViewHolder(
                parent: ViewGroup,
                viewType: Int
            ): RecyclerView.ViewHolder {
                return object : RecyclerView.ViewHolder(
                    LayoutInflater.from(this@MainActivity).inflate(R.layout.list_item, parent, false)
                ) {}
            }

            override fun getItemId(position: Int): Long = position.toLong()

            override fun getItemCount(): Int = 100

            override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
                holder.itemView.imageView.setColorFilter(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
                holder.itemView.textView.text = "item $position"
            }

        }
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menu.add("test").setIcon(android.R.drawable.ic_dialog_email).setOnMenuItemClickListener {
            true
        }.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
        return super.onCreateOptionsMenu(menu)
    }
}

styles.xml (I use AppTheme in the manifest as theme)

<resources xmlns:tools="http://schemas.android.com/tools">

    <style name="AppTheme" parent="Theme.MaterialComponents.NoActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:navigationBarColor" tools:targetApi="lollipop">
            @android:color/transparent
        </item>
        <item name="android:statusBarColor" tools:targetApi="lollipop">@color/colorPrimaryDark
        </item>
    </style>

    <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar" />
</resources>

activity_main.xml

<androidx.coordinatorlayout.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" tools:context=".MainActivity">


    <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">
        <!--app:popupTheme="@style/AppTheme.PopupOverlay"-->
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary" />
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent"
        android:orientation="vertical" android:scrollbars="vertical" app:fastScrollEnabled="true"
        app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
        app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
        app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
        app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:itemCount="100"
        tools:listitem="@layout/list_item" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

line.xml

<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">

    <solid android:color="@android:color/darker_gray" />

    <padding
        android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />
</shape>

line_drawable.xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/line" android:state_pressed="true" />

    <item android:drawable="@drawable/line" />
</selector>

thumb.xml

<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
    <corners
        android:bottomLeftRadius="44dp" android:topLeftRadius="44dp" android:topRightRadius="44dp" />
    <padding
        android:paddingLeft="22dp" android:paddingRight="22dp" />
    <solid android:color="@color/colorPrimaryDark" />
</shape>

thumb_drawable.xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/thumb" android:state_pressed="true" />

    <item android:drawable="@drawable/thumb" />
</selector>

The questions

  1. Why does it occur? It worked fine for various other places...
  2. How can I make the RecyclerView avoid both these cases, yet allow the navigation bar at the bottom show content of the RecyclerView as it is transparent?
  3. In which cases should I add the flag View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN? What does it do to help in these cases?

EDIT: a possible workaround is to avoid using CoordinatorLayout. It works well, but I wanted to do things "the official way". Here's this workaround:

Instead of CoordinatorLayout I used :

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical"
    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">

...

And in code, I've set both margins and padding to the RecyclerView:

    ViewCompat.setOnApplyWindowInsetsListener(recyclerView) { _, insets ->
        val systemWindowInsets = insets.systemWindowInsets
        recyclerView.updatePadding(
            bottom = systemWindowInsets.bottom
        )
        recyclerView.updateMargins(
            left = systemWindowInsets.left,
            right = systemWindowInsets.right
        )
        insets
    }
android developer
  • 114,585
  • 152
  • 739
  • 1,270

2 Answers2

3

As there is a comment discussion on my previous answer, I will not edit that one (it might also benefit someone).

The simplest thing to do, is to let the AppBarLayout handle the top and bottom insets. We can do this by using the android:fitsSystemWindows="true" on the AppBarLayout. For the items to be seen through the navigation bar, use android:clipToPadding="false" on the RecyclerView. Use android:scrollbars="none" on the RecyclerView to disable the normal scrollbar. The layout XML is then:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appBarLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true"
        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" />

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

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"
        android:orientation="vertical"
        android:scrollbars="none"
        app:fastScrollEnabled="true"
        app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
        app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
        app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
        app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

In the activity/fragment, we still have to handle bottom system inset for the RecyclerView and left and right system inset for both the RecyclerView and AppBarLayout. We use margins for theRecyclerView` to get the fast scroller into the content area.

ViewCompat.setOnApplyWindowInsetsListener(recyclerView) { _, insets ->
            val systemWindowInsets = insets.systemWindowInsets
            appBarLayout.updatePadding(left = systemWindowInsets.left, right = systemWindowInsets.right)
            recyclerView.updatePadding(bottom = systemWindowInsets.bottom)
            recyclerView.updateMargins(left = systemWindowInsets.left, right = systemWindowInsets.right)
            insets
        }

Video result here, comment #27.

android developer
  • 114,585
  • 152
  • 739
  • 1,270
Adrijan
  • 353
  • 1
  • 10
  • 1
    Nice. Thank you! This is much less of a workaround like the other person has offered :) – android developer Nov 02 '19 at 17:18
  • @40 I actually have found an issue on your solution. It's also shown on the video (here: https://issuetracker.google.com/issues/143530492#comment27 ) . When you scroll to the bottom, the thumb goes to be behind the nav-bar. It can't be touched there. I'm un-accepting this answer, but putting +1 for it. – android developer Nov 03 '19 at 20:54
  • Yep, because it ignores padding as we discussed. You can either wait for an official better implementation, use the FastScroller from my other answer here or implement your own. It depends on how much you need this functionality. – Adrijan Nov 04 '19 at 17:05
  • So you agree that this is a bug? I should post about this on the issue tracker, right? – android developer Nov 05 '19 at 09:31
  • It is already posted. I am the person from the issue tracker you have been talking to. – Adrijan Nov 05 '19 at 12:19
  • OK I reported here: https://issuetracker.google.com/issues/143972309 – android developer Nov 05 '19 at 22:29
  • I have told you that it's already opened: https://issuetracker.google.com/issues/143786834 – Adrijan Nov 06 '19 at 10:17
  • You have? Where? And how can you understand anything from such a short text and without sample/video/screenshot ? How did you find this? – android developer Nov 06 '19 at 10:29
  • Did you open this issue? https://issuetracker.google.com/issues/143530492. If you did, check comment #16. The person from Google has opened the issue for you. – Adrijan Nov 06 '19 at 13:36
  • Oh I see. Thank you. Still, the text there don't help at all to understand the issue. – android developer Nov 06 '19 at 15:18
  • That's what the "Blocked by" and "Blocking" at the top of the issue are for. – Adrijan Nov 06 '19 at 16:55
  • I don't know what this means. – android developer Nov 06 '19 at 17:49
  • 1
    I've decided to give you +1 for the other solution too, as it seems it works fine as a workaround for this issue. – android developer Nov 07 '19 at 21:21
  • Say, do you know how to solve a similar issue for ActionMode? For some reason I can't find a way to adjust it, so I reported about this here: https://issuetracker.google.com/issues/145055235 – android developer Nov 24 '19 at 17:43
1

One thing to note, you likely have set android:clipToPadding=false" on the RecyclerView although it is not visible above (otherwise the items would not be fully visible behind the navigation bar).

First part

The first part (bottom padding in portrait mode) is very easy to solve. It seems that the CoordinatorLayout somehow moves the RecyclerView down by the top inset (very likely caused by ScrollingViewBehavior, did not research much further). So the solution is to add both the bottom and the top inset to the bottom padding of the recycler:

ViewCompat.setOnApplyWindowInsetsListener(recyclerView) { _, insets -> 
    val systemWindowInsets = insets.systemWindowInsets
    recyclerView.updatePadding(
        left = systemWindowInsets.left,
        // Fix CoordinatorLayout behavior
        bottom = systemWindowInsets.bottom + systemWindowInsets.top
        right = systemWindowInsets.right
    )
    insets
}

Second part explanation

The second part is a bit more tricky. The easier part is the double scrollbars, but then comes the fast scroll behind the navigation bar. The padding is part of the view (check the layout inspector in Android Studio, you will see the RecyclerView extending all the way into the navigation bar). When the RecyclerView fast scroll implementation (which is based on ItemDecoration) displays the line and thumb, it does not account for padding.

The RecyclerView checks the app:fastScrollEnabled="true" attribute and calls initFastScroller(...) method, which creates a FastScroller object (see here). However, you cannot extend this class, as it has a @VisibleForTesting annotation (annotation info here, source code for FastScroller here).

Solution for the second part

Double scrollbars fix

The easy thing to solve is the double scrollbars. You have to disable the normal scrollbars and show only the fast scroll ones. To do that, use android:scrollbars="none" on the RecyclerView instead of android:scrollbars="vertical".

Fast scroll fix

For the fast scroller, what you can do, is copy the code from the FastScroller and change some things to account for padding. Note, that this will also change the portrait mode - the fast scrollbar will extend only to the navigation bar. The final code for the so called FastScroller2 is here on pastebin (it's a bit long, so I won't paste it here). Just create a new Java class called FastScroller2 and paste the code.

You can then use the FastScroller2 like this (called from onCreate)

private fun setupRecyclerView() {
    recyclerView.adapter = ... // Your adapter here
    val thumbDrawable = ContextCompat.getDrawable(this, R.drawable.thumb_drawable) as StateListDrawable?
    val lineDrawable = ContextCompat.getDrawable(this, R.drawable.line_drawable)
    val thickness = resources.getDimensionPixelSize(R.dimen.fastScrollThickness)
    val minRange = resources.getDimensionPixelSize(R.dimen.fastScrollMinimumRange)
    val margin = resources.getDimensionPixelSize(R.dimen.fastScrollMargin)
    if (thumbDrawable != null && lineDrawable != null) {
        // No need to do anything else, the fast scroller will take care of 
        // "connecting" itself to the recycler view
        FastScroller2(recyclerView, thumbDrawable, lineDrawable,
            thumbDrawable, lineDrawable, thickness, minRange, margin, true)
    }
}

The thickness, minRange and margin dimensions were copied from the recycler view library, see here, added to dimens.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="fastScrollThickness">8dp</dimen>
    <dimen name="fastScrollMargin">0dp</dimen>
    <dimen name="fastScrollMinimumRange">50dp</dimen>
</resources>

This also means you don't need any fast scroller code in the layout xml:

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:orientation="vertical"
    android:scrollbars="none"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
    app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
    tools:itemCount="100"
    tools:listitem="@layout/list_item" />

I hope that this helps you.

Adrijan
  • 353
  • 1
  • 10
  • Sorry but this isn't a solution. It's a set of workarounds that might not always work and only adjusts itself to the issue. You just add more space. If this is a bug, they should just fix it – android developer Nov 01 '19 at 12:15
  • It's not a bug as this is intended behavior. Padding IS PART of the view, so they draw it there. What you need is CUSTOM behavior, which the framework does NOT provide for you. – Adrijan Nov 01 '19 at 12:34
  • If you set margins, everything would work ok, as margins are NOT part of the view. But you want the items behind the navigation bar, so you have to use padding with clipToPadding set to false. – Adrijan Nov 01 '19 at 12:36
  • But I have set padding for the RecyclerView, and as I wrote it doesn't work well. I add the bottom inset because that's what's important here. The RecyclerView is already below the toolbar, so that's what's supposed to be missing. Besides, the issue is also on the sides, as the RecyclerView draws content on the navigation bar there too, and ignores the padding I set to there. Why would I need a special behavior? Why can't the original one work? How come it isn't mentioned on the docs about this special case? There are RecyclerView everywhere... I don't understand what you mean. – android developer Nov 01 '19 at 22:49
  • Even you wrote that "CoordinatorLayout somehow moves the RecyclerView down by the top inset " , which means the top is already handled. And visually you can also see it : the RecyclerView is set nicely at the top, so the top is already handled and shouldn't affect anything else. – android developer Nov 01 '19 at 22:51
  • Yes, something weird is going on with CoordinatorLayout and AppBarScrolling behavior. But if you really need CoordinatorLayout, the solution works okay. – Adrijan Nov 02 '19 at 09:29
  • About the RecyclerView, I see now that the normal scrollbar behaves correctly. It is just a missing implementation on their end. What you can do now is wait for them to support this use case or write your own solution, be it a modified FastScroller or your 100% custom solution. – Adrijan Nov 02 '19 at 09:31
  • What I see in this solution is different from what you describe. Can you please post it to Github so I can check it there? Maybe I'm missing here something, because no matter what I try, I always see a different issue that's missing handling. Also, I still consider it all a set of workarounds instead of having it simple. I don't believe Android development should be this annoying, at least not here. – android developer Nov 02 '19 at 10:33
  • I've noticed an issue: On some cases,I would like to add padding to the bottom of the RecyclerView (example: `android:paddingBottom="128dp"` for the RecyclerView),to have some space for FAB that is shown on top,so that it won't interfere with the item at the bottom.Using this solution is problematic,because the fastScroll will either include the part of the transparent navigation bar,or be much smaller as it will use the padding in addition to the navigation bar (`considerPadding`=true).Any suggestion?Here's a link: https://github.com/zhanghai/AndroidFastScroll/issues/34#issuecomment-763206834 – android developer Jan 20 '21 at 01:30
  • I just took a quick 5 minute look, but don't see the whole picture. Are you still using window insets and calling `updatePadding`? How do you add bottom padding, because `updatePadding` would overwrite it? – Adrijan Jan 20 '21 at 11:09
  • In some places in the app I don't need to handle the insets. But I do want to have the ability to have padding to the RecyclerView itself (in the layout XML file), to offer space for FAB to be shown on top without hiding the last items. In the sample I've provided, I don't use insets at all. Do you think I did it wrong? – android developer Jan 20 '21 at 15:44
  • I think it's partially because of clipping. I think that if I disable clipping, there are no issues, but then I can't use the padding trick, and I would have to use something else instead (like margin for the last item, for example). – android developer Jan 21 '21 at 08:17
  • 1
    I don't think XML only is going to work. I think a better ReyclerView solution is to use custom ItemDecoration, that adds a predefined bottom offset to only the last item. An example of this (it's called padding, even though this is more like a margin) can be seen here: https://gist.github.com/vincetreur/4623110107c07c44b390 (this one also adds some top margin). It is likely something you need, because margin will not affect the calculations for `considerPadding = true` – Adrijan Jan 22 '21 at 10:12
  • This will however likely make the item non-clickable in the margin region (where the FAB is). If that's an issue, I can also propose another solution. – Adrijan Jan 22 '21 at 10:18
  • Why would it be a problem that the user presses an empty space (the margin area) ? The FAB is what's important there. Pressing empty space shouldn't do anything... Anyway, I've found this solution for adding spacing at the end, but as I've tried them, I noticed that each has issues, so I wrote it (based on something I've found) here: https://stackoverflow.com/a/65858514/878126 . Another possible good solution is using a view at the bottom, BTW, which will take the whole row. Anyway, thanks. – android developer Jan 23 '21 at 11:28
  • Cool, glad you were able to find a solution. – Adrijan Jan 24 '21 at 07:42