From 5282194ca52e450d948d51e3da5d74a36c4285fa Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Thu, 22 Oct 2026 19:05:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(home):=20=E6=94=AF=E6=8C=81=E6=BB=91?= =?UTF-8?q?=E5=8A=A8=E5=88=87=E6=8D=A2=E9=A6=96=E9=A1=B5=E5=88=86=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/PROGRESS.md | 1 + lib/pages/home/widgets/home_feed_grid.dart | 66 +++++++++++++++++++--- test/home/home_grid_layout_test.dart | 27 +++++++++ 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index 664f32b..814e5e0 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -1112,6 +1112,7 @@ $ flutter test ## 2026-10-22 首页布局对齐修正 - 一级分类未选 18/w400、选中 20/w700;二级 chips 间距 10、左右 8、15/w600,并以稳定 AnimatedContainer 切换。 +- 首页内容区改为横向 PageView:点击与滑动共用 PageController,各 tab 保留纵向滚动位置。 - 四类卡片去底板/描边,封面统一 3:4、8 圆角、行距 10、封底标题间距 6;状态角标为右上角紧凑异形丝带。 - 标题下方恢复左对齐题材文字,使用 `|` 分隔、灰色小字,与封面状态角标保持独立。 - 首页搜索栏到一级、一级到二级、二级到网格间距为 8/8/10;非精选 tab 直接以 10 衔接网格。 diff --git a/lib/pages/home/widgets/home_feed_grid.dart b/lib/pages/home/widgets/home_feed_grid.dart index 23b9b9f..caa1574 100644 --- a/lib/pages/home/widgets/home_feed_grid.dart +++ b/lib/pages/home/widgets/home_feed_grid.dart @@ -43,6 +43,7 @@ class HomeFeedGrid extends StatefulWidget { class _HomeFeedGridState extends State { HomePrimaryTab _selectedPrimary = HomePrimaryTab.featured; + late final PageController _pageController; late final Map _scrollControllers; final Map _selectedTags = { for (final tab in HomePrimaryTab.values) tab: null, @@ -51,6 +52,7 @@ class _HomeFeedGridState extends State { @override void initState() { super.initState(); + _pageController = PageController(initialPage: _selectedPrimary.index); _scrollControllers = { for (final tab in HomePrimaryTab.values) tab: ScrollController(), }; @@ -58,6 +60,7 @@ class _HomeFeedGridState extends State { @override void dispose() { + _pageController.dispose(); for (final controller in _scrollControllers.values) { controller.dispose(); } @@ -93,19 +96,30 @@ class _HomeFeedGridState extends State { tags: tags, selectedPrimary: _selectedPrimary, selectedTag: activeTag, - onPrimarySelected: (tab) => - setState(() => _selectedPrimary = tab), + onPrimarySelected: _selectPrimary, onTagSelected: (tag) => setState(() => _selectedTags[_selectedPrimary] = tag), walletBadge: walletBadge, ), Expanded( - child: IndexedStack( - index: _selectedPrimary.index, - children: [ - for (final tab in HomePrimaryTab.values) - _tabGrid(context, l10n, tab: tab, shortcuts: shortcuts), - ], + child: PageView.builder( + key: const Key('home-primary-page-view'), + controller: _pageController, + itemCount: HomePrimaryTab.values.length, + onPageChanged: (index) => setState( + () => _selectedPrimary = HomePrimaryTab.values[index], + ), + itemBuilder: (context, index) => _KeepAlivePage( + key: PageStorageKey( + 'home-primary-page-${HomePrimaryTab.values[index].name}', + ), + child: _tabGrid( + context, + l10n, + tab: HomePrimaryTab.values[index], + shortcuts: shortcuts, + ), + ), ), ), ], @@ -114,6 +128,21 @@ class _HomeFeedGridState extends State { ); } + void _selectPrimary(HomePrimaryTab tab) { + if (tab == _selectedPrimary) return; + if (!_pageController.hasClients) { + setState(() => _selectedPrimary = tab); + return; + } + unawaited( + _pageController.animateToPage( + tab.index, + duration: AppDimens.homePrimaryTabAnimationDuration, + curve: Curves.easeOut, + ), + ); + } + Widget _tabGrid( BuildContext context, AppLocalizations l10n, { @@ -329,3 +358,24 @@ class _HomeFeedGridState extends State { ); } } + +class _KeepAlivePage extends StatefulWidget { + const _KeepAlivePage({super.key, required this.child}); + + final Widget child; + + @override + State<_KeepAlivePage> createState() => _KeepAlivePageState(); +} + +class _KeepAlivePageState extends State<_KeepAlivePage> + with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return widget.child; + } +} diff --git a/test/home/home_grid_layout_test.dart b/test/home/home_grid_layout_test.dart index 38cb16b..56d64d5 100644 --- a/test/home/home_grid_layout_test.dart +++ b/test/home/home_grid_layout_test.dart @@ -11,6 +11,7 @@ import 'package:fruitshorts/core/data/repositories/catalog_repository.dart'; import 'package:fruitshorts/core/data/repositories/favorite_repository.dart'; import 'package:fruitshorts/core/data/repositories/playback_repository.dart'; import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart'; +import 'package:fruitshorts/core/theme/app_dimens.dart'; import 'package:fruitshorts/pages/home/widgets/home_feed_card.dart'; import 'package:fruitshorts/pages/player/player_view.dart'; import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; @@ -170,6 +171,32 @@ void main() { ); }); + testWidgets('should_swipe_between_primary_sections', (tester) async { + final repository = _PagedCatalogRepository(); + await _pumpApp(tester, repository); + + expect(find.byKey(const Key('home-secondary-tags')), findsOneWidget); + await tester.drag( + find.byKey(const Key('home-primary-page-view')), + const Offset(-320, 0), + ); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('home-secondary-tags')), findsNothing); + final selected = tester.widget( + find.byKey(const Key('home-primary-label-newReleases')), + ); + expect(selected.style.fontSize, AppDimens.homePrimaryTabSelectedFont); + expect(selected.style.fontWeight, FontWeight.w700); + + await tester.drag( + find.byKey(const Key('home-primary-page-view')), + const Offset(320, 0), + ); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('home-secondary-tags')), findsOneWidget); + }); + testWidgets('should_load_next_page_without_expanding_page_size', ( tester, ) async {