From 543d0aadd5d472af307585b40cf9ad23c6bd89fb Mon Sep 17 00:00:00 2001 From: Vicky <10212746+haifeng-jiang@user.noreply.gitee.com> Date: Thu, 29 Oct 2026 15:44:22 +0800 Subject: [PATCH] =?UTF-8?q?style(player):=20=E6=92=AD=E6=94=BE=E9=A1=B5?= =?UTF-8?q?=E4=B8=8E=E9=A6=96=E9=A1=B5=E6=B5=AE=E5=B1=82=E7=9A=84=E6=8E=A7?= =?UTF-8?q?=E4=BB=B6=E6=A0=B7=E5=BC=8F=E7=BB=9F=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 播放页顶部按钮与选集条去掉底色、进度条不再画滑块圆点并压到最底部; 剧名与简介收在互动栏左侧,清屏后只留右下角退出按钮。 首页观看条与进度条共用 20pt 页边距,进度槽底色与播放页一致。 --- .../widgets/exit_clear_screen_button.dart | 12 +- .../player/widgets/home_feed_overlay.dart | 57 +++---- .../widgets/home_feed_overlay_parts.dart | 39 +---- .../widgets/home_feed_recommend_panel.dart | 104 +++++++------ .../player/widgets/player_center_toggle.dart | 4 +- lib/pages/player/widgets/player_overlay.dart | 141 ++++++++++-------- .../player/widgets/player_overlay_parts.dart | 40 +++-- .../player/widgets/seek_progress_bar.dart | 6 +- test/home/home_preview_layout_test.dart | 93 +++++------- test/player/player_center_toggle_test.dart | 4 +- test/player/player_fullscreen_test.dart | 44 +++--- test/player/seek_scrub_visibility_test.dart | 51 +++---- 12 files changed, 298 insertions(+), 297 deletions(-) diff --git a/lib/pages/player/widgets/exit_clear_screen_button.dart b/lib/pages/player/widgets/exit_clear_screen_button.dart index a514aaa..21dafa7 100644 --- a/lib/pages/player/widgets/exit_clear_screen_button.dart +++ b/lib/pages/player/widgets/exit_clear_screen_button.dart @@ -4,6 +4,10 @@ import 'package:flutter/material.dart'; import '../../../core/theme/app_colors.dart'; +const List _iconShadows = [ + Shadow(color: AppColors.shadow, blurRadius: 6, offset: Offset(0, 1)), +]; + /// Shared floating action used to leave clear-screen mode. class ExitClearScreenButton extends StatelessWidget { const ExitClearScreenButton({ @@ -25,11 +29,15 @@ class ExitClearScreenButton extends StatelessWidget { onPressed: onPressed, padding: EdgeInsets.zero, style: IconButton.styleFrom( - backgroundColor: AppColors.controlScrim, + backgroundColor: AppColors.transparent, foregroundColor: AppColors.onPrimary, shape: const CircleBorder(), ), - icon: const Icon(Icons.fullscreen_exit_rounded, size: 18), + icon: const Icon( + Icons.fullscreen_exit_rounded, + size: 22, + shadows: _iconShadows, + ), ), ); } diff --git a/lib/pages/player/widgets/home_feed_overlay.dart b/lib/pages/player/widgets/home_feed_overlay.dart index b97f73a..40fecd0 100644 --- a/lib/pages/player/widgets/home_feed_overlay.dart +++ b/lib/pages/player/widgets/home_feed_overlay.dart @@ -21,13 +21,16 @@ import 'scrub_preview_card.dart'; part 'home_feed_overlay_parts.dart'; const double _progressHeight = 2; -const double _progressHitHeight = 32; + +/// 进度条触控带高度:没有滑块圆点后收到 16pt,够手指按住这条线拖动。 +const double _progressHitHeight = 16; const double _pageHorizontal = 20; const double _railRight = 14; const double _railGap = 12; -// 观看条高 44;2pt 进度线视觉上沿距观看条顶约 7pt(线中心距进度条盒底 8)。 -// 信息面板底边取 44+16=60,使简介↔进度线、进度线↔观看条两侧间距均为 7pt。 -const double _bottomInfoInset = homeFeedWatchAllHeight + 16; +// 进度线贴着屏幕底部(线心距底 4pt),观看条底边只留 8pt,视觉上就是线上一根条。 +const double _watchAllBottomInset = 8; +// 信息面板底边直接贴住观看条顶沿,中间不留缝。 +const double _bottomInfoInset = _watchAllBottomInset + homeFeedWatchAllHeight; const double _railBottomOffset = 40; const Duration _overlayFadeDuration = Duration(milliseconds: 120); const List _boostTextShadows = [ @@ -152,7 +155,7 @@ class _HomeFeedOverlayState extends State { Positioned( left: 0, right: 0, - bottom: homeFeedWatchAllHeight + 12, + bottom: _watchAllBottomInset + homeFeedWatchAllHeight + 12, child: AnimatedOpacity( duration: _overlayFadeDuration, opacity: widget.boosting ? 1 : 0, @@ -162,10 +165,30 @@ class _HomeFeedOverlayState extends State { ), ), ), + // 观看条在下、进度条在上:进度条 32pt 触控盒要盖住观看条下沿的空白, + // 否则拖拽 seek 的起点会被观看条吞掉。两块的可见区域(文字/进度线)互不遮挡。 Positioned( left: _pageHorizontal, right: _pageHorizontal, - bottom: homeFeedWatchAllHeight, + bottom: _watchAllBottomInset, + child: AnimatedOpacity( + duration: _overlayFadeDuration, + opacity: widget.visible && !_scrubbing && !widget.boosting + ? 1 + : 0, + child: IgnorePointer( + ignoring: !widget.visible || _scrubbing || widget.boosting, + child: HomeFeedWatchAllBar( + drama: widget.drama, + onTap: widget.onOpenFullPlayer, + ), + ), + ), + ), + Positioned( + left: _pageHorizontal, + right: _pageHorizontal, + bottom: 0, child: _FeedProgressBar( controller: widget.controller, locked: widget.locked, @@ -182,27 +205,7 @@ class _HomeFeedOverlayState extends State { onSeek: widget.onSeek, ), ), - Positioned( - left: 0, - right: 0, - bottom: 0, - child: AnimatedOpacity( - duration: _overlayFadeDuration, - opacity: widget.visible && !_scrubbing && !widget.boosting - ? 1 - : 0, - child: IgnorePointer( - ignoring: !widget.visible || _scrubbing || widget.boosting, - child: HomeFeedWatchAllBar( - drama: widget.drama, - onTap: widget.onOpenFullPlayer, - ), - ), - ), - ), - // 信息面板放在最后(最上层):进度条 32px 触控盒向上伸进底部避让区, - // 若面板在前,简介/展开按钮底部的触控留白会被进度条吞掉点击。 - // 视觉上避让条带为空,提层不改变观感。 + // 信息面板放在最后(最上层),底部避让区永远优先响应简介/展开按钮的点击。 Positioned( left: _pageHorizontal, right: _railRight, diff --git a/lib/pages/player/widgets/home_feed_overlay_parts.dart b/lib/pages/player/widgets/home_feed_overlay_parts.dart index d938d2d..24b7d31 100644 --- a/lib/pages/player/widgets/home_feed_overlay_parts.dart +++ b/lib/pages/player/widgets/home_feed_overlay_parts.dart @@ -2,10 +2,9 @@ part of 'home_feed_overlay.dart'; -const double _thumbSize = 6; -const double _thumbActiveSize = 14; const double _trackActiveHeight = 6; -const double _lineCenterFromBottom = 8; +// 进度线贴住屏幕底部(线心距底 4pt);播放进度只靠蓝色已播段表达,不画滑块圆点。 +const double _lineCenterFromBottom = 4; const double _timeBubbleWidth = 96; const double _timeBubbleMargin = 12; const Key _inactiveTrackKey = Key('home-feed-progress-inactive'); @@ -305,7 +304,7 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { height: widget.height, child: ColoredBox( key: _inactiveTrackKey, - color: AppColors.border, + color: AppColors.inactiveTrack, ), ), ), @@ -345,20 +344,11 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { Widget _track(double fraction, double width) { final dragging = _preview != null; final trackHeight = dragging ? _trackActiveHeight : widget.height; - final size = dragging ? _thumbActiveSize : _thumbSize; - final finger = (width * fraction).clamp(0.0, width).toDouble(); - final thumbSlotLeft = dragging - ? finger - _thumbActiveSize / 2 - : (finger - _thumbActiveSize / 2) - .clamp(0.0, width - _thumbActiveSize) - .toDouble(); - final playedWidth = dragging ? finger : width * fraction; + final playedWidth = (width * fraction).clamp(0.0, width).toDouble(); final trackSlotBottom = _lineCenterFromBottom - _trackActiveHeight / 2; - final thumbSlotBottom = _lineCenterFromBottom - _thumbActiveSize / 2; return SizedBox( height: widget.hitHeight, child: Stack( - clipBehavior: Clip.none, children: [ Positioned( left: 0, @@ -374,7 +364,8 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { key: const Key('home-feed-progress-track'), width: width, height: trackHeight, - color: AppColors.border, + // 和剧集播放页进度条共用同一个槽底色,两个入口观感一致。 + color: AppColors.inactiveTrack, ), ), Align( @@ -390,24 +381,6 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { ), ), ), - Positioned( - left: thumbSlotLeft, - bottom: thumbSlotBottom, - child: SizedBox.square( - dimension: _thumbActiveSize, - child: Center( - child: Container( - key: const Key('home-feed-progress-thumb'), - width: size, - height: size, - decoration: const BoxDecoration( - color: AppColors.onPrimary, - shape: BoxShape.circle, - ), - ), - ), - ), - ), ], ), ); diff --git a/lib/pages/player/widgets/home_feed_recommend_panel.dart b/lib/pages/player/widgets/home_feed_recommend_panel.dart index 61ed40c..c42ffa0 100644 --- a/lib/pages/player/widgets/home_feed_recommend_panel.dart +++ b/lib/pages/player/widgets/home_feed_recommend_panel.dart @@ -13,7 +13,14 @@ import 'interaction_rail.dart'; const double _seriesFont = 17; const double _tagFont = 12; const double _synopsisFont = 14; -const double _watchAllHeight = 44; +const double _watchAllHeight = 38; + +/// 观看条内部左右留白与圆角:外沿由 overlay 按页面边距对齐,内部不再重复内缩。 +const double _watchAllInset = 12; +const double _watchAllRadius = 12; + +/// 标题、标签、简介之间的行间距:底部信息本来就挤,留 4pt 够分层。 +const double _blockGap = 4; const List _textShadows = [ Shadow(color: AppColors.shadow, blurRadius: 6, offset: Offset(0, 1)), ]; @@ -57,6 +64,7 @@ class _HomeFeedInfoPanelState extends State { final dramaTitle = localizedDramaTitle(l10n, widget.drama); final synopsis = localizedDramaSynopsis(l10n, widget.drama).trim(); final genres = _genreValues(Localizations.localeOf(context).languageCode); + final tags = _tagChips(l10n, genres); return Column( key: const Key('home-feed-info-panel'), mainAxisSize: MainAxisSize.min, @@ -104,18 +112,21 @@ class _HomeFeedInfoPanelState extends State { ], ), ), - const SizedBox(height: 6), - _tagMetaRow(l10n, genres), + // 没有标签就整行跳过,不给标题和简介之间留空档。 + if (tags.isNotEmpty) ...[ + const SizedBox(height: _blockGap), + _tagRow(tags), + ], if (synopsis.isNotEmpty) ...[ - const SizedBox(height: 8), + const SizedBox(height: _blockGap), _synopsis(l10n, synopsis), ], ], ); } - Widget _tagMetaRow(AppLocalizations l10n, List genres) { - final chips = [ + List _tagChips(AppLocalizations l10n, List genres) { + return [ if (isHotDrama(widget.drama)) _TagChip( key: const Key('home-feed-hot-badge'), @@ -125,6 +136,9 @@ class _HomeFeedInfoPanelState extends State { for (final genre in genres) _TagChip(key: Key('home-feed-tag-$genre'), label: genre), ]; + } + + Widget _tagRow(List chips) { return SizedBox( key: const Key('home-feed-tags'), height: 22, @@ -149,10 +163,7 @@ class _HomeFeedInfoPanelState extends State { ); final span = TextSpan( children: [ - TextSpan( - text: prefix, - style: baseStyle, - ), + TextSpan(text: prefix, style: baseStyle), TextSpan( text: ' | ', style: baseStyle.copyWith( @@ -368,7 +379,7 @@ class _TagChip extends StatelessWidget { } } -/// 底部通栏条:🎬 观看完整剧集 全 N 集。 +/// 底部「观看完整剧集」条:浮在底部,不再铺满整屏宽度。 class HomeFeedWatchAllBar extends StatelessWidget { const HomeFeedWatchAllBar({ super.key, @@ -382,42 +393,45 @@ class HomeFeedWatchAllBar extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - return Material( - key: const Key('home-feed-full-series-bg'), - color: AppColors.textPrimary.withValues(alpha: 0.15), - child: InkWell( - key: const Key('home-feed-full-series'), - onTap: onTap, - child: SizedBox( - height: _watchAllHeight, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Row( - children: [ - const Icon( - Icons.movie_rounded, - size: 22, - color: AppColors.onPrimary, - ), - const SizedBox(width: 10), - Expanded( - child: Text( - l10n.watchFullSeries('${drama.episodeCount}'), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: AppColors.textPrimary, - fontSize: 15, - fontWeight: FontWeight.w500, + return ClipRRect( + borderRadius: BorderRadius.circular(_watchAllRadius), + child: Material( + key: const Key('home-feed-full-series-bg'), + color: AppColors.textPrimary.withValues(alpha: 0.15), + child: InkWell( + key: const Key('home-feed-full-series'), + onTap: onTap, + child: SizedBox( + height: _watchAllHeight, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: _watchAllInset), + child: Row( + children: [ + const Icon( + Icons.movie_rounded, + size: 22, + color: AppColors.onPrimary, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + l10n.watchFullSeries('${drama.episodeCount}'), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.textPrimary, + fontSize: 15, + fontWeight: FontWeight.w500, + ), ), ), - ), - const Icon( - Icons.chevron_right_rounded, - size: 22, - color: AppColors.textPrimary, - ), - ], + const Icon( + Icons.chevron_right_rounded, + size: 22, + color: AppColors.textPrimary, + ), + ], + ), ), ), ), diff --git a/lib/pages/player/widgets/player_center_toggle.dart b/lib/pages/player/widgets/player_center_toggle.dart index 78ba374..b72ec66 100644 --- a/lib/pages/player/widgets/player_center_toggle.dart +++ b/lib/pages/player/widgets/player_center_toggle.dart @@ -6,7 +6,9 @@ import '../../../core/theme/app_colors.dart'; const double _toggleSize = 68; const double _iconSize = 36; -const Color _toggleBackground = AppColors.controlSoftScrim; + +/// 播放/暂停压在画面上,垫一层半透明深色圆底保证亮画面下也能看见。 +const Color _toggleBackground = Color(0x66000000); const List _iconShadows = [ Shadow(color: AppColors.shadow, blurRadius: 12), ]; diff --git a/lib/pages/player/widgets/player_overlay.dart b/lib/pages/player/widgets/player_overlay.dart index 3d96604..55ea07c 100644 --- a/lib/pages/player/widgets/player_overlay.dart +++ b/lib/pages/player/widgets/player_overlay.dart @@ -19,8 +19,17 @@ import 'seek_progress_bar.dart'; part 'player_overlay_parts.dart'; -const double _topButtonSize = 36; +const double _topButtonSize = 40; const double _topBarHeight = 58; + +/// 底部信息、选集条、进度条之间的统一行距。 +const double _blockGap = 8; + +/// 底部选集条与右侧清屏按钮的统一高度,两者必须等高。 +const double _bottomActionHeight = 34; + +/// 选集条内部左右留白。 +const double _bottomActionInset = 10; const double _boostHintIconSize = 16; const double _boostHintFontSize = 13; const double _synopsisRailInset = 54; @@ -107,11 +116,12 @@ class PlayerPlaybackOverlay extends StatelessWidget { key: const Key('player-back'), tooltip: data.l10n.backToSeries, icon: Icons.chevron_left_rounded, + iconSize: 30, onPressed: actions.onBack!, ) else const SizedBox(width: _topButtonSize), - const SizedBox(width: 8), + const SizedBox(width: 2), Expanded( child: Text( data.l10n.episodeLabel(data.episode.number), @@ -135,7 +145,7 @@ class PlayerPlaybackOverlay extends StatelessWidget { _TopButton( key: const Key('player-more'), tooltip: data.l10n.more, - icon: Icons.more_vert_rounded, + icon: Icons.more_horiz_rounded, onPressed: actions.onMore, ), ], @@ -226,19 +236,20 @@ class PlayerPlaybackOverlay extends StatelessWidget { child: _bottomInfo(context), ), if (data.boosting) _boostHint(), - _bottomProgress(), - // 加速时收起底栏。拖动进度时底栏只隐藏,占位保留,进度条不挪位。 + // 加速时收起选集条。拖动进度时只隐藏,占位保留,底部不跳动。 scrubbing ? IgnorePointer( child: Opacity(opacity: 0, child: _bottomActions()), ) : Offstage(offstage: data.boosting, child: _bottomActions()), + // 进度条压在最底部,和首页 feed 一致。 + _bottomProgress(), ], ), ); } - /// 加速提示:橙色小三角 + 当前倍速文案,悬在进度条上方。 + /// 加速提示:橙色小三角 + 当前倍速文案,压在底部信息区下方。 Widget _boostHint() { return Padding( key: const Key('player-speed-boost-hint'), @@ -270,25 +281,28 @@ class PlayerPlaybackOverlay extends StatelessWidget { Widget _bottomInfo(BuildContext context) { final series = localizedDramaTitle(data.l10n, data.drama); final synopsis = localizedDramaSynopsis(data.l10n, data.drama); + // 右侧让出互动栏的宽度:剧名和简介一样,收在互动栏左侧,长标题也不会钻到按钮下面。 return Padding( key: const Key('player-controls-bottom'), - padding: const EdgeInsets.fromLTRB(14, 8, 14, 0), + padding: const EdgeInsets.fromLTRB( + 14, + _blockGap, + 14 + _synopsisRailInset, + 0, + ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _detailRow(series), const SizedBox(height: 4), - Padding( - padding: const EdgeInsets.only(right: _synopsisRailInset), - child: _EpisodeSynopsis( - key: const Key('player-episode-synopsis'), - l10n: data.l10n, - drama: data.drama, - episodeNumber: data.episode.number, - synopsis: synopsis, - authorNote: data.authorNote, - ), + _EpisodeSynopsis( + key: const Key('player-episode-synopsis'), + l10n: data.l10n, + drama: data.drama, + episodeNumber: data.episode.number, + synopsis: synopsis, + authorNote: data.authorNote, ), ], ), @@ -296,9 +310,10 @@ class PlayerPlaybackOverlay extends StatelessWidget { } Widget _bottomProgress() { + // 进度条不贴底:底部留 16pt,线心离底边更远,也不再和选集条挤在一起。 return Padding( key: const Key('player-progress-container'), - padding: EdgeInsets.fromLTRB(14, controlsVisible ? 2 : 8, 14, 0), + padding: const EdgeInsets.fromLTRB(14, 2, 14, 16), child: SeekProgressBar( key: const Key('player-progress'), valueListenable: data.controller, @@ -345,10 +360,9 @@ class PlayerPlaybackOverlay extends StatelessWidget { ), ), ), - const SizedBox(width: 4), const Icon( Icons.chevron_right_rounded, - color: AppColors.playerMuted, + color: AppColors.onPrimary, size: 20, shadows: _playerTextShadows, ), @@ -362,55 +376,63 @@ class PlayerPlaybackOverlay extends StatelessWidget { Widget _bottomActions() { return Padding( key: const Key('player-bottom-actions'), - padding: const EdgeInsets.fromLTRB(14, 6, 14, 6), + padding: const EdgeInsets.fromLTRB(14, 2, 14, 0), child: Row( children: [ - Expanded( - child: Material( - key: const Key('player-episodes-pill'), - color: AppColors.playerOverlayPill, - borderRadius: BorderRadius.circular(7), - clipBehavior: Clip.antiAlias, - child: InkWell( - key: const Key('player-episodes'), - onTap: actions.onEpisodes, - borderRadius: BorderRadius.circular(7), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 8, - ), - child: Row( - children: [ - Expanded( - child: Text( - '${data.l10n.episodes} · ' - '${data.l10n.allEpisodesBadge(data.totalEpisodes)} · ' - '${data.l10n.freeWatch}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( + // 清屏后只留右下角的退出按钮,左侧选集条一起收起。 + if (!controlsVisible) + const Spacer() + else ...[ + Expanded( + // 选集条给一层浅底色:视频画面亮时纯文字不好认,也顺带标出可点区域。 + child: Material( + key: const Key('player-episodes-pill'), + color: AppColors.textPrimary.withValues(alpha: 0.14), + borderRadius: BorderRadius.circular(8), + clipBehavior: Clip.antiAlias, + child: InkWell( + key: const Key('player-episodes'), + onTap: actions.onEpisodes, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: _bottomActionInset, + ), + child: SizedBox( + height: _bottomActionHeight, + child: Row( + children: [ + Expanded( + child: Text( + '${data.l10n.episodes} · ' + '${data.l10n.allEpisodesBadge(data.totalEpisodes)} · ' + '${data.l10n.freeWatch}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.onPrimary, + fontSize: 13.5, + fontWeight: FontWeight.w800, + shadows: _playerTextShadows, + ), + ), + ), + const SizedBox(width: 8), + const Icon( + Icons.keyboard_arrow_up_rounded, color: AppColors.onPrimary, - fontSize: 13.5, - fontWeight: FontWeight.w800, + size: 18, shadows: _playerTextShadows, ), - ), + ], ), - const SizedBox(width: 8), - const Icon( - Icons.keyboard_arrow_up_rounded, - color: AppColors.onPrimary, - size: 18, - shadows: _playerTextShadows, - ), - ], + ), ), ), ), ), - ), - const SizedBox(width: 8), + const SizedBox(width: 8), + ], _BottomIconButton( key: const Key('player-bottom-clear-screen'), tooltip: controlsVisible @@ -419,6 +441,7 @@ class PlayerPlaybackOverlay extends StatelessWidget { icon: controlsVisible ? Icons.fullscreen_rounded : Icons.fullscreen_exit_rounded, + height: _bottomActionHeight, onPressed: controlsVisible ? actions.onClear : actions.onExitClear, ), ], diff --git a/lib/pages/player/widgets/player_overlay_parts.dart b/lib/pages/player/widgets/player_overlay_parts.dart index bd8320f..2e01950 100644 --- a/lib/pages/player/widgets/player_overlay_parts.dart +++ b/lib/pages/player/widgets/player_overlay_parts.dart @@ -2,17 +2,20 @@ part of 'player_overlay.dart'; +/// 顶部按钮:直接压在画面上,不加底色,靠文字/图标阴影保证可读性。 class _TopButton extends StatelessWidget { const _TopButton({ super.key, required this.tooltip, required this.icon, required this.onPressed, + this.iconSize = 22, }); final String tooltip; final IconData icon; final VoidCallback onPressed; + final double iconSize; @override Widget build(BuildContext context) { @@ -22,11 +25,11 @@ class _TopButton extends StatelessWidget { tooltip: tooltip, padding: EdgeInsets.zero, style: IconButton.styleFrom( - backgroundColor: AppColors.controlScrim, + backgroundColor: AppColors.transparent, foregroundColor: AppColors.onPrimary, ), onPressed: onPressed, - icon: Icon(icon, size: 18), + icon: Icon(icon, size: iconSize, shadows: _playerTextShadows), ), ); } @@ -41,7 +44,7 @@ class _SpeedEntry extends StatelessWidget { @override Widget build(BuildContext context) { return Material( - color: AppColors.controlScrim, + color: AppColors.transparent, borderRadius: BorderRadius.circular(999), child: InkWell( onTap: onTap, @@ -81,25 +84,30 @@ class _BottomIconButton extends StatelessWidget { required this.tooltip, required this.icon, required this.onPressed, + this.height = 40, }); final String tooltip; final IconData icon; final VoidCallback onPressed; + /// 与左侧选集条等高,两个控件底部齐平。 + final double height; + @override Widget build(BuildContext context) { - return SizedBox.square( - dimension: 40, + return SizedBox( + width: 40, + height: height, child: IconButton( tooltip: tooltip, padding: EdgeInsets.zero, style: IconButton.styleFrom( - backgroundColor: AppColors.playerOverlayPill, + backgroundColor: AppColors.transparent, foregroundColor: AppColors.onPrimary, ), onPressed: onPressed, - icon: Icon(icon, size: 22), + icon: Icon(icon, size: 24, shadows: _playerTextShadows), ), ); } @@ -242,8 +250,10 @@ class _EpisodeSynopsisState extends State<_EpisodeSynopsis> { ), ), const SizedBox(width: 2), - _toggle(widget.l10n.expandSynopsis, - padding: const EdgeInsets.only(left: 6)), + _toggle( + widget.l10n.expandSynopsis, + padding: const EdgeInsets.only(left: 6), + ), ], ), ); @@ -291,12 +301,12 @@ class _EpisodeSynopsisState extends State<_EpisodeSynopsis> { /// 分隔竖线:同字号 w400 + 45% 透明白,靠两侧空格留白。 /// 必须用 TextSpan:WidgetSpan 占位盒的基线对齐会让第n集与简介错位。 TextSpan _separatorSpan() => TextSpan( - text: ' | ', - style: _lineStyle.copyWith( - color: AppColors.onPrimary.withValues(alpha: 0.45), - fontWeight: FontWeight.w400, - ), - ); + text: ' | ', + style: _lineStyle.copyWith( + color: AppColors.onPrimary.withValues(alpha: 0.45), + fontWeight: FontWeight.w400, + ), + ); Widget _toggle(String label, {required EdgeInsets padding}) { return GestureDetector( diff --git a/lib/pages/player/widgets/seek_progress_bar.dart b/lib/pages/player/widgets/seek_progress_bar.dart index 82e8be9..8d12471 100644 --- a/lib/pages/player/widgets/seek_progress_bar.dart +++ b/lib/pages/player/widgets/seek_progress_bar.dart @@ -321,10 +321,8 @@ class _SeekProgressBarState extends State { trackHeight: trackHeight, activeTrackColor: AppColors.blue, inactiveTrackColor: widget.inactiveTrackColor, - thumbColor: AppColors.onPrimary, - thumbShape: RoundSliderThumbShape( - enabledThumbRadius: scrubbing ? 8 : 6, - ), + // 播放页进度条不画滑块圆点:进度只靠蓝色已播段表达。 + thumbShape: SliderComponentShape.noThumb, overlayShape: RoundSliderOverlayShape( overlayRadius: widget.compact ? 12 : 18, ), diff --git a/test/home/home_preview_layout_test.dart b/test/home/home_preview_layout_test.dart index 7f69204..b79ee9d 100644 --- a/test/home/home_preview_layout_test.dart +++ b/test/home/home_preview_layout_test.dart @@ -293,7 +293,10 @@ void main() { expect(find.byKey(const Key('home-feed-synopsis-more')), findsOneWidget); await tester.tap(find.byKey(const Key('home-feed-synopsis-more'))); await tester.pump(); - expect(find.byKey(const Key('home-feed-synopsis-expanded')), findsOneWidget); + expect( + find.byKey(const Key('home-feed-synopsis-expanded')), + findsOneWidget, + ); expect( tester .widget(find.byKey(const Key('home-feed-synopsis-expanded'))) @@ -371,7 +374,7 @@ void main() { find.byKey(const Key('home-feed-progress-inactive')), findsOneWidget, ); - expect(find.byKey(const Key('home-feed-progress-thumb')), findsNothing); + expect(find.byKey(const Key('home-feed-progress-played')), findsNothing); await tester.tap(find.byKey(const Key('rail-share'))); expect(shares, 1); }); @@ -575,26 +578,36 @@ void main() { await tester.pumpAndSettle(); final bar = tester.getRect(find.byKey(const Key('home-feed-full-series'))); - expect(bar.height, 44); + expect(bar.height, 38); final overlayRect = tester.getRect( find.byKey(const Key('home-feed-gesture')), ); - // 通栏观看条铺满浮层宽度,不再左右内缩。 - expect(bar.left, closeTo(overlayRect.left, 0.1)); - expect(bar.width, closeTo(overlayRect.width, 0.1)); - // 进度线中心比观看条顶沿高 8pt。 + // 观看条与底部进度条共用同一组左右边距(各 20pt),宽度一致。 + expect(bar.left, closeTo(overlayRect.left + 20, 0.1)); + expect(bar.width, closeTo(overlayRect.width - 40, 0.1)); expect( - bar.top - - tester - .getCenter(find.byKey(const Key('home-feed-progress-track'))) - .dy, - closeTo(8, 0.1), + bar.left, + closeTo( + tester.getRect(find.byKey(const Key('home-feed-progress'))).left, + 0.1, + ), ); + expect( + bar.width, + closeTo( + tester.getRect(find.byKey(const Key('home-feed-progress'))).width, + 0.1, + ), + ); + // 进度线贴着浮层底部(线心距底 4pt),且位于观看条下方。 + final trackCenter = tester.getCenter( + find.byKey(const Key('home-feed-progress-track')), + ); + expect(overlayRect.bottom - trackCenter.dy, closeTo(4, 0.1)); + expect(trackCenter.dy, greaterThan(bar.bottom)); expect( tester - .widget( - find.byKey(const Key('home-feed-full-series-bg')), - ) + .widget(find.byKey(const Key('home-feed-full-series-bg'))) .color, AppColors.textPrimary.withValues(alpha: 0.15), ); @@ -602,13 +615,9 @@ void main() { tester.getRect(find.textContaining('Watch full series')).center.dy, closeTo(bar.center.dy, 2), ); - expect( - tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))), - const Size(6, 6), - ); expect( tester.getSize(find.byKey(const Key('home-feed-progress'))).height, - greaterThanOrEqualTo(32), + greaterThanOrEqualTo(16), ); expect( tester @@ -620,19 +629,6 @@ void main() { .getRect(find.byKey(const Key('home-feed-progress-track'))) .center .dy; - expect( - tester - .getRect(find.byKey(const Key('home-feed-progress-thumb'))) - .center - .dy, - closeTo( - tester - .getRect(find.byKey(const Key('home-feed-progress-track'))) - .center - .dy, - 0.5, - ), - ); final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('home-feed-progress'))), @@ -704,10 +700,6 @@ void main() { closeTo(trackRect.right - 12, 0.1), ); await tester.pump(const Duration(milliseconds: 130)); - expect( - tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))), - const Size(14, 14), - ); expect( tester.getSize(find.byKey(const Key('home-feed-progress-track'))).height, 6, @@ -719,19 +711,6 @@ void main() { .dy, closeTo(idleTrackCenter, 0.1), ); - expect( - tester - .getRect(find.byKey(const Key('home-feed-progress-thumb'))) - .center - .dy, - closeTo( - tester - .getRect(find.byKey(const Key('home-feed-progress-track'))) - .center - .dy, - 0.5, - ), - ); await gesture.up(); await tester.pumpAndSettle(); expect(scrubVisibility.last, isFalse); @@ -746,10 +725,6 @@ void main() { ); expect(seeked, isNotNull); expect(seeked!.inSeconds, greaterThan(0)); - expect( - tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))), - const Size(6, 6), - ); expect( tester.getSize(find.byKey(const Key('home-feed-progress-track'))).height, 2, @@ -825,11 +800,11 @@ void main() { isBuffering: true, ); await tester.pump(const Duration(milliseconds: 200)); - expect(find.byKey(const Key('home-feed-progress-thumb')), findsOneWidget); + expect(find.byKey(const Key('home-feed-progress-played')), findsOneWidget); expect(find.byKey(const Key('progress-loading')), findsNothing); await tester.pump(const Duration(milliseconds: 200)); - expect(find.byKey(const Key('home-feed-progress-thumb')), findsNothing); + expect(find.byKey(const Key('home-feed-progress-played')), findsNothing); expect(find.byKey(const Key('progress-loading')), findsOneWidget); controller.value = const VideoPlayerValue( @@ -838,7 +813,7 @@ void main() { isInitialized: true, ); await tester.pump(); - expect(find.byKey(const Key('home-feed-progress-thumb')), findsOneWidget); + expect(find.byKey(const Key('home-feed-progress-played')), findsOneWidget); final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('home-feed-progress'))), @@ -853,7 +828,7 @@ void main() { ); await tester.pump(const Duration(milliseconds: 400)); - expect(find.byKey(const Key('home-feed-progress-thumb')), findsOneWidget); + expect(find.byKey(const Key('home-feed-progress-played')), findsOneWidget); expect(find.byKey(const Key('progress-loading')), findsNothing); controller.value = const VideoPlayerValue( @@ -872,7 +847,7 @@ void main() { isBuffering: true, ); await tester.pump(const Duration(milliseconds: 400)); - expect(find.byKey(const Key('home-feed-progress-thumb')), findsOneWidget); + expect(find.byKey(const Key('home-feed-progress-played')), findsOneWidget); expect(find.byKey(const Key('progress-loading')), findsNothing); }); diff --git a/test/player/player_center_toggle_test.dart b/test/player/player_center_toggle_test.dart index 94b051a..66e643b 100644 --- a/test/player/player_center_toggle_test.dart +++ b/test/player/player_center_toggle_test.dart @@ -35,12 +35,12 @@ void main() { Key('player-play-toggle'), ]) { expect(tester.getSize(find.byKey(key)), const Size(68, 68)); - // 圆形底改为半透明黑色,不再是完全透明或蓝色实底。 + // 圆形深色底:压在亮画面上也能看见播放/暂停。 final container = tester.widget( find.descendant(of: find.byKey(key), matching: find.byType(Container)), ); final decoration = container.decoration! as BoxDecoration; - expect(decoration.color, Colors.black26); + expect(decoration.color, const Color(0x66000000)); expect(decoration.shape, BoxShape.circle); final icon = tester.widget( find.descendant(of: find.byKey(key), matching: find.byType(Icon)), diff --git a/test/player/player_fullscreen_test.dart b/test/player/player_fullscreen_test.dart index 045a1b1..f9222b3 100644 --- a/test/player/player_fullscreen_test.dart +++ b/test/player/player_fullscreen_test.dart @@ -983,9 +983,11 @@ void main() { expect(find.byKey(const Key('player-video-tap')), findsOneWidget); expect(find.byKey(const Key('player-bottom-clear-screen')), findsOneWidget); expect(find.byTooltip('Exit clear screen'), findsOneWidget); + // 清屏后左侧选集条一起收起,只留右下角的退出按钮。 + expect(find.byKey(const Key('player-episodes-pill')), findsNothing); expect( tester.getSize(find.byKey(const Key('player-bottom-clear-screen'))), - const Size(40, 40), + const Size(40, 34), ); await tester.tapAt(const Offset(80, 300)); @@ -1100,8 +1102,8 @@ void main() { expect(find.byKey(const Key('player-speed-boost-hint')), findsNothing); expect(find.byKey(const Key('player-interaction-rail')), findsNothing); expect(find.byTooltip('Exit clear screen'), findsOneWidget); - // 清屏态本来显示的选集胶囊与右下按钮,在加速结束后恢复。 - expect(find.byKey(const Key('player-episodes-pill')), findsOneWidget); + // 清屏态只恢复右下退出按钮;左侧选集条在清屏下一直不显示。 + expect(find.byKey(const Key('player-episodes-pill')), findsNothing); expect(find.byKey(const Key('player-bottom-clear-screen')), findsOneWidget); expect( find.descendant( @@ -1118,7 +1120,7 @@ void main() { expect( tester.getSize(find.byKey(const Key('player-back'))), - const Size(36, 36), + const Size(40, 40), ); expect(tester.takeException(), isNull); }); @@ -1160,17 +1162,12 @@ void main() { .child, isNotNull, ); + // 选集条给一层浅底色,标出可点区域。 expect( tester .widget(find.byKey(const Key('player-episodes-pill'))) .color, - AppColors.playerOverlayPill, - ); - expect( - tester - .widget(find.byKey(const Key('player-episodes-pill'))) - .borderRadius, - BorderRadius.circular(7), + AppColors.textPrimary.withValues(alpha: 0.14), ); expect( tester @@ -1186,9 +1183,14 @@ void main() { ?.fontSize, 13.5, ); + // 清屏按钮与左侧选集条等高。 expect( tester.getSize(find.byKey(const Key('player-bottom-clear-screen'))), - const Size(40, 40), + const Size(40, 34), + ); + expect( + tester.getSize(find.byKey(const Key('player-episodes-pill'))).height, + 34, ); expect(find.byKey(const Key('progress-loading')), findsNothing); expect(find.byKey(const Key('progress-inactive')), findsOneWidget); @@ -1289,7 +1291,8 @@ void main() { matching: find.byIcon(Icons.keyboard_arrow_up_rounded), ), ); - expect(pill.right - arrow.right, closeTo(14, 0.5)); + // 选集条左右各留 10pt 内边距,箭头不再贴边。 + expect(pill.right - arrow.right, closeTo(10, 0.5)); expect(arrow.left - label.right, closeTo(8, 0.5)); }); @@ -1551,7 +1554,8 @@ void main() { ), ); expect(chevron.left - title.right, lessThanOrEqualTo(6)); - expect(chevron.right, lessThanOrEqualTo(320 - 14)); + // 剧名和简介一样收在互动栏左侧,不再铺到屏幕最右边。 + expect(chevron.right, lessThanOrEqualTo(320 - 14 - 54)); final synopsis = tester.widget( find.byKey(const Key('player-synopsis')), ); @@ -1647,11 +1651,11 @@ void main() { await tester.tap(find.byKey(const Key('player-episodes'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('player-episodes-sheet')), findsOneWidget); - expect(find.byKey(const Key('player-episodes-close')), findsOneWidget); + expect(find.byKey(const Key('drama-info-sheet')), findsOneWidget); + expect(find.byKey(const Key('drama-info-close')), findsOneWidget); final grid = tester.widget( find.descendant( - of: find.byKey(const Key('player-episodes-sheet')), + of: find.byKey(const Key('drama-info-sheet')), matching: find.byType(GridView), ), ); @@ -1659,14 +1663,14 @@ void main() { grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount; expect(delegate.crossAxisCount, 6); final overlay = tester.widget>( - find.byKey(const Key('player-episodes-system-overlay')), + find.byKey(const Key('drama-info-system-overlay')), ); expect(overlay.value.systemNavigationBarColor, AppColors.card); expect(overlay.value.systemNavigationBarContrastEnforced, isFalse); final safeArea = tester.widget( find .descendant( - of: find.byKey(const Key('player-episodes-sheet')), + of: find.byKey(const Key('drama-info-sheet')), matching: find.byType(SafeArea), ) .first, @@ -2030,7 +2034,7 @@ void _expectFullscreenOverlay(WidgetTester tester) { expect(find.byKey(const Key('player-synopsis')), findsOneWidget); expect(find.byKey(const Key('player-more')), findsOneWidget); for (final key in const [Key('player-back'), Key('player-more')]) { - expect(tester.getSize(find.byKey(key)), const Size(36, 36)); + expect(tester.getSize(find.byKey(key)), const Size(40, 40)); } expect(find.byKey(const Key('player-play-toggle')), findsNothing); expect(find.byKey(const Key('player-episodes')), findsOneWidget); diff --git a/test/player/seek_scrub_visibility_test.dart b/test/player/seek_scrub_visibility_test.dart index f670ee1..69d31ed 100644 --- a/test/player/seek_scrub_visibility_test.dart +++ b/test/player/seek_scrub_visibility_test.dart @@ -37,11 +37,8 @@ void main() { expect(find.byKey(const Key('scrub-overlay')), findsNothing); expect(harness.scrubStarts, 1); expect(_sliderTheme(tester).trackHeight, 6); - expect( - (_sliderTheme(tester).thumbShape! as RoundSliderThumbShape) - .enabledThumbRadius, - 8, - ); + // 不画滑块圆点,进度只靠已播段表达。 + expect(_sliderTheme(tester).thumbShape, SliderComponentShape.noThumb); await gesture.moveBy(const Offset(72, 0)); await tester.pump(); @@ -67,11 +64,7 @@ void main() { expect(harness.scrubEnds, 1); expect(harness.seeks, 1); expect(_sliderTheme(tester).trackHeight, 2); - expect( - (_sliderTheme(tester).thumbShape! as RoundSliderThumbShape) - .enabledThumbRadius, - 6, - ); + expect(_sliderTheme(tester).thumbShape, SliderComponentShape.noThumb); }); testWidgets('feed progress tap does not clear overlay content', ( @@ -154,29 +147,27 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('feed progress thumb stays on the finger when dragged right', ( - tester, - ) async { - await _pumpFeedOverlay(tester); - final bar = find.byKey(const Key('home-feed-progress')); - final gesture = await tester.startGesture(tester.getCenter(bar)); + testWidgets( + 'feed progress played segment follows the finger when dragged right', + (tester) async { + await _pumpFeedOverlay(tester); + final bar = find.byKey(const Key('home-feed-progress')); + final gesture = await tester.startGesture(tester.getCenter(bar)); - await gesture.moveBy(const Offset(800, 0)); - await tester.pump(); + await gesture.moveBy(const Offset(800, 0)); + await tester.pump(); - final thumbCenter = tester - .getRect(find.byKey(const Key('home-feed-progress-thumb'))) - .center - .dx; - final playedRight = tester - .getRect(find.byKey(const Key('home-feed-progress-played'))) - .right; - expect(thumbCenter, closeTo(tester.getRect(bar).right, 0.5)); - expect(playedRight, closeTo(thumbCenter, 0.5)); + expect( + tester + .getRect(find.byKey(const Key('home-feed-progress-played'))) + .right, + closeTo(tester.getRect(bar).right, 0.5), + ); - await gesture.up(); - await tester.pumpAndSettle(); - }); + await gesture.up(); + await tester.pumpAndSettle(); + }, + ); testWidgets('full player drag clears chrome and release restores it', ( tester,