一、前言
为什么别人的UI看起来很有科技感,并且动画,交互非常的丝滑和顺畅,为什么呢?因为我们没有了解MotionLayout。
二、MotionLayout是什么?
MotionLayout 是 Android 中基于 ConstraintLayout 的高级布局容器,在 ConstraintLayout 2.0 版本中引入的。专门用于管理复杂的视图动画和过渡效果。它通过声明式 XML 或代码定义视图的运动路径、关键帧动画和交互行为,适用于实现高度动态的 UI 交互(如折叠效果、滑动抽屉、复杂转场动画等)。
2.1 特点
1、继承自 ConstraintLayout:完全兼容 ConstraintLayout 的约束系统,可直接在现有布局上扩展动画功能。
2、基于关键帧的动画:通过定义起始和结束状态(ConstraintSet),自动生成平滑过渡动画,支持中间关键帧调整。
3、交互驱动动画:动画可由用户手势(如滑动、拖动)或程序事件实时触发,并支持动态响应(如速度跟踪)。
4、可视化工具支持:Android Studio 提供 Motion Editor,支持图形化设计动画路径和属性变化。
三、快速入门
- 添加依赖
确保 build.gradle 中包含最新版本:
dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}
- 定义 MotionScene(动画规则)
在 res/xml 中创建 motion_scene.xml,定义动画的起始/结束状态和过渡:
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 起始状态 -->
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
</ConstraintSet>
<!-- 结束状态 -->
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</ConstraintSet>
<!-- 定义过渡动画 -->
<Transition
android:id="@+id/transition"
app:constraintSetStart="@id/start"
app:constraintSetEnd="@id/end"
app:duration="1000">
<!-- 触发条件:点击按钮 -->
<OnClick
app:targetId="@id/button"
app:clickAction="toggle"/>
</Transition>
</MotionScene>
- 在布局中关联 MotionLayout
在 XML 布局文件中使用 MotionLayout,并绑定 MotionScene:
<androidx.constraintlayout.motion.widget.MotionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/motion_scene">
<Button
android:id="@+id/button"
android:text="Animate"
android:backgroundTint="#6200EE"/>
</androidx.constraintlayout.motion.widget.MotionLayout>
- 运行效果
点击按钮时,它会从屏幕左侧平滑移动到右侧(反之亦然)。
四、实现上图效果
这里就省略了tablayout+viewpager的使用,会使用一个简单的textview来代替
4.1、布局
<androidx.constraintlayout.motion.widget.MotionLayout
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"
xmlns:tools="http://schemas.android.com/tools"
app:layoutDescription="@xml/library_tab_three"
android:id="@+id/root"
tools:progress="0"
android:background="#ccc"
android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"
>
<TextView
android:id="@+id/header"
android:layout_width="0dp"
android:layout_height="75dp"
android:gravity="center_vertical"
android:paddingStart="25dp"
android:paddingEnd="0dp"
android:text="@string/library"
android:textColor="#000000"
android:textSize="26sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/status_bar_parent" />
<TextView
android:id="@+id/body"
android:layout_width="0dp"
android:layout_height="75dp"
android:gravity="center_vertical"
android:paddingStart="25dp"
android:paddingEnd="0dp"
android:text="到置顶显示"
android:textColor="#000000"
android:textSize="26sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/header" />
</androidx.constraintlayout.motion.widget.MotionLayout>
4.2、library_tab_three文件
<?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"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<!-- 定义过渡动画 -->
<Transition
app:constraintSetEnd="@layout/screen_tab_three_end"
app:constraintSetStart="@layout/screen_tab_three"
app:duration="350">
<!--
这是定义基于滑动操作的触发器。
app:touchAnchorId="@id/box":指定了触摸的锚点视图(@id/box)。用户需要在 box 视图上进行滑动操作才能触发动画。
app:touchAnchorSide="top":指定了触摸锚点的上边。这里表示触摸锚点视图(box)的侧边。
app:dragDirection="dragRight":指定了滑动的方向。这里表示当用户向上滑动时触发动画。
-->
<OnSwipe
app:dragDirection="dragUp"
app:touchAnchorId="@id/body"
app:touchAnchorSide="top"
/>
</Transition>
</MotionScene>
4.3、screen_tab_three_end:结束的时候
比如我们结束的时候,要隐藏掉音乐库的控件,给这个控件加一个gone
<androidx.constraintlayout.motion.widget.MotionLayout
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"
xmlns:tools="http://schemas.android.com/tools"
app:layoutDescription="@xml/library_tab_three"
android:id="@+id/root"
tools:progress="0"
android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"
>
<TextView
android:id="@+id/header"
android:layout_width="0dp"
android:layout_height="75dp"
android:gravity="center_vertical"
android:paddingStart="25dp"
android:paddingEnd="0dp"
android:text="@string/library"
android:textColor="#000000"
android:textSize="26sp"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/status_bar_parent"/>
<TextView
android:id="@+id/body"
android:layout_width="0dp"
android:layout_height="75dp"
android:gravity="center_vertical"
android:paddingStart="25dp"
android:paddingEnd="0dp"
android:text="到置顶显示"
android:textColor="#000000"
android:textSize="26sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/header" />
</androidx.constraintlayout.motion.widget.MotionLayout>
4.4、screen_tab_three
内容和布局文件没有什么不同。
<androidx.constraintlayout.motion.widget.MotionLayout
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"
xmlns:tools="http://schemas.android.com/tools"
app:layoutDescription="@xml/library_tab_three"
android:id="@+id/root"
tools:progress="0"
android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"
>
<TextView
android:id="@+id/header"
android:layout_width="0dp"
android:layout_height="75dp"
android:gravity="center_vertical"
android:paddingStart="25dp"
android:paddingEnd="0dp"
android:text="@string/library"
android:textColor="#000000"
android:textSize="26sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/status_bar_parent" />
<TextView
android:id="@+id/body"
android:layout_width="0dp"
android:layout_height="75dp"
android:gravity="center_vertical"
android:paddingStart="25dp"
android:paddingEnd="0dp"
android:text="到置顶显示"
android:textColor="#000000"
android:textSize="26sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/header" />
</androidx.constraintlayout.motion.widget.MotionLayout>
运行程序就可以了。
可以看到这里我并没有使用ConstraintSet来作为开始和结束,而是直接把之前的代码复制过来,start是显示,end文件就隐藏了。为什么这样也可以呢?因为它可以推算出来。