Skip to content

Commit 5fcb48d

Browse files
authored
Fix NavigationRail highlight (#117320)
1 parent cb988c7 commit 5fcb48d

File tree

2 files changed

+286
-15
lines changed

2 files changed

+286
-15
lines changed

packages/flutter/lib/src/material/navigation_rail.dart

+28-15
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'text_theme.dart';
1616
import 'theme.dart';
1717

1818
const double _kCircularIndicatorDiameter = 56;
19+
const double _kIndicatorHeight = 32;
1920

2021
/// A Material Design widget that is meant to be displayed at the left or right of an
2122
/// app to navigate between a small number of views, typically between three and
@@ -590,7 +591,8 @@ class _RailDestination extends StatelessWidget {
590591
);
591592

592593
final bool material3 = Theme.of(context).useMaterial3;
593-
final double indicatorInkOffsetY;
594+
final EdgeInsets destionationPadding = (padding ?? EdgeInsets.zero).resolve(Directionality.of(context));
595+
Offset indicatorOffset;
594596

595597
final Widget themedIcon = IconTheme(
596598
data: iconTheme,
@@ -607,8 +609,10 @@ class _RailDestination extends StatelessWidget {
607609
case NavigationRailLabelType.none:
608610
// Split the destination spacing across the top and bottom to keep the icon centered.
609611
final Widget? spacing = material3 ? const SizedBox(height: _verticalDestinationSpacingM3 / 2) : null;
610-
indicatorInkOffsetY = _verticalDestinationPaddingNoLabel - (_verticalIconLabelSpacingM3 / 2);
611-
612+
indicatorOffset = Offset(
613+
minWidth / 2 + destionationPadding.left,
614+
_verticalDestinationSpacingM3 / 2 + destionationPadding.top,
615+
);
612616
final Widget iconPart = Column(
613617
children: <Widget>[
614618
if (spacing != null) spacing,
@@ -686,8 +690,12 @@ class _RailDestination extends StatelessWidget {
686690
final Widget topSpacing = SizedBox(height: material3 ? 0 : verticalPadding);
687691
final Widget labelSpacing = SizedBox(height: material3 ? lerpDouble(0, _verticalIconLabelSpacingM3, appearingAnimationValue)! : 0);
688692
final Widget bottomSpacing = SizedBox(height: material3 ? _verticalDestinationSpacingM3 : verticalPadding);
689-
indicatorInkOffsetY = _verticalDestinationPaddingWithLabel;
690-
693+
final double indicatorHorizontalPadding = (destionationPadding.left / 2) - (destionationPadding.right / 2);
694+
final double indicatorVerticalPadding = destionationPadding.top;
695+
indicatorOffset = Offset(minWidth / 2 + indicatorHorizontalPadding, indicatorVerticalPadding);
696+
if (minWidth < _NavigationRailDefaultsM2(context).minWidth!) {
697+
indicatorOffset = Offset(minWidth / 2 + _horizontalDestinationSpacingM3, indicatorVerticalPadding);
698+
}
691699
content = Container(
692700
constraints: BoxConstraints(
693701
minWidth: minWidth,
@@ -730,7 +738,12 @@ class _RailDestination extends StatelessWidget {
730738
final Widget topSpacing = SizedBox(height: material3 ? 0 : _verticalDestinationPaddingWithLabel);
731739
final Widget labelSpacing = SizedBox(height: material3 ? _verticalIconLabelSpacingM3 : 0);
732740
final Widget bottomSpacing = SizedBox(height: material3 ? _verticalDestinationSpacingM3 : _verticalDestinationPaddingWithLabel);
733-
indicatorInkOffsetY = _verticalDestinationPaddingWithLabel;
741+
final double indicatorHorizontalPadding = (destionationPadding.left / 2) - (destionationPadding.right / 2);
742+
final double indicatorVerticalPadding = destionationPadding.top;
743+
indicatorOffset = Offset(minWidth / 2 + indicatorHorizontalPadding, indicatorVerticalPadding);
744+
if (minWidth < _NavigationRailDefaultsM2(context).minWidth!) {
745+
indicatorOffset = Offset(minWidth / 2 + _horizontalDestinationSpacingM3, indicatorVerticalPadding);
746+
}
734747
content = Container(
735748
constraints: BoxConstraints(
736749
minWidth: minWidth,
@@ -772,7 +785,7 @@ class _RailDestination extends StatelessWidget {
772785
splashColor: colors.primary.withOpacity(0.12),
773786
hoverColor: colors.primary.withOpacity(0.04),
774787
useMaterial3: material3,
775-
indicatorOffsetY: indicatorInkOffsetY,
788+
indicatorOffset: indicatorOffset,
776789
child: content,
777790
),
778791
),
@@ -794,7 +807,7 @@ class _IndicatorInkWell extends InkResponse {
794807
super.splashColor,
795808
super.hoverColor,
796809
required this.useMaterial3,
797-
required this.indicatorOffsetY,
810+
required this.indicatorOffset,
798811
}) : super(
799812
containedInkWell: true,
800813
highlightShape: BoxShape.rectangle,
@@ -803,18 +816,17 @@ class _IndicatorInkWell extends InkResponse {
803816
);
804817

805818
final bool useMaterial3;
806-
final double indicatorOffsetY;
819+
final Offset indicatorOffset;
807820

808821
@override
809822
RectCallback? getRectCallback(RenderBox referenceBox) {
810-
final double indicatorOffsetX = referenceBox.size.width / 2;
811-
812823
if (useMaterial3) {
813824
return () {
814-
return Rect.fromCenter(
815-
center: Offset(indicatorOffsetX, indicatorOffsetY),
816-
width: _kCircularIndicatorDiameter,
817-
height: 32,
825+
return Rect.fromLTWH(
826+
indicatorOffset.dx - (_kCircularIndicatorDiameter / 2),
827+
indicatorOffset.dy,
828+
_kCircularIndicatorDiameter,
829+
_kIndicatorHeight,
818830
);
819831
};
820832
}
@@ -984,6 +996,7 @@ const double _verticalDestinationPaddingWithLabel = 16.0;
984996
const Widget _verticalSpacer = SizedBox(height: 8.0);
985997
const double _verticalIconLabelSpacingM3 = 4.0;
986998
const double _verticalDestinationSpacingM3 = 12.0;
999+
const double _horizontalDestinationSpacingM3 = 12.0;
9871000

9881001
// Hand coded defaults based on Material Design 2.
9891002
class _NavigationRailDefaultsM2 extends NavigationRailThemeData {

packages/flutter/test/material/navigation_rail_test.dart

+258
Original file line numberDiff line numberDiff line change
@@ -2802,6 +2802,264 @@ void main() {
28022802
);
28032803
});
28042804

2805+
testWidgets('NavigationRail indicator renders ripple - extended', (WidgetTester tester) async {
2806+
// This is a regression test for https://github.com/flutter/flutter/issues/117126
2807+
await _pumpNavigationRail(
2808+
tester,
2809+
navigationRail: NavigationRail(
2810+
selectedIndex: 1,
2811+
extended: true,
2812+
destinations: const <NavigationRailDestination>[
2813+
NavigationRailDestination(
2814+
icon: Icon(Icons.favorite_border),
2815+
selectedIcon: Icon(Icons.favorite),
2816+
label: Text('Abc'),
2817+
),
2818+
NavigationRailDestination(
2819+
icon: Icon(Icons.bookmark_border),
2820+
selectedIcon: Icon(Icons.bookmark),
2821+
label: Text('Def'),
2822+
),
2823+
],
2824+
labelType: NavigationRailLabelType.none,
2825+
),
2826+
);
2827+
2828+
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
2829+
await gesture.addPointer();
2830+
await gesture.moveTo(tester.getCenter(find.byIcon(Icons.favorite_border)));
2831+
await tester.pumpAndSettle();
2832+
2833+
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
2834+
const Rect indicatorRect = Rect.fromLTRB(12.0, 6.0, 68.0, 38.0);
2835+
const Rect includedRect = indicatorRect;
2836+
final Rect excludedRect = includedRect.inflate(10);
2837+
2838+
expect(
2839+
inkFeatures,
2840+
paints
2841+
..clipPath(
2842+
pathMatcher: isPathThat(
2843+
includes: <Offset>[
2844+
includedRect.centerLeft,
2845+
includedRect.topCenter,
2846+
includedRect.centerRight,
2847+
includedRect.bottomCenter,
2848+
],
2849+
excludes: <Offset>[
2850+
excludedRect.centerLeft,
2851+
excludedRect.topCenter,
2852+
excludedRect.centerRight,
2853+
excludedRect.bottomCenter,
2854+
],
2855+
),
2856+
)
2857+
..rect(
2858+
rect: indicatorRect,
2859+
color: const Color(0x0a6750a4),
2860+
)
2861+
..rrect(
2862+
rrect: RRect.fromLTRBR(12.0, 58.0, 68.0, 90.0, const Radius.circular(16)),
2863+
color: const Color(0xffe8def8),
2864+
),
2865+
);
2866+
});
2867+
2868+
testWidgets('NavigationRail indicator renders properly when padding is applied', (WidgetTester tester) async {
2869+
// This is a regression test for https://github.com/flutter/flutter/issues/117126
2870+
await _pumpNavigationRail(
2871+
tester,
2872+
navigationRail: NavigationRail(
2873+
selectedIndex: 1,
2874+
extended: true,
2875+
destinations: const <NavigationRailDestination>[
2876+
NavigationRailDestination(
2877+
padding: EdgeInsets.all(10),
2878+
icon: Icon(Icons.favorite_border),
2879+
selectedIcon: Icon(Icons.favorite),
2880+
label: Text('Abc'),
2881+
),
2882+
NavigationRailDestination(
2883+
padding: EdgeInsets.all(18),
2884+
icon: Icon(Icons.bookmark_border),
2885+
selectedIcon: Icon(Icons.bookmark),
2886+
label: Text('Def'),
2887+
),
2888+
],
2889+
labelType: NavigationRailLabelType.none,
2890+
),
2891+
);
2892+
2893+
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
2894+
await gesture.addPointer();
2895+
await gesture.moveTo(tester.getCenter(find.byIcon(Icons.favorite_border)));
2896+
await tester.pumpAndSettle();
2897+
2898+
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
2899+
const Rect indicatorRect = Rect.fromLTRB(22.0, 16.0, 78.0, 48.0);
2900+
const Rect includedRect = indicatorRect;
2901+
final Rect excludedRect = includedRect.inflate(10);
2902+
2903+
expect(
2904+
inkFeatures,
2905+
paints
2906+
..clipPath(
2907+
pathMatcher: isPathThat(
2908+
includes: <Offset>[
2909+
includedRect.centerLeft,
2910+
includedRect.topCenter,
2911+
includedRect.centerRight,
2912+
includedRect.bottomCenter,
2913+
],
2914+
excludes: <Offset>[
2915+
excludedRect.centerLeft,
2916+
excludedRect.topCenter,
2917+
excludedRect.centerRight,
2918+
excludedRect.bottomCenter,
2919+
],
2920+
),
2921+
)
2922+
..rect(
2923+
rect: indicatorRect,
2924+
color: const Color(0x0a6750a4),
2925+
)
2926+
..rrect(
2927+
rrect: RRect.fromLTRBR(30.0, 96.0, 86.0, 128.0, const Radius.circular(16)),
2928+
color: const Color(0xffe8def8),
2929+
),
2930+
);
2931+
});
2932+
2933+
testWidgets('Indicator renders properly when NavigationRai.minWidth < default minWidth', (WidgetTester tester) async {
2934+
// This is a regression test for https://github.com/flutter/flutter/issues/117126
2935+
await _pumpNavigationRail(
2936+
tester,
2937+
navigationRail: NavigationRail(
2938+
minWidth: 50,
2939+
selectedIndex: 1,
2940+
extended: true,
2941+
destinations: const <NavigationRailDestination>[
2942+
NavigationRailDestination(
2943+
icon: Icon(Icons.favorite_border),
2944+
selectedIcon: Icon(Icons.favorite),
2945+
label: Text('Abc'),
2946+
),
2947+
NavigationRailDestination(
2948+
icon: Icon(Icons.bookmark_border),
2949+
selectedIcon: Icon(Icons.bookmark),
2950+
label: Text('Def'),
2951+
),
2952+
],
2953+
labelType: NavigationRailLabelType.none,
2954+
),
2955+
);
2956+
2957+
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
2958+
await gesture.addPointer();
2959+
await gesture.moveTo(tester.getCenter(find.byIcon(Icons.favorite_border)));
2960+
await tester.pumpAndSettle();
2961+
2962+
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
2963+
const Rect indicatorRect = Rect.fromLTRB(-3.0, 6.0, 53.0, 38.0);
2964+
const Rect includedRect = indicatorRect;
2965+
final Rect excludedRect = includedRect.inflate(10);
2966+
2967+
expect(
2968+
inkFeatures,
2969+
paints
2970+
..clipPath(
2971+
pathMatcher: isPathThat(
2972+
includes: <Offset>[
2973+
includedRect.centerLeft,
2974+
includedRect.topCenter,
2975+
includedRect.centerRight,
2976+
includedRect.bottomCenter,
2977+
],
2978+
excludes: <Offset>[
2979+
excludedRect.centerLeft,
2980+
excludedRect.topCenter,
2981+
excludedRect.centerRight,
2982+
excludedRect.bottomCenter,
2983+
],
2984+
),
2985+
)
2986+
..rect(
2987+
rect: indicatorRect,
2988+
color: const Color(0x0a6750a4),
2989+
)
2990+
..rrect(
2991+
rrect: RRect.fromLTRBR(0.0, 58.0, 50.0, 90.0, const Radius.circular(16)),
2992+
color: const Color(0xffe8def8),
2993+
),
2994+
);
2995+
});
2996+
2997+
testWidgets('NavigationRail indicator renders properly with custom padding and minWidth', (WidgetTester tester) async {
2998+
// This is a regression test for https://github.com/flutter/flutter/issues/117126
2999+
await _pumpNavigationRail(
3000+
tester,
3001+
navigationRail: NavigationRail(
3002+
minWidth: 300,
3003+
selectedIndex: 1,
3004+
extended: true,
3005+
destinations: const <NavigationRailDestination>[
3006+
NavigationRailDestination(
3007+
padding: EdgeInsets.all(10),
3008+
icon: Icon(Icons.favorite_border),
3009+
selectedIcon: Icon(Icons.favorite),
3010+
label: Text('Abc'),
3011+
),
3012+
NavigationRailDestination(
3013+
padding: EdgeInsets.all(18),
3014+
icon: Icon(Icons.bookmark_border),
3015+
selectedIcon: Icon(Icons.bookmark),
3016+
label: Text('Def'),
3017+
),
3018+
],
3019+
labelType: NavigationRailLabelType.none,
3020+
),
3021+
);
3022+
3023+
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
3024+
await gesture.addPointer();
3025+
await gesture.moveTo(tester.getCenter(find.byIcon(Icons.favorite_border)));
3026+
await tester.pumpAndSettle();
3027+
3028+
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
3029+
const Rect indicatorRect = Rect.fromLTRB(132.0, 16.0, 188.0, 48.0);
3030+
const Rect includedRect = indicatorRect;
3031+
final Rect excludedRect = includedRect.inflate(10);
3032+
3033+
expect(
3034+
inkFeatures,
3035+
paints
3036+
..clipPath(
3037+
pathMatcher: isPathThat(
3038+
includes: <Offset>[
3039+
includedRect.centerLeft,
3040+
includedRect.topCenter,
3041+
includedRect.centerRight,
3042+
includedRect.bottomCenter,
3043+
],
3044+
excludes: <Offset>[
3045+
excludedRect.centerLeft,
3046+
excludedRect.topCenter,
3047+
excludedRect.centerRight,
3048+
excludedRect.bottomCenter,
3049+
],
3050+
),
3051+
)
3052+
..rect(
3053+
rect: indicatorRect,
3054+
color: const Color(0x0a6750a4),
3055+
)
3056+
..rrect(
3057+
rrect: RRect.fromLTRBR(140.0, 96.0, 196.0, 128.0, const Radius.circular(16)),
3058+
color: const Color(0xffe8def8),
3059+
),
3060+
);
3061+
});
3062+
28053063
testWidgets('NavigationRail indicator scale transform', (WidgetTester tester) async {
28063064
int selectedIndex = 0;
28073065
Future<void> buildWidget() async {

0 commit comments

Comments
 (0)