RecyclerView inside BottomSheetBehavior - android

I am building an Android screen that allows the user to place multiple "sticker" images on top of a background image. When the user taps on an icon, a BottomSheet containing a RecyclerView of stickers images is rendered from the bottom at around 75% of the screen height. As you will see in the attached GIF, when I scroll up, the top of the BottomSheet moves to the top rather than sticking to its peek height. I am using a BottomSheetBehavior and am setting the peekHeight to collapse/expand it. I was using a BottomSheetDialogFragment but was experiencing the same issue.
Can anyone see what I am missing? I read about nested scrolling so I set "nestedScrollingEnabled" to true for the RecyclerView but that did not seem to help.
In my activity, I render the BottomSheet via an include tag:
<include android:id="#+id/sticker_bottom_sheet" layout="#layout/fragment_sticker_bottom_sheet" />
Here is the bottom sheet layout file:
<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"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/black_overlay"
android:orientation="vertical"
app:layout_behavior="#string/bottom_sheet_behavior"
app:behavior_hideable="true"
app:behavior_peekHeight="0dp"
android:id="#+id/stickerCollectionBottomSheet"
>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/stickerRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:overScrollMode="never"
android:scrollbars="none"
android:nestedScrollingEnabled="true"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
/>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
and then wire up the RecyclerView in the activity:
private fun setupStickerBottomSheet() {
val stickerBottomSheetLayout = binding.stickerBottomSheet?.stickerCollectionBottomSheet
stickerContainerBottomSheet = BottomSheetBehavior.from(stickerBottomSheetLayout)
val stickerRecyclerView = binding.stickerBottomSheet?.stickerRecyclerView
val adapter = StickersAdapter(StickersAdapter.OnStickerTappedListener { s, i ->
onStickerClicked(s, i)
}, viewModel.stickers.data?.toList() ?: emptyList())
val layoutManager = GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false)
stickerRecyclerView?.adapter = adapter
stickerRecyclerView?.layoutManager = layoutManager
}
Thanks in advance!

Related

How to create an android Bottom sheet that fits the size of the sheet with view always aligned to bottom?

I had a bit of trouble figuring out how to word this one when researching so I've made a quick mockup of what I'm trying to achieve below.
my app has a persistent bottom sheet and I would like the contents of the sheet to be resized based on whether the sheet is expanded or half expanded.
My sheet consists of a recycler view and a view at the bottom with some buttons. Currently, the contents of the sheet are always sized as though the sheet is full expanded. This means that the buttons are not displayed unless the sheet is fully expanded and much of the recycler view is also hidden behind the cropped-off sheet. What I want to happen is for the buttons to always be displayed at the bottom of the sheet, even when said sheet is half expanded, and for the recyclerview to fill whatever space is left. Is this possible in a persistent bottom sheet and if now, what means should I use to achieve this? And help would be greatly appreciated.
You can make the buttons layout to always be displayed at the bottom of the sheet by setting its y coordinate by tracking the bottom sheet sliding using addBottomSheetCallback callbacks:
val bottomSheet = findViewById<ConstraintLayout>(R.id.bottom_sheet)
val bottomLayout = findViewById<LinearLayout>(R.id.button_layout)
// initial setting the y coordinates of the bottom layout
bottomSheet.post {
val bottomSheetVisibleHeight = bottomSheet.height - bottomSheet.top
bottomLayout.y =
(bottomSheetVisibleHeight - bottomLayout.height).toFloat()
}
BottomSheetBehavior.from(bottomSheet).apply {
addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
// set the y coordinates of the bottom layout on bottom sheet slide
val bottomSheetVisibleHeight = bottomSheet.height - bottomSheet.top
bottomLayout.y =
(bottomSheetVisibleHeight - bottomLayout.height).toFloat()
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
}
})
}
Layout:
<?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">
<!-- Main Layout -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:behavior_hideable="false"
app:layout_behavior="#string/bottom_sheet_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#CFE2F3"
app:layout_constraintBottom_toTopOf="#+id/button_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="#+id/button_layout"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="#FF03DAC5"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent">
<Button
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_marginEnd="40dp"
android:layout_weight="1"
android:text="cancel" />
<Button
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:text="ok" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

Smoothly animate BottomSheet peek height

I'm trying to animate the height of the BottomSheet on layoutChange, but I can't get anything to work. I've tried several tutorials without luck.
Currently the height just snaps. I'm using a custom BottomSheetDialogFragment(). I've tried TransitionManager.beginDelayedTransition(sheetParent, AutoTransition()) with sheetParent obviously being the top most layout in my setup. It is a coordinatorLayout.
This is my layout file:
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/sheetParent"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="#+id/bottomSheetBehavior"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/bottomSheetContent"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:id="#+id/drag_pill"
android:layout_width="50dp"
android:layout_height="5dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="#drawable/bottom_sheet_pill"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Below the normal view are more views, which I removed to make this a little more readable. I extend and minimize cards in the bottomSheet with the .isVisible = true/false.
How can I smoothly animate that?
You can animate any kind of value that you would like to change in a View using the ValueAnimator.
For example, in your case in order to animate smoothly the peeking height of a bottom sheet, you can use this in your BottomSheetDialogFragment():
Kotlin:
val behavior by lazy { (dialog as BottomSheetDialog?)?.behavior }
val originalHeight = 500 // current height
val updatedHeight = 1000 // desired height
private fun updatePeekHeight(originalHeight: Int, updatedHeight: Int) {
ValueAnimator.ofInt(originalHeight, updatedHeight).apply {
addUpdateListener {
behavior?.peekHeight = it.animatedValue as Int
}
duration = 300 // adjust the duration accordingly
start()
}
}

BottomSheetBehavior with ViewPager2 can't be scrolled down by nested RecyclerView scroll

I have a view that acts like BottomSheetBehavior and this view has ViewPager2 inside. Each ViewPager2's page is a vertical RecyclerView. The issue is that BottomSheet doesn't scroll down when current vertical RecyclerView (which is a page of ViewPager) can't scroll vertically anymore. Everything works file when instead of ViePager I have only one vertical RecyclerView.
The temporary solution is to wrap ViewPager with NestedScrollView but it's horrible for performance and has it's own bugs.
The original layout:
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/core"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#C7C7C7"
tools:context=".MainActivity">
<LinearLayout
android:id="#+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF"
android:elevation="8dp"
android:orientation="vertical"
app:behavior_hideable="true"
app:behavior_peekHeight="300dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<com.google.android.material.tabs.TabLayout
android:id="#+id/tab_layout"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_gravity="center_horizontal"
app:tabGravity="center"
app:tabMode="scrollable" />
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
[Here's how it looks (sorry for the gif quality)]
I've found a solution for this case, I set isNestedScrollingEnabled = false for inner RecyclerView so that BottomSheetBehavior finds another scrolling view
viewPager.children.find { it is RecyclerView }?.let {
(it as RecyclerView).isNestedScrollingEnabled = false
}
BottomSheetBehaviour only detects the first scrollable view. So it is always recommended to use only one scrollable view inside of it.
For More information check this answer bottomsheetbehavior-with-two-recyclerview
And this one also Scroll not working for multiple RecyclerView in BottomSheet
If you really want to have the two scrollable views I recommend you to take a look at this library also AndroidSlidingUpPanel

BottomSheetBehavior padding

I have such kind of fragment layout.
<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:background="#color/colorBackgroundBlack">
<MyCustomView
android:id="#+id/vBottomSlider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior" />
</android.support.design.widget.CoordinatorLayout>
For my vBottomSlider I create a BottomSheetBehavior instance:
val bh = BottomSheetBehavior.from(vBottomSlider)
bh.isHideable = false
bh.peekHeight = 50.dpToPx
bh.setBottomSheetCallback(mBottomCallback)
That's all inside the fragment.
My main activity layout looks like:
<android.support.constraint.ConstraintLayout
android:id="#+id/vMainConstraint"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="#+id/fContent"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.aurelhubert.ahbottomnavigation.AHBottomNavigation
android:id="#+id/vNavigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" />
</android.support.constraint.ConstraintLayout>
For my fContent i set bottom padding as vNavigation height. When i scroll down my vBottomSlider, i hide vNavigation by changing its translationY, and set padding for my fContent (by taking onSlide event from BottomSheetBehavior.BottomSheetCallback).
Sliding with finger completely, works fine.
But when i fling or set EXPANDED or COLLAPSED state programmatically for my BottomSheetBehavior instance its not get scrolled completely. Here is always some space (it seems to be height of my vNavigation).
I've managed to resolve this by editing the source of BottomSheetBehavior.
My solution is little hacky. Send e-mail if you need edited class.

Persistent BottomSheet below ActionBar

I have an app layout with a custom toolbar and a persistent BottomSheet - both inside of a CoordinatorLayout.
On a button click I want to show the BottomSheet. Right now the sheet is displayed fullscreen and overlays the toolbar. By setting the app theme to Theme.AppCompat.Light.DarkActionBar the BottomSheet stays below the ActionBar, but the bar cannot be customized.
Is there a way to limit the height of the persitent BottomSheet to fullscreen - ActionBar height?
This is my code in activity_main.xml
<android.support.design.widget.CoordinatorLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:attrs="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="com.test.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
app:elevation="20dp"
android:elevation="20dp"
android:layout_height="?attr/actionBarSize">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
app:elevation="20dp"
android:elevation="20dp"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"/>
</android.support.design.widget.AppBarLayout>
</LinearLayout>
<include layout="#layout/bottom_sheet_additem"/>
</CoordinatorLayout>
Here is the code of sheet_bottom.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/bottomSheetLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/colorAccent"
app:behavior_hideable="true"
app:behavior_peekHeight="0dp"
android:fitsSystemWindows="true"
app:layout_behavior="#string/bottom_sheet_behavior">
<TextView
android:id="#+id/bottomsheet_text"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="Lorem Ipsum Dolor..."
android:textColor="#FFFFFF" />
</RelativeLayout>
The image on the left hand side shows the BottomSheet which stops below the Toolbar - which is not working with my current code. Currently it looks like the picture on the right.
I had the same problem... I don't know if it's the best solution, but for now, worked for me.
Try to put your include inside another CoordinatorLayout in your activity_main.xml, with a marginTop like this:
<android.support.design.widget.CoordinatorLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:attrs="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="com.test.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
app:elevation="20dp"
android:elevation="20dp"
android:layout_height="?attr/actionBarSize">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
app:elevation="20dp"
android:elevation="20dp"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"/>
</android.support.design.widget.AppBarLayout>
</LinearLayout>
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="56dp">
<include layout="#layout/bottom_sheet_additem"/>
</android.support.design.widget.CoordinatorLayout>
</CoordinatorLayout>
I hope it helps.
We can use app:layout_behavior instead of fixed height
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<include layout="#layout/bottom_sheet_additem"/>
</android.support.design.widget.CoordinatorLayout>
Your sheet_bottom should look like this
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:showIn="#layout/activity_main">
<RelativeLayout
android:id="#+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:behavior_hideable="true"
app:behavior_peekHeight="?android:attr/actionBarSize"
app:elevation="#dimen/size_5dp"
app:layout_behavior="#string/bottom_sheet_behavior">
</RelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
The accepted answer works when you expand the bottom sheet to the full screen, but when collapsing it, it adds additional margin that makes a part of the collapsing layout hidden below the screen, so I decided to put the margin programmatically by listening to the collapse/hidden status of the BottomSheet.
First add the BottomSheet within a CoordintorLayout in xml
And add below callback listener.
mBottomSheetBehavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
CoordinatorLayout bottomSheet = findViewById(..); // inflate the bottom sheet
CoordinatorLayout.LayoutParams layoutParams =
(CoordinatorLayout.LayoutParams) bottomSheet.getLayoutParams();
if (newState == BottomSheetBehavior.STATE_COLLAPSED)
layoutParams.setMargins(0, 0, 0, 0); // remove top margin
else if (newState == BottomSheetBehavior.STATE_EXPANDED) {
layoutParams.setMargins(0, 100, 0, 0); // add top margin
bottomSheet.setLayoutParams(layoutParams);
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
}
});
+1 for updating the margin programmatically. While the hidden part of the BottomSheet can be offset by increasing the peek height, using just the original solution above means the margin projects onto the underlying UI elements so the scroll action area projects off the actual BottomSheet onto the other UI.
Using the onStateChanged method in the BottomSheetCallback means acting on the event that the BottomSheet has already expanded or collapsed. Adding or removing the margin at this stage can result in seeing 'jerky' behaviour where for example the sheet momentarily reaches the fully expanded state covering the appbar before the margin is then programmatically applied to shift the UI components down resulting in a 'flash' as this applies.
Instead I used the onSlide method to detect when the BottomSheet was being slid up or down and added or removed the margins only once the sheet was half way through the transition. If the margins are applied too early in the slide motion then again the user can see the BottomSheet UI jumping up or down not long after initiating the action (they are less likely to notice this at the half way point if they have done a 'fling' up or down motion.
Also I found it worked best to fetch the height of the AppBar and the status bar and use those to set the required padding value for accurate placement in expanded mode.
This challenge can be avoided altogether by using a widget to trigger the BottomSheet state change programmatically.
#Override
public void onSlide(View bottomSheet, float slideOffset) {
boolean inRangeExpanding = oldOffSet < slideOffset;
boolean inRangeCollapsing = oldOffSet > slideOffset;
oldOffSet = slideOffset;
if (inRangeCollapsing && slideOffset < 0.5f) {
//reset padding on top of bottomsheet so there is no padding/overlap onto underlying sheet (which overlaps underlying sheet and so interfers with scrolling behaviour
bSheetView.setPadding(0,10,0,0);
Log.d(TAG,"onSlide STATE_COLLAPSING");
} else if(inRangeExpanding && slideOffset > 0.5f){
//reset padding on top of bottomsheet so there is padding/overlap onto underlying sheet so it does not write over the top of the menu appbar
bSheetView.setPadding(0, topMargin,0,0);
Log.d(TAG,"onSlide STATE_EXPANDING");
}
}

Categories

Resources