实现点击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
相关产品推荐
相关产品推荐

