Skip to content

Commit c87b9f4

Browse files
authored
[animations] fix crash when opening container at edge (flutter#83)
1 parent 8547246 commit c87b9f4

File tree

3 files changed

+211
-75
lines changed

3 files changed

+211
-75
lines changed

packages/animations/lib/src/open_container.dart

Lines changed: 62 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -412,12 +412,9 @@ class _OpenContainerRoute extends ModalRoute<void> {
412412
// work.
413413
final GlobalKey _openBuilderKey = GlobalKey();
414414

415-
// Defines the position of the (opening) [OpenContainer] within the bounds of
416-
// the enclosing [Navigator].
417-
final RectTween _insetsTween = RectTween(end: Rect.zero);
418-
419-
// Defines the size of the [OpenContainer].
420-
final SizeTween _sizeTween = SizeTween();
415+
// Defines the position and the size of the (opening) [OpenContainer] within
416+
// the bounds of the enclosing [Navigator].
417+
final RectTween _rectTween = RectTween();
421418

422419
AnimationStatus _lastAnimationStatus;
423420
AnimationStatus _currentAnimationStatus;
@@ -465,21 +462,14 @@ class _OpenContainerRoute extends ModalRoute<void> {
465462
final RenderBox navigator =
466463
Navigator.of(navigatorContext).context.findRenderObject();
467464
final Size navSize = _getSize(navigator);
468-
_sizeTween.end = navSize;
465+
_rectTween.end = Offset.zero & navSize;
469466

470467
void takeMeasurementsInSourceRoute([Duration _]) {
471468
if (!navigator.attached || hideableKey.currentContext == null) {
472469
return;
473470
}
474-
final Rect srcRect = _getRect(hideableKey, navigator);
475-
_sizeTween.begin = srcRect.size;
476-
_insetsTween.begin = Rect.fromLTRB(
477-
srcRect.left,
478-
srcRect.top,
479-
navSize.width - srcRect.right,
480-
navSize.height - srcRect.bottom,
481-
);
482-
hideableKey.currentState.placeholderSize = _sizeTween.begin;
471+
_rectTween.begin = _getRect(hideableKey, navigator);
472+
hideableKey.currentState.placeholderSize = _rectTween.begin.size;
483473
}
484474

485475
if (delayForSourceRoute) {
@@ -600,73 +590,70 @@ class _OpenContainerRoute extends ModalRoute<void> {
600590
assert(false); // Unreachable.
601591
break;
602592
}
603-
final Rect rect = _insetsTween.evaluate(curvedAnimation);
604-
final Size size = _sizeTween.evaluate(curvedAnimation);
593+
final Rect rect = _rectTween.evaluate(curvedAnimation);
605594
return SizedBox.expand(
606595
child: Container(
607596
color: scrimTween.evaluate(curvedAnimation),
608-
child: Padding(
609-
padding: EdgeInsets.fromLTRB(
610-
rect.left,
611-
rect.top,
612-
rect.right,
613-
rect.bottom,
614-
),
615-
child: SizedBox(
616-
width: size.width,
617-
height: size.height,
618-
child: Material(
619-
clipBehavior: Clip.antiAlias,
620-
animationDuration: Duration.zero,
621-
color: colorTween.evaluate(animation),
622-
shape: _shapeTween.evaluate(curvedAnimation),
623-
elevation: _elevationTween.evaluate(curvedAnimation),
624-
child: Stack(
625-
fit: StackFit.passthrough,
626-
children: <Widget>[
627-
// Closed child fading out.
628-
FittedBox(
629-
fit: BoxFit.fitWidth,
630-
alignment: Alignment.topLeft,
631-
child: SizedBox(
632-
width: _sizeTween.begin.width,
633-
height: _sizeTween.begin.height,
634-
child: hideableKey.currentState.isInTree
635-
? null
636-
: Opacity(
637-
opacity:
638-
closedOpacityTween.evaluate(animation),
639-
child: Builder(
640-
key: closedBuilderKey,
641-
builder: (BuildContext context) {
642-
// Use dummy "open container" callback
643-
// since we are in the process of opening.
644-
return closedBuilder(context, () {});
645-
},
597+
child: Align(
598+
alignment: Alignment.topLeft,
599+
child: Transform.translate(
600+
offset: Offset(rect.left, rect.top),
601+
child: SizedBox(
602+
width: rect.width,
603+
height: rect.height,
604+
child: Material(
605+
clipBehavior: Clip.antiAlias,
606+
animationDuration: Duration.zero,
607+
color: colorTween.evaluate(animation),
608+
shape: _shapeTween.evaluate(curvedAnimation),
609+
elevation: _elevationTween.evaluate(curvedAnimation),
610+
child: Stack(
611+
fit: StackFit.passthrough,
612+
children: <Widget>[
613+
// Closed child fading out.
614+
FittedBox(
615+
fit: BoxFit.fitWidth,
616+
alignment: Alignment.topLeft,
617+
child: SizedBox(
618+
width: _rectTween.begin.width,
619+
height: _rectTween.begin.height,
620+
child: hideableKey.currentState.isInTree
621+
? null
622+
: Opacity(
623+
opacity: closedOpacityTween
624+
.evaluate(animation),
625+
child: Builder(
626+
key: closedBuilderKey,
627+
builder: (BuildContext context) {
628+
// Use dummy "open container" callback
629+
// since we are in the process of opening.
630+
return closedBuilder(context, () {});
631+
},
632+
),
646633
),
647-
),
634+
),
648635
),
649-
),
650-
651-
// Open child fading in.
652-
FittedBox(
653-
fit: BoxFit.fitWidth,
654-
alignment: Alignment.topLeft,
655-
child: SizedBox(
656-
width: _sizeTween.end.width,
657-
height: _sizeTween.end.height,
658-
child: Opacity(
659-
opacity: openOpacityTween.evaluate(animation),
660-
child: Builder(
661-
key: _openBuilderKey,
662-
builder: (BuildContext context) {
663-
return openBuilder(context, closeContainer);
664-
},
636+
637+
// Open child fading in.
638+
FittedBox(
639+
fit: BoxFit.fitWidth,
640+
alignment: Alignment.topLeft,
641+
child: SizedBox(
642+
width: _rectTween.end.width,
643+
height: _rectTween.end.height,
644+
child: Opacity(
645+
opacity: openOpacityTween.evaluate(animation),
646+
child: Builder(
647+
key: _openBuilderKey,
648+
builder: (BuildContext context) {
649+
return openBuilder(context, closeContainer);
650+
},
651+
),
665652
),
666653
),
667654
),
668-
),
669-
],
655+
],
656+
),
670657
),
671658
),
672659
),

packages/animations/lib/src/utils/curves.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
// Copyright 2019 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
15
import 'package:flutter/animation.dart';
26
import 'package:flutter/material.dart';
37
import 'package:flutter/widgets.dart';

packages/animations/test/open_container_test.dart

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,151 @@ void main() {
940940
await tester.pump(const Duration(milliseconds: 100));
941941
expect(_getScrimColor(tester), Colors.transparent);
942942
});
943+
944+
testWidgets(
945+
'Container partly offscreen can be opened without crash - vertical',
946+
(WidgetTester tester) async {
947+
final ScrollController controller =
948+
ScrollController(initialScrollOffset: 50);
949+
await tester.pumpWidget(Center(
950+
child: SizedBox(
951+
height: 200,
952+
width: 200,
953+
child: _boilerplate(
954+
child: ListView.builder(
955+
cacheExtent: 0,
956+
controller: controller,
957+
itemBuilder: (BuildContext context, int index) {
958+
return OpenContainer(
959+
closedBuilder: (BuildContext context, VoidCallback _) {
960+
return SizedBox(
961+
height: 100,
962+
width: 100,
963+
child: Text('Closed $index'),
964+
);
965+
},
966+
openBuilder: (BuildContext context, VoidCallback _) {
967+
return Text('Open $index');
968+
},
969+
);
970+
},
971+
),
972+
),
973+
),
974+
));
975+
976+
void expectClosedState() {
977+
expect(find.text('Closed 0'), findsOneWidget);
978+
expect(find.text('Closed 1'), findsOneWidget);
979+
expect(find.text('Closed 2'), findsOneWidget);
980+
expect(find.text('Closed 3'), findsNothing);
981+
982+
expect(find.text('Open 0'), findsNothing);
983+
expect(find.text('Open 1'), findsNothing);
984+
expect(find.text('Open 2'), findsNothing);
985+
expect(find.text('Open 3'), findsNothing);
986+
}
987+
988+
expectClosedState();
989+
990+
// Open container that's partly visible at top.
991+
await tester.tapAt(
992+
tester.getBottomRight(find.text('Closed 0')) - const Offset(20, 20),
993+
);
994+
await tester.pump();
995+
await tester.pumpAndSettle();
996+
expect(find.text('Closed 0'), findsNothing);
997+
expect(find.text('Open 0'), findsOneWidget);
998+
999+
final NavigatorState navigator = tester.state(find.byType(Navigator));
1000+
navigator.pop();
1001+
await tester.pump();
1002+
await tester.pumpAndSettle();
1003+
expectClosedState();
1004+
1005+
// Open container that's partly visible at bottom.
1006+
await tester.tapAt(
1007+
tester.getTopLeft(find.text('Closed 2')) + const Offset(20, 20),
1008+
);
1009+
await tester.pump();
1010+
await tester.pumpAndSettle();
1011+
1012+
expect(find.text('Closed 2'), findsNothing);
1013+
expect(find.text('Open 2'), findsOneWidget);
1014+
});
1015+
1016+
testWidgets(
1017+
'Container partly offscreen can be opened without crash - horizontal',
1018+
(WidgetTester tester) async {
1019+
final ScrollController controller =
1020+
ScrollController(initialScrollOffset: 50);
1021+
await tester.pumpWidget(Center(
1022+
child: SizedBox(
1023+
height: 200,
1024+
width: 200,
1025+
child: _boilerplate(
1026+
child: ListView.builder(
1027+
scrollDirection: Axis.horizontal,
1028+
cacheExtent: 0,
1029+
controller: controller,
1030+
itemBuilder: (BuildContext context, int index) {
1031+
return OpenContainer(
1032+
closedBuilder: (BuildContext context, VoidCallback _) {
1033+
return SizedBox(
1034+
height: 100,
1035+
width: 100,
1036+
child: Text('Closed $index'),
1037+
);
1038+
},
1039+
openBuilder: (BuildContext context, VoidCallback _) {
1040+
return Text('Open $index');
1041+
},
1042+
);
1043+
},
1044+
),
1045+
),
1046+
),
1047+
));
1048+
1049+
void expectClosedState() {
1050+
expect(find.text('Closed 0'), findsOneWidget);
1051+
expect(find.text('Closed 1'), findsOneWidget);
1052+
expect(find.text('Closed 2'), findsOneWidget);
1053+
expect(find.text('Closed 3'), findsNothing);
1054+
1055+
expect(find.text('Open 0'), findsNothing);
1056+
expect(find.text('Open 1'), findsNothing);
1057+
expect(find.text('Open 2'), findsNothing);
1058+
expect(find.text('Open 3'), findsNothing);
1059+
}
1060+
1061+
expectClosedState();
1062+
1063+
// Open container that's partly visible at left edge.
1064+
await tester.tapAt(
1065+
tester.getBottomRight(find.text('Closed 0')) - const Offset(20, 20),
1066+
);
1067+
await tester.pump();
1068+
await tester.pumpAndSettle();
1069+
expect(find.text('Closed 0'), findsNothing);
1070+
expect(find.text('Open 0'), findsOneWidget);
1071+
1072+
final NavigatorState navigator = tester.state(find.byType(Navigator));
1073+
navigator.pop();
1074+
await tester.pump();
1075+
await tester.pumpAndSettle();
1076+
expectClosedState();
1077+
1078+
// Open container that's partly visible at right edge.
1079+
await tester.tapAt(
1080+
tester.getTopLeft(find.text('Closed 2')) + const Offset(20, 20),
1081+
);
1082+
await tester.pump();
1083+
await tester.pumpAndSettle();
1084+
1085+
expect(find.text('Closed 2'), findsNothing);
1086+
expect(find.text('Open 2'), findsOneWidget);
1087+
});
9431088
}
9441089

9451090
Color _getScrimColor(WidgetTester tester) {

0 commit comments

Comments
 (0)