Search code examples
androidmaterial-designandroid-coordinatorlayoutandroid-appbarlayoutandroid-nestedscrollview

Android CollapsingToolbar/ AppBarLayout scroll behind status bar behaviour top padding


I'd like to use CoordinatoryLayout, AppBarLayout and CollapsingToolbarLayout to create a layout that resembles the below example from Google Calendar.

Desired Google Calendar scroll behaviour

The key things I'm trying to replicate:

  • Scrolling the content behind the status bar
  • Rounded corners at the top of the scroll container
  • Enough room at the top of the screen for the header to not look squashed

The Question Google Calendar appears to be growing the scroll container as the user scrolls. How would I go about doing this or something similar to achieve the look I'm after?

I've put together a quick example of what I'm trying to build:

activity_scrolling.xml

<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"
    android:animateLayoutChanges="true"
    tools:context="uk.co.exampleapplication.ScrollingActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <include
                android:id="@+id/lay_header"
                layout="@layout/layout_header" />

        </com.google.android.material.appbar.CollapsingToolbarLayout>

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@drawable/scroll_header_background"
            android:elevation="16dp"
            android:paddingBottom="12dp">

            <TextView
                android:id="@+id/sectionTitleText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="16dp"
                android:layout_marginTop="32dp"
                android:text="Title"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <Button
                android:id="@+id/filter_button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="22dp"
                android:layout_marginEnd="16dp"
                android:text="Button"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:context=".ScrollingActivity"
        tools:showIn="@layout/activity_scrolling">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/text_margin"
            android:text="@string/large_text" />

    </androidx.core.widget.NestedScrollView>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

layout_header.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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/header"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:paddingHorizontal="16dp"
    android:paddingTop="60dp"
    android:paddingBottom="40dp"
    app:layout_collapseMode="parallax"
    tools:ignore="HardcodedText">

    <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="24dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="16dp"
        android:text="Title"
        android:textColor="#FFF"
        android:textSize="20sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/subtitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Subtitle"
        android:textColor="#FFF"
        android:textSize="16sp"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/title" />

</androidx.constraintlayout.widget.ConstraintLayout>

scroll_header_background.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners
        android:bottomLeftRadius="0dp"
        android:bottomRightRadius="0dp"
        android:topLeftRadius="20dp"
        android:topRightRadius="20dp" />
    <solid android:color="#FFFFFF" />
    <size
        android:width="64dp"
        android:height="64dp" />
</shape>

My attempt is included below. The header scrolls behind the toolbar as desired but I'd like some additional top padding above my views (about the height of the top inset/ status bar would be sufficient). Google Calendar appears to be solving this by having the container grow as the user scrolls.

My attempt at scrollingScreenshot


Solution

  • Implement an AppBarLayout.OnOffsetChangedListener that will adjust top padding on the ConstraintLayout that holds the TextView and the Button. (Call this view "viewToGrow".) You can also do other things in the listener like change the corner radius of the drawable as the appbar scrolls.

    The following example adjusts top padding to give the header more room. This padding is increased as the appbar scrolls up and decreases as it scrolls down. The demo app also removes the corner radius of the drawable during the final 15% of the appbar scroll.

    ScrollingActivity

    class ScrollingActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_scrolling)
    
            val viewToGrow: View = findViewById(R.id.viewToGrow)
            val baseTopPadding = viewToGrow.paddingTop
    
            // Determine how much top padding has to grow while the app bar scrolls.
            var maxDeltaPadding = 0
            val contentView = findViewById<View>(android.R.id.content)
            ViewCompat.setOnApplyWindowInsetsListener(contentView) { _, insets ->
                maxDeltaPadding = insets.systemWindowInsetTop
                insets
            }
    
            // Get key metrics for corner radius shrikage.
            var backgroundRadii: FloatArray? = null
            var maxRadius: FloatArray? = null
            val backgroundDrawable = (viewToGrow.background as GradientDrawable?)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && backgroundDrawable != null) {
                backgroundRadii = backgroundDrawable.cornerRadii
                maxRadius = floatArrayOf(backgroundRadii!![0], backgroundRadii[1])
            }
    
            // Set up the app bar and the offset change listener.
            val appBar: AppBarLayout = findViewById(R.id.app_bar_layout)
            val appBarTotalScrollRange: Float by lazy {
                appBar.totalScrollRange.toFloat()
            }
            appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
                // Add/remove padding gradually as the appbar scrolls.
                val percentOfScrollRange = (-verticalOffset / appBarTotalScrollRange)
                val deltaPadding = maxDeltaPadding * percentOfScrollRange
                val newTopPadding = baseTopPadding + deltaPadding.toInt()
                if (newTopPadding != viewToGrow.paddingTop) {
                    viewToGrow.setPadding(
                        viewToGrow.paddingLeft,
                        newTopPadding,
                        viewToGrow.paddingRight,
                        viewToGrow.paddingBottom
                    )
                    // Change the drawable radius as the appbar scrolls.
                    if (backgroundRadii != null && maxRadius != null) {
                        val radiusShrinkage = if (percentOfScrollRange > (1.0f - CORNER_SHRINK_RANGE)) {
                            (1.0f - percentOfScrollRange) / CORNER_SHRINK_RANGE
                        } else {
                            1.0f
                        }
                        backgroundRadii[0] = maxRadius[0] * radiusShrinkage
                        backgroundRadii[1] = maxRadius[1] * radiusShrinkage
                        backgroundRadii[2] = maxRadius[0] * radiusShrinkage
                        backgroundRadii[3] = maxRadius[1] * radiusShrinkage
                        backgroundDrawable!!.cornerRadii = backgroundRadii
                    }
                }
            })
        }
    
        companion object {
            const val CORNER_SHRINK_RANGE = 0.15f
        }
    }
    

    enter image description here