如何将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
相关产品推荐
相关产品推荐

