Skip to content

Commit 6e2bbf9

Browse files
authored
Fix Animations in NavigationDestination icons don't work (#123400)
1 parent 94f8772 commit 6e2bbf9

File tree

2 files changed

+85
-41
lines changed

2 files changed

+85
-41
lines changed

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

Lines changed: 25 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ class NavigationDestination extends StatelessWidget {
414414
/// animation value of 0 is unselected and 1 is selected.
415415
///
416416
/// See [NavigationDestination] for an example.
417-
class _NavigationDestinationBuilder extends StatelessWidget {
417+
class _NavigationDestinationBuilder extends StatefulWidget {
418418
/// Builds a destination (icon + label) to use in a Material 3 [NavigationBar].
419419
const _NavigationDestinationBuilder({
420420
required this.buildIcon,
@@ -459,31 +459,34 @@ class _NavigationDestinationBuilder extends StatelessWidget {
459459
/// Defaults to null, in which case the [label] text will be used.
460460
final String? tooltip;
461461

462+
@override
463+
State<_NavigationDestinationBuilder> createState() => _NavigationDestinationBuilderState();
464+
}
465+
466+
class _NavigationDestinationBuilderState extends State<_NavigationDestinationBuilder> {
467+
final GlobalKey iconKey = GlobalKey();
468+
462469
@override
463470
Widget build(BuildContext context) {
464471
final _NavigationDestinationInfo info = _NavigationDestinationInfo.of(context);
465472
final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context);
466473
final NavigationBarThemeData defaults = _defaultsFor(context);
467-
final GlobalKey labelKey = GlobalKey();
468474

469-
final bool selected = info.selectedIndex == info.index;
470475
return _NavigationBarDestinationSemantics(
471476
child: _NavigationBarDestinationTooltip(
472-
message: tooltip ?? label,
477+
message: widget.tooltip ?? widget.label,
473478
child: _IndicatorInkWell(
474-
key: UniqueKey(),
475-
labelKey: labelKey,
479+
iconKey: iconKey,
476480
labelBehavior: info.labelBehavior,
477-
selected: selected,
478481
customBorder: navigationBarTheme.indicatorShape ?? defaults.indicatorShape,
479482
onTap: info.onTap,
480483
child: Row(
481484
children: <Widget>[
482485
Expanded(
483486
child: _NavigationBarDestinationLayout(
484-
icon: buildIcon(context),
485-
labelKey: labelKey,
486-
label: buildLabel(context),
487+
icon: widget.buildIcon(context),
488+
iconKey: iconKey,
489+
label: widget.buildLabel(context),
487490
),
488491
),
489492
],
@@ -496,10 +499,8 @@ class _NavigationDestinationBuilder extends StatelessWidget {
496499

497500
class _IndicatorInkWell extends InkResponse {
498501
const _IndicatorInkWell({
499-
super.key,
500-
required this.labelKey,
502+
required this.iconKey,
501503
required this.labelBehavior,
502-
required this.selected,
503504
super.customBorder,
504505
super.onTap,
505506
super.child,
@@ -508,32 +509,15 @@ class _IndicatorInkWell extends InkResponse {
508509
highlightColor: Colors.transparent,
509510
);
510511

511-
final GlobalKey labelKey;
512+
final GlobalKey iconKey;
512513
final NavigationDestinationLabelBehavior labelBehavior;
513-
final bool selected;
514514

515515
@override
516516
RectCallback? getRectCallback(RenderBox referenceBox) {
517-
final RenderBox labelBox = labelKey.currentContext!.findRenderObject()! as RenderBox;
518-
final Rect labelRect = labelBox.localToGlobal(Offset.zero) & labelBox.size;
519-
final double labelPadding;
520-
switch (labelBehavior) {
521-
case NavigationDestinationLabelBehavior.alwaysShow:
522-
labelPadding = labelRect.height / 2;
523-
case NavigationDestinationLabelBehavior.onlyShowSelected:
524-
labelPadding = selected ? labelRect.height / 2 : 0;
525-
case NavigationDestinationLabelBehavior.alwaysHide:
526-
labelPadding = 0;
527-
}
528-
final double indicatorOffsetX = referenceBox.size.width / 2;
529-
final double indicatorOffsetY = referenceBox.size.height / 2 - labelPadding;
530-
531517
return () {
532-
return Rect.fromCenter(
533-
center: Offset(indicatorOffsetX, indicatorOffsetY),
534-
width: _kIndicatorWidth,
535-
height: _kIndicatorHeight,
536-
);
518+
final RenderBox iconBox = iconKey.currentContext!.findRenderObject()! as RenderBox;
519+
final Rect iconRect = iconBox.localToGlobal(Offset.zero) & iconBox.size;
520+
return referenceBox.globalToLocal(iconRect.topLeft) & iconBox.size;
537521
};
538522
}
539523
}
@@ -774,7 +758,7 @@ class _NavigationBarDestinationLayout extends StatelessWidget {
774758
/// 3 [NavigationBar].
775759
const _NavigationBarDestinationLayout({
776760
required this.icon,
777-
required this.labelKey,
761+
required this.iconKey,
778762
required this.label,
779763
});
780764

@@ -783,10 +767,10 @@ class _NavigationBarDestinationLayout extends StatelessWidget {
783767
/// See [NavigationDestination.icon].
784768
final Widget icon;
785769

786-
/// The global key for the label of this destination.
770+
/// The global key for the icon of this destination.
787771
///
788-
/// This is used to determine the position of the label relative to the icon.
789-
final GlobalKey labelKey;
772+
/// This is used to determine the position of the icon.
773+
final GlobalKey iconKey;
790774

791775
/// The label widget that sits below the icon.
792776
///
@@ -796,7 +780,7 @@ class _NavigationBarDestinationLayout extends StatelessWidget {
796780
/// See [NavigationDestination.label].
797781
final Widget label;
798782

799-
static final Key _iconKey = UniqueKey();
783+
static final Key _labelKey = UniqueKey();
800784

801785
@override
802786
Widget build(BuildContext context) {
@@ -810,7 +794,7 @@ class _NavigationBarDestinationLayout extends StatelessWidget {
810794
LayoutId(
811795
id: _NavigationDestinationLayoutDelegate.iconId,
812796
child: RepaintBoundary(
813-
key: _iconKey,
797+
key: iconKey,
814798
child: icon,
815799
),
816800
),
@@ -820,7 +804,7 @@ class _NavigationBarDestinationLayout extends StatelessWidget {
820804
alwaysIncludeSemantics: true,
821805
opacity: animation,
822806
child: RepaintBoundary(
823-
key: labelKey,
807+
key: _labelKey,
824808
child: label,
825809
),
826810
),

packages/flutter/test/material/navigation_bar_test.dart

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
@Tags(<String>['reduced-test-set'])
88
library;
99

10+
import 'dart:math';
11+
1012
import 'package:flutter/foundation.dart';
1113
import 'package:flutter/gestures.dart';
1214
import 'package:flutter/material.dart';
@@ -1217,6 +1219,52 @@ void main() {
12171219

12181220
await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_onlyShowSelected_unselected_m2.png'));
12191221
});
1222+
1223+
testWidgets('Destination icon does not rebuild when tapped', (WidgetTester tester) async {
1224+
// This is a regression test for https://github.com/flutter/flutter/issues/122811.
1225+
1226+
Widget buildNavigationBar() {
1227+
return MaterialApp(
1228+
home: Scaffold(
1229+
bottomNavigationBar: StatefulBuilder(
1230+
builder: (BuildContext context, StateSetter setState) {
1231+
int selectedIndex = 0;
1232+
return NavigationBar(
1233+
selectedIndex: selectedIndex,
1234+
destinations: const <Widget>[
1235+
NavigationDestination(
1236+
icon: IconWithRandomColor(icon: Icons.ac_unit),
1237+
label: 'AC',
1238+
),
1239+
NavigationDestination(
1240+
icon: IconWithRandomColor(icon: Icons.access_alarm),
1241+
label: 'Alarm',
1242+
),
1243+
],
1244+
onDestinationSelected: (int i) {
1245+
setState(() {
1246+
selectedIndex = i;
1247+
});
1248+
},
1249+
);
1250+
}
1251+
),
1252+
),
1253+
);
1254+
}
1255+
1256+
await tester.pumpWidget(buildNavigationBar());
1257+
Icon icon = tester.widget<Icon>(find.byType(Icon).last);
1258+
final Color initialColor = icon.color!;
1259+
1260+
// Trigger a rebuild.
1261+
await tester.tap(find.text('Alarm'));
1262+
await tester.pumpAndSettle();
1263+
1264+
// Icon color should be the same as before the rebuild.
1265+
icon = tester.widget<Icon>(find.byType(Icon).last);
1266+
expect(icon.color, initialColor);
1267+
});
12201268
});
12211269
}
12221270

@@ -1245,3 +1293,15 @@ ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) {
12451293
),
12461294
).decoration as ShapeDecoration?;
12471295
}
1296+
1297+
class IconWithRandomColor extends StatelessWidget {
1298+
const IconWithRandomColor({super.key, required this.icon});
1299+
1300+
final IconData icon;
1301+
1302+
@override
1303+
Widget build(BuildContext context) {
1304+
final Color randomColor = Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
1305+
return Icon(icon, color: randomColor);
1306+
}
1307+
}

0 commit comments

Comments
 (0)