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

如何将Figma设计中的阴影效果完美匹配到Flutter实现中?

问题描述

我在Figma中有一个底部导航栏组件(对应核心CSS效果如下),但多次尝试后都无法在Flutter中完美复刻阴影与毛玻璃效果。

Figma核心样式参考

/* 容器阴影 */
box-shadow: 4px 4px 20px rgba(12, 0, 56, 0.48);
/* 毛玻璃效果 */
backdrop-filter: blur(32px);
/* 容器圆角 */
border-radius: 30.2573px;

当前Flutter实现代码

import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart' show SvgPicture;

class NavigationState with ChangeNotifier {
  int _currentIndex = 0;

  int get currentIndex => _currentIndex;

  void navigateTo(int index) {
    _currentIndex = index;
    notifyListeners();
  }
}

class NavBarPage extends StatelessWidget {
  final int index;
  final Function(int) onItemSelected;

  final Color shadowColor = const Color(0xFF0C0038);

  const NavBarPage({super.key, required this.index, required this.onItemSelected});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return RepaintBoundary(
      child: Padding(
        padding: const EdgeInsets.fromLTRB(0, 0, 0, 20),
        child: BottomAppBar(
          color: Colors.transparent,
          elevation: 0,
          child: Container(
            padding: const EdgeInsets.all(4),
            decoration: BoxDecoration(
              color: Colors.white.withOpacity(0.08),
              border: Border.all(color: Colors.white.withOpacity(0.07)),
              borderRadius: BorderRadius.circular(30),
              boxShadow: [
                BoxShadow(
                  color: shadowColor.withOpacity(0.48),
                  blurRadius: 20,
                  spreadRadius: 0,
                  offset: const Offset(4, 4),
                  blurStyle: BlurStyle.normal,
                ),
              ],
            ),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: List.generate(
                  5,
                  (buttonIndex) => NavButton(
                        iconPath: getIconPathIndex(buttonIndex),
                        isActive: index == buttonIndex,
                        onTap: () => onItemSelected(buttonIndex),
                      )),
            ),
          ),
        ),
      ),
    );
  }

  String getIconPathIndex(int index) {
    switch (index) {
      case 0:
        return 'assets/icons/home.svg';
      case 1:
        return 'assets/icons/map.svg';
      case 2:
        return 'assets/icons/chat.svg';
      case 3:
        return 'assets/icons/calendar.svg';
      case 4:
        return 'assets/icons/profile.svg';
      default:
        return '';
    }
  }
}

class NavButton extends StatelessWidget {
  final String iconPath;
  final bool isActive;
  final VoidCallback onTap;

  const NavButton({super.key, required this.iconPath, this.isActive = false, required this.onTap});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    const double buttonSize = 52;

    final Color primaryColor = theme.colorScheme.primary;
    final Color secondaryColor = theme.colorScheme.secondary;
    final bool isLightTheme = theme.brightness == Brightness.light;

    Color nonActiveColor = isLightTheme
        ? theme.colorScheme.onSurface 
        : Colors.white;
    return RepaintBoundary(
      child: GestureDetector(
        onTap: onTap,
        child: Container(
          width: buttonSize,
          height: buttonSize,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            gradient: isActive
                ? LinearGradient(
                    colors: [secondaryColor, primaryColor],
                    begin: Alignment.topLeft,
                    end: Alignment.bottomRight,
                  )
                : null,
            color: isActive ? null : Colors.transparent,
          ),
          child: Center(
              child: SvgPicture.asset(
            iconPath,
            colorFilter: isActive
                ? const ColorFilter.mode(Colors.white, BlendMode.srcIn)
                : ColorFilter.mode(nonActiveColor.withOpacity(0.38), BlendMode.srcIn),
            width: 24,
            height: 24,
          )),
        ),
      ),
    );
  }
}

解决方案

要完美匹配Figma设计,需调整以下4个关键细节:

1. 修正阴影模糊样式

Flutter默认的BlurStyle.normal会让阴影向组件内部扩散,和Figma的外部偏移阴影不符,改用BlurStyle.outer:

boxShadow: [
  BoxShadow(
    color: const Color(0xFF0C0038).withOpacity(0.48),
    offset: const Offset(4, 4), // 严格匹配CSS的4px偏移
    blurRadius: 20,
    spreadRadius: 0,
    blurStyle: BlurStyle.outer, // 仅在组件外部生成模糊阴影
  ),
],

2. 添加毛玻璃效果

用Flutter的BackdropFilter实现CSS中的backdrop-filter: blur(32px),将容器包裹在该组件内:

child: BackdropFilter(
  filter: const ImageFilter.blur(sigmaX: 32, sigmaY: 32), // 32px模糊度匹配设计
  child: Container(
    // 原容器属性...
  ),
),

3. 精确匹配圆角数值

Figma中容器圆角为30.2573px,替换原代码中的整数30:

borderRadius: BorderRadius.circular(30.2573),

4. 对齐激活按钮渐变方向

Figma中激活按钮的渐变是271.66deg(近似从右上到左下),修正渐变方向:

gradient: isActive
    ? const LinearGradient(
        colors: [Color(0xFF2E1EEA), Color(0xFF3A8FF2)], // 直接用设计中的色值
        begin: Alignment.topRight,
        end: Alignment.bottomLeft,
      )
    : null,

最终完整代码

import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart' show SvgPicture;

class NavigationState with ChangeNotifier {
  int _currentIndex = 0;

  int get currentIndex => _currentIndex;

  void navigateTo(int index) {
    _currentIndex = index;
    notifyListeners();
  }
}

class NavBarPage extends StatelessWidget {
  final int index;
  final Function(int) onItemSelected;

  const NavBarPage({super.key, required this.index, required this.onItemSelected});

  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      child: Padding(
        padding: const EdgeInsets.fromLTRB(0, 0, 0, 20),
        child: BottomAppBar(
          color: Colors.transparent,
          elevation: 0,
          child: BackdropFilter(
            filter: const ImageFilter.blur(sigmaX: 32, sigmaY: 32),
            child: Container(
              padding: const EdgeInsets.all(4),
              decoration: BoxDecoration(
                color: Colors.white.withOpacity(0.08),
                border: Border.all(color: Colors.white.withOpacity(0.07)),
                borderRadius: BorderRadius.circular(30.2573),
                boxShadow: [
                  BoxShadow(
                    color: const Color(0xFF0C0038).withOpacity(0.48),
                    offset: const Offset(4, 4),
                    blurRadius: 20,
                    spreadRadius: 0,
                    blurStyle: BlurStyle.outer,
                  ),
                ],
              ),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: List.generate(
                    5,
                    (buttonIndex) => NavButton(
                          iconPath: getIconPathIndex(buttonIndex),
                          isActive: index == buttonIndex,
                          onTap: () => onItemSelected(buttonIndex),
                        )),
              ),
            ),
          ),
        ),
      ),
    );
  }

  String getIconPathIndex(int index) {
    switch (index) {
      case 0:
        return 'assets/icons/home.svg';
      case 1:
        return 'assets/icons/map.svg';
      case 2:
        return 'assets/icons/chat.svg';
      case 3:
        return 'assets/icons/calendar.svg';
      case 4:
        return 'assets/icons/profile.svg';
      default:
        return '';
    }
  }
}

class NavButton extends StatelessWidget {
  final String iconPath;
  final bool isActive;
  final VoidCallback onTap;

  const NavButton({super.key, required this.iconPath, this.isActive = false, required this.onTap});

  @override
  Widget build(BuildContext context) {
    const double buttonSize = 52;

    return RepaintBoundary(
      child: GestureDetector(
        onTap: onTap,
        child: Container(
          width: buttonSize,
          height: buttonSize,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            gradient: isActive
                ? const LinearGradient(
                    colors: [Color(0xFF2E1EEA), Color(0xFF3A8FF2)],
                    begin: Alignment.topRight,
                    end: Alignment.bottomLeft,
                  )
                : null,
            color: isActive ? null : Colors.transparent,
          ),
          child: Center(
              child: SvgPicture.asset(
            iconPath,
            colorFilter: isActive
                ? const ColorFilter.mode(Colors.white, BlendMode.srcIn)
                : const ColorFilter.mode(Color.fromRGBO(30, 14, 95, 0.38), BlendMode.srcIn),
            width: 24,
            height: 24,
          )),
        ),
      ),
    );
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 16:54:53