Search code examples
androidandroid-constraintlayoutandroid-transitionsandroid-motionlayout

Android motion layout not working accordingly


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?


Solution

  • I finally managed to make it work, both with MotionLayout and CoordinatorLayout. I will only post the Coordinator solution as it is long and I do not have the time, if someone needs it, comment and I will post.

    I created 3 layouts, 1. The main layout with the map, 2. Top bar with the to and from address EditTexts and 3. The bottom layout that slides up and reveals the top bar.

    Solution 1 using CoordinatorLayout

    1. The topbar

      <androidx.constraintlayout.widget.ConstraintLayout
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:focusable="true"
          android:layout_marginBottom="@dimen/medium_margin"
          app:layout_scrollFlags="noScroll">
      
      
          <com.google.android.material.appbar.MaterialToolbar
              android:id="@+id/topToolBar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              app:layout_constraintEnd_toEndOf="parent"
              app:layout_constraintStart_toStartOf="parent"
              app:layout_constraintTop_toTopOf="parent"
              app:navigationIcon="@drawable/ic_arrow_back_black_24dp" />
      
      
          <androidx.constraintlayout.widget.ConstraintLayout
              android:id="@+id/addressViews"
              android:layout_width="match_parent"
              android:layout_height="0dp"
              android:layout_marginStart="@dimen/activity_horizontal_margin"
              android:layout_marginEnd="@dimen/activity_horizontal_margin"
              app:layout_constraintEnd_toEndOf="parent"
              app:layout_constraintStart_toStartOf="parent"
              app:layout_constraintTop_toBottomOf="@+id/topToolBar">
      
      
              <androidx.appcompat.widget.AppCompatImageView
                  android:id="@+id/imgOrigin"
                  android:layout_width="@dimen/activity_horizontal_margin"
                  android:layout_height="@dimen/activity_horizontal_margin"
                  android:layout_marginStart="@dimen/medium_margin"
                  android:layout_marginEnd="@dimen/medium_margin"
                  app:layout_constraintBottom_toBottomOf="@+id/inputOrigin"
                  app:layout_constraintEnd_toStartOf="@+id/inputOrigin"
                  app:layout_constraintStart_toStartOf="parent"
                  app:layout_constraintTop_toTopOf="@+id/inputOrigin"
                  app:srcCompat="@drawable/ic_origin"
                  app:tint="@color/colorAccent" />
      
      
              <View
                  android:layout_width="2dp"
                  android:layout_height="0dp"
                  android:layout_marginTop="@dimen/xsmall_margin"
                  android:layout_marginBottom="@dimen/xsmall_margin"
                  android:background="@drawable/accent_to_color_primary_dark__negative_90_gradient"
                  app:layout_constraintBottom_toTopOf="@+id/imgDestination"
                  app:layout_constraintEnd_toEndOf="@+id/imgOrigin"
                  app:layout_constraintStart_toStartOf="@+id/imgOrigin"
                  app:layout_constraintTop_toBottomOf="@+id/imgOrigin" />
      
              <com.google.android.material.textfield.TextInputLayout
                  android:id="@+id/inputOrigin"
                  android:layout_width="0dp"
                  android:layout_height="wrap_content"
                  android:layout_marginTop="@dimen/medium_margin"
                  android:hint="@string/pick_up_location"
                  android:marqueeRepeatLimit="marquee_forever"
                  app:layout_constraintEnd_toEndOf="parent"
                  app:layout_constraintStart_toEndOf="@+id/imgOrigin"
                  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:background="@color/white"
                      android:inputType="textPostalAddress"
                      android:singleLine="true"
                      android:textAppearance="@style/subtitle1" />
      
              </com.google.android.material.textfield.TextInputLayout>
      
      
              <androidx.appcompat.widget.AppCompatImageView
                  android:id="@+id/imgDestination"
                  android:layout_width="@dimen/activity_horizontal_margin"
                  android:layout_height="@dimen/activity_horizontal_margin"
                  android:layout_marginStart="@dimen/medium_margin"
                  android:layout_marginEnd="@dimen/medium_margin"
                  app:layout_constraintBottom_toBottomOf="@+id/inputDestination"
                  app:layout_constraintEnd_toStartOf="@+id/inputDestination"
                  app:layout_constraintStart_toStartOf="parent"
                  app:layout_constraintTop_toTopOf="@+id/inputDestination"
                  app:srcCompat="@drawable/ic_destination"
                  app:tint="@color/colorPrimaryDark" />
      
              <com.google.android.material.textfield.TextInputLayout
                  android:id="@+id/inputDestination"
                  android:layout_width="0dp"
                  android:layout_height="wrap_content"
                  android:layout_marginTop="@dimen/medium_margin"
                  android:hint="@string/search_destination"
                  app:layout_constraintEnd_toEndOf="parent"
                  app:layout_constraintStart_toEndOf="@+id/imgDestination"
                  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:background="@color/white"
                      android:inputType="textPostalAddress"
                      android:singleLine="true"
                      android:textAppearance="@style/subtitle1" />
      
              </com.google.android.material.textfield.TextInputLayout>
      
          </androidx.constraintlayout.widget.ConstraintLayout>
      
      </androidx.constraintlayout.widget.ConstraintLayout>
      
      </com.google.android.material.appbar.AppBarLayout>
      
    2. The bottom layout that acts like a bottom sheet

      <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      android:id="@+id/cardChooseAddressBottomSheet"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      app:cardBackgroundColor="@color/white"
      app:behavior_hideable="false"
      app:layout_behavior="@string/bottom_sheet_behavior">
      
      
      <androidx.constraintlayout.widget.ConstraintLayout
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          app:layout_constraintEnd_toEndOf="parent"
          app:layout_constraintStart_toStartOf="parent"
          app:layout_constraintTop_toTopOf="parent">
      
      
      
          <ImageView
              android:id="@+id/swipeUpHandle"
              android:layout_width="35dp"
              android:layout_height="30dp"
              android:layout_gravity="center"
              android:layout_marginLeft="@dimen/activity_horizontal_margin"
              android:layout_marginRight="@dimen/activity_horizontal_margin"
              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:text="@string/hi_there"
              android:layout_marginLeft="@dimen/activity_horizontal_margin"
              android:layout_marginRight="@dimen/activity_horizontal_margin"
              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_marginLeft="@dimen/activity_horizontal_margin"
              android:layout_marginRight="@dimen/activity_horizontal_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:iconSize="@dimen/medium_icon"
              app:iconTint="@color/colorAccent"
              app:layout_constraintEnd_toEndOf="parent"
              app:layout_constraintStart_toStartOf="parent"
              app:layout_constraintTop_toBottomOf="@+id/hiThere"
              app:shapeAppearanceOverlay="@style/ShapeAppearanceRoundedMediumAllCorners" />
      
      
          <androidx.recyclerview.widget.RecyclerView
              android:id="@+id/recyclerAddresses"
              android:layout_width="match_parent"
              android:layout_height="0dp"
              app:layout_constraintBottom_toBottomOf="parent"
              app:layout_constraintEnd_toEndOf="parent"
              app:layout_constraintStart_toStartOf="parent"
              app:layout_constraintTop_toBottomOf="@+id/btnSearch" />
      
      
          <androidx.recyclerview.widget.RecyclerView
              android:id="@+id/recyclerSearchAddresses"
              android:layout_width="match_parent"
              android:layout_height="0dp"
              android:background="@color/white"
              android:visibility="gone"
              app:layout_constraintBottom_toBottomOf="parent"
              app:layout_constraintEnd_toEndOf="parent"
              app:layout_constraintStart_toStartOf="parent"
              app:layout_constraintTop_toBottomOf="@id/btnSearch" />
      
      </androidx.constraintlayout.widget.ConstraintLayout>
      
      </com.google.android.material.card.MaterialCardView>
      
    3. And finally the two layouts included in my map layout

      <?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"
          android:layout_width="match_parent"
          android:id="@+id/coordinator"
          android:layout_height="match_parent">
      
          <androidx.constraintlayout.widget.ConstraintLayout
              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="0dp"
                  app:layout_constraintEnd_toEndOf="parent"
                  app:layout_constraintHeight_percent="0.67"
                  app:layout_constraintHorizontal_bias="0.0"
                  app:layout_constraintStart_toStartOf="parent"
                  app:layout_constraintTop_toTopOf="parent" />
      
              <androidx.appcompat.widget.AppCompatImageView
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:id="@+id/imgMapCenter"
                  android:layout_marginBottom="@dimen/xlarge_margin"
                  android:visibility="invisible"
                  android:tint="@color/colorAccent"
                  app:layout_constraintBottom_toBottomOf="@id/map"
                  app:layout_constraintEnd_toEndOf="@id/map"
                  app:layout_constraintStart_toStartOf="@id/map"
                  app:layout_constraintTop_toTopOf="@id/map"
                  app:srcCompat="@drawable/ic_destination" />
          </androidx.constraintlayout.widget.ConstraintLayout>
      
      
          <com.google.android.material.floatingactionbutton.FloatingActionButton
              android:id="@+id/fabMyLocation"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_margin="@dimen/activity_horizontal_margin"
              app:backgroundTint="@color/white"
              app:fabSize="mini"
              app:layout_anchor="@id/cardChooseAddressBottomSheet"
              app:layout_anchorGravity="top|right"
              app:srcCompat="@drawable/ic_origin"
              app:tint="@color/colorAccent" />
          <include layout="@layout/taxi_fragment_set_destination_top_bar" />
      
          <include layout="@layout/taxi_fragment_bottom_sheet_addresses_layout" />
      
      </androidx.coordinatorlayout.widget.CoordinatorLayout>
      

    And my fragment

       final private BottomSheetBehavior.BottomSheetCallback addressBottomSheetCallBack = new BottomSheetBehavior.BottomSheetCallback() {
            @Override
            public void onStateChanged(@NonNull View bottomSheet, int newState) {
                switch (newState) {
                    case BottomSheetBehavior.STATE_HIDDEN:
                        break;
    
                    case BottomSheetBehavior.STATE_SETTLING:
                        break;
    
                    case BottomSheetBehavior.STATE_EXPANDED:
                        if (!allPermissionsGranted())
                            requestForLocationPermissions();
                        topAddressBar.setVisibility(Visibility.VISIBLE);
                         fabMyLocation.hide();
                        break;
    
                    case BottomSheetBehavior.STATE_COLLAPSED:
                        if (!allPermissionsGranted())
                            requestForLocationPermissions();
                        topAddressBar.setVisibility(Visibility.INVISIBLE);
                        fabMyLocation.show();
                        break;
    
                    case BottomSheetBehavior.STATE_DRAGGING:
                        break;
    
                    case BottomSheetBehavior.STATE_HALF_EXPANDED:
                        break;
                }
            }
    
    
    
       BottomSheetBehavior addressBottomSheetBehavior = BottomSheetBehavior.from(cardChooseAddressBottomSheet);
    
          topAddressBar.post(() -> {
                CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) cardChooseAddressBottomSheet.getLayoutParams();
                layoutParams.height = ((Resources.getSystem().getDisplayMetrics().heightPixels + (int) utils.percentageOf(62, btnSearch.getMeasuredHeight())) - topAddressBar.getMeasuredHeight());
            });
    
    
            addressBottomSheetBehavior.setPeekHeight((int) utils.percentageOf(29, Resources.getSystem().getDisplayMetrics().heightPixels), true);
            addressBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
    
    
            topToolBar.setNavigationOnClickListener(view12 -> {
                if (addressBottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
                    utopAddressBar.setVisibility(INVISIBLE);
                    addressBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
                } else
                    Navigation.findNavController(view12).navigateUp();
            });
    
            addressBottomSheetBehavior.addBottomSheetCallback(addressBottomSheetCallBack);
    

    Notice am using INVISIBLE instead of GONE on the topAddressBar? that is because everytime I called GONE the layout would ideally recalculate according to my assumption and the map would flicker, to stop that I had to use invisible as the layout does not shrink instead it still takes up the same space but just not visible.

    Also notice I am adding padding cardChooseAddressBottomSheet.getLayoutParams() this is because I need the Sheet not to go too deep underneath the topAddressBar as not to hide my recyclerview content. The current padding makes sure the recyclerview is fully visible and everything else on top of it is underneath the topAddressBar