From 276534334093a25f5aff836f36cdfb7011fb8cdb Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Thu, 22 Oct 2026 20:15:07 +0800 Subject: [PATCH] =?UTF-8?q?fix(recommend):=20=E4=BF=AE=E6=AD=A3=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E6=8B=96=E5=8A=A8=E4=B8=8E=E5=BA=95=E6=A0=8F=E8=81=94?= =?UTF-8?q?=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/PROGRESS.md | 5 +- lib/pages/home/home_logic.dart | 7 -- lib/pages/home/home_view.dart | 11 +- lib/pages/player/player_logic.dart | 8 +- lib/pages/player/player_view.dart | 10 +- .../player/widgets/home_feed_overlay.dart | 4 +- .../widgets/home_feed_overlay_parts.dart | 104 +++++++++--------- lib/pages/recommend/recommend_page.dart | 23 +--- test/recommend/recommend_sections_test.dart | 14 ++- 9 files changed, 79 insertions(+), 107 deletions(-) diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index c2fc4c1..48c4c2b 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -1183,6 +1183,7 @@ $ flutter test - 按最新反馈移除整行榜单与热度胶囊,信息顺序收敛为标题、剧名、标签、简介、观看全集。 - 剧名 19/w600,箭头 20 白色并紧邻剧名;标签行改为 13/w600 亮灰内联布局。 - 简介「展开/收起」沿用现有三语文案;进度条热区恢复 24 并接入拖动自动清屏与预览卡片。 -- 拖动期间隐藏操作栏、文字及根底栏,顶部导航常驻;松手后持续清屏直到点击画面。 -- 进度条固定中心并动画到 6 高,时间气泡限制在左右 12;松手恢复拖动前浮层状态。 +- 拖动仅控制视频浮层内容,顶部导航与根底栏保持常驻。 +- 进度条固定中心并动画到 6 高,时间气泡限制在左右 12;end/cancel/pointer 兜底均恢复拖动前状态。 - 修复 `scrubCancelRequest` 误置于 `initState` 的编译错误,并清理推荐页未使用变量;`flutter analyze` 无问题。 +- 观看全集按钮相对进度条上移 8,间距调整为 20;`flutter analyze` 无问题。 diff --git a/lib/pages/home/home_logic.dart b/lib/pages/home/home_logic.dart index dab2d46..dfd1f00 100644 --- a/lib/pages/home/home_logic.dart +++ b/lib/pages/home/home_logic.dart @@ -23,7 +23,6 @@ class HomeLogic extends ChangeNotifier { bool drawerOpen = false; bool detailOpen = false; bool videoImmersive = false; - bool scrubHidden = false; bool _dramaNotifyScheduled = false; bool _disposed = false; @@ -145,12 +144,6 @@ class HomeLogic extends ChangeNotifier { notify(); } - void onScrubVisibilityChanged(bool value) { - if (scrubHidden == value) return; - scrubHidden = value; - notify(); - } - @override void dispose() { _disposed = true; diff --git a/lib/pages/home/home_view.dart b/lib/pages/home/home_view.dart index 615bf61..d8e309e 100644 --- a/lib/pages/home/home_view.dart +++ b/lib/pages/home/home_view.dart @@ -239,15 +239,7 @@ class _HomeTabsState extends State<_HomeTabs> { backgroundColor: AppColors.transparent, resizeToAvoidBottomInset: false, body: _body(context), - bottomNavigationBar: logic.videoImmersive - ? null - : Visibility( - visible: !logic.scrubHidden, - maintainSize: true, - maintainAnimation: true, - maintainState: true, - child: _nav(context), - ), + bottomNavigationBar: logic.videoImmersive ? null : _nav(context), ), ); return AnnotatedRegion( @@ -296,7 +288,6 @@ class _HomeTabsState extends State<_HomeTabs> { active: logic.tab == 1, onSearch: () => _openSearch(context), onOpenFilter: () => _pushPage(context, const FilterPage()), - onScrubVisibilityChanged: logic.onScrubVisibilityChanged, ); } diff --git a/lib/pages/player/player_logic.dart b/lib/pages/player/player_logic.dart index 3ad74ec..8060bd8 100644 --- a/lib/pages/player/player_logic.dart +++ b/lib/pages/player/player_logic.dart @@ -914,15 +914,19 @@ class PlayerLogic extends ChangeNotifier { void beginScrub(VideoPlayerPort player) { if (session.active != player) return; scrubbingPlayer = player; - session.setOverlayOpen(true); } void cancelScrub() { if (scrubbingPlayer == null) return; scrubbingPlayer = null; - session.setOverlayOpen(false); } + Future endScrub( + BuildContext context, + VideoPlayerPort player, + Duration position, + ) => seekTo(context, player, position); + Future seekTo( BuildContext context, VideoPlayerPort player, diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index dc9d0d4..1628224 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -70,7 +70,6 @@ class PlayerPage extends StatefulWidget { this.showHomeFeedHeader = true, this.onDramaChanged, this.onImmersiveChanged, - this.onScrubVisibilityChanged, this.onOpenFullPlayer, this.onRefreshFeed, this.onLoadMore, @@ -97,7 +96,6 @@ class PlayerPage extends StatefulWidget { final bool showHomeFeedHeader; final ValueChanged? onDramaChanged; final ValueChanged? onImmersiveChanged; - final ValueChanged? onScrubVisibilityChanged; final Future Function(Drama drama, Episode episode)? onOpenFullPlayer; final Future Function()? onRefreshFeed; final Future Function()? onLoadMore; @@ -273,7 +271,6 @@ class _PlayerPageState extends State void _hideChrome() { if (!_chromeVisible) return; setState(() => _chromeVisible = false); - widget.onScrubVisibilityChanged?.call(true); } /// 长按两侧:切 2 倍速并清屏,记下进入前的状态用于松手恢复。 @@ -288,7 +285,6 @@ class _PlayerPageState extends State _playbackSpeed = _boostPlaybackSpeed; _chromeVisible = false; }); - widget.onScrubVisibilityChanged?.call(true); unawaited(logic.setPlaybackSpeed(_boostPlaybackSpeed)); } @@ -304,14 +300,12 @@ class _PlayerPageState extends State _playbackSpeed = speed; _chromeVisible = chrome; }); - widget.onScrubVisibilityChanged?.call(!chrome); unawaited(logic.setPlaybackSpeed(speed)); } void _setScrubbing(bool value) { if (_scrubbing == value) return; setState(() => _scrubbing = value); - widget.onScrubVisibilityChanged?.call(value); } void _finishFeedScrub(bool wasVisible) { @@ -319,7 +313,6 @@ class _PlayerPageState extends State _scrubbing = false; _chromeVisible = wasVisible; }); - widget.onScrubVisibilityChanged?.call(!wasVisible); } void _cancelFeedScrub() { @@ -333,7 +326,6 @@ class _PlayerPageState extends State _scrubbing = false; _chromeVisible = true; }); - widget.onScrubVisibilityChanged?.call(false); } void _handleFeedSurfaceTap() { @@ -676,7 +668,7 @@ class _PlayerPageState extends State onScrubVisibilityChanged: _setScrubbing, cancelScrubRequest: widget.scrubCancelRequest, onSeek: ready - ? (position) => logic.seekTo(context, videoPort!, position) + ? (position) => logic.endScrub(context, videoPort!, position) : null, thumbnailService: _thumbnailService, spriteAxisService: _spriteAxisService, diff --git a/lib/pages/player/widgets/home_feed_overlay.dart b/lib/pages/player/widgets/home_feed_overlay.dart index f254f84..6b7fa8f 100644 --- a/lib/pages/player/widgets/home_feed_overlay.dart +++ b/lib/pages/player/widgets/home_feed_overlay.dart @@ -22,6 +22,7 @@ const double _progressHitHeight = 28; const double _pageHorizontal = 14; const double _railRight = 8; const double _railGap = 12; +const double _bottomInfoInset = 20; const double _railBottomOffset = 60; const List _boostTextShadows = [ Shadow(color: AppColors.shadow, blurRadius: 5, offset: Offset(0, 1)), @@ -141,7 +142,7 @@ class _HomeFeedOverlayState extends State { Positioned( left: _pageHorizontal, right: _railRight, - bottom: 12, + bottom: _bottomInfoInset, child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ @@ -184,6 +185,7 @@ class _HomeFeedOverlayState extends State { void _endScrub() { final wasVisible = _wasVisibleBeforeScrub; if (_scrubbing) setState(() => _scrubbing = false); + widget.onScrubVisibilityChanged?.call(false); widget.onScrubEnd?.call(wasVisible); } diff --git a/lib/pages/player/widgets/home_feed_overlay_parts.dart b/lib/pages/player/widgets/home_feed_overlay_parts.dart index 034fbab..2f16eab 100644 --- a/lib/pages/player/widgets/home_feed_overlay_parts.dart +++ b/lib/pages/player/widgets/home_feed_overlay_parts.dart @@ -43,6 +43,7 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { double? _preview; Timer? _bufferTimer; bool _buffering = false; + bool _dragging = false; @override void initState() { @@ -61,6 +62,7 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { _attachSource(); } if (oldWidget.cancelScrubRequest != widget.cancelScrubRequest) { + _dragging = false; _preview = null; } if (oldWidget.locked != widget.locked) { @@ -68,6 +70,7 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { _bufferTimer = null; _buffering = false; if (widget.locked) { + _dragging = false; _preview = null; } else { _syncBufferState(notify: false); @@ -88,7 +91,7 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { builder: (context, constraints) { final width = constraints.maxWidth; final source = widget.controller; - final scrubbing = _preview != null; + final scrubbing = _dragging; final loading = !widget.locked && !scrubbing && _buffering; final track = widget.locked ? _inactiveTrack() @@ -115,49 +118,41 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { !loading && (source?.value.isInitialized ?? false); final video = source?.value; - final previewPosition = _preview == null || video == null + final previewPosition = !_dragging || _preview == null || video == null ? null : seekPositionForFraction(_preview!, video.duration); final showPreview = previewPosition != null; - return GestureDetector( - key: const Key('home-feed-progress'), + return Listener( behavior: HitTestBehavior.opaque, - onHorizontalDragStart: interactive - ? (details) { - widget.onScrubStart?.call(); - _update(details.localPosition.dx / width); - } - : null, - onHorizontalDragUpdate: interactive - ? (details) => _update(details.localPosition.dx / width) - : null, - onHorizontalDragEnd: interactive - ? (_) { - widget.onScrubEnd?.call(); - _commit(_preview); - } - : null, - onHorizontalDragCancel: interactive - ? () { - widget.onScrubEnd?.call(); - _cancelPreview(); - } - : null, - child: Stack( - clipBehavior: Clip.none, - children: [ - track, - if (showPreview) - Positioned( - left: _timeBubbleLeft(width, _preview!), - bottom: _lineCenterFromBottom + 16, - child: _ScrubTimeBubble( - key: const Key('scrub-preview-time-bubble'), - position: previewPosition, - duration: video!.duration, + onPointerUp: (_) => _finishScrub(), + onPointerCancel: (_) => _finishScrub(), + child: GestureDetector( + key: const Key('home-feed-progress'), + behavior: HitTestBehavior.opaque, + onHorizontalDragStart: interactive + ? (details) => _startScrub(details.localPosition.dx / width) + : null, + onHorizontalDragUpdate: interactive + ? (details) => _update(details.localPosition.dx / width) + : null, + onHorizontalDragEnd: interactive ? (_) => _finishScrub() : null, + onHorizontalDragCancel: interactive ? _finishScrub : null, + child: Stack( + clipBehavior: Clip.none, + children: [ + track, + if (showPreview) + Positioned( + left: _timeBubbleLeft(width, _preview!), + bottom: _lineCenterFromBottom + 16, + child: _ScrubTimeBubble( + key: const Key('scrub-preview-time-bubble'), + position: previewPosition, + duration: video!.duration, + ), ), - ), - ], + ], + ), ), ); }, @@ -259,23 +254,32 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { ); } + void _startScrub(double fraction) { + if (_dragging) return; + setState(() { + _dragging = true; + _preview = fraction.clamp(0.0, 1.0).toDouble(); + }); + widget.onScrubStart?.call(); + } + void _update(double fraction) { + if (!_dragging) return; setState(() => _preview = fraction.clamp(0.0, 1.0).toDouble()); } - void _commit(double? fraction) { + void _finishScrub() { + if (!_dragging) return; + final fraction = _preview; + setState(() { + _dragging = false; + _preview = null; + }); + widget.onScrubEnd?.call(); if (widget.locked || fraction == null) return; final total = widget.controller?.value.duration.inMilliseconds ?? 0; - if (total > 0) { - widget.onSeek?.call( - Duration(milliseconds: (total * fraction.clamp(0.0, 1.0)).round()), - ); - } - setState(() => _preview = null); - } - - void _cancelPreview() { - setState(() => _preview = null); + if (total <= 0) return; + widget.onSeek?.call(Duration(milliseconds: (total * fraction).round())); } Widget _track(double fraction, double width) { diff --git a/lib/pages/recommend/recommend_page.dart b/lib/pages/recommend/recommend_page.dart index 2ab4a14..c409d8f 100644 --- a/lib/pages/recommend/recommend_page.dart +++ b/lib/pages/recommend/recommend_page.dart @@ -28,14 +28,12 @@ class RecommendPage extends StatefulWidget { required this.active, required this.onSearch, required this.onOpenFilter, - required this.onScrubVisibilityChanged, }); final HomeFeedController feedController; final bool active; final VoidCallback onSearch; final VoidCallback onOpenFilter; - final ValueChanged onScrubVisibilityChanged; @override State createState() => _RecommendPageState(); @@ -43,7 +41,6 @@ class RecommendPage extends StatefulWidget { class _RecommendPageState extends State { RecommendInnerTab _tab = RecommendInnerTab.feed; - bool _scrubbing = false; int _scrubCancelRequest = 0; @override @@ -159,38 +156,22 @@ class _RecommendPageState extends State { _openDrama(drama, episodeId: episode.id), onRefreshFeed: widget.feedController.refreshCurrent, onLoadMore: widget.feedController.loadMore, - onScrubVisibilityChanged: _setScrubbing, scrubCancelRequest: _scrubCancelRequest, ); } void _selectTab(RecommendInnerTab tab) { - final wasScrubbing = _scrubbing; setState(() { _tab = tab; - if (wasScrubbing) _scrubCancelRequest++; - _scrubbing = false; + _scrubCancelRequest++; }); - if (wasScrubbing) widget.onScrubVisibilityChanged(false); } void _handleSearch() { - if (_scrubbing) { - setState(() { - _scrubbing = false; - _scrubCancelRequest++; - }); - widget.onScrubVisibilityChanged(false); - } + setState(() => _scrubCancelRequest++); widget.onSearch(); } - void _setScrubbing(bool value) { - if (_scrubbing == value) return; - setState(() => _scrubbing = value); - widget.onScrubVisibilityChanged(value); - } - Widget _withTopNavSpace(Widget child) { return SafeArea( bottom: false, diff --git a/test/recommend/recommend_sections_test.dart b/test/recommend/recommend_sections_test.dart index 7646fbe..b96f63b 100644 --- a/test/recommend/recommend_sections_test.dart +++ b/test/recommend/recommend_sections_test.dart @@ -102,15 +102,19 @@ void main() { await tester.pump(const Duration(milliseconds: 130)); expect(find.byKey(const Key('recommend-top-nav')), findsOneWidget); expect(tester.getCenter(progress).dy, closeTo(progressCenterY, 0.1)); - final navVisibility = find.ancestor( - of: find.byKey(const Key('home-tabs-bar')), - matching: find.byType(Visibility), + expect(find.byType(AppBottomNav), findsOneWidget); + expect( + tester.getSize(find.byKey(const Key('home-tabs-bar'))).height, + AppDimens.navHeight, ); - expect(tester.widget(navVisibility.first).visible, isFalse); await scrub.up(); await tester.pumpAndSettle(); expect(find.byKey(const Key('recommend-top-nav')), findsOneWidget); - expect(tester.widget(navVisibility.first).visible, isTrue); + expect(find.byType(AppBottomNav), findsOneWidget); + expect( + tester.getSize(find.byKey(const Key('home-tabs-bar'))).height, + AppDimens.navHeight, + ); await tester.tap(find.byKey(const Key('recommend-tab-playlist'))); await tester.pumpAndSettle();