MotionLayout系列文章:
Android | MotionLayout入门级使用教程(一):juejin.cn/post/736642…
Android MotionLayout之KeyFrameSet关键帧详解(二):juejin.cn/post/744151…
效果图
先看效果图:
上述代码基于 MotionLayout 定义了多个动画场景,利用 ConstraintSet 和 Transition 构建了一个复杂的 UI 交互动画。通过点击和滑动操作,控制按钮的显示、隐藏、圆形排列和旋转效果。主要功能有:
- 按钮展开和收回:点击 imageButton,一组按钮(button1 到 button6)从不可见状态展开到围绕中心按钮(imageButton)的圆形排列位置;再次点击 button 等任意按钮,这些按钮会回到中心并隐藏。
- 按钮的旋转动画:通过滑动手势,按钮围绕 imageButton 的圆心逆时针旋转到新的位置。
- 动态背景大小和颜色:中心按钮 imageButton 的背景 background 从小变大,同时颜色会发生变化。
- 关键属性控制:利用 motionStagger 设置按钮逐个展开的动画延迟,实现流畅的动画效果;使用 CustomAttribute 自定义属性(如 colorFilter),改变按钮的颜色。
1、Transition 部分
第一个 Transition:start 到 end,点击 imageButton,触发动画,按钮从隐藏状态展开到圆形排列。motion:staggered="0.4":实现动画逐个延迟的效果;第二个 Transition,start 回到 start,点击任意按钮(button1 到 button6),按钮回到中心隐藏状态。第三个 Transition, end 到 rotated,使用 OnSwipe 定义滑动手势(逆时针旋转),按钮围绕 imageButton 旋转。motion:dragScale=".9"
:设置手势滑动比例。motion:onTouchUp="stop"
:手指抬起时动画停止。
2、ConstraintSet 部分 起始布局 (start):按钮(button1 至 button6)位于 imageButton 的圆形中心(layout_constraintCircleRadius="0dp"),且不可见(visibility="invisible");imageButton初始大小为 64dp,颜色为蓝色(#0178d9)。
中间布局 (end)按钮从 imageButton 展开到不同角度和半径。例如 button 位于角度 270°,半径 180dp;button2 位于角度 330°,半径 200dp。background从小(10dp)放大到 500dp。
旋转布局 (rotated)按钮以 imageButton 为中心重新排列,位置和角度改变:例如 button 的角度从 270° 改为 210°。
示例代码
XML文件
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFF"
app:layoutDescription="@xml/layout_circle_menu_scene">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/background"
android:layout_width="500dp"
android:layout_height="500dp"
android:background="#0178d9"
app:layout_constraintBottom_toBottomOf="@+id/imageButton"
app:layout_constraintEnd_toEndOf="@+id/imageButton"
app:layout_constraintStart_toStartOf="@+id/imageButton"
app:layout_constraintTop_toTopOf="@+id/imageButton"
app:roundPercent="1" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#0000"
android:drawableTop="@drawable/power"
android:text="Logout"
android:textAllCaps="false"
android:textColor="@color/white"
app:layout_constraintCircle="@id/imageButton"
app:layout_constraintCircleAngle="270"
app:layout_constraintCircleRadius="180dp" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#0000"
android:drawableTop="@drawable/search"
android:text="Search"
android:textAllCaps="false"
android:textColor="@color/white"
app:layout_constraintCircle="@id/imageButton"
app:layout_constraintCircleAngle="330"
app:layout_constraintCircleRadius="200dp" />
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#0000"
android:drawableTop="@drawable/cart"
android:text="Wishlist"
android:textAllCaps="false"
android:textColor="@color/white"
app:layout_constraintCircle="@id/imageButton"
app:layout_constraintCircleAngle="300"
app:layout_constraintCircleRadius="200dp" />
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#0000"
android:drawableTop="@drawable/list"
android:text="Dashboard"
android:textAllCaps="false"
android:textColor="@color/white"
app:layout_constraintCircle="@id/imageButton"
app:layout_constraintCircleAngle="360"
app:layout_constraintCircleRadius="200dp" />
<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#0000"
android:drawableTop="@drawable/mail"
android:text="mail"
android:textAllCaps="false"
android:textColor="@color/white"
app:layout_constraintCircle="@id/imageButton"
app:layout_constraintCircleAngle="390"
app:layout_constraintCircleRadius="200dp" />
<Button
android:id="@+id/button6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#0000"
android:drawableTop="@drawable/exit"
android:text="back"
android:textAllCaps="false"
android:textColor="@color/white"
app:layout_constraintCircle="@id/imageButton"
app:layout_constraintCircleAngle="420"
app:layout_constraintCircleRadius="200dp" />
<ImageButton
android:id="@+id/imageButton"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginEnd="64dp"
android:layout_marginBottom="64dp"
android:background="#0000"
android:scaleType="fitCenter"
android:src="@drawable/add_circle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/add_circle" />
</androidx.constraintlayout.motion.widget.MotionLayout>
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:constraintSetEnd="@id/end"
motion:constraintSetStart="@+id/start"
motion:duration="500"
motion:staggered="0.4">
<OnClick motion:targetId="@+id/imageButton" />
</Transition>
<Transition
motion:constraintSetEnd="@+id/start"
motion:duration="200">
<OnClick motion:targetId="@+id/button" />
<OnClick motion:targetId="@+id/button2" />
<OnClick motion:targetId="@+id/button3" />
<OnClick motion:targetId="@+id/button4" />
<OnClick motion:targetId="@+id/button5" />
<OnClick motion:targetId="@+id/button6" />
</Transition>
<Transition
motion:constraintSetEnd="@+id/rotated"
motion:constraintSetStart="@id/end">
<OnSwipe
motion:dragDirection="dragAnticlockwise"
motion:dragScale=".9"
motion:onTouchUp="stop"
motion:rotationCenterId="@id/imageButton"
motion:touchAnchorId="@id/button3" />
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="270"
motion:layout_constraintCircleRadius="0dp"
motion:motionStagger="1" />
<Constraint
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="332"
motion:layout_constraintCircleRadius="0dp"
motion:motionStagger="1" />
<Constraint
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="300"
motion:layout_constraintCircleRadius="0dp"
motion:motionStagger="1" />
<Constraint
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="360"
motion:layout_constraintCircleRadius="0dp"
motion:motionStagger="1" />
<Constraint
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="390"
motion:layout_constraintCircleRadius="0dp"
motion:motionStagger="1" />
<Constraint
android:id="@+id/button6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="420"
motion:layout_constraintCircleRadius="0dp"
motion:motionStagger="1" />
<Constraint
android:id="@+id/background"
android:layout_width="10dp"
android:layout_height="10dp"
android:visibility="invisible"
motion:layout_constraintBottom_toBottomOf="@+id/imageButton"
motion:layout_constraintEnd_toEndOf="@+id/imageButton"
motion:layout_constraintStart_toStartOf="@+id/imageButton"
motion:layout_constraintTop_toTopOf="@+id/imageButton"
motion:motionStagger="2" />
<Constraint
android:id="@+id/imageButton"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="32dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent">
<CustomAttribute
motion:attributeName="colorFilter"
motion:customColorValue="#0178d9" />
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/imageButton"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginEnd="64dp"
android:layout_marginBottom="64dp"
android:visibility="visible"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent">
<CustomAttribute
motion:attributeName="colorFilter"
motion:customColorValue="#FFF" />
</Constraint>
<Constraint
android:id="@+id/background"
android:layout_width="500dp"
android:layout_height="500dp"
motion:layout_constraintBottom_toBottomOf="@+id/imageButton"
motion:layout_constraintEnd_toEndOf="@+id/imageButton"
motion:layout_constraintStart_toStartOf="@+id/imageButton"
motion:layout_constraintTop_toTopOf="@+id/imageButton" />
<Constraint
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
motion:animateRelativeTo="@id/imageButton"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="270"
motion:layout_constraintCircleRadius="180dp" />
<Constraint
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
motion:animateRelativeTo="@id/imageButton"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="330"
motion:layout_constraintCircleRadius="200dp" />
<Constraint
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
motion:animateRelativeTo="@id/imageButton"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="300"
motion:layout_constraintCircleRadius="200dp" />
<Constraint
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
motion:animateRelativeTo="@id/imageButton"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="360"
motion:layout_constraintCircleRadius="200dp" />
<Constraint
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
motion:animateRelativeTo="@id/imageButton"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="390"
motion:layout_constraintCircleRadius="200dp" />
<Constraint
android:id="@+id/button6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
motion:animateRelativeTo="@id/imageButton"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="420"
motion:layout_constraintCircleRadius="200dp" />
</ConstraintSet>
<ConstraintSet
android:id="@+id/rotated"
motion:deriveConstraintsFrom="@+id/end">
<Constraint
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="210"
motion:layout_constraintCircleRadius="180dp" />
<Constraint
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="270"
motion:layout_constraintCircleRadius="200dp" />
<Constraint
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="240"
motion:layout_constraintCircleRadius="200dp" />
<Constraint
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="300"
motion:layout_constraintCircleRadius="200dp" />
<Constraint
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="330"
motion:layout_constraintCircleRadius="200dp" />
<Constraint
android:id="@+id/button6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
motion:layout_constraintCircle="@id/imageButton"
motion:layout_constraintCircleAngle="360"
motion:layout_constraintCircleRadius="200dp" />
</ConstraintSet>
</MotionScene>
上面两个文件即是核心代码了,这个示例来自官方的github,完整代码地址参见:github.com/androidx/co…