From 0aa5a99a22db450aeb2aab0e57959bdb8a2cd31c Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Tue, 20 Oct 2026 19:04:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(flutter):=20=E4=BF=AE=E5=A4=8D=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E4=B8=8E=E6=92=AD=E6=94=BE=E5=99=A8=E5=BA=95=E9=83=A8?= =?UTF-8?q?=E9=97=AA=E7=99=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app.dart | 16 +- lib/core/theme/app_theme.dart | 5 + lib/pages/drama_detail/drama_detail_view.dart | 21 ++- lib/pages/player/player_view.dart | 9 +- .../player/widgets/episode_picker_sheet.dart | 176 ++++++++++-------- test/drama_detail/drama_detail_view_test.dart | 10 + test/player/player_fullscreen_test.dart | 35 ++++ test/theme/app_theme_test.dart | 21 +++ 8 files changed, 201 insertions(+), 92 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 6ab3ac0..f08cbbd 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -50,6 +50,16 @@ import 'core/theme/app_colors.dart'; import 'l10n/app_localizations.dart'; import 'pages/home/home_view.dart'; +/// 全局系统栏样式:浅暖导航栏、关闭 Android 对比遮罩。 +final SystemUiOverlayStyle appSystemOverlayStyle = SystemUiOverlayStyle.dark + .copyWith( + statusBarColor: Colors.transparent, + systemNavigationBarColor: AppColors.ink, + systemNavigationBarDividerColor: Colors.transparent, + systemNavigationBarIconBrightness: Brightness.dark, + systemNavigationBarContrastEnforced: false, + ); + class LumaDramaApp extends StatefulWidget { const LumaDramaApp({ super.key, @@ -263,11 +273,7 @@ class _LumaDramaAppState extends State { theme: AppTheme.light(), darkTheme: AppTheme.light(), builder: (context, child) => AnnotatedRegion( - value: SystemUiOverlayStyle.dark.copyWith( - statusBarColor: Colors.transparent, - systemNavigationBarColor: AppColors.bar, - systemNavigationBarIconBrightness: Brightness.dark, - ), + value: appSystemOverlayStyle, child: child ?? const SizedBox.shrink(), ), home: const HomePage(), diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 07f446e..9683d2a 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -107,6 +107,11 @@ abstract final class AppTheme { side: BorderSide(color: AppColors.border), ), ), + bottomSheetTheme: const BottomSheetThemeData( + backgroundColor: Colors.transparent, + modalBackgroundColor: Colors.transparent, + surfaceTintColor: Colors.transparent, + ), switchTheme: SwitchThemeData( thumbColor: WidgetStateProperty.resolveWith( (states) => states.contains(WidgetState.selected) diff --git a/lib/pages/drama_detail/drama_detail_view.dart b/lib/pages/drama_detail/drama_detail_view.dart index 5780402..d3f0725 100644 --- a/lib/pages/drama_detail/drama_detail_view.dart +++ b/lib/pages/drama_detail/drama_detail_view.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../../core/data/models/drama.dart'; @@ -83,11 +84,21 @@ class _DramaDetailPageState extends State { @override Widget build(BuildContext context) { - return Theme( - data: AppTheme.light(), - child: ListenableBuilder( - listenable: logic, - builder: (context, _) => _page(context), + return AnnotatedRegion( + key: const Key('detail-system-overlay'), + value: SystemUiOverlayStyle.dark.copyWith( + statusBarColor: Colors.transparent, + systemNavigationBarColor: AppColors.ink, + systemNavigationBarDividerColor: Colors.transparent, + systemNavigationBarIconBrightness: Brightness.dark, + systemNavigationBarContrastEnforced: false, + ), + child: Theme( + data: AppTheme.light(), + child: ListenableBuilder( + listenable: logic, + builder: (context, _) => _page(context), + ), ), ); } diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index 6082074..b436a98 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -238,7 +238,14 @@ class _PlayerPageState extends State with WidgetsBindingObserver { ], ); return AnnotatedRegion( - value: SystemUiOverlayStyle.dark, + key: const Key('player-system-overlay'), + value: SystemUiOverlayStyle.light.copyWith( + statusBarColor: Colors.transparent, + systemNavigationBarColor: Colors.black, + systemNavigationBarDividerColor: Colors.transparent, + systemNavigationBarIconBrightness: Brightness.light, + systemNavigationBarContrastEnforced: false, + ), child: Theme( data: AppTheme.light(), child: widget.embedded diff --git a/lib/pages/player/widgets/episode_picker_sheet.dart b/lib/pages/player/widgets/episode_picker_sheet.dart index 5d05cf3..a59d148 100644 --- a/lib/pages/player/widgets/episode_picker_sheet.dart +++ b/lib/pages/player/widgets/episode_picker_sheet.dart @@ -1,6 +1,7 @@ // Author: fruit.com.cn import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import '../../../core/data/models/drama.dart'; import '../../../core/formatting/localized_content.dart'; @@ -37,93 +38,106 @@ class EpisodePickerSheet extends StatelessWidget { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final episodes = drama.episodes; - return Container( - key: const Key('player-episodes-sheet'), - height: MediaQuery.sizeOf(context).height * _sheetHeightFactor, - decoration: const BoxDecoration( - color: AppColors.card, - borderRadius: BorderRadius.vertical(top: Radius.circular(_sheetRadius)), + return AnnotatedRegion( + key: const Key('player-episodes-system-overlay'), + value: SystemUiOverlayStyle.dark.copyWith( + statusBarColor: Colors.transparent, + systemNavigationBarColor: AppColors.card, + systemNavigationBarDividerColor: Colors.transparent, + systemNavigationBarIconBrightness: Brightness.dark, + systemNavigationBarContrastEnforced: false, ), - child: SafeArea( - top: false, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(20, 14, 8, 4), - child: Row( - children: [ - Expanded( - child: Text( - l10n.episodes, - style: const TextStyle( - color: AppColors.onSurface, - fontSize: 20, - fontWeight: FontWeight.w800, + child: Container( + key: const Key('player-episodes-sheet'), + height: MediaQuery.sizeOf(context).height * _sheetHeightFactor, + decoration: const BoxDecoration( + color: AppColors.card, + borderRadius: BorderRadius.vertical( + top: Radius.circular(_sheetRadius), + ), + ), + child: SafeArea( + top: false, + bottom: true, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20, 14, 8, 4), + child: Row( + children: [ + Expanded( + child: Text( + l10n.episodes, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: 20, + fontWeight: FontWeight.w800, + ), ), ), - ), - IconButton( - key: const Key('player-episodes-close'), - tooltip: l10n.close, - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.close_rounded), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 12), - child: Row( - children: [ - Expanded( - child: Text( - localizedDramaTitle(l10n, drama), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: AppColors.onSurface, - fontSize: 14, - fontWeight: FontWeight.w600, - ), + IconButton( + key: const Key('player-episodes-close'), + tooltip: l10n.close, + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close_rounded), ), - ), - Text( - l10n.episodeCountLabel(episodes.length), - style: const TextStyle( - color: AppColors.muted, - fontSize: 12, - ), - ), - ], - ), - ), - Expanded( - child: GridView.builder( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: _columns, - mainAxisSpacing: AppDimens.spaceSm, - crossAxisSpacing: AppDimens.spaceSm, - childAspectRatio: _cellAspect, + ], ), - itemCount: episodes.length, - itemBuilder: (context, index) { - final episode = episodes[index]; - return EpisodePickerCell( - key: Key('player-episode-${episode.number}'), - episode: episode, - selected: episode.number == currentNumber, - lockLabel: l10n.locked, - onTap: () { - Navigator.pop(context); - onSelect(index); - }, - ); - }, ), - ), - ], + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 12), + child: Row( + children: [ + Expanded( + child: Text( + localizedDramaTitle(l10n, drama), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + Text( + l10n.episodeCountLabel(episodes.length), + style: const TextStyle( + color: AppColors.muted, + fontSize: 12, + ), + ), + ], + ), + ), + Expanded( + child: GridView.builder( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: _columns, + mainAxisSpacing: AppDimens.spaceSm, + crossAxisSpacing: AppDimens.spaceSm, + childAspectRatio: _cellAspect, + ), + itemCount: episodes.length, + itemBuilder: (context, index) { + final episode = episodes[index]; + return EpisodePickerCell( + key: Key('player-episode-${episode.number}'), + episode: episode, + selected: episode.number == currentNumber, + lockLabel: l10n.locked, + onTap: () { + Navigator.pop(context); + onSelect(index); + }, + ); + }, + ), + ), + ], + ), ), ), ); diff --git a/test/drama_detail/drama_detail_view_test.dart b/test/drama_detail/drama_detail_view_test.dart index 5fe78a8..f66c0ba 100644 --- a/test/drama_detail/drama_detail_view_test.dart +++ b/test/drama_detail/drama_detail_view_test.dart @@ -1,6 +1,7 @@ // Author: fruit.com.cn import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -53,6 +54,11 @@ void main() { ) async { await _pumpDetail(tester); + final overlay = tester.widget>( + find.byKey(const Key('detail-system-overlay')), + ); + expect(overlay.value.systemNavigationBarColor, AppColors.ink); + expect(overlay.value.systemNavigationBarContrastEnforced, isFalse); expect(find.byKey(const Key('detail-hero')), findsOneWidget); expect( tester.getSize(find.byKey(const Key('detail-poster'))), @@ -92,6 +98,10 @@ void main() { expect(find.byKey(const Key('detail-tags')), findsOneWidget); expect(find.byKey(const Key('detail-episodes-section')), findsOneWidget); expect(tester.getSize(find.byKey(const Key('detail-cta'))).height, 46); + final bottomBar = tester.widget( + find.byKey(const Key('detail-bottom-bar')), + ); + expect((bottomBar.decoration! as BoxDecoration).color, AppColors.ink); expect(find.byKey(const Key('detail-share')), findsOneWidget); expect(find.byKey(const Key('detail-more')), findsOneWidget); for (final key in const [Key('detail-share'), Key('detail-more')]) { diff --git a/test/player/player_fullscreen_test.dart b/test/player/player_fullscreen_test.dart index c01d548..0048073 100644 --- a/test/player/player_fullscreen_test.dart +++ b/test/player/player_fullscreen_test.dart @@ -1,6 +1,7 @@ // Author: fruit.com.cn import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; @@ -50,6 +51,16 @@ void main() { expect(tester.takeException(), isNull); }); + testWidgets('should_use_black_system_navigation_bar_on_feed_and_player', ( + tester, + ) async { + await _pumpApp(tester); + _expectPlayerNavigationBar(tester); + + await _openSeriesPlayer(tester); + _expectPlayerNavigationBar(tester); + }); + testWidgets('should_fall_back_to_black_when_poster_is_empty', (tester) async { await _pumpApp( tester, @@ -716,6 +727,20 @@ void main() { final delegate = grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount; expect(delegate.crossAxisCount, 6); + final overlay = tester.widget>( + find.byKey(const Key('player-episodes-system-overlay')), + ); + expect(overlay.value.systemNavigationBarColor, AppColors.card); + expect(overlay.value.systemNavigationBarContrastEnforced, isFalse); + final safeArea = tester.widget( + find + .descendant( + of: find.byKey(const Key('player-episodes-sheet')), + matching: find.byType(SafeArea), + ) + .first, + ); + expect(safeArea.bottom, isTrue); expect(tester.takeException(), isNull); }); @@ -969,6 +994,16 @@ void main() { }); } +void _expectPlayerNavigationBar(WidgetTester tester) { + final overlay = tester.widget>( + find.byKey(const Key('player-system-overlay')).last, + ); + expect(overlay.value.systemNavigationBarColor, Colors.black); + expect(overlay.value.systemNavigationBarDividerColor, Colors.transparent); + expect(overlay.value.systemNavigationBarContrastEnforced, isFalse); + expect(overlay.value.systemNavigationBarIconBrightness, Brightness.light); +} + void _expectFullscreenOverlay(WidgetTester tester) { expect(find.byKey(const Key('player-back')), findsOneWidget); expect(find.byKey(const Key('player-episode-heading')), findsOneWidget); diff --git a/test/theme/app_theme_test.dart b/test/theme/app_theme_test.dart index 62ad6ef..e7bc83c 100644 --- a/test/theme/app_theme_test.dart +++ b/test/theme/app_theme_test.dart @@ -2,8 +2,10 @@ import 'dart:math'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/app.dart'; import 'package:fruitshorts/core/theme/app_colors.dart'; import 'package:fruitshorts/core/theme/app_theme.dart'; @@ -50,6 +52,25 @@ void main() { expect(theme.appBarTheme.systemOverlayStyle, SystemUiOverlayStyle.dark); }); + test('should_use_ink_navigation_bar_without_contrast_scrim', () { + expect(appSystemOverlayStyle.statusBarColor, Colors.transparent); + expect(appSystemOverlayStyle.systemNavigationBarColor, AppColors.ink); + expect( + appSystemOverlayStyle.systemNavigationBarDividerColor, + Colors.transparent, + ); + expect( + appSystemOverlayStyle.systemNavigationBarIconBrightness, + Brightness.dark, + ); + expect(appSystemOverlayStyle.systemNavigationBarContrastEnforced, isFalse); + + final sheetTheme = AppTheme.light().bottomSheetTheme; + expect(sheetTheme.backgroundColor, Colors.transparent); + expect(sheetTheme.modalBackgroundColor, Colors.transparent); + expect(sheetTheme.surfaceTintColor, Colors.transparent); + }); + test('should_expose_page_gradient_and_soft_card_shadow', () { expect(AppTheme.pageGradient.colors, [AppColors.surfaceTop, AppColors.ink]); expect(AppTheme.cardShadow.color, AppColors.cardShadow);