Unverified Commit dc06326c authored by Youssef Attia's avatar Youssef Attia Committed by GitHub

Fixed issue with Hero Animations and BoxScrollViews in Scaffolds (#105654)

parent 0be4a8e9
...@@ -3,10 +3,11 @@ ...@@ -3,10 +3,11 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'basic.dart'; import 'basic.dart';
import 'binding.dart'; import 'binding.dart';
import 'framework.dart'; import 'framework.dart';
import 'implicit_animations.dart';
import 'media_query.dart';
import 'navigator.dart'; import 'navigator.dart';
import 'overlay.dart'; import 'overlay.dart';
import 'pages.dart'; import 'pages.dart';
...@@ -135,9 +136,15 @@ enum HeroFlightDirection { ...@@ -135,9 +136,15 @@ enum HeroFlightDirection {
/// To make the animations look good, it's critical that the widget tree for the /// 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* /// 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 /// 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 /// B, route B's hero's widget is placed over route A's hero's widget. Additionally,
/// [flightShuttleBuilder] is supplied, its output widget is shown during the /// if the [Hero] subtree changes appearance based on an [InheritedWidget] (such
/// flight transition instead. /// 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 /// By default, both route A and route B's heroes are hidden while the
/// transitioning widget is animating in-flight above the 2 routes. /// transitioning widget is animating in-flight above the 2 routes.
...@@ -910,8 +917,8 @@ class HeroController extends NavigatorObserver { ...@@ -910,8 +917,8 @@ class HeroController extends NavigatorObserver {
final NavigatorState? navigator = this.navigator; final NavigatorState? navigator = this.navigator;
final OverlayState? overlay = navigator?.overlay; final OverlayState? overlay = navigator?.overlay;
// If the navigator or the overlay was removed before this end-of-frame // 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' // callback was called, then don't actually start a transition, and we don't
// t have to worry about any Hero widget we might have hidden in a previous // have to worry about any Hero widget we might have hidden in a previous
// flight, or ongoing flights. // flight, or ongoing flights.
if (navigator == null || overlay == null) { if (navigator == null || overlay == null) {
return; return;
...@@ -998,8 +1005,36 @@ class HeroController extends NavigatorObserver { ...@@ -998,8 +1005,36 @@ class HeroController extends NavigatorObserver {
BuildContext toHeroContext, BuildContext toHeroContext,
) { ) {
final Hero toHero = toHeroContext.widget as Hero; final Hero toHero = toHeroContext.widget as Hero;
final MediaQueryData? toMediaQueryData = MediaQuery.maybeOf(toHeroContext);
final MediaQueryData? fromMediaQueryData = MediaQuery.maybeOf(fromHeroContext);
if (toMediaQueryData == null || fromMediaQueryData == null) {
return toHero.child; 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);
},
);
}
} }
/// Enables or disables [Hero]es in the widget subtree. /// Enables or disables [Hero]es in the widget subtree.
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'dart:ui' show WindowPadding;
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -183,6 +184,25 @@ class MyStatefulWidgetState extends State<MyStatefulWidget> { ...@@ -183,6 +184,25 @@ class MyStatefulWidgetState extends State<MyStatefulWidget> {
Widget build(BuildContext context) => Text(widget.value); 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 { Future<void> main() async {
final ui.Image testImage = await createTestImage(); final ui.Image testImage = await createTestImage();
assert(testImage != null); assert(testImage != null);
...@@ -3073,4 +3093,70 @@ Future<void> main() async { ...@@ -3073,4 +3093,70 @@ Future<void> main() async {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.takeException(), isNull); 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();
},
);
} }
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