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
I want to mimic Lyft's bottom sheet
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
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 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
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>
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>
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