Turn off AnimationControllers when not in use (#5902)

This requires all AnimationController objects to be given a
TickerProvider, a class that can create the Ticker.

It also provides some nice mixins for people who want to have their
State provide a TickerProvider. And a schedulerTickerProvider for those
cases where you just want to see your battery burn.

Also, we now enforce destruction order for elements.
This commit is contained in:
Ian Hickson 2016-09-26 10:57:10 -07:00 committed by GitHub
parent c825237a38
commit 9e673853e5
63 changed files with 1358 additions and 492 deletions

View File

@ -393,11 +393,14 @@ class _RectangleDemoState extends State<_RectangleDemo> {
typedef Widget _DemoBuilder(_ArcDemo demo);
class _ArcDemo {
_ArcDemo(String _title, this.builder) : title = _title, key = new GlobalKey(debugLabel: _title);
_ArcDemo(String _title, this.builder, TickerProvider vsync)
: title = _title,
controller = new AnimationController(duration: const Duration(milliseconds: 500), vsync: vsync),
key = new GlobalKey(debugLabel: _title);
final AnimationController controller = new AnimationController(duration: const Duration(milliseconds: 500));
final String title;
final _DemoBuilder builder;
final AnimationController controller;
final GlobalKey key;
}
@ -410,23 +413,29 @@ class AnimationDemo extends StatefulWidget {
_AnimationDemoState createState() => new _AnimationDemoState();
}
class _AnimationDemoState extends State<AnimationDemo> {
class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateMixin {
static final GlobalKey<TabBarSelectionState<_ArcDemo>> _tabsKey = new GlobalKey<TabBarSelectionState<_ArcDemo>>();
static final List<_ArcDemo> _allDemos = <_ArcDemo>[
new _ArcDemo('POINT', (_ArcDemo demo) {
return new _PointDemo(
key: demo.key,
controller: demo.controller
);
}),
new _ArcDemo('RECTANGLE', (_ArcDemo demo) {
return new _RectangleDemo(
key: demo.key,
controller: demo.controller
);
})
];
List<_ArcDemo> _allDemos;
@override
void initState() {
super.initState();
_allDemos = <_ArcDemo>[
new _ArcDemo('POINT', (_ArcDemo demo) {
return new _PointDemo(
key: demo.key,
controller: demo.controller
);
}, this),
new _ArcDemo('RECTANGLE', (_ArcDemo demo) {
return new _RectangleDemo(
key: demo.key,
controller: demo.controller
);
}, this),
];
}
Future<Null> _play() async {
_ArcDemo demo = _tabsKey.currentState.value;

View File

@ -8,7 +8,8 @@ class NavigationIconView {
NavigationIconView({
Icon icon,
Widget title,
Color color
Color color,
TickerProvider vsync,
}) : _icon = icon,
_color = color,
destinationLabel = new DestinationLabel(
@ -17,13 +18,14 @@ class NavigationIconView {
backgroundColor: color
),
controller = new AnimationController(
duration: kThemeAnimationDuration
duration: kThemeAnimationDuration,
vsync: vsync,
) {
_animation = new CurvedAnimation(
parent: controller,
curve: new Interval(0.5, 1.0, curve: Curves.fastOutSlowIn)
);
}
_animation = new CurvedAnimation(
parent: controller,
curve: new Interval(0.5, 1.0, curve: Curves.fastOutSlowIn)
);
}
final Icon _icon;
final Color _color;
@ -61,7 +63,7 @@ class BottomNavigationDemo extends StatefulWidget {
_BottomNavigationDemoState createState() => new _BottomNavigationDemoState();
}
class _BottomNavigationDemoState extends State<BottomNavigationDemo> {
class _BottomNavigationDemoState extends State<BottomNavigationDemo> with TickerProviderStateMixin {
int _currentIndex = 0;
BottomNavigationBarType _type = BottomNavigationBarType.shifting;
List<NavigationIconView> _navigationViews;
@ -73,22 +75,26 @@ class _BottomNavigationDemoState extends State<BottomNavigationDemo> {
new NavigationIconView(
icon: new Icon(Icons.access_alarm),
title: new Text('Alarm'),
color: Colors.deepPurple[500]
color: Colors.deepPurple[500],
vsync: this,
),
new NavigationIconView(
icon: new Icon(Icons.cloud),
title: new Text('Cloud'),
color: Colors.teal[500]
color: Colors.teal[500],
vsync: this,
),
new NavigationIconView(
icon: new Icon(Icons.favorite),
title: new Text('Favorites'),
color: Colors.indigo[500]
color: Colors.indigo[500],
vsync: this,
),
new NavigationIconView(
icon: new Icon(Icons.event_available),
title: new Text('Event'),
color: Colors.pink[500]
color: Colors.pink[500],
vsync: this,
)
];
@ -124,14 +130,12 @@ class _BottomNavigationDemoState extends State<BottomNavigationDemo> {
return aValue.compareTo(bValue);
});
return new Stack(
children: transitions
);
return new Stack(children: transitions);
}
@override
Widget build(BuildContext context) {
BottomNavigationBar botNavBar = new BottomNavigationBar(
final BottomNavigationBar botNavBar = new BottomNavigationBar(
labels: _navigationViews.map(
(NavigationIconView navigationView) => navigationView.destinationLabel
).toList(),
@ -159,18 +163,18 @@ class _BottomNavigationDemoState extends State<BottomNavigationDemo> {
itemBuilder: (BuildContext context) => <PopupMenuItem<BottomNavigationBarType>>[
new PopupMenuItem<BottomNavigationBarType>(
value: BottomNavigationBarType.fixed,
child: new Text('Fixed')
child: new Text('Fixed'),
),
new PopupMenuItem<BottomNavigationBarType>(
value: BottomNavigationBarType.shifting,
child: new Text('Shifting')
child: new Text('Shifting'),
)
]
)
]
),
body: _buildBody(),
bottomNavigationBar: botNavBar
bottomNavigationBar: botNavBar,
);
}
}

View File

@ -11,7 +11,7 @@ class ProgressIndicatorDemo extends StatefulWidget {
_ProgressIndicatorDemoState createState() => new _ProgressIndicatorDemoState();
}
class _ProgressIndicatorDemoState extends State<ProgressIndicatorDemo> {
class _ProgressIndicatorDemoState extends State<ProgressIndicatorDemo> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _animation;
@ -19,7 +19,8 @@ class _ProgressIndicatorDemoState extends State<ProgressIndicatorDemo> {
void initState() {
super.initState();
_controller = new AnimationController(
duration: const Duration(milliseconds: 1500)
duration: const Duration(milliseconds: 1500),
vsync: this,
)..forward();
_animation = new CurvedAnimation(

View File

@ -95,19 +95,20 @@ class GalleryHome extends StatefulWidget {
GalleryHomeState createState() => new GalleryHomeState();
}
class GalleryHomeState extends State<GalleryHome> {
static final Key _homeKey = new ValueKey<String>("Gallery Home");
class GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStateMixin {
static final Key _homeKey = new ValueKey<String>('Gallery Home');
static final GlobalKey<ScrollableState> _scrollableKey = new GlobalKey<ScrollableState>();
final AnimationController _controller = new AnimationController(
duration: const Duration(milliseconds: 600),
debugLabel: "preview banner"
);
AnimationController _controller;
@override
void initState() {
super.initState();
_controller.forward();
_controller = new AnimationController(
duration: const Duration(milliseconds: 600),
debugLabel: 'preview banner',
vsync: this,
)..forward();
}
@override

View File

@ -9,6 +9,13 @@ import 'dart:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
class NonStopVSync implements TickerProvider {
const NonStopVSync();
@override
Ticker createTicker(TickerCallback onTick) => new Ticker(onTick);
}
void main() {
// We first create a render object that represents a green box.
@ -41,7 +48,8 @@ void main() {
// To make the square spin, we use an animation that repeats every 1800
// milliseconds.
AnimationController animation = new AnimationController(
duration: const Duration(milliseconds: 1800)
duration: const Duration(milliseconds: 1800),
vsync: const NonStopVSync(),
)..repeat();
// The animation will produce a value between 0.0 and 1.0 each frame, but we
// want to rotate the square using a value between 0.0 and math.PI. To change

View File

@ -206,7 +206,7 @@ class IsolateExampleWidget extends StatefulWidget {
}
// Main application state.
class IsolateExampleState extends State<StatefulWidget> {
class IsolateExampleState extends State<StatefulWidget> with SingleTickerProviderStateMixin {
String _status = 'Idle';
String _label = 'Start';
@ -219,7 +219,8 @@ class IsolateExampleState extends State<StatefulWidget> {
void initState() {
super.initState();
_animation = new AnimationController(
duration: const Duration(milliseconds: 3600)
duration: const Duration(milliseconds: 3600),
vsync: this,
)..repeat();
_calculationManager = new CalculationManager(
onProgressListener: _handleProgressUpdate,

View File

@ -9,7 +9,7 @@ class SpinningSquare extends StatefulWidget {
_SpinningSquareState createState() => new _SpinningSquareState();
}
class _SpinningSquareState extends State<SpinningSquare> {
class _SpinningSquareState extends State<SpinningSquare> with SingleTickerProviderStateMixin {
AnimationController _animation;
@override
@ -19,7 +19,8 @@ class _SpinningSquareState extends State<SpinningSquare> {
// represents an entire turn of the square whereas in the other examples
// we used 0.0 -> math.PI, which is only half a turn.
_animation = new AnimationController(
duration: const Duration(milliseconds: 3600)
duration: const Duration(milliseconds: 3600),
vsync: this,
)..repeat();
}

View File

@ -5,8 +5,9 @@
import 'dart:async';
import 'dart:ui' as ui show lerpDouble;
import 'package:flutter/scheduler.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/scheduler.dart';
import 'package:meta/meta.dart';
import 'animation.dart';
import 'curves.dart';
@ -19,7 +20,7 @@ enum _AnimationDirection {
forward,
/// The animation is running backwards, from end to beginning.
reverse
reverse,
}
/// A controller for an animation.
@ -40,29 +41,33 @@ class AnimationController extends Animation<double>
/// Creates an animation controller.
///
/// * value is the initial value of the animation.
/// * duration is the length of time this animation should last.
/// * debugLabel is a string to help identify this animation during debugging (used by toString).
/// * lowerBound is the smallest value this animation can obtain and the value at which this animation is deemed to be dismissed.
/// * upperBound is the largest value this animation can obtain and the value at which this animation is deemed to be completed.
/// * [value] is the initial value of the animation.
/// * [duration] is the length of time this animation should last.
/// * [debugLabel] is a string to help identify this animation during debugging (used by [toString]).
/// * [lowerBound] is the smallest value this animation can obtain and the value at which this animation is deemed to be dismissed.
/// * [upperBound] is the largest value this animation can obtain and the value at which this animation is deemed to be completed.
/// * `vsync` is the [TickerProvider] for the current context. It can be changed by calling [resync].
AnimationController({
double value,
this.duration,
this.debugLabel,
this.lowerBound: 0.0,
this.upperBound: 1.0
this.upperBound: 1.0,
@required TickerProvider vsync,
}) {
assert(upperBound >= lowerBound);
assert(vsync != null);
_direction = _AnimationDirection.forward;
_ticker = new Ticker(_tick);
_ticker = vsync.createTicker(_tick);
_internalSetValue(value ?? lowerBound);
}
/// Creates an animation controller with no upper or lower bound for its value.
///
/// * value is the initial value of the animation.
/// * duration is the length of time this animation should last.
/// * debugLabel is a string to help identify this animation during debugging (used by toString).
/// * [value] is the initial value of the animation.
/// * [duration] is the length of time this animation should last.
/// * [debugLabel] is a string to help identify this animation during debugging (used by [toString]).
/// * `vsync` is the [TickerProvider] for the current context. It can be changed by calling [resync].
///
/// This constructor is most useful for animations that will be driven using a
/// physics simulation, especially when the physics simulation has no
@ -70,12 +75,14 @@ class AnimationController extends Animation<double>
AnimationController.unbounded({
double value: 0.0,
this.duration,
this.debugLabel
this.debugLabel,
@required TickerProvider vsync,
}) : lowerBound = double.NEGATIVE_INFINITY,
upperBound = double.INFINITY {
assert(value != null);
assert(vsync != null);
_direction = _AnimationDirection.forward;
_ticker = new Ticker(_tick);
_ticker = vsync.createTicker(_tick);
_internalSetValue(value);
}
@ -98,6 +105,14 @@ class AnimationController extends Animation<double>
Duration duration;
Ticker _ticker;
/// Recreates the [Ticker] with the new [TickerProvider].
void resync(TickerProvider vsync) {
Ticker oldTicker = _ticker;
_ticker = vsync.createTicker(_tick);
_ticker.absorbTicker(oldTicker);
}
Simulation _simulation;
/// The current value of the animation.
@ -145,7 +160,12 @@ class AnimationController extends Animation<double>
Duration _lastElapsedDuration;
/// Whether this animation is currently animating in either the forward or reverse direction.
bool get isAnimating => _ticker.isTicking;
///
/// This is separate from whether it is actively ticking. An animation
/// controller's ticker might get muted, in which case the animation
/// controller's callbacks will no longer fire even though time is continuing
/// to pass. See [Ticker.muted] and [TickerMode].
bool get isAnimating => _ticker.isActive;
_AnimationDirection _direction;
@ -239,16 +259,21 @@ class AnimationController extends Animation<double>
}
/// Stops running this animation.
///
/// This does not trigger any notifications. The animation stops in its
/// current state.
void stop() {
_simulation = null;
_lastElapsedDuration = null;
_ticker.stop();
}
/// Stops running this animation.
/// Release the resources used by this object. The object is no longer usable
/// after this method is called.
@override
void dispose() {
stop();
_ticker.dispose();
super.dispose();
}
AnimationStatus _lastReportedStatus = AnimationStatus.dismissed;
@ -277,9 +302,10 @@ class AnimationController extends Animation<double>
@override
String toStringDetails() {
String paused = isAnimating ? '' : '; paused';
String silenced = _ticker.muted ? '; silenced' : '';
String label = debugLabel == null ? '' : '; for $debugLabel';
String more = '${super.toStringDetails()} ${value.toStringAsFixed(3)}';
return '$more$paused$label';
return '$more$paused$silenced$label';
}
}

View File

@ -524,6 +524,7 @@ class TrainHoppingAnimation extends Animation<double>
_currentTrain = null;
_nextTrain?.removeListener(_valueChangeHandler);
_nextTrain = null;
super.dispose();
}
@override

View File

@ -4,6 +4,8 @@
import 'dart:ui' show VoidCallback;
import 'package:meta/meta.dart';
import 'animation.dart';
abstract class _ListenerMixin {
@ -50,8 +52,10 @@ abstract class AnimationEagerListenerMixin implements _ListenerMixin {
@override
void didUnregisterListener() { }
/// Release any resources used by this object.
void dispose();
/// Release the resources used by this object. The object is no longer usable
/// after this method is called.
@mustCallSuper
void dispose() { }
}
/// A mixin that implements the addListener/removeListener protocol and notifies

View File

@ -139,7 +139,7 @@ class BottomNavigationBar extends StatefulWidget {
BottomNavigationBarState createState() => new BottomNavigationBarState();
}
class BottomNavigationBarState extends State<BottomNavigationBar> {
class BottomNavigationBarState extends State<BottomNavigationBar> with TickerProviderStateMixin {
List<AnimationController> _controllers;
List<CurvedAnimation> animations;
double _weight;
@ -156,7 +156,8 @@ class BottomNavigationBarState extends State<BottomNavigationBar> {
super.initState();
_controllers = new List<AnimationController>.generate(config.labels.length, (int index) {
return new AnimationController(
duration: kThemeAnimationDuration
duration: kThemeAnimationDuration,
vsync: this,
)..addListener(_rebuild);
});
animations = new List<CurvedAnimation>.generate(config.labels.length, (int index) {
@ -166,9 +167,7 @@ class BottomNavigationBarState extends State<BottomNavigationBar> {
reverseCurve: Curves.fastOutSlowIn.flipped
);
});
_controllers[config.currentIndex].value = 1.0;
_backgroundColor = config.labels[config.currentIndex].backgroundColor;
}
@ -271,7 +270,8 @@ class BottomNavigationBarState extends State<BottomNavigationBar> {
new _Circle(
state: this,
index: index,
color: config.labels[index].backgroundColor
color: config.labels[index].backgroundColor,
vsync: this,
)..controller.addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
setState(() {
@ -289,7 +289,6 @@ class BottomNavigationBarState extends State<BottomNavigationBar> {
if (config.currentIndex != oldConfig.currentIndex) {
if (config.type == BottomNavigationBarType.shifting)
_pushCircle(config.currentIndex);
_controllers[oldConfig.currentIndex].reverse();
_controllers[config.currentIndex].forward();
}
@ -298,7 +297,6 @@ class BottomNavigationBarState extends State<BottomNavigationBar> {
@override
Widget build(BuildContext context) {
Widget bottomNavigation;
switch (config.type) {
case BottomNavigationBarType.fixed:
final List<Widget> children = <Widget>[];
@ -311,7 +309,6 @@ class BottomNavigationBarState extends State<BottomNavigationBar> {
themeData.primaryColor : themeData.accentColor
)
);
for (int i = 0; i < config.labels.length; i += 1) {
children.add(
new Flexible(
@ -329,16 +326,16 @@ class BottomNavigationBarState extends State<BottomNavigationBar> {
margin: new EdgeInsets.only(
top: new Tween<double>(
begin: 8.0,
end: 6.0
).evaluate(animations[i])
end: 6.0,
).evaluate(animations[i]),
),
child: new IconTheme(
data: new IconThemeData(
color: colorTween.evaluate(animations[i])
color: colorTween.evaluate(animations[i]),
),
child: config.labels[i].icon
)
)
child: config.labels[i].icon,
),
),
),
new Align(
alignment: FractionalOffset.bottomCenter,
@ -347,41 +344,36 @@ class BottomNavigationBarState extends State<BottomNavigationBar> {
child: new DefaultTextStyle(
style: new TextStyle(
fontSize: 14.0,
color: colorTween.evaluate(animations[i])
color: colorTween.evaluate(animations[i]),
),
child: new Transform(
transform: new Matrix4.diagonal3(new Vector3.all(
new Tween<double>(
begin: 0.85,
end: 1.0,
).evaluate(animations[i])
).evaluate(animations[i]),
)),
alignment: FractionalOffset.bottomCenter,
child: config.labels[i].title
)
)
)
)
]
)
)
)
child: config.labels[i].title,
),
),
),
),
],
),
),
),
);
}
bottomNavigation = new SizedBox(
width: _maxWidth,
child: new Row(
children: children
)
child: new Row(children: children),
);
break;
case BottomNavigationBarType.shifting:
final List<Widget> children = <Widget>[];
_computeWeight();
for (int i = 0; i < config.labels.length; i += 1) {
children.add(
new Flexible(
@ -402,16 +394,16 @@ class BottomNavigationBarState extends State<BottomNavigationBar> {
margin: new EdgeInsets.only(
top: new Tween<double>(
begin: 18.0,
end: 6.0
).evaluate(animations[i])
end: 6.0,
).evaluate(animations[i]),
),
child: new IconTheme(
data: new IconThemeData(
color: Colors.white
),
child: config.labels[i].icon
)
)
child: config.labels[i].icon,
),
),
),
new Align(
alignment: FractionalOffset.bottomCenter,
@ -425,24 +417,22 @@ class BottomNavigationBarState extends State<BottomNavigationBar> {
color: Colors.white
),
child: config.labels[i].title
)
)
)
)
]
)
)
)
),
),
),
),
],
),
),
),
);
}
bottomNavigation = new SizedBox(
width: _maxWidth,
child: new Row(
children: children
)
);
break;
}
@ -467,20 +457,20 @@ class BottomNavigationBarState extends State<BottomNavigationBar> {
child: new CustomPaint(
painter: new _RadialPainter(
circles: _circles.toList()
)
)
),
),
),
new Material( // Splashes.
type: MaterialType.transparency,
child: new Center(
child: bottomNavigation
)
)
]
)
)
)
]
),
),
],
),
),
),
],
);
}
}
@ -489,20 +479,21 @@ class _Circle {
_Circle({
this.state,
this.index,
this.color
this.color,
@required TickerProvider vsync,
}) {
assert(this.state != null);
assert(this.index != null);
assert(this.color != null);
controller = new AnimationController(
duration: kThemeAnimationDuration
duration: kThemeAnimationDuration,
vsync: vsync,
);
animation = new CurvedAnimation(
parent: controller,
curve: Curves.fastOutSlowIn
);
controller.forward();
}

View File

@ -78,10 +78,11 @@ class BottomSheet extends StatefulWidget {
_BottomSheetState createState() => new _BottomSheetState();
/// Creates an animation controller suitable for controlling a [BottomSheet].
static AnimationController createAnimationController() {
static AnimationController createAnimationController(TickerProvider vsync) {
return new AnimationController(
duration: _kBottomSheetDuration,
debugLabel: 'BottomSheet'
debugLabel: 'BottomSheet',
vsync: vsync,
);
}
}
@ -222,7 +223,7 @@ class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
@override
AnimationController createAnimationController() {
assert(_animationController == null);
_animationController = BottomSheet.createAnimationController();
_animationController = BottomSheet.createAnimationController(navigator.overlay);
return _animationController;
}

View File

@ -30,7 +30,7 @@ import 'toggleable.dart';
/// * [Slider]
/// * <https://www.google.com/design/spec/components/selection-controls.html#selection-controls-checkbox>
/// * <https://www.google.com/design/spec/components/lists-controls.html#lists-controls-types-of-list-controls>
class Checkbox extends StatelessWidget {
class Checkbox extends StatefulWidget {
/// Creates a material design checkbox.
///
/// The checkbox itself does not maintain any state. Instead, when the state of
@ -68,15 +68,21 @@ class Checkbox extends StatelessWidget {
/// The width of a checkbox widget.
static const double width = 18.0;
@override
_CheckboxState createState() => new _CheckboxState();
}
class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
ThemeData themeData = Theme.of(context);
return new _CheckboxRenderObjectWidget(
value: value,
activeColor: activeColor ?? themeData.accentColor,
inactiveColor: onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor,
onChanged: onChanged
value: config.value,
activeColor: config.activeColor ?? themeData.accentColor,
inactiveColor: config.onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor,
onChanged: config.onChanged,
vsync: this,
);
}
}
@ -84,27 +90,31 @@ class Checkbox extends StatelessWidget {
class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
_CheckboxRenderObjectWidget({
Key key,
this.value,
this.activeColor,
this.inactiveColor,
this.onChanged
@required this.value,
@required this.activeColor,
@required this.inactiveColor,
@required this.onChanged,
@required this.vsync,
}) : super(key: key) {
assert(value != null);
assert(activeColor != null);
assert(inactiveColor != null);
assert(vsync != null);
}
final bool value;
final Color activeColor;
final Color inactiveColor;
final ValueChanged<bool> onChanged;
final TickerProvider vsync;
@override
_RenderCheckbox createRenderObject(BuildContext context) => new _RenderCheckbox(
value: value,
activeColor: activeColor,
inactiveColor: inactiveColor,
onChanged: onChanged
onChanged: onChanged,
vsync: vsync,
);
@override
@ -113,7 +123,8 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
..value = value
..activeColor = activeColor
..inactiveColor = inactiveColor
..onChanged = onChanged;
..onChanged = onChanged
..vsync = vsync;
}
}
@ -127,13 +138,15 @@ class _RenderCheckbox extends RenderToggleable {
bool value,
Color activeColor,
Color inactiveColor,
ValueChanged<bool> onChanged
ValueChanged<bool> onChanged,
@required TickerProvider vsync,
}): super(
value: value,
activeColor: activeColor,
inactiveColor: inactiveColor,
onChanged: onChanged,
size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius)
size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius),
vsync: vsync,
);
@override

View File

@ -675,7 +675,7 @@ class _SortArrow extends StatefulWidget {
_SortArrowState createState() => new _SortArrowState();
}
class _SortArrowState extends State<_SortArrow> {
class _SortArrowState extends State<_SortArrow> with TickerProviderStateMixin {
AnimationController _opacityController;
Animation<double> _opacityAnimation;
@ -691,7 +691,8 @@ class _SortArrowState extends State<_SortArrow> {
super.initState();
_opacityAnimation = new CurvedAnimation(
parent: _opacityController = new AnimationController(
duration: config.duration
duration: config.duration,
vsync: this,
),
curve: Curves.fastOutSlowIn
)
@ -702,7 +703,8 @@ class _SortArrowState extends State<_SortArrow> {
end: math.PI
).animate(new CurvedAnimation(
parent: _orientationController = new AnimationController(
duration: config.duration
duration: config.duration,
vsync: this,
),
curve: Curves.easeIn
))

View File

@ -111,11 +111,11 @@ class DrawerController extends StatefulWidget {
/// State for a [DrawerController].
///
/// Typically used by a [Scaffold] to [open] and [close] the drawer.
class DrawerControllerState extends State<DrawerController> {
class DrawerControllerState extends State<DrawerController> with SingleTickerProviderStateMixin {
@override
void initState() {
super.initState();
_controller = new AnimationController(duration: _kBaseSettleDuration)
_controller = new AnimationController(duration: _kBaseSettleDuration, vsync: this)
..addListener(_animationChanged)
..addStatusListener(_animationStatusChanged);
}

View File

@ -60,14 +60,14 @@ class ExpandIcon extends StatefulWidget {
_ExpandIconState createState() => new _ExpandIconState();
}
class _ExpandIconState extends State<ExpandIcon> {
class _ExpandIconState extends State<ExpandIcon> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _iconTurns;
@override
void initState() {
super.initState();
_controller = new AnimationController(duration: kThemeAnimationDuration);
_controller = new AnimationController(duration: kThemeAnimationDuration, vsync: this);
_iconTurns = new Tween<double>(begin: 0.0, end: 0.5).animate(
new CurvedAnimation(
parent: _controller,

View File

@ -6,6 +6,7 @@ import 'dart:math' as math;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';
import 'constants.dart';
@ -219,7 +220,7 @@ class Material extends StatefulWidget {
}
}
class _MaterialState extends State<Material> {
class _MaterialState extends State<Material> with TickerProviderStateMixin {
final GlobalKey _inkFeatureRenderer = new GlobalKey(debugLabel: 'ink renderer');
Color _getBackgroundColor(BuildContext context) {
@ -254,7 +255,8 @@ class _MaterialState extends State<Material> {
child: new _InkFeatures(
key: _inkFeatureRenderer,
color: backgroundColor,
child: contents
child: contents,
vsync: this,
)
);
if (config.type == MaterialType.circle) {
@ -295,7 +297,14 @@ const double _kSplashConfirmedVelocity = 1.0; // logical pixels per millisecond
const double _kSplashInitialSize = 0.0; // logical pixels
class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController {
_RenderInkFeatures({ RenderBox child, this.color }) : super(child);
_RenderInkFeatures({ RenderBox child, @required this.vsync, this.color }) : super(child) {
assert(vsync != null);
}
// This class should exist in a 1:1 relationship with a MaterialState object,
// since there's no current support for dynamically changing the ticker
// provider.
final TickerProvider vsync;
// This is here to satisfy the MaterialInkController contract.
// The actual painting of this color is done by a Container in the
@ -338,7 +347,8 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
targetRadius: radius,
clipCallback: clipCallback,
repositionToReferenceBox: !containedInkWell,
onRemoved: onRemoved
onRemoved: onRemoved,
vsync: vsync,
);
addInkFeature(splash);
return splash;
@ -366,7 +376,8 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
color: color,
shape: shape,
rectCallback: rectCallback,
onRemoved: onRemoved
onRemoved: onRemoved,
vsync: vsync,
);
addInkFeature(highlight);
return highlight;
@ -405,16 +416,27 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
}
class _InkFeatures extends SingleChildRenderObjectWidget {
_InkFeatures({ Key key, this.color, Widget child }) : super(key: key, child: child);
_InkFeatures({ Key key, this.color, Widget child, @required this.vsync }) : super(key: key, child: child);
// This widget must be owned by a MaterialState, which must be provided as the vsync.
// This relationship must be 1:1 and cannot change for the lifetime of the MaterialState.
final Color color;
final TickerProvider vsync;
@override
_RenderInkFeatures createRenderObject(BuildContext context) => new _RenderInkFeatures(color: color);
_RenderInkFeatures createRenderObject(BuildContext context) {
return new _RenderInkFeatures(
color: color,
vsync: vsync
);
}
@override
void updateRenderObject(BuildContext context, _RenderInkFeatures renderObject) {
renderObject.color = color;
assert(vsync == renderObject.vsync);
}
}
@ -488,17 +510,17 @@ class _InkSplash extends InkFeature implements InkSplash {
this.targetRadius,
this.clipCallback,
this.repositionToReferenceBox,
VoidCallback onRemoved
VoidCallback onRemoved,
@required TickerProvider vsync,
}) : super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) {
_radiusController = new AnimationController(duration: _kUnconfirmedSplashDuration)
_radiusController = new AnimationController(duration: _kUnconfirmedSplashDuration, vsync: vsync)
..addListener(controller.markNeedsPaint)
..forward();
_radius = new Tween<double>(
begin: _kSplashInitialSize,
end: targetRadius
).animate(_radiusController);
_alphaController = new AnimationController(duration: _kHighlightFadeDuration)
_alphaController = new AnimationController(duration: _kHighlightFadeDuration, vsync: vsync)
..addListener(controller.markNeedsPaint)
..addStatusListener(_handleAlphaStatusChanged);
_alpha = new IntTween(
@ -578,10 +600,11 @@ class _InkHighlight extends InkFeature implements InkHighlight {
this.rectCallback,
Color color,
this.shape,
VoidCallback onRemoved
VoidCallback onRemoved,
@required TickerProvider vsync,
}) : _color = color,
super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) {
_alphaController = new AnimationController(duration: _kHighlightFadeDuration)
_alphaController = new AnimationController(duration: _kHighlightFadeDuration, vsync: vsync)
..addListener(controller.markNeedsPaint)
..addStatusListener(_handleAlphaStatusChanged)
..forward();

View File

@ -136,7 +136,7 @@ class _AnimationTuple {
double gapStart;
}
class _MergeableMaterialState extends State<MergeableMaterial> {
class _MergeableMaterialState extends State<MergeableMaterial> with TickerProviderStateMixin {
List<MergeableMaterialItem> _children;
final Map<LocalKey, _AnimationTuple> _animationTuples =
<LocalKey, _AnimationTuple>{};
@ -157,7 +157,8 @@ class _MergeableMaterialState extends State<MergeableMaterial> {
void _initGap(MaterialGap gap) {
final AnimationController controller = new AnimationController(
duration: kThemeAnimationDuration
duration: kThemeAnimationDuration,
vsync: this,
);
final CurvedAnimation startAnimation = new CurvedAnimation(

View File

@ -121,13 +121,9 @@ class OverscrollIndicator extends StatefulWidget {
_OverscrollIndicatorState createState() => new _OverscrollIndicatorState();
}
class _OverscrollIndicatorState extends State<OverscrollIndicator> {
final AnimationController _extentAnimation = new AnimationController(
lowerBound: _kMinIndicatorExtent,
upperBound: _kMaxIndicatorExtent,
duration: _kNormalHideDuration
);
class _OverscrollIndicatorState extends State<OverscrollIndicator> with SingleTickerProviderStateMixin {
AnimationController _extentAnimation;
bool _scrollUnderway = false;
Timer _hideTimer;
Axis _scrollDirection;
@ -136,6 +132,17 @@ class _OverscrollIndicatorState extends State<OverscrollIndicator> {
double _maxScrollOffset;
Point _dragPosition;
@override
void initState() {
super.initState();
_extentAnimation = new AnimationController(
lowerBound: _kMinIndicatorExtent,
upperBound: _kMaxIndicatorExtent,
duration: _kNormalHideDuration,
vsync: this,
);
}
void _hide([Duration duration=_kTimeoutHideDuration]) {
_scrollUnderway = false;
_hideTimer?.cancel();

View File

@ -216,7 +216,7 @@ class CheckedPopupMenuItem<T> extends PopupMenuItem<T> {
_CheckedPopupMenuItemState<T> createState() => new _CheckedPopupMenuItemState<T>();
}
class _CheckedPopupMenuItemState<T> extends _PopupMenuItemState<CheckedPopupMenuItem<T>> {
class _CheckedPopupMenuItemState<T> extends _PopupMenuItemState<CheckedPopupMenuItem<T>> with SingleTickerProviderStateMixin {
static const Duration _kFadeDuration = const Duration(milliseconds: 150);
AnimationController _controller;
Animation<double> get _opacity => _controller.view;
@ -224,7 +224,7 @@ class _CheckedPopupMenuItemState<T> extends _PopupMenuItemState<CheckedPopupMenu
@override
void initState() {
super.initState();
_controller = new AnimationController(duration: _kFadeDuration)
_controller = new AnimationController(duration: _kFadeDuration, vsync: this)
..value = config.checked ? 1.0 : 0.0
..addListener(() => setState(() { /* animation changed */ }));
}

View File

@ -146,7 +146,7 @@ class LinearProgressIndicator extends ProgressIndicator {
_LinearProgressIndicatorState createState() => new _LinearProgressIndicatorState();
}
class _LinearProgressIndicatorState extends State<LinearProgressIndicator> {
class _LinearProgressIndicatorState extends State<LinearProgressIndicator> with SingleTickerProviderStateMixin {
Animation<double> _animation;
AnimationController _controller;
@ -154,7 +154,8 @@ class _LinearProgressIndicatorState extends State<LinearProgressIndicator> {
void initState() {
super.initState();
_controller = new AnimationController(
duration: const Duration(milliseconds: 1500)
duration: const Duration(milliseconds: 1500),
vsync: this,
)..repeat();
_animation = new CurvedAnimation(parent: _controller, curve: Curves.fastOutSlowIn);
}
@ -310,13 +311,16 @@ final Animatable<int> _kStepTween = new StepTween(begin: 0, end: 5);
final Animatable<double> _kRotationTween = new CurveTween(curve: new SawTooth(5));
class _CircularProgressIndicatorState extends State<CircularProgressIndicator> {
class _CircularProgressIndicatorState extends State<CircularProgressIndicator> with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = _buildController();
_controller = new AnimationController(
duration: const Duration(milliseconds: 6666),
vsync: this,
)..repeat();
}
@override
@ -325,10 +329,6 @@ class _CircularProgressIndicatorState extends State<CircularProgressIndicator> {
super.dispose();
}
AnimationController _buildController() {
return new AnimationController(duration: const Duration(milliseconds: 6666))..repeat();
}
Widget _buildIndicator(BuildContext context, double headValue, double tailValue, int stepValue, double rotationValue) {
return new Container(
constraints: new BoxConstraints(

View File

@ -35,7 +35,7 @@ const double _kInnerRadius = 5.0;
/// * [Slider]
/// * [Switch]
/// * <https://www.google.com/design/spec/components/selection-controls.html#selection-controls-radio-button>
class Radio<T> extends StatelessWidget {
class Radio<T> extends StatefulWidget {
/// Creates a material design radio button.
///
/// The radio button itself does not maintain any state. Instead, when the state
@ -77,7 +77,12 @@ class Radio<T> extends StatelessWidget {
/// Defaults to accent color of the current [Theme].
final Color activeColor;
bool get _enabled => onChanged != null;
@override
_RadioState<T> createState() => new _RadioState<T>();
}
class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
bool get _enabled => config.onChanged != null;
Color _getInactiveColor(ThemeData themeData) {
return _enabled ? themeData.unselectedWidgetColor : themeData.disabledColor;
@ -85,7 +90,7 @@ class Radio<T> extends StatelessWidget {
void _handleChanged(bool selected) {
if (selected)
onChanged(value);
config.onChanged(config.value);
}
@override
@ -93,12 +98,13 @@ class Radio<T> extends StatelessWidget {
assert(debugCheckHasMaterial(context));
ThemeData themeData = Theme.of(context);
return new Semantics(
checked: value == groupValue,
checked: config.value == config.groupValue,
child: new _RadioRenderObjectWidget(
selected: value == groupValue,
activeColor: activeColor ?? themeData.accentColor,
selected: config.value == config.groupValue,
activeColor: config.activeColor ?? themeData.accentColor,
inactiveColor: _getInactiveColor(themeData),
onChanged: _enabled ? _handleChanged : null
onChanged: _enabled ? _handleChanged : null,
vsync: this,
)
);
}
@ -107,27 +113,31 @@ class Radio<T> extends StatelessWidget {
class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
_RadioRenderObjectWidget({
Key key,
this.selected,
this.activeColor,
this.inactiveColor,
this.onChanged
@required this.selected,
@required this.activeColor,
@required this.inactiveColor,
this.onChanged,
@required this.vsync,
}) : super(key: key) {
assert(selected != null);
assert(activeColor != null);
assert(inactiveColor != null);
assert(vsync != null);
}
final bool selected;
final Color inactiveColor;
final Color activeColor;
final ValueChanged<bool> onChanged;
final TickerProvider vsync;
@override
_RenderRadio createRenderObject(BuildContext context) => new _RenderRadio(
value: selected,
activeColor: activeColor,
inactiveColor: inactiveColor,
onChanged: onChanged
onChanged: onChanged,
vsync: vsync,
);
@override
@ -136,7 +146,8 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
..value = selected
..activeColor = activeColor
..inactiveColor = inactiveColor
..onChanged = onChanged;
..onChanged = onChanged
..vsync = vsync;
}
}
@ -145,13 +156,15 @@ class _RenderRadio extends RenderToggleable {
bool value,
Color activeColor,
Color inactiveColor,
ValueChanged<bool> onChanged
ValueChanged<bool> onChanged,
@required TickerProvider vsync,
}): super(
value: value,
activeColor: activeColor,
inactiveColor: inactiveColor,
onChanged: onChanged,
size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius)
size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius),
vsync: vsync,
);
@override

View File

@ -138,9 +138,9 @@ class RefreshIndicator extends StatefulWidget {
/// Contains the state for a [RefreshIndicator]. This class can be used to
/// programmatically show the refresh indicator, see the [show] method.
class RefreshIndicatorState extends State<RefreshIndicator> {
final AnimationController _sizeController = new AnimationController();
final AnimationController _scaleController = new AnimationController();
class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderStateMixin {
AnimationController _sizeController;
AnimationController _scaleController;
Animation<double> _sizeFactor;
Animation<double> _scaleFactor;
Animation<double> _value;
@ -154,15 +154,16 @@ class RefreshIndicatorState extends State<RefreshIndicator> {
@override
void initState() {
super.initState();
_sizeFactor = new Tween<double>(begin: 0.0, end: _kDragSizeFactorLimit).animate(_sizeController);
_scaleFactor = new Tween<double>(begin: 1.0, end: 0.0).animate(_scaleController);
// The "value" of the circular progress indicator during a drag.
_value = new Tween<double>(
_sizeController = new AnimationController(vsync: this);
_sizeFactor = new Tween<double>(begin: 0.0, end: _kDragSizeFactorLimit).animate(_sizeController);
_value = new Tween<double>( // The "value" of the circular progress indicator during a drag.
begin: 0.0,
end: 0.75
)
.animate(_sizeController);
).animate(_sizeController);
_scaleController = new AnimationController(vsync: this);
_scaleFactor = new Tween<double>(begin: 1.0, end: 0.0).animate(_scaleController);
}
@override

View File

@ -169,10 +169,9 @@ class _FloatingActionButtonTransition extends StatefulWidget {
_FloatingActionButtonTransitionState createState() => new _FloatingActionButtonTransitionState();
}
class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTransition> {
final AnimationController _previousController = new AnimationController(duration: _kFloatingActionButtonSegue);
final AnimationController _currentController = new AnimationController(duration: _kFloatingActionButtonSegue);
class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTransition> with TickerProviderStateMixin {
AnimationController _previousController;
AnimationController _currentController;
CurvedAnimation _previousAnimation;
CurvedAnimation _currentAnimation;
Widget _previousChild;
@ -180,21 +179,29 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
@override
void initState() {
super.initState();
// If we start out with a child, have the child appear fully visible instead
// of animating in.
if (config.child != null)
_currentController.value = 1.0;
_previousController = new AnimationController(
duration: _kFloatingActionButtonSegue,
vsync: this,
)..addStatusListener(_handleAnimationStatusChanged);
_previousAnimation = new CurvedAnimation(
parent: _previousController,
curve: Curves.easeIn
);
_currentController = new AnimationController(
duration: _kFloatingActionButtonSegue,
vsync: this,
);
_currentAnimation = new CurvedAnimation(
parent: _currentController,
curve: Curves.easeIn
);
_previousController.addStatusListener(_handleAnimationStatusChanged);
// If we start out with a child, have the child appear fully visible instead
// of animating in.
if (config.child != null)
_currentController.value = 1.0;
}
@override
@ -357,7 +364,7 @@ class Scaffold extends StatefulWidget {
///
/// Can display [SnackBar]s and [BottomSheet]s. Retrieve a [ScaffoldState] from
/// the current [BuildContext] using [Scaffold.of].
class ScaffoldState extends State<Scaffold> {
class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
static final Object _kScaffoldStorageIdentifier = new Object();
@ -401,7 +408,7 @@ class ScaffoldState extends State<Scaffold> {
/// will be added to a queue and displayed after the earlier snack bars have
/// closed.
ScaffoldFeatureController<SnackBar, Null> showSnackBar(SnackBar snackbar) {
_snackBarController ??= SnackBar.createAnimationController()
_snackBarController ??= SnackBar.createAnimationController(vsync: this)
..addStatusListener(_handleSnackBarStatusChange);
if (_snackBars.isEmpty) {
assert(_snackBarController.isDismissed);
@ -475,7 +482,7 @@ class ScaffoldState extends State<Scaffold> {
// PERSISTENT BOTTOM SHEET API
final List<Widget> _dismissedBottomSheets = <Widget>[];
final List<_PersistentBottomSheet> _dismissedBottomSheets = <_PersistentBottomSheet>[];
PersistentBottomSheetController<dynamic> _currentBottomSheet;
/// Shows a persistent material design bottom sheet.
@ -504,7 +511,7 @@ class ScaffoldState extends State<Scaffold> {
}
Completer<dynamic/*=T*/> completer = new Completer<dynamic/*=T*/>();
GlobalKey<_PersistentBottomSheetState> bottomSheetKey = new GlobalKey<_PersistentBottomSheetState>();
AnimationController controller = BottomSheet.createAnimationController()
AnimationController controller = BottomSheet.createAnimationController(this)
..forward();
_PersistentBottomSheet bottomSheet;
LocalHistoryEntry entry = new LocalHistoryEntry(
@ -554,7 +561,7 @@ class ScaffoldState extends State<Scaffold> {
@override
void initState() {
super.initState();
_appBarController = new AnimationController();
_appBarController = new AnimationController(vsync: this);
// Use an explicit identifier to guard against the possibility that the
// Scaffold's key is recreated by the Widget that creates the Scaffold.
List<double> scrollValues = PageStorage.of(context)?.readState(context,
@ -569,11 +576,15 @@ class ScaffoldState extends State<Scaffold> {
@override
void dispose() {
_appBarController.stop();
_snackBarController?.stop();
_appBarController.dispose();
_snackBarController?.dispose();
_snackBarController = null;
_snackBarTimer?.cancel();
_snackBarTimer = null;
for (_PersistentBottomSheet bottomSheet in _dismissedBottomSheets)
bottomSheet.animationController.dispose();
if (_currentBottomSheet != null)
_currentBottomSheet._widget.animationController.dispose();
PageStorage.of(context)?.writeState(context, <double>[_scrollOffset, _scrollOffsetDelta],
identifier: _kScaffoldStorageIdentifier
);

View File

@ -95,8 +95,8 @@ class Scrollbar extends StatefulWidget {
_ScrollbarState createState() => new _ScrollbarState();
}
class _ScrollbarState extends State<Scrollbar> {
final AnimationController _fade = new AnimationController(duration: _kScrollbarThumbFadeDuration);
class _ScrollbarState extends State<Scrollbar> with SingleTickerProviderStateMixin {
AnimationController _fade;
CurvedAnimation _opacity;
double _scrollOffset;
Axis _scrollDirection;
@ -106,6 +106,7 @@ class _ScrollbarState extends State<Scrollbar> {
@override
void initState() {
super.initState();
_fade = new AnimationController(duration: _kScrollbarThumbFadeDuration, vsync: this);
_opacity = new CurvedAnimation(parent: _fade, curve: Curves.fastOutSlowIn);
}

View File

@ -39,7 +39,7 @@ import 'typography.dart';
/// * [Radio]
/// * [Switch]
/// * <https://www.google.com/design/spec/components/sliders.html>
class Slider extends StatelessWidget {
class Slider extends StatefulWidget {
/// Creates a material design slider.
///
/// The slider itself does not maintain any state. Instead, when the state of
@ -107,20 +107,26 @@ class Slider extends StatelessWidget {
/// Defaults to accent color of the current [Theme].
final Color activeColor;
@override
_SliderState createState() => new _SliderState();
}
class _SliderState extends State<Slider> with TickerProviderStateMixin {
void _handleChanged(double value) {
assert(onChanged != null);
onChanged(value * (max - min) + min);
assert(config.onChanged != null);
config.onChanged(value * (config.max - config.min) + config.min);
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
return new _SliderRenderObjectWidget(
value: (value - min) / (max - min),
divisions: divisions,
label: label,
activeColor: activeColor ?? Theme.of(context).accentColor,
onChanged: onChanged != null ? _handleChanged : null
value: (config.value - config.min) / (config.max - config.min),
divisions: config.divisions,
label: config.label,
activeColor: config.activeColor ?? Theme.of(context).accentColor,
onChanged: config.onChanged != null ? _handleChanged : null,
vsync: this,
);
}
}
@ -132,7 +138,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
this.divisions,
this.label,
this.activeColor,
this.onChanged
this.onChanged,
this.vsync,
}) : super(key: key);
final double value;
@ -140,6 +147,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
final String label;
final Color activeColor;
final ValueChanged<double> onChanged;
final TickerProvider vsync;
@override
_RenderSlider createRenderObject(BuildContext context) => new _RenderSlider(
@ -147,7 +155,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
divisions: divisions,
label: label,
activeColor: activeColor,
onChanged: onChanged
onChanged: onChanged,
vsync: vsync,
);
@override
@ -158,6 +167,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
..label = label
..activeColor = activeColor
..onChanged = onChanged;
// Ticker provider cannot change since there's a 1:1 relationship between
// the _SliderRenderObjectWidget object and the _SliderState object.
}
}
@ -199,7 +210,8 @@ class _RenderSlider extends RenderConstrainedBox implements SemanticActionHandle
int divisions,
String label,
Color activeColor,
this.onChanged
this.onChanged,
TickerProvider vsync,
}) : _value = value,
_divisions = divisions,
_activeColor = activeColor,
@ -210,14 +222,18 @@ class _RenderSlider extends RenderConstrainedBox implements SemanticActionHandle
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd;
_reactionController = new AnimationController(duration: kRadialReactionDuration);
_reactionController = new AnimationController(
duration: kRadialReactionDuration,
vsync: vsync,
);
_reaction = new CurvedAnimation(
parent: _reactionController,
curve: Curves.fastOutSlowIn
)..addListener(markNeedsPaint);
_position = new AnimationController(
value: value,
duration: _kDiscreteTransitionDuration
duration: _kDiscreteTransitionDuration,
vsync: vsync,
)..addListener(markNeedsPaint);
}

View File

@ -204,10 +204,11 @@ class SnackBar extends StatelessWidget {
// API for Scaffold.addSnackBar():
/// Creates an animation controller useful for driving a snack bar's entrance and exit animation.
static AnimationController createAnimationController() {
static AnimationController createAnimationController({ @required TickerProvider vsync }) {
return new AnimationController(
duration: _kSnackBarTransitionDuration,
debugLabel: 'SnackBar'
debugLabel: 'SnackBar',
vsync: vsync,
);
}

View File

@ -176,7 +176,7 @@ class Stepper extends StatefulWidget {
_StepperState createState() => new _StepperState();
}
class _StepperState extends State<Stepper> {
class _StepperState extends State<Stepper> with TickerProviderStateMixin {
List<GlobalKey> _keys;
final Map<int, StepState> _oldStates = new Map<int, StepState>();
@ -608,7 +608,8 @@ class _StepperState extends State<Stepper> {
new AnimatedSize(
curve: Curves.fastOutSlowIn,
duration: kThemeAnimationDuration,
child: config.steps[config.currentStep].content
vsync: this,
child: config.steps[config.currentStep].content,
),
_buildVerticalControls()
]

View File

@ -32,7 +32,7 @@ import 'toggleable.dart';
/// * [Radio]
/// * [Slider]
/// * <https://www.google.com/design/spec/components/selection-controls.html#selection-controls-switch>
class Switch extends StatelessWidget {
class Switch extends StatefulWidget {
/// Creates a material design switch.
///
/// The switch itself does not maintain any state. Instead, when the state of
@ -74,18 +74,31 @@ class Switch extends StatelessWidget {
/// An image to use on the thumb of this switch when the switch is off.
final ImageProvider inactiveThumbImage;
@override
_SwitchState createState() => new _SwitchState();
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('value: ${value ? "on" : "off"}');
if (onChanged == null)
description.add('disabled');
}
}
class _SwitchState extends State<Switch> with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
final ThemeData themeData = Theme.of(context);
final bool isDark = themeData.brightness == Brightness.dark;
final Color activeThumbColor = activeColor ?? themeData.accentColor;
final Color activeThumbColor = config.activeColor ?? themeData.accentColor;
final Color activeTrackColor = activeThumbColor.withAlpha(0x80);
Color inactiveThumbColor;
Color inactiveTrackColor;
if (onChanged != null) {
if (config.onChanged != null) {
inactiveThumbColor = isDark ? Colors.grey[400] : Colors.grey[50];
inactiveTrackColor = isDark ? Colors.white30 : Colors.black26;
} else {
@ -94,25 +107,18 @@ class Switch extends StatelessWidget {
}
return new _SwitchRenderObjectWidget(
value: value,
value: config.value,
activeColor: activeThumbColor,
inactiveColor: inactiveThumbColor,
activeThumbImage: activeThumbImage,
inactiveThumbImage: inactiveThumbImage,
activeThumbImage: config.activeThumbImage,
inactiveThumbImage: config.inactiveThumbImage,
activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor,
configuration: createLocalImageConfiguration(context),
onChanged: onChanged
onChanged: config.onChanged,
vsync: this,
);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('value: ${value ? "on" : "off"}');
if (onChanged == null)
description.add('disabled');
}
}
class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
@ -126,7 +132,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
this.activeTrackColor,
this.inactiveTrackColor,
this.configuration,
this.onChanged
this.onChanged,
this.vsync,
}) : super(key: key);
final bool value;
@ -138,6 +145,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
final Color inactiveTrackColor;
final ImageConfiguration configuration;
final ValueChanged<bool> onChanged;
final TickerProvider vsync;
@override
_RenderSwitch createRenderObject(BuildContext context) => new _RenderSwitch(
@ -149,7 +157,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor,
configuration: configuration,
onChanged: onChanged
onChanged: onChanged,
vsync: vsync,
);
@override
@ -163,7 +172,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
..activeTrackColor = activeTrackColor
..inactiveTrackColor = inactiveTrackColor
..configuration = configuration
..onChanged = onChanged;
..onChanged = onChanged
..vsync = vsync;
}
}
@ -184,7 +194,8 @@ class _RenderSwitch extends RenderToggleable {
Color activeTrackColor,
Color inactiveTrackColor,
ImageConfiguration configuration,
ValueChanged<bool> onChanged
ValueChanged<bool> onChanged,
@required TickerProvider vsync,
}) : _activeThumbImage = activeThumbImage,
_inactiveThumbImage = inactiveThumbImage,
_activeTrackColor = activeTrackColor,
@ -195,7 +206,8 @@ class _RenderSwitch extends RenderToggleable {
activeColor: activeColor,
inactiveColor: inactiveColor,
onChanged: onChanged,
size: const Size(_kSwitchWidth, _kSwitchHeight)
size: const Size(_kSwitchWidth, _kSwitchHeight),
vsync: vsync,
) {
_drag = new HorizontalDragGestureRecognizer()
..onStart = _handleDragStart

View File

@ -523,26 +523,27 @@ class TabBarSelection<T> extends StatefulWidget {
///
/// Subclasses of [TabBarSelection] typically use [State] objects that extend
/// this class.
class TabBarSelectionState<T> extends State<TabBarSelection<T>> {
class TabBarSelectionState<T> extends State<TabBarSelection<T>> with SingleTickerProviderStateMixin {
// Both the TabBar and TabBarView classes access _controller because they
// alternately drive selection progress between tabs.
AnimationController _controller;
/// An animation that updates as the selected tab changes.
Animation<double> get animation => _controller.view;
// Both the TabBar and TabBarView classes access _controller because they
// alternately drive selection progress between tabs.
final AnimationController _controller = new AnimationController(duration: _kTabBarScroll, value: 1.0);
final Map<T, int> _valueToIndex = new Map<T, int>();
void _initValueToIndex() {
_valueToIndex.clear();
int index = 0;
for(T value in values)
_valueToIndex[value] = index++;
}
@override
void initState() {
super.initState();
_controller = new AnimationController(
duration: _kTabBarScroll,
value: 1.0,
vsync: this,
);
_value = config.value ?? PageStorage.of(context)?.readState(context) ?? values.first;
// If the selection's values have changed since the selected value was saved with
@ -561,6 +562,13 @@ class TabBarSelectionState<T> extends State<TabBarSelection<T>> {
_initValueToIndex();
}
void _initValueToIndex() {
_valueToIndex.clear();
int index = 0;
for (T value in values)
_valueToIndex[value] = index++;
}
void _writeValue() {
PageStorage.of(context)?.writeState(context, _value);
}

View File

@ -410,11 +410,14 @@ class _Dial extends StatefulWidget {
_DialState createState() => new _DialState();
}
class _DialState extends State<_Dial> {
class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
@override
void initState() {
super.initState();
_thetaController = new AnimationController(duration: _kDialAnimateDuration);
_thetaController = new AnimationController(
duration: _kDialAnimateDuration,
vsync: this,
);
_thetaTween = new Tween<double>(begin: _getThetaForTime(config.selectedTime));
_theta = _thetaTween.animate(new CurvedAnimation(
parent: _thetaController,

View File

@ -5,6 +5,8 @@
import 'package:flutter/animation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:meta/meta.dart';
import 'constants.dart';
@ -26,38 +28,88 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic
Size size,
Color activeColor,
Color inactiveColor,
ValueChanged<bool> onChanged
ValueChanged<bool> onChanged,
@required TickerProvider vsync,
}) : _value = value,
_activeColor = activeColor,
_inactiveColor = inactiveColor,
_onChanged = onChanged,
_vsync = vsync,
super(additionalConstraints: new BoxConstraints.tight(size)) {
assert(value != null);
assert(activeColor != null);
assert(inactiveColor != null);
assert(vsync != null);
_tap = new TapGestureRecognizer()
..onTapDown = _handleTapDown
..onTap = _handleTap
..onTapUp = _handleTapUp
..onTapCancel = _handleTapCancel;
_positionController = new AnimationController(
duration: _kToggleDuration,
value: _value ? 1.0 : 0.0
value: value ? 1.0 : 0.0,
vsync: vsync,
);
_position = new CurvedAnimation(
parent: _positionController,
curve: Curves.linear
curve: Curves.linear,
)..addListener(markNeedsPaint)
..addStatusListener(_handlePositionStateChanged);
_reactionController = new AnimationController(duration: kRadialReactionDuration);
_reactionController = new AnimationController(
duration: kRadialReactionDuration,
vsync: vsync,
);
_reaction = new CurvedAnimation(
parent: _reactionController,
curve: Curves.fastOutSlowIn
curve: Curves.fastOutSlowIn,
)..addListener(markNeedsPaint);
}
/// Used by subclasses to manipulate the visual value of the control.
///
/// Some controls respond to user input by updating their visual value. For
/// example, the thumb of a switch moves from one position to another when
/// dragged. These controls manipulate this animation controller to update
/// their [position] and eventually trigger an [onChanged] callback when the
/// animation reaches either 0.0 or 1.0.
@protected
AnimationController get positionController => _positionController;
AnimationController _positionController;
/// The visual value of the control.
///
/// When the control is inactive, the [value] is false and this animation has
/// the value 0.0. When the control is active, the value is [true] and this
/// animation has the value 1.0. When the control is changing from inactive
/// to active (or vice versa), [value] is the target value and this animation
/// gradually updates from 0.0 to 1.0 (or vice versa).
CurvedAnimation get position => _position;
CurvedAnimation _position;
/// Used by subclasses to control the radial reaction animation.
///
/// Some controls have a radial ink reaction to user input. This animation
/// controller can be used to start or stop these ink reactions.
///
/// Subclasses should call [paintRadialReaction] to actually paint the radial
/// reaction.
@protected
AnimationController get reactionController => _reactionController;
AnimationController _reactionController;
Animation<double> _reaction;
/// The [TickerProvider] for the [AnimationController]s that run the animations.
TickerProvider get vsync => _vsync;
TickerProvider _vsync;
set vsync(TickerProvider value) {
assert(value != null);
if (value == _vsync)
return;
_vsync = value;
positionController.resync(vsync);
reactionController.resync(vsync);
}
/// Whether this control is current "active" (checked, on, selected) or "inactive" (unchecked, off, not selected).
///
/// When the value changes, this object starts the [positionController] and
@ -138,50 +190,17 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic
/// grey color and its value cannot be changed.
bool get isInteractive => onChanged != null;
/// The visual value of the control.
///
/// When the control is inactive, the [value] is false and this animation has
/// the value 0.0. When the control is active, the value is [true] and this
/// animation has the value 1.0. When the control is changing from inactive
/// to active (or vice versa), [value] is the target value and this animation
/// gradually updates from 0.0 to 1.0 (or vice versa).
CurvedAnimation get position => _position;
CurvedAnimation _position;
/// Used by subclasses to manipulate the visual value of the control.
///
/// Some controls respond to user input by updating their visual value. For
/// example, the thumb of a switch moves from one position to another when
/// dragged. These controls manipulate this animation controller to update
/// their [position] and eventually trigger an [onChanged] callback when the
/// animation reaches either 0.0 or 1.0.
AnimationController get positionController => _positionController;
AnimationController _positionController;
/// Used by subclasses to control the radial reaction animation.
///
/// Some controls have a radial ink reaction to user input. This animation
/// controller can be used to start or stop these ink reactions.
///
/// Subclasses should call [paintRadialReaction] to actually paint the radial
/// reaction.
AnimationController get reactionController => _reactionController;
AnimationController _reactionController;
Animation<double> _reaction;
TapGestureRecognizer _tap;
Point _downPosition;
@override
void attach(PipelineOwner owner) {
super.attach(owner);
if (_positionController != null) {
if (value)
_positionController.forward();
else
_positionController.reverse();
}
if (_reactionController != null && isInteractive) {
if (value)
_positionController.forward();
else
_positionController.reverse();
if (isInteractive) {
switch (_reactionController.status) {
case AnimationStatus.forward:
_reactionController.forward();
@ -199,8 +218,8 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic
@override
void detach() {
_positionController?.stop();
_reactionController?.stop();
_positionController.stop();
_reactionController.stop();
super.detach();
}

View File

@ -93,7 +93,7 @@ class Tooltip extends StatefulWidget {
}
}
class _TooltipState extends State<Tooltip> {
class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
AnimationController _controller;
OverlayEntry _entry;
Timer _timer;
@ -101,7 +101,7 @@ class _TooltipState extends State<Tooltip> {
@override
void initState() {
super.initState();
_controller = new AnimationController(duration: _kFadeDuration)
_controller = new AnimationController(duration: _kFadeDuration, vsync: this)
..addStatusListener(_handleStatusChanged);
}
@ -178,7 +178,7 @@ class _TooltipState extends State<Tooltip> {
void dispose() {
if (_entry != null)
_removeEntry();
_controller.stop();
_controller.dispose();
super.dispose();
}

View File

@ -142,7 +142,7 @@ class TwoLevelSublist extends StatefulWidget {
_TwoLevelSublistState createState() => new _TwoLevelSublistState();
}
class _TwoLevelSublistState extends State<TwoLevelSublist> {
class _TwoLevelSublistState extends State<TwoLevelSublist> with SingleTickerProviderStateMixin {
AnimationController _controller;
CurvedAnimation _easeOutAnimation;
CurvedAnimation _easeInAnimation;
@ -157,7 +157,7 @@ class _TwoLevelSublistState extends State<TwoLevelSublist> {
@override
void initState() {
super.initState();
_controller = new AnimationController(duration: _kExpand);
_controller = new AnimationController(duration: _kExpand, vsync: this);
_easeOutAnimation = new CurvedAnimation(parent: _controller, curve: Curves.easeOut);
_easeInAnimation = new CurvedAnimation(parent: _controller, curve: Curves.easeIn);
_borderColor = new ColorTween(begin: Colors.transparent);
@ -173,7 +173,7 @@ class _TwoLevelSublistState extends State<TwoLevelSublist> {
@override
void dispose() {
_controller.stop();
_controller.dispose();
super.dispose();
}
@ -243,8 +243,8 @@ class _TwoLevelSublistState extends State<TwoLevelSublist> {
..end = config.backgroundColor ?? Colors.transparent;
return new AnimatedBuilder(
animation: _controller.view,
builder: buildList
animation: _controller.view,
builder: buildList
);
}
}

View File

@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/animation.dart';
import 'package:flutter/scheduler.dart';
import 'package:meta/meta.dart';
import 'box.dart';
@ -10,29 +11,41 @@ import 'object.dart';
import 'shifted_box.dart';
/// A render object that animates its size to its child's size over a given
/// [duration] and with a given [curve]. In case the child's size is animating
/// as opposed to abruptly changing size, the parent behaves like a normal
/// container.
/// [duration] and with a given [curve]. If the child's size itself animates
/// (i.e. if it changes size two frames in a row, as opposed to abruptly
/// changing size in one frame then remaining that size in subsequent frames),
/// this render object sizes itself to fit the child instead of animating
/// itself.
///
/// In case the child overflows the current animated size of the parent, it gets
/// clipped automatically.
/// When the child overflows the current animated size of this render object, it
/// is clipped.
class RenderAnimatedSize extends RenderAligningShiftedBox {
/// Creates a render object that animates its size to match its child.
/// The [duration] and [curve] arguments define the animation. The [alignment]
/// argument is used to align the child in the case where the parent is not
/// The [duration] and [curve] arguments define the animation.
///
/// The [alignment] argument is used to align the child when the parent is not
/// (yet) the same size as the child.
///
/// The arguments [duration], [curve], and [alignment] should not be null.
/// The [duration] is required.
///
/// The [vsync] should specify a [TickerProvider] for the animation
/// controller.
///
/// The arguments [duration], [curve], [alignment], and [vsync] must
/// not be null.
RenderAnimatedSize({
@required TickerProvider vsync,
@required Duration duration,
Curve curve: Curves.linear,
RenderBox child,
FractionalOffset alignment: FractionalOffset.center,
@required Duration duration
}) : super(child: child, alignment: alignment) {
RenderBox child,
}) : _vsync = vsync, super(child: child, alignment: alignment) {
assert(vsync != null);
assert(duration != null);
assert(curve != null);
_controller = new AnimationController(
duration: duration
vsync: vsync,
duration: duration,
)..addListener(() {
if (_controller.value != _lastValue)
markNeedsLayout();
@ -68,6 +81,17 @@ class RenderAnimatedSize extends RenderAligningShiftedBox {
_animation.curve = value;
}
/// The [TickerProvider] for the [AnimationController] that runs the animation.
TickerProvider get vsync => _vsync;
TickerProvider _vsync;
set vsync(TickerProvider value) {
assert(value != null);
if (value == _vsync)
return;
_vsync = value;
_controller.resync(vsync);
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
@ -104,8 +128,7 @@ class RenderAnimatedSize extends RenderAligningShiftedBox {
size = child.size;
_controller.stop();
} else {
// Don't register first change (i.e. when _targetSize == _sourceSize)
// as a last-frame change.
// Don't register first change as a last-frame change.
if (_sizeTween.end != _sizeTween.begin)
_didChangeTargetSizeLastFrame = true;

View File

@ -198,7 +198,7 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
/// then invokes post-frame callbacks (registered with [addPostFrameCallback].
///
/// Some bindings (for example, the [WidgetsBinding]) add extra steps to this
/// list.
/// list (for example, see [WidgetsBinding.beginFrame]).
//
// When editing the above, also update widgets/binding.dart's copy.
@protected

View File

@ -86,6 +86,45 @@ class _FrameCallbackEntry {
StackTrace debugStack;
}
/// The various phases that a [SchedulerBinding] goes through during
/// [SchedulerBinding.handleBeginFrame].
///
/// This is exposed by [SchedulerBinding.schedulerPhase].
///
/// The values of this enum are ordered in the same order as the phases occur,
/// so their relative index values can be compared to each other.
///
/// See also [WidgetsBinding.beginFrame].
enum SchedulerPhase {
/// No frame is being processed. Tasks (scheduled by
/// [WidgetsBinding.scheduleTask]), microtasks (scheduled by
/// [scheduleMicrotask]), [Timer] callbacks, event handlers (e.g. from user
/// input), and other callbacks (e.g. from [Future]s, [Stream]s, and the like)
/// may be executing.
idle,
/// The transient callbacks (scheduled by
/// [WidgetsBinding.scheduleFrameCallback] and
/// [WidgetsBinding.addFrameCallback]) are currently executing.
///
/// Typically, these callbacks handle updating objects to new animation states.
transientCallbacks,
/// The persistent callbacks (scheduled by
/// [WidgetsBinding.addPersistentFrameCallback]) are currently executing.
///
/// Typically, this is the build/layout/paint pipeline. See
/// [WidgetsBinding.beginFrame].
persistentCallbacks,
/// The post-frame callbacks (scheduled by
/// [WidgetsBinding.addPostFrameCallback]) are currently executing.
///
/// Typically, these callbacks handle cleanup and scheduling of work for the
/// next frame.
postFrameCallbacks,
}
/// Scheduler for running the following:
///
/// * _Frame callbacks_, triggered by the system's
@ -402,9 +441,9 @@ abstract class SchedulerBinding extends BindingBase {
bool get hasScheduledFrame => _hasScheduledFrame;
bool _hasScheduledFrame = false;
/// Whether this scheduler is currently producing a frame in [handleBeginFrame].
bool get isProducingFrame => _isProducingFrame;
bool _isProducingFrame = false;
/// The phase that the scheduler is currently operating under.
SchedulerPhase get schedulerPhase => _schedulerPhase;
SchedulerPhase _schedulerPhase = SchedulerPhase.idle;
/// Schedules a new frame using [scheduleFrame] if this object is not
/// currently producing a frame.
@ -412,7 +451,7 @@ abstract class SchedulerBinding extends BindingBase {
/// After this is called, the framework ensures that the end of the
/// [handleBeginFrame] function will (eventually) be reached.
void ensureVisualUpdate() {
if (_isProducingFrame)
if (schedulerPhase != SchedulerPhase.idle)
return;
scheduleFrame();
}
@ -530,20 +569,21 @@ abstract class SchedulerBinding extends BindingBase {
return true;
});
assert(!_isProducingFrame);
_isProducingFrame = true;
assert(schedulerPhase == SchedulerPhase.idle);
_hasScheduledFrame = false;
try {
// TRANSIENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.transientCallbacks;
_invokeTransientFrameCallbacks(_currentFrameTimeStamp);
// PERSISTENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
_isProducingFrame = false;
// POST-FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.postFrameCallbacks;
List<FrameCallback> localPostFrameCallbacks =
new List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
@ -551,7 +591,7 @@ abstract class SchedulerBinding extends BindingBase {
_invokeFrameCallback(callback, _currentFrameTimeStamp);
} finally {
_isProducingFrame = false; // just in case we throw before setting it above
_schedulerPhase = SchedulerPhase.idle;
_currentFrameTimeStamp = null;
Timeline.finishSync();
assert(() {
@ -567,7 +607,7 @@ abstract class SchedulerBinding extends BindingBase {
void _invokeTransientFrameCallbacks(Duration timeStamp) {
Timeline.startSync('Animate');
assert(_isProducingFrame);
assert(schedulerPhase == SchedulerPhase.transientCallbacks);
Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
_transientCallbacks = new Map<int, _FrameCallbackEntry>();
callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {

View File

@ -4,6 +4,9 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart';
import 'binding.dart';
/// Signature for the [onTick] constructor argument of the [Ticker] class.
@ -12,35 +15,112 @@ import 'binding.dart';
/// at the time of the callback being called.
typedef void TickerCallback(Duration elapsed);
/// An interface implemented by classes that can vend [Ticker] objects.
abstract class TickerProvider {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const TickerProvider();
/// Creates a ticker with the given callback.
///
/// The kind of ticker provided depends on the kind of ticker provider.
Ticker createTicker(TickerCallback onTick);
}
/// Calls its callback once per animation frame.
///
/// When created, a ticker is initially disabled. Call [start] to
/// enable the ticker.
///
/// See also [SchedulerBinding.scheduleFrameCallback].
/// A [Ticker] can be silenced by setting [muted] to true. While silenced, time
/// still elapses, and [start] and [stop] can still be called, but no callbacks
/// are called.
///
/// Tickers are driven by the [SchedulerBinding]. See
/// [SchedulerBinding.scheduleFrameCallback].
class Ticker {
/// Creates a ticker that will call [onTick] once per frame while running.
Ticker(TickerCallback onTick) : _onTick = onTick;
final TickerCallback _onTick;
///
/// An optional label can be provided for debugging purposes. That label
/// will appear in the [toString] output in debug builds.
Ticker(this._onTick, { this.debugLabel }) {
assert(() {
_debugCreationStack = StackTrace.current;
return true;
});
}
Completer<Null> _completer;
int _animationId;
Duration _startTime;
/// Whether this ticker has been silenced.
///
/// While silenced, a ticker's clock can still run, but the callback will not
/// be called.
bool get muted => _muted;
bool _muted = false;
/// When set to true, silences the ticker, so that it is no longer ticking. If
/// a tick is already scheduled, it will unschedule it. This will not
/// unschedule the next frame, though.
///
/// When set to false, unsilences the ticker, potentially scheduling a frame
/// to handle the next tick.
set muted(bool value) {
if (value == muted)
return;
_muted = value;
if (value) {
unscheduleTick();
} else if (shouldScheduleTick) {
scheduleTick();
}
}
/// Whether this ticker has scheduled a call to call its callback
/// on the next frame.
bool get isTicking => _completer != null;
/// Starts calling the ticker's callback once per animation frame.
///
/// The returned future resolves once the ticker stops ticking.
/// A ticker that is [muted] can be active (see [isActive]) yet not be
/// ticking. In that case, the ticker will not call its callback, and
/// [isTicking] will be false, but time will still be progressing.
// TODO(ianh): we should teach the scheduler binding about the lifecycle events
// and then this could return an accurate view of the actual scheduler.
bool get isTicking => _completer != null && !muted;
/// Whether time is elapsing for this ticker. Becomes true when [start] is
/// called and false when [stop] is called.
///
/// A ticker can be active yet not be actually ticking (i.e. not be calling
/// the callback). To determine if a ticker is actually ticking, use
/// [isTicking].
bool get isActive => _completer != null;
Duration _startTime;
/// Starts the clock for this ticker. If the ticker is not [muted], then this
/// also starts calling the ticker's callback once per animation frame.
///
/// The returned future resolves once the ticker [stop]s ticking.
///
/// Calling this sets [isActive] to true.
///
/// This method cannot be called while the ticker is active. To restart the
/// ticker, first [stop] it.
Future<Null> start() {
assert(!isTicking);
assert(() {
if (isTicking) {
throw new FlutterError(
'A ticker was started twice.\n'
'A ticker that is already active cannot be started again without first stopping it.\n'
'The affected ticker was: ${ this.toString(debugIncludeStack: true) }'
);
}
return true;
});
assert(_startTime == null);
_completer = new Completer<Null>();
_scheduleTick();
if (SchedulerBinding.instance.isProducingFrame)
if (shouldScheduleTick)
scheduleTick();
if (SchedulerBinding.instance.schedulerPhase.index > SchedulerPhase.idle.index &&
SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index)
_startTime = SchedulerBinding.instance.currentFrameTimeStamp;
return _completer.future;
}
@ -48,29 +128,49 @@ class Ticker {
/// Stops calling the ticker's callback.
///
/// Causes the future returned by [start] to resolve.
///
/// Calling this sets [isActive] to false.
///
/// This method does nothing if called when the ticker is inactive.
void stop() {
if (!isTicking)
return;
_startTime = null;
if (_animationId != null) {
SchedulerBinding.instance.cancelFrameCallbackWithId(_animationId);
_animationId = null;
}
// We take the _completer into a local variable so that isTicking is false
// when we actually complete the future (isTicking uses _completer
// to determine its state).
// when we actually complete the future (isTicking uses _completer to
// determine its state).
Completer<Null> localCompleter = _completer;
_completer = null;
_startTime = null;
assert(!isTicking);
unscheduleTick();
localCompleter.complete();
}
final TickerCallback _onTick;
int _animationId;
/// Whether this ticker has already scheduled a frame callback.
@protected
bool get scheduled => _animationId != null;
/// Whether a tick should be scheduled.
///
/// If this is true, then calling [scheduleTick] should succeed.
///
/// Reasons why a tick should not be scheduled include:
///
/// * A tick has already been scheduled for the coming frame.
/// * The ticker is not active ([start] has not been called).
/// * The ticker is not ticking, e.g. because it is [muted] (see [isTicking]).
bool get shouldScheduleTick => isTicking && !scheduled;
void _tick(Duration timeStamp) {
assert(isTicking);
assert(_animationId != null);
assert(scheduled);
_animationId = null;
if (_startTime == null)
@ -78,14 +178,90 @@ class Ticker {
_onTick(timeStamp - _startTime);
// The onTick callback may have scheduled another tick already.
if (isTicking && _animationId == null)
_scheduleTick(rescheduling: true);
// The onTick callback may have scheduled another tick already, for
// example by calling stop then start again.
if (shouldScheduleTick)
scheduleTick(rescheduling: true);
}
void _scheduleTick({ bool rescheduling: false }) {
/// Schedules a tick for the next frame.
///
/// This should only be called if [shouldScheduleTick] is true.
@protected
void scheduleTick({ bool rescheduling: false }) {
assert(isTicking);
assert(_animationId == null);
assert(!scheduled);
assert(shouldScheduleTick);
_animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
}
/// Cancels the frame callback that was requested by [scheduleTick], if any.
///
/// Calling this method when no tick is [scheduled] is harmless.
///
/// This method should not be called when [shouldScheduleTick] would return
/// true if no tick was scheduled.
@protected
void unscheduleTick() {
if (scheduled) {
SchedulerBinding.instance.cancelFrameCallbackWithId(_animationId);
_animationId = null;
}
assert(!shouldScheduleTick);
}
/// Makes this ticker take the state of another ticker, and disposes the other
/// ticker.
///
/// This is useful if an object with a [Ticker] is given a new
/// [TickerProvider] but needs to maintain continuity. In particular, this
/// maintains the identity of the [Future] returned by the [start] function of
/// the original [Ticker] if the original ticker is active.
///
/// This ticker must not be active when this method is called.
void absorbTicker(Ticker originalTicker) {
assert(!isTicking);
assert(_completer == null);
assert(_startTime == null);
assert(_animationId == null);
_completer = originalTicker._completer;
_startTime = originalTicker._startTime;
if (shouldScheduleTick)
scheduleTick();
originalTicker.dispose();
}
/// Release the resources used by this object. The object is no longer usable
/// after this method is called.
@mustCallSuper
void dispose() {
_completer = null;
// We intentionally don't null out _startTime. This means that if start()
// was ever called, the object is now in a bogus state. This weakly helps
// catch cases of use-after-dispose.
unscheduleTick();
}
final String debugLabel;
StackTrace _debugCreationStack;
@override
String toString({ bool debugIncludeStack: false }) {
final StringBuffer buffer = new StringBuffer();
buffer.write('$runtimeType(');
assert(() {
buffer.write(debugLabel ?? '');
return true;
});
buffer.write(')');
assert(() {
if (debugIncludeStack) {
buffer.writeln();
buffer.writeln('The stack trace when the $runtimeType was actually created was:');
FlutterError.defaultStackFilter(_debugCreationStack.toString().trimRight().split('\n')).forEach(buffer.writeln);
}
return true;
});
return buffer.toString();
}
}

View File

@ -8,6 +8,7 @@ import 'package:meta/meta.dart';
import 'animated_size.dart';
import 'basic.dart';
import 'framework.dart';
import 'ticker_provider.dart';
import 'transitions.dart';
/// Specifies which of the children to show. See [AnimatedCrossFade].
@ -80,7 +81,7 @@ class AnimatedCrossFade extends StatefulWidget {
_AnimatedCrossFadeState createState() => new _AnimatedCrossFadeState();
}
class _AnimatedCrossFadeState extends State<AnimatedCrossFade> {
class _AnimatedCrossFadeState extends State<AnimatedCrossFade> with TickerProviderStateMixin {
_AnimatedCrossFadeState() : super();
AnimationController _controller;
@ -90,7 +91,7 @@ class _AnimatedCrossFadeState extends State<AnimatedCrossFade> {
@override
void initState() {
super.initState();
_controller = new AnimationController(duration: config.duration);
_controller = new AnimationController(duration: config.duration, vsync: this);
if (config.crossFadeState == CrossFadeState.showSecond)
_controller.value = 1.0;
_firstAnimation = _initAnimation(config.firstCurve, true);
@ -185,6 +186,7 @@ class _AnimatedCrossFadeState extends State<AnimatedCrossFade> {
alignment: FractionalOffset.topCenter,
duration: config.duration,
curve: config.sizeCurve,
vsync: this,
child: new Stack(
overflow: Overflow.visible,
children: children

View File

@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:meta/meta.dart';
import 'basic.dart';
@ -19,7 +20,8 @@ class AnimatedSize extends SingleChildRenderObjectWidget {
Widget child,
this.alignment: FractionalOffset.center,
this.curve: Curves.linear,
@required this.duration
@required this.duration,
@required this.vsync,
}) : super(key: key, child: child);
/// The alignment of the child within the parent when the parent is not yet
@ -42,12 +44,16 @@ class AnimatedSize extends SingleChildRenderObjectWidget {
/// size.
final Duration duration;
/// The [TickerProvider] for this widget.
final TickerProvider vsync;
@override
RenderAnimatedSize createRenderObject(BuildContext context) {
return new RenderAnimatedSize(
alignment: alignment,
duration: duration,
curve: curve
curve: curve,
vsync: vsync,
);
}
@ -57,6 +63,7 @@ class AnimatedSize extends SingleChildRenderObjectWidget {
renderObject
..alignment = alignment
..duration = duration
..curve = curve;
..curve = curve
..vsync = vsync;
}
}

View File

@ -5,9 +5,10 @@
import 'package:meta/meta.dart';
import 'basic.dart';
import 'transitions.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'ticker_provider.dart';
import 'transitions.dart';
const Duration _kDismissDuration = const Duration(milliseconds: 200);
const Curve _kResizeTimeCurve = const Interval(0.4, 1.0, curve: Curves.ease);
@ -113,11 +114,11 @@ class Dismissable extends StatefulWidget {
_DismissableState createState() => new _DismissableState();
}
class _DismissableState extends State<Dismissable> {
class _DismissableState extends State<Dismissable> with TickerProviderStateMixin {
@override
void initState() {
super.initState();
_moveController = new AnimationController(duration: _kDismissDuration)
_moveController = new AnimationController(duration: _kDismissDuration, vsync: this)
..addStatusListener(_handleDismissStatusChanged);
_updateMoveAnimation();
}
@ -278,7 +279,7 @@ class _DismissableState extends State<Dismissable> {
if (config.onDismissed != null)
config.onDismissed(_dismissDirection);
} else {
_resizeController = new AnimationController(duration: config.resizeDuration)
_resizeController = new AnimationController(duration: config.resizeDuration, vsync: this)
..addListener(_handleResizeProgressChanged);
_resizeController.forward();
setState(() {

View File

@ -405,9 +405,7 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
_keyboardHandle.release();
if (_cursorTimer != null)
_stopCursorTimer();
scheduleMicrotask(() { // can't hide while disposing, since it triggers a rebuild
_selectionOverlay?.dispose();
});
_selectionOverlay?.dispose();
super.dispose();
}
@ -432,14 +430,12 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
_stopCursorTimer();
if (_selectionOverlay != null) {
scheduleMicrotask(() { // can't update while disposing, since it triggers a rebuild
if (focused) {
_selectionOverlay.update(config.value);
} else {
_selectionOverlay?.hide();
_selectionOverlay = null;
}
});
if (focused) {
_selectionOverlay.update(config.value);
} else {
_selectionOverlay?.dispose();
_selectionOverlay = null;
}
}
return new _EditableLineWidget(

View File

@ -1207,21 +1207,23 @@ class _InactiveElements {
}
return true;
});
element.unmount();
assert(element._debugLifecycleState == _ElementLifecycle.defunct);
element.visitChildren((Element child) {
assert(child._parent == element);
_unmount(child);
});
element.unmount();
assert(element._debugLifecycleState == _ElementLifecycle.defunct);
}
void _unmountAll() {
_locked = true;
final List<Element> elements = _elements.toList()..sort(Element._sort);
_elements.clear();
try {
_locked = true;
for (Element element in _elements)
for (Element element in elements.reversed)
_unmount(element);
} finally {
_elements.clear();
assert(_elements.isEmpty);
_locked = false;
}
}
@ -1365,7 +1367,8 @@ abstract class BuildContext {
/// [State.initState] methods, because those methods would not get called
/// again if the inherited value were to change. To ensure that the widget
/// correctly updates itself when the inherited value changes, only call this
/// (directly or indirectly) from build methods or layout and paint callbacks.
/// (directly or indirectly) from build methods, layout and paint callbacks, or
/// from [State.dependenciesChanged].
///
/// It is also possible to call this from interaction event handlers (e.g.
/// gesture callbacks) or timers, to obtain a value once, if that value is not
@ -1373,6 +1376,12 @@ abstract class BuildContext {
///
/// Calling this method is O(1) with a small constant factor, but will lead to
/// the widget being rebuilt more often.
///
/// Once a widget registers a dependency on a particular type by calling this
/// method, it will be rebuilt, and [State.dependenciesChanged] will be
/// called, whenever changes occur relating to that widget until the next time
/// the widget or one of its ancestors is moved (for example, because an
/// ancestor is added or removed).
InheritedWidget inheritFromWidgetOfExactType(Type targetType);
/// Returns the nearest ancestor widget of the given type, which must be the
@ -1647,7 +1656,7 @@ class BuildOwner {
});
}
}
_dirtyElements.sort(_elementSort);
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
int dirtyCount = _dirtyElements.length;
int index = 0;
@ -1668,7 +1677,7 @@ class BuildOwner {
}
index += 1;
if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) {
_dirtyElements.sort(_elementSort);
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
dirtyCount = _dirtyElements.length;
while (index > 0 && _dirtyElements[index - 1].dirty) {
@ -1715,18 +1724,6 @@ class BuildOwner {
assert(_debugStateLockLevel >= 0);
}
static int _elementSort(BuildableElement a, BuildableElement b) {
if (a.depth < b.depth)
return -1;
if (b.depth < a.depth)
return 1;
if (b.dirty && !a.dirty)
return -1;
if (a.dirty && !b.dirty)
return 1;
return 0;
}
/// Complete the element build pass by unmounting any elements that are no
/// longer active.
///
@ -1837,6 +1834,18 @@ abstract class Element implements BuildContext {
int get depth => _depth;
int _depth;
static int _sort(BuildableElement a, BuildableElement b) {
if (a.depth < b.depth)
return -1;
if (b.depth < a.depth)
return 1;
if (b.dirty && !a.dirty)
return -1;
if (a.dirty && !b.dirty)
return 1;
return 0;
}
/// The configuration for this element.
@override
Widget get widget => _widget;
@ -2206,6 +2215,7 @@ abstract class Element implements BuildContext {
// We unregistered our dependencies in deactivate, but never cleared the list.
// Since we're going to be reused, let's clear our list now.
_dependencies?.clear();
_hadUnsatisfiedDependencies = false;
_updateInheritance();
assert(() { _debugLifecycleState = _ElementLifecycle.active; return true; });
}
@ -2554,19 +2564,6 @@ abstract class BuildableElement extends Element {
owner._debugCurrentBuildTarget = this;
return true;
});
_hadUnsatisfiedDependencies = false;
// In theory, we would also clear our actual _dependencies here. However, to
// clear it we'd have to notify each of them, unregister from them, and then
// reregister as soon as the build function re-dependended on it. So to
// avoid faffing around we just never unregister our dependencies except
// when we're deactivated. In principle this means we might be getting
// notified about widget types we once inherited from but no longer do, but
// in practice this is so rare that the extra cost when it does happen is
// far outweighed by the avoided work in the common case.
// We _do_ clear the list properly any time our ancestor chain changes in a
// way that might result in us getting a different Element's Widget for a
// particular Type. This avoids the potential of being registered to
// multiple identically-typed Widgets' Elements at the same time.
performRebuild();
assert(() {
assert(owner._debugCurrentBuildTarget == this);

View File

@ -9,6 +9,7 @@ import 'basic.dart';
import 'container.dart';
import 'framework.dart';
import 'text.dart';
import 'ticker_provider.dart';
/// An interpolation between two [BoxConstraint]s.
class BoxConstraintsTween extends Tween<BoxConstraints> {
@ -114,7 +115,7 @@ typedef Tween<T> TweenConstructor<T>(T targetValue);
typedef Tween<T> TweenVisitor<T>(Tween<T> tween, T targetValue, TweenConstructor<T> constructor);
/// A base class for widgets with implicit animations.
abstract class AnimatedWidgetBaseState<T extends ImplicitlyAnimatedWidget> extends State<T> {
abstract class AnimatedWidgetBaseState<T extends ImplicitlyAnimatedWidget> extends State<T> with SingleTickerProviderStateMixin {
AnimationController _controller;
/// The animation driving this widget's implicit animations.
@ -126,7 +127,8 @@ abstract class AnimatedWidgetBaseState<T extends ImplicitlyAnimatedWidget> exten
super.initState();
_controller = new AnimationController(
duration: config.duration,
debugLabel: '${config.toStringShort()}'
debugLabel: '${config.toStringShort()}',
vsync: this,
)..addListener(_handleAnimationChanged);
_updateCurve();
_constructTweens();

View File

@ -28,14 +28,16 @@ class MimicableHandle {
/// An overlay entry that is mimicking another widget.
class MimicOverlayEntry {
MimicOverlayEntry._(this._handle) {
_overlayEntry = new OverlayEntry(builder: _build);
MimicOverlayEntry._(this._handle, this._overlay) {
_initialGlobalBounds = _handle.globalBounds;
_overlayEntry = new OverlayEntry(builder: _build);
_overlay.insert(_overlayEntry);
}
Rect _initialGlobalBounds;
MimicableHandle _handle;
OverlayState _overlay;
OverlayEntry _overlayEntry;
// Animation state
@ -63,7 +65,8 @@ class MimicOverlayEntry {
_curve = curve;
// TODO(abarth): Support changing the animation target when in flight.
assert(_controller == null);
_controller = new AnimationController(duration: duration)
// TODO(ianh): Need to get a TickerProvider that's tied to the Overlay's TickerMode.
_controller = new AnimationController(duration: duration, vsync: _overlay)
..addListener(_overlayEntry.markNeedsBuild);
return _controller.forward();
}
@ -214,9 +217,7 @@ class MimicableState extends State<Mimicable> {
/// placed in the enclosing overlay.
MimicOverlayEntry liftToOverlay() {
OverlayState overlay = Overlay.of(context, debugRequiredFor: config);
MimicOverlayEntry entry = new MimicOverlayEntry._(startMimic());
overlay.insert(entry._overlayEntry);
return entry;
return new MimicOverlayEntry._(startMimic(), overlay);
}
void _stopMimic() {

View File

@ -10,6 +10,7 @@ import 'binding.dart';
import 'focus.dart';
import 'framework.dart';
import 'overlay.dart';
import 'ticker_provider.dart';
/// An abstraction for an entry managed by a [Navigator].
///
@ -325,7 +326,7 @@ class Navigator extends StatefulWidget {
}
/// The state for a [Navigator] widget.
class NavigatorState extends State<Navigator> {
class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
final GlobalKey<OverlayState> _overlayKey = new GlobalKey<OverlayState>();
final List<Route<dynamic>> _history = new List<Route<dynamic>>();

View File

@ -2,14 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:collection';
import 'package:meta/meta.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:meta/meta.dart';
import 'basic.dart';
import 'debug.dart';
import 'framework.dart';
import 'ticker_provider.dart';
/// A place in an [Overlay] that can contain a widget.
///
@ -116,9 +119,27 @@ class OverlayEntry {
final GlobalKey<_OverlayEntryState> _key = new GlobalKey<_OverlayEntryState>();
/// Remove this entry from the overlay.
///
/// This should only be called once.
///
/// If this method is called while the [SchedulerBinding.schedulerPhase] is
/// [SchedulerBinding.persistentCallbacks], i.e. during the build, layout, or
/// paint phases (see [WidgetsBinding.beginFrame]), then the removal is
/// delayed until the post-frame callbacks phase. Otherwise the removal is
/// done synchronously. This means that it is safe to call during builds, but
/// also that if you do call this during a build, the UI will not update until
/// the next frame (i.e. many milliseconds later).
void remove() {
_overlay?._remove(this);
assert(_overlay != null);
OverlayState overlay = _overlay;
_overlay = null;
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
overlay._remove(this);
});
} else {
overlay._remove(this);
}
}
/// Cause this entry to rebuild during the next pipeline flush.
@ -226,7 +247,7 @@ class Overlay extends StatefulWidget {
///
/// Used to insert [OverlayEntry]s into the overlay using the [insert] and
/// [insertAll] functions.
class OverlayState extends State<Overlay> {
class OverlayState extends State<Overlay> with TickerProviderStateMixin {
final List<OverlayEntry> _entries = new List<OverlayEntry>();
@override
@ -268,9 +289,10 @@ class OverlayState extends State<Overlay> {
}
void _remove(OverlayEntry entry) {
_entries.remove(entry);
if (mounted)
if (mounted) {
_entries.remove(entry);
setState(() { /* entry was removed */ });
}
}
/// (DEBUG ONLY) Check whether a given entry is visible (i.e., not behind an
@ -319,7 +341,7 @@ class OverlayState extends State<Overlay> {
if (entry.opaque)
onstage = false;
} else if (entry.maintainState) {
offstageChildren.add(new _OverlayEntry(entry));
offstageChildren.add(new TickerMode(enabled: false, child: new _OverlayEntry(entry)));
}
}
return new _Theatre(

View File

@ -122,7 +122,11 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
AnimationController createAnimationController() {
Duration duration = transitionDuration;
assert(duration != null && duration >= Duration.ZERO);
return new AnimationController(duration: duration, debugLabel: debugLabel);
return new AnimationController(
duration: duration,
debugLabel: debugLabel,
vsync: navigator,
);
}
/// Called to create the animation that exposes the current progress of

View File

@ -18,6 +18,7 @@ import 'notification_listener.dart';
import 'page_storage.dart';
import 'scroll_behavior.dart';
import 'scroll_configuration.dart';
import 'ticker_provider.dart';
/// Identifies one or both limits of a [Scrollable] in terms of its scrollDirection.
enum ScrollableEdge {
@ -262,11 +263,11 @@ class Scrollable extends StatefulWidget {
/// terms of the [pixelOffsetToScrollOffset] and
/// [scrollOffsetToPixelOffset] methods.
@optionalTypeArgs
class ScrollableState<T extends Scrollable> extends State<T> {
class ScrollableState<T extends Scrollable> extends State<T> with SingleTickerProviderStateMixin {
@override
void initState() {
super.initState();
_controller = new AnimationController.unbounded()
_controller = new AnimationController.unbounded(vsync: this)
..addListener(_handleAnimationChanged)
..addStatusListener(_handleAnimationStatusChanged);
_scrollOffset = PageStorage.of(context)?.readState(context) ?? config.initialScrollOffset ?? 0.0;

View File

@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:meta/meta.dart';
import 'basic.dart';
@ -82,6 +83,10 @@ class TextSelectionOverlay implements TextSelectionDelegate {
this.toolbarBuilder
}): _input = input {
assert(context != null);
final OverlayState overlay = Overlay.of(context);
assert(overlay != null);
_handleController = new AnimationController(duration: _kFadeDuration, vsync: overlay);
_toolbarController = new AnimationController(duration: _kFadeDuration, vsync: overlay);
}
/// The context in which the selection handles should appear.
@ -117,8 +122,8 @@ class TextSelectionOverlay implements TextSelectionDelegate {
/// Controls the fade-in animations.
static const Duration _kFadeDuration = const Duration(milliseconds: 150);
final AnimationController _handleController = new AnimationController(duration: _kFadeDuration);
final AnimationController _toolbarController = new AnimationController(duration: _kFadeDuration);
AnimationController _handleController;
AnimationController _toolbarController;
Animation<double> get _handleOpacity => _handleController.view;
Animation<double> get _toolbarOpacity => _toolbarController.view;
@ -153,11 +158,26 @@ class TextSelectionOverlay implements TextSelectionDelegate {
}
/// Updates the overlay after the [selection] has changed.
///
/// If this method is called while the [SchedulerBinding.schedulerPhase] is
/// [SchedulerBinding.persistentCallbacks], i.e. during the build, layout, or
/// paint phases (see [WidgetsBinding.beginFrame]), then the update is delayed
/// until the post-frame callbacks phase. Otherwise the update is done
/// synchronously. This means that it is safe to call during builds, but also
/// that if you do call this during a build, the UI will not update until the
/// next frame (i.e. many milliseconds later).
void update(InputValue newInput) {
if (_input == newInput)
return;
_input = newInput;
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback(_markNeedsBuild);
} else {
_markNeedsBuild();
}
}
void _markNeedsBuild([Duration duration]) {
if (_handles != null) {
_handles[0].markNeedsBuild();
_handles[1].markNeedsBuild();

View File

@ -0,0 +1,220 @@
// Copyright 2016 The Chromium 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/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:meta/meta.dart';
import 'framework.dart';
export 'package:flutter/scheduler.dart' show TickerProvider;
/// Enables or disables tickers (and thus animation controllers) in the widget
/// subtree.
///
/// This only works if [AnimationController] objects are created using
/// widget-aware ticker providers. For example, using a
/// [TickerProviderStateMixin] or a [SingleTickerProviderStateMixin].
class TickerMode extends InheritedWidget {
/// Creates a widget that enables or disables tickers.
///
/// The [enabled] argument must not be null.
TickerMode({
Key key,
@required this.enabled,
Widget child
}) : super(key: key, child: child) {
assert(enabled != null);
}
/// The current ticker mode of this subtree.
///
/// If true, then tickers in this subtree will tick.
///
/// If false, then tickers in this subtree will not tick. Animations driven by
/// such tickers are not paused, they just don't call their callbacks. Time
/// still elapses.
final bool enabled;
/// Whether tickers in the given subtree should be enabled or disabled.
///
/// This is used automatically by [TickerProviderStateMixin] and
/// [SingleTickerProviderStateMixin] to decide if their tickers should be
/// enabled or disabled.
///
/// In the absence of a [TickerMode] widget, this function defaults to true.
static bool of(BuildContext context) {
TickerMode widget = context.inheritFromWidgetOfExactType(TickerMode);
return widget?.enabled ?? true;
}
@override
bool updateShouldNotify(TickerMode old) => enabled != old.enabled;
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('mode: ${ enabled ? "enabled" : "disabled" }');
}
}
/// Provides a single [Ticker] that is configured to only tick while the current
/// tree is enabled, as defined by [TickerMode].
///
/// To create the [AnimationController] in a [State] that only uses a single
/// [AnimationController], mix in this class, then pass `vsync: this`
/// to the animation controller constructor.
///
/// This mixin only supports vending a single ticker. If you might have multiple
/// [AnimationController] objects over the lifetime of the [State], use a full
/// [TickerProviderStateMixin] instead.
abstract class SingleTickerProviderStateMixin implements State<dynamic>, TickerProvider { // ignore: TYPE_ARGUMENT_NOT_MATCHING_BOUNDS, https://github.com/dart-lang/sdk/issues/25232
Ticker _ticker;
@override
Ticker createTicker(TickerCallback onTick) {
assert(() {
if (_ticker == null)
return true;
throw new FlutterError(
'$runtimeType is a SingleTickerProviderStateMixin but multiple tickers were created.\n'
'A SingleTickerProviderStateMixin can only be used as a TickerProvider once. If a '
'State is used for multiple AnimationController objects, or if it is passed to other '
'objects and those objects might use it more than one time in total, then instead of '
'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.'
);
});
_ticker = new Ticker(onTick, debugLabel: 'created by $this');
return _ticker;
}
@override
void dispose() {
assert(() {
if (_ticker == null || !_ticker.isActive)
return true;
throw new FlutterError(
'$this was disposed with an active Ticker.\n'
'$runtimeType created a Ticker via its SingleTickerProviderStateMixin, but at the time '
'dispose() was called on the mixin, that Ticker was still active. The Ticker must '
'be disposed before calling super.dispose(). Tickers used by AnimationControllers '
'should be disposed by calling dispose() on the AnimationController itself. '
'Otherwise, the ticker will leak.\n'
'The offending ticker was: ${_ticker.toString(debugIncludeStack: true)}'
);
});
super.dispose();
}
@override
void dependenciesChanged() {
_ticker.muted = !TickerMode.of(context);
super.dependenciesChanged();
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (_ticker != null) {
if (_ticker.isActive && _ticker.muted)
description.add('ticker active but muted');
else
if (_ticker.isActive)
description.add('ticker active');
else
if (_ticker.muted)
description.add('ticker inactive and muted');
else
description.add('ticker inactive');
}
}
}
/// Provides [Ticker] objects that are configured to only tick while the current
/// tree is enabled, as defined by [TickerMode].
///
/// To create an [AnimationController] in a class that uses this mixin, pass
/// `vsync: this` to the animation controller constructor whenever you
/// create a new animation controller.
///
/// If you only have a single [Ticker] (for example only a single
/// [AnimationController]) for the lifetime of your [State], then using a
/// [SingleTickerProviderStateMixin] is more efficient. This is the common case.
abstract class TickerProviderStateMixin implements State<dynamic>, TickerProvider { // ignore: TYPE_ARGUMENT_NOT_MATCHING_BOUNDS, https://github.com/dart-lang/sdk/issues/25232
Set<Ticker> _tickers;
@override
Ticker createTicker(TickerCallback onTick) {
_tickers ??= new Set<_WidgetTicker>();
final _WidgetTicker result = new _WidgetTicker(onTick, this, debugLabel: 'created by $this');
_tickers.add(result);
return result;
}
void _removeTicker(_WidgetTicker ticker) {
assert(_tickers != null);
assert(_tickers.contains(ticker));
_tickers.remove(ticker);
}
@override
void dispose() {
assert(() {
if (_tickers != null) {
for (Ticker ticker in _tickers) {
if (ticker.isActive) {
throw new FlutterError(
'$this was disposed with an active Ticker.\n'
'$runtimeType created a Ticker via its TickerProviderStateMixin, but at the time '
'dispose() was called on the mixin, that Ticker was still active. All Tickers must '
'be disposed before calling super.dispose(). Tickers used by AnimationControllers '
'should be disposed by calling dispose() on the AnimationController itself. '
'Otherwise, the ticker will leak.\n'
'The offending ticker was: ${ticker.toString(debugIncludeStack: true)}'
);
}
}
}
return true;
});
super.dispose();
}
@override
void dependenciesChanged() {
final bool muted = !TickerMode.of(context);
if (_tickers != null) {
for (Ticker ticker in _tickers)
ticker.muted = muted;
}
super.dependenciesChanged();
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (_tickers != null)
description.add('tracking ${_tickers.length} ticker${_tickers.length == 1 ? "" : "s"}');
}
}
// This class should really be called _DisposingTicker or some such, but this
// class name leaks into stack traces and error messages and that name would be
// confusing. Instead we use the less precise but more anodyne "_WidgetTicker",
// which attracts less attention.
class _WidgetTicker extends Ticker {
_WidgetTicker(TickerCallback onTick, this._creator, { String debugLabel }) : super(onTick, debugLabel: debugLabel);
final TickerProviderStateMixin _creator;
@override
void dispose() {
_creator._removeTicker(this);
super.dispose();
}
}

View File

@ -48,15 +48,16 @@ export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/routes.dart';
export 'src/widgets/scroll_behavior.dart';
export 'src/widgets/scroll_configuration.dart';
export 'src/widgets/scrollable.dart';
export 'src/widgets/scrollable_grid.dart';
export 'src/widgets/scrollable_list.dart';
export 'src/widgets/scrollable.dart';
export 'src/widgets/semantics_debugger.dart';
export 'src/widgets/size_changed_layout_notifier.dart';
export 'src/widgets/status_transitions.dart';
export 'src/widgets/table.dart';
export 'src/widgets/text_selection.dart';
export 'src/widgets/text.dart';
export 'src/widgets/text_selection.dart';
export 'src/widgets/ticker_provider.dart';
export 'src/widgets/title.dart';
export 'src/widgets/transitions.dart';
export 'src/widgets/unique_widget.dart';

View File

@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/animation.dart';
import 'package:flutter/widgets.dart';
import 'animation_tester.dart';
void main() {
setUp(() {
WidgetsFlutterBinding.ensureInitialized();
@ -14,7 +16,8 @@ void main() {
test('Can set value during status callback', () {
AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 100)
duration: const Duration(milliseconds: 100),
vsync: const TestVSync(),
);
bool didComplete = false;
bool didDismiss = false;
@ -45,7 +48,8 @@ void main() {
test('Receives status callbacks for forward and reverse', () {
AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 100)
duration: const Duration(milliseconds: 100),
vsync: const TestVSync(),
);
List<double> valueLog = <double>[];
List<AnimationStatus> log = <AnimationStatus>[];
@ -107,7 +111,8 @@ void main() {
test('Forward and reverse from values', () {
AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 100)
duration: const Duration(milliseconds: 100),
vsync: const TestVSync(),
);
List<double> valueLog = <double>[];
List<AnimationStatus> statusLog = <AnimationStatus>[];
@ -134,7 +139,8 @@ void main() {
test('Forward only from value', () {
AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 100)
duration: const Duration(milliseconds: 100),
vsync: const TestVSync(),
);
List<double> valueLog = <double>[];
List<AnimationStatus> statusLog = <AnimationStatus>[];
@ -154,7 +160,8 @@ void main() {
test('Can fling to upper and lower bounds', () {
AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 100)
duration: const Duration(milliseconds: 100),
vsync: const TestVSync(),
);
controller.fling();
@ -166,7 +173,8 @@ void main() {
AnimationController largeRangeController = new AnimationController(
duration: const Duration(milliseconds: 100),
lowerBound: -30.0,
upperBound: 45.0
upperBound: 45.0,
vsync: const TestVSync(),
);
largeRangeController.fling();
@ -182,7 +190,8 @@ void main() {
test('lastElapsedDuration control test', () {
AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 100)
duration: const Duration(milliseconds: 100),
vsync: const TestVSync(),
);
controller.forward();
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 20));
@ -194,7 +203,8 @@ void main() {
test('toString control test', () {
AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 100)
duration: const Duration(milliseconds: 100),
vsync: const TestVSync(),
);
expect(controller.toString(), hasOneLineDescription);
controller.forward();

View File

@ -0,0 +1,12 @@
// Copyright 2016 The Chromium 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/scheduler.dart';
class TestVSync implements TickerProvider {
const TestVSync();
@override
Ticker createTicker(TickerCallback onTick) => new Ticker(onTick);
}

View File

@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/animation.dart';
import 'package:flutter/widgets.dart';
import 'animation_tester.dart';
void main() {
setUp(() {
WidgetsFlutterBinding.ensureInitialized();
@ -24,7 +26,8 @@ void main() {
curvedAnimation.reverseCurve = Curves.elasticOut;
expect(curvedAnimation.toString(), hasOneLineDescription);
AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 500)
duration: const Duration(milliseconds: 500),
vsync: const TestVSync(),
);
controller
..value = 0.5
@ -48,7 +51,9 @@ void main() {
});
test('ProxyAnimation set parent generates value changed', () {
AnimationController controller = new AnimationController();
AnimationController controller = new AnimationController(
vsync: const TestVSync(),
);
controller.value = 0.5;
bool didReceiveCallback = false;
ProxyAnimation animation = new ProxyAnimation()
@ -65,7 +70,9 @@ void main() {
});
test('ReverseAnimation calls listeners', () {
AnimationController controller = new AnimationController();
AnimationController controller = new AnimationController(
vsync: const TestVSync(),
);
controller.value = 0.5;
bool didReceiveCallback = false;
void listener() {
@ -85,8 +92,12 @@ void main() {
});
test('TrainHoppingAnimation', () {
AnimationController currentTrain = new AnimationController();
AnimationController nextTrain = new AnimationController();
AnimationController currentTrain = new AnimationController(
vsync: const TestVSync(),
);
AnimationController nextTrain = new AnimationController(
vsync: const TestVSync(),
);
currentTrain.value = 0.5;
nextTrain.value = 0.75;
bool didSwitchTrains = false;

View File

@ -6,19 +6,25 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/animation.dart';
import 'package:flutter/widgets.dart';
import 'animation_tester.dart';
void main() {
test('Can chain tweens', () {
Tween<double> tween = new Tween<double>(begin: 0.30, end: 0.50);
expect(tween, hasOneLineDescription);
Animatable<double> chain = tween.chain(new Tween<double>(begin: 0.50, end: 1.0));
AnimationController controller = new AnimationController();
AnimationController controller = new AnimationController(
vsync: const TestVSync(),
);
expect(chain.evaluate(controller), 0.40);
expect(chain, hasOneLineDescription);
});
test('Can animated tweens', () {
Tween<double> tween = new Tween<double>(begin: 0.30, end: 0.50);
AnimationController controller = new AnimationController();
AnimationController controller = new AnimationController(
vsync: const TestVSync(),
);
Animation<double> animation = tween.animate(controller);
controller.value = 0.50;
expect(animation.value, 0.40);

View File

@ -21,6 +21,7 @@ void main() {
new Center(
child: new AnimatedSize(
duration: const Duration(milliseconds: 200),
vsync: tester,
child: new SizedBox(
width: 100.0,
height: 100.0
@ -37,6 +38,7 @@ void main() {
new Center(
child: new AnimatedSize(
duration: new Duration(milliseconds: 200),
vsync: tester,
child: new SizedBox(
width: 200.0,
height: 200.0
@ -63,6 +65,7 @@ void main() {
new Center(
child: new AnimatedSize(
duration: new Duration(milliseconds: 200),
vsync: tester,
child: new SizedBox(
width: 100.0,
height: 100.0
@ -94,6 +97,7 @@ void main() {
height: 100.0,
child: new AnimatedSize(
duration: const Duration(milliseconds: 200),
vsync: tester,
child: new SizedBox(
width: 100.0,
height: 100.0
@ -114,6 +118,7 @@ void main() {
height: 100.0,
child: new AnimatedSize(
duration: const Duration(milliseconds: 200),
vsync: tester,
child: new SizedBox(
width: 200.0,
height: 200.0
@ -134,6 +139,7 @@ void main() {
new Center(
child: new AnimatedSize(
duration: const Duration(milliseconds: 200),
vsync: tester,
child: new AnimatedContainer(
duration: const Duration(milliseconds: 100),
width: 100.0,
@ -151,6 +157,7 @@ void main() {
new Center(
child: new AnimatedSize(
duration: const Duration(milliseconds: 200),
vsync: tester,
child: new AnimatedContainer(
duration: const Duration(milliseconds: 100),
width: 200.0,

View File

@ -32,7 +32,9 @@ class TestFlowDelegate extends FlowDelegate {
void main() {
testWidgets('Flow control test', (WidgetTester tester) async {
AnimationController startOffset = new AnimationController.unbounded();
AnimationController startOffset = new AnimationController.unbounded(
vsync: tester,
);
List<int> log = <int>[];
Widget buildBox(int i) {

View File

@ -23,7 +23,8 @@ void main() {
)
);
final AnimationController controller = new AnimationController(
duration: const Duration(seconds: 10)
duration: const Duration(seconds: 10),
vsync: tester,
);
final List<Size> sizes = <Size>[];
final List<Offset> positions = <Offset>[];

View File

@ -0,0 +1,53 @@
// Copyright 2015 The Chromium 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_test/flutter_test.dart';
import 'package:flutter/material.dart';
void main() {
testWidgets('TickerMode', (WidgetTester tester) async {
await tester.pumpWidget(new TickerMode(
enabled: false,
child: new LinearProgressIndicator()
));
expect(tester.binding.transientCallbackCount, 0);
await tester.pumpWidget(new TickerMode(
enabled: true,
child: new LinearProgressIndicator()
));
expect(tester.binding.transientCallbackCount, 1);
await tester.pumpWidget(new TickerMode(
enabled: false,
child: new LinearProgressIndicator()
));
expect(tester.binding.transientCallbackCount, 0);
});
testWidgets('Navigation with TickerMode', (WidgetTester tester) async {
await tester.pumpWidget(new MaterialApp(
home: new LinearProgressIndicator(),
routes: <String, WidgetBuilder>{
'/test': (BuildContext context) => new Text('hello'),
},
));
expect(tester.binding.transientCallbackCount, 1);
tester.state/*<NavigatorState>*/(find.byType(Navigator)).pushNamed('/test');
expect(tester.binding.transientCallbackCount, 2);
await tester.pump();
expect(tester.binding.transientCallbackCount, 2);
await tester.pump(const Duration(seconds: 5));
expect(tester.binding.transientCallbackCount, 0);
tester.state/*<NavigatorState>*/(find.byType(Navigator)).pop();
expect(tester.binding.transientCallbackCount, 1);
await tester.pump();
expect(tester.binding.transientCallbackCount, 2);
await tester.pump(const Duration(seconds: 5));
expect(tester.binding.transientCallbackCount, 1);
});
}

View File

@ -226,14 +226,17 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
/// test failures.
bool showAppDumpInErrors = false;
/// Call the callback inside a [FakeAsync] scope on which [pump] can
/// Call the testBody inside a [FakeAsync] scope on which [pump] can
/// advance time.
///
/// Returns a future which completes when the test has run.
///
/// Called by the [testWidgets] and [benchmarkWidgets] functions to
/// run a test.
Future<Null> runTest(Future<Null> callback());
///
/// The `invariantTester` argument is called after the `testBody`'s [Future]
/// completes. If it throws, then the test is marked as failed.
Future<Null> runTest(Future<Null> testBody(), VoidCallback invariantTester);
/// This is called during test execution before and after the body has been
/// executed.
@ -266,7 +269,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
_currentTestCompleter.complete(null);
}
Future<Null> _runTest(Future<Null> callback()) {
Future<Null> _runTest(Future<Null> testBody(), VoidCallback invariantTester) {
assert(inTest);
_oldExceptionHandler = FlutterError.onError;
int _exceptionCount = 0; // number of un-taken exceptions
@ -357,20 +360,20 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
);
_parentZone = Zone.current;
Zone testZone = _parentZone.fork(specification: errorHandlingZoneSpecification);
testZone.runUnaryGuarded(_runTestBody, callback)
testZone.runBinaryGuarded(_runTestBody, testBody, invariantTester)
.whenComplete(_testCompletionHandler);
asyncBarrier(); // When using AutomatedTestWidgetsFlutterBinding, this flushes the microtasks.
return _currentTestCompleter.future;
}
Future<Null> _runTestBody(Future<Null> callback()) async {
Future<Null> _runTestBody(Future<Null> testBody(), VoidCallback invariantTester) async {
assert(inTest);
runApp(new Container(key: new UniqueKey(), child: _kPreTestMessage)); // Reset the tree to a known state.
await pump();
// run the test
await callback();
await testBody();
asyncBarrier(); // drains the microtasks in `flutter test` mode (when using AutomatedTestWidgetsFlutterBinding)
if (_pendingExceptionDetails == null) {
@ -379,6 +382,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
// alone so that we don't cause more spurious errors.
runApp(new Container(key: new UniqueKey(), child: _kPostTestMessage)); // Unmount any remaining widgets.
await pump();
invariantTester();
_verifyInvariants();
}
@ -489,24 +493,24 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
}
@override
Future<Null> runTest(Future<Null> callback()) {
Future<Null> runTest(Future<Null> testBody(), VoidCallback invariantTester) {
assert(!inTest);
assert(_fakeAsync == null);
assert(_clock == null);
_fakeAsync = new FakeAsync();
_clock = _fakeAsync.getClock(new DateTime.utc(2015, 1, 1));
Future<Null> callbackResult;
Future<Null> testBodyResult;
_fakeAsync.run((FakeAsync fakeAsync) {
assert(fakeAsync == _fakeAsync);
callbackResult = _runTest(callback);
testBodyResult = _runTest(testBody, invariantTester);
assert(inTest);
});
// callbackResult is a Future that was created in the Zone of the fakeAsync.
// testBodyResult is a Future that was created in the Zone of the fakeAsync.
// This means that if we call .then() on it (as the test framework is about to),
// it will register a microtask to handle the future _in the fake async zone_.
// To avoid this, we wrap it in a Future that we've created _outside_ the fake
// async zone.
return new Future<Null>.value(callbackResult);
return new Future<Null>.value(testBodyResult);
}
@override
@ -686,10 +690,10 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
}
@override
Future<Null> runTest(Future<Null> callback()) async {
Future<Null> runTest(Future<Null> testBody(), VoidCallback invariantTester) async {
assert(!inTest);
_inTest = true;
return _runTest(callback);
return _runTest(testBody, invariantTester);
}
@override

View File

@ -6,6 +6,7 @@ import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
import 'package:test/test.dart' as test_package;
@ -50,7 +51,7 @@ void testWidgets(String description, WidgetTesterCallback callback, {
WidgetTester tester = new WidgetTester._(binding);
timeout ??= binding.defaultTestTimeout;
test_package.group('-', () {
test_package.test(description, () => binding.runTest(() => callback(tester)), skip: skip);
test_package.test(description, () => binding.runTest(() => callback(tester), tester._endOfTestVerifications), skip: skip);
test_package.tearDown(binding.postTest);
}, timeout: timeout);
}
@ -108,7 +109,7 @@ Future<Null> benchmarkWidgets(WidgetTesterCallback callback) {
TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
assert(binding is! AutomatedTestWidgetsFlutterBinding);
WidgetTester tester = new WidgetTester._(binding);
return binding.runTest(() => callback(tester)) ?? new Future<Null>.value();
return binding.runTest(() => callback(tester), tester._endOfTestVerifications) ?? new Future<Null>.value();
}
/// Assert that `actual` matches `matcher`.
@ -143,7 +144,10 @@ void expectSync(dynamic actual, dynamic matcher, {
}
/// Class that programmatically interacts with widgets and the test environment.
class WidgetTester extends WidgetController implements HitTestDispatcher {
///
/// For convenience, instances of this class (such as the one provided by
/// `testWidget`) can be used as the `vsync` for `AnimationController` objects.
class WidgetTester extends WidgetController implements HitTestDispatcher, TickerProvider {
WidgetTester._(TestWidgetsFlutterBinding binding) : super(binding) {
if (binding is LiveTestWidgetsFlutterBinding)
binding.deviceEventDispatcher = this;
@ -328,6 +332,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher {
}
bool _isPrivate(Type type) {
// used above so that we don't suggest matchers for private types
return '_'.matchAsPrefix(type.toString()) != null;
}
@ -348,4 +353,62 @@ class WidgetTester extends WidgetController implements HitTestDispatcher {
Future<Null> idle() {
return TestAsyncUtils.guard(() => binding.idle());
}
Set<Ticker> _tickers;
@override
Ticker createTicker(TickerCallback onTick) {
_tickers ??= new Set<_TestTicker>();
final _TestTicker result = new _TestTicker(onTick, _removeTicker);
_tickers.add(result);
return result;
}
void _removeTicker(_TestTicker ticker) {
assert(_tickers != null);
assert(_tickers.contains(ticker));
_tickers.remove(ticker);
}
/// Throws an exception if any tickers created by the [WidgetTester] are still
/// active when the method is called.
///
/// An argument can be specified to provide a string that will be used in the
/// error message. It should be an adverbial phrase describing the current
/// situation, such as "at the end of the test".
void verifyTickersWereDisposed([ String when = 'when none should have been' ]) {
assert(when != null);
if (_tickers != null) {
for (Ticker ticker in _tickers) {
if (ticker.isActive) {
throw new FlutterError(
'A Ticker was active $when.\n'
'All Tickers must be disposed. Tickers used by AnimationControllers '
'should be disposed by calling dispose() on the AnimationController itself. '
'Otherwise, the ticker will leak.\n'
'The offending ticker was: ${ticker.toString(debugIncludeStack: true)}'
);
}
}
}
}
void _endOfTestVerifications() {
verifyTickersWereDisposed('at the end of the test');
}
}
typedef void _TickerDisposeCallback(_TestTicker ticker);
class _TestTicker extends Ticker {
_TestTicker(TickerCallback onTick, this._onDispose) : super(onTick);
_TickerDisposeCallback _onDispose;
@override
void dispose() {
if (_onDispose != null)
_onDispose(this);
super.dispose();
}
}

View File

@ -73,7 +73,10 @@ void main() {
await tester.pumpWidget(new Text('foo'));
int count;
AnimationController test = new AnimationController(duration: const Duration(milliseconds: 5100));
AnimationController test = new AnimationController(
duration: const Duration(milliseconds: 5100),
vsync: tester,
);
count = await tester.pumpUntilNoTransientCallbacks(const Duration(seconds: 1));
expect(count, 0);