feat(player): 更多面板按参考图重做

This commit is contained in:
Zhou Weidong committed 2026-10-21 10:34:18 +08:00
1 parent c06a67c4b0
commit 7fccd16750
3 files changed
+260 -131

No files matched your search

+141 -125
View File
@@ -6,12 +6,21 @@ import '../../../core/theme/app_colors.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
const double _sheetRadius = 20; const double _sheetRadius = 20;
const double _rowHeight = 44; const double _rowHeight = 56;
const double _groupGap = 12; const double _groupGap = 12;
const double _segmentHeight = 28; const double _groupVerticalPadding = 6;
const double _groupRadius = 16;
const double _rowHorizontalPadding = 14;
const double _rowIconSize = 22;
const double _rowIconGap = 14;
const double _rowLabelSize = 16;
const double _rowChevronGap = 10;
const double _segmentTrackHeight = 34;
const double _segmentHeight = 26;
const double _segmentRadius = 8; const double _segmentRadius = 8;
const double _segmentTrackRadius = 10;
const double _segmentTrackPadding = 4;
const double _segmentPadding = 10; const double _segmentPadding = 10;
const double _segmentGap = 6;
const List<double> _speedOptions = [0.75, 1.0, 1.25, 1.5, 2.0, 3.0]; const List<double> _speedOptions = [0.75, 1.0, 1.25, 1.5, 2.0, 3.0];
const List<String> _qualityOptions = ['AUTO', '720P', '1080P']; const List<String> _qualityOptions = ['AUTO', '720P', '1080P'];
const String _autoQuality = 'AUTO'; const String _autoQuality = 'AUTO';
@@ -82,7 +91,7 @@ class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
final l10n = widget.l10n; final l10n = widget.l10n;
return Material( return Material(
key: const Key('player-more-sheet'), key: const Key('player-more-sheet'),
color: AppColors.card, color: AppColors.ink,
borderRadius: const BorderRadius.vertical( borderRadius: const BorderRadius.vertical(
top: Radius.circular(_sheetRadius), top: Radius.circular(_sheetRadius),
), ),
@@ -90,7 +99,7 @@ class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
child: SafeArea( child: SafeArea(
top: false, top: false,
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 20), padding: const EdgeInsets.fromLTRB(12, 8, 12, 16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -105,31 +114,30 @@ class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
), ),
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 12),
_header(l10n), _group([_speedPicker(), _qualityPicker(l10n)]),
_speedPicker(),
const SizedBox(height: 4),
_qualityPicker(l10n),
const SizedBox(height: _groupGap), const SizedBox(height: _groupGap),
_actionRow( _group([
key: const Key('player-more-clear'),
icon: Icons.fullscreen_rounded,
title: l10n.clearPlayback,
onTap: _clear,
),
if (widget.actions.onPip != null)
_actionRow( _actionRow(
key: const Key('player-more-pip'), key: const Key('player-more-clear'),
icon: Icons.picture_in_picture_alt_rounded, icon: Icons.fullscreen,
title: l10n.pictureInPicture, title: l10n.clearPlayback,
onTap: () => _closeThen(widget.actions.onPip!), onTap: _clear,
), ),
_actionRow( if (widget.actions.onPip != null)
key: const Key('player-more-offline'), _actionRow(
icon: Icons.download_rounded, key: const Key('player-more-pip'),
title: l10n.offlineCache, icon: Icons.picture_in_picture_alt_outlined,
onTap: () => _closeThen(widget.actions.onDownload), title: l10n.pictureInPicture,
), onTap: () => _closeThen(widget.actions.onPip!),
),
_actionRow(
key: const Key('player-more-offline'),
icon: Icons.download_outlined,
title: l10n.offlineCache,
onTap: () => _closeThen(widget.actions.onDownload),
),
]),
], ],
), ),
), ),
@@ -137,35 +145,21 @@ class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
); );
} }
Widget _header(AppLocalizations l10n) => Row( Widget _group(List<Widget> rows) => Container(
children: [ width: double.infinity,
Expanded( padding: const EdgeInsets.symmetric(vertical: _groupVerticalPadding),
child: Text( decoration: BoxDecoration(
l10n.more, color: AppColors.card,
style: const TextStyle( borderRadius: BorderRadius.circular(_groupRadius),
color: AppColors.onSurface, ),
fontSize: 15, child: Column(mainAxisSize: MainAxisSize.min, children: rows),
fontWeight: FontWeight.w700,
),
),
),
SizedBox.square(
dimension: 32,
child: IconButton(
key: const Key('player-more-close'),
tooltip: l10n.close,
padding: EdgeInsets.zero,
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close_rounded, size: 18),
),
),
],
); );
Widget _speedPicker() => _optionRow( Widget _speedPicker() => _optionRow(
rowKey: const Key('player-more-speed-row'), rowKey: const Key('player-more-speed-row'),
optionsKey: const Key('player-more-speed'), optionsKey: const Key('player-more-speed'),
icon: Icons.speed_rounded, trackKey: const Key('player-more-speed-track'),
icon: Icons.speed_outlined,
title: widget.l10n.playbackSpeed, title: widget.l10n.playbackSpeed,
options: [ options: [
for (final value in _speedOptions) for (final value in _speedOptions)
@@ -184,7 +178,8 @@ class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
Widget _qualityPicker(AppLocalizations l10n) => _optionRow( Widget _qualityPicker(AppLocalizations l10n) => _optionRow(
rowKey: const Key('player-more-quality-row'), rowKey: const Key('player-more-quality-row'),
optionsKey: const Key('player-more-quality'), optionsKey: const Key('player-more-quality'),
icon: Icons.high_quality_rounded, trackKey: const Key('player-more-quality-track'),
icon: Icons.high_quality_outlined,
title: l10n.playbackQuality, title: l10n.playbackQuality,
options: [ options: [
for (final quality in _qualityOptions) _qualitySegment(l10n, quality), for (final quality in _qualityOptions) _qualitySegment(l10n, quality),
@@ -194,6 +189,7 @@ class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
Widget _optionRow({ Widget _optionRow({
required Key rowKey, required Key rowKey,
required Key optionsKey, required Key optionsKey,
required Key trackKey,
required IconData icon, required IconData icon,
required String title, required String title,
required List<Widget> options, required List<Widget> options,
@@ -201,45 +197,52 @@ class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
return SizedBox( return SizedBox(
key: rowKey, key: rowKey,
height: _rowHeight, height: _rowHeight,
child: Row( child: Padding(
children: [ padding: const EdgeInsets.symmetric(horizontal: _rowHorizontalPadding),
SizedBox( child: Row(
width: 88, children: [
child: Row( Icon(icon, color: AppColors.onSurface, size: _rowIconSize),
children: [ const SizedBox(width: _rowIconGap),
Icon(icon, color: AppColors.muted, size: 18), Expanded(
const SizedBox(width: 6), child: Text(
Expanded( title,
child: Text( maxLines: 1,
title, overflow: TextOverflow.ellipsis,
maxLines: 1, style: const TextStyle(
overflow: TextOverflow.ellipsis, color: AppColors.onSurface,
style: const TextStyle( fontSize: _rowLabelSize,
color: AppColors.onSurface, fontWeight: FontWeight.w600,
fontSize: 12, ),
fontWeight: FontWeight.w600, ),
),
const SizedBox(width: _rowChevronGap),
Expanded(
flex: 2,
child: Align(
alignment: Alignment.centerRight,
child: SingleChildScrollView(
key: optionsKey,
scrollDirection: Axis.horizontal,
child: Container(
key: trackKey,
height: _segmentTrackHeight,
padding: const EdgeInsets.symmetric(
horizontal: _segmentTrackPadding,
),
decoration: BoxDecoration(
color: AppColors.raised,
borderRadius: BorderRadius.circular(_segmentTrackRadius),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: options,
), ),
), ),
), ),
],
),
),
const SizedBox(width: 6),
Expanded(
child: SingleChildScrollView(
key: optionsKey,
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (var index = 0; index < options.length; index++) ...[
if (index > 0) const SizedBox(width: _segmentGap),
options[index],
],
],
), ),
), ),
), ],
], ),
), ),
); );
} }
@@ -275,26 +278,34 @@ class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
onTap: onTap, onTap: onTap,
child: SizedBox( child: SizedBox(
height: _rowHeight, height: _rowHeight,
child: Row( child: Padding(
children: [ padding: const EdgeInsets.symmetric(
Icon(icon, color: AppColors.muted, size: 20), horizontal: _rowHorizontalPadding,
const SizedBox(width: 12), ),
Expanded( child: Row(
child: Text( children: [
title, Icon(icon, color: AppColors.onSurface, size: _rowIconSize),
style: const TextStyle( const SizedBox(width: _rowIconGap),
color: AppColors.onSurface, Expanded(
fontSize: 14, child: Text(
fontWeight: FontWeight.w600, title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.onSurface,
fontSize: _rowLabelSize,
fontWeight: FontWeight.w600,
),
), ),
), ),
), const SizedBox(width: _rowChevronGap),
const Icon( const Icon(
Icons.chevron_right_rounded, Icons.chevron_right_rounded,
color: AppColors.subtle, color: AppColors.subtle,
size: 18, size: _rowIconSize,
), ),
], ],
),
), ),
), ),
), ),
@@ -330,36 +341,41 @@ class _SegmentButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final background = selected
? AppColors.blue
: enabled
? AppColors.raised
: AppColors.cardAlt;
final foreground = selected final foreground = selected
? AppColors.onPrimary
: enabled
? AppColors.onSurface ? AppColors.onSurface
: enabled
? AppColors.muted
: AppColors.subtle; : AppColors.subtle;
return Material( return Material(
color: background, color: Colors.transparent,
borderRadius: BorderRadius.circular(_segmentRadius), borderRadius: BorderRadius.circular(_segmentRadius),
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(_segmentRadius), borderRadius: BorderRadius.circular(_segmentRadius),
child: SizedBox( child: Container(
height: _segmentHeight, height: _segmentHeight,
child: Padding( padding: const EdgeInsets.symmetric(horizontal: _segmentPadding),
padding: const EdgeInsets.symmetric(horizontal: _segmentPadding), alignment: Alignment.center,
child: Center( decoration: BoxDecoration(
child: Text( color: selected ? AppColors.card : Colors.transparent,
label, borderRadius: BorderRadius.circular(_segmentRadius),
maxLines: 1, boxShadow: selected
style: TextStyle( ? const [
color: foreground, BoxShadow(
fontSize: 12, color: Color(0x14000000),
fontWeight: selected ? FontWeight.w700 : FontWeight.w500, blurRadius: 4,
), offset: Offset(0, 1),
), ),
]
: null,
),
child: Text(
label,
maxLines: 1,
style: TextStyle(
color: foreground,
fontSize: 14,
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
), ),
), ),
), ),
+19 -6
View File
@@ -50,7 +50,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(const Key('player-more-quality-AUTO')), findsOneWidget); expect(find.byKey(const Key('player-more-quality-AUTO')), findsOneWidget);
expect(find.byKey(const Key('player-more-offline')), findsOneWidget); expect(find.byKey(const Key('player-more-offline')), findsOneWidget);
await tester.tap(find.byKey(const Key('player-more-close'))); await tester.tapAt(const Offset(10, 10));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
@@ -290,7 +290,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(const Key('player-more-sheet')), findsOneWidget); expect(find.byKey(const Key('player-more-sheet')), findsOneWidget);
await tester.tap(find.byKey(const Key('player-more-close'))); await tester.tapAt(const Offset(10, 10));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// 长按弹更多不改播放状态,仍处于播放中。 // 长按弹更多不改播放状态,仍处于播放中。
expect(find.byKey(const Key('player-play-toggle')), findsNothing); expect(find.byKey(const Key('player-play-toggle')), findsNothing);
@@ -832,6 +832,7 @@ void main() {
expect(find.byKey(const Key('player-more-offline')), findsOneWidget); expect(find.byKey(const Key('player-more-offline')), findsOneWidget);
expect(find.byKey(const Key('player-more-clear')), findsOneWidget); expect(find.byKey(const Key('player-more-clear')), findsOneWidget);
expect(find.byKey(const Key('player-more-drag-handle')), findsOneWidget); expect(find.byKey(const Key('player-more-drag-handle')), findsOneWidget);
expect(find.byKey(const Key('player-more-close')), findsNothing);
for (final removed in const [ for (final removed in const [
Key('player-more-not-interested'), Key('player-more-not-interested'),
Key('player-more-danmaku'), Key('player-more-danmaku'),
@@ -845,15 +846,27 @@ void main() {
} }
expect( expect(
tester.getSize(find.byKey(const Key('player-more-speed-1.0'))).height, tester.getSize(find.byKey(const Key('player-more-speed-1.0'))).height,
28, 26,
);
expect(
tester.getSize(find.byKey(const Key('player-more-speed-track'))).height,
34,
); );
expect( expect(
tester.getSize(find.byKey(const Key('player-more-speed-row'))).height, tester.getSize(find.byKey(const Key('player-more-speed-row'))).height,
44, 56,
); );
expect( expect(
tester.getSize(find.byKey(const Key('player-more-quality-row'))).height, tester.getSize(find.byKey(const Key('player-more-quality-row'))).height,
44, 56,
);
expect(
tester.getSize(find.byKey(const Key('player-more-clear'))).height,
56,
);
expect(
tester.getSize(find.byKey(const Key('player-more-offline'))).height,
56,
); );
expect( expect(
tester tester
@@ -881,7 +894,7 @@ void main() {
) )
.style .style
?.fontSize, ?.fontSize,
12, 14,
); );
await tester.ensureVisible(find.byKey(const Key('player-more-speed-1.5'))); await tester.ensureVisible(find.byKey(const Key('player-more-speed-1.5')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
+100
View File
@@ -3,6 +3,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/l10n/app_localizations.dart'; import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/player/widgets/player_more_sheet.dart'; import 'package:fruitshorts/pages/player/widgets/player_more_sheet.dart';
@@ -14,6 +15,105 @@ void main() {
expect(find.byKey(const Key('player-more-clear')), findsOneWidget); expect(find.byKey(const Key('player-more-clear')), findsOneWidget);
expect(find.byKey(const Key('player-more-offline')), findsOneWidget); expect(find.byKey(const Key('player-more-offline')), findsOneWidget);
expect(find.byKey(const Key('player-more-pip')), findsNothing); expect(find.byKey(const Key('player-more-pip')), findsNothing);
expect(find.byKey(const Key('player-more-close')), findsNothing);
});
testWidgets('should_render_reference_layout_metrics', (tester) async {
await _openSheet(tester);
expect(
tester.widget<Material>(find.byKey(const Key('player-more-sheet'))).color,
AppColors.ink,
);
expect(
tester.getSize(find.byKey(const Key('player-more-drag-handle'))),
const Size(36, 4),
);
for (final key in const [
Key('player-more-speed-row'),
Key('player-more-quality-row'),
Key('player-more-clear'),
Key('player-more-offline'),
]) {
expect(tester.getSize(find.byKey(key)).height, 56);
}
for (final key in const [
Key('player-more-speed-track'),
Key('player-more-quality-track'),
]) {
expect(tester.getSize(find.byKey(key)).height, 34);
final trackDecoration =
tester.widget<Container>(find.byKey(key)).decoration!
as BoxDecoration;
expect(trackDecoration.color, AppColors.raised);
}
for (final key in const [
Key('player-more-speed-0.75'),
Key('player-more-speed-1.0'),
Key('player-more-speed-1.25'),
]) {
expect(tester.getSize(find.byKey(key)).height, 26);
}
final speedLabel = tester.widget<Text>(
find
.descendant(
of: find.byKey(const Key('player-more-speed-row')),
matching: find.byType(Text),
)
.first,
);
expect(speedLabel.style?.fontSize, 16);
expect(speedLabel.style?.fontWeight, FontWeight.w600);
expect(tester.widget<Icon>(find.byIcon(Icons.speed_outlined)).size, 22);
expect(tester.widget<Icon>(find.byIcon(Icons.fullscreen)).size, 22);
final selected = tester.widget<Container>(
find
.descendant(
of: find.byKey(const Key('player-more-speed-1.0')),
matching: find.byType(Container),
)
.first,
);
final selectedDecoration = selected.decoration! as BoxDecoration;
expect(selectedDecoration.color, AppColors.card);
expect(selectedDecoration.boxShadow, isNotEmpty);
final selectedText = tester.widget<Text>(
find.descendant(
of: find.byKey(const Key('player-more-speed-1.0')),
matching: find.text('1.0x'),
),
);
expect(selectedText.style?.color, AppColors.onSurface);
expect(selectedText.style?.fontWeight, FontWeight.w700);
final unselected = tester.widget<Container>(
find
.descendant(
of: find.byKey(const Key('player-more-speed-1.25')),
matching: find.byType(Container),
)
.first,
);
final unselectedDecoration = unselected.decoration! as BoxDecoration;
expect(unselectedDecoration.color, Colors.transparent);
final unselectedText = tester.widget<Text>(
find.descendant(
of: find.byKey(const Key('player-more-speed-1.25')),
matching: find.text('1.25x'),
),
);
expect(unselectedText.style?.color, AppColors.muted);
expect(unselectedText.style?.fontWeight, FontWeight.w500);
final disabledText = tester.widget<Text>(
find.descendant(
of: find.byKey(const Key('player-more-quality-720P')),
matching: find.byType(Text),
),
);
expect(disabledText.style?.color, AppColors.subtle);
}); });
testWidgets('should_trigger_pip_and_close_sheet_when_tapped', (tester) async { testWidgets('should_trigger_pip_and_close_sheet_when_tapped', (tester) async {