Skip to content

Commit

Permalink
Fixed issue with Hero Animations and BoxScrollViews in Scaffolds (#10…
Browse files Browse the repository at this point in the history
…5654)
  • Loading branch information
youssef-attia committed Jun 13, 2022
1 parent 0be4a8e commit dc06326
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 7 deletions.
49 changes: 42 additions & 7 deletions packages/flutter/lib/src/widgets/heroes.dart
Expand Up @@ -3,10 +3,11 @@
// found in the LICENSE file.

import 'package:flutter/foundation.dart';

import 'basic.dart';
import 'binding.dart';
import 'framework.dart';
import 'implicit_animations.dart';
import 'media_query.dart';
import 'navigator.dart';
import 'overlay.dart';
import 'pages.dart';
Expand Down Expand Up @@ -135,9 +136,15 @@ enum HeroFlightDirection {
/// To make the animations look good, it's critical that the widget tree for the
/// hero in both locations be essentially identical. The widget of the *target*
/// is, by default, used to do the transition: when going from route A to route
/// B, route B's hero's widget is placed over route A's hero's widget. If a
/// [flightShuttleBuilder] is supplied, its output widget is shown during the
/// flight transition instead.
/// B, route B's hero's widget is placed over route A's hero's widget. Additionally,
/// if the [Hero] subtree changes appearance based on an [InheritedWidget] (such
/// as [MediaQuery] or [Theme]), then the hero animation may have discontinuity
/// at the start or the end of the animation because route A and route B provides
/// different such [InheritedWidget]s. Consider providing a custom [flightShuttleBuilder]
/// to ensure smooth transitions. The default [flightShuttleBuilder] interpolates
/// [MediaQuery]'s paddings. If your [Hero] widget uses custom [InheritedWidget]s
/// and displays a discontinuity in the animation, try to provide custom in-flight
/// transition using [flightShuttleBuilder].
///
/// By default, both route A and route B's heroes are hidden while the
/// transitioning widget is animating in-flight above the 2 routes.
Expand Down Expand Up @@ -910,8 +917,8 @@ class HeroController extends NavigatorObserver {
final NavigatorState? navigator = this.navigator;
final OverlayState? overlay = navigator?.overlay;
// If the navigator or the overlay was removed before this end-of-frame
// callback was called, then don't actually start a transition, and we don'
// t have to worry about any Hero widget we might have hidden in a previous
// callback was called, then don't actually start a transition, and we don't
// have to worry about any Hero widget we might have hidden in a previous
// flight, or ongoing flights.
if (navigator == null || overlay == null) {
return;
Expand Down Expand Up @@ -998,7 +1005,35 @@ class HeroController extends NavigatorObserver {
BuildContext toHeroContext,
) {
final Hero toHero = toHeroContext.widget as Hero;
return toHero.child;

final MediaQueryData? toMediaQueryData = MediaQuery.maybeOf(toHeroContext);
final MediaQueryData? fromMediaQueryData = MediaQuery.maybeOf(fromHeroContext);

if (toMediaQueryData == null || fromMediaQueryData == null) {
return toHero.child;
}

final EdgeInsets fromHeroPadding = fromMediaQueryData.padding;
final EdgeInsets toHeroPadding = toMediaQueryData.padding;

return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) {
return MediaQuery(
data: toMediaQueryData.copyWith(
padding: (flightDirection == HeroFlightDirection.push)
? EdgeInsetsTween(
begin: fromHeroPadding,
end: toHeroPadding,
).evaluate(animation)
: EdgeInsetsTween(
begin: toHeroPadding,
end: fromHeroPadding,
).evaluate(animation),
),
child: toHero.child);
},
);
}
}

Expand Down
86 changes: 86 additions & 0 deletions packages/flutter/test/widgets/heroes_test.dart
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import 'dart:ui' as ui;
import 'dart:ui' show WindowPadding;

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

class FakeWindowPadding implements WindowPadding {
const FakeWindowPadding({
this.left = 0.0,
this.top = 0.0,
this.right = 0.0,
this.bottom = 0.0,
});

@override
final double left;
@override
final double top;
@override
final double right;
@override
final double bottom;
}


Future<void> main() async {
final ui.Image testImage = await createTestImage();
assert(testImage != null);
Expand Down Expand Up @@ -3073,4 +3093,70 @@ Future<void> main() async {
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
testWidgets('smooth transition between different incoming data', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
const Key imageKey1 = Key('image1');
const Key imageKey2 = Key('image2');
final TestImageProvider imageProvider = TestImageProvider(testImage);
final TestWidgetsFlutterBinding testBinding = tester.binding;

testBinding.window.paddingTestValue = const FakeWindowPadding(top: 50);

await tester.pumpWidget(
MaterialApp(
navigatorKey: navigatorKey,
home: Scaffold(
appBar: AppBar(title: const Text('test')),
body: Hero(
tag: 'imageHero',
child: GridView.count(
crossAxisCount: 3,
shrinkWrap: true,
children: <Widget>[
Image(image: imageProvider, key: imageKey1),
],
),
),
),
),
);

final MaterialPageRoute<void> route2 = MaterialPageRoute<void>(
builder: (BuildContext context) {
return Scaffold(
body: Hero(
tag: 'imageHero',
child: GridView.count(
crossAxisCount: 3,
shrinkWrap: true,
children: <Widget>[
Image(image: imageProvider, key: imageKey2),
],
),
),
);
},
);

// Load images.
imageProvider.complete();
await tester.pump();

final double forwardRest = tester.getTopLeft(find.byType(Image)).dy;
navigatorKey.currentState!.push(route2);
await tester.pump();
await tester.pump(const Duration(milliseconds: 1));
expect(tester.getTopLeft(find.byType(Image)).dy, moreOrLessEquals(forwardRest, epsilon: 0.1));
await tester.pumpAndSettle();

navigatorKey.currentState!.pop(route2);
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
expect(tester.getTopLeft(find.byType(Image)).dy, moreOrLessEquals(forwardRest, epsilon: 0.1));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.byType(Image)).dy, moreOrLessEquals(forwardRest, epsilon: 0.1));

testBinding.window.clearAllTestValues();
},
);
}

0 comments on commit dc06326

Please sign in to comment.