fix(recommend): 修正进度拖动与底栏联动

This commit is contained in:
Zhou Weidong committed 2026-10-22 20:15:07 +08:00
1 parent afe7a49ef0
commit 2765343340
9 files changed
+79 -107

No files matched your search

-7
View File
@@ -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;
+1 -10
View File
@@ -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<SystemUiOverlayStyle>(
@@ -296,7 +288,6 @@ class _HomeTabsState extends State<_HomeTabs> {
active: logic.tab == 1,
onSearch: () => _openSearch(context),
onOpenFilter: () => _pushPage(context, const FilterPage()),
onScrubVisibilityChanged: logic.onScrubVisibilityChanged,
);
}
+6 -2
View File
@@ -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<void> endScrub(
BuildContext context,
VideoPlayerPort player,
Duration position,
) => seekTo(context, player, position);
Future<void> seekTo(
BuildContext context,
VideoPlayerPort player,
+1 -9
View File
@@ -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<Drama>? onDramaChanged;
final ValueChanged<bool>? onImmersiveChanged;
final ValueChanged<bool>? onScrubVisibilityChanged;
final Future<void> Function(Drama drama, Episode episode)? onOpenFullPlayer;
final Future<bool> Function()? onRefreshFeed;
final Future<void> Function()? onLoadMore;
@@ -273,7 +271,6 @@ class _PlayerPageState extends State<PlayerPage>
void _hideChrome() {
if (!_chromeVisible) return;
setState(() => _chromeVisible = false);
widget.onScrubVisibilityChanged?.call(true);
}
/// 长按两侧:切 2 倍速并清屏,记下进入前的状态用于松手恢复。
@@ -288,7 +285,6 @@ class _PlayerPageState extends State<PlayerPage>
_playbackSpeed = _boostPlaybackSpeed;
_chromeVisible = false;
});
widget.onScrubVisibilityChanged?.call(true);
unawaited(logic.setPlaybackSpeed(_boostPlaybackSpeed));
}
@@ -304,14 +300,12 @@ class _PlayerPageState extends State<PlayerPage>
_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<PlayerPage>
_scrubbing = false;
_chromeVisible = wasVisible;
});
widget.onScrubVisibilityChanged?.call(!wasVisible);
}
void _cancelFeedScrub() {
@@ -333,7 +326,6 @@ class _PlayerPageState extends State<PlayerPage>
_scrubbing = false;
_chromeVisible = true;
});
widget.onScrubVisibilityChanged?.call(false);
}
void _handleFeedSurfaceTap() {
@@ -676,7 +668,7 @@ class _PlayerPageState extends State<PlayerPage>
onScrubVisibilityChanged: _setScrubbing,
cancelScrubRequest: widget.scrubCancelRequest,
onSeek: ready
? (position) => logic.seekTo(context, videoPort!, position)
? (position) => logic.endScrub(context, videoPort!, position)
: null,
thumbnailService: _thumbnailService,
spriteAxisService: _spriteAxisService,
@@ -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<Shadow> _boostTextShadows = [
Shadow(color: AppColors.shadow, blurRadius: 5, offset: Offset(0, 1)),
@@ -141,7 +142,7 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
Positioned(
left: _pageHorizontal,
right: _railRight,
bottom: 12,
bottom: _bottomInfoInset,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
@@ -184,6 +185,7 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
void _endScrub() {
final wasVisible = _wasVisibleBeforeScrub;
if (_scrubbing) setState(() => _scrubbing = false);
widget.onScrubVisibilityChanged?.call(false);
widget.onScrubEnd?.call(wasVisible);
}
@@ -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) {
+2 -21
View File
@@ -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<bool> onScrubVisibilityChanged;
@override
State<RecommendPage> createState() => _RecommendPageState();
@@ -43,7 +41,6 @@ class RecommendPage extends StatefulWidget {
class _RecommendPageState extends State<RecommendPage> {
RecommendInnerTab _tab = RecommendInnerTab.feed;
bool _scrubbing = false;
int _scrubCancelRequest = 0;
@override
@@ -159,38 +156,22 @@ class _RecommendPageState extends State<RecommendPage> {
_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,