fix(material): keep navigation tint continuous through system inset

This commit is contained in:
zarzet
2026-09-27 18:18:19 +07:00
parent 3980f1f851
commit c2eab0da51
2 changed files with 90 additions and 1 deletions
+2 -1
View File
@@ -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(
+88
View File
@@ -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<RenderRepaintBoundary>(
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 {