style(player): 播放页与首页浮层的控件样式统一

播放页顶部按钮与选集条去掉底色、进度条不再画滑块圆点并压到最底部;
剧名与简介收在互动栏左侧,清屏后只留右下角退出按钮。
首页观看条与进度条共用 20pt 页边距,进度槽底色与播放页一致。
This commit is contained in:
Vicky authored and trae-solo-agent committed 2026-10-29 15:44:22 +08:00
1 parent d5bcda54d3
commit 543d0aadd5
12 files changed
+213 -212

No files matched your search

@@ -4,6 +4,10 @@ import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
const List<Shadow> _iconShadows = [
Shadow(color: AppColors.shadow, blurRadius: 6, offset: Offset(0, 1)),
];
/// Shared floating action used to leave clear-screen mode. /// Shared floating action used to leave clear-screen mode.
class ExitClearScreenButton extends StatelessWidget { class ExitClearScreenButton extends StatelessWidget {
const ExitClearScreenButton({ const ExitClearScreenButton({
@@ -25,11 +29,15 @@ class ExitClearScreenButton extends StatelessWidget {
onPressed: onPressed, onPressed: onPressed,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: AppColors.controlScrim, backgroundColor: AppColors.transparent,
foregroundColor: AppColors.onPrimary, foregroundColor: AppColors.onPrimary,
shape: const CircleBorder(), shape: const CircleBorder(),
), ),
icon: const Icon(Icons.fullscreen_exit_rounded, size: 18), icon: const Icon(
Icons.fullscreen_exit_rounded,
size: 22,
shadows: _iconShadows,
),
), ),
); );
} }
+30 -27
View File
@@ -21,13 +21,16 @@ import 'scrub_preview_card.dart';
part 'home_feed_overlay_parts.dart'; part 'home_feed_overlay_parts.dart';
const double _progressHeight = 2; const double _progressHeight = 2;
const double _progressHitHeight = 32;
/// 进度条触控带高度:没有滑块圆点后收到 16pt,够手指按住这条线拖动。
const double _progressHitHeight = 16;
const double _pageHorizontal = 20; const double _pageHorizontal = 20;
const double _railRight = 14; const double _railRight = 14;
const double _railGap = 12; const double _railGap = 12;
// 观看条高 44;2pt 进度线视觉上沿距观看条顶约 7pt(线中心距进度条盒底 8)。 // 进度线贴着屏幕底部(线心距底 4pt),观看条底边只留 8pt,视觉上就是线上一根条。
// 信息面板底边取 44+16=60,使简介↔进度线、进度线↔观看条两侧间距均为 7pt。 const double _watchAllBottomInset = 8;
const double _bottomInfoInset = homeFeedWatchAllHeight + 16; // 信息面板底边直接贴住观看条顶沿,中间不留缝。
const double _bottomInfoInset = _watchAllBottomInset + homeFeedWatchAllHeight;
const double _railBottomOffset = 40; const double _railBottomOffset = 40;
const Duration _overlayFadeDuration = Duration(milliseconds: 120); const Duration _overlayFadeDuration = Duration(milliseconds: 120);
const List<Shadow> _boostTextShadows = [ const List<Shadow> _boostTextShadows = [
@@ -152,7 +155,7 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
bottom: homeFeedWatchAllHeight + 12, bottom: _watchAllBottomInset + homeFeedWatchAllHeight + 12,
child: AnimatedOpacity( child: AnimatedOpacity(
duration: _overlayFadeDuration, duration: _overlayFadeDuration,
opacity: widget.boosting ? 1 : 0, opacity: widget.boosting ? 1 : 0,
@@ -162,10 +165,30 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
), ),
), ),
), ),
// 观看条在下、进度条在上:进度条 32pt 触控盒要盖住观看条下沿的空白,
// 否则拖拽 seek 的起点会被观看条吞掉。两块的可见区域(文字/进度线)互不遮挡。
Positioned( Positioned(
left: _pageHorizontal, left: _pageHorizontal,
right: _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( child: _FeedProgressBar(
controller: widget.controller, controller: widget.controller,
locked: widget.locked, locked: widget.locked,
@@ -182,27 +205,7 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
onSeek: widget.onSeek, 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( Positioned(
left: _pageHorizontal, left: _pageHorizontal,
right: _railRight, right: _railRight,
@@ -2,10 +2,9 @@
part of 'home_feed_overlay.dart'; part of 'home_feed_overlay.dart';
const double _thumbSize = 6;
const double _thumbActiveSize = 14;
const double _trackActiveHeight = 6; const double _trackActiveHeight = 6;
const double _lineCenterFromBottom = 8; // 进度线贴住屏幕底部(线心距底 4pt);播放进度只靠蓝色已播段表达,不画滑块圆点。
const double _lineCenterFromBottom = 4;
const double _timeBubbleWidth = 96; const double _timeBubbleWidth = 96;
const double _timeBubbleMargin = 12; const double _timeBubbleMargin = 12;
const Key _inactiveTrackKey = Key('home-feed-progress-inactive'); const Key _inactiveTrackKey = Key('home-feed-progress-inactive');
@@ -305,7 +304,7 @@ class _FeedProgressBarState extends State<_FeedProgressBar> {
height: widget.height, height: widget.height,
child: ColoredBox( child: ColoredBox(
key: _inactiveTrackKey, key: _inactiveTrackKey,
color: AppColors.border, color: AppColors.inactiveTrack,
), ),
), ),
), ),
@@ -345,20 +344,11 @@ class _FeedProgressBarState extends State<_FeedProgressBar> {
Widget _track(double fraction, double width) { Widget _track(double fraction, double width) {
final dragging = _preview != null; final dragging = _preview != null;
final trackHeight = dragging ? _trackActiveHeight : widget.height; final trackHeight = dragging ? _trackActiveHeight : widget.height;
final size = dragging ? _thumbActiveSize : _thumbSize; final playedWidth = (width * fraction).clamp(0.0, width).toDouble();
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 trackSlotBottom = _lineCenterFromBottom - _trackActiveHeight / 2; final trackSlotBottom = _lineCenterFromBottom - _trackActiveHeight / 2;
final thumbSlotBottom = _lineCenterFromBottom - _thumbActiveSize / 2;
return SizedBox( return SizedBox(
height: widget.hitHeight, height: widget.hitHeight,
child: Stack( child: Stack(
clipBehavior: Clip.none,
children: [ children: [
Positioned( Positioned(
left: 0, left: 0,
@@ -374,7 +364,8 @@ class _FeedProgressBarState extends State<_FeedProgressBar> {
key: const Key('home-feed-progress-track'), key: const Key('home-feed-progress-track'),
width: width, width: width,
height: trackHeight, height: trackHeight,
color: AppColors.border, // 和剧集播放页进度条共用同一个槽底色,两个入口观感一致。
color: AppColors.inactiveTrack,
), ),
), ),
Align( 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,
),
),
),
),
),
], ],
), ),
); );
@@ -13,7 +13,14 @@ import 'interaction_rail.dart';
const double _seriesFont = 17; const double _seriesFont = 17;
const double _tagFont = 12; const double _tagFont = 12;
const double _synopsisFont = 14; 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<Shadow> _textShadows = [ const List<Shadow> _textShadows = [
Shadow(color: AppColors.shadow, blurRadius: 6, offset: Offset(0, 1)), Shadow(color: AppColors.shadow, blurRadius: 6, offset: Offset(0, 1)),
]; ];
@@ -57,6 +64,7 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
final dramaTitle = localizedDramaTitle(l10n, widget.drama); final dramaTitle = localizedDramaTitle(l10n, widget.drama);
final synopsis = localizedDramaSynopsis(l10n, widget.drama).trim(); final synopsis = localizedDramaSynopsis(l10n, widget.drama).trim();
final genres = _genreValues(Localizations.localeOf(context).languageCode); final genres = _genreValues(Localizations.localeOf(context).languageCode);
final tags = _tagChips(l10n, genres);
return Column( return Column(
key: const Key('home-feed-info-panel'), key: const Key('home-feed-info-panel'),
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -104,18 +112,21 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
], ],
), ),
), ),
const SizedBox(height: 6), // 没有标签就整行跳过,不给标题和简介之间留空档。
_tagMetaRow(l10n, genres), if (tags.isNotEmpty) ...[
const SizedBox(height: _blockGap),
_tagRow(tags),
],
if (synopsis.isNotEmpty) ...[ if (synopsis.isNotEmpty) ...[
const SizedBox(height: 8), const SizedBox(height: _blockGap),
_synopsis(l10n, synopsis), _synopsis(l10n, synopsis),
], ],
], ],
); );
} }
Widget _tagMetaRow(AppLocalizations l10n, List<String> genres) { List<Widget> _tagChips(AppLocalizations l10n, List<String> genres) {
final chips = <Widget>[ return [
if (isHotDrama(widget.drama)) if (isHotDrama(widget.drama))
_TagChip( _TagChip(
key: const Key('home-feed-hot-badge'), key: const Key('home-feed-hot-badge'),
@@ -125,6 +136,9 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
for (final genre in genres) for (final genre in genres)
_TagChip(key: Key('home-feed-tag-$genre'), label: genre), _TagChip(key: Key('home-feed-tag-$genre'), label: genre),
]; ];
}
Widget _tagRow(List<Widget> chips) {
return SizedBox( return SizedBox(
key: const Key('home-feed-tags'), key: const Key('home-feed-tags'),
height: 22, height: 22,
@@ -149,10 +163,7 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
); );
final span = TextSpan( final span = TextSpan(
children: [ children: [
TextSpan( TextSpan(text: prefix, style: baseStyle),
text: prefix,
style: baseStyle,
),
TextSpan( TextSpan(
text: ' | ', text: ' | ',
style: baseStyle.copyWith( style: baseStyle.copyWith(
@@ -368,7 +379,7 @@ class _TagChip extends StatelessWidget {
} }
} }
/// 底部通栏条:🎬 观看完整剧集 全 N 集。 /// 底部「观看完整剧集」条:浮在底部,不再铺满整屏宽度。
class HomeFeedWatchAllBar extends StatelessWidget { class HomeFeedWatchAllBar extends StatelessWidget {
const HomeFeedWatchAllBar({ const HomeFeedWatchAllBar({
super.key, super.key,
@@ -382,7 +393,9 @@ class HomeFeedWatchAllBar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
return Material( return ClipRRect(
borderRadius: BorderRadius.circular(_watchAllRadius),
child: Material(
key: const Key('home-feed-full-series-bg'), key: const Key('home-feed-full-series-bg'),
color: AppColors.textPrimary.withValues(alpha: 0.15), color: AppColors.textPrimary.withValues(alpha: 0.15),
child: InkWell( child: InkWell(
@@ -391,7 +404,7 @@ class HomeFeedWatchAllBar extends StatelessWidget {
child: SizedBox( child: SizedBox(
height: _watchAllHeight, height: _watchAllHeight,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: _watchAllInset),
child: Row( child: Row(
children: [ children: [
const Icon( const Icon(
@@ -422,6 +435,7 @@ class HomeFeedWatchAllBar extends StatelessWidget {
), ),
), ),
), ),
),
); );
} }
} }
@@ -6,7 +6,9 @@ import '../../../core/theme/app_colors.dart';
const double _toggleSize = 68; const double _toggleSize = 68;
const double _iconSize = 36; const double _iconSize = 36;
const Color _toggleBackground = AppColors.controlSoftScrim;
/// 播放/暂停压在画面上,垫一层半透明深色圆底保证亮画面下也能看见。
const Color _toggleBackground = Color(0x66000000);
const List<Shadow> _iconShadows = [ const List<Shadow> _iconShadows = [
Shadow(color: AppColors.shadow, blurRadius: 12), Shadow(color: AppColors.shadow, blurRadius: 12),
]; ];
+43 -20
View File
@@ -19,8 +19,17 @@ import 'seek_progress_bar.dart';
part 'player_overlay_parts.dart'; part 'player_overlay_parts.dart';
const double _topButtonSize = 36; const double _topButtonSize = 40;
const double _topBarHeight = 58; const double _topBarHeight = 58;
/// 底部信息、选集条、进度条之间的统一行距。
const double _blockGap = 8;
/// 底部选集条与右侧清屏按钮的统一高度,两者必须等高。
const double _bottomActionHeight = 34;
/// 选集条内部左右留白。
const double _bottomActionInset = 10;
const double _boostHintIconSize = 16; const double _boostHintIconSize = 16;
const double _boostHintFontSize = 13; const double _boostHintFontSize = 13;
const double _synopsisRailInset = 54; const double _synopsisRailInset = 54;
@@ -107,11 +116,12 @@ class PlayerPlaybackOverlay extends StatelessWidget {
key: const Key('player-back'), key: const Key('player-back'),
tooltip: data.l10n.backToSeries, tooltip: data.l10n.backToSeries,
icon: Icons.chevron_left_rounded, icon: Icons.chevron_left_rounded,
iconSize: 30,
onPressed: actions.onBack!, onPressed: actions.onBack!,
) )
else else
const SizedBox(width: _topButtonSize), const SizedBox(width: _topButtonSize),
const SizedBox(width: 8), const SizedBox(width: 2),
Expanded( Expanded(
child: Text( child: Text(
data.l10n.episodeLabel(data.episode.number), data.l10n.episodeLabel(data.episode.number),
@@ -135,7 +145,7 @@ class PlayerPlaybackOverlay extends StatelessWidget {
_TopButton( _TopButton(
key: const Key('player-more'), key: const Key('player-more'),
tooltip: data.l10n.more, tooltip: data.l10n.more,
icon: Icons.more_vert_rounded, icon: Icons.more_horiz_rounded,
onPressed: actions.onMore, onPressed: actions.onMore,
), ),
], ],
@@ -226,19 +236,20 @@ class PlayerPlaybackOverlay extends StatelessWidget {
child: _bottomInfo(context), child: _bottomInfo(context),
), ),
if (data.boosting) _boostHint(), if (data.boosting) _boostHint(),
_bottomProgress(), // 加速时收起选集条。拖动进度时只隐藏,占位保留,底部不跳动。
// 加速时收起底栏。拖动进度时底栏只隐藏,占位保留,进度条不挪位。
scrubbing scrubbing
? IgnorePointer( ? IgnorePointer(
child: Opacity(opacity: 0, child: _bottomActions()), child: Opacity(opacity: 0, child: _bottomActions()),
) )
: Offstage(offstage: data.boosting, child: _bottomActions()), : Offstage(offstage: data.boosting, child: _bottomActions()),
// 进度条压在最底部,和首页 feed 一致。
_bottomProgress(),
], ],
), ),
); );
} }
/// 加速提示:橙色小三角 + 当前倍速文案,悬在进度条上方。 /// 加速提示:橙色小三角 + 当前倍速文案,压在底部信息区下方。
Widget _boostHint() { Widget _boostHint() {
return Padding( return Padding(
key: const Key('player-speed-boost-hint'), key: const Key('player-speed-boost-hint'),
@@ -270,18 +281,22 @@ class PlayerPlaybackOverlay extends StatelessWidget {
Widget _bottomInfo(BuildContext context) { Widget _bottomInfo(BuildContext context) {
final series = localizedDramaTitle(data.l10n, data.drama); final series = localizedDramaTitle(data.l10n, data.drama);
final synopsis = localizedDramaSynopsis(data.l10n, data.drama); final synopsis = localizedDramaSynopsis(data.l10n, data.drama);
// 右侧让出互动栏的宽度:剧名和简介一样,收在互动栏左侧,长标题也不会钻到按钮下面。
return Padding( return Padding(
key: const Key('player-controls-bottom'), 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( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_detailRow(series), _detailRow(series),
const SizedBox(height: 4), const SizedBox(height: 4),
Padding( _EpisodeSynopsis(
padding: const EdgeInsets.only(right: _synopsisRailInset),
child: _EpisodeSynopsis(
key: const Key('player-episode-synopsis'), key: const Key('player-episode-synopsis'),
l10n: data.l10n, l10n: data.l10n,
drama: data.drama, drama: data.drama,
@@ -289,16 +304,16 @@ class PlayerPlaybackOverlay extends StatelessWidget {
synopsis: synopsis, synopsis: synopsis,
authorNote: data.authorNote, authorNote: data.authorNote,
), ),
),
], ],
), ),
); );
} }
Widget _bottomProgress() { Widget _bottomProgress() {
// 进度条不贴底:底部留 16pt,线心离底边更远,也不再和选集条挤在一起。
return Padding( return Padding(
key: const Key('player-progress-container'), 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( child: SeekProgressBar(
key: const Key('player-progress'), key: const Key('player-progress'),
valueListenable: data.controller, valueListenable: data.controller,
@@ -345,10 +360,9 @@ class PlayerPlaybackOverlay extends StatelessWidget {
), ),
), ),
), ),
const SizedBox(width: 4),
const Icon( const Icon(
Icons.chevron_right_rounded, Icons.chevron_right_rounded,
color: AppColors.playerMuted, color: AppColors.onPrimary,
size: 20, size: 20,
shadows: _playerTextShadows, shadows: _playerTextShadows,
), ),
@@ -362,24 +376,30 @@ class PlayerPlaybackOverlay extends StatelessWidget {
Widget _bottomActions() { Widget _bottomActions() {
return Padding( return Padding(
key: const Key('player-bottom-actions'), key: const Key('player-bottom-actions'),
padding: const EdgeInsets.fromLTRB(14, 6, 14, 6), padding: const EdgeInsets.fromLTRB(14, 2, 14, 0),
child: Row( child: Row(
children: [ children: [
// 清屏后只留右下角的退出按钮,左侧选集条一起收起。
if (!controlsVisible)
const Spacer()
else ...[
Expanded( Expanded(
// 选集条给一层浅底色:视频画面亮时纯文字不好认,也顺带标出可点区域。
child: Material( child: Material(
key: const Key('player-episodes-pill'), key: const Key('player-episodes-pill'),
color: AppColors.playerOverlayPill, color: AppColors.textPrimary.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(7), borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: InkWell( child: InkWell(
key: const Key('player-episodes'), key: const Key('player-episodes'),
onTap: actions.onEpisodes, onTap: actions.onEpisodes,
borderRadius: BorderRadius.circular(7), borderRadius: BorderRadius.circular(8),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 14, horizontal: _bottomActionInset,
vertical: 8,
), ),
child: SizedBox(
height: _bottomActionHeight,
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
@@ -410,7 +430,9 @@ class PlayerPlaybackOverlay extends StatelessWidget {
), ),
), ),
), ),
),
const SizedBox(width: 8), const SizedBox(width: 8),
],
_BottomIconButton( _BottomIconButton(
key: const Key('player-bottom-clear-screen'), key: const Key('player-bottom-clear-screen'),
tooltip: controlsVisible tooltip: controlsVisible
@@ -419,6 +441,7 @@ class PlayerPlaybackOverlay extends StatelessWidget {
icon: controlsVisible icon: controlsVisible
? Icons.fullscreen_rounded ? Icons.fullscreen_rounded
: Icons.fullscreen_exit_rounded, : Icons.fullscreen_exit_rounded,
height: _bottomActionHeight,
onPressed: controlsVisible ? actions.onClear : actions.onExitClear, onPressed: controlsVisible ? actions.onClear : actions.onExitClear,
), ),
], ],
@@ -2,17 +2,20 @@
part of 'player_overlay.dart'; part of 'player_overlay.dart';
/// 顶部按钮:直接压在画面上,不加底色,靠文字/图标阴影保证可读性。
class _TopButton extends StatelessWidget { class _TopButton extends StatelessWidget {
const _TopButton({ const _TopButton({
super.key, super.key,
required this.tooltip, required this.tooltip,
required this.icon, required this.icon,
required this.onPressed, required this.onPressed,
this.iconSize = 22,
}); });
final String tooltip; final String tooltip;
final IconData icon; final IconData icon;
final VoidCallback onPressed; final VoidCallback onPressed;
final double iconSize;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -22,11 +25,11 @@ class _TopButton extends StatelessWidget {
tooltip: tooltip, tooltip: tooltip,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: AppColors.controlScrim, backgroundColor: AppColors.transparent,
foregroundColor: AppColors.onPrimary, foregroundColor: AppColors.onPrimary,
), ),
onPressed: onPressed, onPressed: onPressed,
icon: Icon(icon, size: 18), icon: Icon(icon, size: iconSize, shadows: _playerTextShadows),
), ),
); );
} }
@@ -41,7 +44,7 @@ class _SpeedEntry extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
color: AppColors.controlScrim, color: AppColors.transparent,
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
@@ -81,25 +84,30 @@ class _BottomIconButton extends StatelessWidget {
required this.tooltip, required this.tooltip,
required this.icon, required this.icon,
required this.onPressed, required this.onPressed,
this.height = 40,
}); });
final String tooltip; final String tooltip;
final IconData icon; final IconData icon;
final VoidCallback onPressed; final VoidCallback onPressed;
/// 与左侧选集条等高,两个控件底部齐平。
final double height;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox.square( return SizedBox(
dimension: 40, width: 40,
height: height,
child: IconButton( child: IconButton(
tooltip: tooltip, tooltip: tooltip,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: AppColors.playerOverlayPill, backgroundColor: AppColors.transparent,
foregroundColor: AppColors.onPrimary, foregroundColor: AppColors.onPrimary,
), ),
onPressed: onPressed, 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), const SizedBox(width: 2),
_toggle(widget.l10n.expandSynopsis, _toggle(
padding: const EdgeInsets.only(left: 6)), widget.l10n.expandSynopsis,
padding: const EdgeInsets.only(left: 6),
),
], ],
), ),
); );
@@ -321,10 +321,8 @@ class _SeekProgressBarState extends State<SeekProgressBar> {
trackHeight: trackHeight, trackHeight: trackHeight,
activeTrackColor: AppColors.blue, activeTrackColor: AppColors.blue,
inactiveTrackColor: widget.inactiveTrackColor, inactiveTrackColor: widget.inactiveTrackColor,
thumbColor: AppColors.onPrimary, // 播放页进度条不画滑块圆点:进度只靠蓝色已播段表达。
thumbShape: RoundSliderThumbShape( thumbShape: SliderComponentShape.noThumb,
enabledThumbRadius: scrubbing ? 8 : 6,
),
overlayShape: RoundSliderOverlayShape( overlayShape: RoundSliderOverlayShape(
overlayRadius: widget.compact ? 12 : 18, overlayRadius: widget.compact ? 12 : 18,
), ),
+34 -59
View File
@@ -293,7 +293,10 @@ void main() {
expect(find.byKey(const Key('home-feed-synopsis-more')), findsOneWidget); expect(find.byKey(const Key('home-feed-synopsis-more')), findsOneWidget);
await tester.tap(find.byKey(const Key('home-feed-synopsis-more'))); await tester.tap(find.byKey(const Key('home-feed-synopsis-more')));
await tester.pump(); await tester.pump();
expect(find.byKey(const Key('home-feed-synopsis-expanded')), findsOneWidget); expect(
find.byKey(const Key('home-feed-synopsis-expanded')),
findsOneWidget,
);
expect( expect(
tester tester
.widget<Text>(find.byKey(const Key('home-feed-synopsis-expanded'))) .widget<Text>(find.byKey(const Key('home-feed-synopsis-expanded')))
@@ -371,7 +374,7 @@ void main() {
find.byKey(const Key('home-feed-progress-inactive')), find.byKey(const Key('home-feed-progress-inactive')),
findsOneWidget, 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'))); await tester.tap(find.byKey(const Key('rail-share')));
expect(shares, 1); expect(shares, 1);
}); });
@@ -575,26 +578,36 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final bar = tester.getRect(find.byKey(const Key('home-feed-full-series'))); final bar = tester.getRect(find.byKey(const Key('home-feed-full-series')));
expect(bar.height, 44); expect(bar.height, 38);
final overlayRect = tester.getRect( final overlayRect = tester.getRect(
find.byKey(const Key('home-feed-gesture')), find.byKey(const Key('home-feed-gesture')),
); );
// 通栏观看条铺满浮层宽度,不再左右内缩。 // 观看条与底部进度条共用同一组左右边距(各 20pt),宽度一致。
expect(bar.left, closeTo(overlayRect.left, 0.1)); expect(bar.left, closeTo(overlayRect.left + 20, 0.1));
expect(bar.width, closeTo(overlayRect.width, 0.1)); expect(bar.width, closeTo(overlayRect.width - 40, 0.1));
// 进度线中心比观看条顶沿高 8pt。
expect( expect(
bar.top - bar.left,
tester closeTo(
.getCenter(find.byKey(const Key('home-feed-progress-track'))) tester.getRect(find.byKey(const Key('home-feed-progress'))).left,
.dy, 0.1,
closeTo(8, 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( expect(
tester tester
.widget<Material>( .widget<Material>(find.byKey(const Key('home-feed-full-series-bg')))
find.byKey(const Key('home-feed-full-series-bg')),
)
.color, .color,
AppColors.textPrimary.withValues(alpha: 0.15), AppColors.textPrimary.withValues(alpha: 0.15),
); );
@@ -602,13 +615,9 @@ void main() {
tester.getRect(find.textContaining('Watch full series')).center.dy, tester.getRect(find.textContaining('Watch full series')).center.dy,
closeTo(bar.center.dy, 2), closeTo(bar.center.dy, 2),
); );
expect(
tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))),
const Size(6, 6),
);
expect( expect(
tester.getSize(find.byKey(const Key('home-feed-progress'))).height, tester.getSize(find.byKey(const Key('home-feed-progress'))).height,
greaterThanOrEqualTo(32), greaterThanOrEqualTo(16),
); );
expect( expect(
tester tester
@@ -620,19 +629,6 @@ void main() {
.getRect(find.byKey(const Key('home-feed-progress-track'))) .getRect(find.byKey(const Key('home-feed-progress-track')))
.center .center
.dy; .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( final gesture = await tester.startGesture(
tester.getCenter(find.byKey(const Key('home-feed-progress'))), tester.getCenter(find.byKey(const Key('home-feed-progress'))),
@@ -704,10 +700,6 @@ void main() {
closeTo(trackRect.right - 12, 0.1), closeTo(trackRect.right - 12, 0.1),
); );
await tester.pump(const Duration(milliseconds: 130)); await tester.pump(const Duration(milliseconds: 130));
expect(
tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))),
const Size(14, 14),
);
expect( expect(
tester.getSize(find.byKey(const Key('home-feed-progress-track'))).height, tester.getSize(find.byKey(const Key('home-feed-progress-track'))).height,
6, 6,
@@ -719,19 +711,6 @@ void main() {
.dy, .dy,
closeTo(idleTrackCenter, 0.1), 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 gesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(scrubVisibility.last, isFalse); expect(scrubVisibility.last, isFalse);
@@ -746,10 +725,6 @@ void main() {
); );
expect(seeked, isNotNull); expect(seeked, isNotNull);
expect(seeked!.inSeconds, greaterThan(0)); expect(seeked!.inSeconds, greaterThan(0));
expect(
tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))),
const Size(6, 6),
);
expect( expect(
tester.getSize(find.byKey(const Key('home-feed-progress-track'))).height, tester.getSize(find.byKey(const Key('home-feed-progress-track'))).height,
2, 2,
@@ -825,11 +800,11 @@ void main() {
isBuffering: true, isBuffering: true,
); );
await tester.pump(const Duration(milliseconds: 200)); 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); expect(find.byKey(const Key('progress-loading')), findsNothing);
await tester.pump(const Duration(milliseconds: 200)); 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); expect(find.byKey(const Key('progress-loading')), findsOneWidget);
controller.value = const VideoPlayerValue( controller.value = const VideoPlayerValue(
@@ -838,7 +813,7 @@ void main() {
isInitialized: true, isInitialized: true,
); );
await tester.pump(); 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( final gesture = await tester.startGesture(
tester.getCenter(find.byKey(const Key('home-feed-progress'))), tester.getCenter(find.byKey(const Key('home-feed-progress'))),
@@ -853,7 +828,7 @@ void main() {
); );
await tester.pump(const Duration(milliseconds: 400)); 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); expect(find.byKey(const Key('progress-loading')), findsNothing);
controller.value = const VideoPlayerValue( controller.value = const VideoPlayerValue(
@@ -872,7 +847,7 @@ void main() {
isBuffering: true, isBuffering: true,
); );
await tester.pump(const Duration(milliseconds: 400)); 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); expect(find.byKey(const Key('progress-loading')), findsNothing);
}); });
+2 -2
View File
@@ -35,12 +35,12 @@ void main() {
Key('player-play-toggle'), Key('player-play-toggle'),
]) { ]) {
expect(tester.getSize(find.byKey(key)), const Size(68, 68)); expect(tester.getSize(find.byKey(key)), const Size(68, 68));
// 圆形底改为半透明黑色,不再是完全透明或蓝色实底。 // 圆形深色底:压在亮画面上也能看见播放/暂停。
final container = tester.widget<Container>( final container = tester.widget<Container>(
find.descendant(of: find.byKey(key), matching: find.byType(Container)), find.descendant(of: find.byKey(key), matching: find.byType(Container)),
); );
final decoration = container.decoration! as BoxDecoration; final decoration = container.decoration! as BoxDecoration;
expect(decoration.color, Colors.black26); expect(decoration.color, const Color(0x66000000));
expect(decoration.shape, BoxShape.circle); expect(decoration.shape, BoxShape.circle);
final icon = tester.widget<Icon>( final icon = tester.widget<Icon>(
find.descendant(of: find.byKey(key), matching: find.byType(Icon)), find.descendant(of: find.byKey(key), matching: find.byType(Icon)),
+24 -20
View File
@@ -983,9 +983,11 @@ void main() {
expect(find.byKey(const Key('player-video-tap')), findsOneWidget); expect(find.byKey(const Key('player-video-tap')), findsOneWidget);
expect(find.byKey(const Key('player-bottom-clear-screen')), findsOneWidget); expect(find.byKey(const Key('player-bottom-clear-screen')), findsOneWidget);
expect(find.byTooltip('Exit clear screen'), findsOneWidget); expect(find.byTooltip('Exit clear screen'), findsOneWidget);
// 清屏后左侧选集条一起收起,只留右下角的退出按钮。
expect(find.byKey(const Key('player-episodes-pill')), findsNothing);
expect( expect(
tester.getSize(find.byKey(const Key('player-bottom-clear-screen'))), 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)); 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-speed-boost-hint')), findsNothing);
expect(find.byKey(const Key('player-interaction-rail')), findsNothing); expect(find.byKey(const Key('player-interaction-rail')), findsNothing);
expect(find.byTooltip('Exit clear screen'), findsOneWidget); 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.byKey(const Key('player-bottom-clear-screen')), findsOneWidget);
expect( expect(
find.descendant( find.descendant(
@@ -1118,7 +1120,7 @@ void main() {
expect( expect(
tester.getSize(find.byKey(const Key('player-back'))), tester.getSize(find.byKey(const Key('player-back'))),
const Size(36, 36), const Size(40, 40),
); );
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
@@ -1160,17 +1162,12 @@ void main() {
.child, .child,
isNotNull, isNotNull,
); );
// 选集条给一层浅底色,标出可点区域。
expect( expect(
tester tester
.widget<Material>(find.byKey(const Key('player-episodes-pill'))) .widget<Material>(find.byKey(const Key('player-episodes-pill')))
.color, .color,
AppColors.playerOverlayPill, AppColors.textPrimary.withValues(alpha: 0.14),
);
expect(
tester
.widget<Material>(find.byKey(const Key('player-episodes-pill')))
.borderRadius,
BorderRadius.circular(7),
); );
expect( expect(
tester tester
@@ -1186,9 +1183,14 @@ void main() {
?.fontSize, ?.fontSize,
13.5, 13.5,
); );
// 清屏按钮与左侧选集条等高。
expect( expect(
tester.getSize(find.byKey(const Key('player-bottom-clear-screen'))), 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-loading')), findsNothing);
expect(find.byKey(const Key('progress-inactive')), findsOneWidget); expect(find.byKey(const Key('progress-inactive')), findsOneWidget);
@@ -1289,7 +1291,8 @@ void main() {
matching: find.byIcon(Icons.keyboard_arrow_up_rounded), 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)); expect(arrow.left - label.right, closeTo(8, 0.5));
}); });
@@ -1551,7 +1554,8 @@ void main() {
), ),
); );
expect(chevron.left - title.right, lessThanOrEqualTo(6)); expect(chevron.left - title.right, lessThanOrEqualTo(6));
expect(chevron.right, lessThanOrEqualTo(320 - 14)); // 剧名和简介一样收在互动栏左侧,不再铺到屏幕最右边。
expect(chevron.right, lessThanOrEqualTo(320 - 14 - 54));
final synopsis = tester.widget<Text>( final synopsis = tester.widget<Text>(
find.byKey(const Key('player-synopsis')), find.byKey(const Key('player-synopsis')),
); );
@@ -1647,11 +1651,11 @@ void main() {
await tester.tap(find.byKey(const Key('player-episodes'))); await tester.tap(find.byKey(const Key('player-episodes')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(const Key('player-episodes-sheet')), findsOneWidget); expect(find.byKey(const Key('drama-info-sheet')), findsOneWidget);
expect(find.byKey(const Key('player-episodes-close')), findsOneWidget); expect(find.byKey(const Key('drama-info-close')), findsOneWidget);
final grid = tester.widget<GridView>( final grid = tester.widget<GridView>(
find.descendant( find.descendant(
of: find.byKey(const Key('player-episodes-sheet')), of: find.byKey(const Key('drama-info-sheet')),
matching: find.byType(GridView), matching: find.byType(GridView),
), ),
); );
@@ -1659,14 +1663,14 @@ void main() {
grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount; grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount;
expect(delegate.crossAxisCount, 6); expect(delegate.crossAxisCount, 6);
final overlay = tester.widget<AnnotatedRegion<SystemUiOverlayStyle>>( final overlay = tester.widget<AnnotatedRegion<SystemUiOverlayStyle>>(
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.systemNavigationBarColor, AppColors.card);
expect(overlay.value.systemNavigationBarContrastEnforced, isFalse); expect(overlay.value.systemNavigationBarContrastEnforced, isFalse);
final safeArea = tester.widget<SafeArea>( final safeArea = tester.widget<SafeArea>(
find find
.descendant( .descendant(
of: find.byKey(const Key('player-episodes-sheet')), of: find.byKey(const Key('drama-info-sheet')),
matching: find.byType(SafeArea), matching: find.byType(SafeArea),
) )
.first, .first,
@@ -2030,7 +2034,7 @@ void _expectFullscreenOverlay(WidgetTester tester) {
expect(find.byKey(const Key('player-synopsis')), findsOneWidget); expect(find.byKey(const Key('player-synopsis')), findsOneWidget);
expect(find.byKey(const Key('player-more')), findsOneWidget); expect(find.byKey(const Key('player-more')), findsOneWidget);
for (final key in const [Key('player-back'), Key('player-more')]) { 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-play-toggle')), findsNothing);
expect(find.byKey(const Key('player-episodes')), findsOneWidget); expect(find.byKey(const Key('player-episodes')), findsOneWidget);
+13 -22
View File
@@ -37,11 +37,8 @@ void main() {
expect(find.byKey(const Key('scrub-overlay')), findsNothing); expect(find.byKey(const Key('scrub-overlay')), findsNothing);
expect(harness.scrubStarts, 1); expect(harness.scrubStarts, 1);
expect(_sliderTheme(tester).trackHeight, 6); expect(_sliderTheme(tester).trackHeight, 6);
expect( // 不画滑块圆点,进度只靠已播段表达。
(_sliderTheme(tester).thumbShape! as RoundSliderThumbShape) expect(_sliderTheme(tester).thumbShape, SliderComponentShape.noThumb);
.enabledThumbRadius,
8,
);
await gesture.moveBy(const Offset(72, 0)); await gesture.moveBy(const Offset(72, 0));
await tester.pump(); await tester.pump();
@@ -67,11 +64,7 @@ void main() {
expect(harness.scrubEnds, 1); expect(harness.scrubEnds, 1);
expect(harness.seeks, 1); expect(harness.seeks, 1);
expect(_sliderTheme(tester).trackHeight, 2); expect(_sliderTheme(tester).trackHeight, 2);
expect( expect(_sliderTheme(tester).thumbShape, SliderComponentShape.noThumb);
(_sliderTheme(tester).thumbShape! as RoundSliderThumbShape)
.enabledThumbRadius,
6,
);
}); });
testWidgets('feed progress tap does not clear overlay content', ( testWidgets('feed progress tap does not clear overlay content', (
@@ -154,9 +147,9 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
testWidgets('feed progress thumb stays on the finger when dragged right', ( testWidgets(
tester, 'feed progress played segment follows the finger when dragged right',
) async { (tester) async {
await _pumpFeedOverlay(tester); await _pumpFeedOverlay(tester);
final bar = find.byKey(const Key('home-feed-progress')); final bar = find.byKey(const Key('home-feed-progress'));
final gesture = await tester.startGesture(tester.getCenter(bar)); final gesture = await tester.startGesture(tester.getCenter(bar));
@@ -164,19 +157,17 @@ void main() {
await gesture.moveBy(const Offset(800, 0)); await gesture.moveBy(const Offset(800, 0));
await tester.pump(); await tester.pump();
final thumbCenter = tester expect(
.getRect(find.byKey(const Key('home-feed-progress-thumb'))) tester
.center
.dx;
final playedRight = tester
.getRect(find.byKey(const Key('home-feed-progress-played'))) .getRect(find.byKey(const Key('home-feed-progress-played')))
.right; .right,
expect(thumbCenter, closeTo(tester.getRect(bar).right, 0.5)); closeTo(tester.getRect(bar).right, 0.5),
expect(playedRight, closeTo(thumbCenter, 0.5)); );
await gesture.up(); await gesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); },
);
testWidgets('full player drag clears chrome and release restores it', ( testWidgets('full player drag clears chrome and release restores it', (
tester, tester,