Unverified Commit 6e2bbf94 authored by Bruno Leroux's avatar Bruno Leroux Committed by GitHub

Fix Animations in NavigationDestination icons don't work (#123400)

parent 94f8772c
......@@ -414,7 +414,7 @@ class NavigationDestination extends StatelessWidget {
/// animation value of 0 is unselected and 1 is selected.
///
/// See [NavigationDestination] for an example.
class _NavigationDestinationBuilder extends StatelessWidget {
class _NavigationDestinationBuilder extends StatefulWidget {
/// Builds a destination (icon + label) to use in a Material 3 [NavigationBar].
const _NavigationDestinationBuilder({
required this.buildIcon,
......@@ -459,31 +459,34 @@ class _NavigationDestinationBuilder extends StatelessWidget {
/// Defaults to null, in which case the [label] text will be used.
final String? tooltip;
@override
State<_NavigationDestinationBuilder> createState() => _NavigationDestinationBuilderState();
}
class _NavigationDestinationBuilderState extends State<_NavigationDestinationBuilder> {
final GlobalKey iconKey = GlobalKey();
@override
Widget build(BuildContext context) {
final _NavigationDestinationInfo info = _NavigationDestinationInfo.of(context);
final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context);
final NavigationBarThemeData defaults = _defaultsFor(context);
final GlobalKey labelKey = GlobalKey();
final bool selected = info.selectedIndex == info.index;
return _NavigationBarDestinationSemantics(
child: _NavigationBarDestinationTooltip(
message: tooltip ?? label,
message: widget.tooltip ?? widget.label,
child: _IndicatorInkWell(
key: UniqueKey(),
labelKey: labelKey,
iconKey: iconKey,
labelBehavior: info.labelBehavior,
selected: selected,
customBorder: navigationBarTheme.indicatorShape ?? defaults.indicatorShape,
onTap: info.onTap,
child: Row(
children: <Widget>[
Expanded(
child: _NavigationBarDestinationLayout(
icon: buildIcon(context),
labelKey: labelKey,
label: buildLabel(context),
icon: widget.buildIcon(context),
iconKey: iconKey,
label: widget.buildLabel(context),
),
),
],
......@@ -496,10 +499,8 @@ class _NavigationDestinationBuilder extends StatelessWidget {
class _IndicatorInkWell extends InkResponse {
const _IndicatorInkWell({
super.key,
required this.labelKey,
required this.iconKey,
required this.labelBehavior,
required this.selected,
super.customBorder,
super.onTap,
super.child,
......@@ -508,32 +509,15 @@ class _IndicatorInkWell extends InkResponse {
highlightColor: Colors.transparent,
);
final GlobalKey labelKey;
final GlobalKey iconKey;
final NavigationDestinationLabelBehavior labelBehavior;
final bool selected;
@override
RectCallback? getRectCallback(RenderBox referenceBox) {
final RenderBox labelBox = labelKey.currentContext!.findRenderObject()! as RenderBox;
final Rect labelRect = labelBox.localToGlobal(Offset.zero) & labelBox.size;
final double labelPadding;
switch (labelBehavior) {
case NavigationDestinationLabelBehavior.alwaysShow:
labelPadding = labelRect.height / 2;
case NavigationDestinationLabelBehavior.onlyShowSelected:
labelPadding = selected ? labelRect.height / 2 : 0;
case NavigationDestinationLabelBehavior.alwaysHide:
labelPadding = 0;
}
final double indicatorOffsetX = referenceBox.size.width / 2;
final double indicatorOffsetY = referenceBox.size.height / 2 - labelPadding;
return () {
return Rect.fromCenter(
center: Offset(indicatorOffsetX, indicatorOffsetY),
width: _kIndicatorWidth,
height: _kIndicatorHeight,
);
final RenderBox iconBox = iconKey.currentContext!.findRenderObject()! as RenderBox;
final Rect iconRect = iconBox.localToGlobal(Offset.zero) & iconBox.size;
return referenceBox.globalToLocal(iconRect.topLeft) & iconBox.size;
};
}
}
......@@ -774,7 +758,7 @@ class _NavigationBarDestinationLayout extends StatelessWidget {
/// 3 [NavigationBar].
const _NavigationBarDestinationLayout({
required this.icon,
required this.labelKey,
required this.iconKey,
required this.label,
});
......@@ -783,10 +767,10 @@ class _NavigationBarDestinationLayout extends StatelessWidget {
/// See [NavigationDestination.icon].
final Widget icon;
/// The global key for the label of this destination.
/// The global key for the icon of this destination.
///
/// This is used to determine the position of the label relative to the icon.
final GlobalKey labelKey;
/// This is used to determine the position of the icon.
final GlobalKey iconKey;
/// The label widget that sits below the icon.
///
......@@ -796,7 +780,7 @@ class _NavigationBarDestinationLayout extends StatelessWidget {
/// See [NavigationDestination.label].
final Widget label;
static final Key _iconKey = UniqueKey();
static final Key _labelKey = UniqueKey();
@override
Widget build(BuildContext context) {
......@@ -810,7 +794,7 @@ class _NavigationBarDestinationLayout extends StatelessWidget {
LayoutId(
id: _NavigationDestinationLayoutDelegate.iconId,
child: RepaintBoundary(
key: _iconKey,
key: iconKey,
child: icon,
),
),
......@@ -820,7 +804,7 @@ class _NavigationBarDestinationLayout extends StatelessWidget {
alwaysIncludeSemantics: true,
opacity: animation,
child: RepaintBoundary(
key: labelKey,
key: _labelKey,
child: label,
),
),
......
......@@ -7,6 +7,8 @@
@Tags(<String>['reduced-test-set'])
library;
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
......@@ -1217,6 +1219,52 @@ void main() {
await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_onlyShowSelected_unselected_m2.png'));
});
testWidgets('Destination icon does not rebuild when tapped', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/122811.
Widget buildNavigationBar() {
return MaterialApp(
home: Scaffold(
bottomNavigationBar: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
int selectedIndex = 0;
return NavigationBar(
selectedIndex: selectedIndex,
destinations: const <Widget>[
NavigationDestination(
icon: IconWithRandomColor(icon: Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: IconWithRandomColor(icon: Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {
setState(() {
selectedIndex = i;
});
},
);
}
),
),
);
}
await tester.pumpWidget(buildNavigationBar());
Icon icon = tester.widget<Icon>(find.byType(Icon).last);
final Color initialColor = icon.color!;
// Trigger a rebuild.
await tester.tap(find.text('Alarm'));
await tester.pumpAndSettle();
// Icon color should be the same as before the rebuild.
icon = tester.widget<Icon>(find.byType(Icon).last);
expect(icon.color, initialColor);
});
});
}
......@@ -1245,3 +1293,15 @@ ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) {
),
).decoration as ShapeDecoration?;
}
class IconWithRandomColor extends StatelessWidget {
const IconWithRandomColor({super.key, required this.icon});
final IconData icon;
@override
Widget build(BuildContext context) {
final Color randomColor = Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
return Icon(icon, color: randomColor);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment