From f47e4dd2980df48e42c890dd1eb846ee9044643a Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Thu, 22 Oct 2026 19:24:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(home):=20=E4=BD=BF=E7=94=A8=E8=83=B6?= =?UTF-8?q?=E7=89=87=E7=9B=98=E5=9B=BE=E7=89=87=E5=AF=BC=E8=88=AA=E5=9B=BE?= =?UTF-8?q?=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/icon/film_reel.png | Bin 0 -> 7666 bytes docs/plans/PROGRESS.md | 6 ++ lib/core/widgets/app_bottom_nav.dart | 99 ++++++++++++++++----------- lib/core/widgets/app_root_tab.dart | 12 +++- test/home/app_bottom_nav_test.dart | 25 +++++++ test/home/home_nav_ui_test.dart | 37 ++++++++-- 6 files changed, 132 insertions(+), 47 deletions(-) create mode 100644 assets/icon/film_reel.png diff --git a/assets/icon/film_reel.png b/assets/icon/film_reel.png new file mode 100644 index 0000000000000000000000000000000000000000..1047c23c9d95071f12a5e4e5154fc249a63be195 GIT binary patch literal 7666 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 literal 0 HcmV?d00001 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', (