Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Static Footer View On Android Bottom Sheet

I am trying to make a static footer on a bottomsheet on Android. Below is a GIF of my current attempt. Notice the footer text at the bottom of the sheet when it is fully expanded. I want this footer text to always show regardless of what state the sheet is in. (e.g. if the sheet is only half expanded, the footer should still show. As the sheet is being expanded/collapsed, the footer should still always show etc.) How can I accomplish this behavior? I have had a look at this: https://github.com/umano/AndroidSlidingUpPanel but I would prefer to get a vanilla android solution that does not involve any external libraries.

enter image description here

Here is my current XML. I think the problem is the footer is anchored to the bottom of the view, but the bottom of the view is only visible if the sheet is fully expanded. I want to see if I can get the footer to show regardless of the state of the sheet:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:airbnb="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:focusableInTouchMode="true"
        android:orientation="vertical"
        android:id="@+id/linear_layout_container">

        <TextView
            android:id="@+id/header_text"
            android:layout_width="match_parent"
            android:text="Header"
            android:layout_height="36dp"
            android:layout_alignParentTop="true"
            />

        <com.airbnb.epoxy.EpoxyRecyclerView
            android:id="@+id/bottom_sheet_recycler_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            airbnb:layoutManager="LinearLayoutManager"
            android:layout_above="@+id/footer_text"
            android:layout_below="@+id/header_text"
            />
        <TextView
            android:id="@+id/footer_text"
            android:layout_width="match_parent"
            android:text="Footer"
            android:layout_height="36dp"
            android:layout_alignParentBottom="true"
            airbnb:layout_anchorGravity="bottom"
            />
    </RelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

Here is what my fragment looks like. It is just a blank fragment that shows a bottom sheet: ContainerFragment.kt


import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.epoxy.EpoxyRecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog

class ContainerFragment : Fragment() {


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.container_fragment_layout, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        view.alpha = 0f
        val bottomSheet = BottomSheetDialog(requireContext())
        val bottomSheetView = LayoutInflater.from(context).inflate(R.layout.test_bottom_sheet_layout, null)
        bottomSheet.setContentView(bottomSheetView)
        bottomSheet.show()
        val epoxyRecyclerView : EpoxyRecyclerView = bottomSheetView.findViewById(R.id.bottom_sheet_recycler_view)
        epoxyRecyclerView.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
        epoxyRecyclerView.withModels {
            (0..100).forEach {
                basicRow {
                    id(it)
                    title("This is the entry at index $it.")
                }
            }
        }
    }
    companion object {
        fun newInstance() = ContainerFragment()
    }
}

Here is my fragment layout: container_fragment_layout.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

</LinearLayout>
like image 432
Ryan Avatar asked Oct 08 '19 18:10

Ryan


2 Answers

You need to do it programmatically but it's really simpler than it looks. Simply adjust the position of the View as the BottomSheet slides.

  1. Add the footer View as a direct child of your BottomSheet
<androidx.coordinatorlayout.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/bottom_sheet"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#EECCCCCC"
            app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">

        <TextView
            android:id="@+id/pinned_bottom"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#500000FF"
            android:padding="16dp"
            android:text="Bottom" />
    </FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
  1. Add a BottomSheetCallback and adjust the translationY in onSlide()
BottomSheetBehavior.from(bottom_sheet).addBottomSheetCallback(
    object : BottomSheetBehavior.BottomSheetCallback() {

        override fun onSlide(bottomSheet: View, slideOffset: Float) {
            val bottomSheetVisibleHeight = bottomSheet.height - bottomSheet.top

            pinned_bottom.translationY =
                (bottomSheetVisibleHeight - pinned_bottom.height).toFloat()
        }

        override fun onStateChanged(bottomSheet: View, newState: Int) {
        }
    })

It runs smoothly because you're simply changing the translationY.

You can also use this technique to pin a View in the center of the BottomSheet:

pinned_center.translationY = (bottomSheetVisibleHeight - pinned_center.height) / 2f

I've made a sample project on GitHub to reproduce both use cases (pinned to center and bottom): https://github.com/dadouf/BottomSheetGravity

Demo

like image 196
David Ferrand Avatar answered Nov 02 '22 01:11

David Ferrand


@DavidFerrand based using insead BottomSheetDialogFragment

val behavior: BottomSheetBehavior<*> = (dialog as BottomSheetDialog).behavior
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
    override fun onStateChanged(bottomSheet: View, newState: Int) {}
    override fun onSlide(bottomSheet: View, slideOffset: Float) {
        binding.bottom.y = ((bottomSheet.parent as View).height - bottomSheet.top - binding.bottom.height).toFloat()
    }
}.apply {
    binding.root.post { onSlide(binding.root.parent as View, 0f) }
})
like image 23
Xan Avatar answered Nov 02 '22 00:11

Xan