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); typedef Widget _DemoBuilder(_ArcDemo demo);
class _ArcDemo { 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 String title;
final _DemoBuilder builder; final _DemoBuilder builder;
final AnimationController controller;
final GlobalKey key; final GlobalKey key;
} }
@ -410,23 +413,29 @@ class AnimationDemo extends StatefulWidget {
_AnimationDemoState createState() => new _AnimationDemoState(); _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 GlobalKey<TabBarSelectionState<_ArcDemo>> _tabsKey = new GlobalKey<TabBarSelectionState<_ArcDemo>>();
static final List<_ArcDemo> _allDemos = <_ArcDemo>[ List<_ArcDemo> _allDemos;
new _ArcDemo('POINT', (_ArcDemo demo) {
return new _PointDemo( @override
key: demo.key, void initState() {
controller: demo.controller super.initState();
); _allDemos = <_ArcDemo>[
}), new _ArcDemo('POINT', (_ArcDemo demo) {
new _ArcDemo('RECTANGLE', (_ArcDemo demo) { return new _PointDemo(
return new _RectangleDemo( key: demo.key,
key: demo.key, controller: demo.controller
controller: demo.controller );
); }, this),
}) new _ArcDemo('RECTANGLE', (_ArcDemo demo) {
]; return new _RectangleDemo(
key: demo.key,
controller: demo.controller
);
}, this),
];
}
Future<Null> _play() async { Future<Null> _play() async {
_ArcDemo demo = _tabsKey.currentState.value; _ArcDemo demo = _tabsKey.currentState.value;

View File

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

View File

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

View File

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

View File

@ -9,6 +9,13 @@ import 'dart:math' as math;
import 'package:flutter/animation.dart'; import 'package:flutter/animation.dart';
import 'package:flutter/rendering.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() { void main() {
// We first create a render object that represents a green box. // 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 // To make the square spin, we use an animation that repeats every 1800
// milliseconds. // milliseconds.
AnimationController animation = new AnimationController( AnimationController animation = new AnimationController(
duration: const Duration(milliseconds: 1800) duration: const Duration(milliseconds: 1800),
vsync: const NonStopVSync(),
)..repeat(); )..repeat();
// The animation will produce a value between 0.0 and 1.0 each frame, but we // 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 // 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. // Main application state.
class IsolateExampleState extends State<StatefulWidget> { class IsolateExampleState extends State<StatefulWidget> with SingleTickerProviderStateMixin {
String _status = 'Idle'; String _status = 'Idle';
String _label = 'Start'; String _label = 'Start';
@ -219,7 +219,8 @@ class IsolateExampleState extends State<StatefulWidget> {
void initState() { void initState() {
super.initState(); super.initState();
_animation = new AnimationController( _animation = new AnimationController(
duration: const Duration(milliseconds: 3600) duration: const Duration(milliseconds: 3600),
vsync: this,
)..repeat(); )..repeat();
_calculationManager = new CalculationManager( _calculationManager = new CalculationManager(
onProgressListener: _handleProgressUpdate, onProgressListener: _handleProgressUpdate,

View File

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

View File

@ -5,8 +5,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui show lerpDouble; import 'dart:ui' as ui show lerpDouble;
import 'package:flutter/scheduler.dart';
import 'package:flutter/physics.dart'; import 'package:flutter/physics.dart';
import 'package:flutter/scheduler.dart';
import 'package:meta/meta.dart';
import 'animation.dart'; import 'animation.dart';
import 'curves.dart'; import 'curves.dart';
@ -19,7 +20,7 @@ enum _AnimationDirection {
forward, forward,
/// The animation is running backwards, from end to beginning. /// The animation is running backwards, from end to beginning.
reverse reverse,
} }
/// A controller for an animation. /// A controller for an animation.
@ -40,29 +41,33 @@ class AnimationController extends Animation<double>
/// Creates an animation controller. /// Creates an animation controller.
/// ///
/// * value is the initial value of the animation. /// * [value] is the initial value of the animation.
/// * duration is the length of time this animation should last. /// * [duration] is the length of time this animation should last.
/// * debugLabel is a string to help identify this animation during debugging (used by toString). /// * [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. /// * [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. /// * [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({ AnimationController({
double value, double value,
this.duration, this.duration,
this.debugLabel, this.debugLabel,
this.lowerBound: 0.0, this.lowerBound: 0.0,
this.upperBound: 1.0 this.upperBound: 1.0,
@required TickerProvider vsync,
}) { }) {
assert(upperBound >= lowerBound); assert(upperBound >= lowerBound);
assert(vsync != null);
_direction = _AnimationDirection.forward; _direction = _AnimationDirection.forward;
_ticker = new Ticker(_tick); _ticker = vsync.createTicker(_tick);
_internalSetValue(value ?? lowerBound); _internalSetValue(value ?? lowerBound);
} }
/// Creates an animation controller with no upper or lower bound for its value. /// Creates an animation controller with no upper or lower bound for its value.
/// ///
/// * value is the initial value of the animation. /// * [value] is the initial value of the animation.
/// * duration is the length of time this animation should last. /// * [duration] is the length of time this animation should last.
/// * debugLabel is a string to help identify this animation during debugging (used by toString). /// * [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 /// This constructor is most useful for animations that will be driven using a
/// physics simulation, especially when the physics simulation has no /// physics simulation, especially when the physics simulation has no
@ -70,12 +75,14 @@ class AnimationController extends Animation<double>
AnimationController.unbounded({ AnimationController.unbounded({
double value: 0.0, double value: 0.0,
this.duration, this.duration,
this.debugLabel this.debugLabel,
@required TickerProvider vsync,
}) : lowerBound = double.NEGATIVE_INFINITY, }) : lowerBound = double.NEGATIVE_INFINITY,
upperBound = double.INFINITY { upperBound = double.INFINITY {
assert(value != null); assert(value != null);
assert(vsync != null);
_direction = _AnimationDirection.forward; _direction = _AnimationDirection.forward;
_ticker = new Ticker(_tick); _ticker = vsync.createTicker(_tick);
_internalSetValue(value); _internalSetValue(value);
} }
@ -98,6 +105,14 @@ class AnimationController extends Animation<double>
Duration duration; Duration duration;
Ticker _ticker; 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; Simulation _simulation;
/// The current value of the animation. /// The current value of the animation.
@ -145,7 +160,12 @@ class AnimationController extends Animation<double>
Duration _lastElapsedDuration; Duration _lastElapsedDuration;
/// Whether this animation is currently animating in either the forward or reverse direction. /// 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; _AnimationDirection _direction;
@ -239,16 +259,21 @@ class AnimationController extends Animation<double>
} }
/// Stops running this animation. /// Stops running this animation.
///
/// This does not trigger any notifications. The animation stops in its
/// current state.
void stop() { void stop() {
_simulation = null; _simulation = null;
_lastElapsedDuration = null; _lastElapsedDuration = null;
_ticker.stop(); _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 @override
void dispose() { void dispose() {
stop(); _ticker.dispose();
super.dispose();
} }
AnimationStatus _lastReportedStatus = AnimationStatus.dismissed; AnimationStatus _lastReportedStatus = AnimationStatus.dismissed;
@ -277,9 +302,10 @@ class AnimationController extends Animation<double>
@override @override
String toStringDetails() { String toStringDetails() {
String paused = isAnimating ? '' : '; paused'; String paused = isAnimating ? '' : '; paused';
String silenced = _ticker.muted ? '; silenced' : '';
String label = debugLabel == null ? '' : '; for $debugLabel'; String label = debugLabel == null ? '' : '; for $debugLabel';
String more = '${super.toStringDetails()} ${value.toStringAsFixed(3)}'; 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; _currentTrain = null;
_nextTrain?.removeListener(_valueChangeHandler); _nextTrain?.removeListener(_valueChangeHandler);
_nextTrain = null; _nextTrain = null;
super.dispose();
} }
@override @override

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import 'dart:math' as math;
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart'; import 'package:vector_math/vector_math_64.dart';
import 'constants.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'); final GlobalKey _inkFeatureRenderer = new GlobalKey(debugLabel: 'ink renderer');
Color _getBackgroundColor(BuildContext context) { Color _getBackgroundColor(BuildContext context) {
@ -254,7 +255,8 @@ class _MaterialState extends State<Material> {
child: new _InkFeatures( child: new _InkFeatures(
key: _inkFeatureRenderer, key: _inkFeatureRenderer,
color: backgroundColor, color: backgroundColor,
child: contents child: contents,
vsync: this,
) )
); );
if (config.type == MaterialType.circle) { 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 const double _kSplashInitialSize = 0.0; // logical pixels
class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController { 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. // This is here to satisfy the MaterialInkController contract.
// The actual painting of this color is done by a Container in the // 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, targetRadius: radius,
clipCallback: clipCallback, clipCallback: clipCallback,
repositionToReferenceBox: !containedInkWell, repositionToReferenceBox: !containedInkWell,
onRemoved: onRemoved onRemoved: onRemoved,
vsync: vsync,
); );
addInkFeature(splash); addInkFeature(splash);
return splash; return splash;
@ -366,7 +376,8 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
color: color, color: color,
shape: shape, shape: shape,
rectCallback: rectCallback, rectCallback: rectCallback,
onRemoved: onRemoved onRemoved: onRemoved,
vsync: vsync,
); );
addInkFeature(highlight); addInkFeature(highlight);
return highlight; return highlight;
@ -405,16 +416,27 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
} }
class _InkFeatures extends SingleChildRenderObjectWidget { 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 Color color;
final TickerProvider vsync;
@override @override
_RenderInkFeatures createRenderObject(BuildContext context) => new _RenderInkFeatures(color: color); _RenderInkFeatures createRenderObject(BuildContext context) {
return new _RenderInkFeatures(
color: color,
vsync: vsync
);
}
@override @override
void updateRenderObject(BuildContext context, _RenderInkFeatures renderObject) { void updateRenderObject(BuildContext context, _RenderInkFeatures renderObject) {
renderObject.color = color; renderObject.color = color;
assert(vsync == renderObject.vsync);
} }
} }
@ -488,17 +510,17 @@ class _InkSplash extends InkFeature implements InkSplash {
this.targetRadius, this.targetRadius,
this.clipCallback, this.clipCallback,
this.repositionToReferenceBox, this.repositionToReferenceBox,
VoidCallback onRemoved VoidCallback onRemoved,
@required TickerProvider vsync,
}) : super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) { }) : super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) {
_radiusController = new AnimationController(duration: _kUnconfirmedSplashDuration) _radiusController = new AnimationController(duration: _kUnconfirmedSplashDuration, vsync: vsync)
..addListener(controller.markNeedsPaint) ..addListener(controller.markNeedsPaint)
..forward(); ..forward();
_radius = new Tween<double>( _radius = new Tween<double>(
begin: _kSplashInitialSize, begin: _kSplashInitialSize,
end: targetRadius end: targetRadius
).animate(_radiusController); ).animate(_radiusController);
_alphaController = new AnimationController(duration: _kHighlightFadeDuration, vsync: vsync)
_alphaController = new AnimationController(duration: _kHighlightFadeDuration)
..addListener(controller.markNeedsPaint) ..addListener(controller.markNeedsPaint)
..addStatusListener(_handleAlphaStatusChanged); ..addStatusListener(_handleAlphaStatusChanged);
_alpha = new IntTween( _alpha = new IntTween(
@ -578,10 +600,11 @@ class _InkHighlight extends InkFeature implements InkHighlight {
this.rectCallback, this.rectCallback,
Color color, Color color,
this.shape, this.shape,
VoidCallback onRemoved VoidCallback onRemoved,
@required TickerProvider vsync,
}) : _color = color, }) : _color = color,
super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) { super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) {
_alphaController = new AnimationController(duration: _kHighlightFadeDuration) _alphaController = new AnimationController(duration: _kHighlightFadeDuration, vsync: vsync)
..addListener(controller.markNeedsPaint) ..addListener(controller.markNeedsPaint)
..addStatusListener(_handleAlphaStatusChanged) ..addStatusListener(_handleAlphaStatusChanged)
..forward(); ..forward();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -138,9 +138,9 @@ class RefreshIndicator extends StatefulWidget {
/// Contains the state for a [RefreshIndicator]. This class can be used to /// Contains the state for a [RefreshIndicator]. This class can be used to
/// programmatically show the refresh indicator, see the [show] method. /// programmatically show the refresh indicator, see the [show] method.
class RefreshIndicatorState extends State<RefreshIndicator> { class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderStateMixin {
final AnimationController _sizeController = new AnimationController(); AnimationController _sizeController;
final AnimationController _scaleController = new AnimationController(); AnimationController _scaleController;
Animation<double> _sizeFactor; Animation<double> _sizeFactor;
Animation<double> _scaleFactor; Animation<double> _scaleFactor;
Animation<double> _value; Animation<double> _value;
@ -154,15 +154,16 @@ class RefreshIndicatorState extends State<RefreshIndicator> {
@override @override
void initState() { void initState() {
super.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. _sizeController = new AnimationController(vsync: this);
_value = new Tween<double>( _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, begin: 0.0,
end: 0.75 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 @override

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -523,26 +523,27 @@ class TabBarSelection<T> extends StatefulWidget {
/// ///
/// Subclasses of [TabBarSelection] typically use [State] objects that extend /// Subclasses of [TabBarSelection] typically use [State] objects that extend
/// this class. /// 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. /// An animation that updates as the selected tab changes.
Animation<double> get animation => _controller.view; 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>(); 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 @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = new AnimationController(
duration: _kTabBarScroll,
value: 1.0,
vsync: this,
);
_value = config.value ?? PageStorage.of(context)?.readState(context) ?? values.first; _value = config.value ?? PageStorage.of(context)?.readState(context) ?? values.first;
// If the selection's values have changed since the selected value was saved with // 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(); _initValueToIndex();
} }
void _initValueToIndex() {
_valueToIndex.clear();
int index = 0;
for (T value in values)
_valueToIndex[value] = index++;
}
void _writeValue() { void _writeValue() {
PageStorage.of(context)?.writeState(context, _value); PageStorage.of(context)?.writeState(context, _value);
} }

View File

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

View File

@ -5,6 +5,8 @@
import 'package:flutter/animation.dart'; import 'package:flutter/animation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:meta/meta.dart';
import 'constants.dart'; import 'constants.dart';
@ -26,38 +28,88 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic
Size size, Size size,
Color activeColor, Color activeColor,
Color inactiveColor, Color inactiveColor,
ValueChanged<bool> onChanged ValueChanged<bool> onChanged,
@required TickerProvider vsync,
}) : _value = value, }) : _value = value,
_activeColor = activeColor, _activeColor = activeColor,
_inactiveColor = inactiveColor, _inactiveColor = inactiveColor,
_onChanged = onChanged, _onChanged = onChanged,
_vsync = vsync,
super(additionalConstraints: new BoxConstraints.tight(size)) { super(additionalConstraints: new BoxConstraints.tight(size)) {
assert(value != null); assert(value != null);
assert(activeColor != null); assert(activeColor != null);
assert(inactiveColor != null); assert(inactiveColor != null);
assert(vsync != null);
_tap = new TapGestureRecognizer() _tap = new TapGestureRecognizer()
..onTapDown = _handleTapDown ..onTapDown = _handleTapDown
..onTap = _handleTap ..onTap = _handleTap
..onTapUp = _handleTapUp ..onTapUp = _handleTapUp
..onTapCancel = _handleTapCancel; ..onTapCancel = _handleTapCancel;
_positionController = new AnimationController( _positionController = new AnimationController(
duration: _kToggleDuration, duration: _kToggleDuration,
value: _value ? 1.0 : 0.0 value: value ? 1.0 : 0.0,
vsync: vsync,
); );
_position = new CurvedAnimation( _position = new CurvedAnimation(
parent: _positionController, parent: _positionController,
curve: Curves.linear curve: Curves.linear,
)..addListener(markNeedsPaint) )..addListener(markNeedsPaint)
..addStatusListener(_handlePositionStateChanged); ..addStatusListener(_handlePositionStateChanged);
_reactionController = new AnimationController(
_reactionController = new AnimationController(duration: kRadialReactionDuration); duration: kRadialReactionDuration,
vsync: vsync,
);
_reaction = new CurvedAnimation( _reaction = new CurvedAnimation(
parent: _reactionController, parent: _reactionController,
curve: Curves.fastOutSlowIn curve: Curves.fastOutSlowIn,
)..addListener(markNeedsPaint); )..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). /// 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 /// 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. /// grey color and its value cannot be changed.
bool get isInteractive => onChanged != null; 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; TapGestureRecognizer _tap;
Point _downPosition; Point _downPosition;
@override @override
void attach(PipelineOwner owner) { void attach(PipelineOwner owner) {
super.attach(owner); super.attach(owner);
if (_positionController != null) { if (value)
if (value) _positionController.forward();
_positionController.forward(); else
else _positionController.reverse();
_positionController.reverse(); if (isInteractive) {
}
if (_reactionController != null && isInteractive) {
switch (_reactionController.status) { switch (_reactionController.status) {
case AnimationStatus.forward: case AnimationStatus.forward:
_reactionController.forward(); _reactionController.forward();
@ -199,8 +218,8 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic
@override @override
void detach() { void detach() {
_positionController?.stop(); _positionController.stop();
_reactionController?.stop(); _reactionController.stop();
super.detach(); 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; AnimationController _controller;
OverlayEntry _entry; OverlayEntry _entry;
Timer _timer; Timer _timer;
@ -101,7 +101,7 @@ class _TooltipState extends State<Tooltip> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = new AnimationController(duration: _kFadeDuration) _controller = new AnimationController(duration: _kFadeDuration, vsync: this)
..addStatusListener(_handleStatusChanged); ..addStatusListener(_handleStatusChanged);
} }
@ -178,7 +178,7 @@ class _TooltipState extends State<Tooltip> {
void dispose() { void dispose() {
if (_entry != null) if (_entry != null)
_removeEntry(); _removeEntry();
_controller.stop(); _controller.dispose();
super.dispose(); super.dispose();
} }

View File

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

View File

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

View File

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

View File

@ -86,6 +86,45 @@ class _FrameCallbackEntry {
StackTrace debugStack; 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: /// Scheduler for running the following:
/// ///
/// * _Frame callbacks_, triggered by the system's /// * _Frame callbacks_, triggered by the system's
@ -402,9 +441,9 @@ abstract class SchedulerBinding extends BindingBase {
bool get hasScheduledFrame => _hasScheduledFrame; bool get hasScheduledFrame => _hasScheduledFrame;
bool _hasScheduledFrame = false; bool _hasScheduledFrame = false;
/// Whether this scheduler is currently producing a frame in [handleBeginFrame]. /// The phase that the scheduler is currently operating under.
bool get isProducingFrame => _isProducingFrame; SchedulerPhase get schedulerPhase => _schedulerPhase;
bool _isProducingFrame = false; SchedulerPhase _schedulerPhase = SchedulerPhase.idle;
/// Schedules a new frame using [scheduleFrame] if this object is not /// Schedules a new frame using [scheduleFrame] if this object is not
/// currently producing a frame. /// 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 /// After this is called, the framework ensures that the end of the
/// [handleBeginFrame] function will (eventually) be reached. /// [handleBeginFrame] function will (eventually) be reached.
void ensureVisualUpdate() { void ensureVisualUpdate() {
if (_isProducingFrame) if (schedulerPhase != SchedulerPhase.idle)
return; return;
scheduleFrame(); scheduleFrame();
} }
@ -530,20 +569,21 @@ abstract class SchedulerBinding extends BindingBase {
return true; return true;
}); });
assert(!_isProducingFrame); assert(schedulerPhase == SchedulerPhase.idle);
_isProducingFrame = true;
_hasScheduledFrame = false; _hasScheduledFrame = false;
try { try {
// TRANSIENT FRAME CALLBACKS // TRANSIENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.transientCallbacks;
_invokeTransientFrameCallbacks(_currentFrameTimeStamp); _invokeTransientFrameCallbacks(_currentFrameTimeStamp);
// PERSISTENT FRAME CALLBACKS // PERSISTENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (FrameCallback callback in _persistentCallbacks) for (FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp); _invokeFrameCallback(callback, _currentFrameTimeStamp);
_isProducingFrame = false;
// POST-FRAME CALLBACKS // POST-FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.postFrameCallbacks;
List<FrameCallback> localPostFrameCallbacks = List<FrameCallback> localPostFrameCallbacks =
new List<FrameCallback>.from(_postFrameCallbacks); new List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear(); _postFrameCallbacks.clear();
@ -551,7 +591,7 @@ abstract class SchedulerBinding extends BindingBase {
_invokeFrameCallback(callback, _currentFrameTimeStamp); _invokeFrameCallback(callback, _currentFrameTimeStamp);
} finally { } finally {
_isProducingFrame = false; // just in case we throw before setting it above _schedulerPhase = SchedulerPhase.idle;
_currentFrameTimeStamp = null; _currentFrameTimeStamp = null;
Timeline.finishSync(); Timeline.finishSync();
assert(() { assert(() {
@ -567,7 +607,7 @@ abstract class SchedulerBinding extends BindingBase {
void _invokeTransientFrameCallbacks(Duration timeStamp) { void _invokeTransientFrameCallbacks(Duration timeStamp) {
Timeline.startSync('Animate'); Timeline.startSync('Animate');
assert(_isProducingFrame); assert(schedulerPhase == SchedulerPhase.transientCallbacks);
Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks; Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
_transientCallbacks = new Map<int, _FrameCallbackEntry>(); _transientCallbacks = new Map<int, _FrameCallbackEntry>();
callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) { callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {

View File

@ -4,6 +4,9 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart';
import 'binding.dart'; import 'binding.dart';
/// Signature for the [onTick] constructor argument of the [Ticker] class. /// 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. /// at the time of the callback being called.
typedef void TickerCallback(Duration elapsed); 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. /// Calls its callback once per animation frame.
/// ///
/// When created, a ticker is initially disabled. Call [start] to /// When created, a ticker is initially disabled. Call [start] to
/// enable the ticker. /// 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 { class Ticker {
/// Creates a ticker that will call [onTick] once per frame while running. /// Creates a ticker that will call [onTick] once per frame while running.
Ticker(TickerCallback onTick) : _onTick = onTick; ///
/// An optional label can be provided for debugging purposes. That label
final TickerCallback _onTick; /// will appear in the [toString] output in debug builds.
Ticker(this._onTick, { this.debugLabel }) {
assert(() {
_debugCreationStack = StackTrace.current;
return true;
});
}
Completer<Null> _completer; 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 /// Whether this ticker has scheduled a call to call its callback
/// on the next frame. /// 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() { 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); assert(_startTime == null);
_completer = new Completer<Null>(); _completer = new Completer<Null>();
_scheduleTick(); if (shouldScheduleTick)
if (SchedulerBinding.instance.isProducingFrame) scheduleTick();
if (SchedulerBinding.instance.schedulerPhase.index > SchedulerPhase.idle.index &&
SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index)
_startTime = SchedulerBinding.instance.currentFrameTimeStamp; _startTime = SchedulerBinding.instance.currentFrameTimeStamp;
return _completer.future; return _completer.future;
} }
@ -48,29 +128,49 @@ class Ticker {
/// Stops calling the ticker's callback. /// Stops calling the ticker's callback.
/// ///
/// Causes the future returned by [start] to resolve. /// 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() { void stop() {
if (!isTicking) if (!isTicking)
return; 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 // We take the _completer into a local variable so that isTicking is false
// when we actually complete the future (isTicking uses _completer // when we actually complete the future (isTicking uses _completer to
// to determine its state). // determine its state).
Completer<Null> localCompleter = _completer; Completer<Null> localCompleter = _completer;
_completer = null; _completer = null;
_startTime = null;
assert(!isTicking); assert(!isTicking);
unscheduleTick();
localCompleter.complete(); 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) { void _tick(Duration timeStamp) {
assert(isTicking); assert(isTicking);
assert(_animationId != null); assert(scheduled);
_animationId = null; _animationId = null;
if (_startTime == null) if (_startTime == null)
@ -78,14 +178,90 @@ class Ticker {
_onTick(timeStamp - _startTime); _onTick(timeStamp - _startTime);
// The onTick callback may have scheduled another tick already. // The onTick callback may have scheduled another tick already, for
if (isTicking && _animationId == null) // example by calling stop then start again.
_scheduleTick(rescheduling: true); 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(isTicking);
assert(_animationId == null); assert(!scheduled);
assert(shouldScheduleTick);
_animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling); _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 'animated_size.dart';
import 'basic.dart'; import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'ticker_provider.dart';
import 'transitions.dart'; import 'transitions.dart';
/// Specifies which of the children to show. See [AnimatedCrossFade]. /// Specifies which of the children to show. See [AnimatedCrossFade].
@ -80,7 +81,7 @@ class AnimatedCrossFade extends StatefulWidget {
_AnimatedCrossFadeState createState() => new _AnimatedCrossFadeState(); _AnimatedCrossFadeState createState() => new _AnimatedCrossFadeState();
} }
class _AnimatedCrossFadeState extends State<AnimatedCrossFade> { class _AnimatedCrossFadeState extends State<AnimatedCrossFade> with TickerProviderStateMixin {
_AnimatedCrossFadeState() : super(); _AnimatedCrossFadeState() : super();
AnimationController _controller; AnimationController _controller;
@ -90,7 +91,7 @@ class _AnimatedCrossFadeState extends State<AnimatedCrossFade> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = new AnimationController(duration: config.duration); _controller = new AnimationController(duration: config.duration, vsync: this);
if (config.crossFadeState == CrossFadeState.showSecond) if (config.crossFadeState == CrossFadeState.showSecond)
_controller.value = 1.0; _controller.value = 1.0;
_firstAnimation = _initAnimation(config.firstCurve, true); _firstAnimation = _initAnimation(config.firstCurve, true);
@ -185,6 +186,7 @@ class _AnimatedCrossFadeState extends State<AnimatedCrossFade> {
alignment: FractionalOffset.topCenter, alignment: FractionalOffset.topCenter,
duration: config.duration, duration: config.duration,
curve: config.sizeCurve, curve: config.sizeCurve,
vsync: this,
child: new Stack( child: new Stack(
overflow: Overflow.visible, overflow: Overflow.visible,
children: children children: children

View File

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

View File

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

View File

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

View File

@ -1207,21 +1207,23 @@ class _InactiveElements {
} }
return true; return true;
}); });
element.unmount();
assert(element._debugLifecycleState == _ElementLifecycle.defunct);
element.visitChildren((Element child) { element.visitChildren((Element child) {
assert(child._parent == element); assert(child._parent == element);
_unmount(child); _unmount(child);
}); });
element.unmount();
assert(element._debugLifecycleState == _ElementLifecycle.defunct);
} }
void _unmountAll() { void _unmountAll() {
_locked = true;
final List<Element> elements = _elements.toList()..sort(Element._sort);
_elements.clear();
try { try {
_locked = true; for (Element element in elements.reversed)
for (Element element in _elements)
_unmount(element); _unmount(element);
} finally { } finally {
_elements.clear(); assert(_elements.isEmpty);
_locked = false; _locked = false;
} }
} }
@ -1365,7 +1367,8 @@ abstract class BuildContext {
/// [State.initState] methods, because those methods would not get called /// [State.initState] methods, because those methods would not get called
/// again if the inherited value were to change. To ensure that the widget /// again if the inherited value were to change. To ensure that the widget
/// correctly updates itself when the inherited value changes, only call this /// 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. /// 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 /// 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 /// Calling this method is O(1) with a small constant factor, but will lead to
/// the widget being rebuilt more often. /// 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); InheritedWidget inheritFromWidgetOfExactType(Type targetType);
/// Returns the nearest ancestor widget of the given type, which must be the /// 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; _dirtyElementsNeedsResorting = false;
int dirtyCount = _dirtyElements.length; int dirtyCount = _dirtyElements.length;
int index = 0; int index = 0;
@ -1668,7 +1677,7 @@ class BuildOwner {
} }
index += 1; index += 1;
if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) { if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) {
_dirtyElements.sort(_elementSort); _dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false; _dirtyElementsNeedsResorting = false;
dirtyCount = _dirtyElements.length; dirtyCount = _dirtyElements.length;
while (index > 0 && _dirtyElements[index - 1].dirty) { while (index > 0 && _dirtyElements[index - 1].dirty) {
@ -1715,18 +1724,6 @@ class BuildOwner {
assert(_debugStateLockLevel >= 0); 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 /// Complete the element build pass by unmounting any elements that are no
/// longer active. /// longer active.
/// ///
@ -1837,6 +1834,18 @@ abstract class Element implements BuildContext {
int get depth => _depth; int get depth => _depth;
int _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. /// The configuration for this element.
@override @override
Widget get widget => _widget; Widget get widget => _widget;
@ -2206,6 +2215,7 @@ abstract class Element implements BuildContext {
// We unregistered our dependencies in deactivate, but never cleared the list. // We unregistered our dependencies in deactivate, but never cleared the list.
// Since we're going to be reused, let's clear our list now. // Since we're going to be reused, let's clear our list now.
_dependencies?.clear(); _dependencies?.clear();
_hadUnsatisfiedDependencies = false;
_updateInheritance(); _updateInheritance();
assert(() { _debugLifecycleState = _ElementLifecycle.active; return true; }); assert(() { _debugLifecycleState = _ElementLifecycle.active; return true; });
} }
@ -2554,19 +2564,6 @@ abstract class BuildableElement extends Element {
owner._debugCurrentBuildTarget = this; owner._debugCurrentBuildTarget = this;
return true; 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(); performRebuild();
assert(() { assert(() {
assert(owner._debugCurrentBuildTarget == this); assert(owner._debugCurrentBuildTarget == this);

View File

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

View File

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

View File

@ -10,6 +10,7 @@ import 'binding.dart';
import 'focus.dart'; import 'focus.dart';
import 'framework.dart'; import 'framework.dart';
import 'overlay.dart'; import 'overlay.dart';
import 'ticker_provider.dart';
/// An abstraction for an entry managed by a [Navigator]. /// An abstraction for an entry managed by a [Navigator].
/// ///
@ -325,7 +326,7 @@ class Navigator extends StatefulWidget {
} }
/// The state for a [Navigator] widget. /// 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 GlobalKey<OverlayState> _overlayKey = new GlobalKey<OverlayState>();
final List<Route<dynamic>> _history = new List<Route<dynamic>>(); 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 // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:meta/meta.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:meta/meta.dart';
import 'basic.dart'; import 'basic.dart';
import 'debug.dart'; import 'debug.dart';
import 'framework.dart'; import 'framework.dart';
import 'ticker_provider.dart';
/// A place in an [Overlay] that can contain a widget. /// A place in an [Overlay] that can contain a widget.
/// ///
@ -116,9 +119,27 @@ class OverlayEntry {
final GlobalKey<_OverlayEntryState> _key = new GlobalKey<_OverlayEntryState>(); final GlobalKey<_OverlayEntryState> _key = new GlobalKey<_OverlayEntryState>();
/// Remove this entry from the overlay. /// 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() { void remove() {
_overlay?._remove(this); assert(_overlay != null);
OverlayState overlay = _overlay;
_overlay = null; _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. /// 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 /// Used to insert [OverlayEntry]s into the overlay using the [insert] and
/// [insertAll] functions. /// [insertAll] functions.
class OverlayState extends State<Overlay> { class OverlayState extends State<Overlay> with TickerProviderStateMixin {
final List<OverlayEntry> _entries = new List<OverlayEntry>(); final List<OverlayEntry> _entries = new List<OverlayEntry>();
@override @override
@ -268,9 +289,10 @@ class OverlayState extends State<Overlay> {
} }
void _remove(OverlayEntry entry) { void _remove(OverlayEntry entry) {
_entries.remove(entry); if (mounted) {
if (mounted) _entries.remove(entry);
setState(() { /* entry was removed */ }); setState(() { /* entry was removed */ });
}
} }
/// (DEBUG ONLY) Check whether a given entry is visible (i.e., not behind an /// (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) if (entry.opaque)
onstage = false; onstage = false;
} else if (entry.maintainState) { } else if (entry.maintainState) {
offstageChildren.add(new _OverlayEntry(entry)); offstageChildren.add(new TickerMode(enabled: false, child: new _OverlayEntry(entry)));
} }
} }
return new _Theatre( return new _Theatre(

View File

@ -122,7 +122,11 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
AnimationController createAnimationController() { AnimationController createAnimationController() {
Duration duration = transitionDuration; Duration duration = transitionDuration;
assert(duration != null && duration >= Duration.ZERO); 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 /// 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 'page_storage.dart';
import 'scroll_behavior.dart'; import 'scroll_behavior.dart';
import 'scroll_configuration.dart'; import 'scroll_configuration.dart';
import 'ticker_provider.dart';
/// Identifies one or both limits of a [Scrollable] in terms of its scrollDirection. /// Identifies one or both limits of a [Scrollable] in terms of its scrollDirection.
enum ScrollableEdge { enum ScrollableEdge {
@ -262,11 +263,11 @@ class Scrollable extends StatefulWidget {
/// terms of the [pixelOffsetToScrollOffset] and /// terms of the [pixelOffsetToScrollOffset] and
/// [scrollOffsetToPixelOffset] methods. /// [scrollOffsetToPixelOffset] methods.
@optionalTypeArgs @optionalTypeArgs
class ScrollableState<T extends Scrollable> extends State<T> { class ScrollableState<T extends Scrollable> extends State<T> with SingleTickerProviderStateMixin {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = new AnimationController.unbounded() _controller = new AnimationController.unbounded(vsync: this)
..addListener(_handleAnimationChanged) ..addListener(_handleAnimationChanged)
..addStatusListener(_handleAnimationStatusChanged); ..addStatusListener(_handleAnimationStatusChanged);
_scrollOffset = PageStorage.of(context)?.readState(context) ?? config.initialScrollOffset ?? 0.0; _scrollOffset = PageStorage.of(context)?.readState(context) ?? config.initialScrollOffset ?? 0.0;

View File

@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'basic.dart'; import 'basic.dart';
@ -82,6 +83,10 @@ class TextSelectionOverlay implements TextSelectionDelegate {
this.toolbarBuilder this.toolbarBuilder
}): _input = input { }): _input = input {
assert(context != null); 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. /// The context in which the selection handles should appear.
@ -117,8 +122,8 @@ class TextSelectionOverlay implements TextSelectionDelegate {
/// Controls the fade-in animations. /// Controls the fade-in animations.
static const Duration _kFadeDuration = const Duration(milliseconds: 150); static const Duration _kFadeDuration = const Duration(milliseconds: 150);
final AnimationController _handleController = new AnimationController(duration: _kFadeDuration); AnimationController _handleController;
final AnimationController _toolbarController = new AnimationController(duration: _kFadeDuration); AnimationController _toolbarController;
Animation<double> get _handleOpacity => _handleController.view; Animation<double> get _handleOpacity => _handleController.view;
Animation<double> get _toolbarOpacity => _toolbarController.view; Animation<double> get _toolbarOpacity => _toolbarController.view;
@ -153,11 +158,26 @@ class TextSelectionOverlay implements TextSelectionDelegate {
} }
/// Updates the overlay after the [selection] has changed. /// 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) { void update(InputValue newInput) {
if (_input == newInput) if (_input == newInput)
return; return;
_input = newInput; _input = newInput;
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback(_markNeedsBuild);
} else {
_markNeedsBuild();
}
}
void _markNeedsBuild([Duration duration]) {
if (_handles != null) { if (_handles != null) {
_handles[0].markNeedsBuild(); _handles[0].markNeedsBuild();
_handles[1].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/routes.dart';
export 'src/widgets/scroll_behavior.dart'; export 'src/widgets/scroll_behavior.dart';
export 'src/widgets/scroll_configuration.dart'; export 'src/widgets/scroll_configuration.dart';
export 'src/widgets/scrollable.dart';
export 'src/widgets/scrollable_grid.dart'; export 'src/widgets/scrollable_grid.dart';
export 'src/widgets/scrollable_list.dart'; export 'src/widgets/scrollable_list.dart';
export 'src/widgets/scrollable.dart';
export 'src/widgets/semantics_debugger.dart'; export 'src/widgets/semantics_debugger.dart';
export 'src/widgets/size_changed_layout_notifier.dart'; export 'src/widgets/size_changed_layout_notifier.dart';
export 'src/widgets/status_transitions.dart'; export 'src/widgets/status_transitions.dart';
export 'src/widgets/table.dart'; export 'src/widgets/table.dart';
export 'src/widgets/text_selection.dart';
export 'src/widgets/text.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/title.dart';
export 'src/widgets/transitions.dart'; export 'src/widgets/transitions.dart';
export 'src/widgets/unique_widget.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/animation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'animation_tester.dart';
void main() { void main() {
setUp(() { setUp(() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -14,7 +16,8 @@ void main() {
test('Can set value during status callback', () { test('Can set value during status callback', () {
AnimationController controller = new AnimationController( AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 100) duration: const Duration(milliseconds: 100),
vsync: const TestVSync(),
); );
bool didComplete = false; bool didComplete = false;
bool didDismiss = false; bool didDismiss = false;
@ -45,7 +48,8 @@ void main() {
test('Receives status callbacks for forward and reverse', () { test('Receives status callbacks for forward and reverse', () {
AnimationController controller = new AnimationController( AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 100) duration: const Duration(milliseconds: 100),
vsync: const TestVSync(),
); );
List<double> valueLog = <double>[]; List<double> valueLog = <double>[];
List<AnimationStatus> log = <AnimationStatus>[]; List<AnimationStatus> log = <AnimationStatus>[];
@ -107,7 +111,8 @@ void main() {
test('Forward and reverse from values', () { test('Forward and reverse from values', () {
AnimationController controller = new AnimationController( AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 100) duration: const Duration(milliseconds: 100),
vsync: const TestVSync(),
); );
List<double> valueLog = <double>[]; List<double> valueLog = <double>[];
List<AnimationStatus> statusLog = <AnimationStatus>[]; List<AnimationStatus> statusLog = <AnimationStatus>[];
@ -134,7 +139,8 @@ void main() {
test('Forward only from value', () { test('Forward only from value', () {
AnimationController controller = new AnimationController( AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 100) duration: const Duration(milliseconds: 100),
vsync: const TestVSync(),
); );
List<double> valueLog = <double>[]; List<double> valueLog = <double>[];
List<AnimationStatus> statusLog = <AnimationStatus>[]; List<AnimationStatus> statusLog = <AnimationStatus>[];
@ -154,7 +160,8 @@ void main() {
test('Can fling to upper and lower bounds', () { test('Can fling to upper and lower bounds', () {
AnimationController controller = new AnimationController( AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 100) duration: const Duration(milliseconds: 100),
vsync: const TestVSync(),
); );
controller.fling(); controller.fling();
@ -166,7 +173,8 @@ void main() {
AnimationController largeRangeController = new AnimationController( AnimationController largeRangeController = new AnimationController(
duration: const Duration(milliseconds: 100), duration: const Duration(milliseconds: 100),
lowerBound: -30.0, lowerBound: -30.0,
upperBound: 45.0 upperBound: 45.0,
vsync: const TestVSync(),
); );
largeRangeController.fling(); largeRangeController.fling();
@ -182,7 +190,8 @@ void main() {
test('lastElapsedDuration control test', () { test('lastElapsedDuration control test', () {
AnimationController controller = new AnimationController( AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 100) duration: const Duration(milliseconds: 100),
vsync: const TestVSync(),
); );
controller.forward(); controller.forward();
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 20)); WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 20));
@ -194,7 +203,8 @@ void main() {
test('toString control test', () { test('toString control test', () {
AnimationController controller = new AnimationController( AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 100) duration: const Duration(milliseconds: 100),
vsync: const TestVSync(),
); );
expect(controller.toString(), hasOneLineDescription); expect(controller.toString(), hasOneLineDescription);
controller.forward(); 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/animation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'animation_tester.dart';
void main() { void main() {
setUp(() { setUp(() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -24,7 +26,8 @@ void main() {
curvedAnimation.reverseCurve = Curves.elasticOut; curvedAnimation.reverseCurve = Curves.elasticOut;
expect(curvedAnimation.toString(), hasOneLineDescription); expect(curvedAnimation.toString(), hasOneLineDescription);
AnimationController controller = new AnimationController( AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 500) duration: const Duration(milliseconds: 500),
vsync: const TestVSync(),
); );
controller controller
..value = 0.5 ..value = 0.5
@ -48,7 +51,9 @@ void main() {
}); });
test('ProxyAnimation set parent generates value changed', () { test('ProxyAnimation set parent generates value changed', () {
AnimationController controller = new AnimationController(); AnimationController controller = new AnimationController(
vsync: const TestVSync(),
);
controller.value = 0.5; controller.value = 0.5;
bool didReceiveCallback = false; bool didReceiveCallback = false;
ProxyAnimation animation = new ProxyAnimation() ProxyAnimation animation = new ProxyAnimation()
@ -65,7 +70,9 @@ void main() {
}); });
test('ReverseAnimation calls listeners', () { test('ReverseAnimation calls listeners', () {
AnimationController controller = new AnimationController(); AnimationController controller = new AnimationController(
vsync: const TestVSync(),
);
controller.value = 0.5; controller.value = 0.5;
bool didReceiveCallback = false; bool didReceiveCallback = false;
void listener() { void listener() {
@ -85,8 +92,12 @@ void main() {
}); });
test('TrainHoppingAnimation', () { test('TrainHoppingAnimation', () {
AnimationController currentTrain = new AnimationController(); AnimationController currentTrain = new AnimationController(
AnimationController nextTrain = new AnimationController(); vsync: const TestVSync(),
);
AnimationController nextTrain = new AnimationController(
vsync: const TestVSync(),
);
currentTrain.value = 0.5; currentTrain.value = 0.5;
nextTrain.value = 0.75; nextTrain.value = 0.75;
bool didSwitchTrains = false; bool didSwitchTrains = false;

View File

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

View File

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

View File

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

View File

@ -23,7 +23,8 @@ void main() {
) )
); );
final AnimationController controller = new AnimationController( final AnimationController controller = new AnimationController(
duration: const Duration(seconds: 10) duration: const Duration(seconds: 10),
vsync: tester,
); );
final List<Size> sizes = <Size>[]; final List<Size> sizes = <Size>[];
final List<Offset> positions = <Offset>[]; 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. /// test failures.
bool showAppDumpInErrors = false; 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. /// advance time.
/// ///
/// Returns a future which completes when the test has run. /// Returns a future which completes when the test has run.
/// ///
/// Called by the [testWidgets] and [benchmarkWidgets] functions to /// Called by the [testWidgets] and [benchmarkWidgets] functions to
/// run a test. /// 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 /// This is called during test execution before and after the body has been
/// executed. /// executed.
@ -266,7 +269,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
_currentTestCompleter.complete(null); _currentTestCompleter.complete(null);
} }
Future<Null> _runTest(Future<Null> callback()) { Future<Null> _runTest(Future<Null> testBody(), VoidCallback invariantTester) {
assert(inTest); assert(inTest);
_oldExceptionHandler = FlutterError.onError; _oldExceptionHandler = FlutterError.onError;
int _exceptionCount = 0; // number of un-taken exceptions int _exceptionCount = 0; // number of un-taken exceptions
@ -357,20 +360,20 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
); );
_parentZone = Zone.current; _parentZone = Zone.current;
Zone testZone = _parentZone.fork(specification: errorHandlingZoneSpecification); Zone testZone = _parentZone.fork(specification: errorHandlingZoneSpecification);
testZone.runUnaryGuarded(_runTestBody, callback) testZone.runBinaryGuarded(_runTestBody, testBody, invariantTester)
.whenComplete(_testCompletionHandler); .whenComplete(_testCompletionHandler);
asyncBarrier(); // When using AutomatedTestWidgetsFlutterBinding, this flushes the microtasks. asyncBarrier(); // When using AutomatedTestWidgetsFlutterBinding, this flushes the microtasks.
return _currentTestCompleter.future; return _currentTestCompleter.future;
} }
Future<Null> _runTestBody(Future<Null> callback()) async { Future<Null> _runTestBody(Future<Null> testBody(), VoidCallback invariantTester) async {
assert(inTest); assert(inTest);
runApp(new Container(key: new UniqueKey(), child: _kPreTestMessage)); // Reset the tree to a known state. runApp(new Container(key: new UniqueKey(), child: _kPreTestMessage)); // Reset the tree to a known state.
await pump(); await pump();
// run the test // run the test
await callback(); await testBody();
asyncBarrier(); // drains the microtasks in `flutter test` mode (when using AutomatedTestWidgetsFlutterBinding) asyncBarrier(); // drains the microtasks in `flutter test` mode (when using AutomatedTestWidgetsFlutterBinding)
if (_pendingExceptionDetails == null) { if (_pendingExceptionDetails == null) {
@ -379,6 +382,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
// alone so that we don't cause more spurious errors. // alone so that we don't cause more spurious errors.
runApp(new Container(key: new UniqueKey(), child: _kPostTestMessage)); // Unmount any remaining widgets. runApp(new Container(key: new UniqueKey(), child: _kPostTestMessage)); // Unmount any remaining widgets.
await pump(); await pump();
invariantTester();
_verifyInvariants(); _verifyInvariants();
} }
@ -489,24 +493,24 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
} }
@override @override
Future<Null> runTest(Future<Null> callback()) { Future<Null> runTest(Future<Null> testBody(), VoidCallback invariantTester) {
assert(!inTest); assert(!inTest);
assert(_fakeAsync == null); assert(_fakeAsync == null);
assert(_clock == null); assert(_clock == null);
_fakeAsync = new FakeAsync(); _fakeAsync = new FakeAsync();
_clock = _fakeAsync.getClock(new DateTime.utc(2015, 1, 1)); _clock = _fakeAsync.getClock(new DateTime.utc(2015, 1, 1));
Future<Null> callbackResult; Future<Null> testBodyResult;
_fakeAsync.run((FakeAsync fakeAsync) { _fakeAsync.run((FakeAsync fakeAsync) {
assert(fakeAsync == _fakeAsync); assert(fakeAsync == _fakeAsync);
callbackResult = _runTest(callback); testBodyResult = _runTest(testBody, invariantTester);
assert(inTest); 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), // 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_. // 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 // To avoid this, we wrap it in a Future that we've created _outside_ the fake
// async zone. // async zone.
return new Future<Null>.value(callbackResult); return new Future<Null>.value(testBodyResult);
} }
@override @override
@ -686,10 +690,10 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
} }
@override @override
Future<Null> runTest(Future<Null> callback()) async { Future<Null> runTest(Future<Null> testBody(), VoidCallback invariantTester) async {
assert(!inTest); assert(!inTest);
_inTest = true; _inTest = true;
return _runTest(callback); return _runTest(testBody, invariantTester);
} }
@override @override

View File

@ -6,6 +6,7 @@ import 'dart:async';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:test/test.dart' as test_package; import 'package:test/test.dart' as test_package;
@ -50,7 +51,7 @@ void testWidgets(String description, WidgetTesterCallback callback, {
WidgetTester tester = new WidgetTester._(binding); WidgetTester tester = new WidgetTester._(binding);
timeout ??= binding.defaultTestTimeout; timeout ??= binding.defaultTestTimeout;
test_package.group('-', () { 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); test_package.tearDown(binding.postTest);
}, timeout: timeout); }, timeout: timeout);
} }
@ -108,7 +109,7 @@ Future<Null> benchmarkWidgets(WidgetTesterCallback callback) {
TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
assert(binding is! AutomatedTestWidgetsFlutterBinding); assert(binding is! AutomatedTestWidgetsFlutterBinding);
WidgetTester tester = new WidgetTester._(binding); 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`. /// 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 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) { WidgetTester._(TestWidgetsFlutterBinding binding) : super(binding) {
if (binding is LiveTestWidgetsFlutterBinding) if (binding is LiveTestWidgetsFlutterBinding)
binding.deviceEventDispatcher = this; binding.deviceEventDispatcher = this;
@ -328,6 +332,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher {
} }
bool _isPrivate(Type type) { bool _isPrivate(Type type) {
// used above so that we don't suggest matchers for private types
return '_'.matchAsPrefix(type.toString()) != null; return '_'.matchAsPrefix(type.toString()) != null;
} }
@ -348,4 +353,62 @@ class WidgetTester extends WidgetController implements HitTestDispatcher {
Future<Null> idle() { Future<Null> idle() {
return TestAsyncUtils.guard(() => binding.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')); await tester.pumpWidget(new Text('foo'));
int count; 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)); count = await tester.pumpUntilNoTransientCallbacks(const Duration(seconds: 1));
expect(count, 0); expect(count, 0);