MotionLayout

文章目录

  • 1 MotionLayout的背景
    • 1.1 动画的演进
    • 1.2 属性动画与过渡动画的对比
    • 1.3 过渡动画的限制
    • 1.4 ConstraintLayout场景过渡
    • 1.5 MotionLayout场景过渡
  • 2 MotionLayout属性配置及使用示例
    • 2.1 MotionScene
    • 2.2 Transition
      • 2.2.1 OnClick
      • 2.2.2 OnSwipe
      • 2.2.3 KeyFrameSet
        • 2.2.3.1 KeyAttribute
        • 2.2.3.2 KeyPosition
        • 2.2.3.3 KeyPositionType坐标系
        • 2.2.3.4 KeyCycle
        • 2.2.3.5 KeyTimeCycle
    • 2.3 ConstraintSet
      • 2.3.1 Constarint
      • 2.3.2 Layout
      • 2.3.3 Motion
      • 2.3.4 CustomAttribute
      • 2.3.5 Transform

1 MotionLayout的背景

在了解 MotionLayout 之前,建议先了解 ConstriantLayout 的一些基本使用,因为 MotionLayoutConstraintLayout 的子类,参考文章:ConstraintLayout的基本使用。

MotionLayout 作为 ConstraintLayout 的子类,既然是子类自然就是对父类功能的一些增强和优化,ConstraintLyout 已经将布局等相关的工作已经做完了,那么 MotionLayout 是干嘛的?顾名思义,Motion 是运动的意思,ConstriantLayout 是静态布局,而 MotionLayout 自然就是可以让布局“运动”起来的布局。

MotionLayout 可以控制布局控件之间实现动画联动,它是一个动画框架,所使用的动画为过渡动画。过渡动画又是什么?接下来慢慢细说。

1.1 动画的演进

目前的动画主要分为三种:View动画、属性动画、过渡动画。

View动画 Animation 是在API 1很早就已经出现,但现在也基本比较少使用它们,因为使用属性动画也可以完全实现同样的效果。比如 AlphaAnimationScaleAnimation 等。

属性动画是在API 11时出现,是目前最常见和使用的动画,可以很方便的实现控件的透明度、平移、缩放、旋转等动画效果。

过渡动画是在API 18时引入,它主要是用于在两种场景或两种状态之间的切换。

1.2 属性动画与过渡动画的对比

先用属性动画实现一个简单的例子:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@mipmap/ic_launcher"
        tools:ignore="ContentDescription" />
</FrameLayout>

public class ObjectAnimatorActivity extends AppCompatActivity {
     

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
     
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_object_animator);
        final View root = findViewById(R.id.root);

        final ImageView image = findViewById(R.id.image);
        image.setOnClickListener(new View.OnClickListener() {
     
            @Override
            public void onClick(View v) {
     
                int distance = root.getWidth() - image.getWidth();
                image.animate().translationX(distance).start();
            }
        });
    }
}

MotionLayout_第1张图片
功能非常简单,点击时使用属性动画将ImageView从左边移动到右边。

使用属性动画实现这个动画经过了两个步骤:

  • 计算图片从左边移动到右边的距离

  • 创建属性动画执行动画

如果我们想直接通过改变ImageView的属性实现这个效果会是怎样的?

FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) image.getLayoutParams();
lp.gravity = Gravity.END;
image.setLayoutParams(lp);

MotionLayout_第2张图片

我们修改了ImageView的 gravity 属性,但是这样是没有动画效果会直接闪现过去,我们再加上一句代码:

// 添加这句代码,在修改控件参数之前
TransitionManager.beginDelayedTransition((ViewGroup) image.getParent());
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) image.getLayoutParams();
lp.gravity = Gravity.END;
image.setLayoutParams(lp);

运行程序可以发现实现效果和属性动画一样有了平移动画的效果。通过加上一句 TransitionManager.beginDelayedTransiton() 就实现了过渡动画的平移效果,从一个状态过渡到另一个状态。

如果单从这个例子说明过渡动画更方便,说服力还是不够,属性动画能实现而且也可以做更多复杂的动画,还是比过渡动画好用。那过渡动画还有哪些是属性动画不具备的?过渡动画可以改变控件的属性,属性动画不能。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/image1"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="@mipmap/ic_launcher"
        tools:ignore="ContentDescription" />

    <ImageView
        android:id="@+id/image2"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_margin="5dp"
        android:background="@mipmap/ic_launcher"
        tools:ignore="ContentDescription" />

    <ImageView
        android:id="@+id/image3"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="@mipmap/ic_launcher"
        tools:ignore="ContentDescription" />
</LinearLayout>

// 属性动画
final ImageView image = findViewById(R.id.image2);
image.animate()
        .scaleX(2.0f)
        .scaleY(2.0f)
        .start();

// 过渡动画
TransitionManager.beginDelayedTransition((ViewGroup) image.getParent());
ViewGroup.LayoutParams lp = image.getLayoutParams();
lp.width *= 2;
lp.height *= 2;
image.setLayoutParams(lp);

属性动画效果如下:
MotionLayout_第3张图片
过渡动画效果如下:
MotionLayout_第4张图片

过渡动画的实现本质可以简单分为两个步骤:

  • 定义两个场景之间的过渡:肯定有开始场景结束场景,会记录两个场景控件的各种参数

  • 创建动画并执行动画:有了上一步记录的控件参数,就可以创建执行动画的参数

1.3 过渡动画的限制

还是先上一个过渡动画的示例:

具体示例代码如下:

activity_film.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <include layout="@layout/film_start_scene" />
</android.support.constraint.ConstraintLayout>

film_start_scene.xml

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/background"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@color/colorPrimary"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/image_film_cover"
        android:layout_width="120dp"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:adjustViewBounds="true"
        android:contentDescription="@null"
        android:src="@drawable/film_cover"
        app:layout_constraintBottom_toBottomOf="@+id/background"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/background" />

    <TextView
        android:id="@+id/text_film_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:paddingVertical="8dp"
        android:textColor="@android:color/white"
        android:textSize="14sp"
        app:layout_constraintStart_toEndOf="@id/image_film_cover"
        app:layout_constraintTop_toTopOf="@id/image_film_cover" />

    <RatingBar
        android:id="@+id/rating_film_rating"
        style="?attr/ratingBarStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:numStars="5"
        android:paddingVertical="8dp"
        android:progressTint="#FFD600"
        app:layout_constraintStart_toStartOf="@+id/text_film_title"
        app:layout_constraintTop_toBottomOf="@id/text_film_title" />

    <TextView
        android:id="@+id/film_description_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:text="@string/file_description_title"
        android:textColor="@color/colorPrimary"
        android:textSize="16sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/background" />

    <TextView
        android:id="@+id/text_film_description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingHorizontal="16dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/film_description_title" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab_favourite"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:src="@drawable/ic_bookmark"
        android:tint="#FFD600"
        app:fabSize="mini"
        app:layout_constraintBottom_toBottomOf="@id/background"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/background" />
</merge>

film_end_scene.xml

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/background"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimary"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/image_film_cover"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:contentDescription="@null"
        android:src="@drawable/film_cover"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/text_film_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:paddingVertical="8dp"
        android:textColor="@android:color/white"
        android:textSize="14sp"
        app:layout_constraintStart_toEndOf="@id/image_film_cover"
        app:layout_constraintTop_toTopOf="@id/image_film_cover" />

    <RatingBar
        android:id="@+id/rating_film_rating"
        style="?attr/ratingBarStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:numStars="5"
        android:paddingVertical="8dp"
        android:progressTint="#FFD600"
        app:layout_constraintStart_toStartOf="@+id/text_film_title"
        app:layout_constraintTop_toBottomOf="@id/text_film_title" />

    <TextView
        android:id="@+id/film_description_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:text="@string/file_description_title"
        android:textColor="@color/colorPrimary"
        android:textSize="16sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/background" />

    <TextView
        android:id="@+id/text_film_description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingHorizontal="16dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/film_description_title" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab_favourite"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        android:src="@drawable/ic_bookmark"
        android:tint="#FFD600"
        app:fabSize="mini"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
</merge>

ic_bookmark.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
    
    <path
        android:fillColor="@android:color/white"
        android:pathData="M17,3L7,3c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3L19,5c0,-1.1 -0.9,-2 -2,-2zM17,18l-5,-2.18L7,18L7,5h10v13z" />
</vector>

public class FilmActivity extends AppCompatActivity implements View.OnClickListener  {
     

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
     
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_film);

        bindData();
    }

    private void bindData() {
     
        findViewById(R.id.image_film_cover).setOnClickListener(this);
        ((RatingBar) findViewById(R.id.rating_film_rating)).setRating(4.5f);
        ((TextView) findViewById(R.id.text_film_title)).setText(R.string.film_title);
        ((TextView) findViewById(R.id.text_film_description)).setText(R.string.film_description);
    }

    private boolean toggle = true;

    @Override
    public void onClick(View v) {
     
        ViewGroup root = findViewById(R.id.root);
        Scene startScene = Scene.getSceneForLayout(root, R.layout.film_start_scene, this);
        Scene endScene = Scene.getSceneForLayout(root, R.layout.film_end_scene, this);
        if (toggle) {
     
            TransitionManager.go(endScene);
        } else {
     
            TransitionManager.go(startScene);
        }

        // 需要重新绑定数据,否则点击后会无法切换场景
        // TransitionManager.go()切换场景后会将当前场景的控件全部移除替换为结束场景的控件对象
        // 两个场景的对象不一样,所以需要重新绑定
        // 虽然会重复的创建对象,但不需要过于担心性能问题
        bindData();
        toggle = !toggle;
    }
}

如果你的程序运行的是support而不是androidx,在使用 ic_bookmark 资源可能会编译失败,需要在 build.gradle 添加支持:

android {
     
    defaultConfig {
     
        vectorDrawables.useSupportLibrary = true
    }
}

如果使用上面讲解的 TransitionManager.beginDelayedTransition() 实现上面的处理将会是比较麻烦的事情,你需要在代码对控件各个参数属性进行修改。

这个示例将控件的开始场景和结束场景分别用 film_start_scene.xmlfilm_end_scene.xml 两个布局文件管理,再用 TransitionManager.go() 将场景装载。但缺点也比较明显:

  • 每次场景切换都会移除控件替换并需要重新绑定调用 bindData()

  • 场景之间有一些控件参数并不需要但还是要加上。比如结束场景最终展示只需要 FloatinActionButton 、封面 ImageView 和一个背景 View,但还是得加上开始场景那些控件,否则会抛出异常

有没有一种办法可以实现:既可以在xml添加修改属性管理,又可以不重复绑定添加控件?使用 ConstraintLayout 作为根布局可以解决这个问题。

1.4 ConstraintLayout场景过渡

activity_start_scene.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/background"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@color/colorPrimary"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/image_film_cover"
        android:layout_width="120dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        android:adjustViewBounds="true"
        android:contentDescription="@null"
        android:src="@drawable/film_cover"
        app:layout_constraintBottom_toBottomOf="@+id/background"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/background" />

    <TextView
        android:id="@+id/text_film_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:paddingVertical="8dp"
        android:textColor="@android:color/white"
        android:textSize="14sp"
        app:layout_constraintStart_toEndOf="@id/image_film_cover"
        app:layout_constraintTop_toTopOf="@id/image_film_cover" />

    <RatingBar
        android:id="@+id/rating_film_rating"
        style="?attr/ratingBarStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:numStars="5"
        android:paddingVertical="8dp"
        android:progressTint="#FFD600"
        app:layout_constraintStart_toStartOf="@+id/text_film_title"
        app:layout_constraintTop_toBottomOf="@id/text_film_title" />

    <TextView
        android:id="@+id/film_description_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:text="@string/file_description_title"
        android:textColor="@color/colorPrimary"
        android:textSize="16sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/background" />


    <TextView
        android:id="@+id/text_film_description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingHorizontal="16dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/film_description_title" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab_favourite"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:src="@drawable/ic_bookmark_24dp"
        android:tint="#FFD600"
        app:fabSize="mini"
        app:layout_constraintBottom_toBottomOf="@id/background"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/background" />
</android.support.constraint.ConstraintLayout>

activity_end_scene.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/background"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimary"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/image_film_cover"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:contentDescription="@null"
        android:src="@drawable/film_cover"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab_favourite"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        android:src="@drawable/ic_bookmark_24dp"
        android:tint="#FFD600"
        app:fabSize="mini"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</android.support.constraint.ConstraintLayout>

public class FilmActivity extends AppCompatActivity implements View.OnClickListener  {
     

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
     
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_start_scene);

        findViewById(R.id.image_film_cover).setOnClickListener(this);
        ((RatingBar) findViewById(R.id.rating_film_rating)).setRating(4.5f);
        ((TextView) findViewById(R.id.text_film_title)).setText(R.string.film_title);
        ((TextView) findViewById(R.id.text_film_description)).setText(R.string.film_description);
    }

    private boolean toggle = true;

    @Override
    public void onClick(View v) {
     
        ConstraintLayout root = findViewById(R.id.root);
        TransitionManager.beginDelayedTransition(root);
        ConstraintSet constraintSet = new ConstraintSet();
        if (toggle) {
     
            constraintSet.clone(this, R.layout.activity_end_scene);
        } else {
     
            constraintSet.clone(this, R.layout.activity_start_scene);
        }
        constraintSet.applyTo(root);
        toggle = !toggle;
    }
}

实现的效果和上面使用 TransitionManager.go() 相同。

ConstraintLayout 通过 ConstraintSet.clone() 后调用 ConstraintSet.applyTo() 就可以实现场景的切换。虽然解决了上面提出的问题,但这种方式实现场景过渡还是不够完美:

  • 不能停留在任意位置,applyTo() 设置了结束场景后就只能执行完成

  • 不支持触摸反馈,根据手指的拖动伸缩

  • 定义的两个xml文件有很多重复的控件属性,需要两个xml布局文件放在layout目录管理

根据上面探讨的问题,能解决这些问题的动画框架 MotionLayout 就诞生了。

1.5 MotionLayout场景过渡

现在使用 MotionLayout 实现上面的效果。

activity_motion_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.motion.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/film_motion">

    <View
        android:id="@+id/background"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@color/colorPrimary"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/image_film_cover"
        android:layout_width="120dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        android:adjustViewBounds="true"
        android:contentDescription="@null"
        android:src="@drawable/film_cover"
        app:layout_constraintBottom_toBottomOf="@+id/background"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/background" />

    <TextView
        android:id="@+id/text_film_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:paddingVertical="8dp"
        android:textColor="@android:color/white"
        android:textSize="14sp"
        app:layout_constraintStart_toEndOf="@id/image_film_cover"
        app:layout_constraintTop_toTopOf="@id/image_film_cover" />

    <RatingBar
        android:id="@+id/rating_film_rating"
        style="?attr/ratingBarStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:numStars="5"
        android:paddingVertical="8dp"
        android:progressTint="#FFD600"
        app:layout_constraintStart_toStartOf="@+id/text_film_title"
        app:layout_constraintTop_toBottomOf="@id/text_film_title" />

    <TextView
        android:id="@+id/film_description_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:text="@string/file_description_title"
        android:textColor="@color/colorPrimary"
        android:textSize="16sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/background" />

    <TextView
        android:id="@+id/text_film_description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingHorizontal="16dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/film_description_title" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab_favourite"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:src="@drawable/ic_bookmark_24dp"
        android:tint="#FFD600"
        app:fabSize="mini"
        app:layout_constraintBottom_toBottomOf="@id/background"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/background" />
</android.support.constraint.motion.MotionLayout>

res/xml 目录定义 MotionLayout 使用的动画执行场景:

<?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_scene"
        app:constraintSetStart="@id/start_scene"
        app:duration="200">

        <!--    设置点击封面执行过渡动画    -->
        <OnClick
            app:clickAction="toggle"
            app:targetId="@id/image_film_cover" />
    </Transition>

    <!--  因为开始场景已经默认设置在setContentView(),这里不设置也关系不大  -->
    <ConstraintSet android:id="@+id/start_scene">

    </ConstraintSet>

    <ConstraintSet android:id="@+id/end_scene">
        <!--   
        	   结束场景的控件,在MotionLayout只会认Constraint
        	   布局相关属性也可以写在<Constraint>标签下的<Layout>
        -->
        <Constraint
            android:id="@+id/background"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/colorPrimary"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@+id/image_film_cover"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:contentDescription="@null"
            android:src="@drawable/film_cover"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@+id/fab_favourite"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="16dp"
            android:layout_marginBottom="16dp"
            android:src="@drawable/ic_bookmark_24dp"
            android:tint="#FFD600"
            app:fabSize="mini"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />
    </ConstraintSet>
</MotionScene>

除了可以通过点击外,还可以实现触摸反馈处理场景切换:

<!--    设置向右拖动封面执行过渡动画    -->
<OnSwipe
    app:dragDirection="dragEnd"
    app:onTouchUp="autoComplete"
    app:touchAnchorId="@id/image_film_cover" />

跟随手指拖动停止:

<OnSwipe
    app:dragDirection="dragEnd"
    app:onTouchUp="stop"
    app:touchAnchorId="@id/image_film_cover" />

2 MotionLayout属性配置及使用示例

MotionLayout 作为 ConstraintLayout 的子类,需要修改 ConstraintLayout 的依赖版本为2.0或以上才能使用:

implementation 'com.android.support.constraint:constraint-layout:2.0.0-beta7'

关于 MotionLayout 这篇文章会讲解比较常用的一些属性参数,具体其他参数的使用和示例可以参考官网:MotionLayout、MotionLayout示例

2.1 MotionScene

MotionScene 定义 MotionLayout 布局动画执行场景等具体参数,作为xml文件的根元素,可以定义三个子元素:

<MotionScene>
	<Transition/>
	<ConstraintSet/>
	<StateSet/>
</MotionScene>

2.2 Transition

Transition 定义一组过渡动画,支持修改动画执行期间布局控件的运动轨迹等。

<MotionScene>
	<Transition
		app:constraintSetStart="@+id/start"
		app:constrantSetEnd="@+id/end"
		app:duration="1000">
		<OnClick/>
		<OnSwipe/>
		<KeyFrameSet>
			<KeyAttribure/>
			<KeyCycle/>
			<KeyPosition/>
			<KeyTimeCycle/>
		</KeyFrameSet>
	</Transition>
</MotionScene>
属性 描述
constraintSetStart 指定开始场景 ConstraintSet 的id或布局xml,即开始时的控件布局状态
constraintSetEnd 指定结束场景 ConstraintSet 的id或布局xml,即结束时的控件布局状态
duration 动画执行时间
motionInterpolator 设置动画执行插值器
<?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">

	<!--  bounce实现回弹效果  -->
    <Transition
        app:constraintSetEnd="@id/end_scene"
        app:constraintSetStart="@id/start_scene"
        app:duration="1000"
        app:motionInterpolator="bounce">

        <OnClick
            app:clickAction="toggle"
            app:targetId="@id/view" />
    </Transition>

    <ConstraintSet android:id="@+id/start_scene">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <Motion app:pathMotionArc="startHorizontal" />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end_scene">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintRight_toRightOf="parent" />
        </Constraint>
    </ConstraintSet>
</MotionScene>

MotionLayout_第5张图片

2.2.1 OnClick

OnClick 设置在点击时触发执行过渡动画。

<MotionScene>
	<Transition>
		<OnClick
			app:clickAction="toggle"
			app:targetId="@id/btn_toggle" />
	</Transition>
</MotionScene>
属性 描述
clickAction 设置点击按钮时动画的执行方式,toggletransitionToEndtransitionToStartjumpToEndjumpToStart
targetId 设置点击执行动画的控件id

注意:如果一个控件已经定义了 就不能去响应

2.2.2 OnSwipe

OnSwipe 设置在触摸拖拽滑动时执行过渡动画。

<MotionScene>
	<Transition>
		<OnSwipe
			app:dragDirection="dragStart"
			app:touchAnchorId="@+id/anchor_id"
			app:onTouchUp="autoComplete" />
	</Transition>
</MotionScene>
属性 描述
dragDirection 设置执行过渡动画的触摸拖拽方向,dragUpdragDowndragLeft/dragStartdragRight/dragEnd
touchAnchorId 设置启动触摸拖拽动画的控件id
onTouchUp 设置触摸松手后执行的场景状态,autoCompleteautoCompleteToStartautoCompleteToEndstopdecelaratedecelarateToComplete;当值为 autoComplete 时,如果触摸拖动松手时的状态靠近开始场景就会往开始场景的状态执行,否则就执行结束场景的状态;当值为 stop 表示跟随手指拖动的位置停在某个位置

注意:如果一个控件已经定义了 就不能去响应

2.2.3 KeyFrameSet

KeyFrameSet 定义控件在动画执行期间具体可以怎样移动,通过属性 设置。

<MotionScene>
	<Transition>
		<KeyFrameSet>
			<KeyAttribute/>
			<KeyCycle/>
			<KeyPosition/>
			<KeyTimeCycle/>
		</KeyFrameSet>
	</Transition>
</MotionScene>

2.2.3.1 KeyAttribute

KeyAttribute 控制动画执行期间在关键帧再处理特定的动画属性,也可以称为属性关键帧。

<MotionScene>
	<Transition>
		<KeyAttribute
			app:framePosition="50"
			app:motionTarget="@id/view"
			app:rotation="180">
			
			<CustomAttribute
				app:attributeName="backgroundColor"
				app:customColorValue="@android:color/black" />
		</KeyAttribute>
	</Transition>
</MotionScene>
属性 描述
framePosition 动画关键帧百分比,取值范围0-100。比如值为50,表示动画执行到50%的位置
motionTarget 被处理的控件id
rotation 设置view的旋转角度
rotationX 设置view横坐标的旋转角度
rotationY 设置view纵坐标的旋转角度
scaleX 设置view水平方向的缩放
scaleY 设置view垂直方向的缩放
translationX 设置viewX轴平移
translationY 设置viewY轴平移
translationZ 设置viewZ轴平移

KeyAttribute 也支持在对应关键帧动态设置 CustomAttribute 修改控件属性,例如控件背景、文本颜色等等,这在后续节点说明该属性。

<?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_scene"
        app:constraintSetStart="@id/start_scene"
        app:duration="1000">

        <OnClick
            app:clickAction="toggle"
            app:targetId="@id/view" />

        <KeyFrameSet>
        	<!--  实现动画执行到50%的时候旋转180度并且放大,然后恢复原样 -->
            <KeyAttribute
                android:rotation="180"
                android:scaleX="2"
                android:scaleY="2"
                app:framePosition="50"
                app:motionTarget="@id/view" />
        </KeyFrameSet>
    </Transition>

    <ConstraintSet android:id="@+id/start_scene">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <Motion app:pathMotionArc="startHorizontal" />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end_scene">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintRight_toRightOf="parent" />
        </Constraint>
    </ConstraintSet>
</MotionScene>

MotionLayout_第6张图片

2.2.3.2 KeyPosition

KeyPosition 处理动画执行期间在某个关键帧移动位置,可以称为位置关键帧。

<MotionScene>
	<Transition>
		<KeyPosition
			app:framePosition="50"
			app:keyPositionType="deltaRelative"
			app:motionTarget="@id/view"
			app:percentX="1"
			app:percentY="0" />
	</Transition>
</MotionScene>
属性 描述
motionTarget 被处理的控件id
framePosition 动画关键帧百分比,取值范围0-100
keyPositionType 指定关键帧使用的坐标系,有三种坐标系:parentRelativedeltaRelativepathRelative
percentX 指定关键帧运动轨迹的水平相对位置,取值范围0-1;keyPositionType 选择不同的坐标系运动轨迹位置也有所不同
percentY 指定关键帧运动轨迹的垂直相对位置,取值范围0-1;keyPositionType 选择不同的坐标系运动轨迹位置也有所不同
pathMotionArc 修改所在关键帧的运动轨迹,startHorizontal 表示起始方向是水平方向;startVertical 表示起始方向是垂直方向,最终会让运动轨迹形成弧形;none 表示不处理运动轨迹为直线;flip 表示设置往后的运动轨迹与之前的相反
<?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_scene"
        app:constraintSetStart="@id/start_scene"
        app:duration="1000">

        <OnClick
            app:clickAction="toggle"
            app:targetId="@id/view" />

        <KeyFrameSet>
        	<!-- 
        		动画执行到50%时移动到相对于parent的中间位置、指定运动轨迹和之前的相反 
				需要注意的是,pathMotionArc要生效,控件的起始场景Constraint也必须设置pathMotionArc,否则会失效默认为直线
			-->
            <KeyPosition
                app:framePosition="50"
                app:keyPositionType="parentRelative"
                app:motionTarget="@id/view"
                app:pathMotionArc="flip"
                app:percentX="0.5"
                app:percentY="0.5" />
        </KeyFrameSet>
    </Transition>

    <ConstraintSet android:id="@+id/start_scene">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

			<!-- 起始场景设置pathMotionArc,否则位置关键帧的pathMotionArc不生效  -->
            <Motion app:pathMotionArc="startHorizontal" />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end_scene">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintRight_toRightOf="parent" />
        </Constraint>
    </ConstraintSet>
</MotionScene>

MotionLayout_第7张图片

2.2.3.3 KeyPositionType坐标系

keyPositionType 指定不同的坐标系统,每一个关键帧 KeyPosition 都是独立的可以按需指定自己使用的坐标系统

  • parentRelative

该坐标系是让控件的位置关键帧相对于父容器,比如上面的例子就是相对于父容器 MotionLayout

MotionLayout_第8张图片

  • deltaRelative

该坐标系是让控件的位置关键帧相对于开始位置和结束位置,开始位置的中心坐标是控件当前关键帧的中心点,结束位置的中心坐标是控件当前关键帧的中心点。

如果你想要控件在水平方向或垂直方向上移动,这个坐标系会非常有用,因为坐标系是相对于自身位置的,所以可以根据自身位置很方便拿到位置信息。

为了能够清晰看清坐标系的差别,将上面例子的结束位置调整到中间位置:

MotionLayout_第9张图片

  • pathRelative

简单可以理解为该坐标系是通过开始位置和结束位置的长度定义到y轴的长度。

MotionLayout_第10张图片
如果上面的不好理解,那我们将开始位置移动到中间位置:

MotionLayout_第11张图片

使用三种坐标系演示一个示例:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.motion.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motion_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/motion_scene_test">

    <TextView
        android:id="@+id/a"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="A"
        android:textSize="24sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/b"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="B"
        android:textSize="24sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/c"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="C"
        android:textSize="24sp"
        android:textStyle="bold" />

    <Button
        android:id="@+id/btn_toggle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="toggle"
        android:textAllCaps="false" />

    <Button
        android:id="@+id/btn_show_debug"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="showPath"
        android:textAllCaps="false" />
</android.support.constraint.motion.MotionLayout>

final MotionLayout motionLayout = findViewById(R.id.motion_layout);

findViewById(R.id.btn_show_debug).setOnClickListener(new View.OnClickListener() {
     
    @Override
    public void onClick(View v) {
     
        motionLayout.setDebugMode(MotionLayout.DEBUG_SHOW_PATH);
    }
});

<?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">

        <OnClick
            app:clickAction="toggle"
            app:targetId="@id/btn_toggle" />

        <KeyFrameSet>
            <KeyPosition
                app:framePosition="50"
                app:keyPositionType="deltaRelative"
                app:motionTarget="@id/a"
                app:percentX="1"
                app:percentY="0" />

            <KeyPosition
                app:framePosition="50"
                app:keyPositionType="pathRelative"
                app:motionTarget="@id/b"
                app:percentX="0.5"
                app:percentY="-0.5" />

            <KeyPosition
                app:framePosition="50"
                app:keyPositionType="parentRelative"
                app:motionTarget="@id/c"
                app:percentX="0.5"
                app:percentY="0.25" />
        </KeyFrameSet>
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/a"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="A"
            android:textSize="24sp"
            android:textStyle="bold"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <CustomAttribute
                app:attributeName="textColor"
                app:customColorValue="@android:color/darker_gray" />
        </Constraint>

        <Constraint
            android:id="@+id/b"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="B"
            android:textSize="24sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toTopOf="@+id/c"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/a">

            <CustomAttribute
                app:attributeName="textColor"
                app:customColorValue="@android:color/darker_gray" />
        </Constraint>

        <Constraint
            android:id="@+id/c"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="C"
            android:textSize="24sp"
            android:textStyle="bold"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/b">

            <CustomAttribute
                app:attributeName="textColor"
                app:customColorValue="@android:color/darker_gray" />
        </Constraint>

        <Constraint
            android:id="@+id/btn_toggle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@+id/btn_show_debug"
            app:layout_constraintRight_toRightOf="parent" />

        <Constraint
            android:id="@+id/btn_show_debug"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toRightOf="@+id/btn_toggle"
            app:layout_constraintRight_toRightOf="parent" />
    </ConstraintSet>


    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/a"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="A"
            android:textSize="24sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintRight_toLeftOf="@+id/b"
            app:layout_constraintTop_toTopOf="parent">

            <CustomAttribute
                app:attributeName="textColor"
                app:customColorValue="@color/colorPrimary" />
        </Constraint>

        <Constraint
            android:id="@+id/b"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="B"
            android:textSize="24sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <CustomAttribute
                app:attributeName="textColor"
                app:customColorValue="@color/colorAccent" />
        </Constraint>

        <Constraint
            android:id="@+id/c"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="C"
            android:textSize="24sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toRightOf="@+id/b"
            app:layout_constraintTop_toTopOf="parent">

            <CustomAttribute
                app:attributeName="textColor"
                app:customColorValue="@android:color/black" />
        </Constraint>

        <Constraint
            android:id="@+id/btn_toggle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@+id/btn_show_debug" />

        <Constraint
            android:id="@+id/btn_show_debug"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toRightOf="@+id/btn_toggle"
            app:layout_constraintRight_toRightOf="parent" />
    </ConstraintSet>
</MotionScene>

MotionLayout_第12张图片

2.2.3.4 KeyCycle

KeyCycle 属性可以指定在关键帧范围内执行重复动画。

属性 描述
motionTarget 被处理的控件id
framePosition 动画关键帧百分比,取值范围0-100
wavePeriod 循环次数,在指定的 framePosition 执行重复动画次数
waveShape 振幅循环模型,比如 sincos
rotation 设置view的旋转角度
rotationX 设置view横坐标的旋转角度
rotationY 设置view纵坐标的旋转角度
scaleX 设置view水平方向的缩放
scaleY 设置view垂直方向的缩放
translationX 设置viewX轴平移
translationY 设置viewY轴平移
translationZ 设置viewZ轴平移

比如现在有一个需求:拖动一个View在执行到一半时左右各旋转45°。可以用 KeyAttribute 属性关键帧实现:

<?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_scene"
        app:constraintSetStart="@id/start_scene"
        app:duration="1000">

        <OnSwipe
            app:dragDirection="dragRight"
            app:touchAnchorId="@id/view" />

        <KeyFrameSet>
            <KeyAttribute
                android:rotation="-45"
                app:framePosition="45"
                app:motionTarget="@id/view" />

            <KeyAttribute
                android:rotation="0"
                app:framePosition="50"
                app:motionTarget="@id/view"/>

            <KeyAttribute
                android:rotation="45"
                app:framePosition="55"
                app:motionTarget="@id/view"/>
        </KeyFrameSet>
    </Transition>

    <ConstraintSet android:id="@+id/start_scene">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end_scene">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
        </Constraint>
    </ConstraintSet>
</MotionScene>

上面需求比较简单,如果后续要求在执行到一半时循环旋转45°各10次,那就要写很多的 KeyAttribute。针对这种需求,完全可以通过 KeyCycle 实现:

<?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_scene"
        app:constraintSetStart="@id/start_scene"
        app:duration="1000">

        <OnSwipe
            app:dragDirection="dragRight"
            app:touchAnchorId="@id/view" />

        <KeyFrameSet>
            <!--     
            	KeyCycle定义的是一个区间
            	第一个KeyCycle和最后一个KeyCycle是定义区间范围的
            	中间的KeyCycle可以随意定义振幅循环次数       
            -->
            <KeyCycle
                android:rotation="0"
                app:framePosition="0"
                app:motionTarget="@id/view"
                app:wavePeriod="0"
                app:waveShape="sin" />

            <KeyCycle
                android:rotation="45"
                app:framePosition="50"
                app:motionTarget="@id/view"
                app:wavePeriod="1"
                app:waveShape="sin" />

            <KeyCycle
                android:rotation="0"
                app:framePosition="100"
                app:motionTarget="@id/view"
                app:wavePeriod="0"
                app:waveShape="sin" />
        </KeyFrameSet>
    </Transition>

    <ConstraintSet android:id="@+id/start_scene">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end_scene">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
        </Constraint>
    </ConstraintSet>
</MotionScene>

使用 KeyCycle 指定 wavePeriod 就可以指定在对应关键帧动画循环次数。

MotionLayout_第13张图片

2.2.3.5 KeyTimeCycle

KeyTimeCycleKeyCycle 一样也是处理重复动画,但不同在于它是以时间为度量来处理的。

<?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_scene"
        app:constraintSetStart="@id/start_scene"
        app:duration="1000">

        <OnSwipe
            app:dragDirection="dragRight"
            app:touchAnchorId="@id/view" />

        <KeyFrameSet>
            <KeyTimeCycle
                android:rotation="0"
                app:framePosition="0"
                app:motionTarget="@id/view"
                app:wavePeriod="0"
                app:waveShape="sin" />

            <KeyTimeCycle
                android:rotation="45"
                app:framePosition="50"
                app:motionTarget="@id/view"
                app:wavePeriod="0.5"
                app:waveShape="sin" />

            <KeyTimeCycle
                android:rotation="0"
                app:framePosition="100"
                app:motionTarget="@id/view"
                app:wavePeriod="0"
                app:waveShape="sin" />
        </KeyFrameSet>
    </Transition>

    <ConstraintSet android:id="@+id/start_scene">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end_scene">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
        </Constraint>
    </ConstraintSet>
</MotionScene>

MotionLayout_第14张图片

2.3 ConstraintSet

ConstraintSet 指定开始场景和结束场景,对 ConstriantSet 指定id后在 通过 constraintSetStartconstraintSetEnd 设置一组过渡动画的场景切换。

2.3.1 Constarint

MotionLayout 配置文件中,可以将 Constraint 理解为在对应 ConstraintSet 场景下的View控件,只是在配置文件中是不会认ImageView或TextView等等,都用 Constraint 代替。

<MotionScene>
	<ConstraintSet>
		<Constraint
			android:id="@+id/view"
			android:layout_width="80dp"
            android:layout_height="80dp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
	</ConstraintSet>
</MotionScene>

2.3.2 Layout

LayoutConstraint 的子节点,主要是声明控件的布局属性。

<MotionScene>
	<ConstraintSet>
		<Constraint android:id="@+id/view">
			<Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintRight_toRightOf="parent" />
		</Constraint>
	</ConstraintSet>
</MotionScene>

相比直接将布局属性写在 Constraint 节点,使用 Layout 会更加合理。

2.3.3 Motion

MotionConstraint 的子节点,可以指定控件的运动轨迹,默认是直线运动轨迹,通过它可以很方便的将运动轨迹修改为曲线。

<MotionScene>
	<ConstraintSet>
		<Constraint>
			<Motion 
                app:pathMotionArc="startVertical"
                app:transitionEasing="decelerate" />
		</Constraint>
	</ConstraintSet>
</MotionScene>
属性 描述
pathMotionArc 指定曲线运动轨迹方向,startHorizontal 表示起始方向是水平方向,startVertical 表示起始方向是垂直方向,最终会让运动轨迹形成弧形
transitionEasing 指定动画运动的插值器,支持 accelaratedecelaratelinearstandard
  • startHorizontal
    MotionLayout_第15张图片
  • startVertical
    MotionLayout_第16张图片

2.3.4 CustomAttribute

CustomAttributeConstraint 的子节点,在该节点声明控件背景、文本颜色等其他自定义属性。

<MotionScene>
	<ConstraintSet>
		<Constraint>
            <CustomAttribute
                app:attributeName="backgroundColor"
                app:customColorValue="@color/colorAccent" />
		</Constraint>
	</ConstraintSet>
</MotionScene>
属性 描述
attributeName 设置要修改的控件属性,比如要调用控件的 setTextColor,则执行属性名 textColor
customColorValue 修改颜色属性值
customIntegerValue 修改int属性值
customFloatValue 修改float属性值
customStringValue 修改string属性值
customDimension 修改dimension属性值
customBoolean 修改boolean属性值
<?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_scene"
        app:constraintSetStart="@id/start_scene"
        app:duration="1000">

        <OnClick
            app:clickAction="toggle"
            app:targetId="@id/view" />
    </Transition>

    <ConstraintSet android:id="@+id/start_scene">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <Motion
                app:pathMotionArc="startVertical"
                app:transitionEasing="decelerate" />

            <CustomAttribute
                app:attributeName="backgroundColor"
                app:customColorValue="@color/colorAccent" />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end_scene">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintRight_toRightOf="parent" />

            <CustomAttribute
                app:attributeName="backgroundColor"
                app:customColorValue="@color/colorPrimary" />
        </Constraint>
    </ConstraintSet>
</MotionScene>

MotionLayout_第17张图片

2.3.5 Transform

TransformConstraint 的子节点,可以指定从开始场景到结束场景之间添加动画效果,比如缩放、旋转等。

<MotionScene>
	<ConstraintSet>
		<Constraint>
			<Transform
				android:rotation="360"
                android:scaleX="2"
                android:scaleY="2" />
		</Constraint>
	</ConstraintSet>
</MotionScene>
属性 描述
rotation 设置view的旋转角度
rotationX 设置view横坐标的旋转角度
rotationY 设置view纵坐标的旋转角度
scaleX 设置view水平方向的缩放
scaleY 设置view垂直方向的缩放
translationX 设置viewX轴平移
translationY 设置viewY轴平移
translationZ 设置viewZ轴平移
<?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_scene"
        app:constraintSetStart="@id/start_scene"
        app:duration="1000">

        <OnClick
            app:clickAction="toggle"
            app:targetId="@id/view" />
    </Transition>

    <ConstraintSet android:id="@+id/start_scene">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <Motion
                app:pathMotionArc="startVertical"
                app:transitionEasing="decelerate" />

            <CustomAttribute
                app:attributeName="backgroundColor"
                app:customColorValue="@color/colorAccent" />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end_scene">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintRight_toRightOf="parent" />

            <Transform
                android:rotation="360"
                android:scaleX="1.33"
                android:scaleY="1.33"
                android:translationZ="12dp" />

            <CustomAttribute
                app:attributeName="backgroundColor"
                app:customColorValue="@color/colorPrimary" />
        </Constraint>
    </ConstraintSet>
</MotionScene>

MotionLayout_第18张图片

你可能感兴趣的:(Android,系统控件,动画)