Adds animations package with open container transition (#30)

This commit is contained in:
Michael Goderbauer
2019-11-26 09:54:09 -08:00
committed by GitHub
parent cb58456da4
commit 0b809df740
9 changed files with 1773 additions and 0 deletions

73
packages/animations/.gitignore vendored Normal file
View File

@ -0,0 +1,73 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.packages
.pub-cache/
.pub/
build/
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages

View File

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: ce3c467292293176452a3b4b4fee70073f5af9b3
channel: master
project_type: package

View File

@ -0,0 +1,3 @@
## [0.0.1] - August 28, 2019
* Open Container to full screen transition

View File

@ -0,0 +1,27 @@
Copyright 2019 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,15 @@
# Fancy pre-built Animations for Flutter
**This package is still under heavy development and may change frequently.**
This package contains pre-canned animations for commonly-desired effects. The animations can be customized with your content and dropped into your application to delight your users.
## Available Animations
Currently, the following animated effects are available in this library:
### Material's [Open Container Transitions](https://material.io/design/motion/choreography.html#transformation)
Tapping on a container (e.g. a card or a button) will expand the container to reveal more information.
*TODO(goderbauer): Add example videos of this effect.*

View File

@ -0,0 +1,5 @@
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export 'src/open_container.dart';

View File

@ -0,0 +1,691 @@
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
/// Signature for a function that creates a [Widget] to be used within an
/// [OpenContainer].
///
/// The `action` callback provided to [OpenContainer.openBuilder] can be used
/// to open the container. The `action` callback provided to
/// [OpenContainer.closedBuilder] can be used to close the container again.
typedef OpenContainerBuilder = Widget Function(
BuildContext context,
VoidCallback action,
);
/// A container that grows to fill the screen to reveal new content when tapped.
///
/// While the container is closed, it shows the [Widget] returned by
/// [closedBuilder]. When the container is tapped it grows to fill the entire
/// size of the surrounding [Navigator] while fading out the widget returned by
/// [closedBuilder] and fading in the widget returned by [openBuilder]. When the
/// container is closed again via the callback provided to [openBuilder] or via
/// Android's back button, the animation is reversed: The container shrinks back
/// to its original size while the widget returned by [openBuilder] is faded out
/// and the widget returned by [openBuilder] is faded back in.
///
/// By default, the container is in the closed state. During the transition from
/// closed to open and vice versa the widgets returned by the [openBuilder] and
/// [closedBuilder] exist in the tree at the same time. Therefore, the widgets
/// returned by these builders cannot include the same global key.
///
// TODO(goderbauer): Add example animations and sample code.
///
/// See also:
///
/// * [Transitions with animated containers](https://material.io/design/motion/choreography.html#transformation)
/// in the Material spec.
class OpenContainer extends StatefulWidget {
/// Creates an [OpenContainer].
///
/// All arguments except for [key] must not be null. The arguments
/// [closedBuilder] and [closedBuilder] are required.
const OpenContainer({
Key key,
this.closedColor = Colors.white,
this.openColor = Colors.white,
this.closedElevation = 1.0,
this.openElevation = 4.0,
this.closedShape = const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
this.openShape = const RoundedRectangleBorder(),
@required this.closedBuilder,
@required this.openBuilder,
this.tappable = true,
this.transitionDuration = const Duration(milliseconds: 300),
}) : assert(closedColor != null),
assert(openColor != null),
assert(closedElevation != null),
assert(openElevation != null),
assert(closedShape != null),
assert(openShape != null),
assert(closedBuilder != null),
assert(openBuilder != null),
assert(tappable != null),
super(key: key);
/// Background color of the container while it is closed.
///
/// When the container is opened, it will first transition from this color
/// to [Colors.white] and then transition from there to [openColor] in one
/// smooth animation. When the container is closed, it will transition back to
/// this color from [openColor] via [Colors.white].
///
/// Defaults to [Colors.white].
///
/// See also:
///
/// * [Material.color], which is used to implement this property.
final Color closedColor;
/// Background color of the container while it is open.
///
/// When the container is closed, it will first transition from [closedColor]
/// to [Colors.white] and then transition from there to this color in one
/// smooth animation. When the container is closed, it will transition back to
/// [closedColor] from this color via [Colors.white].
///
/// Defaults to [Colors.white].
///
/// See also:
///
/// * [Material.color], which is used to implement this property.
final Color openColor;
/// Elevation of the container while it is closed.
///
/// When the container is opened, it will transition from this elevation to
/// [openElevation]. When the container is closed, it will transition back
/// from [openElevation] to this elevation.
///
/// Defaults to 1.0.
///
/// See also:
///
/// * [Material.elevation], which is used to implement this property.
final double closedElevation;
/// Elevation of the container while it is open.
///
/// When the container is opened, it will transition to this elevation from
/// [closedElevation]. When the container is closed, it will transition back
/// from this elevation to [closedElevation].
///
/// Defaults to 4.0.
///
/// See also:
///
/// * [Material.elevation], which is used to implement this property.
final double openElevation;
/// Shape of the container while it is closed.
///
/// When the container is opened it will transition from this shape to
/// [openShape]. When the container is closed, it will transition back to this
/// shape.
///
/// Defaults to a [RoundedRectangleBorder] with a [Radius.circular] of 4.0.
///
/// See also:
///
/// * [Material.shape], which is used to implement this property.
final ShapeBorder closedShape;
/// Shape of the container while it is open.
///
/// When the container is opened it will transition from [closedShape] to
/// this shape. When the container is closed, it will transition from this
/// shape back to [closedShape].
///
/// Defaults to a rectangular.
///
/// See also:
///
/// * [Material.shape], which is used to implement this property.
final ShapeBorder openShape;
/// Called to obtain the child for the container in the closed state.
///
/// The [Widget] returned by this builder is faded out when the container
/// opens and at the same time the widget returned by [openBuilder] is faded
/// in while the container grows to fill the surrounding [Navigator].
///
/// The `action` callback provided to the builder can be called to open the
/// container.
final OpenContainerBuilder closedBuilder;
/// Called to obtain the child for the container in the open state.
///
/// The [Widget] returned by this builder is faded in when the container
/// opens and at the same time the widget returned by [closedBuilder] is
/// faded out while the container grows to fill the surrounding [Navigator].
///
/// The `action` callback provided to the builder can be called to close the
/// container.
final OpenContainerBuilder openBuilder;
/// Whether the entire closed container can be tapped to open it.
///
/// Defaults to true.
///
/// When this is set to false the container can only be opened by calling the
/// `action` callback that is provided to the [closedBuilder].
final bool tappable;
/// The time it will take to animate the container from its closed to its
/// open state and vice versa.
///
/// Defaults to 300ms.
final Duration transitionDuration;
@override
_OpenContainerState createState() => _OpenContainerState();
}
class _OpenContainerState extends State<OpenContainer> {
// Key used in [_OpenContainerRoute] to hide the widget returned by
// [OpenContainer.openBuilder] in the source route while the container is
// opening/open. A copy of that widget is included in the
// [_OpenContainerRoute] where it fades out. To avoid issues with double
// shadows and transparency, we hide it in the source route.
final GlobalKey<_HideableState> _hideableKey = GlobalKey<_HideableState>();
// Key used to steal the state of the widget returned by
// [OpenContainer.openBuilder] from the source route and attach it to the
// same widget included in the [_OpenContainerRoute] where it fades out.
final GlobalKey _closedBuilderKey = GlobalKey();
void openContainer() {
Navigator.of(context).push(_OpenContainerRoute(
closedColor: widget.closedColor,
openColor: widget.openColor,
closedElevation: widget.closedElevation,
openElevation: widget.openElevation,
closedShape: widget.closedShape,
openShape: widget.openShape,
closedBuilder: widget.closedBuilder,
openBuilder: widget.openBuilder,
hideableKey: _hideableKey,
closedBuilderKey: _closedBuilderKey,
transitionDuration: widget.transitionDuration,
));
}
@override
Widget build(BuildContext context) {
return _Hideable(
key: _hideableKey,
child: GestureDetector(
onTap: widget.tappable ? openContainer : null,
child: Material(
clipBehavior: Clip.antiAlias,
color: widget.closedColor,
elevation: widget.closedElevation,
shape: widget.closedShape,
child: Builder(
key: _closedBuilderKey,
builder: (BuildContext context) {
return widget.closedBuilder(context, openContainer);
},
),
),
),
);
}
}
/// Controls the visibility of its child.
///
/// The child can be in one of three states:
///
/// * It is included in the tree and fully visible. (The `placeholderSize` is
/// null and `isVisible` is true.)
/// * It is included in the tree, but not visible; its size is maintained.
/// (The `placeholderSize` is null and `isVisible` is false.)
/// * It is not included in the tree. Instead a [SizedBox] of dimensions
/// specified by `placeholderSize` is included in the tree. (The value of
/// `isVisible` is ignored).
class _Hideable extends StatefulWidget {
const _Hideable({
Key key,
this.child,
}) : super(key: key);
final Widget child;
@override
State<_Hideable> createState() => _HideableState();
}
class _HideableState extends State<_Hideable> {
/// When non-null the child is replaced by a [SizedBox] of the set size.
Size get placeholderSize => _placeholderSize;
Size _placeholderSize;
set placeholderSize(Size value) {
if (_placeholderSize == value) {
return;
}
setState(() {
_placeholderSize = value;
});
}
/// When true the child is not visible, but will maintain its size.
///
/// The value of this property is ignored when [placeholderSize] is non-null
/// (i.e. [isInTree] returns false).
bool get isVisible => _visible;
bool _visible = true;
set isVisible(bool value) {
assert(value != null);
if (_visible == value) {
return;
}
setState(() {
_visible = value;
});
}
/// Whether the child is currently included in the tree.
///
/// When it is included, it may be visible or not according to [isVisible].
bool get isInTree => _placeholderSize == null;
@override
Widget build(BuildContext context) {
if (_placeholderSize != null) {
return SizedBox.fromSize(size: _placeholderSize);
}
return Opacity(
opacity: _visible ? 1.0 : 0.0,
child: widget.child,
);
}
}
class _OpenContainerRoute extends ModalRoute<void> {
_OpenContainerRoute({
@required Color closedColor,
@required this.openColor,
@required double closedElevation,
@required this.openElevation,
@required ShapeBorder closedShape,
@required this.openShape,
@required this.closedBuilder,
@required this.openBuilder,
@required this.hideableKey,
@required this.closedBuilderKey,
@required this.transitionDuration,
}) : assert(closedColor != null),
assert(openColor != null),
assert(closedElevation != null),
assert(openElevation != null),
assert(closedShape != null),
assert(openBuilder != null),
assert(closedBuilder != null),
assert(hideableKey != null),
assert(closedBuilderKey != null),
_elevationTween = Tween<double>(
begin: closedElevation,
end: openElevation,
),
_shapeTween = ShapeBorderTween(
begin: closedShape,
end: openShape,
),
_colorTween = _FlippableTweenSequence<Color>(<TweenSequenceItem<Color>>[
TweenSequenceItem<Color>(
tween: ColorTween(begin: closedColor, end: Colors.white),
weight: 4 / 12,
),
TweenSequenceItem<Color>(
tween: ColorTween(begin: Colors.white, end: openColor),
weight: 8 / 12,
),
]);
final Color openColor;
final double openElevation;
final ShapeBorder openShape;
final OpenContainerBuilder closedBuilder;
final OpenContainerBuilder openBuilder;
// See [_OpenContainerState._hideableKey].
final GlobalKey<_HideableState> hideableKey;
// See [_OpenContainerState._closedBuilderKey].
final GlobalKey closedBuilderKey;
@override
final Duration transitionDuration;
final Tween<double> _elevationTween;
final ShapeBorderTween _shapeTween;
final _FlippableTweenSequence<Color> _colorTween;
final _FlippableTweenSequence<double> _closedOpacityTween =
_FlippableTweenSequence<double>(<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: Tween<double>(begin: 1.0, end: 0.0),
weight: 4 / 12,
),
TweenSequenceItem<double>(
tween: ConstantTween<double>(0.0),
weight: 8 / 12,
),
]);
final _FlippableTweenSequence<double> _openOpacityTween =
_FlippableTweenSequence<double>(<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: ConstantTween<double>(0.0),
weight: 4 / 12,
),
TweenSequenceItem<double>(
tween: Tween<double>(begin: 0.0, end: 1.0),
weight: 8 / 12,
),
]);
// Key used for the widget returned by [OpenContainer.openBuilder] to keep
// its state when the shape of the widget tree is changed at the end of the
// animation to remove all the craft that was necessary to make the animation
// work.
final GlobalKey _openBuilderKey = GlobalKey();
// Defines the position of the (opening) [OpenContainer] within the bounds of
// the enclosing [Navigator].
final RectTween _insetsTween = RectTween(end: Rect.zero);
// Defines the size of the [OpenContainer].
final SizeTween _sizeTween = SizeTween();
AnimationStatus _lastAnimationStatus;
AnimationStatus _currentAnimationStatus;
@override
TickerFuture didPush() {
_takeMeasurements(navigatorContext: hideableKey.currentContext);
animation.addStatusListener((AnimationStatus status) {
_lastAnimationStatus = _currentAnimationStatus;
_currentAnimationStatus = status;
switch (status) {
case AnimationStatus.dismissed:
hideableKey.currentState
..placeholderSize = null
..isVisible = true;
break;
case AnimationStatus.completed:
hideableKey.currentState
..placeholderSize = null
..isVisible = false;
break;
case AnimationStatus.forward:
case AnimationStatus.reverse:
break;
}
});
return super.didPush();
}
@override
bool didPop(void result) {
_takeMeasurements(
navigatorContext: subtreeContext,
delayForSourceRoute: true,
);
return super.didPop(result);
}
void _takeMeasurements({
BuildContext navigatorContext,
bool delayForSourceRoute = false,
}) {
final RenderBox navigator =
Navigator.of(navigatorContext).context.findRenderObject();
final Size navSize = _getSize(navigator);
_sizeTween.end = navSize;
void takeMeasurementsInSourceRoute([Duration _]) {
if (!navigator.attached || hideableKey.currentContext == null) {
return;
}
final Rect srcRect = _getRect(hideableKey, navigator);
_sizeTween.begin = srcRect.size;
_insetsTween.begin = Rect.fromLTRB(
srcRect.left,
srcRect.top,
navSize.width - srcRect.right,
navSize.height - srcRect.bottom,
);
hideableKey.currentState.placeholderSize = _sizeTween.begin;
}
if (delayForSourceRoute) {
SchedulerBinding.instance
.addPostFrameCallback(takeMeasurementsInSourceRoute);
} else {
takeMeasurementsInSourceRoute();
}
}
Size _getSize(RenderBox render) {
assert(render != null && render.hasSize);
return render.size;
}
// Returns the bounds of the [RenderObject] identified by `key` in the
// coordinate system of `ancestor`.
Rect _getRect(GlobalKey key, RenderBox ancestor) {
assert(key.currentContext != null);
assert(ancestor != null && ancestor.hasSize);
final RenderBox render = key.currentContext.findRenderObject();
assert(render != null && render.hasSize);
return MatrixUtils.transformRect(
render.getTransformTo(ancestor),
Offset.zero & render.size,
);
}
bool get _transitionWasInterrupted {
bool wasInProgress = false;
bool isInProgress = false;
switch (_currentAnimationStatus) {
case AnimationStatus.completed:
case AnimationStatus.dismissed:
isInProgress = false;
break;
case AnimationStatus.forward:
case AnimationStatus.reverse:
isInProgress = true;
break;
}
switch (_lastAnimationStatus) {
case AnimationStatus.completed:
case AnimationStatus.dismissed:
wasInProgress = false;
break;
case AnimationStatus.forward:
case AnimationStatus.reverse:
wasInProgress = true;
break;
}
return wasInProgress && isInProgress;
}
void closeContainer() {
Navigator.of(subtreeContext).pop();
}
@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return Align(
alignment: Alignment.topLeft,
child: AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
if (animation.isCompleted) {
return SizedBox.expand(
child: Material(
color: openColor,
elevation: openElevation,
shape: openShape,
child: Builder(
key: _openBuilderKey,
builder: (BuildContext context) {
return openBuilder(context, closeContainer);
},
),
),
);
}
final Animation<double> curvedAnimation = CurvedAnimation(
parent: animation,
curve: Curves.fastOutSlowIn,
reverseCurve:
_transitionWasInterrupted ? null : Curves.fastOutSlowIn.flipped,
);
TweenSequence<Color> colorTween;
TweenSequence<double> closedOpacityTween, openOpacityTween;
switch (animation.status) {
case AnimationStatus.dismissed:
case AnimationStatus.forward:
closedOpacityTween = _closedOpacityTween;
openOpacityTween = _openOpacityTween;
colorTween = _colorTween;
break;
case AnimationStatus.reverse:
if (_transitionWasInterrupted) {
closedOpacityTween = _closedOpacityTween;
openOpacityTween = _openOpacityTween;
colorTween = _colorTween;
break;
}
closedOpacityTween = _closedOpacityTween.flipped;
openOpacityTween = _openOpacityTween.flipped;
colorTween = _colorTween.flipped;
break;
case AnimationStatus.completed:
assert(false); // Unreachable.
break;
}
final Rect rect = _insetsTween.evaluate(curvedAnimation);
final Size size = _sizeTween.evaluate(curvedAnimation);
return Padding(
padding: EdgeInsets.fromLTRB(
rect.left,
rect.top,
rect.right,
rect.bottom,
),
child: SizedBox(
width: size.width,
height: size.height,
child: Material(
clipBehavior: Clip.antiAlias,
animationDuration: Duration.zero,
color: colorTween.evaluate(animation),
shape: _shapeTween.evaluate(curvedAnimation),
elevation: _elevationTween.evaluate(curvedAnimation),
child: Stack(
fit: StackFit.passthrough,
children: <Widget>[
// Closed child fading out.
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.topLeft,
child: SizedBox(
width: _sizeTween.begin.width,
height: _sizeTween.begin.height,
child: hideableKey.currentState.isInTree
? null
: Opacity(
opacity: closedOpacityTween.evaluate(animation),
child: Builder(
key: closedBuilderKey,
builder: (BuildContext context) {
// Use dummy "open container" callback
// since we are in the process of opening.
return closedBuilder(context, () {});
},
),
),
),
),
// Open child fading in.
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.topLeft,
child: SizedBox(
width: _sizeTween.end.width,
height: _sizeTween.end.height,
child: Opacity(
opacity: openOpacityTween.evaluate(animation),
child: Builder(
key: _openBuilderKey,
builder: (BuildContext context) {
return openBuilder(context, closeContainer);
},
),
),
),
),
],
),
),
),
);
},
),
);
}
@override
bool get maintainState => true;
@override
Color get barrierColor => null;
@override
bool get opaque => true;
@override
bool get barrierDismissible => false;
@override
String get barrierLabel => null;
}
class _FlippableTweenSequence<T> extends TweenSequence<T> {
_FlippableTweenSequence(this._items) : super(_items);
final List<TweenSequenceItem<T>> _items;
_FlippableTweenSequence<T> _flipped;
_FlippableTweenSequence<T> get flipped {
if (_flipped == null) {
final List<TweenSequenceItem<T>> newItems = <TweenSequenceItem<T>>[];
for (int i = 0; i < _items.length; i++) {
newItems.add(TweenSequenceItem<T>(
tween: _items[i].tween,
weight: _items[_items.length - 1 - i].weight,
));
}
_flipped = _FlippableTweenSequence<T>(newItems);
}
return _flipped;
}
}

View File

@ -0,0 +1,15 @@
name: animations
description: Fancy pre-built Animations for Flutter.
version: 0.0.1
homepage: https://github.com/flutter/packages/tree/master/packages/animations
environment:
sdk: ">=2.1.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter

View File

@ -0,0 +1,934 @@
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:animations/src/open_container.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Container opens', (WidgetTester tester) async {
const ShapeBorder shape = RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8.0)),
);
bool closedBuilderCalled = false;
bool openBuilderCalled = false;
await tester.pumpWidget(_boilerplate(
child: Center(
child: OpenContainer(
closedColor: Colors.green,
openColor: Colors.blue,
closedElevation: 4.0,
openElevation: 8.0,
closedShape: shape,
closedBuilder: (BuildContext context, VoidCallback _) {
closedBuilderCalled = true;
return const Text('Closed');
},
openBuilder: (BuildContext context, VoidCallback _) {
openBuilderCalled = true;
return const Text('Open');
},
),
),
));
// Closed container has the expected properties.
final StatefulElement srcMaterialElement = tester.firstElement(
find.ancestor(
of: find.text('Closed'),
matching: find.byType(Material),
),
);
final Material srcMaterial = srcMaterialElement.widget;
expect(srcMaterial.color, Colors.green);
expect(srcMaterial.elevation, 4.0);
expect(srcMaterial.shape, shape);
expect(find.text('Closed'), findsOneWidget);
expect(find.text('Open'), findsNothing);
expect(closedBuilderCalled, isTrue);
expect(openBuilderCalled, isFalse);
final Rect srcMaterialRect = tester.getRect(
find.byElementPredicate((Element e) => e == srcMaterialElement),
);
// Open the container.
await tester.tap(find.text('Closed'));
expect(find.text('Closed'), findsOneWidget);
expect(find.text('Open'), findsNothing);
await tester.pump();
// On the first frame of the animation everything still looks like before.
final StatefulElement destMaterialElement = tester.firstElement(
find.ancestor(
of: find.text('Closed'),
matching: find.byType(Material),
),
);
final Material closedMaterial = destMaterialElement.widget;
expect(closedMaterial.color, Colors.green);
expect(closedMaterial.elevation, 4.0);
expect(closedMaterial.shape, shape);
expect(find.text('Closed'), findsOneWidget);
expect(find.text('Open'), findsOneWidget);
final Rect closedMaterialRect = tester.getRect(
find.byElementPredicate((Element e) => e == destMaterialElement),
);
expect(closedMaterialRect, srcMaterialRect);
expect(_getOpacity(tester, 'Open'), 0.0);
expect(_getOpacity(tester, 'Closed'), 1.0);
final _TrackedData dataClosed = _TrackedData(
closedMaterial,
closedMaterialRect,
);
// The fade-out takes 4/12 of 300ms. Let's jump to the midpoint of that.
await tester.pump(const Duration(milliseconds: 50)); // 300 * 2/12 = 50
final _TrackedData dataMidFadeOut = _TrackedData(
destMaterialElement.widget,
tester.getRect(
find.byElementPredicate((Element e) => e == destMaterialElement),
),
);
_expectMaterialPropertiesHaveAdvanced(
smallerMaterial: dataClosed,
biggerMaterial: dataMidFadeOut,
tester: tester,
);
expect(_getOpacity(tester, 'Open'), 0.0);
expect(_getOpacity(tester, 'Closed'), lessThan(1.0));
expect(_getOpacity(tester, 'Closed'), greaterThan(0.0));
// Let's jump to the crossover point at 4/12 of 300ms.
await tester.pump(const Duration(milliseconds: 50)); // 300 * 2/12 = 50
final _TrackedData dataMidpoint = _TrackedData(
destMaterialElement.widget,
tester.getRect(
find.byElementPredicate((Element e) => e == destMaterialElement),
),
);
_expectMaterialPropertiesHaveAdvanced(
smallerMaterial: dataMidFadeOut,
biggerMaterial: dataMidpoint,
tester: tester,
);
expect(_getOpacity(tester, 'Open'), moreOrLessEquals(0.0));
expect(_getOpacity(tester, 'Closed'), moreOrLessEquals(0.0));
// Let's jump to the middle of the fade-in at 8/12 of 300ms
await tester.pump(const Duration(milliseconds: 100)); // 300 * 4/12 = 100
final _TrackedData dataMidFadeIn = _TrackedData(
destMaterialElement.widget,
tester.getRect(
find.byElementPredicate((Element e) => e == destMaterialElement),
),
);
_expectMaterialPropertiesHaveAdvanced(
smallerMaterial: dataMidpoint,
biggerMaterial: dataMidFadeIn,
tester: tester,
);
expect(_getOpacity(tester, 'Open'), lessThan(1.0));
expect(_getOpacity(tester, 'Open'), greaterThan(0.0));
expect(_getOpacity(tester, 'Closed'), 0.0);
// Let's jump almost to the end of the transition.
await tester.pump(const Duration(milliseconds: 100));
final _TrackedData dataTransitionDone = _TrackedData(
destMaterialElement.widget,
tester.getRect(
find.byElementPredicate((Element e) => e == destMaterialElement),
),
);
_expectMaterialPropertiesHaveAdvanced(
smallerMaterial: dataMidFadeIn,
biggerMaterial: dataTransitionDone,
tester: tester,
);
expect(_getOpacity(tester, 'Open'), 1.0);
expect(_getOpacity(tester, 'Closed'), 0.0);
expect(dataTransitionDone.material.color, Colors.blue);
expect(dataTransitionDone.material.elevation, 8.0);
expect(dataTransitionDone.radius, 0.0);
expect(dataTransitionDone.rect, const Rect.fromLTRB(0, 0, 800, 600));
await tester.pump(const Duration(milliseconds: 1));
expect(find.text('Closed'), findsNothing); // No longer in the tree.
expect(find.text('Open'), findsOneWidget);
final StatefulElement finalMaterialElement = tester.firstElement(
find.ancestor(
of: find.text('Open'),
matching: find.byType(Material),
),
);
final _TrackedData dataOpen = _TrackedData(
finalMaterialElement.widget,
tester.getRect(
find.byElementPredicate((Element e) => e == finalMaterialElement),
),
);
expect(dataOpen.material.color, dataTransitionDone.material.color);
expect(dataOpen.material.elevation, dataTransitionDone.material.elevation);
expect(dataOpen.radius, dataTransitionDone.radius);
expect(dataOpen.rect, dataTransitionDone.rect);
});
testWidgets('Container closes', (WidgetTester tester) async {
const ShapeBorder shape = RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8.0)),
);
await tester.pumpWidget(_boilerplate(
child: Center(
child: OpenContainer(
closedColor: Colors.green,
openColor: Colors.blue,
closedElevation: 4.0,
openElevation: 8.0,
closedShape: shape,
closedBuilder: (BuildContext context, VoidCallback _) {
return const Text('Closed');
},
openBuilder: (BuildContext context, VoidCallback _) {
return const Text('Open');
},
),
),
));
await tester.tap(find.text('Closed'));
await tester.pumpAndSettle();
// Open container has the expected properties.
expect(find.text('Closed'), findsNothing);
expect(find.text('Open'), findsOneWidget);
final StatefulElement initialMaterialElement = tester.firstElement(
find.ancestor(
of: find.text('Open'),
matching: find.byType(Material),
),
);
final _TrackedData dataOpen = _TrackedData(
initialMaterialElement.widget,
tester.getRect(
find.byElementPredicate((Element e) => e == initialMaterialElement),
),
);
expect(dataOpen.material.color, Colors.blue);
expect(dataOpen.material.elevation, 8.0);
expect(dataOpen.radius, 0.0);
expect(dataOpen.rect, const Rect.fromLTRB(0, 0, 800, 600));
// Close the container.
final NavigatorState navigator = tester.state(find.byType(Navigator));
navigator.pop();
await tester.pump();
expect(find.text('Closed'), findsOneWidget);
expect(find.text('Open'), findsOneWidget);
final StatefulElement materialElement = tester.firstElement(
find.ancestor(
of: find.text('Open'),
matching: find.byType(Material),
),
);
final _TrackedData dataTransitionStart = _TrackedData(
materialElement.widget,
tester.getRect(
find.byElementPredicate((Element e) => e == materialElement),
),
);
expect(dataTransitionStart.material.color, dataOpen.material.color);
expect(dataTransitionStart.material.elevation, dataOpen.material.elevation);
expect(dataTransitionStart.radius, dataOpen.radius);
expect(dataTransitionStart.rect, dataOpen.rect);
expect(_getOpacity(tester, 'Open'), 1.0);
expect(_getOpacity(tester, 'Closed'), 0.0);
// Jump to mid-point of fade-out: 2/12 of 300.
await tester.pump(const Duration(milliseconds: 50)); // 300 * 2/12 = 50
final _TrackedData dataMidFadeOut = _TrackedData(
materialElement.widget,
tester.getRect(
find.byElementPredicate((Element e) => e == materialElement),
),
);
_expectMaterialPropertiesHaveAdvanced(
smallerMaterial: dataMidFadeOut,
biggerMaterial: dataTransitionStart,
tester: tester,
);
expect(_getOpacity(tester, 'Closed'), 0.0);
expect(_getOpacity(tester, 'Open'), lessThan(1.0));
expect(_getOpacity(tester, 'Open'), greaterThan(0.0));
// Let's jump to the crossover point at 4/12 of 300ms.
await tester.pump(const Duration(milliseconds: 50)); // 300 * 2/12 = 50
final _TrackedData dataMidpoint = _TrackedData(
materialElement.widget,
tester.getRect(
find.byElementPredicate((Element e) => e == materialElement),
),
);
_expectMaterialPropertiesHaveAdvanced(
smallerMaterial: dataMidpoint,
biggerMaterial: dataMidFadeOut,
tester: tester,
);
expect(_getOpacity(tester, 'Open'), moreOrLessEquals(0.0));
expect(_getOpacity(tester, 'Closed'), moreOrLessEquals(0.0));
// Let's jump to the middle of the fade-in at 8/12 of 300ms
await tester.pump(const Duration(milliseconds: 100)); // 300 * 4/12 = 100
final _TrackedData dataMidFadeIn = _TrackedData(
materialElement.widget,
tester.getRect(
find.byElementPredicate((Element e) => e == materialElement),
),
);
_expectMaterialPropertiesHaveAdvanced(
smallerMaterial: dataMidFadeIn,
biggerMaterial: dataMidpoint,
tester: tester,
);
expect(_getOpacity(tester, 'Closed'), lessThan(1.0));
expect(_getOpacity(tester, 'Closed'), greaterThan(0.0));
expect(_getOpacity(tester, 'Open'), 0.0);
// Let's jump almost to the end of the transition.
await tester.pump(const Duration(milliseconds: 100));
final _TrackedData dataTransitionDone = _TrackedData(
materialElement.widget,
tester.getRect(
find.byElementPredicate((Element e) => e == materialElement),
),
);
_expectMaterialPropertiesHaveAdvanced(
smallerMaterial: dataTransitionDone,
biggerMaterial: dataMidFadeIn,
tester: tester,
);
expect(_getOpacity(tester, 'Closed'), 1.0);
expect(_getOpacity(tester, 'Open'), 0.0);
expect(dataTransitionDone.material.color, Colors.green);
expect(dataTransitionDone.material.elevation, 4.0);
expect(dataTransitionDone.radius, 8.0);
await tester.pump(const Duration(milliseconds: 1));
expect(find.text('Open'), findsNothing); // No longer in the tree.
expect(find.text('Closed'), findsOneWidget);
final StatefulElement finalMaterialElement = tester.firstElement(
find.ancestor(
of: find.text('Closed'),
matching: find.byType(Material),
),
);
final _TrackedData dataClosed = _TrackedData(
finalMaterialElement.widget,
tester.getRect(
find.byElementPredicate((Element e) => e == finalMaterialElement),
),
);
expect(dataClosed.material.color, dataTransitionDone.material.color);
expect(
dataClosed.material.elevation,
dataTransitionDone.material.elevation,
);
expect(dataClosed.radius, dataTransitionDone.radius);
expect(dataClosed.rect, dataTransitionDone.rect);
});
testWidgets('Cannot tap container if tappable=false',
(WidgetTester tester) async {
await tester.pumpWidget(_boilerplate(
child: Center(
child: OpenContainer(
tappable: false,
closedBuilder: (BuildContext context, VoidCallback _) {
return const Text('Closed');
},
openBuilder: (BuildContext context, VoidCallback _) {
return const Text('Open');
},
),
),
));
expect(find.text('Open'), findsNothing);
expect(find.text('Closed'), findsOneWidget);
await tester.tap(find.text('Closed'));
await tester.pumpAndSettle();
expect(find.text('Open'), findsNothing);
expect(find.text('Closed'), findsOneWidget);
});
testWidgets('Action callbacks work', (WidgetTester tester) async {
VoidCallback open, close;
await tester.pumpWidget(_boilerplate(
child: Center(
child: OpenContainer(
tappable: false,
closedBuilder: (BuildContext context, VoidCallback action) {
open = action;
return const Text('Closed');
},
openBuilder: (BuildContext context, VoidCallback action) {
close = action;
return const Text('Open');
},
),
),
));
expect(find.text('Open'), findsNothing);
expect(find.text('Closed'), findsOneWidget);
expect(open, isNotNull);
open();
await tester.pumpAndSettle();
expect(find.text('Open'), findsOneWidget);
expect(find.text('Closed'), findsNothing);
expect(close, isNotNull);
close();
await tester.pumpAndSettle();
expect(find.text('Open'), findsNothing);
expect(find.text('Closed'), findsOneWidget);
});
testWidgets('open widget keeps state', (WidgetTester tester) async {
await tester.pumpWidget(_boilerplate(
child: Center(
child: OpenContainer(
closedBuilder: (BuildContext context, VoidCallback action) {
return const Text('Closed');
},
openBuilder: (BuildContext context, VoidCallback action) {
return Switch(
value: true,
onChanged: (bool v) {},
);
},
),
),
));
await tester.tap(find.text('Closed'));
await tester.pump(const Duration(milliseconds: 200));
final State stateOpening = tester.state(find.byType(Switch));
expect(stateOpening, isNotNull);
await tester.pumpAndSettle();
expect(find.text('Closed'), findsNothing);
final State stateOpen = tester.state(find.byType(Switch));
expect(stateOpen, isNotNull);
expect(stateOpen, same(stateOpening));
final NavigatorState navigator = tester.state(find.byType(Navigator));
navigator.pop();
await tester.pump(const Duration(milliseconds: 200));
expect(find.text('Closed'), findsOneWidget);
final State stateClosing = tester.state(find.byType(Switch));
expect(stateClosing, isNotNull);
expect(stateClosing, same(stateOpen));
});
testWidgets('closed widget keeps state', (WidgetTester tester) async {
VoidCallback open;
await tester.pumpWidget(_boilerplate(
child: Center(
child: OpenContainer(
closedBuilder: (BuildContext context, VoidCallback action) {
open = action;
return Switch(
value: true,
onChanged: (bool v) {},
);
},
openBuilder: (BuildContext context, VoidCallback action) {
return const Text('Open');
},
),
),
));
final State stateClosed = tester.state(find.byType(Switch));
expect(stateClosed, isNotNull);
open();
await tester.pump(const Duration(milliseconds: 200));
expect(find.text('Open'), findsOneWidget);
final State stateOpening = tester.state(find.byType(Switch));
expect(stateOpening, same(stateClosed));
await tester.pumpAndSettle();
expect(find.byType(Switch), findsNothing);
expect(find.text('Open'), findsOneWidget);
final State stateOpen = tester.state(find.byType(
Switch,
skipOffstage: false,
));
expect(stateOpen, same(stateOpening));
final NavigatorState navigator = tester.state(find.byType(Navigator));
navigator.pop();
await tester.pump(const Duration(milliseconds: 200));
expect(find.text('Open'), findsOneWidget);
final State stateClosing = tester.state(find.byType(Switch));
expect(stateClosing, same(stateOpen));
await tester.pumpAndSettle();
expect(find.text('Open'), findsNothing);
final State stateClosedAgain = tester.state(find.byType(Switch));
expect(stateClosedAgain, same(stateClosing));
});
testWidgets('closes to the right location when src position has changed',
(WidgetTester tester) async {
final Widget openContainer = OpenContainer(
closedBuilder: (BuildContext context, VoidCallback action) {
return Container(
height: 100,
width: 100,
child: const Text('Closed'),
);
},
openBuilder: (BuildContext context, VoidCallback action) {
return GestureDetector(
onTap: action,
child: const Text('Open'),
);
},
);
await tester.pumpWidget(_boilerplate(
child: Align(
alignment: Alignment.topLeft,
child: openContainer,
),
));
final Rect originTextRect = tester.getRect(find.text('Closed'));
expect(originTextRect.topLeft, Offset.zero);
await tester.tap(find.text('Closed'));
await tester.pumpAndSettle();
expect(find.text('Open'), findsOneWidget);
expect(find.text('Closed'), findsNothing);
await tester.pumpWidget(_boilerplate(
child: Align(
alignment: Alignment.bottomLeft,
child: openContainer,
),
));
expect(find.text('Open'), findsOneWidget);
expect(find.text('Closed'), findsNothing);
await tester.tap(find.text('Open'));
await tester.pump(); // Need one frame to measure things in the old route.
await tester.pump(const Duration(milliseconds: 300));
expect(find.text('Open'), findsOneWidget);
expect(find.text('Closed'), findsOneWidget);
final Rect transitionEndTextRect = tester.getRect(find.text('Open'));
expect(transitionEndTextRect.topLeft, const Offset(0.0, 600.0 - 100.0));
await tester.pump(const Duration(milliseconds: 1));
expect(find.text('Open'), findsNothing);
expect(find.text('Closed'), findsOneWidget);
final Rect finalTextRect = tester.getRect(find.text('Closed'));
expect(finalTextRect.topLeft, transitionEndTextRect.topLeft);
});
testWidgets('src changes size while open', (WidgetTester tester) async {
double closedSize = 100;
final Widget openContainer = _boilerplate(
child: Center(
child: OpenContainer(
closedBuilder: (BuildContext context, VoidCallback action) {
return Container(
height: closedSize,
width: closedSize,
child: const Text('Closed'),
);
},
openBuilder: (BuildContext context, VoidCallback action) {
return GestureDetector(
onTap: action,
child: const Text('Open'),
);
},
),
),
);
await tester.pumpWidget(openContainer);
final Size orignalClosedRect = tester.getSize(find
.ancestor(
of: find.text('Closed'),
matching: find.byType(Material),
)
.first);
expect(orignalClosedRect, const Size(100, 100));
await tester.tap(find.text('Closed'));
await tester.pumpAndSettle();
expect(find.text('Open'), findsOneWidget);
expect(find.text('Closed'), findsNothing);
closedSize = 200;
await tester.pumpWidget(openContainer);
expect(find.text('Open'), findsOneWidget);
expect(find.text('Closed'), findsNothing);
await tester.tap(find.text('Open'));
await tester.pump(); // Need one frame to measure things in the old route.
await tester.pump(const Duration(milliseconds: 300));
expect(find.text('Open'), findsOneWidget);
expect(find.text('Closed'), findsOneWidget);
final Size transitionEndSize = tester.getSize(find
.ancestor(
of: find.text('Open'),
matching: find.byType(Material),
)
.first);
expect(transitionEndSize, const Size(200, 200));
await tester.pump(const Duration(milliseconds: 1));
expect(find.text('Open'), findsNothing);
expect(find.text('Closed'), findsOneWidget);
final Size finalSize = tester.getSize(find
.ancestor(
of: find.text('Closed'),
matching: find.byType(Material),
)
.first);
expect(finalSize, const Size(200, 200));
});
testWidgets('transition is interrupted and should not jump',
(WidgetTester tester) async {
await tester.pumpWidget(_boilerplate(
child: Center(
child: OpenContainer(
closedBuilder: (BuildContext context, VoidCallback action) {
return const Text('Closed');
},
openBuilder: (BuildContext context, VoidCallback action) {
return const Text('Open');
},
),
),
));
await tester.tap(find.text('Closed'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 150));
expect(find.text('Open'), findsOneWidget);
expect(find.text('Closed'), findsOneWidget);
final Material openingMaterial = tester.firstWidget(find.ancestor(
of: find.text('Closed'),
matching: find.byType(Material),
));
final Rect openingRect = tester.getRect(
find.byWidgetPredicate((Widget w) => w == openingMaterial),
);
// Close the container while it is half way to open.
final NavigatorState navigator = tester.state(find.byType(Navigator));
navigator.pop();
await tester.pump();
final Material closingMaterial = tester.firstWidget(find.ancestor(
of: find.text('Open'),
matching: find.byType(Material),
));
final Rect closingRect = tester.getRect(
find.byWidgetPredicate((Widget w) => w == closingMaterial),
);
expect(closingMaterial.elevation, openingMaterial.elevation);
expect(closingMaterial.color, openingMaterial.color);
expect(closingMaterial.shape, openingMaterial.shape);
expect(closingRect, openingRect);
});
testWidgets('navigator is not full size', (WidgetTester tester) async {
await tester.pumpWidget(Center(
child: SizedBox(
width: 300,
height: 400,
child: _boilerplate(
child: Center(
child: OpenContainer(
closedBuilder: (BuildContext context, VoidCallback action) {
return const Text('Closed');
},
openBuilder: (BuildContext context, VoidCallback action) {
return const Text('Open');
},
),
),
),
),
));
const Rect fullNavigator = Rect.fromLTWH(250, 100, 300, 400);
expect(tester.getRect(find.byType(Navigator)), fullNavigator);
final Rect materialRectClosed = tester.getRect(find
.ancestor(
of: find.text('Closed'),
matching: find.byType(Material),
)
.first);
await tester.tap(find.text('Closed'));
await tester.pump();
expect(find.text('Open'), findsOneWidget);
expect(find.text('Closed'), findsOneWidget);
final Rect materialRectTransitionStart = tester.getRect(find.ancestor(
of: find.text('Closed'),
matching: find.byType(Material),
));
expect(materialRectTransitionStart, materialRectClosed);
await tester.pump(const Duration(milliseconds: 300));
expect(find.text('Open'), findsOneWidget);
expect(find.text('Closed'), findsOneWidget);
final Rect materialRectTransitionEnd = tester.getRect(find.ancestor(
of: find.text('Open'),
matching: find.byType(Material),
));
expect(materialRectTransitionEnd, fullNavigator);
await tester.pumpAndSettle();
expect(find.text('Open'), findsOneWidget);
expect(find.text('Closed'), findsNothing);
final Rect materialRectOpen = tester.getRect(find.ancestor(
of: find.text('Open'),
matching: find.byType(Material),
));
expect(materialRectOpen, fullNavigator);
});
testWidgets('does not crash when disposed right after pop',
(WidgetTester tester) async {
await tester.pumpWidget(Center(
child: SizedBox(
width: 300,
height: 400,
child: _boilerplate(
child: Center(
child: OpenContainer(
closedBuilder: (BuildContext context, VoidCallback action) {
return const Text('Closed');
},
openBuilder: (BuildContext context, VoidCallback action) {
return const Text('Open');
},
),
),
),
),
));
await tester.tap(find.text('Closed'));
await tester.pumpAndSettle();
final NavigatorState navigator = tester.state(find.byType(Navigator));
navigator.pop();
await tester.pumpWidget(const Placeholder());
expect(tester.takeException(), isNull);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
testWidgets('can specify a duration', (WidgetTester tester) async {
await tester.pumpWidget(Center(
child: SizedBox(
width: 300,
height: 400,
child: _boilerplate(
child: Center(
child: OpenContainer(
transitionDuration: const Duration(seconds: 2),
closedBuilder: (BuildContext context, VoidCallback action) {
return const Text('Closed');
},
openBuilder: (BuildContext context, VoidCallback action) {
return const Text('Open');
},
),
),
),
),
));
expect(find.text('Open'), findsNothing);
expect(find.text('Closed'), findsOneWidget);
await tester.tap(find.text('Closed'));
await tester.pump();
// Jump to the end of the transition.
await tester.pump(const Duration(seconds: 2));
expect(find.text('Open'), findsOneWidget); // faded in
expect(find.text('Closed'), findsOneWidget); // faded out
await tester.pump(const Duration(milliseconds: 1));
expect(find.text('Open'), findsOneWidget);
expect(find.text('Closed'), findsNothing);
final NavigatorState navigator = tester.state(find.byType(Navigator));
navigator.pop();
await tester.pump();
// Jump to the end of the transition.
await tester.pump(const Duration(seconds: 2));
expect(find.text('Open'), findsOneWidget); // faded out
expect(find.text('Closed'), findsOneWidget); // faded in
await tester.pump(const Duration(milliseconds: 1));
expect(find.text('Open'), findsNothing);
expect(find.text('Closed'), findsOneWidget);
});
testWidgets('can specify an open shape', (WidgetTester tester) async {
await tester.pumpWidget(Center(
child: SizedBox(
width: 300,
height: 400,
child: _boilerplate(
child: Center(
child: OpenContainer(
closedShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
openShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
closedBuilder: (BuildContext context, VoidCallback action) {
return const Text('Closed');
},
openBuilder: (BuildContext context, VoidCallback action) {
return const Text('Open');
},
),
),
),
),
));
expect(find.text('Open'), findsNothing);
expect(find.text('Closed'), findsOneWidget);
final double closedRadius = _getRadius(tester.firstWidget(find.ancestor(
of: find.text('Closed'),
matching: find.byType(Material),
)));
expect(closedRadius, 10.0);
await tester.tap(find.text('Closed'));
await tester.pump();
expect(find.text('Open'), findsOneWidget);
expect(find.text('Closed'), findsOneWidget);
final double openingRadius = _getRadius(tester.firstWidget(find.ancestor(
of: find.text('Open'),
matching: find.byType(Material),
)));
expect(openingRadius, 10.0);
await tester.pump(const Duration(milliseconds: 150));
expect(find.text('Open'), findsOneWidget);
expect(find.text('Closed'), findsOneWidget);
final double halfwayRadius = _getRadius(tester.firstWidget(find.ancestor(
of: find.text('Open'),
matching: find.byType(Material),
)));
expect(halfwayRadius, greaterThan(10.0));
expect(halfwayRadius, lessThan(40.0));
await tester.pump(const Duration(milliseconds: 150));
expect(find.text('Open'), findsOneWidget);
expect(find.text('Closed'), findsOneWidget);
final double openRadius = _getRadius(tester.firstWidget(find.ancestor(
of: find.text('Open'),
matching: find.byType(Material),
)));
expect(openRadius, 40.0);
await tester.pump(const Duration(milliseconds: 1));
expect(find.text('Closed'), findsNothing);
expect(find.text('Open'), findsOneWidget);
final double finalRadius = _getRadius(tester.firstWidget(find.ancestor(
of: find.text('Open'),
matching: find.byType(Material),
)));
expect(finalRadius, 40.0);
});
}
void _expectMaterialPropertiesHaveAdvanced({
@required _TrackedData biggerMaterial,
@required _TrackedData smallerMaterial,
@required WidgetTester tester,
}) {
expect(biggerMaterial.material.color, isNot(smallerMaterial.material.color));
expect(
biggerMaterial.material.elevation,
greaterThan(smallerMaterial.material.elevation),
);
expect(biggerMaterial.radius, lessThan(smallerMaterial.radius));
expect(biggerMaterial.rect.height, greaterThan(smallerMaterial.rect.height));
expect(biggerMaterial.rect.width, greaterThan(smallerMaterial.rect.width));
expect(biggerMaterial.rect.top, lessThan(smallerMaterial.rect.top));
expect(biggerMaterial.rect.left, lessThan(smallerMaterial.rect.left));
}
double _getOpacity(WidgetTester tester, String label) {
final Opacity widget = tester.firstWidget(find.ancestor(
of: find.text(label),
matching: find.byType(Opacity),
));
return widget.opacity;
}
class _TrackedData {
_TrackedData(this.material, this.rect);
final Material material;
final Rect rect;
double get radius => _getRadius(material);
}
double _getRadius(Material material) {
final RoundedRectangleBorder shape = material.shape;
if (shape == null) {
return 0.0;
}
final BorderRadius radius = shape.borderRadius;
return radius.topRight.x;
}
Widget _boilerplate({@required Widget child}) {
return MaterialApp(
home: Scaffold(
body: child,
),
);
}