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
+298 -297

No files matched your search

@@ -4,6 +4,10 @@ import 'package:flutter/material.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.
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,
),
),
);
}
+30 -27
View File
@@ -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<Shadow> _boostTextShadows = [
@@ -152,7 +155,7 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
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<HomeFeedOverlay> {
),
),
),
// 观看条在下、进度条在上:进度条 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<HomeFeedOverlay> {
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,
@@ -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,
),
),
),
),
),
],
),
);
@@ -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<Shadow> _textShadows = [
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 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<HomeFeedInfoPanel> {
],
),
),
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<String> genres) {
final chips = <Widget>[
List<Widget> _tagChips(AppLocalizations l10n, List<String> genres) {
return [
if (isHotDrama(widget.drama))
_TagChip(
key: const Key('home-feed-hot-badge'),
@@ -125,6 +136,9 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
for (final genre in genres)
_TagChip(key: Key('home-feed-tag-$genre'), label: genre),
];
}
Widget _tagRow(List<Widget> chips) {
return SizedBox(
key: const Key('home-feed-tags'),
height: 22,
@@ -149,10 +163,7 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
);
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,
),
],
),
),
),
),
@@ -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<Shadow> _iconShadows = [
Shadow(color: AppColors.shadow, blurRadius: 12),
];
+82 -59
View File
@@ -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,
),
],
@@ -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(
@@ -321,10 +321,8 @@ class _SeekProgressBarState extends State<SeekProgressBar> {
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,
),
+34 -59
View File
@@ -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<Text>(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<Material>(
find.byKey(const Key('home-feed-full-series-bg')),
)
.widget<Material>(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);
});
+2 -2
View File
@@ -35,12 +35,12 @@ void main() {
Key('player-play-toggle'),
]) {
expect(tester.getSize(find.byKey(key)), const Size(68, 68));
// 圆形底改为半透明黑色,不再是完全透明或蓝色实底。
// 圆形深色底:压在亮画面上也能看见播放/暂停。
final container = tester.widget<Container>(
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<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-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<Material>(find.byKey(const Key('player-episodes-pill')))
.color,
AppColors.playerOverlayPill,
);
expect(
tester
.widget<Material>(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<Text>(
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<GridView>(
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<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.systemNavigationBarContrastEnforced, isFalse);
final safeArea = tester.widget<SafeArea>(
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);
+21 -30
View File
@@ -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,