Android MotionLayout 示例:打造动态交互菜单示例

894 阅读7分钟

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…