fixes CupertinoFullscreenDialogTransition leaks (#147168)

This commit is contained in:
Dimil Kalathiya 2024-04-27 01:49:31 +05:30 committed by GitHub
parent de411a5976
commit cc9ac7d13c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 123 additions and 23 deletions

View file

@ -506,44 +506,96 @@ class _CupertinoPageTransitionState extends State<CupertinoPageTransition> {
///
/// For example, used when creating a new calendar event by bringing in the next
/// screen from the bottom.
class CupertinoFullscreenDialogTransition extends StatelessWidget {
class CupertinoFullscreenDialogTransition extends StatefulWidget {
/// Creates an iOS-style transition used for summoning fullscreen dialogs.
///
const CupertinoFullscreenDialogTransition({
super.key,
required this.primaryRouteAnimation,
required this.secondaryRouteAnimation,
required this.child,
required this.linearTransition,
});
/// * `primaryRouteAnimation` is a linear route animation from 0.0 to 1.0
/// when this screen is being pushed.
final Animation<double> primaryRouteAnimation;
/// * `secondaryRouteAnimation` is a linear route animation from 0.0 to 1.0
/// when another screen is being pushed on top of this one.
/// * `linearTransition` is whether to perform the secondary transition linearly.
final Animation<double> secondaryRouteAnimation;
/// * `linearTransition` is whether to perform the transitions linearly.
/// Used to precisely track back gesture drags.
CupertinoFullscreenDialogTransition({
super.key,
required Animation<double> primaryRouteAnimation,
required Animation<double> secondaryRouteAnimation,
required this.child,
required bool linearTransition,
}) : _positionAnimation = CurvedAnimation(
parent: primaryRouteAnimation,
final bool linearTransition;
/// The widget below this widget in the tree.
final Widget child;
@override
State<CupertinoFullscreenDialogTransition> createState() => _CupertinoFullscreenDialogTransitionState();
}
class _CupertinoFullscreenDialogTransitionState extends State<CupertinoFullscreenDialogTransition> {
/// When this page is coming in to cover another page.
late Animation<Offset> _primaryPositionAnimation;
/// When this page is becoming covered by another page.
late Animation<Offset> _secondaryPositionAnimation;
/// Curve of primary page which is coming in to cover another page.
CurvedAnimation? _primaryPositionCurve;
/// Curve of secondary page which is becoming covered by another page.
CurvedAnimation? _secondaryPositionCurve;
@override
void initState() {
super.initState();
_setupAnimation();
}
@override
void didUpdateWidget(covariant CupertinoFullscreenDialogTransition oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.primaryRouteAnimation != widget.primaryRouteAnimation ||
oldWidget.secondaryRouteAnimation != widget.secondaryRouteAnimation ||
oldWidget.child != widget.child ||
oldWidget.linearTransition != widget.linearTransition) {
_disposeCurve();
_setupAnimation();
}
}
@override
void dispose() {
_disposeCurve();
super.dispose();
}
void _disposeCurve() {
_primaryPositionCurve?.dispose();
_secondaryPositionCurve?.dispose();
_primaryPositionCurve = null;
_secondaryPositionCurve = null;
}
void _setupAnimation() {
_primaryPositionAnimation = (_primaryPositionCurve = CurvedAnimation(
parent: widget.primaryRouteAnimation,
curve: Curves.linearToEaseOut,
// The curve must be flipped so that the reverse animation doesn't play
// an ease-in curve, which iOS does not use.
reverseCurve: Curves.linearToEaseOut.flipped,
).drive(_kBottomUpTween),
)).drive(_kBottomUpTween);
_secondaryPositionAnimation =
(linearTransition
? secondaryRouteAnimation
: CurvedAnimation(
parent: secondaryRouteAnimation,
(widget.linearTransition
? widget.secondaryRouteAnimation
: _secondaryPositionCurve = CurvedAnimation(
parent: widget.secondaryRouteAnimation,
curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeInToLinear,
)
).drive(_kMiddleLeftTween);
}
final Animation<Offset> _positionAnimation;
// When this page is becoming covered by another page.
final Animation<Offset> _secondaryPositionAnimation;
/// The widget below this widget in the tree.
final Widget child;
@override
Widget build(BuildContext context) {
@ -554,8 +606,8 @@ class CupertinoFullscreenDialogTransition extends StatelessWidget {
textDirection: textDirection,
transformHitTests: false,
child: SlideTransition(
position: _positionAnimation,
child: child,
position: _primaryPositionAnimation,
child: widget.child,
),
);
}

View file

@ -12,6 +12,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
import '../widgets/semantics_tester.dart';
@ -2431,6 +2432,53 @@ void main() {
expect(tester.getBottomRight(find.byType(Placeholder)).dx, 390.0);
});
});
testWidgets(
// TODO(polina-c): remove when fixed https://github.com/flutter/flutter/issues/145600 [leak-tracking-opt-in]
experimentalLeakTesting: LeakTesting.settings.withTracked(classes: <String>['CurvedAnimation']),
'Fullscreen route does not leak CurveAnimation', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (BuildContext context) {
return CupertinoButton(
child: const Text('Button'),
onPressed: () {
Navigator.push<void>(context, CupertinoPageRoute<void>(
fullscreenDialog: true,
builder: (BuildContext context) {
return Column(
children: <Widget>[
const Placeholder(),
CupertinoButton(
child: const Text('Close'),
onPressed: () {
Navigator.pop<void>(context);
},
),
],
);
},
));
},
);
},
),
),
);
// Enter animation.
await tester.tap(find.text('Button'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
// Exit animation
await tester.tap(find.text('Close'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
});
}
class MockNavigatorObserver extends NavigatorObserver {