You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

实现点击FAB平滑过渡为自定义对话框的动画效果

实现FAB到自定义对话框的变形过渡动画

这种动画的核心是让FAB通过形状、尺寸、位置的平滑过渡,逐步「变身」成自定义对话框的容器,而非直接弹出独立的Dialog组件。以下是具体实现方案:

核心思路

通过属性动画控制FAB的位置、缩放比例、圆角弧度,让它从圆形按钮逐步过渡到自定义对话框的大小和形状;动画后期切换显示自定义对话框内容,隐藏FAB,实现「变形」的视觉效果。关闭对话框时执行反向动画,恢复FAB初始状态。


实现步骤

1. 布局准备

在页面布局中,将自定义对话框内容与FAB放在同一父容器内,初始隐藏对话框内容,仅显示FAB。

示例布局(activity_main.xml):

<RelativeLayout 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:padding="16dp">

    <!-- 自定义对话框容器,初始隐藏 -->
    <LinearLayout
        android:id="@+id/custom_dialog_container"
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:background="@drawable/dialog_background"
        android:orientation="vertical"
        android:padding="20dp"
        android:visibility="gone">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="自定义对话框标题"
            android:textSize="18sp"
            android:textStyle="bold"/>

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="输入内容"
            android:layout_marginVertical="12dp"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="end">

            <Button
                android:id="@+id/btn_cancel"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="取消"/>
            <Button
                android:id="@+id/btn_confirm"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="确认"
                android:layout_marginLeft="12dp"/>
        </LinearLayout>
    </LinearLayout>

    <!-- 悬浮按钮 -->
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentEnd="true"
        android:src="@drawable/ic_add"
        app:fabSize="normal"/>

</RelativeLayout>

对话框圆角背景(drawable/dialog_background.xml):

<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#ffffff"/>
    <corners android:radius="16dp"/>
</shape>

2. 动画逻辑实现

在代码中处理FAB的点击事件,执行变形动画;同时处理对话框取消按钮的反向动画。

示例代码(MainActivity.kt):

import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.os.Bundle
import android.view.View
import android.view.ViewOutlineProvider
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.floatingactionbutton.FloatingActionButton

class MainActivity : AppCompatActivity() {
    private lateinit var fab: FloatingActionButton
    private lateinit var dialogContainer: LinearLayout
    private var isDialogShown = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        fab = findViewById(R.id.fab)
        dialogContainer = findViewById(R.id.custom_dialog_container)
        val btnCancel = findViewById<View>(R.id.btn_cancel)

        fab.setOnClickListener {
            if (!isDialogShown) expandFabToDialog()
        }

        btnCancel.setOnClickListener {
            collapseDialogToFab()
        }
    }

    // FAB变形为对话框的动画
    private fun expandFabToDialog() {
        // 获取FAB和对话框的屏幕位置
        val fabLocation = IntArray(2)
        fab.getLocationOnScreen(fabLocation)
        val dialogLocation = IntArray(2)
        dialogContainer.getLocationOnScreen(dialogLocation)

        // 计算位置偏移量
        val translationX = dialogLocation[0] - fabLocation[0]
        val translationY = dialogLocation[1] - fabLocation[1]

        // 计算尺寸缩放比例
        val scaleX = dialogContainer.width.toFloat() / fab.width
        val scaleY = dialogContainer.height.toFloat() / fab.height

        // 移动动画
        val translateXAnim = ObjectAnimator.ofFloat(fab, View.TRANSLATION_X, translationX)
        val translateYAnim = ObjectAnimator.ofFloat(fab, View.TRANSLATION_Y, translationY)

        // 缩放动画
        val scaleXAnim = ObjectAnimator.ofFloat(fab, View.SCALE_X, scaleX)
        val scaleYAnim = ObjectAnimator.ofFloat(fab, View.SCALE_Y, scaleY)

        // 圆角变化动画:从圆形变为对话框圆角
        val cornerAnim = ObjectAnimator.ofInt(0, 16.dpToPx())
        cornerAnim.addUpdateListener { animator ->
            val radius = animator.animatedValue as Int
            fab.outlineProvider = object : ViewOutlineProvider() {
                override fun getOutline(view: View, outline: android.graphics.Outline) {
                    outline.setRoundRect(0, 0, view.width, view.height, radius)
                }
            }
            fab.invalidateOutline()
        }

        // 组合并执行动画
        val animatorSet = AnimatorSet()
        animatorSet.playTogether(translateXAnim, translateYAnim, scaleXAnim, scaleYAnim, cornerAnim)
        animatorSet.duration = 300

        // 动画结束后切换显示状态
        animatorSet.addListener(object : android.animation.Animator.AnimatorListener {
            override fun onAnimationEnd(animation: android.animation.Animator?) {
                fab.visibility = View.INVISIBLE
                dialogContainer.visibility = View.VISIBLE
                isDialogShown = true
            }
            override fun onAnimationStart(animation: android.animation.Animator?) {}
            override fun onAnimationCancel(animation: android.animation.Animator?) {}
            override fun onAnimationRepeat(animation: android.animation.Animator?) {}
        })

        animatorSet.start()
    }

    // 对话框收缩回FAB的动画
    private fun collapseDialogToFab() {
        dialogContainer.visibility = View.INVISIBLE
        fab.visibility = View.VISIBLE

        // 反向动画:恢复位置、缩放、圆角
        val translateXAnim = ObjectAnimator.ofFloat(fab, View.TRANSLATION_X, 0f)
        val translateYAnim = ObjectAnimator.ofFloat(fab, View.TRANSLATION_Y, 0f)
        val scaleXAnim = ObjectAnimator.ofFloat(fab, View.SCALE_X, 1f)
        val scaleYAnim = ObjectAnimator.ofFloat(fab, View.SCALE_Y, 1f)

        val cornerAnim = ObjectAnimator.ofInt(16.dpToPx(), fab.width/2)
        cornerAnim.addUpdateListener { animator ->
            val radius = animator.animatedValue as Int
            fab.outlineProvider = object : ViewOutlineProvider() {
                override fun getOutline(view: View, outline: android.graphics.Outline) {
                    outline.setRoundRect(0, 0, view.width, view.height, radius)
                }
            }
            fab.invalidateOutline()
        }

        val animatorSet = AnimatorSet()
        animatorSet.playTogether(translateXAnim, translateYAnim, scaleXAnim, scaleYAnim, cornerAnim)
        animatorSet.duration = 300

        animatorSet.addListener(object : android.animation.Animator.AnimatorListener {
            override fun onAnimationEnd(animation: android.animation.Animator?) {
                isDialogShown = false
            }
            override fun onAnimationStart(animation: android.animation.Animator?) {}
            override fun onAnimationCancel(animation: android.animation.Animator?) {}
            override fun onAnimationRepeat(animation: android.animation.Animator?) {}
        })

        animatorSet.start()
    }

    // 扩展函数:dp转px
    private fun Int.dpToPx(): Int {
        val density = resources.displayMetrics.density
        return (this * density).toInt()
    }
}

3. 关键注意事项

  • 轮廓控制:通过ViewOutlineProvider动态修改FAB的形状,保证圆角过渡平滑自然。
  • 动画同步:使用AnimatorSet让移动、缩放、圆角动画同步执行,避免视觉脱节。
  • 层级管理:动画过程中FAB需处于最上层,动画结束后切换为对话框内容显示。

简化方案

如果手动写动画太繁琐,可以使用Material Design的MotionLayout,通过XML定义FAB与对话框的过渡状态,无需编写大量动画代码,实现更简洁的状态切换。

内容的提问来源于stack exchange,提问作者Taki

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 01:42:54