From c2eab0da517540c9f6c32a7ebf648fbcd15f25c9 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Sun, 27 Sep 2026 18:18:19 +0700 Subject: [PATCH] fix(material): keep navigation tint continuous through system inset --- lib/widgets/expressive_navigation_bar.dart | 3 +- test/material_expressive_test.dart | 88 ++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/lib/widgets/expressive_navigation_bar.dart b/lib/widgets/expressive_navigation_bar.dart index 6d045b76..6655119f 100644 --- a/lib/widgets/expressive_navigation_bar.dart +++ b/lib/widgets/expressive_navigation_bar.dart @@ -34,6 +34,7 @@ class ExpressiveNavigationBar extends StatelessWidget { return MaterialExpressiveScope( child: ColoredBox( color: backgroundColor, + // Paint the tint once across both the controls and system gesture area. // Use the app's scaled MediaQuery insets, rather than raw view metrics. child: SafeArea( top: false, @@ -44,7 +45,7 @@ class ExpressiveNavigationBar extends StatelessWidget { autoLayout: false, safeArea: false, padding: EdgeInsets.zero, - backgroundColor: backgroundColor, + backgroundColor: Colors.transparent, destinations: [ for (final destination in destinations) _NavigationDestination( diff --git a/test/material_expressive_test.dart b/test/material_expressive_test.dart index 46d36674..2d5274a5 100644 --- a/test/material_expressive_test.dart +++ b/test/material_expressive_test.dart @@ -1,5 +1,8 @@ +import 'dart:ui' as ui; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart' show RenderRepaintBoundary; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -294,6 +297,91 @@ void main() { expect(tester.takeException(), isNull); }); + for (final dark in [false, true]) { + for (final reduceMotion in [false, true]) { + testWidgets('navigation tint continues through system insets ' + '(dark: $dark, reduced motion: $reduceMotion)', (tester) async { + final capture = GlobalKey(); + const backdrop = Color(0xff725ca8); + final theme = dark ? AppTheme.dark() : AppTheme.light(); + final tint = theme.colorScheme.surfaceContainer.withValues(alpha: 0.72); + for (final bottomInset in [24.0, 48.0]) { + await tester.pumpWidget( + _host( + Builder( + builder: (context) => MediaQuery( + data: MediaQuery.of( + context, + ).copyWith(padding: EdgeInsets.only(bottom: bottomInset)), + child: RepaintBoundary( + key: capture, + child: SizedBox( + width: 320, + child: ColoredBox( + color: backdrop, + child: ExpressiveNavigationBar( + selectedIndex: 0, + onDestinationSelected: (_) {}, + backgroundColor: tint, + destinations: const [ + NavigationDestination( + icon: Icon(Icons.home), + label: 'Home', + ), + NavigationDestination( + icon: Icon(Icons.settings), + label: 'Settings', + ), + ], + ), + ), + ), + ), + ), + ), + theme: theme, + reduceMotion: reduceMotion, + ), + ); + await tester.pumpAndSettle(); + final colors = await tester.runAsync(() async { + final boundary = tester.renderObject( + find.byKey(capture), + ); + final image = await boundary.toImage(); + try { + final bytes = (await image.toByteData( + format: ui.ImageByteFormat.rawRgba, + ))!; + return [ + // Sample between destinations and at the physical bottom. + for (final y in [32, image.height - 1]) + Color.fromARGB( + bytes.getUint8((y * image.width + 160) * 4 + 3), + bytes.getUint8((y * image.width + 160) * 4), + bytes.getUint8((y * image.width + 160) * 4 + 1), + bytes.getUint8((y * image.width + 160) * 4 + 2), + ), + ]; + } finally { + image.dispose(); + } + }); + expect(colors![0], colors[1]); + final expected = Color.alphaBlend(tint, backdrop); + expect(colors[0].r, closeTo(expected.r, 1 / 255)); + expect(colors[0].g, closeTo(expected.g, 1 / 255)); + expect(colors[0].b, closeTo(expected.b, 1 / 255)); + expect( + tester.getSize(find.byType(ExpressiveNavigationBar)).height, + 64 + bottomInset, + ); + expect(tester.takeException(), isNull); + } + }); + } + } + testWidgets('reduced motion keeps interactive Material fallback', ( tester, ) async {