How i can set Half Expanded state for my BottomSheet - android

I have layout with bottom sheet.
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="#+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="#color/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="#layout/content_main_weather_map" />
<include layout="#layout/bottom_sheet" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Bottom sheet layout
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView 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/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/white"
android:clipToPadding="true"
app:behavior_peekHeight="80dp"
app:layout_behavior="#string/bottom_sheet_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/weather_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
tools:listitem="#layout/item_weather" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
It is necessary for me that my bottom sheet opens first half, and after re-dragging it opens to full screen. How is it done in google maps app. But I have no idea how to do this.

It is better to use the framework with its full potential. As official documentation states for method setFitToContents :
Sets whether the height of the expanded sheet is determined by the height of its contents, or if it is expanded in two stages (half the height of the parent
container, full height of parent container). Default value is true.
So all you need is set setFitToContent to false with:
behavior = BottomSheetBehavior.from(your_bottom_sheet_xml)
behavior.isFitToContents = false
behavior.halfExpandedRatio = 0.6f
With this 3-line-code the bottom sheet will expand till 60% of the screen at first, and afterwards it will fully expand to 100%.
Hope it helps!

Just set BottomSheetBehaivor state to BottomSheetBehavior.STATE_HALF_EXPANDED.
Also if you need after full expanding let user again go back to half expanded mode, you need to set peek height to half of window height.
val bottomSheetBehavior = BottomSheetBehavior.from<NestedScrollView>(bottom_sheet)
val metrics = resources.displayMetrics
bottomSheetBehavior.peekHeight = metrics.heightPixels / 2
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED

I have tried the #Massab and #HeyAlex but didn't match my desired behavior.
With the following solution in kotlin, if your bottomsheet sliding is near the expanded state, it stays expanded, if is near the half state, stays at half and if it's near collapsed, it stays collapsed:
val bottomSheet = view.findViewById<View>(R.id.bottom_sheet1)
val mBottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
mBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
mBottomSheetBehavior.addBottomSheetCallback(object: BottomSheetBehavior.BottomSheetCallback(){
override fun onStateChanged(bottomSheet: View, newState: Int) {
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
val upperState = 0.66
val lowerState = 0.33
if (bottomSheetEventsFilterBehavior.state == BottomSheetBehavior.STATE_SETTLING ) {
if(slideOffset >= upperState){
mBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
if(slideOffset > lowerState && slideOffset < upperState){
mBottomSheetBehavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
}
if(slideOffset <= lowerState){
mBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
}
})

Although this question has been answered, but just got another way to implement this behavior so sharing for others.
Create a global variable and initialize it with the default state of your BottomSheetBehavior, like
int state = BottomSheetBehavior.STATE_COLLAPSED;
Then, in BottomSheetBehavior.BottomSheetCallback update your state variable to the current state
and in
BottomSheetBehavior.STATE_DRAGGING, if state is not half expanded,
set the state to BottomSheetBehavior.STATE_HALF_EXPANDED
sheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View view, int i) {
switch (i) {
case BottomSheetBehavior.STATE_COLLAPSED:
state = BottomSheetBehavior.STATE_COLLAPSED;
binder.imgRefresh.setVisibility(View.GONE);
break;
case BottomSheetBehavior.STATE_EXPANDED:
binder.imgRefresh.setVisibility(View.VISIBLE);
state = BottomSheetBehavior.STATE_EXPANDED;
break;
case BottomSheetBehavior.STATE_DRAGGING:
if (state != BottomSheetBehavior.STATE_HALF_EXPANDED) {
sheetBehavior.setState(BottomSheetBehavior.STATE_HALF_EXPANDED);
}
break;
case BottomSheetBehavior.STATE_HALF_EXPANDED:
state = BottomSheetBehavior.STATE_HALF_EXPANDED;
break;
}
}
#Override
public void onSlide(#NonNull View view, float v) {
binder.viewExtender.setAlpha(1 - v);
}
});
This will make your BottomSheet to take three steps , i.e., Collapsed, Half Expanded, Expanded.
Hope it can help someone!

class BottomSheetFragment : BottomSheetDialogFragment() {
/* inside of your Bottom Sheet Dialog Fragment */
override fun onStart() {
super.onStart()
BottomSheetBehavior.from(requireView().parent as View).apply {
state = BottomSheetBehavior.STATE_HALF_EXPANDED
}
}
}

Use this block in onCreateView before returning root view
dialog!!.setOnShowListener { dialog ->
val d = dialog as BottomSheetDialog
BottomSheetBehavior.from(requireView().parent as View).apply {
state = BottomSheetBehavior.STATE_EXPANDED
}
}

Related

Scrim color change callback on CollapsingToolbarLayout?

Problem
Given the following view hierarchy
<?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/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="96dp"
android:backgroundTint="?android:colorBackground">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="#+id/collapsing_toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:collapsedTitleGravity="start"
app:collapsedTitleTextAppearance="?textAppearanceHeadline6"
app:contentScrim="?colorSurface"
app:expandedTitleGravity="start|bottom"
app:expandedTitleMarginBottom="16dp"
app:expandedTitleMarginEnd="16dp"
app:expandedTitleMarginStart="16dp"
app:expandedTitleTextAppearance="?textAppearanceHeadline5"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:statusBarScrim="?android:colorBackground"
app:toolbarId="#+id/toolbar">
<com.google.android.material.appbar.MaterialToolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:background="#null"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/menu_section_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
When a user scrolls the list
Then status bar background color should change as per the top navigation bar's background at-least in expanded and collapsed state.
Current Solution
Implement a custom OnOffsetChangedListener
internal abstract class AppBarLayoutOffsetStateChangeListener : AppBarLayout.OnOffsetChangedListener {
enum class State {
COLLAPSED, EXPANDED, SCROLLING
}
private var postJob: Runnable? = null
abstract fun onStateChange(state: State)
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
if (abs(verticalOffset) - appBarLayout.totalScrollRange == 0) {
postJob = Runnable {
onStateChange(State.COLLAPSED)
}
appBarLayout.postDelayed(postJob, DELAY_COLLAPSED_STATE)
} else if (verticalOffset == 0) {
onStateChange(State.EXPANDED)
} else {
onStateChange(State.SCROLLING)
postJob?.let {
appBarLayout.removeCallbacks(it)
postJob = null
}
}
}
private companion object {
const val DELAY_COLLAPSED_STATE = 1000L // in ms.
}
}
Set an instance of AppBarLayoutOffsetStateChangeListener to AppBarLayout
appBarLayout.addOnOffsetChangedListener(object : AppBarLayoutOffsetStateChangeListener() {
override fun onStateChange(state: State) {
when (state) {
State.EXPANDED -> {
// set status bar color to `AppBarLayout` background tint.
}
State.COLLAPSED -> {
// set status bar color to `CollapsingToolbarLayout` content scrim color
}
else -> Unit
}
}
})
Rough edges
CollapsingToolbarLayout color animate changes independently of AppBarLayout vertical offset, so had to add DELAY_COLLAPSED_STATE of 1 second ! just to synchronize the two animations.
Status bar colors in Expanded and Collapsed states are picked from two different views, after cross-checking using Layout Inspector.
Question to the community
Is there a better solution ?

Bugged BottomSheet which is shown when not required

I have a BottomSheet in my App whic have to be shown ONLY at the BottomAppBar navigation button click.
The issue is that in my BottomAppBar i have a FAB button which handles a long click which shows a View and a FAB menu,
when long clicked it make even the BottomSheet expanded when there is no code for it!
Here is my BottomSheet:
<LinearLayout
android:id="#+id/bottomSheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:behavior_hideable="true"
android:elevation="8dp"
android:clickable="true"
android:focusable="true"
app:behavior_peekHeight="0dp"
app:layout_behavior="#string/bottom_sheet_behavior">
<include layout="#layout/bottom_sheet" />
</LinearLayout>
Here is the only click where i make it EXPANDED_HALF:
bottomAppBar.setNavigationOnClickListener {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
}
And here is my longClick which make it fully expanded:
fabNuovo.setOnLongClickListener {
if (View.GONE == fabBGLayout.visibility) {
showFAB()
}else {
closeFAB()
}
}
private fun showFAB() {
fabNuovo.setImageDrawable(ContextCompat.getDrawable(this#LetturaActivity,R.drawable.ic_baseline_close_24))
fabSend.visibility = View.VISIBLE
fabBGLayout.visibility = View.VISIBLE
fabSend.animate().translationY(-120F)
}
private fun closeFAB() {
fabNuovo.setImageDrawable(ContextCompat.getDrawable(this#LetturaActivity,R.drawable.ic_baseline_done_24))
fabBGLayout.visibility = View.GONE
fabSend.visibility = View.GONE
fabSend.animate().translationY(0f)
}
EDIT:
It's not expanded for really as it doesn't cast any callback in BottomSheetCallback...
And even if i click to dismiss the FAB menu it's expanded if not and collapsed if expanded...
EDIT2:
The issue is surely given from one of the following lines in showFAB and closeFAB:
fabSend.visibility = View.VISIBLE
fabBGLayout.visibility = View.VISIBLE
fabSend.animate().translationY(-120F)

Viewpager2 inside Nestedscrollview consist two Fragment with Recyclerview having problem

I made a viewpager2 which has two Fragments, inside each Fragment there is a Recyclerview. The viewpager itself is inside a Nestedscrollview in order to hide the toolbar when scroll up. Here is my code:
<?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"
android:orientation="vertical"
>
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appbarlayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#color/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways">
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.tabs.TabLayout
android:id="#+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
app:tabGravity="fill"
app:tabMode="fixed"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<include layout="#layout/material_design_floating_action_menu" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
As I said the viewPager2 have two fragment each of them have a recyclerview. Here problem is, fragment 2 recyclerView take the same height of fragment 1 recyclerView though both recyclerView have different list items and their height should be depends on the list items. I mean, I am expecting these recyclerViews height should act separately based on the list. How can I solve this issue? Please let me know if you need fragment code.
Edit:
Activity code which holds the viewPager2
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initToolbar();
init();
viewPager.setAdapter(createCardAdapter());
new TabLayoutMediator(tabLayout, viewPager,
new TabLayoutMediator.TabConfigurationStrategy() {
#Override public void onConfigureTab(#NonNull TabLayout.Tab tab, int position) {
//tab.setText("Tab " + (position + 1));
if(position == 0){
tab.setText("Home");
}else if(position == 1){
tab.setText("Events");
}
}
}).attach();
RunnTimePermissions.requestForAllRuntimePermissions(this);
showNotifyDialog();
}
ViewPager Adapter Code:
public class ViewPagerAdapter extends FragmentStateAdapter {
private static final int CARD_ITEM_SIZE = 2;
public ViewPagerAdapter(#NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
}
#NonNull #Override public Fragment createFragment(int position) {
switch (position){
case 0:
return HomeFragment.newInstance("abc","abc");
// break;
case 1:
return EventListFragment.newInstance("abc","abc");
// break;
}
return HomeFragment.newInstance("abc","abc");
}
#Override public int getItemCount() {
return CARD_ITEM_SIZE;
}
}
Fragment 1 layout:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragment.EventListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_event_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:layout_marginTop="16dp"
/>
</FrameLayout>
Fragment 2 layout
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragment.MedicineListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_medicine_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:layout_marginTop="16dp"
/>
</FrameLayout>
Alright buddy. I did it! I have the solution to this issue.
First off, here's the official response from Google after I opened an issue.
https://issuetracker.google.com/issues/188474850?pli=1
Anyway, on to the fix. Replace the NestedScrollView with this class:
https://gist.github.com/AfzalivE/fdce03eeee8e16203bcc37ba26d7abf3
The idea is to basically create a very light-weight version of NestedScrollView. Instead of messing with child heights, we listen to the children's scroll and either let them scroll, or forward the scrolling to the BottomSheetBehavior. For example when the RecyclerView has been scrolled all the way to the top, then it doesn't consume any of the scrolling, so we can forward that to the bottom sheet so it can scroll. And we only allow the RecyclerView to scroll when the Bottom sheet is expanded.
Also, I must add that this scenario works out of the box with Jetpack Compose + Accompanist-pager so just do that if you really need perfect functionality.
class BottomSheetScrollView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs),
NestedScrollingParent2 {
private val TAG = "NestedScroll3"
private val childHelper = NestedScrollingChildHelper(this).apply {
isNestedScrollingEnabled = true
}
private var behavior: BottomSheetBehavior<*>? = null
var started = false
var canScroll = false
var pendingCanScroll = false
var dyPreScroll = 0
init {
ViewCompat.setNestedScrollingEnabled(this, true)
}
private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
onNextScrollStop(newState == BottomSheetBehavior.STATE_EXPANDED)
Log.d(
"BottomSheet",
"Can scroll CHANGED to: $canScroll, because bottom sheet state is ${
getBottomSheetStateString(newState)
}"
)
}
override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
}
fun onNextScrollStop(canScroll: Boolean) {
pendingCanScroll = canScroll
}
override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
// ViewPager2's RecyclerView does not participate in this nested scrolling.
// This allows it to show it's overscroll indicator.
if (target is RecyclerView) {
val layoutManager = target.layoutManager as LinearLayoutManager
if (layoutManager.orientation == LinearLayoutManager.HORIZONTAL) {
target.isNestedScrollingEnabled = false
}
}
if (!started) {
Log.d(TAG, "started nested scroll from $target")
childHelper.startNestedScroll(axes, type)
started = true
}
return true
}
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
Log.d(TAG, "accepted nested scroll from $target")
}
override fun onStopNestedScroll(target: View, type: Int) {
if (started) {
childHelper.stopNestedScroll(type)
started = false
Log.d(
TAG,
"stopped nested scroll from $target, changing canScroll from $canScroll to $pendingCanScroll"
)
canScroll = pendingCanScroll
}
}
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int
) {
Log.d(
TAG,
"onNestedScroll: dxC: $dxConsumed, dyC: $dyConsumed, dxU: $dxUnconsumed, dyU: $dyUnconsumed"
)
if (dyUnconsumed == dyPreScroll && dyPreScroll < 0) {
canScroll = false
Log.d(TAG, "Can scroll CHANGED to: $canScroll, because scrolled to the top of the list")
}
}
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
Log.d(
TAG,
"onNestedPreScroll: dx: $dx, dy: $dy, consumed: [ ${consumed.joinToString(", ")} ]"
)
if (!canScroll) {
childHelper.dispatchNestedPreScroll(dx, dy, consumed, null, type)
// Ensure all dy is consumed to prevent premature scrolling when not allowed.
consumed[1] = dy
} else {
dyPreScroll = dy
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
behavior = findBottomSheetBehaviorParent(parent) as BottomSheetBehavior<*>?
behavior?.addBottomSheetCallback(bottomSheetCallback)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
behavior?.removeBottomSheetCallback(bottomSheetCallback)
}
private fun findBottomSheetBehaviorParent(parent: ViewParent?): CoordinatorLayout.Behavior<*>? {
if (parent !is View) {
throw IllegalArgumentException(
"None of this view's ancestors are associated with BottomSheetBehavior"
)
}
val layoutParams = parent.layoutParams
return if (layoutParams is CoordinatorLayout.LayoutParams && layoutParams.behavior != null) {
layoutParams.behavior
} else {
findBottomSheetBehaviorParent((parent as View).parent)
}
}
private fun getBottomSheetStateString(state: Int): String {
return when (state) {
1 -> "STATE_DRAGGING"
2 -> "STATE_SETTLING"
3 -> "STATE_EXPANDED"
4 -> "STATE_COLLAPSED"
5 -> "STATE_HIDDEN"
6 -> "STATE_HALF_EXPANDED"
else -> "0"
}
}
}
I do not understand why you have recyclerview inside FrameLayout .
Here problem is, fragment 2 recyclerView take the same height of
fragment 1 recyclerView though both recyclerView have different list
items and their height should be depends on the list items
RecyclerView generally always depends on the item_layout until we have not fixed its height n width to match_parent.
In your case you have fixed RecyclerView android:layout_width and android:layout_width to match_parent in both the Fragment .
Try this:
Fragment 1
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragment.EventListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_event_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
/>
</FrameLayout>
Fragment 2
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragment.MedicineListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_medicine_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
/>
</FrameLayout>
P.S: assuming each reacyclerView's item_layout would be having height as wrap_content

How do I achieve on scroll behavior where both the toolbar and bottom navigation should hide?Only one of them is working

I am trying to achieve the coordinator layout behaviour where on scrolling the recycler view can hide both the toolbar and bottom navigation view. So far I have achieved one success i.e bottom navigation bottom bar does hide but with one caveat that it remains active even when the keyboard is on(how do I fix that too?)
My main concern here is how do I achieve the same feature of bottom navigation view of hiding into the toolbar?
I have included the custom toolbar in Appbar layout, but I have tried to add the Toolbar layout tag too in the AppBar nothing works, It just remains the same.
And for the bottomnavigation jumping up on top I don't know what to do? till now I have added snap scroll flags on the bottomnavigation view to stop this behaviour and also snap flag didn't work, I think so, coz it remains in halfway position while going up on search tap.
Got this BottomNavigationBehavior from the wonderful article.
reference
video showing behavior
image for snap behavior
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
>
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:elevation="0dp"
android:background="#android:color/transparent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
>
<include
app:layout_scrollFlags="scroll|enterAlways|snap"
layout="#layout/browser_search_tap_tb"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/browser_tb"
/>
</com.google.android.material.appbar.AppBarLayout>
<!--Scrolling effect for the bottom nav menu-->
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="#layout/rv_test_items"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:id="#+id/rv_test"
/>
<!--Bottom navigation view for the Selection of the Tabs and Items in Menu-->
<com.google.android.material.bottomnavigation.BottomNavigationView
app:layout_scrollFlags="scroll|enterAlways|snap"
android:id="#+id/browser_bottom_nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#ffff"
app:layout_behavior="com.example.android.browserui.BottomNavigationBehavior"
app:labelVisibilityMode="unlabeled"
app:menu="#menu/bottom_nav_menu"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
browser_search_tap_tb.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar
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="?actionBarSize"
android:background="#android:color/transparent"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="#style/ThemeOverlay.AppCompat"
app:contentInsetStart="8dp"
app:contentInsetEnd="8dp"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:layout_width="0dp"
android:layout_height="match_parent"
android:hint="Search or type new address"
android:padding="8dp"
android:paddingEnd="12dp"
android:paddingStart="12dp"
android:drawableEnd="#drawable/ic_mic"
android:inputType="textWebEditText"
android:background="#drawable/rounded_et_search"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" android:id="#+id/et_search_bar_tap"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar>
BottomNavigationBehavior.kt
class BottomNavigationBehavior<V : View>(context: Context, attrs: AttributeSet) :
CoordinatorLayout.Behavior<V>(context, attrs) {
private var lastStartedType: Int = 0
private var offsetAnimator: ValueAnimator? = null
var isSnappingEnabled = false
override fun layoutDependsOn(parent: CoordinatorLayout, child: V, dependency: View): Boolean {
if (dependency is Snackbar.SnackbarLayout) {
updateSnackbar(child, dependency)
}
return super.layoutDependsOn(parent, child, dependency)
}
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout, child: V, directTargetChild: View, target: View, axes: Int, type: Int
): Boolean {
if (axes != ViewCompat.SCROLL_AXIS_VERTICAL)
return false
lastStartedType = type
offsetAnimator?.cancel()
return true
}
override fun onNestedPreScroll(
coordinatorLayout: CoordinatorLayout, child: V, target: View, dx: Int, dy: Int, consumed: IntArray, type: Int
) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
child.translationY = max(0f, min(child.height.toFloat(), child.translationY + dy))
}
override fun onStopNestedScroll(coordinatorLayout: CoordinatorLayout, child: V, target: View, type: Int) {
if (!isSnappingEnabled)
return
// add snap behaviour
// Logic here borrowed from AppBarLayout onStopNestedScroll code
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
// find nearest seam
val currTranslation = child.translationY
val childHalfHeight = child.height * 0.5f
// translate down
if (currTranslation >= childHalfHeight) {
animateBarVisibility(child, isVisible = false)
}
// translate up
else {
animateBarVisibility(child, isVisible = true)
}
}
}
private fun animateBarVisibility(child: View, isVisible: Boolean) {
if (offsetAnimator == null) {
offsetAnimator = ValueAnimator().apply {
interpolator = DecelerateInterpolator()
duration = 150L
}
offsetAnimator?.addUpdateListener {
child.translationY = it.animatedValue as Float
}
} else {
offsetAnimator?.cancel()
}
val targetTranslation = if (isVisible) 0f else child.height.toFloat()
offsetAnimator?.setFloatValues(child.translationY, targetTranslation)
offsetAnimator?.start()
}
private fun updateSnackbar(child: View, snackbarLayout: Snackbar.SnackbarLayout) {
if (snackbarLayout.layoutParams is CoordinatorLayout.LayoutParams) {
val params = snackbarLayout.layoutParams as CoordinatorLayout.LayoutParams
params.anchorId = child.id
params.anchorGravity = Gravity.TOP
params.gravity = Gravity.TOP
snackbarLayout.layoutParams = params
}
}
}
So Finally Resolved My issue after getting my head around for 4 days here are some changes which I did and solved the issues :
activity_main.xml
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:elevation="0dp"
android:background="#android:color/transparent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"<!--Removed this line-->
>
<!--Bottom navigation view for the Selection of the Tabs and Items in Menu-->
<com.google.android.material.bottomnavigation.BottomNavigationView
app:layout_scrollFlags="scroll|enterAlways|snap"<!--Removed this line-->
android:id="#+id/browser_bottom_nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#ffff"
app:layout_behavior="com.example.android.browserui.BottomNavigationBehavior"
app:labelVisibilityMode="unlabeled"
app:menu="#menu/bottom_nav_menu"
/>
Here in activity_main.xml, I removed the layout_behavior because the appbar itself is invoking the scroll behavior for other layout items, it acts as a parent.
app:layout_behavior="#string/appbar_scrolling_view_behavior"
Also removed scrollflags from the bottom navigation view, AS I was implementing this behavior from the Class BottomNavigationBehavior.kt, You find the implementation below
app:layout_scrollFlags="scroll|enterAlways|snap"
browser_search_tap_tb.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar
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="?actionBarSize"
android:background="#android:color/transparent"
app:layout_scrollFlags="scroll|enterAlways" <!--Removed this line-->
app:popupTheme="#style/ThemeOverlay.AppCompat"
app:contentInsetStart="8dp"
app:contentInsetEnd="8dp"
>
Here in browser_search_tb, I removed the following line because it was overriding the Scrollflags in co-ordinator layout so removed it and it worked flawlessly
app:layout_scrollFlags="scroll|enterAlways"
BottomNavigationBehavior.kt
override fun onStopNestedScroll(coordinatorLayout: CoordinatorLayout, child: V, target: View, type: Int) {
if (!isSnappingEnabled)
return // removed this line
{
// add snap behaviour
// Logic here borrowed from AppBarLayout onStopNestedScroll code
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
// find nearest seam
val currTranslation = child.translationY
val childHalfHeight = child.height * 0.5f
// translate down
if (currTranslation >= childHalfHeight) {
animateBarVisibility(child, isVisible = false)
}
// translate up
else {
animateBarVisibility(child, isVisible = true)
}
}
}
}
Here I removed the
return
and added braces{} to the if statement and snap feature did work properly
Hope this answer will help you and will cut down your debugging time.
The above reference is one of the best article you can find for scrolling behavior on the internet it simple and smooth to grasp

RecyclerView in BottomSheet not working as expected

I have a problem with RecyclerView directly inside of layout with bottomsheetbehaviour. The problem is that when bottom sheet is expanded and content is scrolled down, when I go to scroll back up it causes Bottom Sheet to start collapsing, instead of RecyclerView first being scrolled back to top.
Here's a video to demonstrate the problem. As you can see the problem appears when I scroll down on expanded bottom sheet. It immediately start to collapse instead of "waiting" for RecyclerView to scroll to top first.
Here is my layout code
<?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:id="#+id/scheduleRoot"
android:layout_height="match_parent"
tools:context=".schedule.ScheduleFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/scheduleSheet"
app:behavior_peekHeight="300dp"
android:elevation="16dp"
android:clickable="false"
android:focusable="false"
android:background="#drawable/bg_bottom_sheet"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/scheduleRecyclerView"
android:clickable="true"
android:focusable="true"
android:layout_marginTop="8dp"/>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Any help is appreciated!
I just encountered same problem, but I fixed it by adding this to onCreate:
androidx.core.view.ViewCompat.setNestedScrollingEnabled(recyclerview, false);
Add
android:nestedScrollingEnabled="true"
in the root layout of BottomSheetDialogFragment.
I had similar issue: Maybe the solution to my problem will give you some ideas. My bottom sheet was expanded to full height with recycler view in it; the bottom sheet was collapsing on user-drag, even though the first item in recycler view wasn't visible yet.
So, what I did:
You can enable/disable bottom sheet dragging by "isDraggable" = true/false
Add OnScrollListener for recycler view.
Override onScrolled and check layoutManager.findFirstVisibleItemPosition() in it
If first item is visible - update bottom sheet behavior.isDraggable = true, i also added small delay before setting behavior.isDraggable = true, because bottom sheet was collapsing too fast, but you might not need it
Maybe it's not optimal but it was fitting my needs and maybe will help you.
Your recyclerview item has overighted the scrolling state, so this error generates. The layout you provided does not have enough data to determine the cause. You change the item is a unique view to check
I played with this for a long time and tried way too many solutions. For me, this worked best:
val layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.layoutManager = layoutManager
binding.recyclerView.setOnScrollChangeListener { _, _, _, _, _ ->
if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED) {
bottomSheetBehavior.isDraggable = layoutManager.findFirstCompletelyVisibleItemPosition() == 0
} else {
bottomSheetBehavior.isDraggable = true
}
}
The key to the solution is is controlling users ability drag the bottom sheet while the recyclerview is partially scrolled. The method only allows scrolling again once the top most cell is fully visible.
Its not ideal as the user may want to grab the very top of the bottom sheet (assuming its not part of the recycler view) and dismiss the bottom sheet regardless of its scroll position. Im just accepting.
Whatever you do, do not try these, as they just disable any recycling functionality and all cells are loaded at instantiation having a really bad impact on performance:
wrap_content
or:
binding.recyclerView.isNestedScrollingEnabled = false
Enable the scroll state of BottomSheet to allow scroll if recyclerview 0th item is visible.
activity_main.xml
<layout 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">
<data />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#A8A7A7"
tools:context=".MainActivity">
<LinearLayout
android:id="#+id/parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#fff"
android:orientation="vertical"
app:behavior_hideable="true"
app:behavior_peekHeight="80dp"
app:layout_behavior="com.asadmukhtar.recyclerviewinsidebottomsheet.LockableBottomSheetBehavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:gravity="center"
android:text="Drag Me"
android:textColor="#000"
android:textSize="20sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_items"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
LockableBottomSheet file that used for handling allow dragging option or not.
class LockableBottomSheetBehavior<V : View?> : BottomSheetBehavior<V> {
private var mAllowUserDragging = true
constructor()
constructor(context: Context, attrs: AttributeSet?) : super(
context,
attrs
)
fun setAllowUserDragging(allowUserDragging: Boolean) {
mAllowUserDragging = allowUserDragging
}
override fun onInterceptTouchEvent(
parent: CoordinatorLayout,
child: V,
event: MotionEvent
): Boolean {
return if (!mAllowUserDragging) {
false
} else super.onInterceptTouchEvent(parent, child, event)
}
}
MainActivity.java
var bottomSheetBehavior: LockableBottomSheetBehavior<*>? = null
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding =
DataBindingUtil.setContentView(this, R.layout.activity_main)
setUpBottomSheetBehaviour()
binding.rvItems.layoutManager = LinearLayoutManager(this)
binding.rvItems.adapter = RecyclerViewAdapter(this)
binding.rvItems.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
val firstPosition = (binding.rvItems.layoutManager as LinearLayoutManager)
.findFirstVisibleItemPosition()
updateBottomSheetLockState(firstPosition == 0)
}
})
}
fun updateBottomSheetLockState(allow: Boolean) {
bottomSheetBehavior?.setAllowUserDragging(allow)
}
fun updateBottomSheetState(state: Int) {
if (bottomSheetBehavior != null) {
bottomSheetBehavior?.state = state
}
}
private fun setUpBottomSheetBehaviour() {
val bottomSheetBehavior: BottomSheetBehavior<LinearLayout> =
BottomSheetBehavior.from(binding.parent)
this.bottomSheetBehavior = bottomSheetBehavior as LockableBottomSheetBehavior<*>
updateBottomSheetState(BottomSheetBehavior.STATE_COLLAPSED)
}
Your implementation might need more coding and with the provided code we might not able to give you good feedback.
Try this documentation
https://material.io/develop/android/components/bottom-sheet-behavior/
Plus I found this another implementation.
https://www.youtube.com/watch?v=WeaylHAwIIk

Categories

Resources