觉得有帮助就给个关注吧 ^ . ^...
Using MotionLayout with CoordinatorLayout
(Note that MotionLayout can be used to implement similar behavior as CoordinatorLayout. We will show examples of this in an upcoming article)
An easy way to take advantage of MotionLayout is to use it to specify how some part of the screen content can be animated. This way, you can add more interesting motion to existing layouts / screens you already set up in your application, without having to start completely from scratch.
For example, you may want to build the following screen:
The general idea here is to replace the Toolbar element in an AppBarLayout with a MotionLayout. We then let CoordinatorLayout drive the progress of the animation.
As you can control a MotionLayout’s transition position by calling setProgress()
, we can build a simple subclass to do so automatically by listening to the AppBarLayout offset changes:
package com.google.androidstudio.motionlayoutexample.utils
import android.content.Context
import android.support.constraint.motion.MotionLayout
import android.support.design.widget.AppBarLayout
import android.util.AttributeSet
class CollapsibleToolbar @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), AppBarLayout.OnOffsetChangedListener {
override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) {
progress = -verticalOffset / appBarLayout?.totalScrollRange?.toFloat()!!
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
(parent as? AppBarLayout)?.addOnOffsetChangedListener(this)
}
}
Your CoordinatorLayout XML file can then be modified to use this subclass instead of Toolbar:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.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/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false"
android:background="@color/contentBackground">
<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/app_bar_height"
android:theme="@style/AppTheme.AppBarOverlay">
<include layout="@layout/motion_09_coordinatorlayout_header"/>
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_scrolling" />
</android.support.design.widget.CoordinatorLayout>
The only thing left to do is then to create a MotionLayout file containing the views that we want to animate. Here we have an ImageView serving as background, and a TextView:
<?xml version="1.0" encoding="utf-8"?>
<com.google.androidstudio.motionlayoutexample.utils.CollapsibleToolbar
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/motionLayout"
app:layoutDescription="@xml/scene_09"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="50dp"
android:fitsSystemWindows="false"
app:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed">
<ImageView
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@color/colorAccent"
android:scaleType="centerCrop"
android:src="@drawable/monterey"/>
<TextView
android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:transformPivotX="0dp"
android:transformPivotY="0dp"
android:text="Monterey"
android:textColor="#FFF"
android:textSize="32dp" />
</com.google.androidstudio.motionlayoutexample.utils.CollapsibleToolbar>
Finally, the animation itself can be described in a MotionScene:
<?xml version="1.0" encoding="utf-8"?>
<MotionScene
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<Transition
motion:constraintSetStart="@+id/start"
motion:constraintSetEnd="@+id/end" />
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="1.0"
motion:layout_constraintBottom_toBottomOf="parent"/>
<Constraint
android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:rotation="-90.0"
motion:layout_constraintBottom_toBottomOf="@+id/background"
motion:layout_constraintStart_toStartOf="parent"/>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.2"
motion:layout_constraintBottom_toBottomOf="parent"/>
<Constraint
android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
android:rotation="0.0"
motion:layout_constraintBottom_toBottomOf="@+id/background"
motion:layout_constraintStart_toStartOf="parent" />
</ConstraintSet>
</MotionScene>
Using MotionLayout with DrawerLayout
DrawerLayout is another class provided by the Android framework to bring up a side drawer. Rather than the usual menu, we may want to build something a little more interesting:
Similar to what we did to integrate MotionLayout with CoordinatorLayout/AppBarLayout, we can build a small subclass that will automatically set the MotionLayout’s progress:
package com.google.androidstudio.motionlayoutexample.utils
import android.content.Context
import android.support.constraint.motion.MotionLayout
import android.support.v4.widget.DrawerLayout
import android.util.AttributeSet
import android.view.View
class DrawerContent @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), DrawerLayout.DrawerListener {
override fun onDrawerStateChanged(newState: Int) {
}
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
progress = slideOffset
}
override fun onDrawerClosed(drawerView: View) {
}
override fun onDrawerOpened(drawerView: View) {
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
(parent as? DrawerLayout)?.addDrawerListener(this)
}
}
This subclass will automatically sets the progress given the slideOffset
value, from the onDrawerSlide()
callback.
Using this subclass, we can easily integrate a MotionLayout in a DrawerLayout:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/motionLayout"
android:background="@color/colorPrimaryDark">
<include layout="@layout/motion_12_drawerlayout_content"/>
<include layout="@layout/motion_13_drawerlayout_menu"/>
</android.support.v4.widget.DrawerLayout>
The motion_12_drawerlayout_content.xml
file simply contains a similar layout as the one we used in our previous CoordinatorLayout example.
The menu file is the one using MotionLayout (or rather, the DrawerContent subclass we created):
<?xml version="1.0" encoding="utf-8"?>
<com.google.androidstudio.motionlayoutexample.utils.DrawerContent
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/menu"
android:layout_width="180dp"
android:layout_height="match_parent"
android:layout_gravity="start"
app:layoutDescription="@xml/scene_13_menu"
android:background="@color/colorPrimaryDark">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Monterey"
android:textSize="20sp"
android:textStyle="italic"
android:typeface="serif"
android:textColor="#FFF"
app:layout_constraintBottom_toTopOf="@+id/textView3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Information"
app:fontFamily="sans-serif-smallcaps"
android:textColor="#FFF"
app:layout_constraintBottom_toTopOf="@+id/textView4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/view" />
<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Directions"
app:fontFamily="sans-serif-smallcaps"
android:textColor="#FFF"
app:layout_constraintBottom_toTopOf="@+id/textView5"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2" />
<TextView
android:id="@+id/textView5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Sight-Seeing"
app:fontFamily="sans-serif-smallcaps"
android:textColor="#FFF"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView4" />
<View
android:id="@+id/view"
android:background="#c2c1c1"
android:layout_width="100dp"
android:layout_height="1dp"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toTopOf="@+id/textView2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView3" />
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="California"
android:textColor="#FFF"
app:fontFamily="cursive"
app:layout_constraintBottom_toTopOf="@+id/view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</com.google.androidstudio.motionlayoutexample.utils.DrawerContent>
The MotionScene file simply rotates the different elements (check the rotation
attribute between the start
and end
ConstraintSet):
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@+id/start"
motion:duration="250" />
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:rotation="90"
android:translationX="100dp"
motion:layout_constraintBottom_toTopOf="@+id/textView3"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent"
motion:layout_constraintVertical_chainStyle="spread" />
<Constraint
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:rotation="90"
android:translationX="100dp"
motion:layout_constraintBottom_toTopOf="@+id/textView4"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toBottomOf="@+id/view" />
<Constraint
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:rotation="90"
android:translationX="100dp"
motion:layout_constraintBottom_toTopOf="@+id/textView5"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toBottomOf="@+id/textView2" />
<Constraint
android:id="@+id/textView5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:rotation="90"
android:translationX="100dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toBottomOf="@+id/textView4" />
<Constraint
android:id="@+id/view"
android:layout_width="100dp"
android:layout_height="1dp"
android:layout_marginTop="16dp"
android:rotation="90"
android:translationX="100dp"
motion:layout_constraintBottom_toTopOf="@+id/textView2"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toBottomOf="@+id/textView3" />
<Constraint
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:rotation="90"
android:translationX="100dp"
motion:layout_constraintBottom_toTopOf="@+id/view"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toBottomOf="@+id/textView" />
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
motion:layout_constraintBottom_toTopOf="@+id/textView3"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintHorizontal_bias="0.5"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent"
motion:layout_constraintVertical_bias="0.0"
motion:layout_constraintVertical_chainStyle="packed" />
<Constraint
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
motion:layout_constraintBottom_toTopOf="@+id/textView4"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintHorizontal_bias="0.5"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toBottomOf="@+id/view" />
<Constraint
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
motion:layout_constraintBottom_toTopOf="@+id/textView5"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintHorizontal_bias="0.5"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toBottomOf="@+id/textView2" />
<Constraint
android:id="@+id/textView5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintHorizontal_bias="0.5"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toBottomOf="@+id/textView4" />
<Constraint
android:id="@+id/view"
android:layout_width="100dp"
android:layout_height="1dp"
android:layout_marginTop="16dp"
motion:layout_constraintBottom_toTopOf="@+id/textView2"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintHorizontal_bias="0.5"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toBottomOf="@+id/textView3" />
<Constraint
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
motion:layout_constraintBottom_toTopOf="@+id/view"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintHorizontal_bias="0.5"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toBottomOf="@+id/textView" />
</ConstraintSet>
</MotionScene>
Using MotionLayout with ViewPager
In a similar way, we may want to have a more interesting ViewPager header than typical:
We can use a similar trick to integrate with the ViewPager infrastructure, by creating a subclass to pass the current position:
package com.google.androidstudio.motionlayoutexample.utils
import android.content.Context
import android.support.constraint.motion.MotionLayout
import android.support.v4.view.ViewPager
import android.util.AttributeSet
class ViewpagerHeader @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
var numPages = 3
progress = (position + positionOffset) / (numPages - 1)
}
override fun onPageSelected(position: Int) {
}
}
The math is pretty straightforward — onPageScrolled() gives us the position index of the page (so here, 0, 1 or 2 as we have 3 pages), the positionOffset (from 0 to 1, offset from the position…). The total progress of the animation can then be set to be:
progress = (position + positionOffset) / (numPages-1)
Using Lottie with MotionLayout
The previous example built a simple animated header using actual ImageViews. You can also easily integrate Lottie files in your MotionLayout, and set the progress directly — MotionLayout will be able to drive it.
Let’s change our previous example to instead use a LottieAnimationView:
Let’s replace the layout file of our MotionLayout-based ViewPager header to only contain a LottieAnimationView for simplicity’s sake:
<?xml version="1.0" encoding="utf-8"?>
<com.google.androidstudio.motionlayoutexample.utils.ViewpagerHeader xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/motionLayout"
app:layoutDescription="@xml/scene_23"
android:layout_width="match_parent"
app:progress="0"
android:layout_height="230dp">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/animation_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:lottie_rawRes="@raw/walkthrough"/>
</com.google.androidstudio.motionlayoutexample.utils.ViewpagerHeader>
The key modification in the MotionScene is by using the motion:progress
attribute:
<Constraint
android:id="@+id/animation_view"
android:layout\_width="match\_parent"
android:layout\_height="match\_parent"
motion:progress="0"/>
As LottieAnimationView has a setProgress() function, this will result in MotionLayout calling it, and allowing you to drive the Lottie progress directly.
The full MotionScene file can be written as:
<?xml version="1.0" encoding="utf-8"?>
<MotionScene
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<Transition
motion:constraintSetStart="@+id/start"
motion:constraintSetEnd="@+id/end">
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/animation_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
motion:progress="0"/>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/animation_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
motion:progress="1"/>
</ConstraintSet>
</MotionScene>