I've a single Activity with a BottomNavigationView inside of it's layout:
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:layout_behavior="#string/hide_bottom_view_on_scroll_behavior"
app:menu="#menu/menu_home_bottom_navigation"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
My bottom_avigation changes nav_host FragmentContainerView with fragments. All of this fragments have NestedScrollView or RecyclerView and because of app:layout_behavior="#string/hide_bottom_view_on_scroll_behavior", my bottom_navigation automatically hides/shows on scrollDown/scrollUp.
I saw this question: Hide/Show bottomNavigationView on Scroll
. I'm currently using the answer given by Abhishek Singh but the problem is not this.
This is my problem: Imagine FragA and FragB both have RecyclerViews but FragA has less items causing that all items fit to the screen and not scrollable. Now when I switch from FragA to FragB and then scrollDown, bottom_navigation hides with animation and if I press back button I cannot see bottom_navigation anymore and because FragA is not scrollable I cannot make it visible by scrolling.
I've also tried bottom_navigation.visibility = View.Visible in FragA onResume event, but still does not work. I think that it somehow translates bottom_navigation to the bottom and because of that this code does not help.
So how can I fix this issue?
Since there is no part from your code here my solution would be to listen on the back button:
maybe you can check this article it would be helpful
Android: onBackPressed() for Fragments
And there change the visibility for the BottomNavigationView.
I found the answer. instead of changing the visibility property of the bottom_navigation, I wrote two extension functions on BottomNavigationView for hiding/showing it:
private fun BottomNavigationView.showUp() {
animate().setDuration(200L).translationY(0f).withStartAction { visibility = View.VISIBLE }.start()
}
private fun BottomNavigationView.hideDown() {
animate().setDuration(200L).translationY(height.toFloat()).withEndAction { visibility = View.GONE }.start()
}
Now in onResume of FragA I have this:
override onResume() {
super.onResume()
bottom_navigation.showUp()
}
I made a custom view with a progress bar. To add it in all my fragments i made a base fragment. In the method onActivityCreated i added the following code:
activity?.run {
loading = LoadingView(this)
loading?.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
loading?.visibility = View.GONE
(view as? ViewGroup)?.addView(loading)
}
And it works, but, when i make it visible, the buttons overlapped the progres bar in my LoadingView. So, when i show it i added brintToFront() method on a first test (that didnt work) and i saw i also had to use invalidate
protected fun showLoading() {
loading?.visibility = View.VISIBLE
loading?.bringToFront()
loading?.invalidate()
}
As this wasnt working either i started looking for a solution here and i found that the solution could be adding translationZ or elevation properties. So i tried to, but none is working.
The XML file of my view is:
<androidx.constraintlayout.widget.ConstraintLayout
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">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
What am i doing wrong?
I've tried this and settings translationZ to 6f or more brings my view in front of buttons.
It seems that it should be set programmatically.
protected fun showLoading() {
loading?.translationZ = 6f
loading?.visibility = View.VISIBLE
}
https://material.io/design/components/backdrop.html
I found this on Material Design, but couldn't find any resources.
Thinking about its layout, I think it's made up of any layout with material card view, and I am trying to make my activity file using layout + material card view. Is this method correct to make backdrop layout?
Also, I want to know about which layout I should use. Is RelativeLayout can be the way? I don't get it actually.
This component (BackDrop) is still under development for the Android Material Components library as of 16 December 2018.
However, if you are using Material Components already, it's not that hard to implement your own. You will need the following:
CoordinatorLayout as the root layout
a BottomSheetBehaviour applied to an immediate child of the root layout
The provided solution below, looks like the following image...
The example below uses a fragment, I'll ommit the details of the hosting activity because it is irrelevant to the question/answer. However, you can do exactly the same with an activity. Your fragment layout file will look like below...
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="#+id/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">
<!--This the interface sitting behind the backdrop and shown when it is collapsed-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#color/colorPrimary"
android:padding="#dimen/activity_spacing">
<EditText
android:id="#+id/searchTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableStart="#drawable/ic_search_primary_xlight_24dp"
style="#style/EditTextStyle.Inverse.Large.Light"
android:hint="#string/search_hint"/>
<EditText
android:id="#+id/datesFilterButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableStart="#drawable/ic_calendar_primary_xlight_24dp"
style="#style/EditTextStyle.Inverse.Large.Light"
android:hint="#string/select_dates_hint"/>
</LinearLayout>
<!--This is the backdrop's content with a BottomSheetBehaviour applied to it-->
<LinearLayout
android:id="#+id/contentLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:behavior_peekHeight="56dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<!--This is the backdrop's header with a title and icon-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:clickable="true"
android:background="#drawable/ic_list_header_background"
android:padding="#dimen/activity_spacing"
android:elevation="4dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
style="#style/TextAppearance.Stems.Body2"
android:text="0 items(s)"/>
<ImageView
android:id="#+id/filterIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/ic_filter_black_24dp"
android:layout_gravity="end"/>
</LinearLayout>
<!--And finally this is the body of the backdrop's content. You can add here whatever you need inside a view group (LinearLayout, RelativeLayout, SwipeRefreshLayout, ConstraintLayout, etc.)-->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="#+id/swiperefresh"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/colorBackground">
<!--The content's body goes here-->
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
There's a couple of things you need to be aware of here. First, the LinearLayout that sits behind the backdrop its using the colorPrimary color which exactly the same as the Toolbar's background color...the toolbar has been ommitted for clarity, it is declared in the hosting activity (remember, this solution is for a fragment).
Then the fragment's class will look like this...
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
coordinatorLayout = (CoordinatorLayout)inflater.inflate(R.layout.fragment_hazards, container, false);
Context context = getContext();
if(context != null){
setTitle(context.getString(R.string.title_hazards));
}
filterIcon = coordinatorLayout.findViewById(R.id.filterIcon);
LinearLayout contentLayout = coordinatorLayout.findViewById(R.id.contentLayout);
sheetBehavior = BottomSheetBehavior.from(contentLayout);
sheetBehavior.setFitToContents(false);
sheetBehavior.setHideable(false);//prevents the boottom sheet from completely hiding off the screen
sheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);//initially state to fully expanded
filterIcon.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
toggleFilters();
}
});
return coordinatorLayout;
}
private void toggleFilters(){
if(sheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED){
sheetBehavior.setState(BottomSheetBehavior.STATE_HALF_EXPANDED);
}
else {
sheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
}
And that's it, the only thing you need to keep in mind is that root layout has to be a CoordinatorLayout and that the BottomSheetBehaviour has to be applied to an immediate child of the root layout
Round Corners
You will also notice that I'm not using a CardView in the BackDrop's header to get the nice rounded corners the CardView comes with. That's because I only need the top corners to be rounded and the default implementation of CardView doesn't allow you to explicitly set individual corners. Instead, I used a good old LinearLayout and provided my own drawable for its background (ic_list_header_background). Here's the xml declaration of this drawable...
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#color/colorBackground" />
<corners android:topLeftRadius="16dp" android:topRightRadius="16dp" />
</shape>
Nothing really fancy, just a rectangular shape with selective rounded corners (the top ones)
Toolbar's Drop Shadow
You will want to remove the ToolBar's drop shadow, to do so, you can set its elevation to 0dp or programmatically remove the outline provider on the parent AppBarLayout as below...
appBarLayout.setOutlineProvider(null);
obviously, this is assuming that your Toolbar is inside an AppBarLayout as per the guidelines
I hope this really helps someone out there while the Material Component's BackDrop is still under development. It's not perfect because you are still bound to the functionalities exposed by the BottomSheetBehaviour component that it's quite limited. But if you are picky or want to go fancy, I'd recommend implementing your own BottomSheetBehaviour by extending the default one
Disabling user's swipe gesture
Based on Material Design Guidelines, it is recommended not to use swipe gestures on the front layer of the backdrop
Don’t use the swipe gesture on the front layer to reveal the back layer.
However, by default, the BottomSheetBehaviour doesn't expose any properties or APIs to disable swipe gestures. To achieve that, you will need to implement your own by extending the BottomSheetBehaviour overriding all gesture-related methods. Here's an example I'm using in one of my projects (written in Kotlin)
class GestureLockedBottomSheetBehavior<V: View>(context: Context, attributeSet: AttributeSet?) : BottomSheetBehavior<V>(context, attributeSet){
constructor(context: Context):this(context, null)
override fun onInterceptTouchEvent(parent: CoordinatorLayout, child: V, event: MotionEvent): Boolean = false
override fun onTouchEvent(parent: CoordinatorLayout, child: V, event: MotionEvent): Boolean = false
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: V,
directTargetChild: View,
target: View,
axes: Int,
type: Int
): Boolean = false
override fun onNestedPreScroll(
coordinatorLayout: CoordinatorLayout,
child: V,
target: View,
dx: Int,
dy: Int,
consumed: IntArray,
type: Int
) { }
override fun onStopNestedScroll(coordinatorLayout: CoordinatorLayout, child: V, target: View, type: Int) { }
override fun onNestedFling(
coordinatorLayout: CoordinatorLayout,
child: V,
target: View,
velocityX: Float,
velocityY: Float,
consumed: Boolean
): Boolean = false
}
Even if you're not familiar with Kotlin it shouldn't be hard to figure out that all I'm doing is overriding a bunch on methods and return false or doing nothing by not calling the super class's counterpart
Then to use this GestureLockedBottomSheetBehavior class, you will need to replace it in your layout as below...
<LinearLayout
android:id="#+id/contentLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:behavior_peekHeight="56dp"
app:layout_behavior="ui.behaviors.GestureLockedBottomSheetBehavior">
...
</LinearLayout>
Just make sure the fully-qualified name is set according to the package your custom class resides in.
It is under development right now (Backdrop github page).
Code & how to.. would be available once it's developed. So, right now you have to create your own customized backdrop or wait for it.
I'll suggest if you want to do it, then take FrameLayout and add some CardView
in it with some margins to get look like backdrop, add some animations on transitions to it & your custom backdrop would be ready.
I'd like to add a small modification to Leo's answer:
I'm using a RecyclerView inside a LinearLayout. This LinearLayout is my bottomsheet. Using the GestureLockedBottomSheetBehavior as suggested by Leo does not allow the RecyclerView to scroll and the following is what I did to overcome that problem.
This is the custom bottomsheet behavior class that I used finally to make the RecyclerView scroll.
class GestureLockedBottomSheetBehavior<V: View>(context: Context, attributeSet: AttributeSet) : BottomSheetBehavior<V>(context, attributeSet){
override fun onInterceptTouchEvent(parent: CoordinatorLayout, child: V, event: MotionEvent): Boolean = false
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: V,
directTargetChild: View,
target: View,
axes: Int,
type: Int
): Boolean = false
}
With this my RecyclerView is scrolling. But it scrolls only when the state of the bottom sheet is STATE_EXPANDED and the RecyclerView does not scroll when the state of the bottom sheet is STATE_HALF_EXPANDED.
If anyone knows how to solve this, it would be very helpful.
Our implementation using Linear Layout and Bottom Sheet Behavior here:
https://github.com/keikenofficial/keiken-android/tree/master/app/src/main/java/com/keiken/view/backdrop
https://user-images.githubusercontent.com/11440565/64996052-26b0be00-d8dd-11e9-95f8-f643ec68e679.gif
https://user-images.githubusercontent.com/11440565/64996027-1567b180-d8dd-11e9-9f50-549479ff0480.gif
https://user-images.githubusercontent.com/11440565/64996019-100a6700-d8dd-11e9-8592-1c9d55b439ce.gif
https://user-images.githubusercontent.com/11440565/64996044-1d275600-d8dd-11e9-8fd1-8780859c74e1.gif)
Let's say that my Bottom Sheet has lines of widgets like the following. If I want to show only the first two lines (i.e., the first two LinearLayouts) initially, but not the rest of the widgets below. I do not want those to be seen initially. How can I set the correct peek height? Hard-coding app:behavior_peekHeight probably would not work, so I would need to set it programatically, but how to calculate the height?
Or is there a more recommended way to get the same result? I mean, if I test Google Maps, long pressing a location first shows only the title part as the bottom sheet, but when I try to scroll up the bottom sheet, it feels as if the title part (which might not have been a real bottom sheet) is replaced by a real bottom sheet that contains all the elements. If my explanation is not enough, please try Google Maps yourself.
<android.support.v4.widget.NestedScrollView
android:id="#+id/bottom_sheet"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView/>
<android.support.v7.widget.AppCompatSpinner/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView/>
<TextView/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView/>
<TextView/>
</LinearLayout>
<android.support.v7.widget.RecyclerView/>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
I would solve this by using a ViewTreeObserver.OnGlobalLayoutListener to wait for your bottom sheet to be laid out, and then calling BottomSheetBehavior.setPeekHeight() with the y-coordinate of the first view you don't want to see.
private BottomSheetBehavior<View> behavior;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View bottomSheet = findViewById(R.id.bottomSheet);
behavior = BottomSheetBehavior.from(bottomSheet);
final LinearLayout inner = findViewById(R.id.inner);
inner.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
inner.getViewTreeObserver().removeOnGlobalLayoutListener(this);
View hidden = inner.getChildAt(2);
behavior.setPeekHeight(hidden.getTop());
}
});
}
In this case, my bottom sheet is a NestedScrollView holding a LinearLayout that holds many TextViews. By setting the peek height to be the top of the third TextView (obtained by getChildAt(2)), my bottom sheet winds up showing exactly two TextViews while collapsed.
Customized #Ben P.'s answer to target a view id as a reference of the peekHeight and made a function:
/**
* Gets the bottom part of the target view and sets it as the peek height of the specified #{BottomSheetBehavior}
*
* #param layout - layout of the bottom sheet.
* #param targetViewId - id of the target view. Must be a view inside the 'layout' param.
* #param behavior - bottom sheet behavior recipient.
*/
private fun <T : ViewGroup> getViewBottomHeight(layout: ViewGroup,
targetViewId: Int,
behavior: BottomSheetBehavior<T>) {
layout.apply {
viewTreeObserver.addOnGlobalLayoutListener(
object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
behavior.peekHeight = findViewById<View>(targetViewId).bottom
}
})
}
}
In our use case, we needed to target the bottom part of the view, so we set it that way. It can be adjusted depending on the use-case.
That's smart!
My problem was trying to getTop() or getHeight() at wrong timing, it returns 0 if the view is not ready.
And yes, use viewTreeObserver to avoid that.
This is actually no different with #Ben P.'s previous answer, just a kotlin version:
class MyBottomSheetDialog() : BottomSheetDialogFragment(){
private val binding by viewBinding(SomeLayoutViewBinding::bind)
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(this.dialog as BottomSheetDialog).behavior.let { behavior ->
/* Set "pivotView" as interested target and make it the pivot of peek */
binding.pivotView.viewTreeObserver.addOnGlobalLayoutListener(object :
OnGlobalLayoutListener {
override fun onGlobalLayout() {
binding.pivotView.viewTreeObserver.removeOnGlobalLayoutListener(this)
behavior.peekHeight = binding.pivotView.top
}
})
}
}
}
I'm trying to make a bottomsheet using google support library. The goal is to have a sheet that:
Can be hidden programmatically only
Its height is calculated automatically
Is defined statically in xml
So far so good, simple stuff. There is also this promising isHideable() which defaults to false.
But the bottomsheet seems to ignore the isHideable when the sheet is set to STATE_EXPANDED (although its not going to cover the whole screen). The only way to make it unhideable is to set a peek height (which I don't want). Is there a way to have it expanded and not-hideable without setting the height manually (or via layout change triggers)
Here is the (super slim) code used:
Activity.java
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View bottomSheet = findViewById(R.id.bottomsheet);
BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
behavior.setHideable(false);
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.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:fitsSystemWindows="true">
<LinearLayout
android:id="#+id/bottomsheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:behavior_hideable="false"
app:layout_behavior="#string/bottom_sheet_behavior"
android:background="#android:color/white">
<TextView
android:layout_width="match_parent"
android:layout_height="300dp"
android:text=":) :) :)"/>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
Behavior
The simplest but hackish way I've found so far:
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
bottomSheet.post(new Runnable() {
#Override
public void run() {
behavior.setPeekHeight(bottomSheet.getHeight());
}
});
And of course when there is need to hide it firstly call setHideable(true).
This is just a workaround that might lead to weird behavior.