diff --git a/assets/icon/film_reel.png b/assets/icon/film_reel.png new file mode 100644 index 0000000..1047c23 Binary files /dev/null and b/assets/icon/film_reel.png differ diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index 12f49a4..cc142c9 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -1150,3 +1150,9 @@ $ flutter test - `AppRootTab.home` 未选中改为 `theaters_outlined`,选中改为 `theaters`。 - 其余三个根导航图标、26 尺寸及白/灰颜色规则保持不变;测试断言同步。 - 按当前流程未执行 analyze/test。 + +## 2026-10-22 首页圆形胶片盘图标 + +- `AppRootTab` 新增可选 `assetPath`,首页使用 `assets/icon/film_reel.png`。 +- 共享导航通过 `ColorFiltered + Image.asset` 按选中态着色,尺寸保持 26,缺图回退胶片 Material 图标。 +- 推荐、追剧、我的图标不变;素材与测试断言同步,按当前流程未执行 analyze/test。 diff --git a/lib/core/widgets/app_bottom_nav.dart b/lib/core/widgets/app_bottom_nav.dart index a4163df..f9a4ec0 100644 --- a/lib/core/widgets/app_bottom_nav.dart +++ b/lib/core/widgets/app_bottom_nav.dart @@ -14,11 +14,13 @@ class AppBottomNavItem { required this.label, required this.onTap, this.selected = false, + this.assetPath, }); final Key tabKey; final IconData icon; final IconData selectedIcon; + final String? assetPath; final String label; final VoidCallback onTap; final bool selected; @@ -61,49 +63,66 @@ class _NavDestination extends StatelessWidget { final Color unselectedColor; @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.navHeight), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AnimatedScale( - duration: const Duration(milliseconds: 160), - curve: Curves.easeOut, - scale: item.selected ? 1 : 0.96, - child: Icon( - item.selected ? item.selectedIcon : item.icon, - size: AppDimens.navIcon, - color: item.selected - ? AppColors.navSelected - : unselectedColor, + Widget build(BuildContext context) { + final color = item.selected ? AppColors.navSelected : unselectedColor; + final fallbackIcon = item.selected ? item.selectedIcon : item.icon; + return 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.navHeight), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedScale( + duration: const Duration(milliseconds: 160), + curve: Curves.easeOut, + scale: item.selected ? 1 : 0.96, + child: item.assetPath == null + ? Icon( + fallbackIcon, + size: AppDimens.navIcon, + color: color, + ) + : ColorFiltered( + colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + child: Image.asset( + item.assetPath!, + width: AppDimens.navIcon, + height: AppDimens.navIcon, + fit: BoxFit.contain, + errorBuilder: (_, _, _) => Icon( + item.icon, + size: AppDimens.navIcon, + color: color, + ), + ), + ), ), - ), - const SizedBox(height: AppDimens.navLabelGap), - Text( - item.label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: AppDimens.navLabel, - fontWeight: item.selected ? FontWeight.w600 : FontWeight.w400, - color: item.selected - ? AppColors.navSelected - : unselectedColor, - letterSpacing: 0, + const SizedBox(height: AppDimens.navLabelGap), + Text( + item.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: AppDimens.navLabel, + fontWeight: item.selected + ? FontWeight.w600 + : FontWeight.w400, + color: color, + letterSpacing: 0, + ), ), - ), - ], + ], + ), ), ), ), - ), - ); + ); + } } diff --git a/lib/core/widgets/app_root_tab.dart b/lib/core/widgets/app_root_tab.dart index dec8033..c0e9d2d 100644 --- a/lib/core/widgets/app_root_tab.dart +++ b/lib/core/widgets/app_root_tab.dart @@ -6,7 +6,16 @@ import '../../l10n/app_localizations.dart'; import 'app_bottom_nav.dart'; /// 客户端根导航的固定顺序。 -enum AppRootTab { home, recommend, following, mine } +enum AppRootTab { + home(assetPath: 'assets/icon/film_reel.png'), + recommend(), + following(), + mine(); + + const AppRootTab({this.assetPath}); + + final String? assetPath; +} extension AppRootTabUi on AppRootTab { Key get tabKey => switch (this) { @@ -62,6 +71,7 @@ class AppRootBottomNav extends StatelessWidget { tabKey: tab.tabKey, icon: tab.icon, selectedIcon: tab.selectedIcon, + assetPath: tab.assetPath, label: tab.label(l10n), selected: tab == selected, onTap: () => onSelected(tab), diff --git a/test/home/app_bottom_nav_test.dart b/test/home/app_bottom_nav_test.dart index 0b04e00..37a44be 100644 --- a/test/home/app_bottom_nav_test.dart +++ b/test/home/app_bottom_nav_test.dart @@ -179,4 +179,29 @@ void main() { findsWidgets, ); }); + + testWidgets('should_fallback_when_asset_icon_is_missing', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + bottomNavigationBar: AppBottomNav( + items: [ + AppBottomNavItem( + tabKey: const Key('tab-home'), + icon: Icons.theaters_outlined, + selectedIcon: Icons.theaters, + assetPath: 'assets/icon/missing_film_reel.png', + label: 'Home', + selected: true, + onTap: () {}, + ), + ], + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.theaters_outlined), findsOneWidget); + }); } diff --git a/test/home/home_nav_ui_test.dart b/test/home/home_nav_ui_test.dart index e831fc3..1214bb8 100644 --- a/test/home/home_nav_ui_test.dart +++ b/test/home/home_nav_ui_test.dart @@ -191,7 +191,7 @@ void main() { expect( find.descendant( of: find.byKey(const Key('tab-home')), - matching: find.byIcon(Icons.theaters), + matching: find.byType(Image), ), findsOneWidget, ); @@ -218,14 +218,28 @@ void main() { ); expect( tester - .widget( + .widget( find.descendant( of: find.byKey(const Key('tab-home')), - matching: find.byIcon(Icons.theaters), + matching: find.byType(ColorFiltered), ), ) - .color, - AppColors.navSelected, + .colorFilter, + const ColorFilter.mode(AppColors.navSelected, BlendMode.srcIn), + ); + final selectedHomeImage = tester.widget( + find.descendant( + of: find.byKey(const Key('tab-home')), + matching: find.byType(Image), + ), + ); + expect( + (selectedHomeImage.image as AssetImage).assetName, + 'assets/icon/film_reel.png', + ); + expect( + Size(selectedHomeImage.width!, selectedHomeImage.height!), + const Size.square(AppDimens.navIcon), ); await tester.tap(find.byKey(const Key('tab-recommend'))); @@ -271,10 +285,21 @@ void main() { expect( find.descendant( of: find.byKey(const Key('tab-home')), - matching: find.byIcon(Icons.theaters_outlined), + matching: find.byType(Image), ), findsOneWidget, ); + expect( + tester + .widget( + find.descendant( + of: find.byKey(const Key('tab-home')), + matching: find.byType(ColorFiltered), + ), + ) + .colorFilter, + const ColorFilter.mode(AppColors.navUnselected, BlendMode.srcIn), + ); }); testWidgets('should_map_four_tabs_to_home_recommend_following_and_me', (