I have a rather complex BottomSheetLayout which layout is as follow
The root view of my bottom sheet is a custom FrameLayout that allows to round it's corner (both background and children). Nothing else (nothing touch-related)
Then, I use the usual ConstraintLayout in order to layout my Bottom sheet.
This ConstraintLayout contains, amongst other views, a vertical RecyclerView:
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp">
<!-- other views -->
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/events"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="25dp"
android:layout_marginBottom="74dp"
app:layout_constraintTop_toBottomOf="#+id/days"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:background="#{viewModel.colors.defaultBackgroundColor}"
tools:background="#ECF0F3"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="#layout/event_item"
tools:itemCount="10" />
</androidx.constraintlayout.widget.ConstraintLayout>
I have no particular issue while dragging my bottom sheet, however, when fully expanded I was expecting the be able to scroll the content of my RecyclerView. But I cannot.
After a lot of researches, I managed to make it scroll by enabling scrolling when my Fragment's view is inflated :
ViewCompat.setNestedScrollingEnabled(this.binding.bottomSheetEvents.getRoot(), true);
However, doing so has a weird consequence. When my bottom sheet's state is EXPANDED, I can finally scroll my RecyclerView, but then there is absolutely no way to drag my Bottom sheet any more : it remains fully expanded.
I have tried a few other ways.
I have tried wrapping my NestedScrollView. In past experience I was able to have the full content of my bottom sheet scrollable thanks to NestedScrollView, but in this case, I only want to scroll my RecyclerView. What ever is above it must remain idle.
I have tired this.binding.bottomSheetEvents.events.setNestedScrollingEnabled(false); but there is no difference.
My belief is that when the bottom sheet is fully expanded, it dispatches scroll events to inner children that can supports its. And, backwards, it knows, at some point, when uses wishes to collapse said bottom sheet. So I guess, something wrong must be happening there.
Further informations:
this bottomsheet is included in my fragment which roots view is a CoordinatorLayout obviously.
the fragment is also hosted in CoordinatorLayout with an AppBar
the include layout uses the app:layout_behavior="#string/bottom_sheet_behavior"
and the include layout also uses behavior_fitToContents set to false so that I can use method setExpandedOffset to prevent the bottom sheet to reach the top.
Version used : 1.1.0-alpha07
Thanks for the help!
Related
Reproducible sample project on github: recyclerview-bottomsheet-not-recycling
On Android it is common to have a draggable bottomsheet. For any scrollable content, RecyclerView is a great candidate. However, when put inside a bottomsheet, recycling/virtualization does not occur for the items that would have been visible if the bottomsheet was fully expanded. This implies a significant performance hit for no apparent reason. I.e. views are inflated and the onDraw() method is called. I believe tons of BottomSheet featured apps silently suffer from this. There ought to be a solution to this non-performant behavior.
Google issue exists: https://issuetracker.google.com/issues/180537056
How can you configure/tweak RecyclerView or its LayoutManager to prevent invisible bottomsheet items from being rendered? Please avoid ViewStub solutions. Let's focus on the root of the problem - the RecyclerView.
Illustration
E.g. If the height of the main layout is 1000px and the invisible RecyclerView has items with a height of 200px, then five views will be immediately created and rendered:
My App
Main View (1000px)
Collapsed bottom sheet
Item 1 renderedItem 2 renderedItem 3 renderedItem 4 renderedItem 5 renderedItem 6 NOT renderedItem 7 NOT rendered...
And a screenshot from the sample app on github. Here a bunch of items below the screen have been rendered - even though they are invisible.
The relevant part of the layout
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="#+id/layout_retainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/black">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:clipToPadding="false"
android:background="#color/white"
app:behavior_hideable="false"
app:behavior_peekHeight="#dimen/bottom_sheet_peek_height"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
android:adapter="#{adapter}" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
So we have a 3 part app structure with 2 toolbars: 1 that is significant and should be always visible and 1 that is insignificant and should only be visible when user has scrolled to the top of page .the third part is a complex structure with nested view group, swipe to refresh, recycler view and some extra views . Note that we are not using the Collapsible toolbar or any other toolbar, but rather custom viewgroups.
I tried to achieve the behavior of toolbars with my current xml structure like this (notice the layout behavior flags) (PS: i cannot share the exact code,but would provide more details if needed)
<Coordinator layout>
<include layout=“xyz”> <— <appbarlayout>
<linearlayout :layout_behavior:scroll|enterAlways …/>
<linearlayout :layout_behavior:noscroll …/>
</appbarlayout>
<swipe refresh layout : layout_behavior="....AppBarLayout$ScrollingViewBehavior”>
<Nested ScrollView : mp/mp , fillviewport :true>
<constraint layout : mp/mp>
<RecyclerView : nested scrollin enabled:false (via java code)>
<View>
</constraint layout>
</nested scrollview>
</swipe refresh layout>,
</Coordinator layout>
<!-- mp = match_parent -->
the result looks something like this( gif / video ) (note the gif/video has some delay, please wait 7-8 seconds to see the action ) .
As you can see, the upper toolbar gets hidden when scrolled very hardly, but when scrolled slowly, the upper bar does not hide.
What can i do to fix this? i have tried changing the behavior flags as well as setting the heights as wrap content. I am guessing it is either due to wrong flags or due to complexity in bottom layout
When I'm scrolling down, the items above the RecyclerView does not scroll unless I start touching from the layout above, and it only scrolls down when I have reached the end of the RecyclerView.
<NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout>
<Some other items.../>
</LinearLayout>
<RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</NestedScrollView>
Note:
I actually use a fixed size for the RecyclerView, setting it via the code below:
float height_recyclerview = (ScreenUtil.getHeight(context) - (height_banner + height_bottom_navigation + height_create_post));
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, (int) height_recyclerview);
rv.setLayoutParams(layoutParams);
Why do I use fixed size if it works smoothly with wrap_content?
I will be displaying possibly thousands of items that may have
images, which will hurt performance if it does not actually do
recycling because of the issue that the RecyclerView is inside the
NestedScrollView
I have implemented an EndlessRecyclerViewScrollListener which has an
issue that it keeps loading more data from server continuously if
implemented with a RecyclerView that is within whatever scrollable
view, or if it is in a scrollable view, but does not have a fixed
height, even if you are not scrolling down.
I have tried the following:
set nested scrolling to false on the recycler view
try using scroll view instead of nested scroll view
a bunch of other code related to layouts and scrolling behaviors that others suggested which didn't work for me because I'm implementing it in a much more complicated layout and the fact that I use EndlessRecyclerViewScrollListener
What I want to fix?
I want to make the page scroll like a single page, not as a separate scrollable view.
Note that my recycler view has a fixed height that takes the entire screen's space meaning that its height is actually fit assuming that the linear layout above is not visible anymore if the user has scrolled down.
The ideal scenario is to make the scrollview scroll down first, to make the recycler view take the entire screen, so that the recyclerview will scroll however the user wants to.
Then the linearlayout above which should not be visible anymore if the recycler view has taken up all the space of the screen, should only show up if the recycler view has reached the top/first item, if the user keeps scrolling back up.
Read this.
Add app:layout_behavior="#string/appbar_scrolling_view_behavior" to your recycler xml.
<android.support.v7.widget.RecyclerView
android:id="#+id/conversation"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
NestedScrollView Smooth Scrolling
recyclerView.isNestedScrollingEnabled = true
Do this programmatically
<androidx.core.widget.NestedScrollView 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="0dp"
android:fillViewport="true"
...
I have a RecyclerView using a LinearLayoutManager with HORIZONTAL orientation, nested inside a FrameLayout using the BottomSheet Behavior.
When attempting to drag vertically across the RecyclerView, the BottomSheet doesn't respond to the drag event. Presumably this is because vertical scrolling is disabled for a LayoutManager with horizontal orientation.
I've tried overriding LinearLayoutManager.canScrollVertically() and returning true. This sort of works.. If you drag vertically in a very careful manner, the BottomSheet will respond. As soon as any horizontal movement is involved however, the BottomSheet stops responding to vertical drag events.
I'm not sure if overriding canScrollVertically() is the right approach here - it certainly doesn't feel right from a UX point of view.
I've also noticed that if I use a ViewPager rather than a RecyclerView with a horizontally oriented LayoutManager, the BottomSheet responds to vertical swipe events as desired.
Is there some other method of LayoutManager, RecyclerView, BottomSheet Behavior, or something else altogether that can help propagate the vertical scroll events on to the BottomSheet Behavior?
There's an example of the problem here:
https://github.com/timusus/bottomsheet-test
(Problem can be reproduced with commit #f59a7031)
Just expand the first bottom sheet.
Where does the problem lies? In FrameLayout. BottomSheet works perfectly when put inside CoordinatorLayout. Then BottomSheet can pass it's scrolling state through CoordinatorLayout to other views put as direct children of CoordinatorLayout.
Why RecyclerView was not able to pass scroll state to BottomSheet? It is not a direct child of CoordinatorLayout. But there exists a way to pass them: RecyclerView must be in put in view that implements NestedScrollingParent and NestedScrollingChild. The answer to that is: NestedScrollView
So your fragment_sheetX.xml layouts should look like:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff"
android:orientation="vertical"
android:fillViewport="true">
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.v4.widget.NestedScrollView>
Notice also android:fillViewport="true" as otherwise, your RecyclerView will not take whole height.
However it still will not work. Why? RecyclerView must be told to pass vertical scrolling to parent. How? The answer is recyclerView.setNestedScrollingEnabled(false);, but that is better described here.
Btw: MultiSheetView is a great feature and a very interesting approach to mobile UX design.
I have a bottom sheet with its height and width set to match_parent. So when on button click I set the behavior to STATE_EXPANDED like this:
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
My Bottomsheet is defined as below:
<FrameLayout
android:id="#+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:elevation="#dimen/design_appbar_elevation"
app:behavior_hideable="true"
app:layout_behavior="#string/bottom_sheet_behavior">
<include
android:id="#+id/bottom_sheet_content"
layout="#layout/bottomsheet_layout" />
</FrameLayout>
I am monitoring states with the BottomSheet Callbacks.
I click on a button and bottom sheet expanded to full screen.
Its current State is STATE_EXPANDED
I quickly swipe down on the bottom sheet. (Not fully drag till it closed, simple swipe down like scrolling)
It stops at the middle and its state is logged as STATE_COLLAPSED
If I swipe again it is all gone and its state is STATE_HIDDEN
I don't understand why it stops in the middle. How can I make it hidden with a single swipe.
I tried that by setting peek_height to 0dp. By this, it never encounters the STATE_HIDDEN. When hidden, its state becomes STATE_COLLAPSED. I just don't understand this states.
How to achieve STATE_HIDDEN with a single swipe down?
Kinda late but I just stumbled upon this while searching for something similar.
This is how you can skip the collapsed state:
In XML by adding app:behavior_skipCollapsed="true" to the BottomSheet view.
OR
Programmatically with setSkipCollapsed(boolean).