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
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:
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>
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
}
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 the
RecyclerView` 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.
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).
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
}
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).
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"
.
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.
User contributions licensed under CC BY-SA 3.0