Skip to content

Commit dc06326

Browse files
Fixed issue with Hero Animations and BoxScrollViews in Scaffolds (#105654)
1 parent 0be4a8e commit dc06326

File tree

2 files changed

+128
-7
lines changed

2 files changed

+128
-7
lines changed

packages/flutter/lib/src/widgets/heroes.dart

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
// found in the LICENSE file.
44

55
import 'package:flutter/foundation.dart';
6-
76
import 'basic.dart';
87
import 'binding.dart';
98
import 'framework.dart';
9+
import 'implicit_animations.dart';
10+
import 'media_query.dart';
1011
import 'navigator.dart';
1112
import 'overlay.dart';
1213
import 'pages.dart';
@@ -135,9 +136,15 @@ enum HeroFlightDirection {
135136
/// To make the animations look good, it's critical that the widget tree for the
136137
/// hero in both locations be essentially identical. The widget of the *target*
137138
/// is, by default, used to do the transition: when going from route A to route
138-
/// B, route B's hero's widget is placed over route A's hero's widget. If a
139-
/// [flightShuttleBuilder] is supplied, its output widget is shown during the
140-
/// flight transition instead.
139+
/// B, route B's hero's widget is placed over route A's hero's widget. Additionally,
140+
/// if the [Hero] subtree changes appearance based on an [InheritedWidget] (such
141+
/// as [MediaQuery] or [Theme]), then the hero animation may have discontinuity
142+
/// at the start or the end of the animation because route A and route B provides
143+
/// different such [InheritedWidget]s. Consider providing a custom [flightShuttleBuilder]
144+
/// to ensure smooth transitions. The default [flightShuttleBuilder] interpolates
145+
/// [MediaQuery]'s paddings. If your [Hero] widget uses custom [InheritedWidget]s
146+
/// and displays a discontinuity in the animation, try to provide custom in-flight
147+
/// transition using [flightShuttleBuilder].
141148
///
142149
/// By default, both route A and route B's heroes are hidden while the
143150
/// transitioning widget is animating in-flight above the 2 routes.
@@ -910,8 +917,8 @@ class HeroController extends NavigatorObserver {
910917
final NavigatorState? navigator = this.navigator;
911918
final OverlayState? overlay = navigator?.overlay;
912919
// If the navigator or the overlay was removed before this end-of-frame
913-
// callback was called, then don't actually start a transition, and we don'
914-
// t have to worry about any Hero widget we might have hidden in a previous
920+
// callback was called, then don't actually start a transition, and we don't
921+
// have to worry about any Hero widget we might have hidden in a previous
915922
// flight, or ongoing flights.
916923
if (navigator == null || overlay == null) {
917924
return;
@@ -998,7 +1005,35 @@ class HeroController extends NavigatorObserver {
9981005
BuildContext toHeroContext,
9991006
) {
10001007
final Hero toHero = toHeroContext.widget as Hero;
1001-
return toHero.child;
1008+
1009+
final MediaQueryData? toMediaQueryData = MediaQuery.maybeOf(toHeroContext);
1010+
final MediaQueryData? fromMediaQueryData = MediaQuery.maybeOf(fromHeroContext);
1011+
1012+
if (toMediaQueryData == null || fromMediaQueryData == null) {
1013+
return toHero.child;
1014+
}
1015+
1016+
final EdgeInsets fromHeroPadding = fromMediaQueryData.padding;
1017+
final EdgeInsets toHeroPadding = toMediaQueryData.padding;
1018+
1019+
return AnimatedBuilder(
1020+
animation: animation,
1021+
builder: (BuildContext context, Widget? child) {
1022+
return MediaQuery(
1023+
data: toMediaQueryData.copyWith(
1024+
padding: (flightDirection == HeroFlightDirection.push)
1025+
? EdgeInsetsTween(
1026+
begin: fromHeroPadding,
1027+
end: toHeroPadding,
1028+
).evaluate(animation)
1029+
: EdgeInsetsTween(
1030+
begin: toHeroPadding,
1031+
end: fromHeroPadding,
1032+
).evaluate(animation),
1033+
),
1034+
child: toHero.child);
1035+
},
1036+
);
10021037
}
10031038
}
10041039

packages/flutter/test/widgets/heroes_test.dart

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'dart:ui' as ui;
6+
import 'dart:ui' show WindowPadding;
67

78
import 'package:flutter/cupertino.dart';
89
import 'package:flutter/foundation.dart';
@@ -183,6 +184,25 @@ class MyStatefulWidgetState extends State<MyStatefulWidget> {
183184
Widget build(BuildContext context) => Text(widget.value);
184185
}
185186

187+
class FakeWindowPadding implements WindowPadding {
188+
const FakeWindowPadding({
189+
this.left = 0.0,
190+
this.top = 0.0,
191+
this.right = 0.0,
192+
this.bottom = 0.0,
193+
});
194+
195+
@override
196+
final double left;
197+
@override
198+
final double top;
199+
@override
200+
final double right;
201+
@override
202+
final double bottom;
203+
}
204+
205+
186206
Future<void> main() async {
187207
final ui.Image testImage = await createTestImage();
188208
assert(testImage != null);
@@ -3073,4 +3093,70 @@ Future<void> main() async {
30733093
await tester.pumpAndSettle();
30743094
expect(tester.takeException(), isNull);
30753095
});
3096+
testWidgets('smooth transition between different incoming data', (WidgetTester tester) async {
3097+
final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
3098+
const Key imageKey1 = Key('image1');
3099+
const Key imageKey2 = Key('image2');
3100+
final TestImageProvider imageProvider = TestImageProvider(testImage);
3101+
final TestWidgetsFlutterBinding testBinding = tester.binding;
3102+
3103+
testBinding.window.paddingTestValue = const FakeWindowPadding(top: 50);
3104+
3105+
await tester.pumpWidget(
3106+
MaterialApp(
3107+
navigatorKey: navigatorKey,
3108+
home: Scaffold(
3109+
appBar: AppBar(title: const Text('test')),
3110+
body: Hero(
3111+
tag: 'imageHero',
3112+
child: GridView.count(
3113+
crossAxisCount: 3,
3114+
shrinkWrap: true,
3115+
children: <Widget>[
3116+
Image(image: imageProvider, key: imageKey1),
3117+
],
3118+
),
3119+
),
3120+
),
3121+
),
3122+
);
3123+
3124+
final MaterialPageRoute<void> route2 = MaterialPageRoute<void>(
3125+
builder: (BuildContext context) {
3126+
return Scaffold(
3127+
body: Hero(
3128+
tag: 'imageHero',
3129+
child: GridView.count(
3130+
crossAxisCount: 3,
3131+
shrinkWrap: true,
3132+
children: <Widget>[
3133+
Image(image: imageProvider, key: imageKey2),
3134+
],
3135+
),
3136+
),
3137+
);
3138+
},
3139+
);
3140+
3141+
// Load images.
3142+
imageProvider.complete();
3143+
await tester.pump();
3144+
3145+
final double forwardRest = tester.getTopLeft(find.byType(Image)).dy;
3146+
navigatorKey.currentState!.push(route2);
3147+
await tester.pump();
3148+
await tester.pump(const Duration(milliseconds: 1));
3149+
expect(tester.getTopLeft(find.byType(Image)).dy, moreOrLessEquals(forwardRest, epsilon: 0.1));
3150+
await tester.pumpAndSettle();
3151+
3152+
navigatorKey.currentState!.pop(route2);
3153+
await tester.pump();
3154+
await tester.pump(const Duration(milliseconds: 300));
3155+
expect(tester.getTopLeft(find.byType(Image)).dy, moreOrLessEquals(forwardRest, epsilon: 0.1));
3156+
await tester.pumpAndSettle();
3157+
expect(tester.getTopLeft(find.byType(Image)).dy, moreOrLessEquals(forwardRest, epsilon: 0.1));
3158+
3159+
testBinding.window.clearAllTestValues();
3160+
},
3161+
);
30763162
}

0 commit comments

Comments
 (0)