Last Updated
Viewed 04 Times

I'm learning about Motion Layout, wondering whether it is possible with it to implement expandable list (RecyclerView would be the best). The problem is that you can't create motion constarint with root-element changing (in my case root is MotionLayout, as single element of list), for incresing it's height after clicking it. Is it even possible with MotionLayout?

I am new to motionlayout and been following various tutorials online like this to get an understanding of how it works. From a nutshell I have come to know it basically animates constraintSets, you have a start and end constraintSet which you can further customize with KeyFrameSets. I have this layout

enter image description here

I want to mimic Lyft's bottom sheet

enter image description here

With my layout the Where are you going button is suppose to slowly fade out as the search destination textInputs fade in. The recyclerview at the bottom is suppose to hold saved addresses, it will not be affected. I tried this implementation using a standard bottomsheet but had challenges with the animation, it had this weird flickering so I decided to use a MotionLayout with a normal view.

My bottomsheet layout is as follows

<com.google.android.material.card.MaterialCardView 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/cardChooseAddressBottomSheet"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clickable="true"
    app:shapeAppearance="@style/ShapeAppearanceRoundedLargeTopCorners">



    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/bottomSheetConstraintLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/activity_horizontal_margin"
        android:layout_marginRight="@dimen/activity_horizontal_margin"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">



        <ImageView
            android:id="@+id/swipeUpHandle"
            android:layout_width="50dp"
            android:layout_height="30dp"
            android:layout_gravity="center"
            android:background="@drawable/ic_swipe_up_handle"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />



        <com.google.android.material.textview.MaterialTextView
            android:id="@+id/hiThere"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/medium_margin"
            android:text="@string/hi_there"
            android:textAppearance="@style/h6_headline"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/swipeUpHandle"
            />


        <com.google.android.material.button.MaterialButton
            android:id="@+id/btnSearch"
            style="@style/Widget.MaterialComponents.Button.OutlinedButton"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:layout_marginTop="@dimen/medium_margin"
            android:gravity="start|center_vertical"
            android:letterSpacing="0.0"
            android:text="@string/where_are_you_going"
            android:textAllCaps="false"
            android:textAppearance="@style/subtitle1"
            android:textColor="@android:color/darker_gray"
            app:backgroundTint="@android:color/white"
            app:icon="@drawable/ic_search"
            app:iconTint="@android:color/black"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/hiThere"
            app:shapeAppearanceOverlay="@style/ShapeAppearanceRoundedMediumAllCorners" />



        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:id="@+id/addressViews"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/btnSearch">



            <com.google.android.material.textfield.TextInputLayout
                android:id="@+id/inputOrigin"
                style="@style/textInput"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/medium_margin"
                android:hint="@string/search_destination"
                android:textColorHint="@android:color/darker_gray"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent">

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/edtOrigin"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:inputType="text"
                    android:textAppearance="@style/subtitle1"
                    android:textColor="@android:color/white" />

            </com.google.android.material.textfield.TextInputLayout>

            <com.google.android.material.textfield.TextInputLayout
                android:id="@+id/inputDestination"
                style="@style/textInput"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/medium_margin"
                android:hint="@string/search_destination"
                android:textColorHint="@android:color/darker_gray"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/inputOrigin">

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/edtDestination"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:inputType="text"
                    android:textAppearance="@style/subtitle1"
                    android:textColor="@android:color/white" />

            </com.google.android.material.textfield.TextInputLayout>


        </androidx.constraintlayout.widget.ConstraintLayout>

        <androidx.recyclerview.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:id="@+id/recyclerAddresses"
            android:layout_marginTop="@dimen/medium_margin"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/addressViews"
            tools:listitem="@layout/recycler_view_item" />
    </androidx.constraintlayout.widget.ConstraintLayout>

</com.google.android.material.card.MaterialCardView>

And my parent layout where I include the bottomsheet is as follows

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout 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/motionLayout"
    app:layoutDescription="@xml/taxi_bottomsheet_scene"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <fragment
        android:id="@+id/map"
        android:name="com.google.android.gms.maps.SupportMapFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />



    <include
        layout="@layout/choose_destination_bottom_sheet_layout"/>

</androidx.constraintlayout.motion.widget.MotionLayout>

And finally my taxi_bottomsheet_scene motion scene is

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">



    <Transition
        app:constraintSetEnd="@+id/expanded"
        app:constraintSetStart="@+id/collapsed"
        app:duration="1000">

        <OnSwipe
            app:touchAnchorId="@+id/btnSearch"
            app:touchAnchorSide="top"
            app:dragDirection="dragUp"/>


    </Transition>


    <ConstraintSet android:id="@+id/expanded">

        <Constraint
            android:id="@+id/cardChooseAddressBottomSheet"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHeight_percent="1"
            app:layout_constraintHorizontal_bias="1.0"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0" />



        <Constraint
            android:id="@+id/addressViews"
            app:layout_constraintHeight_percent="1"/>


        <Constraint
            android:id="@+id/btnSearch"
            app:layout_constraintHeight_percent="0"/>

    </ConstraintSet>




    <ConstraintSet android:id="@+id/collapsed">

        <Constraint
            android:id="@+id/cardChooseAddressBottomSheet"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHeight_percent="0.4"
            app:layout_constraintHorizontal_bias="1.0"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="1.0"  />


        <Constraint
            android:id="@+id/addressViews"
            app:layout_constraintHeight_percent="0.0"/>



        <Constraint
            android:id="@+id/btnSearch"
            app:layout_constraintHeight_percent="0.0"/>
    </ConstraintSet>





</MotionScene>

When I launch this app I cannot get the bottomsheet to slide up, it simply does not respond in any way. One thing I noticed though is after adding the app:layoutDescription="@xml/taxi_bottomsheet_scene"attribute, the bottom sheet size changed to what I had specified in the constraintSetStart but the addressViews view did not.

So my layout looks like

parent layout after attaching it to the motion scene

So my question is, where I am going wrong for my bottomsheet not to respond to my swipes and addressViews to disappear in the initial state?

I'm trying to reproduce coordinator layout collapsing toolbar animation with motion layout.

The transition work very well when I scroll from recyclerview (the touchAnchorId is set to recycler) but when I scroll from the image the animation is not fluid.

I attach the collapsed and expanded layout and the collapsing scene.

Thanks in advance for the help

Expanded:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:descendantFocusability="blocksDescendants"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layoutDescription="@xml/collapsing_scene">

    <ImageView android:layout_width="match_parent"
               android:layout_height="match_parent"
               android:scaleType="centerCrop"
               android:id="@+id/imageview"
               android:contentDescription="@null"
               app:layout_constraintTop_toTopOf="parent"
               app:layout_constraintEnd_toEndOf="parent"
               app:layout_constraintBottom_toBottomOf="parent"
               app:layout_constraintStart_toStartOf="parent"
               app:srcCompat="@drawable/android"/>

    <androidx.recyclerview.widget.RecyclerView
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:id="@+id/recycler"
            android:background="#F44336"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/guideline"/>

    <androidx.constraintlayout.widget.Guideline
            android:orientation="horizontal"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/guideline"
            app:layout_constraintGuide_percent="0.5"/>

</androidx.constraintlayout.motion.widget.MotionLayout>

Collapsed:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:descendantFocusability="blocksDescendants"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <ImageView android:layout_width="0dp"
               android:layout_height="0dp"
               android:scaleType="centerCrop"
               android:id="@+id/imageview"
               android:contentDescription="@null"
               app:layout_constraintTop_toTopOf="parent"
               app:layout_constraintEnd_toEndOf="parent"
               app:layout_constraintBottom_toTopOf="parent"
               app:layout_constraintStart_toStartOf="parent"
               app:srcCompat="@drawable/android"/>

    <androidx.recyclerview.widget.RecyclerView
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:id="@+id/recycler"
            android:background="#F44336"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/imageview"/>

    <androidx.constraintlayout.widget.Guideline
            android:orientation="horizontal"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/guideline"
            app:layout_constraintGuide_percent="0.5"/>

</androidx.constraintlayout.motion.widget.MotionLayout>

Transition:

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:app="http://schemas.android.com/apk/res-auto">

    <Transition
            app:constraintSetEnd="@layout/collapsed"
            app:constraintSetStart="@layout/expanded"
            app:duration="500">

        <OnSwipe
                app:dragDirection="dragUp"
                app:touchAnchorId="@id/recycler"
                app:touchAnchorSide="top"
                app:onTouchUp="stop"/>
    </Transition>

</MotionScene>

Animation

I am using alpha4 implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha4' of constraint layout. First I created a single layout and do onswipe animation it is working perfectly with motion layout then I patched the layout with a recyclerview. Now problem comes into picture Issue:

  1. If I am doing onswipe in a particular item of rv then it's getting lagged.
  2. Suppose I am doing swipe on the 1st item of the rv now after full swipe you will find that same swiped has already happen with 13th no item, 25th no item and so on. So after every 12 item same swipe you can check while you are scrolling.

I have posted the whole project in Github and also created a sample video.

_For quick gist of motion layout here

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout
    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="wrap_content"
    app:layoutDescription="@xml/scene_btn"
    app:showPaths="true"
    >

    <androidx.cardview.widget.CardView
        android:id="@+id/iv_recorded_program"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        app:cardElevation="1dp"
        app:cardCornerRadius="0dp"
        app:layout_constraintEnd_toStartOf="@+id/rl_recorded_program_info"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <ImageView
            style="@style/styleRecordedProgramImage"
            android:layout_width="@dimen/guide_list_width"
            android:layout_height="@dimen/guide_list_height"
            android:visibility="visible"
            />
    </androidx.cardview.widget.CardView>

    <include
        android:id="@+id/rl_recorded_program_info"
        layout="@layout/text"
        android:layout_width="0dp"
        android:layout_height="@dimen/guide_list_height"
        android:background="@color/colorPrimary"
        android:visibility="visible"
        app:layout_constraintEnd_toStartOf="@+id/button"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/iv_recorded_program"
        app:layout_constraintTop_toTopOf="parent"/>

    <ImageView
        android:id="@+id/button"
        android:layout_width="0dp"
        android:layout_height="@dimen/guide_list_height"
        android:background="@color/colorAccent"
        android:visibility="visible"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>


</androidx.constraintlayout.motion.widget.MotionLayout>

And here is the layout description

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
             xmlns:app="http://schemas.android.com/apk/res-auto"
    >

    <Transition
        app:constraintSetEnd="@+id/end"
        app:constraintSetStart="@+id/start"
        app:duration="1000">
        <OnSwipe
            app:dragDirection="dragRight"
            app:touchAnchorId="@id/rl_recorded_program_info"
            app:touchAnchorSide="right"/>
    </Transition>


    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@id/iv_recorded_program"
            android:layout_width="@dimen/guide_list_width"
            android:layout_height="@dimen/guide_list_height"
            android:elevation="10dp"
            android:visibility="visible"
            app:layout_constraintEnd_toStartOf="@+id/rl_recorded_program_info"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>

        <Constraint
            android:id="@id/rl_recorded_program_info"
            android:layout_width="0dp"
            android:layout_height="@dimen/guide_list_height"
            android:visibility="visible"
            app:layout_constraintEnd_toStartOf="@+id/button"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/iv_recorded_program"
            app:layout_constraintTop_toTopOf="parent"/>


        <Constraint
            android:id="@id/button"
            android:layout_width="0dp"
            android:layout_height="@dimen/guide_list_height"
            android:visibility="visible"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>

    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@id/iv_recorded_program"
            android:layout_width="@dimen/guide_list_width"
            android:layout_height="@dimen/guide_list_height"
            android:elevation="10dp"
            android:visibility="visible"
            app:layout_constraintEnd_toStartOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>

        <Constraint
            android:id="@id/rl_recorded_program_info"
            android:layout_width="0dp"
            android:layout_height="@dimen/guide_list_height"
            app:layout_constraintEnd_toStartOf="@+id/button"
            app:layout_constraintStart_toStartOf="@+id/iv_recorded_program"
            app:layout_constraintTop_toTopOf="parent"/>

        <Constraint
            android:id="@id/button"
            android:layout_width="0dp"
            android:layout_height="@dimen/guide_list_height"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@+id/iv_recorded_program"
            app:layout_constraintTop_toTopOf="parent"/>


    </ConstraintSet>

</MotionScene>

Similar Question 9 (1 solutions) : Orchestrating Android Motion Layout animations

cc