From 9e673853e55ddb00adab56adbd3bf74e5b5e9cd7 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Mon, 26 Sep 2016 10:57:10 -0700 Subject: [PATCH] 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. --- .../lib/demo/animation_demo.dart | 43 ++-- .../lib/demo/bottom_navigation_demo.dart | 42 ++-- .../lib/demo/progress_indicator_demo.dart | 5 +- .../flutter_gallery/lib/gallery/home.dart | 15 +- .../layers/rendering/spinning_square.dart | 10 +- examples/layers/services/isolate.dart | 5 +- examples/layers/widgets/spinning_square.dart | 5 +- .../src/animation/animation_controller.dart | 62 +++-- .../flutter/lib/src/animation/animations.dart | 1 + .../lib/src/animation/listener_helpers.dart | 8 +- .../src/material/bottom_navigation_bar.dart | 109 ++++---- .../lib/src/material/bottom_sheet.dart | 7 +- .../flutter/lib/src/material/checkbox.dart | 39 ++- .../flutter/lib/src/material/data_table.dart | 8 +- packages/flutter/lib/src/material/drawer.dart | 4 +- .../flutter/lib/src/material/expand_icon.dart | 4 +- .../flutter/lib/src/material/material.dart | 49 +++- .../lib/src/material/mergeable_material.dart | 5 +- .../src/material/overscroll_indicator.dart | 19 +- .../flutter/lib/src/material/popup_menu.dart | 4 +- .../lib/src/material/progress_indicator.dart | 16 +- packages/flutter/lib/src/material/radio.dart | 43 ++-- .../lib/src/material/refresh_indicator.dart | 19 +- .../flutter/lib/src/material/scaffold.dart | 43 ++-- .../flutter/lib/src/material/scrollbar.dart | 5 +- packages/flutter/lib/src/material/slider.dart | 42 +++- .../flutter/lib/src/material/snack_bar.dart | 5 +- .../flutter/lib/src/material/stepper.dart | 5 +- packages/flutter/lib/src/material/switch.dart | 52 ++-- packages/flutter/lib/src/material/tabs.dart | 30 ++- .../flutter/lib/src/material/time_picker.dart | 7 +- .../flutter/lib/src/material/toggleable.dart | 113 +++++---- .../flutter/lib/src/material/tooltip.dart | 6 +- .../lib/src/material/two_level_list.dart | 10 +- .../lib/src/rendering/animated_size.dart | 51 ++-- .../flutter/lib/src/rendering/binding.dart | 2 +- .../flutter/lib/src/scheduler/binding.dart | 58 ++++- .../flutter/lib/src/scheduler/ticker.dart | 232 +++++++++++++++--- .../lib/src/widgets/animated_cross_fade.dart | 6 +- .../lib/src/widgets/animated_size.dart | 13 +- .../flutter/lib/src/widgets/dismissable.dart | 9 +- .../flutter/lib/src/widgets/editable.dart | 18 +- .../flutter/lib/src/widgets/framework.dart | 63 +++-- .../lib/src/widgets/implicit_animations.dart | 6 +- packages/flutter/lib/src/widgets/mimic.dart | 13 +- .../flutter/lib/src/widgets/navigator.dart | 3 +- packages/flutter/lib/src/widgets/overlay.dart | 34 ++- packages/flutter/lib/src/widgets/routes.dart | 6 +- .../flutter/lib/src/widgets/scrollable.dart | 5 +- .../lib/src/widgets/text_selection.dart | 26 +- .../lib/src/widgets/ticker_provider.dart | 220 +++++++++++++++++ packages/flutter/lib/widgets.dart | 5 +- .../animation/animation_controller_test.dart | 26 +- .../test/animation/animation_tester.dart | 12 + .../test/animation/animations_test.dart | 21 +- .../flutter/test/animation/tween_test.dart | 10 +- .../test/widget/animated_size_test.dart | 7 + packages/flutter/test/widget/flow_test.dart | 4 +- .../flutter/test/widget/positioned_test.dart | 3 +- .../test/widget/ticker_provider_test.dart | 53 ++++ packages/flutter_test/lib/src/binding.dart | 30 ++- .../flutter_test/lib/src/widget_tester.dart | 69 +++++- .../flutter_test/test/widget_tester_test.dart | 5 +- 63 files changed, 1358 insertions(+), 492 deletions(-) create mode 100644 packages/flutter/lib/src/widgets/ticker_provider.dart create mode 100644 packages/flutter/test/animation/animation_tester.dart create mode 100644 packages/flutter/test/widget/ticker_provider_test.dart diff --git a/examples/flutter_gallery/lib/demo/animation_demo.dart b/examples/flutter_gallery/lib/demo/animation_demo.dart index 30f7ea26a5a..ee1e662cb1b 100644 --- a/examples/flutter_gallery/lib/demo/animation_demo.dart +++ b/examples/flutter_gallery/lib/demo/animation_demo.dart @@ -393,11 +393,14 @@ class _RectangleDemoState extends State<_RectangleDemo> { typedef Widget _DemoBuilder(_ArcDemo demo); class _ArcDemo { - _ArcDemo(String _title, this.builder) : title = _title, key = new GlobalKey(debugLabel: _title); + _ArcDemo(String _title, this.builder, TickerProvider vsync) + : title = _title, + controller = new AnimationController(duration: const Duration(milliseconds: 500), vsync: vsync), + key = new GlobalKey(debugLabel: _title); - final AnimationController controller = new AnimationController(duration: const Duration(milliseconds: 500)); final String title; final _DemoBuilder builder; + final AnimationController controller; final GlobalKey key; } @@ -410,23 +413,29 @@ class AnimationDemo extends StatefulWidget { _AnimationDemoState createState() => new _AnimationDemoState(); } -class _AnimationDemoState extends State { +class _AnimationDemoState extends State with TickerProviderStateMixin { static final GlobalKey> _tabsKey = new GlobalKey>(); - static final List<_ArcDemo> _allDemos = <_ArcDemo>[ - new _ArcDemo('POINT', (_ArcDemo demo) { - return new _PointDemo( - key: demo.key, - controller: demo.controller - ); - }), - new _ArcDemo('RECTANGLE', (_ArcDemo demo) { - return new _RectangleDemo( - key: demo.key, - controller: demo.controller - ); - }) - ]; + List<_ArcDemo> _allDemos; + + @override + void initState() { + super.initState(); + _allDemos = <_ArcDemo>[ + new _ArcDemo('POINT', (_ArcDemo demo) { + return new _PointDemo( + key: demo.key, + controller: demo.controller + ); + }, this), + new _ArcDemo('RECTANGLE', (_ArcDemo demo) { + return new _RectangleDemo( + key: demo.key, + controller: demo.controller + ); + }, this), + ]; + } Future _play() async { _ArcDemo demo = _tabsKey.currentState.value; diff --git a/examples/flutter_gallery/lib/demo/bottom_navigation_demo.dart b/examples/flutter_gallery/lib/demo/bottom_navigation_demo.dart index ad4f9d481ce..c6e74f72b15 100644 --- a/examples/flutter_gallery/lib/demo/bottom_navigation_demo.dart +++ b/examples/flutter_gallery/lib/demo/bottom_navigation_demo.dart @@ -8,7 +8,8 @@ class NavigationIconView { NavigationIconView({ Icon icon, Widget title, - Color color + Color color, + TickerProvider vsync, }) : _icon = icon, _color = color, destinationLabel = new DestinationLabel( @@ -17,13 +18,14 @@ class NavigationIconView { backgroundColor: color ), controller = new AnimationController( - duration: kThemeAnimationDuration + duration: kThemeAnimationDuration, + vsync: vsync, ) { - _animation = new CurvedAnimation( - parent: controller, - curve: new Interval(0.5, 1.0, curve: Curves.fastOutSlowIn) - ); - } + _animation = new CurvedAnimation( + parent: controller, + curve: new Interval(0.5, 1.0, curve: Curves.fastOutSlowIn) + ); + } final Icon _icon; final Color _color; @@ -61,7 +63,7 @@ class BottomNavigationDemo extends StatefulWidget { _BottomNavigationDemoState createState() => new _BottomNavigationDemoState(); } -class _BottomNavigationDemoState extends State { +class _BottomNavigationDemoState extends State with TickerProviderStateMixin { int _currentIndex = 0; BottomNavigationBarType _type = BottomNavigationBarType.shifting; List _navigationViews; @@ -73,22 +75,26 @@ class _BottomNavigationDemoState extends State { new NavigationIconView( icon: new Icon(Icons.access_alarm), title: new Text('Alarm'), - color: Colors.deepPurple[500] + color: Colors.deepPurple[500], + vsync: this, ), new NavigationIconView( icon: new Icon(Icons.cloud), title: new Text('Cloud'), - color: Colors.teal[500] + color: Colors.teal[500], + vsync: this, ), new NavigationIconView( icon: new Icon(Icons.favorite), title: new Text('Favorites'), - color: Colors.indigo[500] + color: Colors.indigo[500], + vsync: this, ), new NavigationIconView( icon: new Icon(Icons.event_available), title: new Text('Event'), - color: Colors.pink[500] + color: Colors.pink[500], + vsync: this, ) ]; @@ -124,14 +130,12 @@ class _BottomNavigationDemoState extends State { return aValue.compareTo(bValue); }); - return new Stack( - children: transitions - ); + return new Stack(children: transitions); } @override Widget build(BuildContext context) { - BottomNavigationBar botNavBar = new BottomNavigationBar( + final BottomNavigationBar botNavBar = new BottomNavigationBar( labels: _navigationViews.map( (NavigationIconView navigationView) => navigationView.destinationLabel ).toList(), @@ -159,18 +163,18 @@ class _BottomNavigationDemoState extends State { itemBuilder: (BuildContext context) => >[ new PopupMenuItem( value: BottomNavigationBarType.fixed, - child: new Text('Fixed') + child: new Text('Fixed'), ), new PopupMenuItem( value: BottomNavigationBarType.shifting, - child: new Text('Shifting') + child: new Text('Shifting'), ) ] ) ] ), body: _buildBody(), - bottomNavigationBar: botNavBar + bottomNavigationBar: botNavBar, ); } } diff --git a/examples/flutter_gallery/lib/demo/progress_indicator_demo.dart b/examples/flutter_gallery/lib/demo/progress_indicator_demo.dart index ff76a5c22bf..f4bb242f82a 100644 --- a/examples/flutter_gallery/lib/demo/progress_indicator_demo.dart +++ b/examples/flutter_gallery/lib/demo/progress_indicator_demo.dart @@ -11,7 +11,7 @@ class ProgressIndicatorDemo extends StatefulWidget { _ProgressIndicatorDemoState createState() => new _ProgressIndicatorDemoState(); } -class _ProgressIndicatorDemoState extends State { +class _ProgressIndicatorDemoState extends State with SingleTickerProviderStateMixin { AnimationController _controller; Animation _animation; @@ -19,7 +19,8 @@ class _ProgressIndicatorDemoState extends State { void initState() { super.initState(); _controller = new AnimationController( - duration: const Duration(milliseconds: 1500) + duration: const Duration(milliseconds: 1500), + vsync: this, )..forward(); _animation = new CurvedAnimation( diff --git a/examples/flutter_gallery/lib/gallery/home.dart b/examples/flutter_gallery/lib/gallery/home.dart index f61f35e5e15..7882cf14688 100644 --- a/examples/flutter_gallery/lib/gallery/home.dart +++ b/examples/flutter_gallery/lib/gallery/home.dart @@ -95,19 +95,20 @@ class GalleryHome extends StatefulWidget { GalleryHomeState createState() => new GalleryHomeState(); } -class GalleryHomeState extends State { - static final Key _homeKey = new ValueKey("Gallery Home"); +class GalleryHomeState extends State with SingleTickerProviderStateMixin { + static final Key _homeKey = new ValueKey('Gallery Home'); static final GlobalKey _scrollableKey = new GlobalKey(); - final AnimationController _controller = new AnimationController( - duration: const Duration(milliseconds: 600), - debugLabel: "preview banner" - ); + AnimationController _controller; @override void initState() { super.initState(); - _controller.forward(); + _controller = new AnimationController( + duration: const Duration(milliseconds: 600), + debugLabel: 'preview banner', + vsync: this, + )..forward(); } @override diff --git a/examples/layers/rendering/spinning_square.dart b/examples/layers/rendering/spinning_square.dart index 6d9c2718a3f..aa81d051ed2 100644 --- a/examples/layers/rendering/spinning_square.dart +++ b/examples/layers/rendering/spinning_square.dart @@ -9,6 +9,13 @@ import 'dart:math' as math; import 'package:flutter/animation.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; + +class NonStopVSync implements TickerProvider { + const NonStopVSync(); + @override + Ticker createTicker(TickerCallback onTick) => new Ticker(onTick); +} void main() { // We first create a render object that represents a green box. @@ -41,7 +48,8 @@ void main() { // To make the square spin, we use an animation that repeats every 1800 // milliseconds. AnimationController animation = new AnimationController( - duration: const Duration(milliseconds: 1800) + duration: const Duration(milliseconds: 1800), + vsync: const NonStopVSync(), )..repeat(); // The animation will produce a value between 0.0 and 1.0 each frame, but we // want to rotate the square using a value between 0.0 and math.PI. To change diff --git a/examples/layers/services/isolate.dart b/examples/layers/services/isolate.dart index c588afe69fa..208bb7efea6 100644 --- a/examples/layers/services/isolate.dart +++ b/examples/layers/services/isolate.dart @@ -206,7 +206,7 @@ class IsolateExampleWidget extends StatefulWidget { } // Main application state. -class IsolateExampleState extends State { +class IsolateExampleState extends State with SingleTickerProviderStateMixin { String _status = 'Idle'; String _label = 'Start'; @@ -219,7 +219,8 @@ class IsolateExampleState extends State { void initState() { super.initState(); _animation = new AnimationController( - duration: const Duration(milliseconds: 3600) + duration: const Duration(milliseconds: 3600), + vsync: this, )..repeat(); _calculationManager = new CalculationManager( onProgressListener: _handleProgressUpdate, diff --git a/examples/layers/widgets/spinning_square.dart b/examples/layers/widgets/spinning_square.dart index 155ce6114cc..e9a4f7b903c 100644 --- a/examples/layers/widgets/spinning_square.dart +++ b/examples/layers/widgets/spinning_square.dart @@ -9,7 +9,7 @@ class SpinningSquare extends StatefulWidget { _SpinningSquareState createState() => new _SpinningSquareState(); } -class _SpinningSquareState extends State { +class _SpinningSquareState extends State with SingleTickerProviderStateMixin { AnimationController _animation; @override @@ -19,7 +19,8 @@ class _SpinningSquareState extends State { // represents an entire turn of the square whereas in the other examples // we used 0.0 -> math.PI, which is only half a turn. _animation = new AnimationController( - duration: const Duration(milliseconds: 3600) + duration: const Duration(milliseconds: 3600), + vsync: this, )..repeat(); } diff --git a/packages/flutter/lib/src/animation/animation_controller.dart b/packages/flutter/lib/src/animation/animation_controller.dart index a7bf24fb42d..df4d5bdd5c3 100644 --- a/packages/flutter/lib/src/animation/animation_controller.dart +++ b/packages/flutter/lib/src/animation/animation_controller.dart @@ -5,8 +5,9 @@ import 'dart:async'; import 'dart:ui' as ui show lerpDouble; -import 'package:flutter/scheduler.dart'; import 'package:flutter/physics.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:meta/meta.dart'; import 'animation.dart'; import 'curves.dart'; @@ -19,7 +20,7 @@ enum _AnimationDirection { forward, /// The animation is running backwards, from end to beginning. - reverse + reverse, } /// A controller for an animation. @@ -40,29 +41,33 @@ class AnimationController extends Animation /// Creates an animation controller. /// - /// * value is the initial value of the animation. - /// * duration is the length of time this animation should last. - /// * debugLabel is a string to help identify this animation during debugging (used by toString). - /// * lowerBound is the smallest value this animation can obtain and the value at which this animation is deemed to be dismissed. - /// * upperBound is the largest value this animation can obtain and the value at which this animation is deemed to be completed. + /// * [value] is the initial value of the animation. + /// * [duration] is the length of time this animation should last. + /// * [debugLabel] is a string to help identify this animation during debugging (used by [toString]). + /// * [lowerBound] is the smallest value this animation can obtain and the value at which this animation is deemed to be dismissed. + /// * [upperBound] is the largest value this animation can obtain and the value at which this animation is deemed to be completed. + /// * `vsync` is the [TickerProvider] for the current context. It can be changed by calling [resync]. AnimationController({ double value, this.duration, this.debugLabel, this.lowerBound: 0.0, - this.upperBound: 1.0 + this.upperBound: 1.0, + @required TickerProvider vsync, }) { assert(upperBound >= lowerBound); + assert(vsync != null); _direction = _AnimationDirection.forward; - _ticker = new Ticker(_tick); + _ticker = vsync.createTicker(_tick); _internalSetValue(value ?? lowerBound); } /// Creates an animation controller with no upper or lower bound for its value. /// - /// * value is the initial value of the animation. - /// * duration is the length of time this animation should last. - /// * debugLabel is a string to help identify this animation during debugging (used by toString). + /// * [value] is the initial value of the animation. + /// * [duration] is the length of time this animation should last. + /// * [debugLabel] is a string to help identify this animation during debugging (used by [toString]). + /// * `vsync` is the [TickerProvider] for the current context. It can be changed by calling [resync]. /// /// This constructor is most useful for animations that will be driven using a /// physics simulation, especially when the physics simulation has no @@ -70,12 +75,14 @@ class AnimationController extends Animation AnimationController.unbounded({ double value: 0.0, this.duration, - this.debugLabel + this.debugLabel, + @required TickerProvider vsync, }) : lowerBound = double.NEGATIVE_INFINITY, upperBound = double.INFINITY { assert(value != null); + assert(vsync != null); _direction = _AnimationDirection.forward; - _ticker = new Ticker(_tick); + _ticker = vsync.createTicker(_tick); _internalSetValue(value); } @@ -98,6 +105,14 @@ class AnimationController extends Animation Duration duration; Ticker _ticker; + + /// Recreates the [Ticker] with the new [TickerProvider]. + void resync(TickerProvider vsync) { + Ticker oldTicker = _ticker; + _ticker = vsync.createTicker(_tick); + _ticker.absorbTicker(oldTicker); + } + Simulation _simulation; /// The current value of the animation. @@ -145,7 +160,12 @@ class AnimationController extends Animation Duration _lastElapsedDuration; /// Whether this animation is currently animating in either the forward or reverse direction. - bool get isAnimating => _ticker.isTicking; + /// + /// This is separate from whether it is actively ticking. An animation + /// controller's ticker might get muted, in which case the animation + /// controller's callbacks will no longer fire even though time is continuing + /// to pass. See [Ticker.muted] and [TickerMode]. + bool get isAnimating => _ticker.isActive; _AnimationDirection _direction; @@ -239,16 +259,21 @@ class AnimationController extends Animation } /// Stops running this animation. + /// + /// This does not trigger any notifications. The animation stops in its + /// current state. void stop() { _simulation = null; _lastElapsedDuration = null; _ticker.stop(); } - /// Stops running this animation. + /// Release the resources used by this object. The object is no longer usable + /// after this method is called. @override void dispose() { - stop(); + _ticker.dispose(); + super.dispose(); } AnimationStatus _lastReportedStatus = AnimationStatus.dismissed; @@ -277,9 +302,10 @@ class AnimationController extends Animation @override String toStringDetails() { String paused = isAnimating ? '' : '; paused'; + String silenced = _ticker.muted ? '; silenced' : ''; String label = debugLabel == null ? '' : '; for $debugLabel'; String more = '${super.toStringDetails()} ${value.toStringAsFixed(3)}'; - return '$more$paused$label'; + return '$more$paused$silenced$label'; } } diff --git a/packages/flutter/lib/src/animation/animations.dart b/packages/flutter/lib/src/animation/animations.dart index 0337c36dbe5..ad9ad9c6eb6 100644 --- a/packages/flutter/lib/src/animation/animations.dart +++ b/packages/flutter/lib/src/animation/animations.dart @@ -524,6 +524,7 @@ class TrainHoppingAnimation extends Animation _currentTrain = null; _nextTrain?.removeListener(_valueChangeHandler); _nextTrain = null; + super.dispose(); } @override diff --git a/packages/flutter/lib/src/animation/listener_helpers.dart b/packages/flutter/lib/src/animation/listener_helpers.dart index 7b8e4c1bd37..77c9f9a5e79 100644 --- a/packages/flutter/lib/src/animation/listener_helpers.dart +++ b/packages/flutter/lib/src/animation/listener_helpers.dart @@ -4,6 +4,8 @@ import 'dart:ui' show VoidCallback; +import 'package:meta/meta.dart'; + import 'animation.dart'; abstract class _ListenerMixin { @@ -50,8 +52,10 @@ abstract class AnimationEagerListenerMixin implements _ListenerMixin { @override void didUnregisterListener() { } - /// Release any resources used by this object. - void dispose(); + /// Release the resources used by this object. The object is no longer usable + /// after this method is called. + @mustCallSuper + void dispose() { } } /// A mixin that implements the addListener/removeListener protocol and notifies diff --git a/packages/flutter/lib/src/material/bottom_navigation_bar.dart b/packages/flutter/lib/src/material/bottom_navigation_bar.dart index 59cab1c154b..54d24f2c7de 100644 --- a/packages/flutter/lib/src/material/bottom_navigation_bar.dart +++ b/packages/flutter/lib/src/material/bottom_navigation_bar.dart @@ -139,7 +139,7 @@ class BottomNavigationBar extends StatefulWidget { BottomNavigationBarState createState() => new BottomNavigationBarState(); } -class BottomNavigationBarState extends State { +class BottomNavigationBarState extends State with TickerProviderStateMixin { List _controllers; List animations; double _weight; @@ -156,7 +156,8 @@ class BottomNavigationBarState extends State { super.initState(); _controllers = new List.generate(config.labels.length, (int index) { return new AnimationController( - duration: kThemeAnimationDuration + duration: kThemeAnimationDuration, + vsync: this, )..addListener(_rebuild); }); animations = new List.generate(config.labels.length, (int index) { @@ -166,9 +167,7 @@ class BottomNavigationBarState extends State { reverseCurve: Curves.fastOutSlowIn.flipped ); }); - _controllers[config.currentIndex].value = 1.0; - _backgroundColor = config.labels[config.currentIndex].backgroundColor; } @@ -271,7 +270,8 @@ class BottomNavigationBarState extends State { new _Circle( state: this, index: index, - color: config.labels[index].backgroundColor + color: config.labels[index].backgroundColor, + vsync: this, )..controller.addStatusListener((AnimationStatus status) { if (status == AnimationStatus.completed) { setState(() { @@ -289,7 +289,6 @@ class BottomNavigationBarState extends State { if (config.currentIndex != oldConfig.currentIndex) { if (config.type == BottomNavigationBarType.shifting) _pushCircle(config.currentIndex); - _controllers[oldConfig.currentIndex].reverse(); _controllers[config.currentIndex].forward(); } @@ -298,7 +297,6 @@ class BottomNavigationBarState extends State { @override Widget build(BuildContext context) { Widget bottomNavigation; - switch (config.type) { case BottomNavigationBarType.fixed: final List children = []; @@ -311,7 +309,6 @@ class BottomNavigationBarState extends State { themeData.primaryColor : themeData.accentColor ) ); - for (int i = 0; i < config.labels.length; i += 1) { children.add( new Flexible( @@ -329,16 +326,16 @@ class BottomNavigationBarState extends State { margin: new EdgeInsets.only( top: new Tween( begin: 8.0, - end: 6.0 - ).evaluate(animations[i]) + end: 6.0, + ).evaluate(animations[i]), ), child: new IconTheme( data: new IconThemeData( - color: colorTween.evaluate(animations[i]) + color: colorTween.evaluate(animations[i]), ), - child: config.labels[i].icon - ) - ) + child: config.labels[i].icon, + ), + ), ), new Align( alignment: FractionalOffset.bottomCenter, @@ -347,41 +344,36 @@ class BottomNavigationBarState extends State { child: new DefaultTextStyle( style: new TextStyle( fontSize: 14.0, - color: colorTween.evaluate(animations[i]) + color: colorTween.evaluate(animations[i]), ), child: new Transform( transform: new Matrix4.diagonal3(new Vector3.all( new Tween( begin: 0.85, end: 1.0, - ).evaluate(animations[i]) + ).evaluate(animations[i]), )), alignment: FractionalOffset.bottomCenter, - child: config.labels[i].title - ) - ) - ) - ) - ] - ) - ) - ) + child: config.labels[i].title, + ), + ), + ), + ), + ], + ), + ), + ), ); } - bottomNavigation = new SizedBox( width: _maxWidth, - child: new Row( - children: children - ) + child: new Row(children: children), ); - break; + case BottomNavigationBarType.shifting: final List children = []; - _computeWeight(); - for (int i = 0; i < config.labels.length; i += 1) { children.add( new Flexible( @@ -402,16 +394,16 @@ class BottomNavigationBarState extends State { margin: new EdgeInsets.only( top: new Tween( begin: 18.0, - end: 6.0 - ).evaluate(animations[i]) + end: 6.0, + ).evaluate(animations[i]), ), child: new IconTheme( data: new IconThemeData( color: Colors.white ), - child: config.labels[i].icon - ) - ) + child: config.labels[i].icon, + ), + ), ), new Align( alignment: FractionalOffset.bottomCenter, @@ -425,24 +417,22 @@ class BottomNavigationBarState extends State { color: Colors.white ), child: config.labels[i].title - ) - ) - ) - ) - ] - ) - ) - ) + ), + ), + ), + ), + ], + ), + ), + ), ); } - bottomNavigation = new SizedBox( width: _maxWidth, child: new Row( children: children ) ); - break; } @@ -467,20 +457,20 @@ class BottomNavigationBarState extends State { child: new CustomPaint( painter: new _RadialPainter( circles: _circles.toList() - ) - ) + ), + ), ), new Material( // Splashes. type: MaterialType.transparency, child: new Center( child: bottomNavigation - ) - ) - ] - ) - ) - ) - ] + ), + ), + ], + ), + ), + ), + ], ); } } @@ -489,20 +479,21 @@ class _Circle { _Circle({ this.state, this.index, - this.color + this.color, + @required TickerProvider vsync, }) { assert(this.state != null); assert(this.index != null); assert(this.color != null); controller = new AnimationController( - duration: kThemeAnimationDuration + duration: kThemeAnimationDuration, + vsync: vsync, ); animation = new CurvedAnimation( parent: controller, curve: Curves.fastOutSlowIn ); - controller.forward(); } diff --git a/packages/flutter/lib/src/material/bottom_sheet.dart b/packages/flutter/lib/src/material/bottom_sheet.dart index 9391941964e..1b4bf0ab289 100644 --- a/packages/flutter/lib/src/material/bottom_sheet.dart +++ b/packages/flutter/lib/src/material/bottom_sheet.dart @@ -78,10 +78,11 @@ class BottomSheet extends StatefulWidget { _BottomSheetState createState() => new _BottomSheetState(); /// Creates an animation controller suitable for controlling a [BottomSheet]. - static AnimationController createAnimationController() { + static AnimationController createAnimationController(TickerProvider vsync) { return new AnimationController( duration: _kBottomSheetDuration, - debugLabel: 'BottomSheet' + debugLabel: 'BottomSheet', + vsync: vsync, ); } } @@ -222,7 +223,7 @@ class _ModalBottomSheetRoute extends PopupRoute { @override AnimationController createAnimationController() { assert(_animationController == null); - _animationController = BottomSheet.createAnimationController(); + _animationController = BottomSheet.createAnimationController(navigator.overlay); return _animationController; } diff --git a/packages/flutter/lib/src/material/checkbox.dart b/packages/flutter/lib/src/material/checkbox.dart index c129aa27a9e..b75021adf06 100644 --- a/packages/flutter/lib/src/material/checkbox.dart +++ b/packages/flutter/lib/src/material/checkbox.dart @@ -30,7 +30,7 @@ import 'toggleable.dart'; /// * [Slider] /// * /// * -class Checkbox extends StatelessWidget { +class Checkbox extends StatefulWidget { /// Creates a material design checkbox. /// /// The checkbox itself does not maintain any state. Instead, when the state of @@ -68,15 +68,21 @@ class Checkbox extends StatelessWidget { /// The width of a checkbox widget. static const double width = 18.0; + @override + _CheckboxState createState() => new _CheckboxState(); +} + +class _CheckboxState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); ThemeData themeData = Theme.of(context); return new _CheckboxRenderObjectWidget( - value: value, - activeColor: activeColor ?? themeData.accentColor, - inactiveColor: onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor, - onChanged: onChanged + value: config.value, + activeColor: config.activeColor ?? themeData.accentColor, + inactiveColor: config.onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor, + onChanged: config.onChanged, + vsync: this, ); } } @@ -84,27 +90,31 @@ class Checkbox extends StatelessWidget { class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { _CheckboxRenderObjectWidget({ Key key, - this.value, - this.activeColor, - this.inactiveColor, - this.onChanged + @required this.value, + @required this.activeColor, + @required this.inactiveColor, + @required this.onChanged, + @required this.vsync, }) : super(key: key) { assert(value != null); assert(activeColor != null); assert(inactiveColor != null); + assert(vsync != null); } final bool value; final Color activeColor; final Color inactiveColor; final ValueChanged onChanged; + final TickerProvider vsync; @override _RenderCheckbox createRenderObject(BuildContext context) => new _RenderCheckbox( value: value, activeColor: activeColor, inactiveColor: inactiveColor, - onChanged: onChanged + onChanged: onChanged, + vsync: vsync, ); @override @@ -113,7 +123,8 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { ..value = value ..activeColor = activeColor ..inactiveColor = inactiveColor - ..onChanged = onChanged; + ..onChanged = onChanged + ..vsync = vsync; } } @@ -127,13 +138,15 @@ class _RenderCheckbox extends RenderToggleable { bool value, Color activeColor, Color inactiveColor, - ValueChanged onChanged + ValueChanged onChanged, + @required TickerProvider vsync, }): super( value: value, activeColor: activeColor, inactiveColor: inactiveColor, onChanged: onChanged, - size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius) + size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius), + vsync: vsync, ); @override diff --git a/packages/flutter/lib/src/material/data_table.dart b/packages/flutter/lib/src/material/data_table.dart index e087b1cb1d7..1b79d020264 100644 --- a/packages/flutter/lib/src/material/data_table.dart +++ b/packages/flutter/lib/src/material/data_table.dart @@ -675,7 +675,7 @@ class _SortArrow extends StatefulWidget { _SortArrowState createState() => new _SortArrowState(); } -class _SortArrowState extends State<_SortArrow> { +class _SortArrowState extends State<_SortArrow> with TickerProviderStateMixin { AnimationController _opacityController; Animation _opacityAnimation; @@ -691,7 +691,8 @@ class _SortArrowState extends State<_SortArrow> { super.initState(); _opacityAnimation = new CurvedAnimation( parent: _opacityController = new AnimationController( - duration: config.duration + duration: config.duration, + vsync: this, ), curve: Curves.fastOutSlowIn ) @@ -702,7 +703,8 @@ class _SortArrowState extends State<_SortArrow> { end: math.PI ).animate(new CurvedAnimation( parent: _orientationController = new AnimationController( - duration: config.duration + duration: config.duration, + vsync: this, ), curve: Curves.easeIn )) diff --git a/packages/flutter/lib/src/material/drawer.dart b/packages/flutter/lib/src/material/drawer.dart index 9f3a205a4cb..a22a6cebff2 100644 --- a/packages/flutter/lib/src/material/drawer.dart +++ b/packages/flutter/lib/src/material/drawer.dart @@ -111,11 +111,11 @@ class DrawerController extends StatefulWidget { /// State for a [DrawerController]. /// /// Typically used by a [Scaffold] to [open] and [close] the drawer. -class DrawerControllerState extends State { +class DrawerControllerState extends State with SingleTickerProviderStateMixin { @override void initState() { super.initState(); - _controller = new AnimationController(duration: _kBaseSettleDuration) + _controller = new AnimationController(duration: _kBaseSettleDuration, vsync: this) ..addListener(_animationChanged) ..addStatusListener(_animationStatusChanged); } diff --git a/packages/flutter/lib/src/material/expand_icon.dart b/packages/flutter/lib/src/material/expand_icon.dart index cb72f5227bf..538bb0f8700 100644 --- a/packages/flutter/lib/src/material/expand_icon.dart +++ b/packages/flutter/lib/src/material/expand_icon.dart @@ -60,14 +60,14 @@ class ExpandIcon extends StatefulWidget { _ExpandIconState createState() => new _ExpandIconState(); } -class _ExpandIconState extends State { +class _ExpandIconState extends State with SingleTickerProviderStateMixin { AnimationController _controller; Animation _iconTurns; @override void initState() { super.initState(); - _controller = new AnimationController(duration: kThemeAnimationDuration); + _controller = new AnimationController(duration: kThemeAnimationDuration, vsync: this); _iconTurns = new Tween(begin: 0.0, end: 0.5).animate( new CurvedAnimation( parent: _controller, diff --git a/packages/flutter/lib/src/material/material.dart b/packages/flutter/lib/src/material/material.dart index 2e05afa5143..a5cb73429c7 100644 --- a/packages/flutter/lib/src/material/material.dart +++ b/packages/flutter/lib/src/material/material.dart @@ -6,6 +6,7 @@ import 'dart:math' as math; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; import 'package:vector_math/vector_math_64.dart'; import 'constants.dart'; @@ -219,7 +220,7 @@ class Material extends StatefulWidget { } } -class _MaterialState extends State { +class _MaterialState extends State with TickerProviderStateMixin { final GlobalKey _inkFeatureRenderer = new GlobalKey(debugLabel: 'ink renderer'); Color _getBackgroundColor(BuildContext context) { @@ -254,7 +255,8 @@ class _MaterialState extends State { child: new _InkFeatures( key: _inkFeatureRenderer, color: backgroundColor, - child: contents + child: contents, + vsync: this, ) ); if (config.type == MaterialType.circle) { @@ -295,7 +297,14 @@ const double _kSplashConfirmedVelocity = 1.0; // logical pixels per millisecond const double _kSplashInitialSize = 0.0; // logical pixels class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController { - _RenderInkFeatures({ RenderBox child, this.color }) : super(child); + _RenderInkFeatures({ RenderBox child, @required this.vsync, this.color }) : super(child) { + assert(vsync != null); + } + + // This class should exist in a 1:1 relationship with a MaterialState object, + // since there's no current support for dynamically changing the ticker + // provider. + final TickerProvider vsync; // This is here to satisfy the MaterialInkController contract. // The actual painting of this color is done by a Container in the @@ -338,7 +347,8 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController targetRadius: radius, clipCallback: clipCallback, repositionToReferenceBox: !containedInkWell, - onRemoved: onRemoved + onRemoved: onRemoved, + vsync: vsync, ); addInkFeature(splash); return splash; @@ -366,7 +376,8 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController color: color, shape: shape, rectCallback: rectCallback, - onRemoved: onRemoved + onRemoved: onRemoved, + vsync: vsync, ); addInkFeature(highlight); return highlight; @@ -405,16 +416,27 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController } class _InkFeatures extends SingleChildRenderObjectWidget { - _InkFeatures({ Key key, this.color, Widget child }) : super(key: key, child: child); + _InkFeatures({ Key key, this.color, Widget child, @required this.vsync }) : super(key: key, child: child); + + // This widget must be owned by a MaterialState, which must be provided as the vsync. + // This relationship must be 1:1 and cannot change for the lifetime of the MaterialState. final Color color; + final TickerProvider vsync; + @override - _RenderInkFeatures createRenderObject(BuildContext context) => new _RenderInkFeatures(color: color); + _RenderInkFeatures createRenderObject(BuildContext context) { + return new _RenderInkFeatures( + color: color, + vsync: vsync + ); + } @override void updateRenderObject(BuildContext context, _RenderInkFeatures renderObject) { renderObject.color = color; + assert(vsync == renderObject.vsync); } } @@ -488,17 +510,17 @@ class _InkSplash extends InkFeature implements InkSplash { this.targetRadius, this.clipCallback, this.repositionToReferenceBox, - VoidCallback onRemoved + VoidCallback onRemoved, + @required TickerProvider vsync, }) : super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) { - _radiusController = new AnimationController(duration: _kUnconfirmedSplashDuration) + _radiusController = new AnimationController(duration: _kUnconfirmedSplashDuration, vsync: vsync) ..addListener(controller.markNeedsPaint) ..forward(); _radius = new Tween( begin: _kSplashInitialSize, end: targetRadius ).animate(_radiusController); - - _alphaController = new AnimationController(duration: _kHighlightFadeDuration) + _alphaController = new AnimationController(duration: _kHighlightFadeDuration, vsync: vsync) ..addListener(controller.markNeedsPaint) ..addStatusListener(_handleAlphaStatusChanged); _alpha = new IntTween( @@ -578,10 +600,11 @@ class _InkHighlight extends InkFeature implements InkHighlight { this.rectCallback, Color color, this.shape, - VoidCallback onRemoved + VoidCallback onRemoved, + @required TickerProvider vsync, }) : _color = color, super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) { - _alphaController = new AnimationController(duration: _kHighlightFadeDuration) + _alphaController = new AnimationController(duration: _kHighlightFadeDuration, vsync: vsync) ..addListener(controller.markNeedsPaint) ..addStatusListener(_handleAlphaStatusChanged) ..forward(); diff --git a/packages/flutter/lib/src/material/mergeable_material.dart b/packages/flutter/lib/src/material/mergeable_material.dart index 64dba9b34bf..5186b2d4770 100644 --- a/packages/flutter/lib/src/material/mergeable_material.dart +++ b/packages/flutter/lib/src/material/mergeable_material.dart @@ -136,7 +136,7 @@ class _AnimationTuple { double gapStart; } -class _MergeableMaterialState extends State { +class _MergeableMaterialState extends State with TickerProviderStateMixin { List _children; final Map _animationTuples = {}; @@ -157,7 +157,8 @@ class _MergeableMaterialState extends State { void _initGap(MaterialGap gap) { final AnimationController controller = new AnimationController( - duration: kThemeAnimationDuration + duration: kThemeAnimationDuration, + vsync: this, ); final CurvedAnimation startAnimation = new CurvedAnimation( diff --git a/packages/flutter/lib/src/material/overscroll_indicator.dart b/packages/flutter/lib/src/material/overscroll_indicator.dart index 6769c5417df..2e8350b5cab 100644 --- a/packages/flutter/lib/src/material/overscroll_indicator.dart +++ b/packages/flutter/lib/src/material/overscroll_indicator.dart @@ -121,13 +121,9 @@ class OverscrollIndicator extends StatefulWidget { _OverscrollIndicatorState createState() => new _OverscrollIndicatorState(); } -class _OverscrollIndicatorState extends State { - final AnimationController _extentAnimation = new AnimationController( - lowerBound: _kMinIndicatorExtent, - upperBound: _kMaxIndicatorExtent, - duration: _kNormalHideDuration - ); +class _OverscrollIndicatorState extends State with SingleTickerProviderStateMixin { + AnimationController _extentAnimation; bool _scrollUnderway = false; Timer _hideTimer; Axis _scrollDirection; @@ -136,6 +132,17 @@ class _OverscrollIndicatorState extends State { double _maxScrollOffset; Point _dragPosition; + @override + void initState() { + super.initState(); + _extentAnimation = new AnimationController( + lowerBound: _kMinIndicatorExtent, + upperBound: _kMaxIndicatorExtent, + duration: _kNormalHideDuration, + vsync: this, + ); + } + void _hide([Duration duration=_kTimeoutHideDuration]) { _scrollUnderway = false; _hideTimer?.cancel(); diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index f902216282f..ad19532aa0a 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -216,7 +216,7 @@ class CheckedPopupMenuItem extends PopupMenuItem { _CheckedPopupMenuItemState createState() => new _CheckedPopupMenuItemState(); } -class _CheckedPopupMenuItemState extends _PopupMenuItemState> { +class _CheckedPopupMenuItemState extends _PopupMenuItemState> with SingleTickerProviderStateMixin { static const Duration _kFadeDuration = const Duration(milliseconds: 150); AnimationController _controller; Animation get _opacity => _controller.view; @@ -224,7 +224,7 @@ class _CheckedPopupMenuItemState extends _PopupMenuItemState setState(() { /* animation changed */ })); } diff --git a/packages/flutter/lib/src/material/progress_indicator.dart b/packages/flutter/lib/src/material/progress_indicator.dart index ac07fc0d931..46162fc5767 100644 --- a/packages/flutter/lib/src/material/progress_indicator.dart +++ b/packages/flutter/lib/src/material/progress_indicator.dart @@ -146,7 +146,7 @@ class LinearProgressIndicator extends ProgressIndicator { _LinearProgressIndicatorState createState() => new _LinearProgressIndicatorState(); } -class _LinearProgressIndicatorState extends State { +class _LinearProgressIndicatorState extends State with SingleTickerProviderStateMixin { Animation _animation; AnimationController _controller; @@ -154,7 +154,8 @@ class _LinearProgressIndicatorState extends State { void initState() { super.initState(); _controller = new AnimationController( - duration: const Duration(milliseconds: 1500) + duration: const Duration(milliseconds: 1500), + vsync: this, )..repeat(); _animation = new CurvedAnimation(parent: _controller, curve: Curves.fastOutSlowIn); } @@ -310,13 +311,16 @@ final Animatable _kStepTween = new StepTween(begin: 0, end: 5); final Animatable _kRotationTween = new CurveTween(curve: new SawTooth(5)); -class _CircularProgressIndicatorState extends State { +class _CircularProgressIndicatorState extends State with SingleTickerProviderStateMixin { AnimationController _controller; @override void initState() { super.initState(); - _controller = _buildController(); + _controller = new AnimationController( + duration: const Duration(milliseconds: 6666), + vsync: this, + )..repeat(); } @override @@ -325,10 +329,6 @@ class _CircularProgressIndicatorState extends State { super.dispose(); } - AnimationController _buildController() { - return new AnimationController(duration: const Duration(milliseconds: 6666))..repeat(); - } - Widget _buildIndicator(BuildContext context, double headValue, double tailValue, int stepValue, double rotationValue) { return new Container( constraints: new BoxConstraints( diff --git a/packages/flutter/lib/src/material/radio.dart b/packages/flutter/lib/src/material/radio.dart index 1bb814370d0..8c8220dd3c8 100644 --- a/packages/flutter/lib/src/material/radio.dart +++ b/packages/flutter/lib/src/material/radio.dart @@ -35,7 +35,7 @@ const double _kInnerRadius = 5.0; /// * [Slider] /// * [Switch] /// * -class Radio extends StatelessWidget { +class Radio extends StatefulWidget { /// Creates a material design radio button. /// /// The radio button itself does not maintain any state. Instead, when the state @@ -77,7 +77,12 @@ class Radio extends StatelessWidget { /// Defaults to accent color of the current [Theme]. final Color activeColor; - bool get _enabled => onChanged != null; + @override + _RadioState createState() => new _RadioState(); +} + +class _RadioState extends State> with TickerProviderStateMixin { + bool get _enabled => config.onChanged != null; Color _getInactiveColor(ThemeData themeData) { return _enabled ? themeData.unselectedWidgetColor : themeData.disabledColor; @@ -85,7 +90,7 @@ class Radio extends StatelessWidget { void _handleChanged(bool selected) { if (selected) - onChanged(value); + config.onChanged(config.value); } @override @@ -93,12 +98,13 @@ class Radio extends StatelessWidget { assert(debugCheckHasMaterial(context)); ThemeData themeData = Theme.of(context); return new Semantics( - checked: value == groupValue, + checked: config.value == config.groupValue, child: new _RadioRenderObjectWidget( - selected: value == groupValue, - activeColor: activeColor ?? themeData.accentColor, + selected: config.value == config.groupValue, + activeColor: config.activeColor ?? themeData.accentColor, inactiveColor: _getInactiveColor(themeData), - onChanged: _enabled ? _handleChanged : null + onChanged: _enabled ? _handleChanged : null, + vsync: this, ) ); } @@ -107,27 +113,31 @@ class Radio extends StatelessWidget { class _RadioRenderObjectWidget extends LeafRenderObjectWidget { _RadioRenderObjectWidget({ Key key, - this.selected, - this.activeColor, - this.inactiveColor, - this.onChanged + @required this.selected, + @required this.activeColor, + @required this.inactiveColor, + this.onChanged, + @required this.vsync, }) : super(key: key) { assert(selected != null); assert(activeColor != null); assert(inactiveColor != null); + assert(vsync != null); } final bool selected; final Color inactiveColor; final Color activeColor; final ValueChanged onChanged; + final TickerProvider vsync; @override _RenderRadio createRenderObject(BuildContext context) => new _RenderRadio( value: selected, activeColor: activeColor, inactiveColor: inactiveColor, - onChanged: onChanged + onChanged: onChanged, + vsync: vsync, ); @override @@ -136,7 +146,8 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget { ..value = selected ..activeColor = activeColor ..inactiveColor = inactiveColor - ..onChanged = onChanged; + ..onChanged = onChanged + ..vsync = vsync; } } @@ -145,13 +156,15 @@ class _RenderRadio extends RenderToggleable { bool value, Color activeColor, Color inactiveColor, - ValueChanged onChanged + ValueChanged onChanged, + @required TickerProvider vsync, }): super( value: value, activeColor: activeColor, inactiveColor: inactiveColor, onChanged: onChanged, - size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius) + size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius), + vsync: vsync, ); @override diff --git a/packages/flutter/lib/src/material/refresh_indicator.dart b/packages/flutter/lib/src/material/refresh_indicator.dart index 2633b053525..59a10fd6545 100644 --- a/packages/flutter/lib/src/material/refresh_indicator.dart +++ b/packages/flutter/lib/src/material/refresh_indicator.dart @@ -138,9 +138,9 @@ class RefreshIndicator extends StatefulWidget { /// Contains the state for a [RefreshIndicator]. This class can be used to /// programmatically show the refresh indicator, see the [show] method. -class RefreshIndicatorState extends State { - final AnimationController _sizeController = new AnimationController(); - final AnimationController _scaleController = new AnimationController(); +class RefreshIndicatorState extends State with TickerProviderStateMixin { + AnimationController _sizeController; + AnimationController _scaleController; Animation _sizeFactor; Animation _scaleFactor; Animation _value; @@ -154,15 +154,16 @@ class RefreshIndicatorState extends State { @override void initState() { super.initState(); - _sizeFactor = new Tween(begin: 0.0, end: _kDragSizeFactorLimit).animate(_sizeController); - _scaleFactor = new Tween(begin: 1.0, end: 0.0).animate(_scaleController); - // The "value" of the circular progress indicator during a drag. - _value = new Tween( + _sizeController = new AnimationController(vsync: this); + _sizeFactor = new Tween(begin: 0.0, end: _kDragSizeFactorLimit).animate(_sizeController); + _value = new Tween( // The "value" of the circular progress indicator during a drag. begin: 0.0, end: 0.75 - ) - .animate(_sizeController); + ).animate(_sizeController); + + _scaleController = new AnimationController(vsync: this); + _scaleFactor = new Tween(begin: 1.0, end: 0.0).animate(_scaleController); } @override diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index e4f5172c972..d96673eb694 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -169,10 +169,9 @@ class _FloatingActionButtonTransition extends StatefulWidget { _FloatingActionButtonTransitionState createState() => new _FloatingActionButtonTransitionState(); } -class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTransition> { - final AnimationController _previousController = new AnimationController(duration: _kFloatingActionButtonSegue); - final AnimationController _currentController = new AnimationController(duration: _kFloatingActionButtonSegue); - +class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTransition> with TickerProviderStateMixin { + AnimationController _previousController; + AnimationController _currentController; CurvedAnimation _previousAnimation; CurvedAnimation _currentAnimation; Widget _previousChild; @@ -180,21 +179,29 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr @override void initState() { super.initState(); - // If we start out with a child, have the child appear fully visible instead - // of animating in. - if (config.child != null) - _currentController.value = 1.0; + _previousController = new AnimationController( + duration: _kFloatingActionButtonSegue, + vsync: this, + )..addStatusListener(_handleAnimationStatusChanged); _previousAnimation = new CurvedAnimation( parent: _previousController, curve: Curves.easeIn ); + + _currentController = new AnimationController( + duration: _kFloatingActionButtonSegue, + vsync: this, + ); _currentAnimation = new CurvedAnimation( parent: _currentController, curve: Curves.easeIn ); - _previousController.addStatusListener(_handleAnimationStatusChanged); + // If we start out with a child, have the child appear fully visible instead + // of animating in. + if (config.child != null) + _currentController.value = 1.0; } @override @@ -357,7 +364,7 @@ class Scaffold extends StatefulWidget { /// /// Can display [SnackBar]s and [BottomSheet]s. Retrieve a [ScaffoldState] from /// the current [BuildContext] using [Scaffold.of]. -class ScaffoldState extends State { +class ScaffoldState extends State with TickerProviderStateMixin { static final Object _kScaffoldStorageIdentifier = new Object(); @@ -401,7 +408,7 @@ class ScaffoldState extends State { /// will be added to a queue and displayed after the earlier snack bars have /// closed. ScaffoldFeatureController showSnackBar(SnackBar snackbar) { - _snackBarController ??= SnackBar.createAnimationController() + _snackBarController ??= SnackBar.createAnimationController(vsync: this) ..addStatusListener(_handleSnackBarStatusChange); if (_snackBars.isEmpty) { assert(_snackBarController.isDismissed); @@ -475,7 +482,7 @@ class ScaffoldState extends State { // PERSISTENT BOTTOM SHEET API - final List _dismissedBottomSheets = []; + final List<_PersistentBottomSheet> _dismissedBottomSheets = <_PersistentBottomSheet>[]; PersistentBottomSheetController _currentBottomSheet; /// Shows a persistent material design bottom sheet. @@ -504,7 +511,7 @@ class ScaffoldState extends State { } Completer completer = new Completer(); GlobalKey<_PersistentBottomSheetState> bottomSheetKey = new GlobalKey<_PersistentBottomSheetState>(); - AnimationController controller = BottomSheet.createAnimationController() + AnimationController controller = BottomSheet.createAnimationController(this) ..forward(); _PersistentBottomSheet bottomSheet; LocalHistoryEntry entry = new LocalHistoryEntry( @@ -554,7 +561,7 @@ class ScaffoldState extends State { @override void initState() { super.initState(); - _appBarController = new AnimationController(); + _appBarController = new AnimationController(vsync: this); // Use an explicit identifier to guard against the possibility that the // Scaffold's key is recreated by the Widget that creates the Scaffold. List scrollValues = PageStorage.of(context)?.readState(context, @@ -569,11 +576,15 @@ class ScaffoldState extends State { @override void dispose() { - _appBarController.stop(); - _snackBarController?.stop(); + _appBarController.dispose(); + _snackBarController?.dispose(); _snackBarController = null; _snackBarTimer?.cancel(); _snackBarTimer = null; + for (_PersistentBottomSheet bottomSheet in _dismissedBottomSheets) + bottomSheet.animationController.dispose(); + if (_currentBottomSheet != null) + _currentBottomSheet._widget.animationController.dispose(); PageStorage.of(context)?.writeState(context, [_scrollOffset, _scrollOffsetDelta], identifier: _kScaffoldStorageIdentifier ); diff --git a/packages/flutter/lib/src/material/scrollbar.dart b/packages/flutter/lib/src/material/scrollbar.dart index 552385764e9..ad2376388b3 100644 --- a/packages/flutter/lib/src/material/scrollbar.dart +++ b/packages/flutter/lib/src/material/scrollbar.dart @@ -95,8 +95,8 @@ class Scrollbar extends StatefulWidget { _ScrollbarState createState() => new _ScrollbarState(); } -class _ScrollbarState extends State { - final AnimationController _fade = new AnimationController(duration: _kScrollbarThumbFadeDuration); +class _ScrollbarState extends State with SingleTickerProviderStateMixin { + AnimationController _fade; CurvedAnimation _opacity; double _scrollOffset; Axis _scrollDirection; @@ -106,6 +106,7 @@ class _ScrollbarState extends State { @override void initState() { super.initState(); + _fade = new AnimationController(duration: _kScrollbarThumbFadeDuration, vsync: this); _opacity = new CurvedAnimation(parent: _fade, curve: Curves.fastOutSlowIn); } diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart index 6b9e5306e5f..3beb885e4c8 100644 --- a/packages/flutter/lib/src/material/slider.dart +++ b/packages/flutter/lib/src/material/slider.dart @@ -39,7 +39,7 @@ import 'typography.dart'; /// * [Radio] /// * [Switch] /// * -class Slider extends StatelessWidget { +class Slider extends StatefulWidget { /// Creates a material design slider. /// /// The slider itself does not maintain any state. Instead, when the state of @@ -107,20 +107,26 @@ class Slider extends StatelessWidget { /// Defaults to accent color of the current [Theme]. final Color activeColor; + @override + _SliderState createState() => new _SliderState(); +} + +class _SliderState extends State with TickerProviderStateMixin { void _handleChanged(double value) { - assert(onChanged != null); - onChanged(value * (max - min) + min); + assert(config.onChanged != null); + config.onChanged(value * (config.max - config.min) + config.min); } @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); return new _SliderRenderObjectWidget( - value: (value - min) / (max - min), - divisions: divisions, - label: label, - activeColor: activeColor ?? Theme.of(context).accentColor, - onChanged: onChanged != null ? _handleChanged : null + value: (config.value - config.min) / (config.max - config.min), + divisions: config.divisions, + label: config.label, + activeColor: config.activeColor ?? Theme.of(context).accentColor, + onChanged: config.onChanged != null ? _handleChanged : null, + vsync: this, ); } } @@ -132,7 +138,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { this.divisions, this.label, this.activeColor, - this.onChanged + this.onChanged, + this.vsync, }) : super(key: key); final double value; @@ -140,6 +147,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { final String label; final Color activeColor; final ValueChanged onChanged; + final TickerProvider vsync; @override _RenderSlider createRenderObject(BuildContext context) => new _RenderSlider( @@ -147,7 +155,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { divisions: divisions, label: label, activeColor: activeColor, - onChanged: onChanged + onChanged: onChanged, + vsync: vsync, ); @override @@ -158,6 +167,8 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { ..label = label ..activeColor = activeColor ..onChanged = onChanged; + // Ticker provider cannot change since there's a 1:1 relationship between + // the _SliderRenderObjectWidget object and the _SliderState object. } } @@ -199,7 +210,8 @@ class _RenderSlider extends RenderConstrainedBox implements SemanticActionHandle int divisions, String label, Color activeColor, - this.onChanged + this.onChanged, + TickerProvider vsync, }) : _value = value, _divisions = divisions, _activeColor = activeColor, @@ -210,14 +222,18 @@ class _RenderSlider extends RenderConstrainedBox implements SemanticActionHandle ..onStart = _handleDragStart ..onUpdate = _handleDragUpdate ..onEnd = _handleDragEnd; - _reactionController = new AnimationController(duration: kRadialReactionDuration); + _reactionController = new AnimationController( + duration: kRadialReactionDuration, + vsync: vsync, + ); _reaction = new CurvedAnimation( parent: _reactionController, curve: Curves.fastOutSlowIn )..addListener(markNeedsPaint); _position = new AnimationController( value: value, - duration: _kDiscreteTransitionDuration + duration: _kDiscreteTransitionDuration, + vsync: vsync, )..addListener(markNeedsPaint); } diff --git a/packages/flutter/lib/src/material/snack_bar.dart b/packages/flutter/lib/src/material/snack_bar.dart index 6aca6cc904a..f4ec8747210 100644 --- a/packages/flutter/lib/src/material/snack_bar.dart +++ b/packages/flutter/lib/src/material/snack_bar.dart @@ -204,10 +204,11 @@ class SnackBar extends StatelessWidget { // API for Scaffold.addSnackBar(): /// Creates an animation controller useful for driving a snack bar's entrance and exit animation. - static AnimationController createAnimationController() { + static AnimationController createAnimationController({ @required TickerProvider vsync }) { return new AnimationController( duration: _kSnackBarTransitionDuration, - debugLabel: 'SnackBar' + debugLabel: 'SnackBar', + vsync: vsync, ); } diff --git a/packages/flutter/lib/src/material/stepper.dart b/packages/flutter/lib/src/material/stepper.dart index 1472c4553f8..34ba316456b 100644 --- a/packages/flutter/lib/src/material/stepper.dart +++ b/packages/flutter/lib/src/material/stepper.dart @@ -176,7 +176,7 @@ class Stepper extends StatefulWidget { _StepperState createState() => new _StepperState(); } -class _StepperState extends State { +class _StepperState extends State with TickerProviderStateMixin { List _keys; final Map _oldStates = new Map(); @@ -608,7 +608,8 @@ class _StepperState extends State { new AnimatedSize( curve: Curves.fastOutSlowIn, duration: kThemeAnimationDuration, - child: config.steps[config.currentStep].content + vsync: this, + child: config.steps[config.currentStep].content, ), _buildVerticalControls() ] diff --git a/packages/flutter/lib/src/material/switch.dart b/packages/flutter/lib/src/material/switch.dart index 8920567f5b3..41af96ffb6f 100644 --- a/packages/flutter/lib/src/material/switch.dart +++ b/packages/flutter/lib/src/material/switch.dart @@ -32,7 +32,7 @@ import 'toggleable.dart'; /// * [Radio] /// * [Slider] /// * -class Switch extends StatelessWidget { +class Switch extends StatefulWidget { /// Creates a material design switch. /// /// The switch itself does not maintain any state. Instead, when the state of @@ -74,18 +74,31 @@ class Switch extends StatelessWidget { /// An image to use on the thumb of this switch when the switch is off. final ImageProvider inactiveThumbImage; + @override + _SwitchState createState() => new _SwitchState(); + + @override + void debugFillDescription(List description) { + super.debugFillDescription(description); + description.add('value: ${value ? "on" : "off"}'); + if (onChanged == null) + description.add('disabled'); + } +} + +class _SwitchState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); final ThemeData themeData = Theme.of(context); final bool isDark = themeData.brightness == Brightness.dark; - final Color activeThumbColor = activeColor ?? themeData.accentColor; + final Color activeThumbColor = config.activeColor ?? themeData.accentColor; final Color activeTrackColor = activeThumbColor.withAlpha(0x80); Color inactiveThumbColor; Color inactiveTrackColor; - if (onChanged != null) { + if (config.onChanged != null) { inactiveThumbColor = isDark ? Colors.grey[400] : Colors.grey[50]; inactiveTrackColor = isDark ? Colors.white30 : Colors.black26; } else { @@ -94,25 +107,18 @@ class Switch extends StatelessWidget { } return new _SwitchRenderObjectWidget( - value: value, + value: config.value, activeColor: activeThumbColor, inactiveColor: inactiveThumbColor, - activeThumbImage: activeThumbImage, - inactiveThumbImage: inactiveThumbImage, + activeThumbImage: config.activeThumbImage, + inactiveThumbImage: config.inactiveThumbImage, activeTrackColor: activeTrackColor, inactiveTrackColor: inactiveTrackColor, configuration: createLocalImageConfiguration(context), - onChanged: onChanged + onChanged: config.onChanged, + vsync: this, ); } - - @override - void debugFillDescription(List description) { - super.debugFillDescription(description); - description.add('value: ${value ? "on" : "off"}'); - if (onChanged == null) - description.add('disabled'); - } } class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { @@ -126,7 +132,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { this.activeTrackColor, this.inactiveTrackColor, this.configuration, - this.onChanged + this.onChanged, + this.vsync, }) : super(key: key); final bool value; @@ -138,6 +145,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { final Color inactiveTrackColor; final ImageConfiguration configuration; final ValueChanged onChanged; + final TickerProvider vsync; @override _RenderSwitch createRenderObject(BuildContext context) => new _RenderSwitch( @@ -149,7 +157,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { activeTrackColor: activeTrackColor, inactiveTrackColor: inactiveTrackColor, configuration: configuration, - onChanged: onChanged + onChanged: onChanged, + vsync: vsync, ); @override @@ -163,7 +172,8 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ..activeTrackColor = activeTrackColor ..inactiveTrackColor = inactiveTrackColor ..configuration = configuration - ..onChanged = onChanged; + ..onChanged = onChanged + ..vsync = vsync; } } @@ -184,7 +194,8 @@ class _RenderSwitch extends RenderToggleable { Color activeTrackColor, Color inactiveTrackColor, ImageConfiguration configuration, - ValueChanged onChanged + ValueChanged onChanged, + @required TickerProvider vsync, }) : _activeThumbImage = activeThumbImage, _inactiveThumbImage = inactiveThumbImage, _activeTrackColor = activeTrackColor, @@ -195,7 +206,8 @@ class _RenderSwitch extends RenderToggleable { activeColor: activeColor, inactiveColor: inactiveColor, onChanged: onChanged, - size: const Size(_kSwitchWidth, _kSwitchHeight) + size: const Size(_kSwitchWidth, _kSwitchHeight), + vsync: vsync, ) { _drag = new HorizontalDragGestureRecognizer() ..onStart = _handleDragStart diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 643cd2dedff..a051e847836 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -523,26 +523,27 @@ class TabBarSelection extends StatefulWidget { /// /// Subclasses of [TabBarSelection] typically use [State] objects that extend /// this class. -class TabBarSelectionState extends State> { +class TabBarSelectionState extends State> with SingleTickerProviderStateMixin { + + // Both the TabBar and TabBarView classes access _controller because they + // alternately drive selection progress between tabs. + AnimationController _controller; /// An animation that updates as the selected tab changes. Animation 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 _valueToIndex = new Map(); - void _initValueToIndex() { - _valueToIndex.clear(); - int index = 0; - for(T value in values) - _valueToIndex[value] = index++; - } - @override void initState() { super.initState(); + + _controller = new AnimationController( + duration: _kTabBarScroll, + value: 1.0, + vsync: this, + ); + _value = config.value ?? PageStorage.of(context)?.readState(context) ?? values.first; // If the selection's values have changed since the selected value was saved with @@ -561,6 +562,13 @@ class TabBarSelectionState extends State> { _initValueToIndex(); } + void _initValueToIndex() { + _valueToIndex.clear(); + int index = 0; + for (T value in values) + _valueToIndex[value] = index++; + } + void _writeValue() { PageStorage.of(context)?.writeState(context, _value); } diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart index 09b14b9ec73..ea2eb455a2d 100644 --- a/packages/flutter/lib/src/material/time_picker.dart +++ b/packages/flutter/lib/src/material/time_picker.dart @@ -410,11 +410,14 @@ class _Dial extends StatefulWidget { _DialState createState() => new _DialState(); } -class _DialState extends State<_Dial> { +class _DialState extends State<_Dial> with SingleTickerProviderStateMixin { @override void initState() { super.initState(); - _thetaController = new AnimationController(duration: _kDialAnimateDuration); + _thetaController = new AnimationController( + duration: _kDialAnimateDuration, + vsync: this, + ); _thetaTween = new Tween(begin: _getThetaForTime(config.selectedTime)); _theta = _thetaTween.animate(new CurvedAnimation( parent: _thetaController, diff --git a/packages/flutter/lib/src/material/toggleable.dart b/packages/flutter/lib/src/material/toggleable.dart index d2937cc1b70..5b7fb65fff5 100644 --- a/packages/flutter/lib/src/material/toggleable.dart +++ b/packages/flutter/lib/src/material/toggleable.dart @@ -5,6 +5,8 @@ import 'package:flutter/animation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:meta/meta.dart'; import 'constants.dart'; @@ -26,38 +28,88 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic Size size, Color activeColor, Color inactiveColor, - ValueChanged onChanged + ValueChanged onChanged, + @required TickerProvider vsync, }) : _value = value, _activeColor = activeColor, _inactiveColor = inactiveColor, _onChanged = onChanged, + _vsync = vsync, super(additionalConstraints: new BoxConstraints.tight(size)) { assert(value != null); assert(activeColor != null); assert(inactiveColor != null); + assert(vsync != null); _tap = new TapGestureRecognizer() ..onTapDown = _handleTapDown ..onTap = _handleTap ..onTapUp = _handleTapUp ..onTapCancel = _handleTapCancel; - _positionController = new AnimationController( duration: _kToggleDuration, - value: _value ? 1.0 : 0.0 + value: value ? 1.0 : 0.0, + vsync: vsync, ); _position = new CurvedAnimation( parent: _positionController, - curve: Curves.linear + curve: Curves.linear, )..addListener(markNeedsPaint) ..addStatusListener(_handlePositionStateChanged); - - _reactionController = new AnimationController(duration: kRadialReactionDuration); + _reactionController = new AnimationController( + duration: kRadialReactionDuration, + vsync: vsync, + ); _reaction = new CurvedAnimation( parent: _reactionController, - curve: Curves.fastOutSlowIn + curve: Curves.fastOutSlowIn, )..addListener(markNeedsPaint); } + /// Used by subclasses to manipulate the visual value of the control. + /// + /// Some controls respond to user input by updating their visual value. For + /// example, the thumb of a switch moves from one position to another when + /// dragged. These controls manipulate this animation controller to update + /// their [position] and eventually trigger an [onChanged] callback when the + /// animation reaches either 0.0 or 1.0. + @protected + AnimationController get positionController => _positionController; + AnimationController _positionController; + + /// The visual value of the control. + /// + /// When the control is inactive, the [value] is false and this animation has + /// the value 0.0. When the control is active, the value is [true] and this + /// animation has the value 1.0. When the control is changing from inactive + /// to active (or vice versa), [value] is the target value and this animation + /// gradually updates from 0.0 to 1.0 (or vice versa). + CurvedAnimation get position => _position; + CurvedAnimation _position; + + /// Used by subclasses to control the radial reaction animation. + /// + /// Some controls have a radial ink reaction to user input. This animation + /// controller can be used to start or stop these ink reactions. + /// + /// Subclasses should call [paintRadialReaction] to actually paint the radial + /// reaction. + @protected + AnimationController get reactionController => _reactionController; + AnimationController _reactionController; + Animation _reaction; + + /// The [TickerProvider] for the [AnimationController]s that run the animations. + TickerProvider get vsync => _vsync; + TickerProvider _vsync; + set vsync(TickerProvider value) { + assert(value != null); + if (value == _vsync) + return; + _vsync = value; + positionController.resync(vsync); + reactionController.resync(vsync); + } + /// Whether this control is current "active" (checked, on, selected) or "inactive" (unchecked, off, not selected). /// /// When the value changes, this object starts the [positionController] and @@ -138,50 +190,17 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic /// grey color and its value cannot be changed. bool get isInteractive => onChanged != null; - /// The visual value of the control. - /// - /// When the control is inactive, the [value] is false and this animation has - /// the value 0.0. When the control is active, the value is [true] and this - /// animation has the value 1.0. When the control is changing from inactive - /// to active (or vice versa), [value] is the target value and this animation - /// gradually updates from 0.0 to 1.0 (or vice versa). - CurvedAnimation get position => _position; - CurvedAnimation _position; - - /// Used by subclasses to manipulate the visual value of the control. - /// - /// Some controls respond to user input by updating their visual value. For - /// example, the thumb of a switch moves from one position to another when - /// dragged. These controls manipulate this animation controller to update - /// their [position] and eventually trigger an [onChanged] callback when the - /// animation reaches either 0.0 or 1.0. - AnimationController get positionController => _positionController; - AnimationController _positionController; - - /// Used by subclasses to control the radial reaction animation. - /// - /// Some controls have a radial ink reaction to user input. This animation - /// controller can be used to start or stop these ink reactions. - /// - /// Subclasses should call [paintRadialReaction] to actually paint the radial - /// reaction. - AnimationController get reactionController => _reactionController; - AnimationController _reactionController; - Animation _reaction; - TapGestureRecognizer _tap; Point _downPosition; @override void attach(PipelineOwner owner) { super.attach(owner); - if (_positionController != null) { - if (value) - _positionController.forward(); - else - _positionController.reverse(); - } - if (_reactionController != null && isInteractive) { + if (value) + _positionController.forward(); + else + _positionController.reverse(); + if (isInteractive) { switch (_reactionController.status) { case AnimationStatus.forward: _reactionController.forward(); @@ -199,8 +218,8 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic @override void detach() { - _positionController?.stop(); - _reactionController?.stop(); + _positionController.stop(); + _reactionController.stop(); super.detach(); } diff --git a/packages/flutter/lib/src/material/tooltip.dart b/packages/flutter/lib/src/material/tooltip.dart index 65be39c2d2b..692464e48a9 100644 --- a/packages/flutter/lib/src/material/tooltip.dart +++ b/packages/flutter/lib/src/material/tooltip.dart @@ -93,7 +93,7 @@ class Tooltip extends StatefulWidget { } } -class _TooltipState extends State { +class _TooltipState extends State with SingleTickerProviderStateMixin { AnimationController _controller; OverlayEntry _entry; Timer _timer; @@ -101,7 +101,7 @@ class _TooltipState extends State { @override void initState() { super.initState(); - _controller = new AnimationController(duration: _kFadeDuration) + _controller = new AnimationController(duration: _kFadeDuration, vsync: this) ..addStatusListener(_handleStatusChanged); } @@ -178,7 +178,7 @@ class _TooltipState extends State { void dispose() { if (_entry != null) _removeEntry(); - _controller.stop(); + _controller.dispose(); super.dispose(); } diff --git a/packages/flutter/lib/src/material/two_level_list.dart b/packages/flutter/lib/src/material/two_level_list.dart index 58bfe916c8a..6318bee96f5 100644 --- a/packages/flutter/lib/src/material/two_level_list.dart +++ b/packages/flutter/lib/src/material/two_level_list.dart @@ -142,7 +142,7 @@ class TwoLevelSublist extends StatefulWidget { _TwoLevelSublistState createState() => new _TwoLevelSublistState(); } -class _TwoLevelSublistState extends State { +class _TwoLevelSublistState extends State with SingleTickerProviderStateMixin { AnimationController _controller; CurvedAnimation _easeOutAnimation; CurvedAnimation _easeInAnimation; @@ -157,7 +157,7 @@ class _TwoLevelSublistState extends State { @override void initState() { super.initState(); - _controller = new AnimationController(duration: _kExpand); + _controller = new AnimationController(duration: _kExpand, vsync: this); _easeOutAnimation = new CurvedAnimation(parent: _controller, curve: Curves.easeOut); _easeInAnimation = new CurvedAnimation(parent: _controller, curve: Curves.easeIn); _borderColor = new ColorTween(begin: Colors.transparent); @@ -173,7 +173,7 @@ class _TwoLevelSublistState extends State { @override void dispose() { - _controller.stop(); + _controller.dispose(); super.dispose(); } @@ -243,8 +243,8 @@ class _TwoLevelSublistState extends State { ..end = config.backgroundColor ?? Colors.transparent; return new AnimatedBuilder( - animation: _controller.view, - builder: buildList + animation: _controller.view, + builder: buildList ); } } diff --git a/packages/flutter/lib/src/rendering/animated_size.dart b/packages/flutter/lib/src/rendering/animated_size.dart index d050f87d283..d8c1cb92c1b 100644 --- a/packages/flutter/lib/src/rendering/animated_size.dart +++ b/packages/flutter/lib/src/rendering/animated_size.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/animation.dart'; +import 'package:flutter/scheduler.dart'; import 'package:meta/meta.dart'; import 'box.dart'; @@ -10,29 +11,41 @@ import 'object.dart'; import 'shifted_box.dart'; /// A render object that animates its size to its child's size over a given -/// [duration] and with a given [curve]. In case the child's size is animating -/// as opposed to abruptly changing size, the parent behaves like a normal -/// container. +/// [duration] and with a given [curve]. If the child's size itself animates +/// (i.e. if it changes size two frames in a row, as opposed to abruptly +/// changing size in one frame then remaining that size in subsequent frames), +/// this render object sizes itself to fit the child instead of animating +/// itself. /// -/// In case the child overflows the current animated size of the parent, it gets -/// clipped automatically. +/// When the child overflows the current animated size of this render object, it +/// is clipped. class RenderAnimatedSize extends RenderAligningShiftedBox { /// Creates a render object that animates its size to match its child. - /// The [duration] and [curve] arguments define the animation. The [alignment] - /// argument is used to align the child in the case where the parent is not + /// The [duration] and [curve] arguments define the animation. + /// + /// The [alignment] argument is used to align the child when the parent is not /// (yet) the same size as the child. /// - /// The arguments [duration], [curve], and [alignment] should not be null. + /// The [duration] is required. + /// + /// The [vsync] should specify a [TickerProvider] for the animation + /// controller. + /// + /// The arguments [duration], [curve], [alignment], and [vsync] must + /// not be null. RenderAnimatedSize({ + @required TickerProvider vsync, + @required Duration duration, Curve curve: Curves.linear, - RenderBox child, FractionalOffset alignment: FractionalOffset.center, - @required Duration duration - }) : super(child: child, alignment: alignment) { + RenderBox child, + }) : _vsync = vsync, super(child: child, alignment: alignment) { + assert(vsync != null); assert(duration != null); assert(curve != null); _controller = new AnimationController( - duration: duration + vsync: vsync, + duration: duration, )..addListener(() { if (_controller.value != _lastValue) markNeedsLayout(); @@ -68,6 +81,17 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { _animation.curve = value; } + /// The [TickerProvider] for the [AnimationController] that runs the animation. + TickerProvider get vsync => _vsync; + TickerProvider _vsync; + set vsync(TickerProvider value) { + assert(value != null); + if (value == _vsync) + return; + _vsync = value; + _controller.resync(vsync); + } + @override void attach(PipelineOwner owner) { super.attach(owner); @@ -104,8 +128,7 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { size = child.size; _controller.stop(); } else { - // Don't register first change (i.e. when _targetSize == _sourceSize) - // as a last-frame change. + // Don't register first change as a last-frame change. if (_sizeTween.end != _sizeTween.begin) _didChangeTargetSizeLastFrame = true; diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index 8df3c1a72bc..a09c608e614 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -198,7 +198,7 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding, /// then invokes post-frame callbacks (registered with [addPostFrameCallback]. /// /// Some bindings (for example, the [WidgetsBinding]) add extra steps to this - /// list. + /// list (for example, see [WidgetsBinding.beginFrame]). // // When editing the above, also update widgets/binding.dart's copy. @protected diff --git a/packages/flutter/lib/src/scheduler/binding.dart b/packages/flutter/lib/src/scheduler/binding.dart index c3a0b312093..62b557c5a41 100644 --- a/packages/flutter/lib/src/scheduler/binding.dart +++ b/packages/flutter/lib/src/scheduler/binding.dart @@ -86,6 +86,45 @@ class _FrameCallbackEntry { StackTrace debugStack; } +/// The various phases that a [SchedulerBinding] goes through during +/// [SchedulerBinding.handleBeginFrame]. +/// +/// This is exposed by [SchedulerBinding.schedulerPhase]. +/// +/// The values of this enum are ordered in the same order as the phases occur, +/// so their relative index values can be compared to each other. +/// +/// See also [WidgetsBinding.beginFrame]. +enum SchedulerPhase { + /// No frame is being processed. Tasks (scheduled by + /// [WidgetsBinding.scheduleTask]), microtasks (scheduled by + /// [scheduleMicrotask]), [Timer] callbacks, event handlers (e.g. from user + /// input), and other callbacks (e.g. from [Future]s, [Stream]s, and the like) + /// may be executing. + idle, + + /// The transient callbacks (scheduled by + /// [WidgetsBinding.scheduleFrameCallback] and + /// [WidgetsBinding.addFrameCallback]) are currently executing. + /// + /// Typically, these callbacks handle updating objects to new animation states. + transientCallbacks, + + /// The persistent callbacks (scheduled by + /// [WidgetsBinding.addPersistentFrameCallback]) are currently executing. + /// + /// Typically, this is the build/layout/paint pipeline. See + /// [WidgetsBinding.beginFrame]. + persistentCallbacks, + + /// The post-frame callbacks (scheduled by + /// [WidgetsBinding.addPostFrameCallback]) are currently executing. + /// + /// Typically, these callbacks handle cleanup and scheduling of work for the + /// next frame. + postFrameCallbacks, +} + /// Scheduler for running the following: /// /// * _Frame callbacks_, triggered by the system's @@ -402,9 +441,9 @@ abstract class SchedulerBinding extends BindingBase { bool get hasScheduledFrame => _hasScheduledFrame; bool _hasScheduledFrame = false; - /// Whether this scheduler is currently producing a frame in [handleBeginFrame]. - bool get isProducingFrame => _isProducingFrame; - bool _isProducingFrame = false; + /// The phase that the scheduler is currently operating under. + SchedulerPhase get schedulerPhase => _schedulerPhase; + SchedulerPhase _schedulerPhase = SchedulerPhase.idle; /// Schedules a new frame using [scheduleFrame] if this object is not /// currently producing a frame. @@ -412,7 +451,7 @@ abstract class SchedulerBinding extends BindingBase { /// After this is called, the framework ensures that the end of the /// [handleBeginFrame] function will (eventually) be reached. void ensureVisualUpdate() { - if (_isProducingFrame) + if (schedulerPhase != SchedulerPhase.idle) return; scheduleFrame(); } @@ -530,20 +569,21 @@ abstract class SchedulerBinding extends BindingBase { return true; }); - assert(!_isProducingFrame); - _isProducingFrame = true; + assert(schedulerPhase == SchedulerPhase.idle); _hasScheduledFrame = false; try { // TRANSIENT FRAME CALLBACKS + _schedulerPhase = SchedulerPhase.transientCallbacks; _invokeTransientFrameCallbacks(_currentFrameTimeStamp); // PERSISTENT FRAME CALLBACKS + _schedulerPhase = SchedulerPhase.persistentCallbacks; for (FrameCallback callback in _persistentCallbacks) _invokeFrameCallback(callback, _currentFrameTimeStamp); - _isProducingFrame = false; // POST-FRAME CALLBACKS + _schedulerPhase = SchedulerPhase.postFrameCallbacks; List localPostFrameCallbacks = new List.from(_postFrameCallbacks); _postFrameCallbacks.clear(); @@ -551,7 +591,7 @@ abstract class SchedulerBinding extends BindingBase { _invokeFrameCallback(callback, _currentFrameTimeStamp); } finally { - _isProducingFrame = false; // just in case we throw before setting it above + _schedulerPhase = SchedulerPhase.idle; _currentFrameTimeStamp = null; Timeline.finishSync(); assert(() { @@ -567,7 +607,7 @@ abstract class SchedulerBinding extends BindingBase { void _invokeTransientFrameCallbacks(Duration timeStamp) { Timeline.startSync('Animate'); - assert(_isProducingFrame); + assert(schedulerPhase == SchedulerPhase.transientCallbacks); Map callbacks = _transientCallbacks; _transientCallbacks = new Map(); callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) { diff --git a/packages/flutter/lib/src/scheduler/ticker.dart b/packages/flutter/lib/src/scheduler/ticker.dart index ac7e65314c0..019727e32c4 100644 --- a/packages/flutter/lib/src/scheduler/ticker.dart +++ b/packages/flutter/lib/src/scheduler/ticker.dart @@ -4,6 +4,9 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:meta/meta.dart'; + import 'binding.dart'; /// Signature for the [onTick] constructor argument of the [Ticker] class. @@ -12,35 +15,112 @@ import 'binding.dart'; /// at the time of the callback being called. typedef void TickerCallback(Duration elapsed); +/// An interface implemented by classes that can vend [Ticker] objects. +abstract class TickerProvider { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const TickerProvider(); + + /// Creates a ticker with the given callback. + /// + /// The kind of ticker provided depends on the kind of ticker provider. + Ticker createTicker(TickerCallback onTick); +} + /// Calls its callback once per animation frame. /// /// When created, a ticker is initially disabled. Call [start] to /// enable the ticker. /// -/// See also [SchedulerBinding.scheduleFrameCallback]. +/// A [Ticker] can be silenced by setting [muted] to true. While silenced, time +/// still elapses, and [start] and [stop] can still be called, but no callbacks +/// are called. +/// +/// Tickers are driven by the [SchedulerBinding]. See +/// [SchedulerBinding.scheduleFrameCallback]. class Ticker { /// Creates a ticker that will call [onTick] once per frame while running. - Ticker(TickerCallback onTick) : _onTick = onTick; - - final TickerCallback _onTick; + /// + /// An optional label can be provided for debugging purposes. That label + /// will appear in the [toString] output in debug builds. + Ticker(this._onTick, { this.debugLabel }) { + assert(() { + _debugCreationStack = StackTrace.current; + return true; + }); + } Completer _completer; - int _animationId; - Duration _startTime; + + /// Whether this ticker has been silenced. + /// + /// While silenced, a ticker's clock can still run, but the callback will not + /// be called. + bool get muted => _muted; + bool _muted = false; + /// When set to true, silences the ticker, so that it is no longer ticking. If + /// a tick is already scheduled, it will unschedule it. This will not + /// unschedule the next frame, though. + /// + /// When set to false, unsilences the ticker, potentially scheduling a frame + /// to handle the next tick. + set muted(bool value) { + if (value == muted) + return; + _muted = value; + if (value) { + unscheduleTick(); + } else if (shouldScheduleTick) { + scheduleTick(); + } + } /// Whether this ticker has scheduled a call to call its callback /// on the next frame. - bool get isTicking => _completer != null; - - /// Starts calling the ticker's callback once per animation frame. /// - /// The returned future resolves once the ticker stops ticking. + /// A ticker that is [muted] can be active (see [isActive]) yet not be + /// ticking. In that case, the ticker will not call its callback, and + /// [isTicking] will be false, but time will still be progressing. + // TODO(ianh): we should teach the scheduler binding about the lifecycle events + // and then this could return an accurate view of the actual scheduler. + bool get isTicking => _completer != null && !muted; + + /// Whether time is elapsing for this ticker. Becomes true when [start] is + /// called and false when [stop] is called. + /// + /// A ticker can be active yet not be actually ticking (i.e. not be calling + /// the callback). To determine if a ticker is actually ticking, use + /// [isTicking]. + bool get isActive => _completer != null; + + Duration _startTime; + + /// Starts the clock for this ticker. If the ticker is not [muted], then this + /// also starts calling the ticker's callback once per animation frame. + /// + /// The returned future resolves once the ticker [stop]s ticking. + /// + /// Calling this sets [isActive] to true. + /// + /// This method cannot be called while the ticker is active. To restart the + /// ticker, first [stop] it. Future start() { - assert(!isTicking); + assert(() { + if (isTicking) { + throw new FlutterError( + 'A ticker was started twice.\n' + 'A ticker that is already active cannot be started again without first stopping it.\n' + 'The affected ticker was: ${ this.toString(debugIncludeStack: true) }' + ); + } + return true; + }); assert(_startTime == null); _completer = new Completer(); - _scheduleTick(); - if (SchedulerBinding.instance.isProducingFrame) + if (shouldScheduleTick) + scheduleTick(); + if (SchedulerBinding.instance.schedulerPhase.index > SchedulerPhase.idle.index && + SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index) _startTime = SchedulerBinding.instance.currentFrameTimeStamp; return _completer.future; } @@ -48,29 +128,49 @@ class Ticker { /// Stops calling the ticker's callback. /// /// Causes the future returned by [start] to resolve. + /// + /// Calling this sets [isActive] to false. + /// + /// This method does nothing if called when the ticker is inactive. void stop() { if (!isTicking) return; - _startTime = null; - - if (_animationId != null) { - SchedulerBinding.instance.cancelFrameCallbackWithId(_animationId); - _animationId = null; - } - // We take the _completer into a local variable so that isTicking is false - // when we actually complete the future (isTicking uses _completer - // to determine its state). + // when we actually complete the future (isTicking uses _completer to + // determine its state). Completer localCompleter = _completer; _completer = null; + _startTime = null; assert(!isTicking); + + unscheduleTick(); localCompleter.complete(); } + + final TickerCallback _onTick; + + int _animationId; + + /// Whether this ticker has already scheduled a frame callback. + @protected + bool get scheduled => _animationId != null; + + /// Whether a tick should be scheduled. + /// + /// If this is true, then calling [scheduleTick] should succeed. + /// + /// Reasons why a tick should not be scheduled include: + /// + /// * A tick has already been scheduled for the coming frame. + /// * The ticker is not active ([start] has not been called). + /// * The ticker is not ticking, e.g. because it is [muted] (see [isTicking]). + bool get shouldScheduleTick => isTicking && !scheduled; + void _tick(Duration timeStamp) { assert(isTicking); - assert(_animationId != null); + assert(scheduled); _animationId = null; if (_startTime == null) @@ -78,14 +178,90 @@ class Ticker { _onTick(timeStamp - _startTime); - // The onTick callback may have scheduled another tick already. - if (isTicking && _animationId == null) - _scheduleTick(rescheduling: true); + // The onTick callback may have scheduled another tick already, for + // example by calling stop then start again. + if (shouldScheduleTick) + scheduleTick(rescheduling: true); } - void _scheduleTick({ bool rescheduling: false }) { + /// Schedules a tick for the next frame. + /// + /// This should only be called if [shouldScheduleTick] is true. + @protected + void scheduleTick({ bool rescheduling: false }) { assert(isTicking); - assert(_animationId == null); + assert(!scheduled); + assert(shouldScheduleTick); _animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling); } + + /// Cancels the frame callback that was requested by [scheduleTick], if any. + /// + /// Calling this method when no tick is [scheduled] is harmless. + /// + /// This method should not be called when [shouldScheduleTick] would return + /// true if no tick was scheduled. + @protected + void unscheduleTick() { + if (scheduled) { + SchedulerBinding.instance.cancelFrameCallbackWithId(_animationId); + _animationId = null; + } + assert(!shouldScheduleTick); + } + + /// Makes this ticker take the state of another ticker, and disposes the other + /// ticker. + /// + /// This is useful if an object with a [Ticker] is given a new + /// [TickerProvider] but needs to maintain continuity. In particular, this + /// maintains the identity of the [Future] returned by the [start] function of + /// the original [Ticker] if the original ticker is active. + /// + /// This ticker must not be active when this method is called. + void absorbTicker(Ticker originalTicker) { + assert(!isTicking); + assert(_completer == null); + assert(_startTime == null); + assert(_animationId == null); + _completer = originalTicker._completer; + _startTime = originalTicker._startTime; + if (shouldScheduleTick) + scheduleTick(); + originalTicker.dispose(); + } + + /// Release the resources used by this object. The object is no longer usable + /// after this method is called. + @mustCallSuper + void dispose() { + _completer = null; + // We intentionally don't null out _startTime. This means that if start() + // was ever called, the object is now in a bogus state. This weakly helps + // catch cases of use-after-dispose. + unscheduleTick(); + } + + final String debugLabel; + StackTrace _debugCreationStack; + + @override + String toString({ bool debugIncludeStack: false }) { + final StringBuffer buffer = new StringBuffer(); + buffer.write('$runtimeType('); + assert(() { + buffer.write(debugLabel ?? ''); + return true; + }); + buffer.write(')'); + assert(() { + if (debugIncludeStack) { + buffer.writeln(); + buffer.writeln('The stack trace when the $runtimeType was actually created was:'); + FlutterError.defaultStackFilter(_debugCreationStack.toString().trimRight().split('\n')).forEach(buffer.writeln); + } + return true; + }); + return buffer.toString(); + } } diff --git a/packages/flutter/lib/src/widgets/animated_cross_fade.dart b/packages/flutter/lib/src/widgets/animated_cross_fade.dart index 63683c24b64..67a5d737edb 100644 --- a/packages/flutter/lib/src/widgets/animated_cross_fade.dart +++ b/packages/flutter/lib/src/widgets/animated_cross_fade.dart @@ -8,6 +8,7 @@ import 'package:meta/meta.dart'; import 'animated_size.dart'; import 'basic.dart'; import 'framework.dart'; +import 'ticker_provider.dart'; import 'transitions.dart'; /// Specifies which of the children to show. See [AnimatedCrossFade]. @@ -80,7 +81,7 @@ class AnimatedCrossFade extends StatefulWidget { _AnimatedCrossFadeState createState() => new _AnimatedCrossFadeState(); } -class _AnimatedCrossFadeState extends State { +class _AnimatedCrossFadeState extends State with TickerProviderStateMixin { _AnimatedCrossFadeState() : super(); AnimationController _controller; @@ -90,7 +91,7 @@ class _AnimatedCrossFadeState extends State { @override void initState() { super.initState(); - _controller = new AnimationController(duration: config.duration); + _controller = new AnimationController(duration: config.duration, vsync: this); if (config.crossFadeState == CrossFadeState.showSecond) _controller.value = 1.0; _firstAnimation = _initAnimation(config.firstCurve, true); @@ -185,6 +186,7 @@ class _AnimatedCrossFadeState extends State { alignment: FractionalOffset.topCenter, duration: config.duration, curve: config.sizeCurve, + vsync: this, child: new Stack( overflow: Overflow.visible, children: children diff --git a/packages/flutter/lib/src/widgets/animated_size.dart b/packages/flutter/lib/src/widgets/animated_size.dart index 96af3857b62..f13cc54a3bc 100644 --- a/packages/flutter/lib/src/widgets/animated_size.dart +++ b/packages/flutter/lib/src/widgets/animated_size.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; import 'package:meta/meta.dart'; import 'basic.dart'; @@ -19,7 +20,8 @@ class AnimatedSize extends SingleChildRenderObjectWidget { Widget child, this.alignment: FractionalOffset.center, this.curve: Curves.linear, - @required this.duration + @required this.duration, + @required this.vsync, }) : super(key: key, child: child); /// The alignment of the child within the parent when the parent is not yet @@ -42,12 +44,16 @@ class AnimatedSize extends SingleChildRenderObjectWidget { /// size. final Duration duration; + /// The [TickerProvider] for this widget. + final TickerProvider vsync; + @override RenderAnimatedSize createRenderObject(BuildContext context) { return new RenderAnimatedSize( alignment: alignment, duration: duration, - curve: curve + curve: curve, + vsync: vsync, ); } @@ -57,6 +63,7 @@ class AnimatedSize extends SingleChildRenderObjectWidget { renderObject ..alignment = alignment ..duration = duration - ..curve = curve; + ..curve = curve + ..vsync = vsync; } } diff --git a/packages/flutter/lib/src/widgets/dismissable.dart b/packages/flutter/lib/src/widgets/dismissable.dart index 96ee9eaf68c..4032527bbf2 100644 --- a/packages/flutter/lib/src/widgets/dismissable.dart +++ b/packages/flutter/lib/src/widgets/dismissable.dart @@ -5,9 +5,10 @@ import 'package:meta/meta.dart'; import 'basic.dart'; -import 'transitions.dart'; import 'framework.dart'; import 'gesture_detector.dart'; +import 'ticker_provider.dart'; +import 'transitions.dart'; const Duration _kDismissDuration = const Duration(milliseconds: 200); const Curve _kResizeTimeCurve = const Interval(0.4, 1.0, curve: Curves.ease); @@ -113,11 +114,11 @@ class Dismissable extends StatefulWidget { _DismissableState createState() => new _DismissableState(); } -class _DismissableState extends State { +class _DismissableState extends State with TickerProviderStateMixin { @override void initState() { super.initState(); - _moveController = new AnimationController(duration: _kDismissDuration) + _moveController = new AnimationController(duration: _kDismissDuration, vsync: this) ..addStatusListener(_handleDismissStatusChanged); _updateMoveAnimation(); } @@ -278,7 +279,7 @@ class _DismissableState extends State { if (config.onDismissed != null) config.onDismissed(_dismissDirection); } else { - _resizeController = new AnimationController(duration: config.resizeDuration) + _resizeController = new AnimationController(duration: config.resizeDuration, vsync: this) ..addListener(_handleResizeProgressChanged); _resizeController.forward(); setState(() { diff --git a/packages/flutter/lib/src/widgets/editable.dart b/packages/flutter/lib/src/widgets/editable.dart index 5750fa192ec..30b35571bde 100644 --- a/packages/flutter/lib/src/widgets/editable.dart +++ b/packages/flutter/lib/src/widgets/editable.dart @@ -405,9 +405,7 @@ class RawInputLineState extends ScrollableState { _keyboardHandle.release(); if (_cursorTimer != null) _stopCursorTimer(); - scheduleMicrotask(() { // can't hide while disposing, since it triggers a rebuild - _selectionOverlay?.dispose(); - }); + _selectionOverlay?.dispose(); super.dispose(); } @@ -432,14 +430,12 @@ class RawInputLineState extends ScrollableState { _stopCursorTimer(); if (_selectionOverlay != null) { - scheduleMicrotask(() { // can't update while disposing, since it triggers a rebuild - if (focused) { - _selectionOverlay.update(config.value); - } else { - _selectionOverlay?.hide(); - _selectionOverlay = null; - } - }); + if (focused) { + _selectionOverlay.update(config.value); + } else { + _selectionOverlay?.dispose(); + _selectionOverlay = null; + } } return new _EditableLineWidget( diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index e85bc1309ba..87c095494fe 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -1207,21 +1207,23 @@ class _InactiveElements { } return true; }); - element.unmount(); - assert(element._debugLifecycleState == _ElementLifecycle.defunct); element.visitChildren((Element child) { assert(child._parent == element); _unmount(child); }); + element.unmount(); + assert(element._debugLifecycleState == _ElementLifecycle.defunct); } void _unmountAll() { + _locked = true; + final List elements = _elements.toList()..sort(Element._sort); + _elements.clear(); try { - _locked = true; - for (Element element in _elements) + for (Element element in elements.reversed) _unmount(element); } finally { - _elements.clear(); + assert(_elements.isEmpty); _locked = false; } } @@ -1365,7 +1367,8 @@ abstract class BuildContext { /// [State.initState] methods, because those methods would not get called /// again if the inherited value were to change. To ensure that the widget /// correctly updates itself when the inherited value changes, only call this - /// (directly or indirectly) from build methods or layout and paint callbacks. + /// (directly or indirectly) from build methods, layout and paint callbacks, or + /// from [State.dependenciesChanged]. /// /// It is also possible to call this from interaction event handlers (e.g. /// gesture callbacks) or timers, to obtain a value once, if that value is not @@ -1373,6 +1376,12 @@ abstract class BuildContext { /// /// Calling this method is O(1) with a small constant factor, but will lead to /// the widget being rebuilt more often. + /// + /// Once a widget registers a dependency on a particular type by calling this + /// method, it will be rebuilt, and [State.dependenciesChanged] will be + /// called, whenever changes occur relating to that widget until the next time + /// the widget or one of its ancestors is moved (for example, because an + /// ancestor is added or removed). InheritedWidget inheritFromWidgetOfExactType(Type targetType); /// Returns the nearest ancestor widget of the given type, which must be the @@ -1647,7 +1656,7 @@ class BuildOwner { }); } } - _dirtyElements.sort(_elementSort); + _dirtyElements.sort(Element._sort); _dirtyElementsNeedsResorting = false; int dirtyCount = _dirtyElements.length; int index = 0; @@ -1668,7 +1677,7 @@ class BuildOwner { } index += 1; if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) { - _dirtyElements.sort(_elementSort); + _dirtyElements.sort(Element._sort); _dirtyElementsNeedsResorting = false; dirtyCount = _dirtyElements.length; while (index > 0 && _dirtyElements[index - 1].dirty) { @@ -1715,18 +1724,6 @@ class BuildOwner { assert(_debugStateLockLevel >= 0); } - static int _elementSort(BuildableElement a, BuildableElement b) { - if (a.depth < b.depth) - return -1; - if (b.depth < a.depth) - return 1; - if (b.dirty && !a.dirty) - return -1; - if (a.dirty && !b.dirty) - return 1; - return 0; - } - /// Complete the element build pass by unmounting any elements that are no /// longer active. /// @@ -1837,6 +1834,18 @@ abstract class Element implements BuildContext { int get depth => _depth; int _depth; + static int _sort(BuildableElement a, BuildableElement b) { + if (a.depth < b.depth) + return -1; + if (b.depth < a.depth) + return 1; + if (b.dirty && !a.dirty) + return -1; + if (a.dirty && !b.dirty) + return 1; + return 0; + } + /// The configuration for this element. @override Widget get widget => _widget; @@ -2206,6 +2215,7 @@ abstract class Element implements BuildContext { // We unregistered our dependencies in deactivate, but never cleared the list. // Since we're going to be reused, let's clear our list now. _dependencies?.clear(); + _hadUnsatisfiedDependencies = false; _updateInheritance(); assert(() { _debugLifecycleState = _ElementLifecycle.active; return true; }); } @@ -2554,19 +2564,6 @@ abstract class BuildableElement extends Element { owner._debugCurrentBuildTarget = this; return true; }); - _hadUnsatisfiedDependencies = false; - // In theory, we would also clear our actual _dependencies here. However, to - // clear it we'd have to notify each of them, unregister from them, and then - // reregister as soon as the build function re-dependended on it. So to - // avoid faffing around we just never unregister our dependencies except - // when we're deactivated. In principle this means we might be getting - // notified about widget types we once inherited from but no longer do, but - // in practice this is so rare that the extra cost when it does happen is - // far outweighed by the avoided work in the common case. - // We _do_ clear the list properly any time our ancestor chain changes in a - // way that might result in us getting a different Element's Widget for a - // particular Type. This avoids the potential of being registered to - // multiple identically-typed Widgets' Elements at the same time. performRebuild(); assert(() { assert(owner._debugCurrentBuildTarget == this); diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart index a317f11bb04..41601045d5b 100644 --- a/packages/flutter/lib/src/widgets/implicit_animations.dart +++ b/packages/flutter/lib/src/widgets/implicit_animations.dart @@ -9,6 +9,7 @@ import 'basic.dart'; import 'container.dart'; import 'framework.dart'; import 'text.dart'; +import 'ticker_provider.dart'; /// An interpolation between two [BoxConstraint]s. class BoxConstraintsTween extends Tween { @@ -114,7 +115,7 @@ typedef Tween TweenConstructor(T targetValue); typedef Tween TweenVisitor(Tween tween, T targetValue, TweenConstructor constructor); /// A base class for widgets with implicit animations. -abstract class AnimatedWidgetBaseState extends State { +abstract class AnimatedWidgetBaseState extends State with SingleTickerProviderStateMixin { AnimationController _controller; /// The animation driving this widget's implicit animations. @@ -126,7 +127,8 @@ abstract class AnimatedWidgetBaseState exten super.initState(); _controller = new AnimationController( duration: config.duration, - debugLabel: '${config.toStringShort()}' + debugLabel: '${config.toStringShort()}', + vsync: this, )..addListener(_handleAnimationChanged); _updateCurve(); _constructTweens(); diff --git a/packages/flutter/lib/src/widgets/mimic.dart b/packages/flutter/lib/src/widgets/mimic.dart index 0829deb8087..12ac0675492 100644 --- a/packages/flutter/lib/src/widgets/mimic.dart +++ b/packages/flutter/lib/src/widgets/mimic.dart @@ -28,14 +28,16 @@ class MimicableHandle { /// An overlay entry that is mimicking another widget. class MimicOverlayEntry { - MimicOverlayEntry._(this._handle) { - _overlayEntry = new OverlayEntry(builder: _build); + MimicOverlayEntry._(this._handle, this._overlay) { _initialGlobalBounds = _handle.globalBounds; + _overlayEntry = new OverlayEntry(builder: _build); + _overlay.insert(_overlayEntry); } Rect _initialGlobalBounds; MimicableHandle _handle; + OverlayState _overlay; OverlayEntry _overlayEntry; // Animation state @@ -63,7 +65,8 @@ class MimicOverlayEntry { _curve = curve; // TODO(abarth): Support changing the animation target when in flight. assert(_controller == null); - _controller = new AnimationController(duration: duration) + // TODO(ianh): Need to get a TickerProvider that's tied to the Overlay's TickerMode. + _controller = new AnimationController(duration: duration, vsync: _overlay) ..addListener(_overlayEntry.markNeedsBuild); return _controller.forward(); } @@ -214,9 +217,7 @@ class MimicableState extends State { /// placed in the enclosing overlay. MimicOverlayEntry liftToOverlay() { OverlayState overlay = Overlay.of(context, debugRequiredFor: config); - MimicOverlayEntry entry = new MimicOverlayEntry._(startMimic()); - overlay.insert(entry._overlayEntry); - return entry; + return new MimicOverlayEntry._(startMimic(), overlay); } void _stopMimic() { diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index c6a0cba3d16..9ea1f274403 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -10,6 +10,7 @@ import 'binding.dart'; import 'focus.dart'; import 'framework.dart'; import 'overlay.dart'; +import 'ticker_provider.dart'; /// An abstraction for an entry managed by a [Navigator]. /// @@ -325,7 +326,7 @@ class Navigator extends StatefulWidget { } /// The state for a [Navigator] widget. -class NavigatorState extends State { +class NavigatorState extends State with TickerProviderStateMixin { final GlobalKey _overlayKey = new GlobalKey(); final List> _history = new List>(); diff --git a/packages/flutter/lib/src/widgets/overlay.dart b/packages/flutter/lib/src/widgets/overlay.dart index e631d5db312..ee030eab5ed 100644 --- a/packages/flutter/lib/src/widgets/overlay.dart +++ b/packages/flutter/lib/src/widgets/overlay.dart @@ -2,14 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:collection'; -import 'package:meta/meta.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:meta/meta.dart'; import 'basic.dart'; import 'debug.dart'; import 'framework.dart'; +import 'ticker_provider.dart'; /// A place in an [Overlay] that can contain a widget. /// @@ -116,9 +119,27 @@ class OverlayEntry { final GlobalKey<_OverlayEntryState> _key = new GlobalKey<_OverlayEntryState>(); /// Remove this entry from the overlay. + /// + /// This should only be called once. + /// + /// If this method is called while the [SchedulerBinding.schedulerPhase] is + /// [SchedulerBinding.persistentCallbacks], i.e. during the build, layout, or + /// paint phases (see [WidgetsBinding.beginFrame]), then the removal is + /// delayed until the post-frame callbacks phase. Otherwise the removal is + /// done synchronously. This means that it is safe to call during builds, but + /// also that if you do call this during a build, the UI will not update until + /// the next frame (i.e. many milliseconds later). void remove() { - _overlay?._remove(this); + assert(_overlay != null); + OverlayState overlay = _overlay; _overlay = null; + if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) { + SchedulerBinding.instance.addPostFrameCallback((Duration duration) { + overlay._remove(this); + }); + } else { + overlay._remove(this); + } } /// Cause this entry to rebuild during the next pipeline flush. @@ -226,7 +247,7 @@ class Overlay extends StatefulWidget { /// /// Used to insert [OverlayEntry]s into the overlay using the [insert] and /// [insertAll] functions. -class OverlayState extends State { +class OverlayState extends State with TickerProviderStateMixin { final List _entries = new List(); @override @@ -268,9 +289,10 @@ class OverlayState extends State { } void _remove(OverlayEntry entry) { - _entries.remove(entry); - if (mounted) + if (mounted) { + _entries.remove(entry); setState(() { /* entry was removed */ }); + } } /// (DEBUG ONLY) Check whether a given entry is visible (i.e., not behind an @@ -319,7 +341,7 @@ class OverlayState extends State { if (entry.opaque) onstage = false; } else if (entry.maintainState) { - offstageChildren.add(new _OverlayEntry(entry)); + offstageChildren.add(new TickerMode(enabled: false, child: new _OverlayEntry(entry))); } } return new _Theatre( diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index a1eec8ecde6..d78abd47043 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -122,7 +122,11 @@ abstract class TransitionRoute extends OverlayRoute { AnimationController createAnimationController() { Duration duration = transitionDuration; assert(duration != null && duration >= Duration.ZERO); - return new AnimationController(duration: duration, debugLabel: debugLabel); + return new AnimationController( + duration: duration, + debugLabel: debugLabel, + vsync: navigator, + ); } /// Called to create the animation that exposes the current progress of diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index 3dc4a6bd808..46529c7abab 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -18,6 +18,7 @@ import 'notification_listener.dart'; import 'page_storage.dart'; import 'scroll_behavior.dart'; import 'scroll_configuration.dart'; +import 'ticker_provider.dart'; /// Identifies one or both limits of a [Scrollable] in terms of its scrollDirection. enum ScrollableEdge { @@ -262,11 +263,11 @@ class Scrollable extends StatefulWidget { /// terms of the [pixelOffsetToScrollOffset] and /// [scrollOffsetToPixelOffset] methods. @optionalTypeArgs -class ScrollableState extends State { +class ScrollableState extends State with SingleTickerProviderStateMixin { @override void initState() { super.initState(); - _controller = new AnimationController.unbounded() + _controller = new AnimationController.unbounded(vsync: this) ..addListener(_handleAnimationChanged) ..addStatusListener(_handleAnimationStatusChanged); _scrollOffset = PageStorage.of(context)?.readState(context) ?? config.initialScrollOffset ?? 0.0; diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index 283f9dfd3ee..2d930275b13 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; import 'package:meta/meta.dart'; import 'basic.dart'; @@ -82,6 +83,10 @@ class TextSelectionOverlay implements TextSelectionDelegate { this.toolbarBuilder }): _input = input { assert(context != null); + final OverlayState overlay = Overlay.of(context); + assert(overlay != null); + _handleController = new AnimationController(duration: _kFadeDuration, vsync: overlay); + _toolbarController = new AnimationController(duration: _kFadeDuration, vsync: overlay); } /// The context in which the selection handles should appear. @@ -117,8 +122,8 @@ class TextSelectionOverlay implements TextSelectionDelegate { /// Controls the fade-in animations. static const Duration _kFadeDuration = const Duration(milliseconds: 150); - final AnimationController _handleController = new AnimationController(duration: _kFadeDuration); - final AnimationController _toolbarController = new AnimationController(duration: _kFadeDuration); + AnimationController _handleController; + AnimationController _toolbarController; Animation get _handleOpacity => _handleController.view; Animation get _toolbarOpacity => _toolbarController.view; @@ -153,11 +158,26 @@ class TextSelectionOverlay implements TextSelectionDelegate { } /// Updates the overlay after the [selection] has changed. + /// + /// If this method is called while the [SchedulerBinding.schedulerPhase] is + /// [SchedulerBinding.persistentCallbacks], i.e. during the build, layout, or + /// paint phases (see [WidgetsBinding.beginFrame]), then the update is delayed + /// until the post-frame callbacks phase. Otherwise the update is done + /// synchronously. This means that it is safe to call during builds, but also + /// that if you do call this during a build, the UI will not update until the + /// next frame (i.e. many milliseconds later). void update(InputValue newInput) { if (_input == newInput) return; - _input = newInput; + if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) { + SchedulerBinding.instance.addPostFrameCallback(_markNeedsBuild); + } else { + _markNeedsBuild(); + } + } + + void _markNeedsBuild([Duration duration]) { if (_handles != null) { _handles[0].markNeedsBuild(); _handles[1].markNeedsBuild(); diff --git a/packages/flutter/lib/src/widgets/ticker_provider.dart b/packages/flutter/lib/src/widgets/ticker_provider.dart new file mode 100644 index 00000000000..77b3406bf49 --- /dev/null +++ b/packages/flutter/lib/src/widgets/ticker_provider.dart @@ -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 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, 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 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, TickerProvider { // ignore: TYPE_ARGUMENT_NOT_MATCHING_BOUNDS, https://github.com/dart-lang/sdk/issues/25232 + + Set _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 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(); + } +} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index 5866c3b7c17..9c4b7d1664c 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -48,15 +48,16 @@ export 'src/widgets/raw_keyboard_listener.dart'; export 'src/widgets/routes.dart'; export 'src/widgets/scroll_behavior.dart'; export 'src/widgets/scroll_configuration.dart'; +export 'src/widgets/scrollable.dart'; export 'src/widgets/scrollable_grid.dart'; export 'src/widgets/scrollable_list.dart'; -export 'src/widgets/scrollable.dart'; export 'src/widgets/semantics_debugger.dart'; export 'src/widgets/size_changed_layout_notifier.dart'; export 'src/widgets/status_transitions.dart'; export 'src/widgets/table.dart'; -export 'src/widgets/text_selection.dart'; export 'src/widgets/text.dart'; +export 'src/widgets/text_selection.dart'; +export 'src/widgets/ticker_provider.dart'; export 'src/widgets/title.dart'; export 'src/widgets/transitions.dart'; export 'src/widgets/unique_widget.dart'; diff --git a/packages/flutter/test/animation/animation_controller_test.dart b/packages/flutter/test/animation/animation_controller_test.dart index d2f76135c66..c2396ee4508 100644 --- a/packages/flutter/test/animation/animation_controller_test.dart +++ b/packages/flutter/test/animation/animation_controller_test.dart @@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/animation.dart'; import 'package:flutter/widgets.dart'; +import 'animation_tester.dart'; + void main() { setUp(() { WidgetsFlutterBinding.ensureInitialized(); @@ -14,7 +16,8 @@ void main() { test('Can set value during status callback', () { AnimationController controller = new AnimationController( - duration: const Duration(milliseconds: 100) + duration: const Duration(milliseconds: 100), + vsync: const TestVSync(), ); bool didComplete = false; bool didDismiss = false; @@ -45,7 +48,8 @@ void main() { test('Receives status callbacks for forward and reverse', () { AnimationController controller = new AnimationController( - duration: const Duration(milliseconds: 100) + duration: const Duration(milliseconds: 100), + vsync: const TestVSync(), ); List valueLog = []; List log = []; @@ -107,7 +111,8 @@ void main() { test('Forward and reverse from values', () { AnimationController controller = new AnimationController( - duration: const Duration(milliseconds: 100) + duration: const Duration(milliseconds: 100), + vsync: const TestVSync(), ); List valueLog = []; List statusLog = []; @@ -134,7 +139,8 @@ void main() { test('Forward only from value', () { AnimationController controller = new AnimationController( - duration: const Duration(milliseconds: 100) + duration: const Duration(milliseconds: 100), + vsync: const TestVSync(), ); List valueLog = []; List statusLog = []; @@ -154,7 +160,8 @@ void main() { test('Can fling to upper and lower bounds', () { AnimationController controller = new AnimationController( - duration: const Duration(milliseconds: 100) + duration: const Duration(milliseconds: 100), + vsync: const TestVSync(), ); controller.fling(); @@ -166,7 +173,8 @@ void main() { AnimationController largeRangeController = new AnimationController( duration: const Duration(milliseconds: 100), lowerBound: -30.0, - upperBound: 45.0 + upperBound: 45.0, + vsync: const TestVSync(), ); largeRangeController.fling(); @@ -182,7 +190,8 @@ void main() { test('lastElapsedDuration control test', () { AnimationController controller = new AnimationController( - duration: const Duration(milliseconds: 100) + duration: const Duration(milliseconds: 100), + vsync: const TestVSync(), ); controller.forward(); WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 20)); @@ -194,7 +203,8 @@ void main() { test('toString control test', () { AnimationController controller = new AnimationController( - duration: const Duration(milliseconds: 100) + duration: const Duration(milliseconds: 100), + vsync: const TestVSync(), ); expect(controller.toString(), hasOneLineDescription); controller.forward(); diff --git a/packages/flutter/test/animation/animation_tester.dart b/packages/flutter/test/animation/animation_tester.dart new file mode 100644 index 00000000000..348f473c760 --- /dev/null +++ b/packages/flutter/test/animation/animation_tester.dart @@ -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); +} diff --git a/packages/flutter/test/animation/animations_test.dart b/packages/flutter/test/animation/animations_test.dart index 7f3a3e9ece0..f5708d253e8 100644 --- a/packages/flutter/test/animation/animations_test.dart +++ b/packages/flutter/test/animation/animations_test.dart @@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/animation.dart'; import 'package:flutter/widgets.dart'; +import 'animation_tester.dart'; + void main() { setUp(() { WidgetsFlutterBinding.ensureInitialized(); @@ -24,7 +26,8 @@ void main() { curvedAnimation.reverseCurve = Curves.elasticOut; expect(curvedAnimation.toString(), hasOneLineDescription); AnimationController controller = new AnimationController( - duration: const Duration(milliseconds: 500) + duration: const Duration(milliseconds: 500), + vsync: const TestVSync(), ); controller ..value = 0.5 @@ -48,7 +51,9 @@ void main() { }); test('ProxyAnimation set parent generates value changed', () { - AnimationController controller = new AnimationController(); + AnimationController controller = new AnimationController( + vsync: const TestVSync(), + ); controller.value = 0.5; bool didReceiveCallback = false; ProxyAnimation animation = new ProxyAnimation() @@ -65,7 +70,9 @@ void main() { }); test('ReverseAnimation calls listeners', () { - AnimationController controller = new AnimationController(); + AnimationController controller = new AnimationController( + vsync: const TestVSync(), + ); controller.value = 0.5; bool didReceiveCallback = false; void listener() { @@ -85,8 +92,12 @@ void main() { }); test('TrainHoppingAnimation', () { - AnimationController currentTrain = new AnimationController(); - AnimationController nextTrain = new AnimationController(); + AnimationController currentTrain = new AnimationController( + vsync: const TestVSync(), + ); + AnimationController nextTrain = new AnimationController( + vsync: const TestVSync(), + ); currentTrain.value = 0.5; nextTrain.value = 0.75; bool didSwitchTrains = false; diff --git a/packages/flutter/test/animation/tween_test.dart b/packages/flutter/test/animation/tween_test.dart index c6f44c2e85c..17579e66f59 100644 --- a/packages/flutter/test/animation/tween_test.dart +++ b/packages/flutter/test/animation/tween_test.dart @@ -6,19 +6,25 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/animation.dart'; import 'package:flutter/widgets.dart'; +import 'animation_tester.dart'; + void main() { test('Can chain tweens', () { Tween tween = new Tween(begin: 0.30, end: 0.50); expect(tween, hasOneLineDescription); Animatable chain = tween.chain(new Tween(begin: 0.50, end: 1.0)); - AnimationController controller = new AnimationController(); + AnimationController controller = new AnimationController( + vsync: const TestVSync(), + ); expect(chain.evaluate(controller), 0.40); expect(chain, hasOneLineDescription); }); test('Can animated tweens', () { Tween tween = new Tween(begin: 0.30, end: 0.50); - AnimationController controller = new AnimationController(); + AnimationController controller = new AnimationController( + vsync: const TestVSync(), + ); Animation animation = tween.animate(controller); controller.value = 0.50; expect(animation.value, 0.40); diff --git a/packages/flutter/test/widget/animated_size_test.dart b/packages/flutter/test/widget/animated_size_test.dart index aabc4999812..c0661b45330 100644 --- a/packages/flutter/test/widget/animated_size_test.dart +++ b/packages/flutter/test/widget/animated_size_test.dart @@ -21,6 +21,7 @@ void main() { new Center( child: new AnimatedSize( duration: const Duration(milliseconds: 200), + vsync: tester, child: new SizedBox( width: 100.0, height: 100.0 @@ -37,6 +38,7 @@ void main() { new Center( child: new AnimatedSize( duration: new Duration(milliseconds: 200), + vsync: tester, child: new SizedBox( width: 200.0, height: 200.0 @@ -63,6 +65,7 @@ void main() { new Center( child: new AnimatedSize( duration: new Duration(milliseconds: 200), + vsync: tester, child: new SizedBox( width: 100.0, height: 100.0 @@ -94,6 +97,7 @@ void main() { height: 100.0, child: new AnimatedSize( duration: const Duration(milliseconds: 200), + vsync: tester, child: new SizedBox( width: 100.0, height: 100.0 @@ -114,6 +118,7 @@ void main() { height: 100.0, child: new AnimatedSize( duration: const Duration(milliseconds: 200), + vsync: tester, child: new SizedBox( width: 200.0, height: 200.0 @@ -134,6 +139,7 @@ void main() { new Center( child: new AnimatedSize( duration: const Duration(milliseconds: 200), + vsync: tester, child: new AnimatedContainer( duration: const Duration(milliseconds: 100), width: 100.0, @@ -151,6 +157,7 @@ void main() { new Center( child: new AnimatedSize( duration: const Duration(milliseconds: 200), + vsync: tester, child: new AnimatedContainer( duration: const Duration(milliseconds: 100), width: 200.0, diff --git a/packages/flutter/test/widget/flow_test.dart b/packages/flutter/test/widget/flow_test.dart index 38d533cbe51..ded41402b14 100644 --- a/packages/flutter/test/widget/flow_test.dart +++ b/packages/flutter/test/widget/flow_test.dart @@ -32,7 +32,9 @@ class TestFlowDelegate extends FlowDelegate { void main() { testWidgets('Flow control test', (WidgetTester tester) async { - AnimationController startOffset = new AnimationController.unbounded(); + AnimationController startOffset = new AnimationController.unbounded( + vsync: tester, + ); List log = []; Widget buildBox(int i) { diff --git a/packages/flutter/test/widget/positioned_test.dart b/packages/flutter/test/widget/positioned_test.dart index 1ae39d402b6..f1350a445ef 100644 --- a/packages/flutter/test/widget/positioned_test.dart +++ b/packages/flutter/test/widget/positioned_test.dart @@ -23,7 +23,8 @@ void main() { ) ); final AnimationController controller = new AnimationController( - duration: const Duration(seconds: 10) + duration: const Duration(seconds: 10), + vsync: tester, ); final List sizes = []; final List positions = []; diff --git a/packages/flutter/test/widget/ticker_provider_test.dart b/packages/flutter/test/widget/ticker_provider_test.dart new file mode 100644 index 00000000000..f7671aa9968 --- /dev/null +++ b/packages/flutter/test/widget/ticker_provider_test.dart @@ -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: { + '/test': (BuildContext context) => new Text('hello'), + }, + )); + expect(tester.binding.transientCallbackCount, 1); + tester.state/**/(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/**/(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); + }); +} diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index 75ff34c67c5..d1be525d93e 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -226,14 +226,17 @@ abstract class TestWidgetsFlutterBinding extends BindingBase /// test failures. bool showAppDumpInErrors = false; - /// Call the callback inside a [FakeAsync] scope on which [pump] can + /// Call the testBody inside a [FakeAsync] scope on which [pump] can /// advance time. /// /// Returns a future which completes when the test has run. /// /// Called by the [testWidgets] and [benchmarkWidgets] functions to /// run a test. - Future runTest(Future callback()); + /// + /// The `invariantTester` argument is called after the `testBody`'s [Future] + /// completes. If it throws, then the test is marked as failed. + Future runTest(Future testBody(), VoidCallback invariantTester); /// This is called during test execution before and after the body has been /// executed. @@ -266,7 +269,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase _currentTestCompleter.complete(null); } - Future _runTest(Future callback()) { + Future _runTest(Future testBody(), VoidCallback invariantTester) { assert(inTest); _oldExceptionHandler = FlutterError.onError; int _exceptionCount = 0; // number of un-taken exceptions @@ -357,20 +360,20 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ); _parentZone = Zone.current; Zone testZone = _parentZone.fork(specification: errorHandlingZoneSpecification); - testZone.runUnaryGuarded(_runTestBody, callback) + testZone.runBinaryGuarded(_runTestBody, testBody, invariantTester) .whenComplete(_testCompletionHandler); asyncBarrier(); // When using AutomatedTestWidgetsFlutterBinding, this flushes the microtasks. return _currentTestCompleter.future; } - Future _runTestBody(Future callback()) async { + Future _runTestBody(Future testBody(), VoidCallback invariantTester) async { assert(inTest); runApp(new Container(key: new UniqueKey(), child: _kPreTestMessage)); // Reset the tree to a known state. await pump(); // run the test - await callback(); + await testBody(); asyncBarrier(); // drains the microtasks in `flutter test` mode (when using AutomatedTestWidgetsFlutterBinding) if (_pendingExceptionDetails == null) { @@ -379,6 +382,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase // alone so that we don't cause more spurious errors. runApp(new Container(key: new UniqueKey(), child: _kPostTestMessage)); // Unmount any remaining widgets. await pump(); + invariantTester(); _verifyInvariants(); } @@ -489,24 +493,24 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { } @override - Future runTest(Future callback()) { + Future runTest(Future testBody(), VoidCallback invariantTester) { assert(!inTest); assert(_fakeAsync == null); assert(_clock == null); _fakeAsync = new FakeAsync(); _clock = _fakeAsync.getClock(new DateTime.utc(2015, 1, 1)); - Future callbackResult; + Future testBodyResult; _fakeAsync.run((FakeAsync fakeAsync) { assert(fakeAsync == _fakeAsync); - callbackResult = _runTest(callback); + testBodyResult = _runTest(testBody, invariantTester); assert(inTest); }); - // callbackResult is a Future that was created in the Zone of the fakeAsync. + // testBodyResult is a Future that was created in the Zone of the fakeAsync. // This means that if we call .then() on it (as the test framework is about to), // it will register a microtask to handle the future _in the fake async zone_. // To avoid this, we wrap it in a Future that we've created _outside_ the fake // async zone. - return new Future.value(callbackResult); + return new Future.value(testBodyResult); } @override @@ -686,10 +690,10 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { } @override - Future runTest(Future callback()) async { + Future runTest(Future testBody(), VoidCallback invariantTester) async { assert(!inTest); _inTest = true; - return _runTest(callback); + return _runTest(testBody, invariantTester); } @override diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart index f25dfdd8720..9849ac8b788 100644 --- a/packages/flutter_test/lib/src/widget_tester.dart +++ b/packages/flutter_test/lib/src/widget_tester.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import 'package:test/test.dart' as test_package; @@ -50,7 +51,7 @@ void testWidgets(String description, WidgetTesterCallback callback, { WidgetTester tester = new WidgetTester._(binding); timeout ??= binding.defaultTestTimeout; test_package.group('-', () { - test_package.test(description, () => binding.runTest(() => callback(tester)), skip: skip); + test_package.test(description, () => binding.runTest(() => callback(tester), tester._endOfTestVerifications), skip: skip); test_package.tearDown(binding.postTest); }, timeout: timeout); } @@ -108,7 +109,7 @@ Future benchmarkWidgets(WidgetTesterCallback callback) { TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); assert(binding is! AutomatedTestWidgetsFlutterBinding); WidgetTester tester = new WidgetTester._(binding); - return binding.runTest(() => callback(tester)) ?? new Future.value(); + return binding.runTest(() => callback(tester), tester._endOfTestVerifications) ?? new Future.value(); } /// Assert that `actual` matches `matcher`. @@ -143,7 +144,10 @@ void expectSync(dynamic actual, dynamic matcher, { } /// Class that programmatically interacts with widgets and the test environment. -class WidgetTester extends WidgetController implements HitTestDispatcher { +/// +/// For convenience, instances of this class (such as the one provided by +/// `testWidget`) can be used as the `vsync` for `AnimationController` objects. +class WidgetTester extends WidgetController implements HitTestDispatcher, TickerProvider { WidgetTester._(TestWidgetsFlutterBinding binding) : super(binding) { if (binding is LiveTestWidgetsFlutterBinding) binding.deviceEventDispatcher = this; @@ -328,6 +332,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher { } bool _isPrivate(Type type) { + // used above so that we don't suggest matchers for private types return '_'.matchAsPrefix(type.toString()) != null; } @@ -348,4 +353,62 @@ class WidgetTester extends WidgetController implements HitTestDispatcher { Future idle() { return TestAsyncUtils.guard(() => binding.idle()); } + + Set _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(); + } } diff --git a/packages/flutter_test/test/widget_tester_test.dart b/packages/flutter_test/test/widget_tester_test.dart index 421c080f583..bfb081ab8ec 100644 --- a/packages/flutter_test/test/widget_tester_test.dart +++ b/packages/flutter_test/test/widget_tester_test.dart @@ -73,7 +73,10 @@ void main() { await tester.pumpWidget(new Text('foo')); int count; - AnimationController test = new AnimationController(duration: const Duration(milliseconds: 5100)); + AnimationController test = new AnimationController( + duration: const Duration(milliseconds: 5100), + vsync: tester, + ); count = await tester.pumpUntilNoTransientCallbacks(const Duration(seconds: 1)); expect(count, 0);