From 17faa749b255f07db812b53954f8815eb4a479a5 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Thu, 22 Oct 2026 20:34:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(recommend):=20=E6=94=AF=E6=8C=81=E6=A8=AA?= =?UTF-8?q?=E5=90=91=E5=88=87=E6=8D=A2=E4=B8=89=E4=B8=AA=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/PROGRESS.md | 6 ++ lib/pages/recommend/recommend_page.dart | 88 +++++++++++++++++---- test/recommend/recommend_sections_test.dart | 44 ++++++++++- 3 files changed, 121 insertions(+), 17 deletions(-) diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index cf32104..a351bdd 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -1200,3 +1200,9 @@ $ flutter test - 标题、剧名行和观看全集统一通过 `playDramaFromCatalog` 打开非 `homeMode` 逐集播放页。 - 仅含集数的剧集标题不再显示在标题上方,集数只保留下方进度文案。 - `flutter analyze`:No issues found。 + +## 2026-10-22 推荐页横向切换 + +- 推荐、剧单、全部短剧改为共享 PageController 的横向 PageView,点击与滑动均驱动同一选中态。 +- 三页使用 AutomaticKeepAliveClientMixin 保留视频、滚动位置和筛选状态;顶部导航固定在外层。 +- `flutter analyze`:No issues found。 diff --git a/lib/pages/recommend/recommend_page.dart b/lib/pages/recommend/recommend_page.dart index e060687..2d054c6 100644 --- a/lib/pages/recommend/recommend_page.dart +++ b/lib/pages/recommend/recommend_page.dart @@ -38,8 +38,15 @@ class RecommendPage extends StatefulWidget { class _RecommendPageState extends State { RecommendInnerTab _tab = RecommendInnerTab.feed; + final PageController _pageController = PageController(); int _scrubCancelRequest = 0; + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final categories = context.read(); @@ -51,23 +58,35 @@ class _RecommendPageState extends State { Positioned.fill( child: ListenableBuilder( listenable: Listenable.merge([widget.feedController, categories]), - builder: (context, _) => IndexedStack( - index: _tab.index, + builder: (context, _) => PageView( + key: const Key('recommend-tab-page-view'), + controller: _pageController, + scrollDirection: Axis.horizontal, + onPageChanged: _onPageChanged, children: [ - _recommendFeed(context), - _withTopNavSpace( - RecommendSectionsView( - feedController: widget.feedController, - categories: categories, - onOpenDrama: _openDrama, - onMore: _handleMore, + _KeepAliveTab( + key: const PageStorageKey('recommend-tab-feed'), + child: _recommendFeed(context), + ), + _KeepAliveTab( + key: const PageStorageKey('recommend-tab-playlists'), + child: _withTopNavSpace( + RecommendSectionsView( + feedController: widget.feedController, + categories: categories, + onOpenDrama: _openDrama, + onMore: _handleMore, + ), ), ), - _withTopNavSpace( - AllDramasFilterGrid( - feedController: widget.feedController, - onOpenDrama: _openDrama, - onOpenFilter: widget.onOpenFilter, + _KeepAliveTab( + key: const PageStorageKey('recommend-tab-all'), + child: _withTopNavSpace( + AllDramasFilterGrid( + feedController: widget.feedController, + onOpenDrama: _openDrama, + onOpenFilter: widget.onOpenFilter, + ), ), ), ], @@ -162,6 +181,24 @@ class _RecommendPageState extends State { _tab = tab; _scrubCancelRequest++; }); + if (_pageController.hasClients) { + unawaited( + _pageController.animateToPage( + tab.index, + duration: const Duration(milliseconds: 220), + curve: Curves.easeOut, + ), + ); + } + } + + void _onPageChanged(int index) { + final next = RecommendInnerTab.values[index]; + if (next == _tab) return; + setState(() { + _tab = next; + _scrubCancelRequest++; + }); } void _handleSearch() { @@ -180,10 +217,31 @@ class _RecommendPageState extends State { } void _handleMore(RecommendSectionKind section) { - setState(() => _tab = RecommendInnerTab.allDramas); + _selectTab(RecommendInnerTab.allDramas); } Future _openDrama(Drama drama, {String? episodeId}) async { await playDramaFromCatalog(context, drama, episodeId: episodeId); } } + +class _KeepAliveTab extends StatefulWidget { + const _KeepAliveTab({super.key, required this.child}); + + final Widget child; + + @override + State<_KeepAliveTab> createState() => _KeepAliveTabState(); +} + +class _KeepAliveTabState extends State<_KeepAliveTab> + with AutomaticKeepAliveClientMixin<_KeepAliveTab> { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return widget.child; + } +} diff --git a/test/recommend/recommend_sections_test.dart b/test/recommend/recommend_sections_test.dart index d56c2a3..fa50057 100644 --- a/test/recommend/recommend_sections_test.dart +++ b/test/recommend/recommend_sections_test.dart @@ -91,7 +91,7 @@ void main() { expect(player.homeMode, isTrue); expect(player.embedded, isTrue); expect(player.showHomeFeedHeader, isFalse); - expect(find.byType(PageView), findsOneWidget); + expect(find.byType(PageView), findsNWidgets(2)); expect(find.byKey(const Key('recommend-section-hits')), findsNothing); expect(find.byType(AppBottomNav), findsOneWidget); @@ -269,7 +269,7 @@ void main() { await tester.tap(find.byKey(const Key('recommend-tab-playlist'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('recommend-section-hits')), findsOneWidget); - expect(find.byKey(const Key('recommend-feed-player')), findsNothing); + expect(find.byKey(const Key('recommend-feed-player')), findsOneWidget); expect(find.byType(AppBottomNav), findsOneWidget); await tester.tap(find.byKey(const Key('recommend-tab-all'))); @@ -278,6 +278,46 @@ void main() { expect(find.byKey(const Key('all-dramas-grid')), findsOneWidget); }); + testWidgets('should_switch_inner_tabs_with_horizontal_page_swipes', ( + tester, + ) async { + await _pumpRecommend(tester); + final pages = find.byKey(const Key('recommend-tab-page-view')); + final topNavBefore = tester.getRect( + find.byKey(const Key('recommend-top-nav')), + ); + + await tester.drag(pages, const Offset(-320, 0)); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('recommend-section-hits')), findsOneWidget); + expect(find.byKey(const Key('recommend-feed-player')), findsOneWidget); + expect( + tester + .widget( + find.descendant( + of: find.byKey(const Key('recommend-tab-playlist')), + matching: find.text('Playlists'), + ), + ) + .style + ?.color, + AppColors.textPrimary, + ); + expect( + tester.getRect(find.byKey(const Key('recommend-top-nav'))), + topNavBefore, + ); + + await tester.drag(pages, const Offset(-320, 0)); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('all-dramas-grid')), findsOneWidget); + + await tester.drag(pages, const Offset(640, 0)); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('recommend-feed-player')), findsOneWidget); + expect(find.byKey(const Key('recommend-tab-recommend')), findsOneWidget); + }); + testWidgets('should_open_standalone_player_from_title_and_watch_all', ( tester, ) async {