Android中如何为FAB实现Material Design单页菜单(无需第三方库)
实现FAB转场到全屏菜单的原生方案
嘿,这个Material Design里的FAB转场到全屏菜单的效果确实很精致,不用第三方库完全可以自己实现,我来一步步给你拆解具体做法:
1. 布局准备
首先我们需要在主布局里添加两个核心元素:原有的FAB,以及一个用来承载菜单内容的容器(初始隐藏)。推荐用CoordinatorLayout作为根布局,方便FAB的定位:
<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout 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"> <!-- 主内容区域,比如RecyclerView或者其他布局 --> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <!-- 你的主内容 --> </LinearLayout> <!-- 原有的FAB --> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="16dp" android:src="@drawable/ic_add" app:backgroundTint="@color/primary" /> <!-- 菜单容器,初始隐藏 --> <FrameLayout android:id="@+id/menu_container" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone" android:background="@color/primary"> <!-- 菜单内容,比如按钮、文本等 --> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="32dp" android:gravity="center"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="菜单标题" android:textSize="24sp" android:textColor="@android:color/white"/> <!-- 更多菜单选项 --> </LinearLayout> </FrameLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
注意:菜单容器的背景色最好和FAB的背景色一致,这样转场更连贯。
2. 核心动画实现
这个转场效果分为两部分:FAB的消失动画和菜单的入场/退场动画,需要让两个动画同步执行,并且菜单的初始状态完全贴合FAB的位置和大小。
关键计算逻辑
首先要获取FAB在屏幕上的位置和尺寸,以此确定菜单的初始状态:
- FAB的中心坐标(让菜单初始中心和FAB中心对齐)
- 菜单初始的缩放比例(从FAB大小缩放到全屏)
- 菜单初始的位移(从FAB中心平移到屏幕中心)
具体代码实现(Kotlin示例)
在你的Activity/Fragment里实现以下逻辑:
import android.animation.Animator import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.animation.ValueAnimator import android.os.Bundle import android.view.View import android.widget.FrameLayout import androidx.appcompat.app.AppCompatActivity import com.google.android.material.floatingactionbutton.FloatingActionButton import android.graphics.drawable.GradientDrawable class MainActivity : AppCompatActivity() { private lateinit var fab: FloatingActionButton private lateinit var menuContainer: FrameLayout private var isMenuOpen = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) fab = findViewById(R.id.fab) menuContainer = findViewById(R.id.menu_container) // FAB点击触发菜单切换 fab.setOnClickListener { toggleMenu() } // 点击菜单空白区域关闭菜单 menuContainer.setOnClickListener { toggleMenu() } } private fun toggleMenu() { if (isMenuOpen) { closeMenu() } else { openMenu() } isMenuOpen = !isMenuOpen } private fun openMenu() { // 获取FAB的屏幕坐标和尺寸 val fabLocation = IntArray(2) fab.getLocationOnScreen(fabLocation) val fabX = fabLocation[0].toFloat() val fabY = fabLocation[1].toFloat() val fabWidth = fab.width.toFloat() val fabHeight = fab.height.toFloat() // 计算中心坐标 val fabCenterX = fabX + fabWidth / 2 val fabCenterY = fabY + fabHeight / 2 val screenWidth = resources.displayMetrics.widthPixels.toFloat() val screenHeight = resources.displayMetrics.heightPixels.toFloat() val screenCenterX = screenWidth / 2 val screenCenterY = screenHeight / 2 // 菜单初始状态:和FAB中心对齐,大小为FAB的尺寸 val translationX = fabCenterX - screenCenterX val translationY = fabCenterY - screenCenterY val scaleX = fabWidth / screenWidth val scaleY = fabHeight / screenHeight // 设置菜单初始属性 menuContainer.visibility = View.VISIBLE menuContainer.scaleX = scaleX menuContainer.scaleY = scaleY menuContainer.translationX = translationX menuContainer.translationY = translationY menuContainer.alpha = 0f // 获取菜单背景的Drawable,用来实现圆角动画(从圆形到矩形) val menuBackground = menuContainer.background as GradientDrawable val initialCornerRadius = fabHeight / 2 // FAB的圆角半径是高度的一半 // 菜单入场动画 val menuAnimator = ValueAnimator.ofFloat(0f, 1f) menuAnimator.duration = 300 // 符合Material Design的动画时长 menuAnimator.addUpdateListener { animation -> val progress = animation.animatedValue as Float // 缩放从FAB大小到全屏 menuContainer.scaleX = scaleX + (1f - scaleX) * progress menuContainer.scaleY = scaleY + (1f - scaleY) * progress // 位移从FAB中心到屏幕中心 menuContainer.translationX = translationX * (1f - progress) menuContainer.translationY = translationY * (1f - progress) // 透明度渐变 menuContainer.alpha = progress // 圆角从FAB半径变为0(矩形) menuBackground.cornerRadius = initialCornerRadius * (1f - progress) } // FAB消失动画:缩放+淡出 val fabAnimator = ObjectAnimator.ofPropertyValuesHolder( fab, ObjectAnimator.ofFloat(View.SCALE_X, 1f, 0f), ObjectAnimator.ofFloat(View.SCALE_Y, 1f, 0f), ObjectAnimator.ofFloat(View.ALPHA, 1f, 0f) ) fabAnimator.duration = 200 // 同步执行两个动画 val animatorSet = AnimatorSet() animatorSet.playTogether(menuAnimator, fabAnimator) animatorSet.start() } private fun closeMenu() { val fabLocation = IntArray(2) fab.getLocationOnScreen(fabLocation) val fabX = fabLocation[0].toFloat() val fabY = fabLocation[1].toFloat() val fabWidth = fab.width.toFloat() val fabHeight = fab.height.toFloat() val fabCenterX = fabX + fabWidth / 2 val fabCenterY = fabY + fabHeight / 2 val screenWidth = resources.displayMetrics.widthPixels.toFloat() val screenHeight = resources.displayMetrics.heightPixels.toFloat() val screenCenterX = screenWidth / 2 val screenCenterY = screenHeight / 2 val translationX = fabCenterX - screenCenterX val translationY = fabCenterY - screenCenterY val scaleX = fabWidth / screenWidth val scaleY = fabHeight / screenHeight val menuBackground = menuContainer.background as GradientDrawable val initialCornerRadius = fabHeight / 2 // 菜单退场动画:反向执行入场动画 val menuAnimator = ValueAnimator.ofFloat(1f, 0f) menuAnimator.duration = 300 menuAnimator.addUpdateListener { animation -> val progress = animation.animatedValue as Float menuContainer.scaleX = scaleX + (1f - scaleX) * progress menuContainer.scaleY = scaleY + (1f - scaleY) * progress menuContainer.translationX = translationX * (1f - progress) menuContainer.translationY = translationY * (1f - progress) menuContainer.alpha = progress menuBackground.cornerRadius = initialCornerRadius * (1f - progress) } // 动画结束后隐藏菜单 menuAnimator.addListener(object : Animator.AnimatorListener { override fun onAnimationEnd(animation: Animator?) { menuContainer.visibility = View.GONE } override fun onAnimationStart(animation: Animator?) {} override fun onAnimationCancel(animation: Animator?) {} override fun onAnimationRepeat(animation: Animator?) {} }) // FAB出现动画:反向执行消失动画 val fabAnimator = ObjectAnimator.ofPropertyValuesHolder( fab, ObjectAnimator.ofFloat(View.SCALE_X, 0f, 1f), ObjectAnimator.ofFloat(View.SCALE_Y, 0f, 1f), ObjectAnimator.ofFloat(View.ALPHA, 0f, 1f) ) fabAnimator.duration = 200 val animatorSet = AnimatorSet() animatorSet.playTogether(menuAnimator, fabAnimator) animatorSet.start() } // 处理返回键关闭菜单 override fun onBackPressed() { if (isMenuOpen) { closeMenu() isMenuOpen = false } else { super.onBackPressed() } } }
3. 细节优化
- 圆角动画:通过修改菜单背景的圆角半径,实现从FAB圆形到全屏矩形的平滑过渡,完全贴合Material Design的效果。
- 动画时长:设置为200-300ms,符合Material Design的动效规范,保证流畅自然。
- 触摸拦截:菜单容器的点击事件用来关闭菜单,避免用户误触主内容。
- 状态保存:如果需要处理屏幕旋转,可以在
onSaveInstanceState里保存isMenuOpen的状态,恢复时重新同步布局和动画状态。
这样实现的效果和你想要的完全一致,而且全程使用Android原生API,不需要任何第三方库。
内容的提问来源于stack exchange,提问作者BakaOne
相关产品推荐
相关产品推荐

