From beaabb70c51a30ac28706c941510543760f4e920 Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Mon, 28 Nov 2022 22:44:29 +0200 Subject: [PATCH] Add `IndicatorShape` to `NavigationRailTheme` and fix indicator ripple. (#116108) * Add `IndicatorShape` to `NavigationRailTheme` and fix indicator ripple. * remove unused variables --- .../lib/navigation_rail_template.dart | 17 +- .../lib/src/material/navigation_rail.dart | 112 +++++++++---- .../src/material/navigation_rail_theme.dart | 10 ++ .../test/material/navigation_rail_test.dart | 156 +++++++++++++++++- .../material/navigation_rail_theme_test.dart | 9 + 5 files changed, 257 insertions(+), 47 deletions(-) diff --git a/dev/tools/gen_defaults/lib/navigation_rail_template.dart b/dev/tools/gen_defaults/lib/navigation_rail_template.dart index ce89b361f47..36192197f36 100644 --- a/dev/tools/gen_defaults/lib/navigation_rail_template.dart +++ b/dev/tools/gen_defaults/lib/navigation_rail_template.dart @@ -14,14 +14,14 @@ class NavigationRailTemplate extends TokenTemplate { String generate() => ''' class _${blockName}DefaultsM3 extends NavigationRailThemeData { _${blockName}DefaultsM3(this.context) - : super( - elevation: ${elevation("md.comp.navigation-rail.container")}, - groupAlignment: -1, - labelType: NavigationRailLabelType.none, - useIndicator: true, - minWidth: ${tokens["md.comp.navigation-rail.container.width"]}, - minExtendedWidth: 256, - ); + : super( + elevation: ${elevation("md.comp.navigation-rail.container")}, + groupAlignment: -1, + labelType: NavigationRailLabelType.none, + useIndicator: true, + minWidth: ${tokens["md.comp.navigation-rail.container.width"]}, + minExtendedWidth: 256, + ); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; @@ -53,6 +53,7 @@ class _${blockName}DefaultsM3 extends NavigationRailThemeData { @override Color? get indicatorColor => ${componentColor("md.comp.navigation-rail.active-indicator")}; + @override ShapeBorder? get indicatorShape => ${shape("md.comp.navigation-rail.active-indicator")}; } '''; } diff --git a/packages/flutter/lib/src/material/navigation_rail.dart b/packages/flutter/lib/src/material/navigation_rail.dart index 4ef3104eceb..688f1dbfaaf 100644 --- a/packages/flutter/lib/src/material/navigation_rail.dart +++ b/packages/flutter/lib/src/material/navigation_rail.dart @@ -15,6 +15,8 @@ import 'navigation_rail_theme.dart'; import 'text_theme.dart'; import 'theme.dart'; +const double _kCircularIndicatorDiameter = 56; + /// A Material Design widget that is meant to be displayed at the left or right of an /// app to navigate between a small number of views, typically between three and /// five. @@ -394,6 +396,7 @@ class _NavigationRailState extends State with TickerProviderStat final NavigationRailLabelType labelType = widget.labelType ?? navigationRailTheme.labelType ?? defaults.labelType!; final bool useIndicator = widget.useIndicator ?? navigationRailTheme.useIndicator ?? defaults.useIndicator!; final Color? indicatorColor = widget.indicatorColor ?? navigationRailTheme.indicatorColor ?? defaults.indicatorColor; + final ShapeBorder? indicatorShape = navigationRailTheme.indicatorShape ?? defaults.indicatorShape; // For backwards compatibility, in M2 the opacity of the unselected icons needs // to be set to the default if it isn't in the given theme. This can be removed @@ -443,6 +446,7 @@ class _NavigationRailState extends State with TickerProviderStat padding: widget.destinations[i].padding, useIndicator: useIndicator, indicatorColor: useIndicator ? indicatorColor : null, + indicatorShape: useIndicator ? indicatorShape : null, onTap: () { if (widget.onDestinationSelected != null) { widget.onDestinationSelected!(i); @@ -529,6 +533,7 @@ class _RailDestination extends StatelessWidget { this.padding, required this.useIndicator, this.indicatorColor, + this.indicatorShape, }) : assert(minWidth != null), assert(minExtendedWidth != null), assert(icon != null), @@ -562,6 +567,7 @@ class _RailDestination extends StatelessWidget { final EdgeInsetsGeometry? padding; final bool useIndicator; final Color? indicatorColor; + final ShapeBorder? indicatorShape; final Animation _positionAnimation; @@ -573,6 +579,7 @@ class _RailDestination extends StatelessWidget { ); final bool material3 = Theme.of(context).useMaterial3; + final double indicatorInkOffsetY; final Widget themedIcon = IconTheme( data: iconTheme, @@ -583,12 +590,13 @@ class _RailDestination extends StatelessWidget { child: label, ); - final Widget content; + Widget content; switch (labelType) { case NavigationRailLabelType.none: // Split the destination spacing across the top and bottom to keep the icon centered. final Widget? spacing = material3 ? const SizedBox(height: _verticalDestinationSpacingM3 / 2) : null; + indicatorInkOffsetY = _verticalDestinationPaddingNoLabel - (_verticalIconLabelSpacingM3 / 2); final Widget iconPart = Column( children: [ @@ -600,6 +608,7 @@ class _RailDestination extends StatelessWidget { child: _AddIndicator( addIndicator: useIndicator, indicatorColor: indicatorColor, + indicatorShape: indicatorShape, isCircular: !material3, indicatorAnimation: destinationAnimation, child: themedIcon, @@ -666,6 +675,7 @@ class _RailDestination extends StatelessWidget { final Widget topSpacing = SizedBox(height: material3 ? 0 : verticalPadding); final Widget labelSpacing = SizedBox(height: material3 ? lerpDouble(0, _verticalIconLabelSpacingM3, appearingAnimationValue)! : 0); final Widget bottomSpacing = SizedBox(height: material3 ? _verticalDestinationSpacingM3 : verticalPadding); + indicatorInkOffsetY = _verticalDestinationPaddingWithLabel; content = Container( constraints: BoxConstraints( @@ -682,6 +692,7 @@ class _RailDestination extends StatelessWidget { _AddIndicator( addIndicator: useIndicator, indicatorColor: indicatorColor, + indicatorShape: indicatorShape, isCircular: false, indicatorAnimation: destinationAnimation, child: themedIcon, @@ -708,6 +719,7 @@ class _RailDestination extends StatelessWidget { final Widget topSpacing = SizedBox(height: material3 ? 0 : _verticalDestinationPaddingWithLabel); final Widget labelSpacing = SizedBox(height: material3 ? _verticalIconLabelSpacingM3 : 0); final Widget bottomSpacing = SizedBox(height: material3 ? _verticalDestinationSpacingM3 : _verticalDestinationPaddingWithLabel); + indicatorInkOffsetY = _verticalDestinationPaddingWithLabel; content = Container( constraints: BoxConstraints( minWidth: minWidth, @@ -720,6 +732,7 @@ class _RailDestination extends StatelessWidget { _AddIndicator( addIndicator: useIndicator, indicatorColor: indicatorColor, + indicatorShape: indicatorShape, isCircular: false, indicatorAnimation: destinationAnimation, child: themedIcon, @@ -741,14 +754,14 @@ class _RailDestination extends StatelessWidget { children: [ Material( type: MaterialType.transparency, - child: InkResponse( + child: _IndicatorInkWell( onTap: onTap, - onHover: (_) {}, - highlightShape: BoxShape.rectangle, - borderRadius: material3 ? null : BorderRadius.all(Radius.circular(minWidth / 2.0)), - containedInkWell: true, + borderRadius: BorderRadius.all(Radius.circular(minWidth / 2.0)), + customBorder: indicatorShape, splashColor: colors.primary.withOpacity(0.12), hoverColor: colors.primary.withOpacity(0.04), + useMaterial3: material3, + indicatorOffsetY: indicatorInkOffsetY, child: content, ), ), @@ -761,6 +774,43 @@ class _RailDestination extends StatelessWidget { } } +class _IndicatorInkWell extends InkResponse { + const _IndicatorInkWell({ + super.child, + super.onTap, + ShapeBorder? customBorder, + BorderRadius? borderRadius, + super.splashColor, + super.hoverColor, + required this.useMaterial3, + required this.indicatorOffsetY, + }) : super( + containedInkWell: true, + highlightShape: BoxShape.rectangle, + borderRadius: useMaterial3 ? null : borderRadius, + customBorder: useMaterial3 ? customBorder : null, + ); + + final bool useMaterial3; + final double indicatorOffsetY; + + @override + RectCallback? getRectCallback(RenderBox referenceBox) { + final double indicatorOffsetX = referenceBox.size.width / 2; + + if (useMaterial3) { + return () { + return Rect.fromCenter( + center: Offset(indicatorOffsetX, indicatorOffsetY), + width: _kCircularIndicatorDiameter, + height: 32, + ); + }; + } + return null; + } +} + /// When [addIndicator] is `true`, puts [child] center aligned in a [Stack] with /// a [NavigationIndicator] behind it, otherwise returns [child]. /// @@ -771,6 +821,7 @@ class _AddIndicator extends StatelessWidget { required this.addIndicator, required this.isCircular, required this.indicatorColor, + required this.indicatorShape, required this.indicatorAnimation, required this.child, }); @@ -778,6 +829,7 @@ class _AddIndicator extends StatelessWidget { final bool addIndicator; final bool isCircular; final Color? indicatorColor; + final ShapeBorder? indicatorShape; final Animation indicatorAnimation; final Widget child; @@ -788,19 +840,18 @@ class _AddIndicator extends StatelessWidget { } late final Widget indicator; if (isCircular) { - const double circularIndicatorDiameter = 56; indicator = NavigationIndicator( animation: indicatorAnimation, - height: circularIndicatorDiameter, - width: circularIndicatorDiameter, - borderRadius: BorderRadius.circular(circularIndicatorDiameter / 2), + height: _kCircularIndicatorDiameter, + width: _kCircularIndicatorDiameter, + borderRadius: BorderRadius.circular(_kCircularIndicatorDiameter / 2), color: indicatorColor, ); } else { indicator = NavigationIndicator( animation: indicatorAnimation, - width: 56, - shape: const StadiumBorder(), + width: _kCircularIndicatorDiameter, + shape: indicatorShape, color: indicatorColor, ); } @@ -918,16 +969,16 @@ const double _verticalDestinationSpacingM3 = 12.0; // Hand coded defaults based on Material Design 2. class _NavigationRailDefaultsM2 extends NavigationRailThemeData { _NavigationRailDefaultsM2(BuildContext context) - : _theme = Theme.of(context), - _colors = Theme.of(context).colorScheme, - super( - elevation: 0, - groupAlignment: -1, - labelType: NavigationRailLabelType.none, - useIndicator: false, - minWidth: 72.0, - minExtendedWidth: 256, - ); + : _theme = Theme.of(context), + _colors = Theme.of(context).colorScheme, + super( + elevation: 0, + groupAlignment: -1, + labelType: NavigationRailLabelType.none, + useIndicator: false, + minWidth: 72.0, + minExtendedWidth: 256, + ); final ThemeData _theme; final ColorScheme _colors; @@ -970,14 +1021,14 @@ class _NavigationRailDefaultsM2 extends NavigationRailThemeData { class _NavigationRailDefaultsM3 extends NavigationRailThemeData { _NavigationRailDefaultsM3(this.context) - : super( - elevation: 0.0, - groupAlignment: -1, - labelType: NavigationRailLabelType.none, - useIndicator: true, - minWidth: 80.0, - minExtendedWidth: 256, - ); + : super( + elevation: 0.0, + groupAlignment: -1, + labelType: NavigationRailLabelType.none, + useIndicator: true, + minWidth: 80.0, + minExtendedWidth: 256, + ); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; @@ -1009,6 +1060,7 @@ class _NavigationRailDefaultsM3 extends NavigationRailThemeData { @override Color? get indicatorColor => _colors.secondaryContainer; + @override ShapeBorder? get indicatorShape => const StadiumBorder(); } // END GENERATED TOKEN PROPERTIES - NavigationRail diff --git a/packages/flutter/lib/src/material/navigation_rail_theme.dart b/packages/flutter/lib/src/material/navigation_rail_theme.dart index 45e7514c0bc..742bea30f88 100644 --- a/packages/flutter/lib/src/material/navigation_rail_theme.dart +++ b/packages/flutter/lib/src/material/navigation_rail_theme.dart @@ -49,6 +49,7 @@ class NavigationRailThemeData with Diagnosticable { this.labelType, this.useIndicator, this.indicatorColor, + this.indicatorShape, this.minWidth, this.minExtendedWidth, }); @@ -91,6 +92,9 @@ class NavigationRailThemeData with Diagnosticable { /// when [useIndicator] is true. final Color? indicatorColor; + /// Overrides the default shape of the [NavigationRail]'s selection indicator. + final ShapeBorder? indicatorShape; + /// Overrides the default value of [NavigationRail]'s minimum width when it /// is not extended. final double? minWidth; @@ -112,6 +116,7 @@ class NavigationRailThemeData with Diagnosticable { NavigationRailLabelType? labelType, bool? useIndicator, Color? indicatorColor, + ShapeBorder? indicatorShape, double? minWidth, double? minExtendedWidth, }) { @@ -126,6 +131,7 @@ class NavigationRailThemeData with Diagnosticable { labelType: labelType ?? this.labelType, useIndicator: useIndicator ?? this.useIndicator, indicatorColor: indicatorColor ?? this.indicatorColor, + indicatorShape: indicatorShape ?? this.indicatorShape, minWidth: minWidth ?? this.minWidth, minExtendedWidth: minExtendedWidth ?? this.minExtendedWidth, ); @@ -152,6 +158,7 @@ class NavigationRailThemeData with Diagnosticable { labelType: t < 0.5 ? a?.labelType : b?.labelType, useIndicator: t < 0.5 ? a?.useIndicator : b?.useIndicator, indicatorColor: Color.lerp(a?.indicatorColor, b?.indicatorColor, t), + indicatorShape: ShapeBorder.lerp(a?.indicatorShape, b?.indicatorShape, t), minWidth: lerpDouble(a?.minWidth, b?.minWidth, t), minExtendedWidth: lerpDouble(a?.minExtendedWidth, b?.minExtendedWidth, t), @@ -170,6 +177,7 @@ class NavigationRailThemeData with Diagnosticable { labelType, useIndicator, indicatorColor, + indicatorShape, minWidth, minExtendedWidth, ); @@ -193,6 +201,7 @@ class NavigationRailThemeData with Diagnosticable { && other.labelType == labelType && other.useIndicator == useIndicator && other.indicatorColor == indicatorColor + && other.indicatorShape == indicatorShape && other.minWidth == minWidth && other.minExtendedWidth == minExtendedWidth; } @@ -212,6 +221,7 @@ class NavigationRailThemeData with Diagnosticable { properties.add(DiagnosticsProperty('labelType', labelType, defaultValue: defaultData.labelType)); properties.add(DiagnosticsProperty('useIndicator', useIndicator, defaultValue: defaultData.useIndicator)); properties.add(ColorProperty('indicatorColor', indicatorColor, defaultValue: defaultData.indicatorColor)); + properties.add(DiagnosticsProperty('indicatorShape', indicatorShape, defaultValue: null)); properties.add(DoubleProperty('minWidth', minWidth, defaultValue: defaultData.minWidth)); properties.add(DoubleProperty('minExtendedWidth', minExtendedWidth, defaultValue: defaultData.minExtendedWidth)); } diff --git a/packages/flutter/test/material/navigation_rail_test.dart b/packages/flutter/test/material/navigation_rail_test.dart index 53207a40060..f23539e68e8 100644 --- a/packages/flutter/test/material/navigation_rail_test.dart +++ b/packages/flutter/test/material/navigation_rail_test.dart @@ -2,10 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; void main() { @@ -2314,14 +2317,24 @@ void main() { ), ); + final Iterable indicatorInkWells = tester.allWidgets.where((Widget object) => object.runtimeType.toString() == '_IndicatorInkWell'); final Padding firstItem = tester.widget( - find.descendant(of: find.widgetWithText(InkResponse, 'Abc'), matching: find.widgetWithText(Padding, 'Abc')) + find.descendant( + of: find.widgetWithText(indicatorInkWells.elementAt(0).runtimeType, 'Abc'), + matching: find.widgetWithText(Padding, 'Abc'), + ) ); final Padding secondItem = tester.widget( - find.descendant(of: find.widgetWithText(InkResponse, 'Def'), matching: find.widgetWithText(Padding, 'Def')) + find.descendant( + of: find.widgetWithText(indicatorInkWells.elementAt(1).runtimeType, 'Def'), + matching: find.widgetWithText(Padding, 'Def'), + ) ); final Padding thirdItem = tester.widget( - find.descendant(of: find.widgetWithText(InkResponse, 'Ghi'), matching: find.widgetWithText(Padding, 'Ghi')) + find.descendant( + of: find.widgetWithText(indicatorInkWells.elementAt(2).runtimeType, 'Ghi'), + matching: find.widgetWithText(Padding, 'Ghi'), + ) ); expect(firstItem.padding, defaultPadding); @@ -2361,14 +2374,24 @@ void main() { ), ); + final Iterable indicatorInkWells = tester.allWidgets.where((Widget object) => object.runtimeType.toString() == '_IndicatorInkWell'); final Padding firstItem = tester.widget( - find.descendant(of: find.widgetWithText(InkResponse, 'Abc'), matching: find.widgetWithText(Padding, 'Abc')) + find.descendant( + of: find.widgetWithText(indicatorInkWells.elementAt(0).runtimeType, 'Abc'), + matching: find.widgetWithText(Padding, 'Abc'), + ) ); final Padding secondItem = tester.widget( - find.descendant(of: find.widgetWithText(InkResponse, 'Def'), matching: find.widgetWithText(Padding, 'Def')) + find.descendant( + of: find.widgetWithText(indicatorInkWells.elementAt(1).runtimeType, 'Def'), + matching: find.widgetWithText(Padding, 'Def'), + ) ); final Padding thirdItem = tester.widget( - find.descendant(of: find.widgetWithText(InkResponse, 'Ghi'), matching: find.widgetWithText(Padding, 'Ghi')) + find.descendant( + of: find.widgetWithText(indicatorInkWells.elementAt(2).runtimeType, 'Ghi'), + matching: find.widgetWithText(Padding, 'Ghi'), + ) ); expect(firstItem.padding, defaultPadding); @@ -2408,14 +2431,24 @@ void main() { ), ); + final Iterable indicatorInkWells = tester.allWidgets.where((Widget object) => object.runtimeType.toString() == '_IndicatorInkWell'); final Padding firstItem = tester.widget( - find.descendant(of: find.widgetWithText(InkResponse, 'Abc'), matching: find.widgetWithText(Padding, 'Abc')) + find.descendant( + of: find.widgetWithText(indicatorInkWells.elementAt(0).runtimeType, 'Abc'), + matching: find.widgetWithText(Padding, 'Abc'), + ) ); final Padding secondItem = tester.widget( - find.descendant(of: find.widgetWithText(InkResponse, 'Def'), matching: find.widgetWithText(Padding, 'Def')) + find.descendant( + of: find.widgetWithText(indicatorInkWells.elementAt(1).runtimeType, 'Def'), + matching: find.widgetWithText(Padding, 'Def'), + ) ); final Padding thirdItem = tester.widget( - find.descendant(of: find.widgetWithText(InkResponse, 'Ghi'), matching: find.widgetWithText(Padding, 'Ghi')) + find.descendant( + of: find.widgetWithText(indicatorInkWells.elementAt(2).runtimeType, 'Ghi'), + matching: find.widgetWithText(Padding, 'Ghi'), + ) ); expect(firstItem.padding, defaultPadding); @@ -2708,6 +2741,111 @@ void main() { expect(updatedWidthRTL, defaultWidth + safeAreaPadding); }); + testWidgets('NavigationRail indicator renders ripple', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + navigationRail: NavigationRail( + selectedIndex: 1, + destinations: const [ + NavigationRailDestination( + icon: Icon(Icons.favorite_border), + selectedIcon: Icon(Icons.favorite), + label: Text('Abc'), + ), + NavigationRailDestination( + icon: Icon(Icons.bookmark_border), + selectedIcon: Icon(Icons.bookmark), + label: Text('Def'), + ), + ], + labelType: NavigationRailLabelType.all, + ), + ); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.byIcon(Icons.favorite_border))); + await tester.pumpAndSettle(); + + final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); + const Rect indicatorRect = Rect.fromLTRB(12.0, 0.0, 68.0, 32.0); + const Rect includedRect = indicatorRect; + final Rect excludedRect = includedRect.inflate(10); + + expect( + inkFeatures, + paints + ..clipPath( + pathMatcher: isPathThat( + includes: [ + includedRect.centerLeft, + includedRect.topCenter, + includedRect.centerRight, + includedRect.bottomCenter, + ], + excludes: [ + excludedRect.centerLeft, + excludedRect.topCenter, + excludedRect.centerRight, + excludedRect.bottomCenter, + ], + ), + ) + ..rect( + rect: indicatorRect, + color: const Color(0x0a6750a4), + ) + ..rrect( + rrect: RRect.fromLTRBR(12.0, 72.0, 68.0, 104.0, const Radius.circular(16)), + color: const Color(0xffe8def8), + ), + ); + }); + + testWidgets('NavigationRail indicator scale transform', (WidgetTester tester) async { + int selectedIndex = 0; + Future buildWidget() async { + await _pumpNavigationRail( + tester, + navigationRail: NavigationRail( + selectedIndex: selectedIndex, + destinations: const [ + NavigationRailDestination( + icon: Icon(Icons.favorite_border), + selectedIcon: Icon(Icons.favorite), + label: Text('Abc'), + ), + NavigationRailDestination( + icon: Icon(Icons.bookmark_border), + selectedIcon: Icon(Icons.bookmark), + label: Text('Def'), + ), + ], + labelType: NavigationRailLabelType.all, + ), + ); + } + + await buildWidget(); + 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 buildWidget(); + await tester.pump(const Duration(milliseconds: 100)); + transform = tester.widget(transformFinder).transform; + expect(transform.getColumn(0)[0], closeTo(0.9705023956298828, precisionErrorTolerance)); + + await tester.pump(const Duration(milliseconds: 100)); + transform = tester.widget(transformFinder).transform; + expect(transform.getColumn(0)[0], 1.0); + }); + group('Material 2', () { // Original Material 2 tests. Remove this group after `useMaterial3` has been deprecated. testWidgets('Renders at the correct default width - [labelType]=none (default)', (WidgetTester tester) async { diff --git a/packages/flutter/test/material/navigation_rail_theme_test.dart b/packages/flutter/test/material/navigation_rail_theme_test.dart index 6f0ff40c0af..e0c7205980c 100644 --- a/packages/flutter/test/material/navigation_rail_theme_test.dart +++ b/packages/flutter/test/material/navigation_rail_theme_test.dart @@ -40,6 +40,10 @@ void main() { expect(_destinationsAlign(tester).alignment, Alignment.topCenter); expect(_labelType(tester), NavigationRailLabelType.none); expect(find.byType(NavigationIndicator), findsWidgets); + expect(_indicatorDecoration(tester)?.color, ThemeData().colorScheme.secondaryContainer); + expect(_indicatorDecoration(tester)?.shape, const StadiumBorder()); + final InkResponse inkResponse = tester.allWidgets.firstWhere((Widget object) => object.runtimeType.toString() == '_IndicatorInkWell') as InkResponse; + expect(inkResponse.customBorder, const StadiumBorder()); }); testWidgets('Default values are used when no NavigationRail or NavigationRailThemeData properties are specified (Material 2)', (WidgetTester tester) async { @@ -87,6 +91,7 @@ void main() { const NavigationRailLabelType labelType = NavigationRailLabelType.all; const bool useIndicator = true; const Color indicatorColor = Color(0x00000004); + const ShapeBorder indicatorShape = RoundedRectangleBorder(); await tester.pumpWidget( MaterialApp( @@ -111,6 +116,7 @@ void main() { labelType: labelType, useIndicator: useIndicator, indicatorColor: indicatorColor, + indicatorShape: indicatorShape, ), child: NavigationRail( selectedIndex: 0, @@ -135,6 +141,7 @@ void main() { expect(_labelType(tester), labelType); expect(find.byType(NavigationIndicator), findsWidgets); expect(_indicatorDecoration(tester)?.color, indicatorColor); + expect(_indicatorDecoration(tester)?.shape, indicatorShape); }); testWidgets('NavigationRail values take priority over NavigationRailThemeData values when both properties are specified', (WidgetTester tester) async { @@ -245,6 +252,7 @@ void main() { labelType: NavigationRailLabelType.selected, useIndicator: true, indicatorColor: Color(0x00000096), + indicatorShape: CircleBorder(), ).debugFillProperties(builder); final List description = builder.properties @@ -267,6 +275,7 @@ void main() { expect(description[7], 'labelType: NavigationRailLabelType.selected'); expect(description[8], 'useIndicator: true'); expect(description[9], 'indicatorColor: Color(0x00000096)'); + expect(description[10], 'indicatorShape: CircleBorder(BorderSide(width: 0.0, style: none))'); }); }