diff --git a/packages/flutter/lib/src/material/navigation_bar.dart b/packages/flutter/lib/src/material/navigation_bar.dart index 0390faaf7c8..d0b7017c269 100644 --- a/packages/flutter/lib/src/material/navigation_bar.dart +++ b/packages/flutter/lib/src/material/navigation_bar.dart @@ -17,6 +17,9 @@ import 'text_theme.dart'; import 'theme.dart'; import 'tooltip.dart'; +const double _kIndicatorHeight = 64; +const double _kIndicatorWidth = 32; + // Examples can assume: // late BuildContext context; // late bool _isDrawerOpen; @@ -429,11 +432,14 @@ class _NavigationDestinationBuilder extends StatelessWidget { @override Widget build(BuildContext context) { final _NavigationDestinationInfo info = _NavigationDestinationInfo.of(context); + final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context); + final NavigationBarThemeData defaults = _defaultsFor(context); + return _NavigationBarDestinationSemantics( child: _NavigationBarDestinationTooltip( message: tooltip ?? label, - child: InkWell( - highlightColor: Colors.transparent, + child: _IndicatorInkWell( + customBorder: navigationBarTheme.indicatorShape ?? defaults.indicatorShape, onTap: info.onTap, child: Row( children: [ @@ -451,6 +457,31 @@ class _NavigationDestinationBuilder extends StatelessWidget { } } +class _IndicatorInkWell extends InkResponse { + const _IndicatorInkWell({ + super.child, + super.onTap, + super.customBorder, + }) : super( + containedInkWell: true, + highlightColor: Colors.transparent, + ); + + @override + RectCallback? getRectCallback(RenderBox referenceBox) { + final double indicatorOffsetX = referenceBox.size.width / 2; + const double indicatorOffsetY = 30.0; + + return () { + return Rect.fromCenter( + center: Offset(indicatorOffsetX, indicatorOffsetY), + width: _kIndicatorHeight, + height: _kIndicatorWidth, + ); + }; + } +} + /// Inherited widget for passing data from the [NavigationBar] to the /// [NavigationBar.destinations] children widgets. /// @@ -562,8 +593,8 @@ class NavigationIndicator extends StatelessWidget { super.key, required this.animation, this.color, - this.width = 64, - this.height = 32, + this.width = _kIndicatorHeight, + this.height = _kIndicatorWidth, this.borderRadius = const BorderRadius.all(Radius.circular(16)), this.shape, }); diff --git a/packages/flutter/test/material/navigation_bar_test.dart b/packages/flutter/test/material/navigation_bar_test.dart index 7491d36a13c..93d1afb478a 100644 --- a/packages/flutter/test/material/navigation_bar_test.dart +++ b/packages/flutter/test/material/navigation_bar_test.dart @@ -3,9 +3,12 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../rendering/mock_canvas.dart'; + void main() { testWidgets('Navigation bar updates destinations when tapped', (WidgetTester tester) async { int mutatedIndex = -1; @@ -553,6 +556,122 @@ void main() { expect(newHeight, equals(initialHeight)); }); + + testWidgets('Navigation indicator renders ripple', (WidgetTester tester) async { + final Widget widget = _buildWidget( + NavigationBar( + destinations: const [ + NavigationDestination( + icon: Icon(Icons.ac_unit), + label: 'AC', + ), + NavigationDestination( + icon: Icon(Icons.access_alarm), + label: 'Alarm', + ), + ], + onDestinationSelected: (int i) { + }, + ), + ); + + await tester.pumpWidget(widget); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm))); + await tester.pumpAndSettle(); + + final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); + const Offset indicatorCenter = Offset(600, 30); + const Size includedIndicatorSize = Size(64, 32); + const Size excludedIndicatorSize = Size(74, 40); + expect( + inkFeatures, + paints + ..clipPath( + pathMatcher: isPathThat( + includes: [ + // Left center. + Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy), + // Top center. + Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)), + // Right center. + Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy), + // Bottom center. + Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)), + ], + excludes: [ + // Left center. + Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy), + // Top center. + Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)), + // Right center. + Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy), + // Bottom center. + Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)), + ], + ), + ) + ..circle( + x: indicatorCenter.dx, + y: indicatorCenter.dy, + radius: 35.0, + color: const Color(0x0a000000), + ) + ); + }); + + testWidgets('Navigation indicator scale transform', (WidgetTester tester) async { + int selectedIndex = 0; + + Widget buildNavigationBar() { + return MaterialApp( + theme: ThemeData.light(), + home: Scaffold( + bottomNavigationBar: Center( + child: NavigationBar( + selectedIndex: selectedIndex, + destinations: const [ + NavigationDestination( + icon: Icon(Icons.ac_unit), + label: 'AC', + ), + NavigationDestination( + icon: Icon(Icons.access_alarm), + label: 'Alarm', + ), + ], + onDestinationSelected: (int i) { }, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildNavigationBar()); + await tester.pumpAndSettle(); + final Finder transformFinder = find.descendant( + of: find.byType(NavigationIndicator), + matching: find.byType(Transform), + ).last; + Matrix4 transform = tester.widget(transformFinder).transform; + expect(transform.getColumn(0)[0], 0.0); + + selectedIndex = 1; + await tester.pumpWidget(buildNavigationBar()); + await tester.pump(const Duration(milliseconds: 100)); + transform = tester.widget(transformFinder).transform; + expect(transform.getColumn(0)[0], closeTo(0.7805849514007568, precisionErrorTolerance)); + + await tester.pump(const Duration(milliseconds: 100)); + transform = tester.widget(transformFinder).transform; + expect(transform.getColumn(0)[0], closeTo(0.9473570239543915, precisionErrorTolerance)); + + await tester.pumpAndSettle(); + transform = tester.widget(transformFinder).transform; + expect(transform.getColumn(0)[0], 1.0); + }); } Widget _buildWidget(Widget child) {