feat(flutter): 应用浅色暖橙视觉主题
This commit is contained in:
1 parent
0d2367431d
commit
634b63e16c
56 files changed
+941
-409
No files matched your search
@@ -2,79 +2,102 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Obsidian Flare palette from `docs/design/README.md` and screens 01/02.
|
||||
/// K28-S 浅色暖橙主题色板。
|
||||
///
|
||||
/// Existing semantic names (`blue`, `blueSoft`, `ink`, ...) are kept so page
|
||||
/// code does not need a mass rename; their values now follow the design tokens.
|
||||
/// 保留既有语义名,避免页面大范围改名;实际颜色统一由本文件控制。
|
||||
abstract final class AppColors {
|
||||
/// Primary CTA, selected navigation, playback progress.
|
||||
static const blue = Color(0xFFFF5B3D);
|
||||
/// 主操作、选中态、播放进度。
|
||||
static const blue = Color(0xFFFF7A1A);
|
||||
static const primary = blue;
|
||||
|
||||
/// Pressed state for the primary CTA.
|
||||
static const bluePressed = Color(0xFFFF4321);
|
||||
/// 主操作按下态。
|
||||
static const bluePressed = Color(0xFFFF6A00);
|
||||
|
||||
/// Secondary accent for tags, locks, and highlighted icons.
|
||||
static const blueSoft = Color(0xFF9278EA);
|
||||
/// 粉色辅助强调色。
|
||||
static const blueSoft = Color(0xFFFF6A8A);
|
||||
static const secondary = blueSoft;
|
||||
static const lavender = blueSoft;
|
||||
|
||||
/// Coins and rewards.
|
||||
static const gold = Color(0xFFFBBF24);
|
||||
/// 金币与奖励。
|
||||
static const gold = Color(0xFFF3B34C);
|
||||
|
||||
/// Success and free-state accents.
|
||||
static const success = Color(0xFF34D399);
|
||||
/// 成功状态。
|
||||
static const success = Color(0xFF2FA873);
|
||||
|
||||
/// Liked hearts use the primary accent.
|
||||
static const favorite = blue;
|
||||
/// 收藏和喜欢使用粉色强调。
|
||||
static const favorite = secondary;
|
||||
|
||||
/// Text placed on [gold].
|
||||
static const onGold = Color(0xFF2A1810);
|
||||
/// 金币底色上的文字。
|
||||
static const onGold = Color(0xFF5A3511);
|
||||
|
||||
/// Text and icons placed directly on [blue].
|
||||
/// 主色上的文字与图标。
|
||||
static const onPrimary = Color(0xFFFFFFFF);
|
||||
|
||||
/// Page background.
|
||||
static const ink = Color(0xFF0B0C10);
|
||||
/// 页面背景。
|
||||
static const ink = Color(0xFFFFF8F0);
|
||||
|
||||
/// Bottom navigation background (rendered at 95% with blur).
|
||||
static const bar = Color(0xFF0D0D11);
|
||||
/// 页面顶部暖色渐变起点。
|
||||
static const surfaceTop = Color(0xFFFFE9D6);
|
||||
|
||||
/// Cards lifted off [ink].
|
||||
static const card = Color(0xFF1B1C21);
|
||||
/// 底部导航底色。
|
||||
static const bar = Color(0xFFFFFDFB);
|
||||
|
||||
/// Alternate card surface from the mobile preview.
|
||||
static const cardAlt = Color(0xFF1C1D21);
|
||||
/// 中央操作按钮渐变。
|
||||
static const navCenterStart = Color(0xFFFF9A3D);
|
||||
static const navCenterEnd = Color(0xFFFF6A00);
|
||||
static const navCenterShadow = Color(0x33FF7A1A);
|
||||
|
||||
/// Floating surfaces, inputs, and nested rows.
|
||||
static const raised = Color(0xFF1C1D21);
|
||||
static const quick = Color(0xFF202126);
|
||||
static const input = Color(0xFF25252A);
|
||||
static const goldCard = Color(0xFF201B1E);
|
||||
/// 卡片轻阴影。
|
||||
static const cardShadow = Color(0x14FF8A3D);
|
||||
|
||||
/// Body text on dark surfaces.
|
||||
static const cream = Color(0xFFE6E1E3);
|
||||
/// 卡片与弹层。
|
||||
static const card = Color(0xFFFFFFFF);
|
||||
|
||||
/// Titles and high-emphasis text.
|
||||
static const onSurface = Color(0xFFFFFFFF);
|
||||
/// 次要卡片表面。
|
||||
static const cardAlt = Color(0xFFFFF4E9);
|
||||
|
||||
/// Secondary text.
|
||||
static const muted = Color(0xFFD2CDD0);
|
||||
/// 浮层、输入框和嵌套区域。
|
||||
static const raised = Color(0xFFFFF1E5);
|
||||
static const quick = Color(0xFFFFF0E1);
|
||||
static const input = Color(0xFFFFF4EA);
|
||||
static const goldCard = Color(0xFFFFF4DF);
|
||||
|
||||
/// Weak hints and timestamps.
|
||||
static const subtle = Color(0xFF9A96A3);
|
||||
/// 正文颜色。
|
||||
static const cream = Color(0xFF1F1B16);
|
||||
|
||||
/// Soft orange used for continue labels and secondary times.
|
||||
static const accent = Color(0xFFFFA490);
|
||||
/// 主文字。
|
||||
static const onSurface = Color(0xFF1F1B16);
|
||||
|
||||
/// 8% white border used by cards and inputs.
|
||||
static const border = Color(0x14FFFFFF);
|
||||
/// 次要文字。
|
||||
static const muted = Color(0xFF8C837B);
|
||||
|
||||
/// 14% white border used by glass controls.
|
||||
static const glassBorder = Color(0x24FFFFFF);
|
||||
/// 弱提示与时间。
|
||||
static const subtle = Color(0xFFB3AAA1);
|
||||
|
||||
static const outlineDark = Color(0xFF2A2C36);
|
||||
static const outlineVariantDark = Color(0xFF242329);
|
||||
static const outlineLight = Color(0xFF8E8E93);
|
||||
static const outlineVariantLight = Color(0xFFE4E4E7);
|
||||
/// 橙色强调文案。
|
||||
static const accent = blue;
|
||||
|
||||
/// 卡片描边。
|
||||
static const border = Color(0xFFF0E4D6);
|
||||
|
||||
/// 玻璃面板描边。
|
||||
static const glassBorder = Color(0x66FFFFFF);
|
||||
|
||||
/// 粉色角标。
|
||||
static const badgePink = Color(0xFFFFE3EC);
|
||||
static const badgePinkText = Color(0xFFFF6A8A);
|
||||
|
||||
/// 封面锁定数量与视频遮罩。
|
||||
static const overlayDark = Color(0x33000000);
|
||||
|
||||
/// 封面集数胶囊。
|
||||
static const badgeBeige = Color(0xFFFFF0DF);
|
||||
|
||||
/// 导航未选中。
|
||||
static const navUnselected = muted;
|
||||
|
||||
static const outlineDark = border;
|
||||
static const outlineVariantDark = border;
|
||||
static const outlineLight = muted;
|
||||
static const outlineVariantLight = border;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
/// Cross-page spacing, type, radius, and control sizes.
|
||||
/// 跨页面复用的间距、字号、圆角和控件尺寸。
|
||||
abstract final class AppDimens {
|
||||
static const double spaceXs = 4;
|
||||
static const double spaceSm = 8;
|
||||
@@ -9,17 +9,18 @@ abstract final class AppDimens {
|
||||
static const double spaceXl = 24;
|
||||
|
||||
static const double pageHorizontal = 16;
|
||||
static const double cardRadius = 12;
|
||||
static const double cardRadius = 16;
|
||||
static const double authCardRadius = 20;
|
||||
static const double cardPadding = 16;
|
||||
static const double innerRadius = 10;
|
||||
static const double innerRadius = 12;
|
||||
static const double innerPadding = 12;
|
||||
static const double pillRadius = 20;
|
||||
static const double glassSmallRadius = 12;
|
||||
static const double glassRadius = 14;
|
||||
static const double glassCardRadius = 14;
|
||||
static const double buttonRadius = 9;
|
||||
static const double coverRadius = 10;
|
||||
static const double pillRadius = 999;
|
||||
static const double glassSmallRadius = 16;
|
||||
static const double glassRadius = 20;
|
||||
static const double glassCardRadius = 20;
|
||||
static const double buttonRadius = 999;
|
||||
static const double coverRadius = 16;
|
||||
static const double sheetRadius = 20;
|
||||
|
||||
static const double caption = 12;
|
||||
static const double emptyStateSubtitle = 12.5;
|
||||
@@ -33,7 +34,7 @@ abstract final class AppDimens {
|
||||
static const double previewCardTitle = 13;
|
||||
static const double previewSectionTitle = 14;
|
||||
static const double chipFont = 10;
|
||||
static const double navLabel = 9;
|
||||
static const double navLabel = 10;
|
||||
|
||||
static const double buttonHeight = 48;
|
||||
static const double primaryButtonHeight = 52;
|
||||
@@ -44,8 +45,10 @@ abstract final class AppDimens {
|
||||
static const double tapTarget = 44;
|
||||
static const double contentHeaderHeight = 46;
|
||||
|
||||
static const double navHeight = 54;
|
||||
static const double navIcon = 17;
|
||||
static const double navHeight = 64;
|
||||
static const double navIcon = 20;
|
||||
static const double navCenter = 56;
|
||||
static const double navCenterIcon = 28;
|
||||
static const double progressHeight = 3;
|
||||
static const double progressRadius = 2;
|
||||
static const double progressLineHeight = 3;
|
||||
@@ -60,7 +63,11 @@ abstract final class AppDimens {
|
||||
|
||||
static const double watchPlaceholderHeight = 114;
|
||||
static const double heroBannerHeight = 176;
|
||||
static const double heroBannerRadius = 20;
|
||||
|
||||
static const double cardMeta = 9;
|
||||
static const double cardViews = 9;
|
||||
static const double cardMeta = 12;
|
||||
static const double cardViews = 11;
|
||||
static const double cardTitle = 15;
|
||||
static const double cardShadowBlur = 18;
|
||||
static const double cardShadowY = 6;
|
||||
}
|
||||
@@ -6,79 +6,62 @@ import 'package:flutter/services.dart';
|
||||
import 'app_colors.dart';
|
||||
import 'app_dimens.dart';
|
||||
|
||||
/// Obsidian Flare themes. Dark is the product default; light mirrors the same
|
||||
/// orange primary for system-light devices.
|
||||
/// K28-S 浅色暖橙主题。
|
||||
abstract final class AppTheme {
|
||||
static const _lightBackground = Color(0xFFF7F7F8);
|
||||
static const _darkBackground = AppColors.ink;
|
||||
|
||||
/// Light theme for system light mode.
|
||||
static ThemeData light() => _build(
|
||||
brightness: Brightness.light,
|
||||
surface: _lightBackground,
|
||||
accent: AppColors.blue,
|
||||
/// 页面顶部暖色到米白背景的渐变。
|
||||
static const LinearGradient pageGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [AppColors.surfaceTop, AppColors.ink],
|
||||
stops: [0, 0.24],
|
||||
);
|
||||
|
||||
/// Dark theme for system dark mode.
|
||||
static ThemeData dark() => _build(
|
||||
brightness: Brightness.dark,
|
||||
surface: _darkBackground,
|
||||
accent: AppColors.blue,
|
||||
/// 卡片轻阴影。
|
||||
static const BoxShadow cardShadow = BoxShadow(
|
||||
color: AppColors.cardShadow,
|
||||
blurRadius: AppDimens.cardShadowBlur,
|
||||
offset: Offset(0, AppDimens.cardShadowY),
|
||||
);
|
||||
|
||||
static ThemeData _build({
|
||||
required Brightness brightness,
|
||||
required Color surface,
|
||||
required Color accent,
|
||||
}) {
|
||||
final dark = brightness == Brightness.dark;
|
||||
final scheme = dark
|
||||
? const ColorScheme.dark(
|
||||
primary: AppColors.blue,
|
||||
onPrimary: AppColors.onPrimary,
|
||||
secondary: AppColors.secondary,
|
||||
onSecondary: AppColors.onPrimary,
|
||||
primaryContainer: AppColors.bluePressed,
|
||||
onPrimaryContainer: AppColors.onPrimary,
|
||||
secondaryContainer: AppColors.raised,
|
||||
onSecondaryContainer: AppColors.onPrimary,
|
||||
surface: _darkBackground,
|
||||
onSurface: AppColors.onSurface,
|
||||
outline: AppColors.outlineDark,
|
||||
outlineVariant: AppColors.outlineVariantDark,
|
||||
surfaceTint: AppColors.blue,
|
||||
inversePrimary: AppColors.blueSoft,
|
||||
)
|
||||
: ColorScheme.light(
|
||||
primary: AppColors.blue,
|
||||
onPrimary: AppColors.onPrimary,
|
||||
secondary: AppColors.secondary,
|
||||
onSecondary: AppColors.onPrimary,
|
||||
primaryContainer: AppColors.blue.withValues(alpha: 0.14),
|
||||
onPrimaryContainer: AppColors.ink,
|
||||
secondaryContainer: AppColors.blueSoft.withValues(alpha: 0.14),
|
||||
onSecondaryContainer: AppColors.ink,
|
||||
surface: _lightBackground,
|
||||
onSurface: AppColors.ink,
|
||||
outline: AppColors.outlineLight,
|
||||
outlineVariant: AppColors.outlineVariantLight,
|
||||
surfaceTint: AppColors.blue,
|
||||
inversePrimary: AppColors.blueSoft,
|
||||
);
|
||||
/// 全局浅色暖橙主题。
|
||||
static ThemeData light() {
|
||||
const scheme = ColorScheme.light(
|
||||
primary: AppColors.blue,
|
||||
onPrimary: AppColors.onPrimary,
|
||||
secondary: AppColors.secondary,
|
||||
onSecondary: AppColors.onPrimary,
|
||||
primaryContainer: AppColors.surfaceTop,
|
||||
onPrimaryContainer: AppColors.onSurface,
|
||||
secondaryContainer: AppColors.badgePink,
|
||||
onSecondaryContainer: AppColors.badgePinkText,
|
||||
surface: AppColors.ink,
|
||||
onSurface: AppColors.onSurface,
|
||||
outline: AppColors.border,
|
||||
outlineVariant: AppColors.border,
|
||||
surfaceTint: AppColors.blue,
|
||||
inversePrimary: AppColors.bluePressed,
|
||||
);
|
||||
final base = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: brightness,
|
||||
brightness: Brightness.light,
|
||||
colorScheme: scheme,
|
||||
scaffoldBackgroundColor: surface,
|
||||
scaffoldBackgroundColor: AppColors.ink,
|
||||
fontFamily: 'PlusJakartaSans',
|
||||
progressIndicatorTheme: ProgressIndicatorThemeData(color: accent),
|
||||
sliderTheme: SliderThemeData(
|
||||
activeTrackColor: accent,
|
||||
progressIndicatorTheme: const ProgressIndicatorThemeData(
|
||||
color: AppColors.blue,
|
||||
),
|
||||
sliderTheme: const SliderThemeData(
|
||||
activeTrackColor: AppColors.blue,
|
||||
inactiveTrackColor: AppColors.border,
|
||||
thumbColor: AppColors.onPrimary,
|
||||
),
|
||||
tabBarTheme: TabBarThemeData(labelColor: accent, indicatorColor: accent),
|
||||
tabBarTheme: const TabBarThemeData(
|
||||
labelColor: AppColors.blue,
|
||||
indicatorColor: AppColors.blue,
|
||||
unselectedLabelColor: AppColors.muted,
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(foregroundColor: accent),
|
||||
style: TextButton.styleFrom(foregroundColor: AppColors.blue),
|
||||
),
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
@@ -87,25 +70,26 @@ abstract final class AppTheme {
|
||||
minimumSize: const WidgetStatePropertyAll(
|
||||
Size(0, AppDimens.buttonHeight),
|
||||
),
|
||||
shape: const WidgetStatePropertyAll(StadiumBorder()),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: WidgetStatePropertyAll(
|
||||
dark ? AppColors.blueSoft : AppColors.bluePressed,
|
||||
),
|
||||
foregroundColor: const WidgetStatePropertyAll(AppColors.blue),
|
||||
minimumSize: const WidgetStatePropertyAll(
|
||||
Size(0, AppDimens.buttonHeight),
|
||||
),
|
||||
side: WidgetStatePropertyAll(
|
||||
BorderSide(
|
||||
color: dark ? AppColors.outlineDark : AppColors.outlineLight,
|
||||
),
|
||||
side: const WidgetStatePropertyAll(
|
||||
BorderSide(color: AppColors.border),
|
||||
),
|
||||
shape: const WidgetStatePropertyAll(StadiumBorder()),
|
||||
),
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||
backgroundColor: AppColors.ink,
|
||||
foregroundColor: AppColors.onSurface,
|
||||
elevation: 0,
|
||||
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
||||
titleTextStyle: TextStyle(
|
||||
color: AppColors.onSurface,
|
||||
fontFamily: 'Outfit',
|
||||
@@ -113,6 +97,16 @@ abstract final class AppTheme {
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
iconTheme: const IconThemeData(color: AppColors.onSurface),
|
||||
dividerTheme: const DividerThemeData(color: AppColors.border),
|
||||
cardTheme: const CardThemeData(
|
||||
color: AppColors.card,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(AppDimens.cardRadius)),
|
||||
side: BorderSide(color: AppColors.border),
|
||||
),
|
||||
),
|
||||
switchTheme: SwitchThemeData(
|
||||
thumbColor: WidgetStateProperty.resolveWith(
|
||||
(states) => states.contains(WidgetState.selected)
|
||||
@@ -120,36 +114,35 @@ abstract final class AppTheme {
|
||||
: null,
|
||||
),
|
||||
trackColor: WidgetStateProperty.resolveWith(
|
||||
(states) => states.contains(WidgetState.selected) ? accent : null,
|
||||
(states) =>
|
||||
states.contains(WidgetState.selected) ? AppColors.blue : null,
|
||||
),
|
||||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
backgroundColor: AppColors.bluePressed,
|
||||
contentTextStyle: const TextStyle(color: AppColors.onPrimary),
|
||||
snackBarTheme: const SnackBarThemeData(
|
||||
backgroundColor: AppColors.onSurface,
|
||||
contentTextStyle: TextStyle(color: AppColors.onPrimary),
|
||||
),
|
||||
);
|
||||
final textTheme = base.textTheme
|
||||
.apply(
|
||||
fontFamily: 'PlusJakartaSans',
|
||||
bodyColor: dark ? AppColors.cream : AppColors.ink,
|
||||
displayColor: dark ? AppColors.onSurface : AppColors.ink,
|
||||
bodyColor: AppColors.cream,
|
||||
displayColor: AppColors.onSurface,
|
||||
)
|
||||
.copyWith(
|
||||
displayLarge: _display(base.textTheme.displayLarge, dark),
|
||||
displayMedium: _display(base.textTheme.displayMedium, dark),
|
||||
displaySmall: _display(base.textTheme.displaySmall, dark),
|
||||
headlineLarge: _display(base.textTheme.headlineLarge, dark),
|
||||
headlineMedium: _display(base.textTheme.headlineMedium, dark),
|
||||
headlineSmall: _display(base.textTheme.headlineSmall, dark),
|
||||
titleLarge: _display(base.textTheme.titleLarge, dark),
|
||||
displayLarge: _display(base.textTheme.displayLarge),
|
||||
displayMedium: _display(base.textTheme.displayMedium),
|
||||
displaySmall: _display(base.textTheme.displaySmall),
|
||||
headlineLarge: _display(base.textTheme.headlineLarge),
|
||||
headlineMedium: _display(base.textTheme.headlineMedium),
|
||||
headlineSmall: _display(base.textTheme.headlineSmall),
|
||||
titleLarge: _display(base.textTheme.titleLarge),
|
||||
);
|
||||
return base.copyWith(textTheme: textTheme);
|
||||
}
|
||||
|
||||
static TextStyle? _display(TextStyle? style, bool dark) => style?.copyWith(
|
||||
fontFamily: 'Outfit',
|
||||
color: dark ? AppColors.onSurface : AppColors.ink,
|
||||
);
|
||||
static TextStyle? _display(TextStyle? style) =>
|
||||
style?.copyWith(fontFamily: 'Outfit', color: AppColors.onSurface);
|
||||
|
||||
static Color _buttonColor(Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.pressed) ||
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import '../theme/app_dimens.dart';
|
||||
|
||||
/// One destination rendered by [AppBottomNav].
|
||||
/// 底部导航普通目的地。
|
||||
class AppBottomNavItem {
|
||||
const AppBottomNavItem({
|
||||
required this.tabKey,
|
||||
@@ -26,41 +26,69 @@ class AppBottomNavItem {
|
||||
final bool selected;
|
||||
}
|
||||
|
||||
/// Shared bottom navigation used by the root shell and standalone tabs.
|
||||
/// 底部导航中央操作按钮。
|
||||
class AppBottomNavCenterAction {
|
||||
const AppBottomNavCenterAction({
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
this.key = const Key('nav-center-action'),
|
||||
});
|
||||
|
||||
final Key key;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
}
|
||||
|
||||
/// 全局五槽底部导航。
|
||||
class AppBottomNav extends StatelessWidget {
|
||||
const AppBottomNav({
|
||||
super.key,
|
||||
required this.items,
|
||||
this.centerAction,
|
||||
this.backgroundColor = AppColors.bar,
|
||||
this.barKey,
|
||||
});
|
||||
|
||||
final List<AppBottomNavItem> items;
|
||||
final AppBottomNavCenterAction? centerAction;
|
||||
final Color backgroundColor;
|
||||
final Key? barKey;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Container(
|
||||
key: barKey,
|
||||
height: AppDimens.navHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor.withValues(alpha: 0.95),
|
||||
border: const Border(
|
||||
top: BorderSide(color: AppColors.outlineVariantDark),
|
||||
Widget build(BuildContext context) {
|
||||
final action = centerAction;
|
||||
return ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Container(
|
||||
key: barKey,
|
||||
height: AppDimens.navHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor.withValues(alpha: 0.95),
|
||||
border: const Border(top: BorderSide(color: AppColors.border)),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [for (final item in items) _NavDestination(item: item)],
|
||||
child: action == null
|
||||
? Row(
|
||||
children: [
|
||||
for (final item in items) _NavDestination(item: item),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
for (final item in items.take(2))
|
||||
_NavDestination(item: item),
|
||||
Expanded(child: _CenterDestination(action: action)),
|
||||
for (final item in items.skip(2))
|
||||
_NavDestination(item: item),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavDestination extends StatelessWidget {
|
||||
@@ -78,7 +106,7 @@ class _NavDestination extends StatelessWidget {
|
||||
key: item.tabKey,
|
||||
onTap: item.onTap,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 48),
|
||||
constraints: const BoxConstraints(minHeight: AppDimens.navHeight),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@@ -91,7 +119,7 @@ class _NavDestination extends StatelessWidget {
|
||||
size: AppDimens.navIcon,
|
||||
color: item.selected
|
||||
? AppColors.blue
|
||||
: Colors.white.withValues(alpha: 0.45),
|
||||
: AppColors.navUnselected,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
@@ -104,7 +132,7 @@ class _NavDestination extends StatelessWidget {
|
||||
fontWeight: item.selected ? FontWeight.w700 : FontWeight.w500,
|
||||
color: item.selected
|
||||
? AppColors.blue
|
||||
: Colors.white.withValues(alpha: 0.45),
|
||||
: AppColors.navUnselected,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
@@ -115,3 +143,53 @@ class _NavDestination extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _CenterDestination extends StatelessWidget {
|
||||
const _CenterDestination({required this.action});
|
||||
|
||||
final AppBottomNavCenterAction action;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: action.label,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
shape: const CircleBorder(),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Ink(
|
||||
width: AppDimens.navCenter,
|
||||
height: AppDimens.navCenter,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [AppColors.navCenterStart, AppColors.navCenterEnd],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.navCenterShadow,
|
||||
blurRadius: 14,
|
||||
offset: Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
key: action.key,
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: action.onTap,
|
||||
child: const Icon(
|
||||
Icons.add_rounded,
|
||||
color: AppColors.onPrimary,
|
||||
size: AppDimens.navCenterIcon,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import '../theme/app_dimens.dart';
|
||||
|
||||
/// Dark glass surface used by the home info card and floating controls.
|
||||
/// 首页信息卡与浮层控件使用的浅色玻璃面板。
|
||||
class GlassPanel extends StatelessWidget {
|
||||
const GlassPanel({
|
||||
super.key,
|
||||
@@ -38,7 +38,7 @@ class GlassPanel extends StatelessWidget {
|
||||
const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0x24FFFFFF), Color(0x0DFFFFFF)],
|
||||
colors: [Color(0xE6FFFFFF), Color(0xB3FFFDFB)],
|
||||
),
|
||||
),
|
||||
child: Padding(padding: padding ?? EdgeInsets.zero, child: child),
|
||||
|
||||
Reference in new issue
Block a user