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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.26 10:50:05