androidkotlinfragmentandroid-collapsingtoolbarlayoutandroid-transitions

Glitch with CollapsingToolbarLayout when popping up a fragment


I have a fragment A with a CollapsingToolbarLayout containing a view that collapses when scrolling the page.

At the end of the page, a button navigates to another fragment B.

When I press back on fragment B, fragment B is removed from the navigation stack and fragment A is shown again from the previous position (scroll at the bottom).

Right now everything is ok, but the toolbar of fragment A is initially visible and then collapses during the transition, even if it should not be visible.

Glitch

This is the layout of Fragment A:

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

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

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            style="?attr/collapsingToolbarLayoutLargeStyle"
            android:layout_width="match_parent"
            android:layout_height="280dp"
            android:fitsSystemWindows="true"
            app:contentScrim="?android:colorBackground"
            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
            app:scrimVisibleHeightTrigger="70dp"
            app:statusBarScrim="?android:colorBackground">

            <View
                android:layout_width="match_parent"
                android:layout_height="300dp"
                android:background="#ff0000"
                android:fitsSystemWindows="true" />

            <com.google.android.material.appbar.MaterialToolbar
                android:id="@+id/topAppBar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:elevation="0dp"
                android:fitsSystemWindows="false"
                app:layout_collapseMode="pin"
                app:layout_scrollFlags="scroll|enterAlways"
                app:navigationIcon="@drawable/chevron_back"
                app:navigationIconTint="#00ff00" />
        </com.google.android.material.appbar.CollapsingToolbarLayout>

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

    <androidx.core.widget.NestedScrollView
        android:id="@+id/scrollView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:clipToPadding="false"
        android:fillViewport="true"
        android:orientation="vertical"
        android:paddingHorizontal="@dimen/content_padding"
        android:paddingTop="0dp"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <TextView
                android:id="@+id/short_description"
                style="@style/body_gray"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginVertical="8dp"
                android:text="@string/lorem_ipsum" />
            <Button
                android:id="@+id/next"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginVertical="8dp"
                android:text="Next" />

        </LinearLayout>
    </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

And this is its code:

class FragmentA: Fragment() {
    private var _binding: FragmentABinding? = null

    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View {

        _binding = FragmentABinding.inflate(inflater, container, false)

        binding.topAppBar.setNavigationOnClickListener {
            findNavController().popBackStack()
        }

        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.next.setOnClickListener {
            findNavController().navigate(R.id.action_A_to_B)
            )
        }
    }
}

This is the action used for the navigation:

<action
            android:id="@+id/action_A_to_B"
            app:destination="@id/fragmentb"
            app:enterAnim="@anim/slide_in"
            app:exitAnim="@anim/slide_out"
            app:popEnterAnim="@anim/pop_in"
            app:popExitAnim="@anim/pop_out"
            app:launchSingleTop="true"/>

This is the video of the glitch: https://youtube.com/shorts/0UKtAwOSZQI

This is a minimal project that replicates the problem: https://mega.nz/file/lq5AEY6T#A-PxuVcWGDLSSktGHLwmtfMBgfUYu659EF7LbP770S8


Solution

  • The problem causing that glitch is that the scrim animation of the CollapsingToolbarLayout is enabled by default, so it takes time to fade out the CollapsingToolbarLayout color.

    This can be solved by disabling the scrim animation by setting its duration to 0 using app:scrimAnimationDuration attribute:

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        ....>
    
        <com.google.android.material.appbar.AppBarLayout
            ....>
    
            <com.google.android.material.appbar.CollapsingToolbarLayout
                android:id="@+id/collapsing_toolbar"
                app:scrimAnimationDuration="0">
                
    ...
    

    Edit

    Well, although disabling the scrim animation will solve the issue, but it will raise another whenever the CollapsingToolbarLayout get collapsed or expanded by dragging it up or down; so we need to conditionally disable the scrim animation.

    Now remove the app:scrimAnimationDuration="0" from the layout.

    We need to keep the scrim animation ON by default, but only when we returned back from Fragment B to Fragment A we need to disable it and re-enable it again:

    // In Fragment A
    override fun onResume() {
        super.onResume()
    
        // Make sure to save that on a permanent storage like ViewModel to avoid losing it.
        val duration = binding.collapsingToolbar.scrimAnimationDuration 
       
        // Disable the animation
        binding.collapsingToolbar.scrimAnimationDuration = 0
    
        Handler(Looper.getMainLooper()).postDelayed({
            // Re-Enable the animation
            binding.collapsingToolbar.scrimAnimationDuration = duration
        }, duration)
    }