feat(ui): 增加视觉 token 与统一导航
This commit is contained in:
1 parent
42ce1d1d84
commit
df5fbe5b08
7 files changed
+260
-1
No files matched your search
+11
-1
@@ -3,6 +3,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'core/config/locale_controller.dart';
|
import 'core/config/locale_controller.dart';
|
||||||
@@ -39,6 +40,7 @@ import 'core/data/services/playback_session.dart';
|
|||||||
import 'core/data/services/video_player_port.dart';
|
import 'core/data/services/video_player_port.dart';
|
||||||
import 'core/platform/haptics.dart';
|
import 'core/platform/haptics.dart';
|
||||||
import 'core/theme/app_theme.dart';
|
import 'core/theme/app_theme.dart';
|
||||||
|
import 'core/theme/app_colors.dart';
|
||||||
import 'l10n/app_localizations.dart';
|
import 'l10n/app_localizations.dart';
|
||||||
import 'pages/home/home_view.dart';
|
import 'pages/home/home_view.dart';
|
||||||
|
|
||||||
@@ -218,9 +220,17 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
|
|||||||
localeResolutionCallback: (deviceLocale, _) =>
|
localeResolutionCallback: (deviceLocale, _) =>
|
||||||
resolveAppLocale(locale.locale, deviceLocale),
|
resolveAppLocale(locale.locale, deviceLocale),
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
themeMode: ThemeMode.system,
|
themeMode: ThemeMode.dark,
|
||||||
theme: AppTheme.light(),
|
theme: AppTheme.light(),
|
||||||
darkTheme: AppTheme.dark(),
|
darkTheme: AppTheme.dark(),
|
||||||
|
builder: (context, child) => AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
|
value: SystemUiOverlayStyle.light.copyWith(
|
||||||
|
statusBarColor: Colors.transparent,
|
||||||
|
systemNavigationBarColor: AppColors.bar,
|
||||||
|
systemNavigationBarIconBrightness: Brightness.light,
|
||||||
|
),
|
||||||
|
child: child ?? const SizedBox.shrink(),
|
||||||
|
),
|
||||||
home: const HomePage(),
|
home: const HomePage(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ abstract final class AppColors {
|
|||||||
/// Coins, rewards, and the playback progress line.
|
/// Coins, rewards, and the playback progress line.
|
||||||
static const gold = Color(0xFFF6C453);
|
static const gold = Color(0xFFF6C453);
|
||||||
|
|
||||||
|
/// Favorite/like heart without competing with the chili primary action.
|
||||||
|
static const favorite = Color(0xFFD74A7D);
|
||||||
|
|
||||||
/// Text placed on [gold].
|
/// Text placed on [gold].
|
||||||
static const onGold = Color(0xFF2A1810);
|
static const onGold = Color(0xFF2A1810);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
/// K20 cross-page spacing, type, radius, and control sizes.
|
||||||
|
abstract final class AppDimens {
|
||||||
|
static const double spaceXs = 4;
|
||||||
|
static const double spaceSm = 8;
|
||||||
|
static const double spaceMd = 12;
|
||||||
|
static const double spaceLg = 16;
|
||||||
|
static const double spaceXl = 24;
|
||||||
|
|
||||||
|
static const double pageHorizontal = 16;
|
||||||
|
static const double cardRadius = 16;
|
||||||
|
static const double cardPadding = 16;
|
||||||
|
static const double innerRadius = 12;
|
||||||
|
static const double innerPadding = 12;
|
||||||
|
static const double pillRadius = 20;
|
||||||
|
|
||||||
|
static const double caption = 12;
|
||||||
|
static const double body = 14;
|
||||||
|
static const double listTitle = 15;
|
||||||
|
static const double sectionTitle = 16;
|
||||||
|
static const double pageTitle = 20;
|
||||||
|
static const double detailHeadline = 24;
|
||||||
|
static const double navLabel = 11;
|
||||||
|
|
||||||
|
static const double buttonHeight = 48;
|
||||||
|
static const double primaryButtonHeight = 52;
|
||||||
|
static const double tapTarget = 44;
|
||||||
|
|
||||||
|
static const double navHeight = 64;
|
||||||
|
static const double navIcon = 22;
|
||||||
|
static const double progressHeight = 4;
|
||||||
|
static const double progressRadius = 4;
|
||||||
|
|
||||||
|
static const double accountCardHeight = 64;
|
||||||
|
static const double accountIcon = 18;
|
||||||
|
static const double accountLabel = 11;
|
||||||
|
|
||||||
|
static const double watchPlaceholderHeight = 114;
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
// Author: fruit.com.cn
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'app_colors.dart';
|
import 'app_colors.dart';
|
||||||
|
import 'app_dimens.dart';
|
||||||
|
|
||||||
/// Light paper and warm-cocoa themes for the Indonesia evening palette.
|
/// Light paper and warm-cocoa themes for the Indonesia evening palette.
|
||||||
abstract final class AppTheme {
|
abstract final class AppTheme {
|
||||||
@@ -80,6 +82,32 @@ abstract final class AppTheme {
|
|||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
backgroundColor: WidgetStateProperty.resolveWith(_buttonColor),
|
backgroundColor: WidgetStateProperty.resolveWith(_buttonColor),
|
||||||
foregroundColor: const WidgetStatePropertyAll(AppColors.onPrimary),
|
foregroundColor: const WidgetStatePropertyAll(AppColors.onPrimary),
|
||||||
|
minimumSize: const WidgetStatePropertyAll(
|
||||||
|
Size(0, AppDimens.buttonHeight),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
|
style: ButtonStyle(
|
||||||
|
foregroundColor: WidgetStatePropertyAll(
|
||||||
|
dark ? AppColors.blueSoft : AppColors.bluePressed,
|
||||||
|
),
|
||||||
|
minimumSize: const WidgetStatePropertyAll(
|
||||||
|
Size(0, AppDimens.buttonHeight),
|
||||||
|
),
|
||||||
|
side: WidgetStatePropertyAll(
|
||||||
|
BorderSide(
|
||||||
|
color: dark ? AppColors.outlineDark : AppColors.outlineLight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||||
|
titleTextStyle: TextStyle(
|
||||||
|
color: AppColors.cream,
|
||||||
|
fontSize: AppDimens.pageTitle,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
switchTheme: SwitchThemeData(
|
switchTheme: SwitchThemeData(
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
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,
|
||||||
|
required this.icon,
|
||||||
|
required this.selectedIcon,
|
||||||
|
required this.label,
|
||||||
|
required this.onTap,
|
||||||
|
this.selected = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Key tabKey;
|
||||||
|
final IconData icon;
|
||||||
|
final IconData selectedIcon;
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final bool selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared bottom navigation used by all root tabs and the player fallback.
|
||||||
|
class AppBottomNav extends StatelessWidget {
|
||||||
|
const AppBottomNav({
|
||||||
|
super.key,
|
||||||
|
required this.items,
|
||||||
|
this.backgroundColor = AppColors.bar,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<AppBottomNavItem> items;
|
||||||
|
final Color backgroundColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Container(
|
||||||
|
height: AppDimens.navHeight,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor,
|
||||||
|
border: const Border(
|
||||||
|
top: BorderSide(color: AppColors.outlineVariantDark),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [for (final item in items) _NavDestination(item: item)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NavDestination extends StatelessWidget {
|
||||||
|
const _NavDestination({required this.item});
|
||||||
|
|
||||||
|
final AppBottomNavItem item;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Expanded(
|
||||||
|
child: Semantics(
|
||||||
|
button: true,
|
||||||
|
selected: item.selected,
|
||||||
|
label: item.label,
|
||||||
|
child: InkWell(
|
||||||
|
key: item.tabKey,
|
||||||
|
onTap: item.onTap,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minHeight: AppDimens.tapTarget),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 160),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 14,
|
||||||
|
vertical: 3,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: item.selected ? AppColors.blue : Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
item.selected ? item.selectedIcon : item.icon,
|
||||||
|
size: AppDimens.navIcon,
|
||||||
|
color: item.selected ? AppColors.onPrimary : AppColors.muted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
item.label,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppDimens.navLabel,
|
||||||
|
fontWeight: item.selected ? FontWeight.w700 : FontWeight.w500,
|
||||||
|
color: item.selected ? AppColors.blueSoft : AppColors.muted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:fruitshorts/core/theme/app_dimens.dart';
|
||||||
|
import 'package:fruitshorts/core/widgets/app_bottom_nav.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('should_render_64_high_navigation_and_keep_item_keys', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
bottomNavigationBar: AppBottomNav(
|
||||||
|
items: [
|
||||||
|
AppBottomNavItem(
|
||||||
|
tabKey: const Key('tab-home'),
|
||||||
|
icon: Icons.movie_outlined,
|
||||||
|
selectedIcon: Icons.movie_rounded,
|
||||||
|
label: 'Home',
|
||||||
|
selected: true,
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
AppBottomNavItem(
|
||||||
|
tabKey: const Key('tab-my'),
|
||||||
|
icon: Icons.person_outline_rounded,
|
||||||
|
selectedIcon: Icons.person_rounded,
|
||||||
|
label: 'Me',
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('tab-home')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('tab-my')), findsOneWidget);
|
||||||
|
expect(find.text('Home'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
tester.getSize(find.byType(AppBottomNav)).height,
|
||||||
|
AppDimens.navHeight,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:fruitshorts/core/theme/app_dimens.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('should_expose_the_k20_spacing_and_type_scale', () {
|
||||||
|
expect(AppDimens.pageHorizontal, 16);
|
||||||
|
expect(AppDimens.cardRadius, 16);
|
||||||
|
expect(AppDimens.innerRadius, 12);
|
||||||
|
expect(AppDimens.caption, 12);
|
||||||
|
expect(AppDimens.body, 14);
|
||||||
|
expect(AppDimens.listTitle, 15);
|
||||||
|
expect(AppDimens.sectionTitle, 16);
|
||||||
|
expect(AppDimens.pageTitle, 20);
|
||||||
|
expect(AppDimens.navLabel, 11);
|
||||||
|
expect(AppDimens.buttonHeight, 48);
|
||||||
|
expect(AppDimens.primaryButtonHeight, 52);
|
||||||
|
expect(AppDimens.tapTarget, 44);
|
||||||
|
expect(AppDimens.navHeight, 64);
|
||||||
|
expect(AppDimens.progressHeight, 4);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user