fix(player): 持久化竖滑播放画面层

This commit is contained in:
Zhou Weidong committed 2026-10-26 15:34:24 +08:00
1 parent 5dafc68775
commit 2a32dec183
5 files changed
+593 -101

No files matched your search

+178 -92
View File
@@ -40,6 +40,7 @@ import 'widgets/player_center_toggle.dart';
import 'widgets/player_more_sheet.dart';
import 'widgets/player_overlay.dart';
import 'widgets/player_overlay_models.dart';
import 'widgets/player_render_stack.dart';
import 'widgets/player_speed_sheet.dart';
import 'widgets/player_unlock_sheet.dart';
import 'widgets/player_opening_frame.dart';
@@ -528,6 +529,14 @@ class _PlayerPageState extends State<PlayerPage>
itemCount: indices.length,
onPageChanged: (position) {
final index = indices[position];
if (kDebugMode) {
debugPrint(
'[player-page] '
'ts=${DateTime.now().millisecondsSinceEpoch} '
'phase=session-index '
'previous=${logic.session.currentIndex} next=$index',
);
}
widget.onDramaChanged?.call(logic.queue.entryAt(index).drama);
unawaited(_selectPage(index));
if (position >= indices.length - 3) {
@@ -535,16 +544,37 @@ class _PlayerPageState extends State<PlayerPage>
if (loadMore != null) unawaited(loadMore());
}
},
itemBuilder: (context, position) =>
_buildEpisode(context, indices[position], interactions),
itemBuilder: (context, position) {
final index = indices[position];
if (kDebugMode) {
debugPrint(
'[player-page] '
'ts=${DateTime.now().millisecondsSinceEpoch} '
'phase=item-build page=$position index=$index',
);
}
return _buildEpisode(context, index, interactions);
},
),
);
if (widget.onRefreshFeed == null) return pages;
return FeedPullRefresh(
pageController: logic.pages,
topInset: MediaQuery.paddingOf(context).top + _feedRefreshTopGap,
onRefresh: _refreshHomeFeed,
child: pages,
final pageLayer = widget.onRefreshFeed == null
? pages
: FeedPullRefresh(
pageController: logic.pages,
topInset:
MediaQuery.paddingOf(context).top + _feedRefreshTopGap,
onRefresh: _refreshHomeFeed,
child: pages,
);
return Stack(
fit: StackFit.expand,
children: [
PlayerRenderStack(
pageController: logic.pages,
slots: _renderSlots(indices),
),
pageLayer,
],
);
},
);
@@ -555,7 +585,6 @@ class _PlayerPageState extends State<PlayerPage>
if (widget.homeMode &&
widget.showHomeFeedHeader &&
!_scrubbing &&
_chromeVisible &&
!_boosting &&
!logic.pip &&
!logic.pipPreparing)
@@ -616,6 +645,145 @@ class _PlayerPageState extends State<PlayerPage>
_openMore(context, entry.episode);
}
Episode _resolvedEpisode(int index) {
final entry = logic.queue.entryAt(index);
final sessionEpisode = index >= 0 && index < logic.sessionEpisodes.length
? logic.sessionEpisodes[index]
: entry.episode;
final displayed = widget.homeMode
? logic.displayEpisode(entry.drama, sessionEpisode)
: sessionEpisode;
final needsSessionThumbnails =
(displayed.thumbnailsUrl == null || displayed.thumbnailsUrl!.isEmpty) &&
sessionEpisode.thumbnailsUrl != null;
final needsSessionPoster =
(displayed.posterUrl == null || displayed.posterUrl!.isEmpty) &&
sessionEpisode.posterUrl != null;
if (!needsSessionThumbnails && !needsSessionPoster) return displayed;
return displayed.withVideoUrl(
displayed.videoUrl,
thumbnailsUrl: sessionEpisode.thumbnailsUrl,
posterUrl: sessionEpisode.posterUrl,
);
}
Map<PlayerRenderSlotRole, PlayerRenderSlotData> _renderSlots(
List<int> indices,
) {
final current = logic.session.currentIndex;
final previous =
logic.previousVisibleIndex(current) ??
logic.session.previousPreloadedIndex;
final next =
logic.nextVisibleIndex(current) ?? logic.session.preloadedIndex;
return {
PlayerRenderSlotRole.previous: _renderSlot(
PlayerRenderSlotRole.previous,
previous,
indices,
currentIndex: current,
),
PlayerRenderSlotRole.current: _renderSlot(
PlayerRenderSlotRole.current,
current,
indices,
currentIndex: current,
),
PlayerRenderSlotRole.next: _renderSlot(
PlayerRenderSlotRole.next,
next,
indices,
currentIndex: current,
),
};
}
PlayerRenderSlotData _renderSlot(
PlayerRenderSlotRole role,
int? index,
List<int> indices, {
required int currentIndex,
}) {
if (index == null || index < 0 || index >= logic.sessionEpisodes.length) {
return PlayerRenderSlotData.empty(role);
}
final episode = _resolvedEpisode(index);
final selected = index == currentIndex;
final locked =
logic.isEpisodeLocked(episode) ||
(selected && logic.session.isLocked) ||
logic.blockedEpisodeId == episode.id;
final port = locked
? null
: (selected ? logic.session.active : null) ??
logic.session.preparedPlayerAt(index);
final videoPort = port is VideoPlayerPort ? port : null;
final handoff = selected && !locked ? logic.session.handoffAt(index) : null;
final handoffPort = handoff is VideoPlayerPort ? handoff : null;
final videoReady = videoPort?.controller.value.isInitialized ?? false;
final theater = logic.pip || logic.pipPreparing;
final videoFit = theater ? BoxFit.contain : BoxFit.cover;
final preparedFrameAvailable =
!locked &&
videoReady &&
(selected
? logic.session.reusedWarmAt(index)
: logic.session.preparedPlayerAt(index) != null);
final thumbnailsUrl = episode.thumbnailsUrl;
final posterUrl = episode.posterUrl;
final posterReady = posterUrl?.trim().isNotEmpty ?? false;
final thumbnailReady =
thumbnailsUrl != null &&
_spriteAxisService.cachedFrameAt(thumbnailsUrl, Duration.zero) != null;
if (!locked && !posterReady && thumbnailsUrl != null) {
unawaited(_spriteAxisService.prepareAt(thumbnailsUrl, Duration.zero));
}
if (selected &&
!locked &&
videoPort != null &&
videoPort.controller.value.isInitialized) {
unawaited(
_spriteAxisService.prepareAt(
thumbnailsUrl,
videoPort.controller.value.position,
),
);
}
final pagePosition = indices.indexOf(index);
return PlayerRenderSlotData(
role: role,
index: index,
pagePosition: pagePosition >= 0 ? pagePosition.toDouble() : null,
controller: locked ? null : videoPort?.controller,
videoUrl: videoPort?.controller.dataSource ?? episode.videoUrl,
theater: theater,
warmFrameAvailable: preparedFrameAvailable,
thumbnailReady: thumbnailReady,
posterReady: posterReady,
fallback: handoffPort != null
? PlayerOpeningFallback.handoff
: playerOpeningFallback(
posterUrl: posterUrl,
thumbnailsUrl: thumbnailsUrl,
),
warmSlotReused: logic.session.reusedWarmAt(index),
waitForPlaybackFrame: logic.session.userWantsPlay,
placeholder: locked
? null
: handoffPort != null
? ControllerFramePlaceholder(
controller: handoffPort.controller,
theater: theater,
)
: playerOpeningFrame(
thumbnailsUrl,
posterUrl: posterUrl,
spriteSource: _spriteAxisService,
fit: videoFit,
),
);
}
Widget _buildEpisode(
BuildContext context,
int index,
@@ -624,25 +792,7 @@ class _PlayerPageState extends State<PlayerPage>
final l10n = AppLocalizations.of(context)!;
final entry = logic.queue.entryAt(index);
final drama = entry.drama;
final sessionEpisode = index >= 0 && index < logic.sessionEpisodes.length
? logic.sessionEpisodes[index]
: entry.episode;
final displayed = widget.homeMode
? logic.displayEpisode(drama, sessionEpisode)
: sessionEpisode;
final needsSessionThumbnails =
(displayed.thumbnailsUrl == null || displayed.thumbnailsUrl!.isEmpty) &&
sessionEpisode.thumbnailsUrl != null;
final needsSessionPoster =
(displayed.posterUrl == null || displayed.posterUrl!.isEmpty) &&
sessionEpisode.posterUrl != null;
final episode = needsSessionThumbnails || needsSessionPoster
? displayed.withVideoUrl(
displayed.videoUrl,
thumbnailsUrl: sessionEpisode.thumbnailsUrl,
posterUrl: sessionEpisode.posterUrl,
)
: displayed;
final episode = _resolvedEpisode(index);
final selected = index == logic.session.currentIndex;
final locked =
logic.isEpisodeLocked(episode) ||
@@ -655,79 +805,15 @@ class _PlayerPageState extends State<PlayerPage>
: (selected ? logic.session.active : null) ??
logic.session.preparedPlayerAt(index);
final videoPort = port is VideoPlayerPort ? port : null;
final handoff = selected && !locked ? logic.session.handoffAt(index) : null;
final handoffPort = handoff is VideoPlayerPort ? handoff : null;
// 测试播放器没有纹理。有播放器但不是视频控制器时视为已就绪,避免加载圈一直转。
final videoReady = videoPort?.controller.value.isInitialized ?? false;
if (selected && videoReady) _logSwipeReveal();
final ready = !locked && (videoReady || port != null);
final theater = logic.pip || logic.pipPreparing;
final videoFit = theater ? BoxFit.contain : BoxFit.cover;
final preparedFrameAvailable =
!locked &&
videoReady &&
(selected
? logic.session.reusedWarmAt(index)
: logic.session.preparedPlayerAt(index) != null);
final warmSlotReused = logic.session.reusedWarmAt(index);
final thumbnailsUrl = episode.thumbnailsUrl;
final posterUrl = episode.posterUrl;
final posterReady = posterUrl?.trim().isNotEmpty ?? false;
final openingFrameReady =
thumbnailsUrl != null &&
_spriteAxisService.cachedFrameAt(thumbnailsUrl, Duration.zero) != null;
if (!posterReady && thumbnailsUrl != null) {
unawaited(_spriteAxisService.prepareAt(thumbnailsUrl, Duration.zero));
}
if (selected &&
!locked &&
videoPort != null &&
videoPort.controller.value.isInitialized) {
unawaited(
_spriteAxisService.prepareAt(
episode.thumbnailsUrl,
videoPort.controller.value.position,
),
);
}
return Stack(
fit: StackFit.expand,
children: [
if (widget.homeMode)
const ColoredBox(
key: Key('home-feed-black-background'),
color: AppColors.mediaBlack,
),
PlayerVideoSurface(
controller: locked ? null : videoPort?.controller,
videoUrl: videoPort?.controller.dataSource ?? episode.videoUrl,
theater: theater,
warmFrameAvailable: preparedFrameAvailable,
warmSlotReused: warmSlotReused,
thumbnailReady: openingFrameReady,
posterReady: posterReady,
fallback: handoffPort != null
? PlayerOpeningFallback.handoff
: playerOpeningFallback(
posterUrl: posterUrl,
thumbnailsUrl: thumbnailsUrl,
),
waitForPlaybackFrame: logic.session.userWantsPlay,
placeholder: locked
? null
: handoffPort != null
? ControllerFramePlaceholder(
controller: handoffPort.controller,
theater: theater,
)
: playerOpeningFrame(
thumbnailsUrl,
posterUrl: posterUrl,
spriteSource: _spriteAxisService,
fit: videoFit,
),
),
if (selected &&
!locked &&
!theater &&