mirror of
https://github.com/flutter/packages.git
synced 2025-06-27 21:28:33 +08:00
Adds animations package with open container transition (#30)
This commit is contained in:

committed by
GitHub

parent
cb58456da4
commit
0b809df740
73
packages/animations/.gitignore
vendored
Normal file
73
packages/animations/.gitignore
vendored
Normal 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
|
10
packages/animations/.metadata
Normal file
10
packages/animations/.metadata
Normal 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
|
3
packages/animations/CHANGELOG.md
Normal file
3
packages/animations/CHANGELOG.md
Normal file
@ -0,0 +1,3 @@
|
||||
## [0.0.1] - August 28, 2019
|
||||
|
||||
* Open Container to full screen transition
|
27
packages/animations/LICENSE
Normal file
27
packages/animations/LICENSE
Normal 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.
|
15
packages/animations/README.md
Normal file
15
packages/animations/README.md
Normal 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.*
|
5
packages/animations/lib/animations.dart
Normal file
5
packages/animations/lib/animations.dart
Normal 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';
|
691
packages/animations/lib/src/open_container.dart
Normal file
691
packages/animations/lib/src/open_container.dart
Normal 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;
|
||||
}
|
||||
}
|
15
packages/animations/pubspec.yaml
Normal file
15
packages/animations/pubspec.yaml
Normal 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
|
934
packages/animations/test/open_container_test.dart
Normal file
934
packages/animations/test/open_container_test.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user