From 7fccd167504f5b71afcb28c0fde4639e29f9f5a2 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Wed, 21 Oct 2026 10:34:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(player):=20=E6=9B=B4=E5=A4=9A=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E6=8C=89=E5=8F=82=E8=80=83=E5=9B=BE=E9=87=8D=E5=81=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../player/widgets/player_more_sheet.dart | 266 ++++++++++-------- test/player/player_fullscreen_test.dart | 25 +- test/player/player_more_sheet_test.dart | 100 +++++++ 3 files changed, 260 insertions(+), 131 deletions(-) diff --git a/lib/pages/player/widgets/player_more_sheet.dart b/lib/pages/player/widgets/player_more_sheet.dart index 49eb80f..4f93858 100644 --- a/lib/pages/player/widgets/player_more_sheet.dart +++ b/lib/pages/player/widgets/player_more_sheet.dart @@ -6,12 +6,21 @@ import '../../../core/theme/app_colors.dart'; import '../../../l10n/app_localizations.dart'; const double _sheetRadius = 20; -const double _rowHeight = 44; +const double _rowHeight = 56; 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 _segmentTrackRadius = 10; +const double _segmentTrackPadding = 4; const double _segmentPadding = 10; -const double _segmentGap = 6; const List _speedOptions = [0.75, 1.0, 1.25, 1.5, 2.0, 3.0]; const List _qualityOptions = ['AUTO', '720P', '1080P']; const String _autoQuality = 'AUTO'; @@ -82,7 +91,7 @@ class _PlayerMoreSheetState extends State { final l10n = widget.l10n; return Material( key: const Key('player-more-sheet'), - color: AppColors.card, + color: AppColors.ink, borderRadius: const BorderRadius.vertical( top: Radius.circular(_sheetRadius), ), @@ -90,7 +99,7 @@ class _PlayerMoreSheetState extends State { child: SafeArea( top: false, child: SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 20), + padding: const EdgeInsets.fromLTRB(12, 8, 12, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -105,31 +114,30 @@ class _PlayerMoreSheetState extends State { ), ), ), - const SizedBox(height: 8), - _header(l10n), - _speedPicker(), - const SizedBox(height: 4), - _qualityPicker(l10n), + const SizedBox(height: 12), + _group([_speedPicker(), _qualityPicker(l10n)]), const SizedBox(height: _groupGap), - _actionRow( - key: const Key('player-more-clear'), - icon: Icons.fullscreen_rounded, - title: l10n.clearPlayback, - onTap: _clear, - ), - if (widget.actions.onPip != null) + _group([ _actionRow( - key: const Key('player-more-pip'), - icon: Icons.picture_in_picture_alt_rounded, - title: l10n.pictureInPicture, - onTap: () => _closeThen(widget.actions.onPip!), + key: const Key('player-more-clear'), + icon: Icons.fullscreen, + title: l10n.clearPlayback, + onTap: _clear, ), - _actionRow( - key: const Key('player-more-offline'), - icon: Icons.download_rounded, - title: l10n.offlineCache, - onTap: () => _closeThen(widget.actions.onDownload), - ), + if (widget.actions.onPip != null) + _actionRow( + key: const Key('player-more-pip'), + icon: Icons.picture_in_picture_alt_outlined, + 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 { ); } - Widget _header(AppLocalizations l10n) => Row( - children: [ - Expanded( - child: Text( - l10n.more, - style: const TextStyle( - color: AppColors.onSurface, - fontSize: 15, - 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 _group(List rows) => Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: _groupVerticalPadding), + decoration: BoxDecoration( + color: AppColors.card, + borderRadius: BorderRadius.circular(_groupRadius), + ), + child: Column(mainAxisSize: MainAxisSize.min, children: rows), ); Widget _speedPicker() => _optionRow( rowKey: const Key('player-more-speed-row'), 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, options: [ for (final value in _speedOptions) @@ -184,7 +178,8 @@ class _PlayerMoreSheetState extends State { Widget _qualityPicker(AppLocalizations l10n) => _optionRow( rowKey: const Key('player-more-quality-row'), 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, options: [ for (final quality in _qualityOptions) _qualitySegment(l10n, quality), @@ -194,6 +189,7 @@ class _PlayerMoreSheetState extends State { Widget _optionRow({ required Key rowKey, required Key optionsKey, + required Key trackKey, required IconData icon, required String title, required List options, @@ -201,45 +197,52 @@ class _PlayerMoreSheetState extends State { return SizedBox( key: rowKey, height: _rowHeight, - child: Row( - children: [ - SizedBox( - width: 88, - child: Row( - children: [ - Icon(icon, color: AppColors.muted, size: 18), - const SizedBox(width: 6), - Expanded( - child: Text( - title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: AppColors.onSurface, - fontSize: 12, - fontWeight: FontWeight.w600, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: _rowHorizontalPadding), + child: Row( + children: [ + Icon(icon, color: AppColors.onSurface, size: _rowIconSize), + const SizedBox(width: _rowIconGap), + Expanded( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: _rowLabelSize, + 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 { onTap: onTap, child: SizedBox( height: _rowHeight, - child: Row( - children: [ - Icon(icon, color: AppColors.muted, size: 20), - const SizedBox(width: 12), - Expanded( - child: Text( - title, - style: const TextStyle( - color: AppColors.onSurface, - fontSize: 14, - fontWeight: FontWeight.w600, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: _rowHorizontalPadding, + ), + child: Row( + children: [ + Icon(icon, color: AppColors.onSurface, size: _rowIconSize), + const SizedBox(width: _rowIconGap), + Expanded( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: _rowLabelSize, + fontWeight: FontWeight.w600, + ), ), ), - ), - const Icon( - Icons.chevron_right_rounded, - color: AppColors.subtle, - size: 18, - ), - ], + const SizedBox(width: _rowChevronGap), + const Icon( + Icons.chevron_right_rounded, + color: AppColors.subtle, + size: _rowIconSize, + ), + ], + ), ), ), ), @@ -330,36 +341,41 @@ class _SegmentButton extends StatelessWidget { @override Widget build(BuildContext context) { - final background = selected - ? AppColors.blue - : enabled - ? AppColors.raised - : AppColors.cardAlt; final foreground = selected - ? AppColors.onPrimary - : enabled ? AppColors.onSurface + : enabled + ? AppColors.muted : AppColors.subtle; return Material( - color: background, + color: Colors.transparent, borderRadius: BorderRadius.circular(_segmentRadius), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(_segmentRadius), - child: SizedBox( + child: Container( height: _segmentHeight, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: _segmentPadding), - child: Center( - child: Text( - label, - maxLines: 1, - style: TextStyle( - color: foreground, - fontSize: 12, - fontWeight: selected ? FontWeight.w700 : FontWeight.w500, - ), - ), + padding: const EdgeInsets.symmetric(horizontal: _segmentPadding), + alignment: Alignment.center, + decoration: BoxDecoration( + color: selected ? AppColors.card : Colors.transparent, + borderRadius: BorderRadius.circular(_segmentRadius), + boxShadow: selected + ? const [ + BoxShadow( + color: Color(0x14000000), + blurRadius: 4, + offset: Offset(0, 1), + ), + ] + : null, + ), + child: Text( + label, + maxLines: 1, + style: TextStyle( + color: foreground, + fontSize: 14, + fontWeight: selected ? FontWeight.w700 : FontWeight.w500, ), ), ), diff --git a/test/player/player_fullscreen_test.dart b/test/player/player_fullscreen_test.dart index d8a7b6b..64c1ac7 100644 --- a/test/player/player_fullscreen_test.dart +++ b/test/player/player_fullscreen_test.dart @@ -50,7 +50,7 @@ void main() { await tester.pumpAndSettle(); expect(find.byKey(const Key('player-more-quality-AUTO')), 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(); expect(tester.takeException(), isNull); }); @@ -290,7 +290,7 @@ void main() { await tester.pumpAndSettle(); 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(); // 长按弹更多不改播放状态,仍处于播放中。 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-clear')), 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 [ Key('player-more-not-interested'), Key('player-more-danmaku'), @@ -845,15 +846,27 @@ void main() { } expect( 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( tester.getSize(find.byKey(const Key('player-more-speed-row'))).height, - 44, + 56, ); expect( 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( tester @@ -881,7 +894,7 @@ void main() { ) .style ?.fontSize, - 12, + 14, ); await tester.ensureVisible(find.byKey(const Key('player-more-speed-1.5'))); await tester.pumpAndSettle(); diff --git a/test/player/player_more_sheet_test.dart b/test/player/player_more_sheet_test.dart index 3929b53..5159498 100644 --- a/test/player/player_more_sheet_test.dart +++ b/test/player/player_more_sheet_test.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.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/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-offline')), findsOneWidget); 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(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(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( + 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(find.byIcon(Icons.speed_outlined)).size, 22); + expect(tester.widget(find.byIcon(Icons.fullscreen)).size, 22); + + final selected = tester.widget( + 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( + 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( + 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( + 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( + 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 {