diff --git a/examples/address_book/lib/main.dart b/examples/address_book/lib/main.dart index 636df80513..2bca9be10f 100644 --- a/examples/address_book/lib/main.dart +++ b/examples/address_book/lib/main.dart @@ -11,7 +11,7 @@ class Field extends StatelessComponent { this.inputKey, this.icon, this.placeholder - }): super(key: key); + }) : super(key: key); final GlobalKey inputKey; final String icon; @@ -101,7 +101,7 @@ void main() { title: 'Address Book', theme: theme, routes: { - '/': (NavigatorState navigator, Route route) => new AddressBookHome(navigator: navigator) + '/': (RouteArguments args) => new AddressBookHome(navigator: args.navigator) } )); } diff --git a/examples/demo_launcher/lib/main.dart b/examples/demo_launcher/lib/main.dart index 3a80555ccc..2bbef2362b 100644 --- a/examples/demo_launcher/lib/main.dart +++ b/examples/demo_launcher/lib/main.dart @@ -42,8 +42,8 @@ void launch(String relativeUrl, String bundle) { activity.startActivity(intent); } -class SkyDemo { - SkyDemo({ +class FlutterDemo { + FlutterDemo({ name, this.href, this.bundle, @@ -60,8 +60,8 @@ class SkyDemo { final BoxDecoration decoration; } -List demos = [ - new SkyDemo( +List demos = [ + new FlutterDemo( name: 'Stocks', href: '../../stocks/lib/main.dart', bundle: 'stocks.skyx', @@ -74,7 +74,7 @@ List demos = [ ) ) ), - new SkyDemo( + new FlutterDemo( name: 'Asteroids', href: '../../game/lib/main.dart', bundle: 'game.skyx', @@ -87,7 +87,7 @@ List demos = [ ) ) ), - new SkyDemo( + new FlutterDemo( name: 'Fitness', href: '../../fitness/lib/main.dart', bundle: 'fitness.skyx', @@ -97,7 +97,7 @@ List demos = [ backgroundColor: Colors.indigo[500] ) ), - new SkyDemo( + new FlutterDemo( name: 'Swipe Away', href: '../../widgets/card_collection.dart', bundle: 'cards.skyx', @@ -107,7 +107,7 @@ List demos = [ backgroundColor: Colors.redAccent[200] ) ), - new SkyDemo( + new FlutterDemo( name: 'Interactive Text', href: '../../rendering/interactive_flex.dart', bundle: 'interactive_flex.skyx', @@ -120,7 +120,7 @@ List demos = [ // new SkyDemo( // 'Touch Demo', '../../rendering/touch_demo.dart', 'Simple example showing handling of touch events at a low level'), - new SkyDemo( + new FlutterDemo( name: 'Minedigger Game', href: '../../mine_digger/lib/main.dart', bundle: 'mine_digger.skyx', @@ -138,44 +138,47 @@ List demos = [ const double kCardHeight = 120.0; const EdgeDims kListPadding = const EdgeDims.all(4.0); -class DemoList extends StatelessComponent { - Widget buildCardContents(SkyDemo demo) { - return new Container( - decoration: demo.decoration, - child: new InkWell( - child: new Container( - margin: const EdgeDims.only(top: 24.0, left: 24.0), - child: new Column([ - new Text(demo.name, style: demo.textTheme.title), - new Flexible( - child: new Text(demo.description, style: demo.textTheme.subhead) - ) - ], - alignItems: FlexAlignItems.start +class DemoCard extends StatelessComponent { + DemoCard({ Key key, this.demo }) : super(key: key); + + final FlutterDemo demo; + + Widget build(BuildContext context) { + return new Container( + height: kCardHeight, + child: new Card( + child: new Container( + decoration: demo.decoration, + child: new InkWell( + onTap: () => launch(demo.href, demo.bundle), + child: new Container( + margin: const EdgeDims.only(top: 24.0, left: 24.0), + child: new Column([ + new Text(demo.name, style: demo.textTheme.title), + new Flexible( + child: new Text(demo.description, style: demo.textTheme.subhead) + ) + ], + alignItems: FlexAlignItems.start + ) ) ) ) - ); - } - - Widget buildDemo(BuildContext context, SkyDemo demo) { - return new GestureDetector( - key: demo.key, - onTap: () => launch(demo.href, demo.bundle), - child: new Container( - height: kCardHeight, - child: new Card( - child: buildCardContents(demo) - ) ) ); } +} + +class DemoList extends StatelessComponent { + Widget _buildDemoCard(BuildContext context, FlutterDemo demo) { + return new DemoCard(key: demo.key, demo: demo); + } Widget build(BuildContext context) { - return new ScrollableList( + return new ScrollableList( items: demos, itemExtent: kCardHeight, - itemBuilder: buildDemo, + itemBuilder: _buildDemoCard, padding: kListPadding ); } @@ -200,10 +203,10 @@ class DemoHome extends StatelessComponent { void main() { runApp(new App( - title: 'Sky Demos', + title: 'Flutter Demos', theme: _theme, routes: { - '/': (NavigatorState navigator, Route route) => new DemoHome() + '/': (RouteArguments args) => new DemoHome() } )); } diff --git a/examples/fitness/lib/feed.dart b/examples/fitness/lib/feed.dart index b689531dcf..b7555f92e6 100644 --- a/examples/fitness/lib/feed.dart +++ b/examples/fitness/lib/feed.dart @@ -33,15 +33,13 @@ class DialogMenuItem extends StatelessComponent { Function onPressed; Widget build(BuildContext context) { - return new GestureDetector( - onTap: onPressed, - child: new Container( - height: 48.0, - child: new InkWell( - child: new Padding( - padding: const EdgeDims.symmetric(horizontal: 16.0), - child: new Row(children) - ) + return new Container( + height: 48.0, + child: new InkWell( + onTap: onPressed, + child: new Padding( + padding: const EdgeDims.symmetric(horizontal: 16.0), + child: new Row(children) ) ) ); @@ -62,25 +60,20 @@ class FeedFragment extends StatefulComponent { class FeedFragmentState extends State { FitnessMode _fitnessMode = FitnessMode.feed; - AnimationStatus _snackBarStatus = AnimationStatus.dismissed; + PerformanceStatus _snackBarStatus = PerformanceStatus.dismissed; bool _isShowingSnackBar = false; void _handleFitnessModeChange(FitnessMode value) { setState(() { _fitnessMode = value; - _drawerShowing = false; }); + config.navigator.pop(); } - Drawer buildDrawer() { - if (_drawerStatus == AnimationStatus.dismissed) - return null; - return new Drawer( - showing: _drawerShowing, - level: 3, - onDismissed: _handleDrawerDismissed, + void _showDrawer() { + showDrawer( navigator: config.navigator, - children: [ + child: new Block([ new DrawerHeader(child: new Text('Fitness')), new DrawerItem( icon: 'action/view_list', @@ -100,26 +93,10 @@ class FeedFragmentState extends State { new DrawerItem( icon: 'action/help', child: new Text('Help & Feedback')) - ] + ]) ); } - bool _drawerShowing = false; - AnimationStatus _drawerStatus = AnimationStatus.dismissed; - - void _handleOpenDrawer() { - setState(() { - _drawerShowing = true; - _drawerStatus = AnimationStatus.forward; - }); - } - - void _handleDrawerDismissed() { - setState(() { - _drawerStatus = AnimationStatus.dismissed; - }); - } - void _handleShowSettings() { config.navigator.pop(); config.navigator.pushNamed('/settings'); @@ -137,7 +114,7 @@ class FeedFragmentState extends State { return new ToolBar( left: new IconButton( icon: "navigation/menu", - onPressed: _handleOpenDrawer), + onPressed: _showDrawer), center: new Text(fitnessModeTitle) ); } @@ -149,7 +126,7 @@ class FeedFragmentState extends State { setState(() { _undoItem = item; _isShowingSnackBar = true; - _snackBarStatus = AnimationStatus.forward; + _snackBarStatus = PerformanceStatus.forward; }); } @@ -230,13 +207,13 @@ class FeedFragmentState extends State { } Widget buildSnackBar() { - if (_snackBarStatus == AnimationStatus.dismissed) + if (_snackBarStatus == PerformanceStatus.dismissed) return null; return new SnackBar( showing: _isShowingSnackBar, content: new Text("Item deleted."), actions: [new SnackBarAction(label: "UNDO", onPressed: _handleUndo)], - onDismissed: () { setState(() { _snackBarStatus = AnimationStatus.dismissed; }); } + onDismissed: () { setState(() { _snackBarStatus = PerformanceStatus.dismissed; }); } ); } @@ -264,8 +241,7 @@ class FeedFragmentState extends State { toolbar: buildToolBar(), body: buildBody(), snackBar: buildSnackBar(), - floatingActionButton: buildFloatingActionButton(), - drawer: buildDrawer() + floatingActionButton: buildFloatingActionButton() ); } } diff --git a/examples/fitness/lib/main.dart b/examples/fitness/lib/main.dart index 1f12e111a5..98f87db579 100644 --- a/examples/fitness/lib/main.dart +++ b/examples/fitness/lib/main.dart @@ -92,8 +92,6 @@ class FitnessApp extends StatefulComponent { class FitnessAppState extends State { UserDataImpl _userData; - Map _routes; - void initState() { super.initState(); loadFitnessData().then((UserData data) { @@ -102,36 +100,6 @@ class FitnessAppState extends State { print("Failed to load data: $e"); setState(() => _userData = new UserDataImpl()); }); - - _routes = { - '/': (NavigatorState navigator, Route route) { - return new FeedFragment( - navigator: navigator, - userData: _userData, - onItemCreated: _handleItemCreated, - onItemDeleted: _handleItemDeleted - ); - }, - '/meals/new': (navigator, route) { - return new MealFragment( - navigator: navigator, - onCreated: _handleItemCreated - ); - }, - '/measurements/new': (NavigatorState navigator, Route route) { - return new MeasurementFragment( - navigator: navigator, - onCreated: _handleItemCreated - ); - }, - '/settings': (navigator, route) { - return new SettingsFragment( - navigator: navigator, - userData: _userData, - updater: settingsUpdater - ); - } - }; } void _handleItemCreated(FitnessItem item) { @@ -158,17 +126,43 @@ class FitnessAppState extends State { }); } - final ThemeData _theme = new ThemeData( - brightness: ThemeBrightness.light, - primarySwatch: Colors.indigo, - accentColor: Colors.pinkAccent[200] - ); - Widget build(BuildContext) { return new App( - theme: _theme, + theme: new ThemeData( + brightness: ThemeBrightness.light, + primarySwatch: Colors.indigo, + accentColor: Colors.pinkAccent[200] + ), title: 'Fitness', - routes: _routes + routes: { + '/': (RouteArguments args) { + return new FeedFragment( + navigator: args.navigator, + userData: _userData, + onItemCreated: _handleItemCreated, + onItemDeleted: _handleItemDeleted + ); + }, + '/meals/new': (RouteArguments args) { + return new MealFragment( + navigator: args.navigator, + onCreated: _handleItemCreated + ); + }, + '/measurements/new': (RouteArguments args) { + return new MeasurementFragment( + navigator: args.navigator, + onCreated: _handleItemCreated + ); + }, + '/settings': (RouteArguments args) { + return new SettingsFragment( + navigator: args.navigator, + userData: _userData, + updater: settingsUpdater + ); + } + } ); } } diff --git a/examples/fitness/lib/meal.dart b/examples/fitness/lib/meal.dart index 5e65025a5c..e66fb9f900 100644 --- a/examples/fitness/lib/meal.dart +++ b/examples/fitness/lib/meal.dart @@ -65,12 +65,13 @@ class MealFragmentState extends State { icon: "navigation/close", onPressed: config.navigator.pop), center: new Text('New Meal'), - right: [new InkWell( - child: new GestureDetector( + right: [ + // TODO(abarth): Should this be a FlatButton? + new InkWell( onTap: _handleSave, child: new Text('SAVE') ) - )] + ] ); } diff --git a/examples/fitness/lib/measurement.dart b/examples/fitness/lib/measurement.dart index 573228656e..695d82a4c6 100644 --- a/examples/fitness/lib/measurement.dart +++ b/examples/fitness/lib/measurement.dart @@ -136,12 +136,13 @@ class MeasurementFragmentState extends State { icon: "navigation/close", onPressed: config.navigator.pop), center: new Text('New Measurement'), - right: [new InkWell( - child: new GestureDetector( + right: [ + // TODO(abarth): Should this be a FlatButton? + new InkWell( onTap: _handleSave, child: new Text('SAVE') ) - )] + ] ); } diff --git a/examples/game/example_effect_line.dart b/examples/game/example_effect_line.dart index 4a6e7cc150..05323d00d9 100644 --- a/examples/game/example_effect_line.dart +++ b/examples/game/example_effect_line.dart @@ -62,7 +62,7 @@ class TestAppState extends State { ); } - Column _buildColumn(NavigatorState navigator, Route route) { + Column _buildColumn(RouteArguments args) { return new Column([ new Flexible(child: _buildSpriteWidget()), _buildTabBar() diff --git a/examples/game/lib/main.dart b/examples/game/lib/main.dart index a93038d44d..2cb407aa39 100644 --- a/examples/game/lib/main.dart +++ b/examples/game/lib/main.dart @@ -92,11 +92,11 @@ class GameDemoState extends State { ); } - Widget _buildGameScene(NavigatorState navigator, Route route) { + Widget _buildGameScene(RouteArguments args) { return new SpriteWidget(_game, SpriteBoxTransformMode.fixedWidth); } - Widget _buildMainScene(navigator, route) { + Widget _buildMainScene(RouteArguments args) { return new Stack([ new SpriteWidget(new MainScreenBackground(), SpriteBoxTransformMode.fixedWidth), new Column([ @@ -109,10 +109,10 @@ class GameDemoState extends State { _sounds, (lastScore) { setState(() {_lastScore = lastScore;}); - navigator.pop(); + args.navigator.pop(); } ); - navigator.pushNamed('/game'); + args.navigator.pushNamed('/game'); }, texture: _spriteSheetUI['btn_play_up.png'], textureDown: _spriteSheetUI['btn_play_down.png'], diff --git a/examples/game/pubspec.yaml b/examples/game/pubspec.yaml index 69c481bc3f..8f12e91948 100644 --- a/examples/game/pubspec.yaml +++ b/examples/game/pubspec.yaml @@ -3,6 +3,7 @@ dependencies: sky: any sky_tools: any skysprites: any + box2d: any dependency_overrides: material_design_icons: path: ../../sky/packages/material_design_icons diff --git a/examples/game/test_bed.dart b/examples/game/test_bed.dart new file mode 100644 index 0000000000..653a566c0e --- /dev/null +++ b/examples/game/test_bed.dart @@ -0,0 +1,59 @@ +import 'dart:sky'; + +import 'package:sky/material.dart'; +import 'package:sky/rendering.dart'; +import 'package:sky/services.dart'; +import 'package:sky/widgets.dart'; +import 'package:skysprites/skysprites.dart'; + +AssetBundle _initBundle() { + if (rootBundle != null) + return rootBundle; + return new NetworkAssetBundle(Uri.base); +} + +final AssetBundle _bundle = _initBundle(); + +ImageMap _images; +SpriteSheet _spriteSheet; +TestBedApp _app; + +main() async { + _images = new ImageMap(_bundle); + + await _images.load([ + 'assets/sprites.png' + ]); + + String json = await _bundle.loadString('assets/sprites.json'); + _spriteSheet = new SpriteSheet(_images['assets/sprites.png'], json); + + _app = new TestBedApp(); + runApp(_app); +} + +class TestBedApp extends App { + + Widget build() { + ThemeData theme = new ThemeData( + brightness: ThemeBrightness.light, + primarySwatch: Colors.purple + ); + + return new Theme( + data: theme, + child: new Title( + title: 'Test Bed', + child: new SpriteWidget( + new TestBed(), + SpriteBoxTransformMode.letterbox + ) + ) + ); + } +} + +class TestBed extends NodeWithSize { + TestBed() : super(new Size(1024.0, 1024.0)) { + } +} diff --git a/examples/game/test_drawatlas.dart b/examples/game/test_drawatlas.dart index 135334eccc..dba1748a25 100644 --- a/examples/game/test_drawatlas.dart +++ b/examples/game/test_drawatlas.dart @@ -36,7 +36,7 @@ main() async { title: 'Test drawAtlas', theme: _theme, routes: { - '/': (NavigatorState navigator, Route route) { + '/': (RouteArguments args) { return new SpriteWidget( new TestDrawAtlas(), SpriteBoxTransformMode.fixedWidth diff --git a/examples/game/test_physics.dart b/examples/game/test_physics.dart new file mode 100644 index 0000000000..aabb773bd2 --- /dev/null +++ b/examples/game/test_physics.dart @@ -0,0 +1,96 @@ +import 'dart:sky'; + +import 'package:sky/material.dart'; +import 'package:sky/rendering.dart'; +import 'package:sky/services.dart'; +import 'package:sky/widgets.dart'; +import 'package:skysprites/skysprites.dart'; + +AssetBundle _initBundle() { + if (rootBundle != null) + return rootBundle; + return new NetworkAssetBundle(Uri.base); +} + +final AssetBundle _bundle = _initBundle(); + +ImageMap _images; +SpriteSheet _spriteSheet; + +main() async { + _images = new ImageMap(_bundle); + + await _images.load([ + 'assets/sprites.png' + ]); + + String json = await _bundle.loadString('assets/sprites.json'); + _spriteSheet = new SpriteSheet(_images['assets/sprites.png'], json); + + runApp(new App( + title: 'Test Physics', + theme: new ThemeData( + brightness: ThemeBrightness.light, + primarySwatch: Colors.purple + ), + routes: { + '/': (RouteArguments args) { + return new SpriteWidget( + new TestBed(), + SpriteBoxTransformMode.letterbox + ); + } + } + )); +} + +class TestBed extends NodeWithSize { + Sprite _ship; + Sprite _obstacle; + + TestBed() : super(new Size(1024.0, 1024.0)) { + PhysicsNode physicsNode = new PhysicsNode(new Offset(0.0, 100.0)); + + _ship = new Sprite(_spriteSheet["ship.png"]); + _ship.position = new Point(512.0, 512.0); + _ship.size = new Size(64.0, 64.0); + _ship.physicsBody = new PhysicsBody( + new PhysicsShapeGroup([ + new PhysicsShapeCircle(Point.origin, 32.0), + new PhysicsShapePolygon([new Point(0.0, 0.0), new Point(50.0, 0.0), new Point(50.0, 50.0), new Point(0.0, 50.0)]) + ]), + friction: 0.5, + tag: "ship" + ); + physicsNode.addChild(_ship); + + _obstacle = new Sprite(_spriteSheet["ship.png"]); + _obstacle.position = new Point(532.0, 800.0); + _obstacle.size = new Size(64.0, 64.0); + _obstacle.physicsBody = new PhysicsBody( + new PhysicsShapeCircle(Point.origin, 32.0), + type: PhysicsBodyType.static, + friction: 0.5, + tag: "obstacle" + ); + physicsNode.addChild(_obstacle); + physicsNode.addContactCallback(myCallback, "obstacle", "ship", PhysicsContactType.begin); + + addChild(physicsNode); + + userInteractionEnabled = true; + } + + void myCallback(PhysicsContactType type, PhysicsContact contact) { + print("CONTACT type: $type"); + contact.nodeB.removeFromParent(); + } + + bool handleEvent(SpriteBoxEvent event) { + if (event.type == "pointerdown") { + Point pos = convertPointToNodeSpace(event.boxPosition); + _ship.position = pos; + } + return true; + } +} diff --git a/examples/mine_digger/lib/main.dart b/examples/mine_digger/lib/main.dart index 8c823d8c50..4c78fe30c6 100644 --- a/examples/mine_digger/lib/main.dart +++ b/examples/mine_digger/lib/main.dart @@ -64,33 +64,25 @@ class MineDiggerState extends State { alive = true; hasWon = false; detectedCount = 0; - // Build the arrays. - cells = new List>(); - uiState = new List>(); - for (int iy = 0; iy != rows; iy++) { - cells.add(new List()); - uiState.add(new List()); - for (int ix = 0; ix != cols; ix++) { - cells[iy].add(false); - uiState[iy].add(CellState.covered); - } - } + // Initialize matrices. + cells = new List.generate(rows, (int row) { + return new List.filled(cols, false); + }); + uiState = new List.generate(rows, (int row) { + return new List.filled(cols, CellState.covered); + }); // Place the mines. Random random = new Random(); - int cellsRemaining = rows * cols; int minesRemaining = totalMineCount; - for (int x = 0; x < cols; x += 1) { - for (int y = 0; y < rows; y += 1) { - if (random.nextInt(cellsRemaining) < minesRemaining) { - cells[y][x] = true; - minesRemaining -= 1; - if (minesRemaining <= 0) - return; - } - cellsRemaining -= 1; + while (minesRemaining > 0) { + int pos = random.nextInt(rows * cols); + int row = pos ~/ rows; + int col = pos % cols; + if (!cells[row][col]) { + cells[row][col] = true; + minesRemaining--; } } - assert(false); } PointerEventListener _pointerDownHandlerFor(int posX, int posY) { @@ -106,9 +98,9 @@ class MineDiggerState extends State { Widget buildBoard() { bool hasCoveredCell = false; List flexRows = []; - for (int iy = 0; iy != 9; iy++) { + for (int iy = 0; iy < rows; iy++) { List row = []; - for (int ix = 0; ix != 9; ix++) { + for (int ix = 0; ix < cols; ix++) { CellState state = uiState[iy][ix]; int count = mineCount(ix, iy); if (!alive) { diff --git a/examples/rendering/spinning_flex.dart b/examples/rendering/spinning_flex.dart index db2055ca23..1e72aac95a 100644 --- a/examples/rendering/spinning_flex.dart +++ b/examples/rendering/spinning_flex.dart @@ -9,7 +9,7 @@ import 'package:sky/rendering.dart'; import 'solid_color_box.dart'; -double timeBase; +Duration timeBase; RenderTransform transformBox; void main() { @@ -34,10 +34,10 @@ void main() { scheduler.addPersistentFrameCallback(rotate); } -void rotate(double timeStamp) { +void rotate(Duration timeStamp) { if (timeBase == null) timeBase = timeStamp; - double delta = (timeStamp - timeBase) / 1000; // radians + double delta = (timeStamp - timeBase).inMicroseconds.toDouble() / Duration.MICROSECONDS_PER_SECOND; // radians transformBox.setIdentity(); transformBox.translate(transformBox.size.width / 2.0, transformBox.size.height / 2.0); diff --git a/examples/stocks/lib/main.dart b/examples/stocks/lib/main.dart index 4b2c1e89c3..a931d547ea 100644 --- a/examples/stocks/lib/main.dart +++ b/examples/stocks/lib/main.dart @@ -82,7 +82,7 @@ class StocksAppState extends State { if (path.length != 3) return null; if (_stocks.containsKey(path[2])) - return (navigator, route) => new StockSymbolViewer(navigator, _stocks[path[2]]); + return (RouteArguments args) => new StockSymbolViewer(args.navigator, _stocks[path[2]]); return null; } return null; @@ -93,8 +93,8 @@ class StocksAppState extends State { title: 'Stocks', theme: theme, routes: { - '/': (navigator, route) => new StockHome(navigator, _stocks, _symbols, _optimismSetting, modeUpdater), - '/settings': (navigator, route) => new StockSettings(navigator, _optimismSetting, _backupSetting, settingsUpdater) + '/': (RouteArguments args) => new StockHome(args.navigator, _stocks, _symbols, _optimismSetting, modeUpdater), + '/settings': (RouteArguments args) => new StockSettings(args.navigator, _optimismSetting, _backupSetting, settingsUpdater) }, onGenerateRoute: _getRoute ); diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index 523b668468..091e379f67 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart @@ -25,7 +25,7 @@ class StockHomeState extends State { bool _isSearching = false; String _searchQuery; - AnimationStatus _snackBarStatus = AnimationStatus.dismissed; + PerformanceStatus _snackBarStatus = PerformanceStatus.dismissed; bool _isSnackBarShowing = false; void _handleSearchBegin() { @@ -56,22 +56,6 @@ class StockHomeState extends State { }); } - bool _drawerShowing = false; - AnimationStatus _drawerStatus = AnimationStatus.dismissed; - - void _handleOpenDrawer() { - setState(() { - _drawerShowing = true; - _drawerStatus = AnimationStatus.forward; - }); - } - - void _handleDrawerDismissed() { - setState(() { - _drawerStatus = AnimationStatus.dismissed; - }); - } - bool _autorefresh = false; void _handleAutorefreshChanged(bool value) { setState(() { @@ -91,16 +75,10 @@ class StockHomeState extends State { ); } - Drawer buildDrawer() { - if (_drawerStatus == AnimationStatus.dismissed) - return null; - assert(_drawerShowing); // TODO(mpcomplete): this is always true - return new Drawer( - level: 3, - showing: _drawerShowing, - onDismissed: _handleDrawerDismissed, + void _showDrawer() { + showDrawer( navigator: config.navigator, - children: [ + child: new Block([ new DrawerHeader(child: new Text('Stocks')), new DrawerItem( icon: 'action/assessment', @@ -141,7 +119,7 @@ class StockHomeState extends State { new DrawerItem( icon: 'action/help', child: new Text('Help & Feedback')) - ] + ]) ); } @@ -154,7 +132,7 @@ class StockHomeState extends State { return new ToolBar( left: new IconButton( icon: "navigation/menu", - onPressed: _handleOpenDrawer + onPressed: _showDrawer ), center: new Text('Stocks'), right: [ @@ -246,20 +224,20 @@ class StockHomeState extends State { GlobalKey snackBarKey = new GlobalKey(label: 'snackbar'); Widget buildSnackBar() { - if (_snackBarStatus == AnimationStatus.dismissed) + if (_snackBarStatus == PerformanceStatus.dismissed) return null; return new SnackBar( showing: _isSnackBarShowing, content: new Text("Stock purchased!"), actions: [new SnackBarAction(label: "UNDO", onPressed: _handleUndo)], - onDismissed: () { setState(() { _snackBarStatus = AnimationStatus.dismissed; }); } + onDismissed: () { setState(() { _snackBarStatus = PerformanceStatus.dismissed; }); } ); } void _handleStockPurchased() { setState(() { _isSnackBarShowing = true; - _snackBarStatus = AnimationStatus.forward; + _snackBarStatus = PerformanceStatus.forward; }); } @@ -276,8 +254,7 @@ class StockHomeState extends State { toolbar: _isSearching ? buildSearchBar() : buildToolBar(), body: buildTabNavigator(), snackBar: buildSnackBar(), - floatingActionButton: buildFloatingActionButton(), - drawer: buildDrawer() + floatingActionButton: buildFloatingActionButton() ); } } diff --git a/examples/stocks/lib/stock_row.dart b/examples/stocks/lib/stock_row.dart index c985f82e12..da95327cf1 100644 --- a/examples/stocks/lib/stock_row.dart +++ b/examples/stocks/lib/stock_row.dart @@ -37,13 +37,13 @@ class StockRow extends StatelessComponent { static const double kHeight = 79.0; - GestureTapListener _getTapHandler(StockRowActionCallback callback) { + GestureTapCallback _getTapHandler(StockRowActionCallback callback) { if (callback == null) return null; return () => callback(stock, key, arrowKey, symbolKey, priceKey); } - GestureLongPressListener _getLongPressHandler(StockRowActionCallback callback) { + GestureLongPressCallback _getLongPressHandler(StockRowActionCallback callback) { if (callback == null) return null; return () => callback(stock, key, arrowKey, symbolKey, priceKey); @@ -55,52 +55,50 @@ class StockRow extends StatelessComponent { String changeInPrice = "${stock.percentChange.toStringAsFixed(2)}%"; if (stock.percentChange > 0) changeInPrice = "+" + changeInPrice; - return new GestureDetector( + return new InkWell( onTap: _getTapHandler(onPressed), onLongPress: _getLongPressHandler(onLongPressed), - child: new InkWell( - child: new Container( - padding: const EdgeDims.TRBL(16.0, 16.0, 20.0, 16.0), - decoration: new BoxDecoration( - border: new Border( - bottom: new BorderSide(color: Theme.of(context).dividerColor) - ) + child: new Container( + padding: const EdgeDims.TRBL(16.0, 16.0, 20.0, 16.0), + decoration: new BoxDecoration( + border: new Border( + bottom: new BorderSide(color: Theme.of(context).dividerColor) + ) + ), + child: new Row([ + new Container( + key: arrowKey, + child: new StockArrow(percentChange: stock.percentChange), + margin: const EdgeDims.only(right: 5.0) ), - child: new Row([ - new Container( - key: arrowKey, - child: new StockArrow(percentChange: stock.percentChange), - margin: const EdgeDims.only(right: 5.0) - ), - new Flexible( - child: new Row([ - new Flexible( - flex: 2, - child: new Text( - stock.symbol, - key: symbolKey - ) - ), - new Flexible( - child: new Text( - lastSale, - style: const TextStyle(textAlign: TextAlign.right), - key: priceKey - ) - ), - new Flexible( - child: new Text( - changeInPrice, - style: Theme.of(context).text.caption.copyWith(textAlign: TextAlign.right) - ) - ), - ], - alignItems: FlexAlignItems.baseline, - textBaseline: DefaultTextStyle.of(context).textBaseline - ) + new Flexible( + child: new Row([ + new Flexible( + flex: 2, + child: new Text( + stock.symbol, + key: symbolKey + ) + ), + new Flexible( + child: new Text( + lastSale, + style: const TextStyle(textAlign: TextAlign.right), + key: priceKey + ) + ), + new Flexible( + child: new Text( + changeInPrice, + style: Theme.of(context).text.caption.copyWith(textAlign: TextAlign.right) + ) + ), + ], + alignItems: FlexAlignItems.baseline, + textBaseline: DefaultTextStyle.of(context).textBaseline ) - ]) - ) + ) + ]) ) ); } diff --git a/examples/widgets/card_collection.dart b/examples/widgets/card_collection.dart index 8a69ab8321..fb0458b0ab 100644 --- a/examples/widgets/card_collection.dart +++ b/examples/widgets/card_collection.dart @@ -2,7 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:sky/animation.dart'; +import 'dart:sky' as sky; + import 'package:sky/material.dart'; import 'package:sky/painting.dart'; import 'package:sky/widgets.dart'; @@ -16,15 +17,22 @@ class CardModel { Key get key => new ObjectKey(this); } -class CardCollectionApp extends StatefulComponent { - CardCollectionAppState createState() => new CardCollectionAppState(); +class CardCollection extends StatefulComponent { + CardCollection({ this.navigator }); + + final NavigatorState navigator; + + CardCollectionState createState() => new CardCollectionState(); } -class CardCollectionAppState extends State { +class CardCollectionState extends State { static const TextStyle cardLabelStyle = const TextStyle(color: Colors.white, fontSize: 18.0, fontWeight: bold); + // TODO(hansmuller): need a local image asset + static const _sunshineURL = "http://www.walltor.com/images/wallpaper/good-morning-sunshine-58540.jpg"; + final TextStyle backgroundTextStyle = Typography.white.title.copyWith(textAlign: TextAlign.center); @@ -32,8 +40,7 @@ class CardCollectionAppState extends State { DismissDirection _dismissDirection = DismissDirection.horizontal; bool _snapToCenter = false; bool _fixedSizeCards = false; - bool _drawerShowing = false; - AnimationStatus _drawerStatus = AnimationStatus.dismissed; + bool _sunshine = false; InvalidatorCallback _invalidator; Size _cardCollectionSize = new Size(200.0, 200.0); @@ -108,17 +115,23 @@ class CardCollectionAppState extends State { } } - void _handleOpenDrawer() { - setState(() { - _drawerShowing = true; - _drawerStatus = AnimationStatus.forward; - }); - } - - void _handleDrawerDismissed() { - setState(() { - _drawerStatus = AnimationStatus.dismissed; - }); + void _showDrawer() { + showDrawer( + navigator: config.navigator, + child: new IconTheme( + data: const IconThemeData(color: IconThemeColor.black), + child: new Block([ + new DrawerHeader(child: new Text('Options')), + buildDrawerCheckbox("Snap fling scrolls to center", _snapToCenter, _toggleSnapToCenter), + buildDrawerCheckbox("Fixed size cards", _fixedSizeCards, _toggleFixedSizeCards), + buildDrawerCheckbox("Let the sun shine", _sunshine, _toggleSunshine), + new DrawerDivider(), + buildDrawerRadioItem(DismissDirection.horizontal, 'action/code'), + buildDrawerRadioItem(DismissDirection.left, 'navigation/arrow_back'), + buildDrawerRadioItem(DismissDirection.right, 'navigation/arrow_forward'), + ]) + ) + ); } String _dismissDirectionText(DismissDirection direction) { @@ -139,64 +152,47 @@ class CardCollectionAppState extends State { }); } - _changeDismissDirection(DismissDirection newDismissDirection) { + void _toggleSunshine() { setState(() { - _dismissDirection = newDismissDirection; - _drawerStatus = AnimationStatus.dismissed; + _sunshine = !_sunshine; }); } - Widget buildDrawer() { - if (_drawerStatus == AnimationStatus.dismissed) - return null; + void _changeDismissDirection(DismissDirection newDismissDirection) { + setState(() { + _dismissDirection = newDismissDirection; + }); + config.navigator.pop(); + } - Widget buildDrawerCheckbox(String label, bool value, Function callback) { - return new DrawerItem( - onPressed: callback, - child: new Row([ - new Flexible(child: new Text(label)), - new Checkbox(value: value, onChanged: (_) { callback(); }) - ]) - ); - } + Widget buildDrawerCheckbox(String label, bool value, Function callback) { + return new DrawerItem( + onPressed: callback, + child: new Row([ + new Flexible(child: new Text(label)), + new Checkbox(value: value, onChanged: (_) { callback(); }) + ]) + ); + } - Widget buildDrawerRadioItem(DismissDirection direction, String icon) { - return new DrawerItem( - icon: icon, - onPressed: () { _changeDismissDirection(direction); }, - child: new Row([ - new Flexible(child: new Text(_dismissDirectionText(direction))), - new Radio( - value: direction, - onChanged: _changeDismissDirection, - groupValue: _dismissDirection - ) - ]) - ); - } - - return new IconTheme( - data: const IconThemeData(color: IconThemeColor.black), - child: new Drawer( - level: 3, - showing: _drawerShowing, - onDismissed: _handleDrawerDismissed, - children: [ - new DrawerHeader(child: new Text('Options')), - buildDrawerCheckbox("Snap fling scrolls to center", _snapToCenter, _toggleSnapToCenter), - buildDrawerCheckbox("Fixed size cards", _fixedSizeCards, _toggleFixedSizeCards), - new DrawerDivider(), - buildDrawerRadioItem(DismissDirection.horizontal, 'action/code'), - buildDrawerRadioItem(DismissDirection.left, 'navigation/arrow_back'), - buildDrawerRadioItem(DismissDirection.right, 'navigation/arrow_forward'), - ] - ) + Widget buildDrawerRadioItem(DismissDirection direction, String icon) { + return new DrawerItem( + icon: icon, + onPressed: () { _changeDismissDirection(direction); }, + child: new Row([ + new Flexible(child: new Text(_dismissDirectionText(direction))), + new Radio( + value: direction, + onChanged: _changeDismissDirection, + groupValue: _dismissDirection + ) + ]) ); } Widget buildToolBar() { return new ToolBar( - left: new IconButton(icon: "navigation/menu", onPressed: _handleOpenDrawer), + left: new IconButton(icon: "navigation/menu", onPressed: _showDrawer), center: new Text('Swipe Away'), right: [ new Text(_dismissDirectionText(_dismissDirection)) @@ -285,6 +281,16 @@ class CardCollectionAppState extends State { }); } + sky.Shader _createShader(Rect bounds) { + return new LinearGradient( + begin: Point.origin, + end: new Point(0.0, bounds.height), + colors: [const Color(0x00FFFFFF), const Color(0xFFFFFFFF)], + stops: [0.1, 0.35] + ) + .createShader(); + } + Widget build(BuildContext context) { Widget cardCollection; @@ -306,6 +312,12 @@ class CardCollectionAppState extends State { ); } + if (_sunshine) + cardCollection = new Stack([ + new Column([new NetworkImage(src: _sunshineURL)]), + new ShaderMask(child: cardCollection, shaderCallback: _createShader) + ]); + Widget body = new SizeObserver( callback: _updateCardCollectionSize, child: new Container( @@ -329,24 +341,23 @@ class CardCollectionAppState extends State { body = new Stack([body, indicator]); } - return new Theme( - data: new ThemeData( - brightness: ThemeBrightness.light, - primarySwatch: Colors.blue, - accentColor: Colors.redAccent[200] - ), - child: new Title( - title: 'Cards', - child: new Scaffold( - toolbar: buildToolBar(), - drawer: buildDrawer(), - body: body - ) - ) + return new Scaffold( + toolbar: buildToolBar(), + body: body ); } } void main() { - runApp(new CardCollectionApp()); + runApp(new App( + title: 'Cards', + theme: new ThemeData( + brightness: ThemeBrightness.light, + primarySwatch: Colors.blue, + accentColor: Colors.redAccent[200] + ), + routes: { + '/': (RouteArguments args) => new CardCollection(navigator: args.navigator), + } + )); } diff --git a/examples/widgets/drag_and_drop.dart b/examples/widgets/drag_and_drop.dart index 70b68ef38d..f35934c3af 100644 --- a/examples/widgets/drag_and_drop.dart +++ b/examples/widgets/drag_and_drop.dart @@ -50,7 +50,7 @@ class ExampleDragTargetState extends State { } class Dot extends StatelessComponent { - Dot({ Key key, this.color, this.size }): super(key: key); + Dot({ Key key, this.color, this.size }) : super(key: key); final Color color; final double size; Widget build(BuildContext context) { @@ -66,7 +66,7 @@ class Dot extends StatelessComponent { } class ExampleDragSource extends StatelessComponent { - ExampleDragSource({ Key key, this.navigator, this.name, this.color }): super(key: key); + ExampleDragSource({ Key key, this.navigator, this.name, this.color }) : super(key: key); final NavigatorState navigator; final String name; final Color color; @@ -133,7 +133,7 @@ void main() { runApp(new App( title: 'Drag and Drop Flutter Demo', routes: { - '/': (NavigatorState navigator, Route route) => new DragAndDropApp(navigator: navigator) + '/': (RouteArguments args) => new DragAndDropApp(navigator: args.navigator) } )); } diff --git a/examples/widgets/navigation.dart b/examples/widgets/navigation.dart index c9dd69b161..7ade991408 100644 --- a/examples/widgets/navigation.dart +++ b/examples/widgets/navigation.dart @@ -6,46 +6,46 @@ import 'package:sky/material.dart'; import 'package:sky/widgets.dart'; final Map routes = { - '/': (NavigatorState navigator, Route route) => new Container( + '/': (RouteArguments args) => new Container( padding: const EdgeDims.all(30.0), decoration: new BoxDecoration(backgroundColor: const Color(0xFFCCCCCC)), child: new Column([ new Text("You are at home"), new RaisedButton( child: new Text('GO SHOPPING'), - onPressed: () => navigator.pushNamed('/shopping') + onPressed: () => args.navigator.pushNamed('/shopping') ), new RaisedButton( child: new Text('START ADVENTURE'), - onPressed: () => navigator.pushNamed('/adventure') + onPressed: () => args.navigator.pushNamed('/adventure') )], justifyContent: FlexJustifyContent.center ) ), - '/shopping': (NavigatorState navigator, Route route) => new Container( + '/shopping': (RouteArguments args) => new Container( padding: const EdgeDims.all(20.0), decoration: new BoxDecoration(backgroundColor: const Color(0xFFBF5FFF)), child: new Column([ new Text("Village Shop"), new RaisedButton( child: new Text('RETURN HOME'), - onPressed: () => navigator.pop() + onPressed: () => args.navigator.pop() ), new RaisedButton( child: new Text('GO TO DUNGEON'), - onPressed: () => navigator.pushNamed('/adventure') + onPressed: () => args.navigator.pushNamed('/adventure') )], justifyContent: FlexJustifyContent.center ) ), - '/adventure': (NavigatorState navigator, Route route) => new Container( + '/adventure': (RouteArguments args) => new Container( padding: const EdgeDims.all(20.0), decoration: new BoxDecoration(backgroundColor: const Color(0xFFDC143C)), child: new Column([ new Text("Monster's Lair"), new RaisedButton( child: new Text('RUN!!!'), - onPressed: () => navigator.pop() + onPressed: () => args.navigator.pop() )], justifyContent: FlexJustifyContent.center ) diff --git a/examples/widgets/overlay_geometry.dart b/examples/widgets/overlay_geometry.dart index 8a525204d4..6aa66e91ed 100644 --- a/examples/widgets/overlay_geometry.dart +++ b/examples/widgets/overlay_geometry.dart @@ -165,7 +165,7 @@ void main() { ), title: 'Cards', routes: { - '/': (navigator, route) => new OverlayGeometryApp() + '/': (RouteArguments args) => new OverlayGeometryApp() } )); } diff --git a/examples/widgets/pageable_list.dart b/examples/widgets/pageable_list.dart index e9c079cffe..e68ebbc87a 100644 --- a/examples/widgets/pageable_list.dart +++ b/examples/widgets/pageable_list.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:sky/animation.dart'; import 'package:sky/material.dart'; import 'package:sky/painting.dart'; import 'package:sky/widgets.dart'; @@ -17,6 +16,10 @@ class CardModel { } class PageableListApp extends StatefulComponent { + PageableListApp({ this.navigator }); + + final NavigatorState navigator; + PageableListAppState createState() => new PageableListAppState(); } @@ -85,31 +88,10 @@ class PageableListAppState extends State { }); } - bool _drawerShowing = false; - AnimationStatus _drawerStatus = AnimationStatus.dismissed; - - void _handleOpenDrawer() { - setState(() { - _drawerShowing = true; - _drawerStatus = AnimationStatus.forward; - }); - } - - void _handleDrawerDismissed() { - setState(() { - _drawerStatus = AnimationStatus.dismissed; - }); - } - - Drawer buildDrawer() { - if (_drawerStatus == AnimationStatus.dismissed) - return null; - - return new Drawer( - level: 3, - showing: _drawerShowing, - onDismissed: _handleDrawerDismissed, - children: [ + void _showDrawer() { + showDrawer( + navigator: config.navigator, + child: new Block([ new DrawerHeader(child: new Text('Options')), new DrawerItem( icon: 'navigation/more_horiz', @@ -130,14 +112,13 @@ class PageableListAppState extends State { new Checkbox(value: itemsWrap) ]) ) - ] + ]) ); - } Widget buildToolBar() { return new ToolBar( - left: new IconButton(icon: "navigation/menu", onPressed: _handleOpenDrawer), + left: new IconButton(icon: "navigation/menu", onPressed: _showDrawer), center: new Text('PageableList'), right: [ new Text(scrollDirection == ScrollDirection.horizontal ? "horizontal" : "vertical") @@ -167,25 +148,24 @@ class PageableListAppState extends State { Widget build(BuildContext context) { return new IconTheme( data: const IconThemeData(color: IconThemeColor.white), - child: new Theme( - data: new ThemeData( - brightness: ThemeBrightness.light, - primarySwatch: Colors.blue, - accentColor: Colors.redAccent[200] - ), - child: new Title( - title: 'PageableList', - child: new Scaffold( - drawer: buildDrawer(), - toolbar: buildToolBar(), - body: buildBody(context) - ) - ) + child: new Scaffold( + toolbar: buildToolBar(), + body: buildBody(context) ) ); } } void main() { - runApp(new PageableListApp()); + runApp(new App( + title: 'PageableList', + theme: new ThemeData( + brightness: ThemeBrightness.light, + primarySwatch: Colors.blue, + accentColor: Colors.redAccent[200] + ), + routes: { + '/': (RouteArguments args) => new PageableListApp(navigator: args.navigator), + } + )); } diff --git a/examples/widgets/progress_indicator.dart b/examples/widgets/progress_indicator.dart index 1024d37138..b4b2b410a1 100644 --- a/examples/widgets/progress_indicator.dart +++ b/examples/widgets/progress_indicator.dart @@ -13,24 +13,23 @@ class ProgressIndicatorApp extends StatefulComponent { class ProgressIndicatorAppState extends State { void initState() { super.initState(); - valueAnimation = new ValueAnimation() + valueAnimation = new ValuePerformance() ..duration = const Duration(milliseconds: 1500) ..variable = new AnimatedValue( 0.0, end: 1.0, - curve: ease, - reverseCurve: ease, - interval: new Interval(0.0, 0.9) + curve: new Interval(0.0, 0.9, curve: ease), + reverseCurve: ease ); - valueAnimation.addStatusListener((AnimationStatus status) { - if (status == AnimationStatus.dismissed || status == AnimationStatus.completed) + valueAnimation.addStatusListener((PerformanceStatus status) { + if (status == PerformanceStatus.dismissed || status == PerformanceStatus.completed) reverseValueAnimationDirection(); }); valueAnimation.play(valueAnimationDirection); } - ValueAnimation valueAnimation; - Direction valueAnimationDirection = Direction.forward; + ValuePerformance valueAnimation; + AnimationDirection valueAnimationDirection = AnimationDirection.forward; void handleTap() { setState(() { @@ -43,9 +42,9 @@ class ProgressIndicatorAppState extends State { } void reverseValueAnimationDirection() { - valueAnimationDirection = (valueAnimationDirection == Direction.forward) - ? Direction.reverse - : Direction.forward; + valueAnimationDirection = (valueAnimationDirection == AnimationDirection.forward) + ? AnimationDirection.reverse + : AnimationDirection.forward; valueAnimation.play(valueAnimationDirection); } diff --git a/examples/widgets/spinning_mixed.dart b/examples/widgets/spinning_mixed.dart index 9317390a99..f44b0e0acd 100644 --- a/examples/widgets/spinning_mixed.dart +++ b/examples/widgets/spinning_mixed.dart @@ -54,13 +54,13 @@ Widget builder() { ); } -double timeBase; +Duration timeBase; RenderTransform transformBox; -void rotate(double timeStamp) { +void rotate(Duration timeStamp) { if (timeBase == null) timeBase = timeStamp; - double delta = (timeStamp - timeBase) / 1000; // radians + double delta = (timeStamp - timeBase).inMicroseconds.toDouble() / Duration.MICROSECONDS_PER_SECOND; // radians transformBox.setIdentity(); transformBox.translate(transformBox.size.width / 2.0, transformBox.size.height / 2.0); diff --git a/packages/flutter/lib/animation.dart b/packages/flutter/lib/animation.dart index f35b2538bb..e9c75373b7 100644 --- a/packages/flutter/lib/animation.dart +++ b/packages/flutter/lib/animation.dart @@ -7,12 +7,12 @@ /// This library depends only on core Dart libraries and the `newton` package. library animation; -export 'src/animation/animated_simulation.dart'; export 'src/animation/animated_value.dart'; -export 'src/animation/animation_performance.dart'; +export 'src/animation/performance.dart'; export 'src/animation/clamped_simulation.dart'; export 'src/animation/curves.dart'; export 'src/animation/forces.dart'; export 'src/animation/scheduler.dart'; export 'src/animation/scroll_behavior.dart'; -export 'src/animation/timeline.dart'; +export 'src/animation/simulation_stepper.dart'; +export 'src/animation/ticker.dart'; diff --git a/packages/flutter/lib/src/animation/animated_value.dart b/packages/flutter/lib/src/animation/animated_value.dart index 68da613971..9b4a8daa1b 100644 --- a/packages/flutter/lib/src/animation/animated_value.dart +++ b/packages/flutter/lib/src/animation/animated_value.dart @@ -2,12 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import "dart:sky"; +import 'dart:sky' show Color, Rect; import 'package:sky/src/animation/curves.dart'; /// The direction in which an animation is running -enum Direction { +enum AnimationDirection { /// The animation is running from beginning to end forward, @@ -16,72 +16,52 @@ enum Direction { } /// An interface describing a variable that changes as an animation progresses. -/// -/// AnimatedVariables, by convention, must be cheap to create. This allows them to be used in -/// build functions in Widgets. -abstract class AnimatedVariable { +/// +/// Animatable objects, by convention, must be cheap to create. This allows them +/// to be used in build functions in Widgets. +abstract class Animatable { /// Update the variable to a given time in an animation that is running in the given direction - void setProgress(double t, Direction direction); + void setProgress(double t, AnimationDirection direction); String toString(); } -/// Used by [AnimationPerformance] to convert the timing of a performance to a different timescale. +/// Used by [Performance] to convert the timing of a performance to a different timescale. /// For example, by setting different values for the interval and reverseInterval, a performance /// can be made to take longer in one direction that the other. class AnimationTiming { - AnimationTiming({ - this.interval: const Interval(0.0, 1.0), - this.reverseInterval, - this.curve: linear, - this.reverseCurve - }); + AnimationTiming({ this.curve, this.reverseCurve }); - /// The interval during which this timing is active in the forward direction - Interval interval; - - /// The interval during which this timing is active in the reverse direction - /// - /// If this field is null, the timing defaults to using [interval] in both directions. - Interval reverseInterval; - - /// The curve that this timing applies to the animation clock in the forward direction + /// The curve to use in the forward direction Curve curve; - /// The curve that this timing applies to the animation clock in the reverse direction + /// The curve to use in the reverse direction /// - /// If this field is null, the timing defaults to using [curve] in both directions. + /// If this field is null, use [curve] in both directions. Curve reverseCurve; /// Applies this timing to the given animation clock value in the given direction - double transform(double t, Direction direction) { - Interval interval = _getInterval(direction); - if (interval != null) - t = interval.transform(t); - if (t == 1.0) // Or should we support inverse curves? + double transform(double t, AnimationDirection direction) { + Curve activeCurve = _getActiveCurve(direction); + if (activeCurve == null) return t; - Curve curve = _getCurve(direction); - if (curve != null) - t = curve.transform(t); - return t; + if (t == 0.0 || t == 1.0) { + assert(activeCurve.transform(t).round() == t); + return t; + } + return activeCurve.transform(t); } - Interval _getInterval(Direction direction) { - if (direction == Direction.forward || reverseInterval == null) - return interval; - return reverseInterval; - } - - Curve _getCurve(Direction direction) { - if (direction == Direction.forward || reverseCurve == null) + Curve _getActiveCurve(AnimationDirection direction) { + if (direction == AnimationDirection.forward || reverseCurve == null) return curve; return reverseCurve; } } /// An animated variable with a concrete type -class AnimatedValue extends AnimationTiming implements AnimatedVariable { - AnimatedValue(this.begin, { this.end, Interval interval, Interval reverseInterval, Curve curve, Curve reverseCurve }) - : super(interval: interval, reverseInterval: reverseInterval, curve: curve, reverseCurve: reverseCurve) { +class AnimatedValue extends AnimationTiming implements Animatable { + AnimatedValue(this.begin, { this.end, Curve curve, Curve reverseCurve }) + : super(curve: curve, reverseCurve: reverseCurve) { value = begin; } @@ -98,41 +78,28 @@ class AnimatedValue extends AnimationTiming implements Animat T lerp(double t) => begin + (end - begin) * t; /// Updates the value of this variable according to the given animation clock value and direction - void setProgress(double t, Direction direction) { + void setProgress(double t, AnimationDirection direction) { if (end != null) { t = transform(t, direction); - value = (t == 1.0) ? end : lerp(t); + if (t == 0.0) + value = begin; + else if (t == 1.0) + value = end; + else + value = lerp(t); } } String toString() => 'AnimatedValue(begin=$begin, end=$end, value=$value)'; } -/// A list of animated variables -class AnimatedList extends AnimationTiming implements AnimatedVariable { - /// The list of variables contained in the list - List variables; - - AnimatedList(this.variables, { Interval interval, Interval reverseInterval, Curve curve, Curve reverseCurve }) - : super(interval: interval, reverseInterval: reverseInterval, curve: curve, reverseCurve: reverseCurve); - - // Updates the value of all the variables in the list according to the given animation clock value and direction - void setProgress(double t, Direction direction) { - double adjustedTime = transform(t, direction); - for (AnimatedVariable variable in variables) - variable.setProgress(adjustedTime, direction); - } - - String toString() => 'AnimatedList([$variables])'; -} - /// An animated variable containing a color /// /// This class specializes the interpolation of AnimatedValue to be /// appropriate for colors. class AnimatedColorValue extends AnimatedValue { - AnimatedColorValue(Color begin, { Color end, Interval interval, Interval reverseInterval, Curve curve, Curve reverseCurve }) - : super(begin, end: end, interval: interval, reverseInterval: reverseInterval, curve: curve, reverseCurve: reverseCurve); + AnimatedColorValue(Color begin, { Color end, Curve curve, Curve reverseCurve }) + : super(begin, end: end, curve: curve, reverseCurve: reverseCurve); Color lerp(double t) => Color.lerp(begin, end, t); } @@ -141,9 +108,9 @@ class AnimatedColorValue extends AnimatedValue { /// /// This class specializes the interpolation of AnimatedValue to be /// appropriate for rectangles. -class AnimatedRect extends AnimatedValue { - AnimatedRect(Rect begin, { Rect end, Interval interval, Interval reverseInterval, Curve curve, Curve reverseCurve }) - : super(begin, end: end, interval: interval, reverseInterval: reverseInterval, curve: curve, reverseCurve: reverseCurve); +class AnimatedRectValue extends AnimatedValue { + AnimatedRectValue(Rect begin, { Rect end, Curve curve, Curve reverseCurve }) + : super(begin, end: end, curve: curve, reverseCurve: reverseCurve); Rect lerp(double t) => Rect.lerp(begin, end, t); } diff --git a/packages/flutter/lib/src/animation/curves.dart b/packages/flutter/lib/src/animation/curves.dart index 31fb4a911b..5b44cc43eb 100644 --- a/packages/flutter/lib/src/animation/curves.dart +++ b/packages/flutter/lib/src/animation/curves.dart @@ -27,9 +27,9 @@ class Linear implements Curve { double transform(double t) => t; } -/// A curve that is 0.0 until start, then linear from 0.0 to 1.0 at end, then 1.0 +/// A curve that is 0.0 until start, then curved from 0.0 to 1.0 at end, then 1.0 class Interval implements Curve { - const Interval(this.start, this.end); + const Interval(this.start, this.end, { this.curve: linear }); /// The smallest value for which this interval is 0.0 final double start; @@ -37,12 +37,18 @@ class Interval implements Curve { /// The smallest value for which this interval is 1.0 final double end; + /// The curve to apply between [start] and [end] + final Curve curve; + double transform(double t) { assert(start >= 0.0); assert(start <= 1.0); assert(end >= 0.0); assert(end <= 1.0); - return ((t - start) / (end - start)).clamp(0.0, 1.0); + t = ((t - start) / (end - start)).clamp(0.0, 1.0); + if (t == 0.0 || t == 1.0) + return t; + return curve.transform(t); } } diff --git a/packages/flutter/lib/src/animation/animation_performance.dart b/packages/flutter/lib/src/animation/performance.dart similarity index 61% rename from packages/flutter/lib/src/animation/animation_performance.dart rename to packages/flutter/lib/src/animation/performance.dart index 9105498dc4..92c77f8b59 100644 --- a/packages/flutter/lib/src/animation/animation_performance.dart +++ b/packages/flutter/lib/src/animation/performance.dart @@ -6,10 +6,10 @@ import 'dart:async'; import 'package:sky/src/animation/animated_value.dart'; import 'package:sky/src/animation/forces.dart'; -import 'package:sky/src/animation/timeline.dart'; +import 'package:sky/src/animation/simulation_stepper.dart'; /// The status of an animation -enum AnimationStatus { +enum PerformanceStatus { /// The animation is stopped at the beginning dismissed, @@ -23,27 +23,27 @@ enum AnimationStatus { completed, } -typedef void AnimationPerformanceListener(); -typedef void AnimationPerformanceStatusListener(AnimationStatus status); +typedef void PerformanceListener(); +typedef void PerformanceStatusListener(PerformanceStatus status); -/// An interface that is implemented by [AnimationPerformance] that exposes a +/// An interface that is implemented by [Performance] that exposes a /// read-only view of the underlying performance. This is used by classes that /// want to watch a performance but should not be able to change the /// performance's state. -abstract class WatchableAnimationPerformance { +abstract class PerformanceView { /// Update the given variable according to the current progress of the performance - void updateVariable(AnimatedVariable variable); + void updateVariable(Animatable variable); /// Calls the listener every time the progress of the performance changes - void addListener(AnimationPerformanceListener listener); + void addListener(PerformanceListener listener); /// Stop calling the listener every time the progress of the performance changes - void removeListener(AnimationPerformanceListener listener); + void removeListener(PerformanceListener listener); /// Calls listener every time the status of the performance changes - void addStatusListener(AnimationPerformanceStatusListener listener); + void addStatusListener(PerformanceStatusListener listener); /// Stops calling the listener every time the status of the performance changes - void removeStatusListener(AnimationPerformanceStatusListener listener); + void removeStatusListener(PerformanceStatusListener listener); } -/// A timeline that can be reversed and used to update [AnimatedVariable]s. +/// A timeline that can be reversed and used to update [Animatable]s. /// /// For example, a performance may handle an animation of a menu opening by /// sliding and fading in (changing Y value and opacity) over .5 seconds. The @@ -51,41 +51,38 @@ abstract class WatchableAnimationPerformance { /// may also take direct control of the timeline by manipulating [progress], or /// [fling] the timeline causing a physics-based simulation to take over the /// progression. -class AnimationPerformance implements WatchableAnimationPerformance { - AnimationPerformance({ this.duration, double progress }) { - _timeline = new Timeline(_tick); +class Performance implements PerformanceView { + Performance({ this.duration, double progress }) { + _timeline = new SimulationStepper(_tick); if (progress != null) _timeline.value = progress.clamp(0.0, 1.0); } - /// Returns a [WatchableAnimationPerformance] for this performance, + /// Returns a [PerformanceView] for this performance, /// so that a pointer to this object can be passed around without /// allowing users of that pointer to mutate the AnimationPerformance state. - WatchableAnimationPerformance get view => this; + PerformanceView get view => this; /// The length of time this performance should last Duration duration; - Timeline _timeline; - Direction _direction; + SimulationStepper _timeline; + AnimationDirection _direction; /// The direction used to select the current curve /// /// Curve direction is only reset when we hit the beginning or the end of the /// timeline to avoid discontinuities in the value of any variables this /// performance is used to animate. - Direction _curveDirection; + AnimationDirection _curveDirection; /// If non-null, animate with this timing instead of a linear timing AnimationTiming timing; - /// If non-null, animate with this force instead of a zero-to-one timeline. - Force attachedForce; - /// The progress of this performance along the timeline /// /// Note: Setting this value stops the current animation. - double get progress => _timeline.value; + double get progress => _timeline.value.clamp(0.0, 1.0); void set progress(double t) { // TODO(mpcomplete): should this affect |direction|? stop(); @@ -98,51 +95,45 @@ class AnimationPerformance implements WatchableAnimationPerformance { } /// Whether this animation is stopped at the beginning - bool get isDismissed => status == AnimationStatus.dismissed; + bool get isDismissed => status == PerformanceStatus.dismissed; /// Whether this animation is stopped at the end - bool get isCompleted => status == AnimationStatus.completed; + bool get isCompleted => status == PerformanceStatus.completed; /// Whether this animation is currently animating in either the forward or reverse direction bool get isAnimating => _timeline.isAnimating; /// The current status of this animation - AnimationStatus get status { + PerformanceStatus get status { if (!isAnimating && progress == 1.0) - return AnimationStatus.completed; + return PerformanceStatus.completed; if (!isAnimating && progress == 0.0) - return AnimationStatus.dismissed; - return _direction == Direction.forward ? - AnimationStatus.forward : - AnimationStatus.reverse; + return PerformanceStatus.dismissed; + return _direction == AnimationDirection.forward ? + PerformanceStatus.forward : + PerformanceStatus.reverse; } /// Update the given varaible according to the current progress of this performance - void updateVariable(AnimatedVariable variable) { + void updateVariable(Animatable variable) { variable.setProgress(_curvedProgress, _curveDirection); } /// Start running this animation forwards (towards the end) - Future forward() => play(Direction.forward); + Future forward() => play(AnimationDirection.forward); /// Start running this animation in reverse (towards the beginning) - Future reverse() => play(Direction.reverse); + Future reverse() => play(AnimationDirection.reverse); /// Start running this animation in the given direction - Future play([Direction direction = Direction.forward]) { + Future play([AnimationDirection direction = AnimationDirection.forward]) { _direction = direction; return resume(); } - /// Start running this animation in the most recently direction + /// Start running this animation in the most recent direction Future resume() { - if (attachedForce != null) { - return fling( - velocity: _direction == Direction.forward ? 1.0 : -1.0, - force: attachedForce - ); - } - return _animateTo(_direction == Direction.forward ? 1.0 : 0.0); + return _animateTo(_direction == AnimationDirection.forward ? 1.0 : 0.0); } /// Stop running this animation @@ -158,46 +149,46 @@ class AnimationPerformance implements WatchableAnimationPerformance { Future fling({double velocity: 1.0, Force force}) { if (force == null) force = kDefaultSpringForce; - _direction = velocity < 0.0 ? Direction.reverse : Direction.forward; - return _timeline.fling(force.release(progress, velocity)); + _direction = velocity < 0.0 ? AnimationDirection.reverse : AnimationDirection.forward; + return _timeline.animateWith(force.release(progress, velocity)); } - final List _listeners = new List(); + final List _listeners = new List(); /// Calls the listener every time the progress of this performance changes - void addListener(AnimationPerformanceListener listener) { + void addListener(PerformanceListener listener) { _listeners.add(listener); } /// Stop calling the listener every time the progress of this performance changes - void removeListener(AnimationPerformanceListener listener) { + void removeListener(PerformanceListener listener) { _listeners.remove(listener); } void _notifyListeners() { - List localListeners = new List.from(_listeners); - for (AnimationPerformanceListener listener in localListeners) + List localListeners = new List.from(_listeners); + for (PerformanceListener listener in localListeners) listener(); } - final List _statusListeners = new List(); + final List _statusListeners = new List(); /// Calls listener every time the status of this performance changes - void addStatusListener(AnimationPerformanceStatusListener listener) { + void addStatusListener(PerformanceStatusListener listener) { _statusListeners.add(listener); } /// Stops calling the listener every time the status of this performance changes - void removeStatusListener(AnimationPerformanceStatusListener listener) { + void removeStatusListener(PerformanceStatusListener listener) { _statusListeners.remove(listener); } - AnimationStatus _lastStatus = AnimationStatus.dismissed; + PerformanceStatus _lastStatus = PerformanceStatus.dismissed; void _checkStatusChanged() { - AnimationStatus currentStatus = status; + PerformanceStatus currentStatus = status; if (currentStatus != _lastStatus) { - List localListeners = new List.from(_statusListeners); - for (AnimationPerformanceStatusListener listener in localListeners) + List localListeners = new List.from(_statusListeners); + for (PerformanceStatusListener listener in localListeners) listener(currentStatus); } _lastStatus = currentStatus; @@ -205,7 +196,7 @@ class AnimationPerformance implements WatchableAnimationPerformance { void _updateCurveDirection() { if (status != _lastStatus) { - if (_lastStatus == AnimationStatus.dismissed || _lastStatus == AnimationStatus.completed) + if (_lastStatus == PerformanceStatus.dismissed || _lastStatus == PerformanceStatus.completed) _curveDirection = _direction; } } @@ -230,8 +221,8 @@ class AnimationPerformance implements WatchableAnimationPerformance { } /// An animation performance with an animated variable with a concrete type -class ValueAnimation extends AnimationPerformance { - ValueAnimation({ this.variable, Duration duration, double progress }) : +class ValuePerformance extends Performance { + ValuePerformance({ this.variable, Duration duration, double progress }) : super(duration: duration, progress: progress); AnimatedValue variable; diff --git a/packages/flutter/lib/src/animation/scheduler.dart b/packages/flutter/lib/src/animation/scheduler.dart index 999ba89008..bfa585e0f3 100644 --- a/packages/flutter/lib/src/animation/scheduler.dart +++ b/packages/flutter/lib/src/animation/scheduler.dart @@ -14,7 +14,7 @@ double timeDilation = 1.0; /// scheduler's epoch. Use timeStamp to determine how far to advance animation /// timelines so that all the animations in the system are synchronized to a /// common time base. -typedef void SchedulerCallback(double timeStamp); +typedef void SchedulerCallback(Duration timeStamp); /// Schedules callbacks to run in concert with the engine's animation system class Scheduler { @@ -35,8 +35,10 @@ class Scheduler { /// This function first calls all the callbacks registered by /// [requestAnimationFrame] and then calls all the callbacks registered by /// [addPersistentFrameCallback], which typically drive the rendering pipeline. - void beginFrame(double timeStamp) { - timeStamp /= timeDilation; + void beginFrame(double timeStampMS) { + timeStampMS /= timeDilation; + + Duration timeStamp = new Duration(microseconds: (timeStampMS * Duration.MICROSECONDS_PER_MILLISECOND).round()); _haveScheduledVisualUpdate = false; diff --git a/packages/flutter/lib/src/animation/simulation_stepper.dart b/packages/flutter/lib/src/animation/simulation_stepper.dart new file mode 100644 index 0000000000..dee9cb0c73 --- /dev/null +++ b/packages/flutter/lib/src/animation/simulation_stepper.dart @@ -0,0 +1,105 @@ +// 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 'dart:async'; + +import 'package:newton/newton.dart'; +import 'package:sky/src/animation/animated_value.dart'; +import 'package:sky/src/animation/curves.dart'; +import 'package:sky/src/animation/ticker.dart'; + +/// A simulation that varies from [begin] to [end] over [duration] using [curve] +/// +/// This class is an adaptor between the Simulation interface and the +/// AnimatedValue interface. +class _TweenSimulation extends Simulation { + _TweenSimulation(double begin, double end, Duration duration, Curve curve) + : _durationInSeconds = duration.inMicroseconds.toDouble() / Duration.MICROSECONDS_PER_SECOND, + _tween = new AnimatedValue(begin, end: end, curve: curve) { + assert(_durationInSeconds > 0.0); + assert(begin != null); + assert(end != null); + } + + final double _durationInSeconds; + final AnimatedValue _tween; + + double x(double timeInSeconds) { + assert(timeInSeconds >= 0.0); + final double t = (timeInSeconds / _durationInSeconds).clamp(0.0, 1.0); + _tween.setProgress(t, AnimationDirection.forward); + return _tween.value; + } + + double dx(double timeInSeconds) => 1.0; + + bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds; +} + +typedef TimelineCallback(double value); + +/// Steps a simulation one per frame +class SimulationStepper { + SimulationStepper(TimelineCallback onTick) : _onTick = onTick { + _ticker = new Ticker(_tick); + } + + final TimelineCallback _onTick; + Ticker _ticker; + Simulation _simulation; + + /// The current value of the timeline + double get value => _value; + double _value = 0.0; + void set value(double newValue) { + assert(newValue != null); + assert(!isAnimating); + _value = newValue; + _onTick(_value); + } + + /// Whether the timeline is currently animating + bool get isAnimating => _ticker.isTicking; + + /// Animate value of the timeline to the given target over the given duration + /// + /// Returns a future that resolves when the timeline stops animating, + /// typically when the timeline arives at the target value. + Future animateTo(double target, { Duration duration, Curve curve: linear }) { + assert(duration > Duration.ZERO); + assert(!isAnimating); + return _start(new _TweenSimulation(value, target, duration, curve)); + } + + /// Gives the given simulation control over the timeline + Future animateWith(Simulation simulation) { + stop(); + return _start(simulation); + } + + /// Start ticking the given simulation once per frame + /// + /// Returns a future that resolves when the simulation stops ticking. + Future _start(Simulation simulation) { + assert(simulation != null); + assert(!isAnimating); + _simulation = simulation; + _value = simulation.x(0.0); + return _ticker.start(); + } + + /// Stop animating the timeline + void stop() { + _simulation = null; + _ticker.stop(); + } + + void _tick(Duration elapsed) { + double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.MICROSECONDS_PER_SECOND; + _value = _simulation.x(elapsedInSeconds); + if (_simulation.isDone(elapsedInSeconds)) + stop(); + _onTick(_value); + } +} diff --git a/packages/flutter/lib/src/animation/animated_simulation.dart b/packages/flutter/lib/src/animation/ticker.dart similarity index 50% rename from packages/flutter/lib/src/animation/animated_simulation.dart rename to packages/flutter/lib/src/animation/ticker.dart index 0710dfc6d2..6c0dd32eb0 100644 --- a/packages/flutter/lib/src/animation/animated_simulation.dart +++ b/packages/flutter/lib/src/animation/ticker.dart @@ -4,29 +4,27 @@ import 'dart:async'; -import 'package:newton/newton.dart'; import 'package:sky/src/animation/scheduler.dart'; -const double _kSecondsPerMillisecond = 1000.0; - -// TODO(abarth): Change from double to Duration. -typedef _TickerCallback(double timeStamp); +typedef TickerCallback(Duration elapsed); /// Calls its callback once per animation frame class Ticker { /// Constructs a ticker that will call onTick once per frame while running - Ticker(_TickerCallback onTick) : _onTick = onTick; + Ticker(TickerCallback onTick) : _onTick = onTick; - final _TickerCallback _onTick; + final TickerCallback _onTick; Completer _completer; int _animationId; + Duration _startTime; /// Start calling onTick once per animation frame /// /// The returned future resolves once the ticker stops ticking. Future start() { assert(!isTicking); + assert(_startTime == null); _completer = new Completer(); _scheduleTick(); return _completer.future; @@ -39,12 +37,14 @@ class Ticker { if (!isTicking) return; + _startTime = null; + if (_animationId != null) { scheduler.cancelAnimationFrame(_animationId); _animationId = null; } - // We take the _completer into a local variable so that !isTicking + // 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). Completer localCompleter = _completer; @@ -56,12 +56,15 @@ class Ticker { /// Whether this ticker has scheduled a call to onTick bool get isTicking => _completer != null; - void _tick(double timeStamp) { + void _tick(Duration timeStamp) { assert(isTicking); assert(_animationId != null); _animationId = null; - _onTick(timeStamp); + if (_startTime == null) + _startTime = timeStamp; + + _onTick(timeStamp - _startTime); // The onTick callback may have scheduled another tick already. if (isTicking && _animationId == null) @@ -74,63 +77,3 @@ class Ticker { _animationId = scheduler.requestAnimationFrame(_tick); } } - -/// Ticks a simulation once per frame -class AnimatedSimulation { - - AnimatedSimulation(Function onTick) : _onTick = onTick { - _ticker = new Ticker(_tick); - } - - final Function _onTick; - Ticker _ticker; - - Simulation _simulation; - double _startTime; - - double _value = 0.0; - /// The current value of the simulation - double get value => _value; - void set value(double newValue) { - assert(!_ticker.isTicking); - _value = newValue; - _onTick(_value); - } - - /// Start ticking the given simulation once per frame - /// - /// Returns a future that resolves when the simulation stops ticking. - Future start(Simulation simulation) { - assert(simulation != null); - assert(!_ticker.isTicking); - _simulation = simulation; - _startTime = null; - _value = simulation.x(0.0); - return _ticker.start(); - } - - /// Stop ticking the current simulation - void stop() { - _simulation = null; - _startTime = null; - _ticker.stop(); - } - - /// Whether this object is currently ticking a simulation - bool get isAnimating => _ticker.isTicking; - - void _tick(double timeStamp) { - if (_startTime == null) - _startTime = timeStamp; - - double timeInSeconds = (timeStamp - _startTime) / _kSecondsPerMillisecond; - _value = _simulation.x(timeInSeconds); - final bool isLastTick = _simulation.isDone(timeInSeconds); - - if (isLastTick) - stop(); - - _onTick(_value); - } - -} diff --git a/packages/flutter/lib/src/animation/timeline.dart b/packages/flutter/lib/src/animation/timeline.dart deleted file mode 100644 index 75da47d5f8..0000000000 --- a/packages/flutter/lib/src/animation/timeline.dart +++ /dev/null @@ -1,87 +0,0 @@ -// 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 'dart:async'; - -import 'package:newton/newton.dart'; - -import 'package:sky/src/animation/animated_simulation.dart'; - -/// A simulation that linearly varies from [begin] to [end] over [duration] -class TweenSimulation extends Simulation { - final double _durationInSeconds; - - /// The initial value of the simulation - final double begin; - - /// The terminal value of the simulation - final double end; - - TweenSimulation(Duration duration, this.begin, this.end) : - _durationInSeconds = duration.inMicroseconds / Duration.MICROSECONDS_PER_SECOND { - assert(_durationInSeconds > 0.0); - assert(begin != null && begin >= 0.0 && begin <= 1.0); - assert(end != null && end >= 0.0 && end <= 1.0); - } - - double x(double timeInSeconds) { - assert(timeInSeconds >= 0.0); - final double t = timeInSeconds / _durationInSeconds; - return t >= 1.0 ? end : begin + (end - begin) * t; - } - - double dx(double timeInSeconds) => 1.0; - - bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds; -} - -/// A timeline for an animation -class Timeline { - Timeline(Function onTick) { - _animation = new AnimatedSimulation(onTick); - } - - AnimatedSimulation _animation; - - /// The current value of the timeline - double get value => _animation.value.clamp(0.0, 1.0); - void set value(double newValue) { - assert(newValue != null && newValue >= 0.0 && newValue <= 1.0); - assert(!isAnimating); - _animation.value = newValue; - } - - /// Whether the timeline is currently animating - bool get isAnimating => _animation.isAnimating; - - Future _start({ - Duration duration, - double begin: 0.0, - double end: 1.0 - }) { - assert(!_animation.isAnimating); - assert(duration > Duration.ZERO); - return _animation.start(new TweenSimulation(duration, begin, end)); - } - - /// Animate value of the timeline to the given target over the given duration - /// - /// Returns a future that resolves when the timeline stops animating, - /// typically when the timeline arives at the target value. - Future animateTo(double target, { Duration duration }) { - assert(duration > Duration.ZERO); - return _start(duration: duration, begin: value, end: target); - } - - /// Stop animating the timeline - void stop() { - _animation.stop(); - } - - // Gives the given simulation control over the timeline - Future fling(Simulation simulation) { - stop(); - return _animation.start(simulation); - } -} diff --git a/packages/flutter/lib/src/gestures/long_press.dart b/packages/flutter/lib/src/gestures/long_press.dart index f7e280b7a7..ec1945dea6 100644 --- a/packages/flutter/lib/src/gestures/long_press.dart +++ b/packages/flutter/lib/src/gestures/long_press.dart @@ -9,13 +9,13 @@ import 'package:sky/src/gestures/constants.dart'; import 'package:sky/src/gestures/pointer_router.dart'; import 'package:sky/src/gestures/recognizer.dart'; -typedef void GestureLongPressListener(); +typedef void GestureLongPressCallback(); class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer { LongPressGestureRecognizer({ PointerRouter router, this.onLongPress }) : super(router: router, deadline: kTapTimeout + kLongPressTimeout); - GestureLongPressListener onLongPress; + GestureLongPressCallback onLongPress; void didExceedDeadline() { resolve(GestureDisposition.accepted); diff --git a/packages/flutter/lib/src/gestures/recognizer.dart b/packages/flutter/lib/src/gestures/recognizer.dart index 488b8e9c1a..b92fac39c0 100644 --- a/packages/flutter/lib/src/gestures/recognizer.dart +++ b/packages/flutter/lib/src/gestures/recognizer.dart @@ -12,7 +12,9 @@ import 'package:sky/src/gestures/pointer_router.dart'; export 'package:sky/src/gestures/pointer_router.dart' show PointerRouter; abstract class GestureRecognizer extends GestureArenaMember { - GestureRecognizer({ PointerRouter router }) : _router = router; + GestureRecognizer({ PointerRouter router }) : _router = router { + assert(_router != null); + } PointerRouter _router; diff --git a/packages/flutter/lib/src/gestures/show_press.dart b/packages/flutter/lib/src/gestures/show_press.dart index 1f56cba4d0..299b6ae540 100644 --- a/packages/flutter/lib/src/gestures/show_press.dart +++ b/packages/flutter/lib/src/gestures/show_press.dart @@ -8,13 +8,13 @@ import 'package:sky/src/gestures/arena.dart'; import 'package:sky/src/gestures/constants.dart'; import 'package:sky/src/gestures/recognizer.dart'; -typedef void GestureShowPressListener(); +typedef void GestureShowPressCallback(); class ShowPressGestureRecognizer extends PrimaryPointerGestureRecognizer { ShowPressGestureRecognizer({ PointerRouter router, this.onShowPress }) : super(router: router, deadline: kTapTimeout); - GestureShowPressListener onShowPress; + GestureShowPressCallback onShowPress; void didExceedDeadline() { // Show press isn't an exclusive gesture. We can recognize a show press diff --git a/packages/flutter/lib/src/gestures/tap.dart b/packages/flutter/lib/src/gestures/tap.dart index 69cb7bbd4a..fdb1db6e88 100644 --- a/packages/flutter/lib/src/gestures/tap.dart +++ b/packages/flutter/lib/src/gestures/tap.dart @@ -8,13 +8,15 @@ import 'package:sky/src/gestures/arena.dart'; import 'package:sky/src/gestures/constants.dart'; import 'package:sky/src/gestures/recognizer.dart'; -typedef void GestureTapListener(); +typedef void GestureTapCallback(); class TapGestureRecognizer extends PrimaryPointerGestureRecognizer { TapGestureRecognizer({ PointerRouter router, this.onTap }) : super(router: router, deadline: kTapTimeout); - GestureTapListener onTap; + GestureTapCallback onTap; + GestureTapCallback onTapDown; + GestureTapCallback onTapCancel; void didExceedDeadline() { stopTrackingPointer(primaryPointer); @@ -22,9 +24,22 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer { } void handlePrimaryPointer(sky.PointerEvent event) { - if (event.type == 'pointerup') { + if (event.type == 'pointerdown') { + if (onTapDown != null) + onTapDown(); + } else if (event.type == 'pointerup') { resolve(GestureDisposition.accepted); - onTap(); + if (onTap != null) + onTap(); + } + } + + void rejectGesture(int pointer) { + super.rejectGesture(pointer); + if (pointer == primaryPointer) { + assert(state == GestureRecognizerState.defunct); + if (onTapCancel != null) + onTapCancel(); } } } diff --git a/packages/flutter/lib/src/painting/radial_reaction.dart b/packages/flutter/lib/src/painting/radial_reaction.dart index c7997e4e5e..c2822cb9da 100644 --- a/packages/flutter/lib/src/painting/radial_reaction.dart +++ b/packages/flutter/lib/src/painting/radial_reaction.dart @@ -30,13 +30,13 @@ class RadialReaction { _outerOpacity = new AnimatedValue(0.0, end: _kMaxOpacity, curve: easeOut); _innerCenter = new AnimatedValue(startPosition, end: center, curve: easeOut); _innerRadius = new AnimatedValue(0.0, end: radius, curve: easeOut); - _showPerformance = new AnimationPerformance(duration: _kShowDuration) + _showPerformance = new Performance(duration: _kShowDuration) ..addListener(() { _showPerformance.updateVariable(_outerOpacity); _showPerformance.updateVariable(_innerCenter); _showPerformance.updateVariable(_innerRadius); }); - _fade = new ValueAnimation( + _fade = new ValuePerformance( variable: new AnimatedValue(1.0, end: 0.0, curve: easeIn), duration: _kHideDuration ); @@ -48,14 +48,14 @@ class RadialReaction { /// The radius of the circle in which the reaction occurs final double radius; - AnimationPerformance _showPerformance; + Performance _showPerformance; AnimatedValue _outerOpacity; AnimatedValue _innerCenter; AnimatedValue _innerRadius; Future _showComplete; - ValueAnimation _fade; + ValuePerformance _fade; /// Show the reaction /// diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index 59bf21b5c8..bf9e31ad12 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -78,7 +78,7 @@ class FlutterBinding extends HitTestTarget { } /// Pump the rendering pipeline to generate a frame for the given time stamp - void beginFrame(double timeStamp) { + void beginFrame(Duration timeStamp) { RenderObject.flushLayout(); _renderView.updateCompositingBits(); RenderObject.flushPaint(); diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index 91ba89c548..4baf5c12e8 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.dart @@ -178,6 +178,60 @@ class BoxConstraints extends Constraints { (minHeight <= size.height) && (size.height <= math.max(minHeight, maxHeight)); } + BoxConstraints operator*(double other) { + return new BoxConstraints( + minWidth: minWidth * other, + maxWidth: maxWidth * other, + minHeight: minHeight * other, + maxHeight: maxHeight * other + ); + } + + BoxConstraints operator/(double other) { + return new BoxConstraints( + minWidth: minWidth / other, + maxWidth: maxWidth / other, + minHeight: minHeight / other, + maxHeight: maxHeight / other + ); + } + + BoxConstraints operator~/(double other) { + return new BoxConstraints( + minWidth: (minWidth ~/ other).toDouble(), + maxWidth: (maxWidth ~/ other).toDouble(), + minHeight: (minHeight ~/ other).toDouble(), + maxHeight: (maxHeight ~/ other).toDouble() + ); + } + + BoxConstraints operator%(double other) { + return new BoxConstraints( + minWidth: minWidth % other, + maxWidth: maxWidth % other, + minHeight: minHeight % other, + maxHeight: maxHeight % other + ); + } + + /// Linearly interpolate between two BoxConstraints + /// + /// If either is null, this function interpolates from [BoxConstraints.zero]. + static BoxConstraints lerp(BoxConstraints a, BoxConstraints b, double t) { + if (a == null && b == null) + return null; + if (a == null) + return b * t; + if (b == null) + return a * (1.0 - t); + return new BoxConstraints( + minWidth: sky.lerpDouble(a.minWidth, b.minWidth, t), + maxWidth: sky.lerpDouble(a.maxWidth, b.maxWidth, t), + minHeight: sky.lerpDouble(a.minHeight, b.minHeight, t), + maxHeight: sky.lerpDouble(a.maxHeight, b.maxHeight, t) + ); + } + bool operator ==(other) { if (identical(this, other)) return true; diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart index 49d1e3df58..1e22221d39 100644 --- a/packages/flutter/lib/src/rendering/layer.dart +++ b/packages/flutter/lib/src/rendering/layer.dart @@ -87,6 +87,29 @@ class PictureLayer extends Layer { } +/// A layer that indicates to the compositor that it should display +/// certain statistics within it +class StatisticsLayer extends Layer { + StatisticsLayer({ + Offset offset: Offset.zero, + this.paintBounds, + this.optionsMask + }) : super(offset: offset); + + /// The rectangle in this layer's coodinate system that bounds the recording + Rect paintBounds; + + /// A mask specifying the statistics to display + int optionsMask; + + void addToScene(sky.SceneBuilder builder, Offset layerOffset) { + assert(optionsMask != null); + builder.addStatistics(optionsMask, paintBounds.shift(layerOffset)); + } + +} + + /// A composited layer that has a list of children class ContainerLayer extends Layer { ContainerLayer({ Offset offset: Offset.zero }) : super(offset: offset); diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 07cee15b5f..4ebca858aa 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -16,6 +16,8 @@ import 'package:vector_math/vector_math_64.dart'; export 'dart:sky' show Point, Offset, Size, Rect, Color, Paint, Path; export 'package:sky/src/rendering/hit_test.dart' show HitTestTarget, HitTestEntry, HitTestResult; +typedef sky.Shader ShaderCallback(Rect bounds); + /// Base class for data associated with a [RenderObject] by its parent /// /// Some render objects wish to store data on their children, such as their @@ -130,6 +132,15 @@ class PaintingContext { } } + void paintStatistics(int optionsMask, Offset offset, Size size) { + StatisticsLayer statsLayer = new StatisticsLayer( + offset: offset, + paintBounds: new Rect.fromLTWH(0.0, 0.0, size.width, size.height), + optionsMask : optionsMask + ); + _containerLayer.append(statsLayer); + } + // Below we have various variants of the paintChild() method, which // do additional work, such as clipping or transforming, at the same // time as painting the children. @@ -300,6 +311,34 @@ class PaintingContext { } } + static Paint _getPaintForShaderMask(Rect bounds, + ShaderCallback shaderCallback, + sky.TransferMode transferMode) { + return new Paint() + ..transferMode = transferMode + ..shader = shaderCallback(bounds); + } + + void paintChildWithShaderMask(RenderObject child, + Point childPosition, + Rect bounds, + ShaderCallback shaderCallback, + sky.TransferMode transferMode) { + assert(debugCanPaintChild(child)); + final Offset childOffset = childPosition.toOffset(); + if (!child.needsCompositing) { + canvas.saveLayer(bounds, new Paint()); + canvas.translate(childOffset.dx, childOffset.dy); + insertChild(child, Offset.zero); + Paint shaderPaint = _getPaintForShaderMask(bounds, shaderCallback, transferMode); + canvas.drawRect(Offset.zero & new Size(bounds.width, bounds.height), shaderPaint); + canvas.restore(); + } else { + // TODO(hansmuller) support compositing ShaderMasks + assert('Support for compositing ShaderMasks is TBD' is String); + } + } + /// Instructs the child to draw itself onto this context at the given offset /// /// Do not call directly. This function is visible so that it can be diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 7bd0b9a425..1f9a7e3353 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -659,6 +659,37 @@ class RenderColorFilter extends RenderProxyBox { } } +class RenderShaderMask extends RenderProxyBox { + RenderShaderMask({ RenderBox child, ShaderCallback shaderCallback, sky.TransferMode transferMode }) + : _shaderCallback = shaderCallback, _transferMode = transferMode, super(child) { + } + + ShaderCallback get shaderCallback => _shaderCallback; + ShaderCallback _shaderCallback; + void set shaderCallback (ShaderCallback newShaderCallback) { + assert(newShaderCallback != null); + if (_shaderCallback == newShaderCallback) + return; + _shaderCallback = newShaderCallback; + markNeedsPaint(); + } + + sky.TransferMode get transferMode => _transferMode; + sky.TransferMode _transferMode; + void set transferMode (sky.TransferMode newTransferMode) { + assert(newTransferMode != null); + if (_transferMode == newTransferMode) + return; + _transferMode = newTransferMode; + markNeedsPaint(); + } + + void paint(PaintingContext context, Offset offset) { + if (child != null) + context.paintChildWithShaderMask(child, offset.toPoint(), offset & size, _shaderCallback, _transferMode); + } +} + /// Clips its child using a rectangle /// /// Prevents its child from painting outside its bounds. diff --git a/packages/flutter/lib/src/rendering/statistics_box.dart b/packages/flutter/lib/src/rendering/statistics_box.dart new file mode 100644 index 0000000000..98780ea889 --- /dev/null +++ b/packages/flutter/lib/src/rendering/statistics_box.dart @@ -0,0 +1,47 @@ +// 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:sky/src/rendering/box.dart'; +import 'package:sky/src/rendering/object.dart'; + +class StatisticsBox extends RenderBox { + + StatisticsBox({int optionsMask: 0}) : _optionsMask = optionsMask; + + int _optionsMask; + int get optionsMask => _optionsMask; + void set optionsMask (int mask) { + if (mask == _optionsMask) { + return; + } + _optionsMask = mask; + markNeedsPaint(); + } + + bool get sizedByParent => true; + + double getMinIntrinsicWidth(BoxConstraints constraints) { + return constraints.minWidth; + } + + double getMaxIntrinsicWidth(BoxConstraints constraints) { + return constraints.maxWidth; + } + + double getMinIntrinsicHeight(BoxConstraints constraints) { + return constraints.minHeight; + } + + double getMaxIntrinsicHeight(BoxConstraints constraints) { + return constraints.maxHeight; + } + + void performResize() { + size = constraints.constrain(Size.infinite); + } + + void paint(PaintingContext context, Offset offset) { + context.paintStatistics(optionsMask, offset, size); + } +} diff --git a/packages/flutter/lib/src/rendering/toggleable.dart b/packages/flutter/lib/src/rendering/toggleable.dart index ad9d7bc132..bd98d27cdb 100644 --- a/packages/flutter/lib/src/rendering/toggleable.dart +++ b/packages/flutter/lib/src/rendering/toggleable.dart @@ -24,15 +24,15 @@ abstract class RenderToggleable extends RenderConstrainedBox { : _value = value, _onChanged = onChanged, super(additionalConstraints: new BoxConstraints.tight(size)) { - _performance = new ValueAnimation( + _performance = new ValuePerformance( variable: new AnimatedValue(0.0, end: 1.0, curve: easeIn, reverseCurve: easeOut), duration: _kToggleDuration, progress: _value ? 1.0 : 0.0 )..addListener(markNeedsPaint); } - ValueAnimation get performance => _performance; - ValueAnimation _performance; + ValuePerformance get performance => _performance; + ValuePerformance _performance; double get position => _performance.value; @@ -51,7 +51,7 @@ abstract class RenderToggleable extends RenderConstrainedBox { ); } - void detatch() { + void detach() { _tap.dispose(); _tap = null; super.detach(); @@ -68,7 +68,7 @@ abstract class RenderToggleable extends RenderConstrainedBox { if (value == _value) return; _value = value; - performance.play(value ? Direction.forward : Direction.reverse); + performance.play(value ? AnimationDirection.forward : AnimationDirection.reverse); } ValueChanged get onChanged => _onChanged; diff --git a/packages/flutter/lib/src/widgets/animated_component.dart b/packages/flutter/lib/src/widgets/animated_component.dart index d9b27490c9..cf6acbc34f 100644 --- a/packages/flutter/lib/src/widgets/animated_component.dart +++ b/packages/flutter/lib/src/widgets/animated_component.dart @@ -9,13 +9,13 @@ abstract class AnimatedComponent extends StatefulComponent { const AnimatedComponent({ Key key, this.direction, this.duration }) : super(key: key); final Duration duration; - final Direction direction; + final AnimationDirection direction; } abstract class AnimatedState extends State { void initState() { super.initState(); - _performance = new AnimationPerformance(duration: config.duration); + _performance = new Performance(duration: config.duration); performance.addStatusListener(_handleAnimationStatusChanged); if (buildDependsOnPerformance) { performance.addListener(() { @@ -34,13 +34,13 @@ abstract class AnimatedState extends State { performance.play(config.direction); } - AnimationPerformance get performance => _performance; - AnimationPerformance _performance; + Performance get performance => _performance; + Performance _performance; - void _handleAnimationStatusChanged(AnimationStatus status) { - if (status == AnimationStatus.completed) + void _handleAnimationStatusChanged(PerformanceStatus status) { + if (status == PerformanceStatus.completed) handleCompleted(); - else if (status == AnimationStatus.dismissed) + else if (status == PerformanceStatus.dismissed) handleDismissed(); } diff --git a/packages/flutter/lib/src/widgets/animated_container.dart b/packages/flutter/lib/src/widgets/animated_container.dart new file mode 100644 index 0000000000..e9c56a115a --- /dev/null +++ b/packages/flutter/lib/src/widgets/animated_container.dart @@ -0,0 +1,226 @@ +// 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:sky/animation.dart'; +import 'package:sky/src/widgets/basic.dart'; +import 'package:sky/src/widgets/framework.dart'; + +import 'package:vector_math/vector_math_64.dart'; + +class AnimatedBoxConstraintsValue extends AnimatedValue { + AnimatedBoxConstraintsValue(BoxConstraints begin, { BoxConstraints end, Curve curve, Curve reverseCurve }) + : super(begin, end: end, curve: curve, reverseCurve: reverseCurve); + + BoxConstraints lerp(double t) => BoxConstraints.lerp(begin, end, t); +} + +class AnimatedBoxDecorationValue extends AnimatedValue { + AnimatedBoxDecorationValue(BoxDecoration begin, { BoxDecoration end, Curve curve, Curve reverseCurve }) + : super(begin, end: end, curve: curve, reverseCurve: reverseCurve); + + BoxDecoration lerp(double t) => BoxDecoration.lerp(begin, end, t); +} + +class AnimatedEdgeDimsValue extends AnimatedValue { + AnimatedEdgeDimsValue(EdgeDims begin, { EdgeDims end, Curve curve, Curve reverseCurve }) + : super(begin, end: end, curve: curve, reverseCurve: reverseCurve); + + EdgeDims lerp(double t) => EdgeDims.lerp(begin, end, t); +} + +class AnimatedMatrix4Value extends AnimatedValue { + AnimatedMatrix4Value(Matrix4 begin, { Matrix4 end, Curve curve, Curve reverseCurve }) + : super(begin, end: end, curve: curve, reverseCurve: reverseCurve); + + Matrix4 lerp(double t) { + // TODO(mpcomplete): Animate the full matrix. Will animating the cells + // separately work? + Vector3 beginT = begin.getTranslation(); + Vector3 endT = end.getTranslation(); + Vector3 lerpT = beginT*(1.0-t) + endT*t; + return new Matrix4.identity()..translate(lerpT); + } +} + +class AnimatedContainer extends StatefulComponent { + AnimatedContainer({ + Key key, + this.child, + this.constraints, + this.decoration, + this.foregroundDecoration, + this.margin, + this.padding, + this.transform, + this.width, + this.height, + this.curve: linear, + this.duration + }) : super(key: key) { + assert(margin == null || margin.isNonNegative); + assert(padding == null || padding.isNonNegative); + assert(curve != null); + assert(duration != null); + } + + final Widget child; + final BoxConstraints constraints; + final BoxDecoration decoration; + final BoxDecoration foregroundDecoration; + final EdgeDims margin; + final EdgeDims padding; + final Matrix4 transform; + final double width; + final double height; + + final Curve curve; + final Duration duration; + + AnimatedContainerState createState() => new AnimatedContainerState(); +} + +class AnimatedContainerState extends State { + AnimatedBoxConstraintsValue _constraints; + AnimatedBoxDecorationValue _decoration; + AnimatedBoxDecorationValue _foregroundDecoration; + AnimatedEdgeDimsValue _margin; + AnimatedEdgeDimsValue _padding; + AnimatedMatrix4Value _transform; + AnimatedValue _width; + AnimatedValue _height; + + Performance _performance; + + void initState() { + super.initState(); + _performance = new Performance(duration: config.duration) + ..timing = new AnimationTiming(curve: config.curve) + ..addListener(_updateAllVariables); + _configAllVariables(); + } + + void didUpdateConfig(AnimatedContainer oldConfig) { + _performance + ..duration = config.duration + ..timing.curve = config.curve; + if (_configAllVariables()) { + _performance.progress = 0.0; + _performance.play(); + } + } + + void dispose() { + _performance.stop(); + super.dispose(); + } + + void _updateVariable(Animatable variable) { + if (variable != null) + _performance.updateVariable(variable); + } + + void _updateAllVariables() { + setState(() { + _updateVariable(_constraints); + _updateVariable(_constraints); + _updateVariable(_decoration); + _updateVariable(_foregroundDecoration); + _updateVariable(_margin); + _updateVariable(_padding); + _updateVariable(_transform); + _updateVariable(_width); + _updateVariable(_height); + }); + } + + bool _configVariable(AnimatedValue variable, dynamic targetValue) { + dynamic currentValue = variable.value; + variable.end = targetValue; + variable.begin = currentValue; + return currentValue != targetValue; + } + + bool _configAllVariables() { + bool needsAnimation = false; + if (config.constraints != null) { + _constraints ??= new AnimatedBoxConstraintsValue(config.constraints); + if (_configVariable(_constraints, config.constraints)) + needsAnimation = true; + } else { + _constraints = null; + } + + if (config.decoration != null) { + _decoration ??= new AnimatedBoxDecorationValue(config.decoration); + if (_configVariable(_decoration, config.decoration)) + needsAnimation = true; + } else { + _decoration = null; + } + + if (config.foregroundDecoration != null) { + _foregroundDecoration ??= new AnimatedBoxDecorationValue(config.foregroundDecoration); + if (_configVariable(_foregroundDecoration, config.foregroundDecoration)) + needsAnimation = true; + } else { + _foregroundDecoration = null; + } + + if (config.margin != null) { + _margin ??= new AnimatedEdgeDimsValue(config.margin); + if (_configVariable(_margin, config.margin)) + needsAnimation = true; + } else { + _margin = null; + } + + if (config.padding != null) { + _padding ??= new AnimatedEdgeDimsValue(config.padding); + if (_configVariable(_padding, config.padding)) + needsAnimation = true; + } else { + _padding = null; + } + + if (config.transform != null) { + _transform ??= new AnimatedMatrix4Value(config.transform); + if (_configVariable(_transform, config.transform)) + needsAnimation = true; + } else { + _transform = null; + } + + if (config.width != null) { + _width ??= new AnimatedValue(config.width); + if (_configVariable(_width, config.width)) + needsAnimation = true; + } else { + _width = null; + } + + if (config.height != null) { + _height ??= new AnimatedValue(config.height); + if (_configVariable(_height, config.height)) + needsAnimation = true; + } else { + _height = null; + } + + return needsAnimation; + } + + Widget build(BuildContext context) { + return new Container( + child: config.child, + constraints: _constraints?.value, + decoration: _decoration?.value, + foregroundDecoration: _foregroundDecoration?.value, + margin: _margin?.value, + padding: _padding?.value, + transform: _transform?.value, + width: _width?.value, + height: _height?.value + ); + } +} diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index 79f9bbab10..ea317a4f0b 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -32,7 +32,15 @@ class App extends StatefulComponent { this.theme, this.routes, this.onGenerateRoute - }): super(key: key); + }) : super(key: key) { + assert(() { + 'The "routes" argument to App() is required.'; + 'This might be a sign that you have not upgraded to our new Widgets framework.'; + 'For more details see: https://groups.google.com/forum/#!topic/flutter-dev/hcX3OvLws9c'; + '...or look at our examples: https://github.com/flutter/engine/tree/master/examples'; + return routes != null; + }); + } final String title; final ThemeData theme; diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 66bc316745..22f7d3e6a0 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -22,6 +22,7 @@ export 'package:sky/rendering.dart' show FlexAlignItems, FlexDirection, FlexJustifyContent, + Matrix4, Offset, Paint, Path, @@ -69,6 +70,33 @@ class ColorFilter extends OneChildRenderObjectWidget { } } +class ShaderMask extends OneChildRenderObjectWidget { + ShaderMask({ + Key key, + this.shaderCallback, + this.transferMode: sky.TransferMode.modulate, + Widget child + }) : super(key: key, child: child) { + assert(shaderCallback != null); + assert(transferMode != null); + } + + final ShaderCallback shaderCallback; + final sky.TransferMode transferMode; + + RenderShaderMask createRenderObject() { + return new RenderShaderMask( + shaderCallback: shaderCallback, + transferMode: transferMode + ); + } + + void updateRenderObject(RenderShaderMask renderObject, ShaderMask oldWidget) { + renderObject.shaderCallback = shaderCallback; + renderObject.transferMode = transferMode; + } +} + class DecoratedBox extends OneChildRenderObjectWidget { DecoratedBox({ Key key, @@ -392,11 +420,11 @@ class Container extends StatelessComponent { this.constraints, this.decoration, this.foregroundDecoration, - this.width, - this.height, this.margin, this.padding, - this.transform + this.transform, + this.width, + this.height }) : super(key: key) { assert(margin == null || margin.isNonNegative); assert(padding == null || padding.isNonNegative); @@ -684,6 +712,11 @@ class DefaultTextStyle extends InheritedWidget { } bool updateShouldNotify(DefaultTextStyle old) => style != old.style; + + void debugFillDescription(List description) { + super.debugFillDescription(description); + '$style'.split('\n').forEach(description.add); + } } class Text extends StatelessComponent { @@ -710,6 +743,13 @@ class Text extends StatelessComponent { text = new StyledTextSpan(combinedStyle, [text]); return new Paragraph(text: text); } + + void debugFillDescription(List description) { + super.debugFillDescription(description); + description.add('"$data"'); + if (style != null) + '$style'.split('\n').forEach(description.add); + } } class Image extends LeafRenderObjectWidget { @@ -882,7 +922,7 @@ class Listener extends OneChildRenderObjectWidget { this.onPointerMove, this.onPointerUp, this.onPointerCancel - }): super(key: key, child: child); + }) : super(key: key, child: child); final PointerEventListener onPointerDown; final PointerEventListener onPointerMove; @@ -940,4 +980,4 @@ class KeyedSubtree extends StatelessComponent { final Widget child; Widget build(BuildContext context) => child; -} \ No newline at end of file +} diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index 792c6a161a..59056dfd4f 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -35,7 +35,7 @@ class WidgetFlutterBinding extends FlutterBinding { ); } - void beginFrame(double timeStamp) { + void beginFrame(Duration timeStamp) { buildDirtyElements(); super.beginFrame(timeStamp); Element.finalizeTree(); diff --git a/packages/flutter/lib/src/widgets/checkbox.dart b/packages/flutter/lib/src/widgets/checkbox.dart index 2f69665986..89cdaee293 100644 --- a/packages/flutter/lib/src/widgets/checkbox.dart +++ b/packages/flutter/lib/src/widgets/checkbox.dart @@ -61,7 +61,7 @@ class _CheckboxWrapper extends LeafRenderObjectWidget { this.onChanged, this.uncheckedColor, this.accentColor - }): super(key: key) { + }) : super(key: key) { assert(uncheckedColor != null); assert(accentColor != null); } diff --git a/packages/flutter/lib/src/widgets/date_picker.dart b/packages/flutter/lib/src/widgets/date_picker.dart index ed3a91c8b7..5773c0a264 100644 --- a/packages/flutter/lib/src/widgets/date_picker.dart +++ b/packages/flutter/lib/src/widgets/date_picker.dart @@ -375,22 +375,20 @@ class YearPickerState extends ScrollableWidgetListState { for(int i = start; i < start + count; i++) { int year = config.firstDate.year + i; String label = year.toString(); - Widget item = new GestureDetector( + Widget item = new InkWell( key: new Key(label), onTap: () { DateTime result = new DateTime(year, config.selectedDate.month, config.selectedDate.day); config.onChanged(result); }, - child: new InkWell( - child: new Container( - height: config.itemExtent, - decoration: year == config.selectedDate.year ? new BoxDecoration( - backgroundColor: Theme.of(context).primarySwatch[100], - shape: Shape.circle - ) : null, - child: new Center( - child: new Text(label, style: style) - ) + child: new Container( + height: config.itemExtent, + decoration: year == config.selectedDate.year ? new BoxDecoration( + backgroundColor: Theme.of(context).primarySwatch[100], + shape: Shape.circle + ) : null, + child: new Center( + child: new Text(label, style: style) ) ) ); diff --git a/packages/flutter/lib/src/widgets/dialog.dart b/packages/flutter/lib/src/widgets/dialog.dart index 9098bebc1d..4efe02b5a9 100644 --- a/packages/flutter/lib/src/widgets/dialog.dart +++ b/packages/flutter/lib/src/widgets/dialog.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'package:sky/animation.dart'; +import 'package:sky/gestures.dart'; import 'package:sky/material.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/focus.dart'; @@ -30,7 +31,7 @@ class Dialog extends StatelessComponent { this.contentPadding, this.actions, this.onDismiss - }): super(key: key); + }) : super(key: key); /// The (optional) title of the dialog is displayed in a large font at the top /// of the dialog. @@ -52,7 +53,7 @@ class Dialog extends StatelessComponent { final List actions; /// An (optional) callback that is called when the dialog is dismissed. - final Function onDismiss; + final GestureTapCallback onDismiss; Color _getColor(BuildContext context) { switch (Theme.of(context).brightness) { @@ -140,11 +141,11 @@ class DialogRoute extends Route { Duration get transitionDuration => _kTransitionDuration; bool get opaque => false; - Widget build(NavigatorState navigator, WatchableAnimationPerformance nextRoutePerformance) { + Widget build(NavigatorState navigator, PerformanceView nextRoutePerformance) { return new FadeTransition( performance: performance, opacity: new AnimatedValue(0.0, end: 1.0, curve: easeOut), - child: builder(navigator, this) + child: builder(new RouteArguments(navigator: navigator, previousPerformance: this.performance, nextPerformance: nextRoutePerformance)) ); } @@ -158,11 +159,11 @@ Future showDialog(NavigatorState navigator, DialogBuilder builder) { Completer completer = new Completer(); navigator.push(new DialogRoute( completer: completer, - builder: (navigator, route) { + builder: (RouteArguments args) { return new Focus( - key: new GlobalObjectKey(route), + key: new GlobalObjectKey(completer), autofocus: true, - child: builder(navigator) + child: builder(args.navigator) ); } )); diff --git a/packages/flutter/lib/src/widgets/dismissable.dart b/packages/flutter/lib/src/widgets/dismissable.dart index fe270ab390..b9c11e2ecf 100644 --- a/packages/flutter/lib/src/widgets/dismissable.dart +++ b/packages/flutter/lib/src/widgets/dismissable.dart @@ -12,7 +12,7 @@ import 'package:sky/src/widgets/gesture_detector.dart'; const Duration _kCardDismissFadeout = const Duration(milliseconds: 200); const Duration _kCardDismissResize = const Duration(milliseconds: 300); -final Interval _kCardDismissResizeInterval = new Interval(0.4, 1.0); +const Curve _kCardDismissResizeCurve = const Interval(0.4, 1.0, curve: ease); const double _kMinFlingVelocity = 700.0; const double _kMinFlingVelocityDelta = 400.0; const double _kFlingVelocityScale = 1.0 / 300.0; @@ -50,15 +50,15 @@ class Dismissable extends StatefulComponent { class DismissableState extends State { void initState() { super.initState(); - _fadePerformance = new AnimationPerformance(duration: _kCardDismissFadeout); - _fadePerformance.addStatusListener((AnimationStatus status) { - if (status == AnimationStatus.completed) + _fadePerformance = new Performance(duration: _kCardDismissFadeout); + _fadePerformance.addStatusListener((PerformanceStatus status) { + if (status == PerformanceStatus.completed) _handleFadeCompleted(); }); } - AnimationPerformance _fadePerformance; - AnimationPerformance _resizePerformance; + Performance _fadePerformance; + Performance _resizePerformance; Size _size; double _dragExtent = 0.0; @@ -97,16 +97,11 @@ class DismissableState extends State { assert(_resizePerformance == null); setState(() { - _resizePerformance = new AnimationPerformance() + _resizePerformance = new Performance() ..duration = _kCardDismissResize ..addListener(_handleResizeProgressChanged); _resizePerformance.play(); }); - // Our squash curve (ease) does not return v=0.0 for t=0.0, so we - // technically resize on the first frame. To make sure this doesn't confuse - // any other widgets (like MixedViewport, which checks for this kind of - // thing), we report a resize straight away. - _maybeCallOnResized(); } void _handleResizeProgressChanged() { @@ -226,13 +221,12 @@ class DismissableState extends State { Widget build(BuildContext context) { if (_resizePerformance != null) { // make sure you remove this widget once it's been dismissed! - assert(_resizePerformance.status == AnimationStatus.forward); + assert(_resizePerformance.status == PerformanceStatus.forward); AnimatedValue squashAxisExtent = new AnimatedValue( _directionIsYAxis ? _size.width : _size.height, end: 0.0, - curve: ease, - interval: _kCardDismissResizeInterval + curve: _kCardDismissResizeCurve ); return new SquashTransition( diff --git a/packages/flutter/lib/src/widgets/drag_target.dart b/packages/flutter/lib/src/widgets/drag_target.dart index 19f372cf6d..51800d7891 100644 --- a/packages/flutter/lib/src/widgets/drag_target.dart +++ b/packages/flutter/lib/src/widgets/drag_target.dart @@ -45,7 +45,7 @@ class Draggable extends StatefulComponent { this.feedback, this.feedbackOffset: Offset.zero, this.dragAnchor: DragAnchor.child - }): super(key: key) { + }) : super(key: key) { assert(navigator != null); assert(child != null); assert(feedback != null); @@ -258,7 +258,7 @@ class DragRoute extends Route { bool get opaque => false; Duration get transitionDuration => const Duration(); - Widget build(NavigatorState navigator, WatchableAnimationPerformance nextRoutePerformance) { + Widget build(NavigatorState navigator, PerformanceView nextRoutePerformance) { return new Positioned( left: _lastOffset.dx, top: _lastOffset.dy, diff --git a/packages/flutter/lib/src/widgets/drawer.dart b/packages/flutter/lib/src/widgets/drawer.dart index 04435cccca..1d73b9fc84 100644 --- a/packages/flutter/lib/src/widgets/drawer.dart +++ b/packages/flutter/lib/src/widgets/drawer.dart @@ -2,17 +2,16 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:sky/animation.dart'; import 'package:sky/material.dart'; +import 'package:sky/src/widgets/animated_container.dart'; import 'package:sky/src/widgets/framework.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/gesture_detector.dart'; import 'package:sky/src/widgets/navigator.dart'; -import 'package:sky/src/widgets/scrollable.dart'; import 'package:sky/src/widgets/theme.dart'; import 'package:sky/src/widgets/transitions.dart'; +import 'package:sky/src/widgets/focus.dart'; // TODO(eseidel): Draw width should vary based on device size: // http://www.google.com/design/spec/layout/structure.html#structure-side-nav @@ -35,22 +34,16 @@ const Duration _kThemeChangeDuration = const Duration(milliseconds: 200); const Point _kOpenPosition = Point.origin; const Point _kClosedPosition = const Point(-_kWidth, 0.0); -typedef void DrawerDismissedCallback(); - class Drawer extends StatefulComponent { Drawer({ Key key, - this.children, - this.showing: false, - this.level: 0, - this.onDismissed, + this.child, + this.level: 3, this.navigator }) : super(key: key); - final List children; - final bool showing; + final Widget child; final int level; - final DrawerDismissedCallback onDismissed; final NavigatorState navigator; DrawerState createState() => new DrawerState(); @@ -59,57 +52,37 @@ class Drawer extends StatefulComponent { class DrawerState extends State { void initState() { super.initState(); - _performance = new AnimationPerformance(duration: _kBaseSettleDuration); - _performance.addStatusListener((AnimationStatus status) { - if (status == AnimationStatus.dismissed) - _handleDismissed(); - }); - // Use a spring force for animating the drawer. We can't use curves for - // this because we need a linear curve in order to track the user's finger - // while dragging. - _performance.attachedForce = kDefaultSpringForce; - if (config.navigator != null) { - // TODO(ianh): This is crazy. We should convert drawer to use a pattern like openDialog(). - // https://github.com/domokit/sky_engine/pull/1186 - scheduleMicrotask(() { - config.navigator.pushState(this, (_) => _performance.reverse()); + _performance = new Performance(duration: _kBaseSettleDuration) + ..addStatusListener((PerformanceStatus status) { + if (status == PerformanceStatus.dismissed) + config.navigator.pop(); }); - } - _performance.play(_direction); + _open(); } - AnimationPerformance _performance; - - Direction get _direction => config.showing ? Direction.forward : Direction.reverse; - - void didUpdateConfig(Drawer oldConfig) { - if (config.showing != oldConfig.showing) - _performance.play(_direction); - } + Performance _performance; Widget build(BuildContext context) { - var mask = new GestureDetector( + Widget mask = new GestureDetector( + onTap: _close, child: new ColorTransition( performance: _performance.view, - color: new AnimatedColorValue(Colors.transparent, end: const Color(0x7F000000)), + color: new AnimatedColorValue(Colors.transparent, end: Colors.black54), child: new Container() - ), - onTap: () { - _performance.reverse(); - } + ) ); Widget content = new SlideTransition( performance: _performance.view, position: new AnimatedValue(_kClosedPosition, end: _kOpenPosition), - // TODO(abarth): Use AnimatedContainer - child: new Container( - // behavior: implicitlyAnimate(const Duration(milliseconds: 200)), + child: new AnimatedContainer( + curve: ease, + duration: _kThemeChangeDuration, decoration: new BoxDecoration( backgroundColor: Theme.of(context).canvasColor, boxShadow: shadows[config.level]), width: _kWidth, - child: new Block(config.children) + child: config.child ) ); @@ -117,33 +90,64 @@ class DrawerState extends State { onHorizontalDragStart: _performance.stop, onHorizontalDragUpdate: _handleDragUpdate, onHorizontalDragEnd: _handleDragEnd, - child: new Stack([ mask, content ]) + child: new Stack([ + mask, + new Positioned( + top: 0.0, + left: 0.0, + bottom: 0.0, + child: content + ) + ]) ); } - void _handleDismissed() { - if (config.navigator != null && - config.navigator.currentRoute is StateRoute && - (config.navigator.currentRoute as StateRoute).owner == this) // TODO(ianh): remove cast once analyzer is cleverer - config.navigator.pop(); - if (config.onDismissed != null) - config.onDismissed(); - } - bool get _isMostlyClosed => _performance.progress < 0.5; - void _settle() { _isMostlyClosed ? _performance.reverse() : _performance.play(); } - void _handleDragUpdate(double delta) { _performance.progress += delta / _kWidth; } + void _open() { + _performance.fling(velocity: 1.0); + } + + void _close() { + _performance.fling(velocity: -1.0); + } + void _handleDragEnd(Offset velocity) { if (velocity.dx.abs() >= _kMinFlingVelocity) { _performance.fling(velocity: velocity.dx * _kFlingVelocityScale); + } else if (_isMostlyClosed) { + _close(); } else { - _settle(); + _open(); } } - +} + +class DrawerRoute extends Route { + DrawerRoute({ this.child, this.level }); + + final Widget child; + final int level; + + bool get opaque => false; + + Widget build(NavigatorState navigator, PerformanceView nextRoutePerformance) { + return new Focus( + key: new GlobalObjectKey(this), + autofocus: true, + child: new Drawer( + child: child, + level: level, + navigator: navigator + ) + ); + } +} + +void showDrawer({ NavigatorState navigator, Widget child, int level: 3 }) { + navigator.push(new DrawerRoute(child: child, level: level)); } diff --git a/packages/flutter/lib/src/widgets/drawer_item.dart b/packages/flutter/lib/src/widgets/drawer_item.dart index cd670b5aef..2850d9418d 100644 --- a/packages/flutter/lib/src/widgets/drawer_item.dart +++ b/packages/flutter/lib/src/widgets/drawer_item.dart @@ -10,7 +10,6 @@ import 'package:sky/painting.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/button_state.dart'; import 'package:sky/src/widgets/framework.dart'; -import 'package:sky/src/widgets/gesture_detector.dart'; import 'package:sky/src/widgets/icon.dart'; import 'package:sky/src/widgets/ink_well.dart'; import 'package:sky/src/widgets/theme.dart'; @@ -21,7 +20,7 @@ class DrawerItem extends StatefulComponent { final String icon; final Widget child; - final GestureTapListener onPressed; + final GestureTapCallback onPressed; final bool selected; DrawerItemState createState() => new DrawerItemState(); @@ -76,14 +75,12 @@ class DrawerItemState extends ButtonState { ) ); - return new GestureDetector( - onTap: config.onPressed, - child: new Container( - height: 48.0, - decoration: new BoxDecoration(backgroundColor: _getBackgroundColor(themeData)), - child: new InkWell( - child: new Row(flexChildren) - ) + return new Container( + height: 48.0, + decoration: new BoxDecoration(backgroundColor: _getBackgroundColor(themeData)), + child: new InkWell( + onTap: config.onPressed, + child: new Row(flexChildren) ) ); } diff --git a/packages/flutter/lib/src/widgets/flat_button.dart b/packages/flutter/lib/src/widgets/flat_button.dart index 501799005c..02beead355 100644 --- a/packages/flutter/lib/src/widgets/flat_button.dart +++ b/packages/flutter/lib/src/widgets/flat_button.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:sky/gestures.dart'; import 'package:sky/material.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/framework.dart'; @@ -13,7 +14,7 @@ class FlatButton extends MaterialButton { Key key, Widget child, bool enabled: true, - Function onPressed + GestureTapCallback onPressed }) : super(key: key, child: child, enabled: enabled, diff --git a/packages/flutter/lib/src/widgets/floating_action_button.dart b/packages/flutter/lib/src/widgets/floating_action_button.dart index b61993abe4..6b7d4b66c3 100644 --- a/packages/flutter/lib/src/widgets/floating_action_button.dart +++ b/packages/flutter/lib/src/widgets/floating_action_button.dart @@ -6,7 +6,6 @@ import 'package:sky/gestures.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/button_state.dart'; import 'package:sky/src/widgets/framework.dart'; -import 'package:sky/src/widgets/gesture_detector.dart'; import 'package:sky/src/widgets/icon.dart'; import 'package:sky/src/widgets/ink_well.dart'; import 'package:sky/src/widgets/material.dart'; @@ -26,7 +25,7 @@ class FloatingActionButton extends StatefulComponent { final Widget child; final Color backgroundColor; - final GestureTapListener onPressed; + final GestureTapCallback onPressed; FloatingActionButtonState createState() => new FloatingActionButtonState(); } @@ -46,17 +45,15 @@ class FloatingActionButtonState extends ButtonState { type: MaterialType.circle, level: highlight ? 3 : 2, child: new ClipOval( - child: new GestureDetector( - onTap: config.onPressed, - child: new Container( - width: _kSize, - height: _kSize, - child: new InkWell( - child: new Center( - child: new IconTheme( - data: new IconThemeData(color: iconThemeColor), - child: config.child - ) + child: new Container( + width: _kSize, + height: _kSize, + child: new InkWell( + onTap: config.onPressed, + child: new Center( + child: new IconTheme( + data: new IconThemeData(color: iconThemeColor), + child: config.child ) ) ) diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 617ff6057a..05ce1e691c 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -181,7 +181,16 @@ abstract class Widget { /// Inflates this configuration to a concrete instance. Element createElement(); - String toString() => '$runtimeType'; + String toString() { + final String name = key == null ? '$runtimeType' : '$runtimeType-$key'; + final List data = []; + debugFillDescription(data); + if (data.isEmpty) + return 'name'; + return 'name(${data.join("; ")})'; + } + + void debugFillDescription(List description) { } } /// RenderObjectWidgets provide the configuration for [RenderObjectElement]s, @@ -482,9 +491,11 @@ final _InactiveElements _inactiveElements = new _InactiveElements(); typedef void ElementVisitor(Element element); abstract class BuildContext { - InheritedWidget inheritedWidgetOfType(Type targetType); + Widget get widget; RenderObject findRenderObject(); + InheritedWidget inheritedWidgetOfType(Type targetType); void visitAncestorElements(bool visitor(Element element)); + void visitChildElements(void visitor(Element element)); } /// Elements are the instantiations of Widget configurations. @@ -536,6 +547,13 @@ abstract class Element implements BuildContext { /// Calls the argument for each child. Must be overridden by subclasses that support having children. void visitChildren(ElementVisitor visitor) { } + /// Wrapper around visitChildren for BuildContext. + void visitChildElements(void visitor(Element element)) { + // don't allow visitChildElements() during build, since children aren't necessarily built yet + assert(BuildableElement._debugStateLockLevel == 0); + visitChildren(visitor); + } + bool detachChild(Element child) => false; /// This method is the core of the system. @@ -782,6 +800,8 @@ abstract class Element implements BuildContext { description.add('no depth'); if (widget == null) description.add('no widget'); + else + widget.debugFillDescription(description); } String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) { @@ -948,11 +968,11 @@ abstract class BuildableElement extends Element { /// Instantiation of StatelessComponent widgets. class StatelessComponentElement extends BuildableElement { - StatelessComponentElement(StatelessComponent widget) : super(widget) { + StatelessComponentElement(T widget) : super(widget) { _builder = widget.build; } - void update(StatelessComponent newWidget) { + void update(T newWidget) { super.update(newWidget); assert(widget == newWidget); _builder = widget.build; @@ -962,10 +982,10 @@ class StatelessComponentElement extends BuildableE } /// Instantiation of StatefulComponent widgets. -class StatefulComponentElement extends BuildableElement { - StatefulComponentElement(StatefulComponent widget) +class StatefulComponentElement> extends BuildableElement { + StatefulComponentElement(T widget) : _state = widget.createState(), super(widget) { - assert(_state._debugTypesAreRight(widget)); + assert(_state._debugTypesAreRight(widget)); // can't use T and U, since normally we don't actually set those assert(_state._element == null); _state._element = this; assert(_builder == null); @@ -988,10 +1008,10 @@ class StatefulComponentElement extends BuildableElement { assert(() { _state._debugLifecycleState = _StateLifecycle.ready; return true; }); } - State get state => _state; - State _state; + U get state => _state; + U _state; - void update(StatefulComponent newWidget) { + void update(T newWidget) { super.update(newWidget); assert(widget == newWidget); StatefulComponent oldConfig = _state._config; diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart index 991d612fb0..b974761f53 100644 --- a/packages/flutter/lib/src/widgets/gesture_detector.dart +++ b/packages/flutter/lib/src/widgets/gesture_detector.dart @@ -15,6 +15,8 @@ class GestureDetector extends StatefulComponent { this.child, this.onTap, this.onDoubleTap, + this.onTapDown, + this.onTapCancel, this.onShowPress, this.onLongPress, this.onVerticalDragStart, @@ -32,10 +34,14 @@ class GestureDetector extends StatefulComponent { }) : super(key: key); final Widget child; - final GestureTapListener onTap; + + final GestureTapCallback onTap; + final GestureTapCallback onTapDown; + final GestureTapCallback onTapCancel; final GestureTapListener onDoubleTap; - final GestureShowPressListener onShowPress; - final GestureLongPressListener onLongPress; + + final GestureShowPressCallback onShowPress; + final GestureLongPressCallback onLongPress; final GestureDragStartCallback onVerticalDragStart; final GestureDragUpdateCallback onVerticalDragUpdate; @@ -57,69 +63,24 @@ class GestureDetector extends StatefulComponent { } class GestureDetectorState extends State { - void initState() { - super.initState(); - didUpdateConfig(null); - } - final PointerRouter _router = FlutterBinding.instance.pointerRouter; TapGestureRecognizer _tap; - TapGestureRecognizer _ensureTap() { - if (_tap == null) - _tap = new TapGestureRecognizer(router: _router); - return _tap; - } - DoubleTapGestureRecognizer _doubleTap; - DoubleTapGestureRecognizer _ensureDoubleTap() { - if (_doubleTap == null) - _doubleTap = new DoubleTapGestureRecognizer(router: _router); - return _doubleTap; - } - ShowPressGestureRecognizer _showPress; - ShowPressGestureRecognizer _ensureShowPress() { - if (_showPress == null) - _showPress = new ShowPressGestureRecognizer(router: _router); - return _showPress; - } - LongPressGestureRecognizer _longPress; - LongPressGestureRecognizer _ensureLongPress() { - if (_longPress == null) - _longPress = new LongPressGestureRecognizer(router: _router); - return _longPress; - } - VerticalDragGestureRecognizer _verticalDrag; - VerticalDragGestureRecognizer _ensureVerticalDrag() { - if (_verticalDrag == null) - _verticalDrag = new VerticalDragGestureRecognizer(router: _router); - return _verticalDrag; - } - HorizontalDragGestureRecognizer _horizontalDrag; - HorizontalDragGestureRecognizer _ensureHorizontalDrag() { - if (_horizontalDrag == null) - _horizontalDrag = new HorizontalDragGestureRecognizer(router: _router); - return _horizontalDrag; - } - PanGestureRecognizer _pan; - PanGestureRecognizer _ensurePan() { - assert(_scale == null); // Scale is a superset of pan; just use scale - if (_pan == null) - _pan = new PanGestureRecognizer(router: _router); - return _pan; + ScaleGestureRecognizer _scale; + + void initState() { + super.initState(); + _syncAll(); } - ScaleGestureRecognizer _scale; - ScaleGestureRecognizer _ensureScale() { - assert(_pan == null); // Scale is a superset of pan; just use scale - if (_scale == null) - _scale = new ScaleGestureRecognizer(router: _router); - return _scale; + void didUpdateConfig(GestureDetector oldConfig) { + _syncAll(); } void dispose() { @@ -134,7 +95,7 @@ class GestureDetectorState extends State { super.dispose(); } - void didUpdateConfig(GestureDetector oldConfig) { + void _syncAll() { _syncTap(); _syncDoubleTap(); _syncShowPress(); @@ -146,10 +107,15 @@ class GestureDetectorState extends State { } void _syncTap() { - if (config.onTap == null) + if (config.onTap == null && config.onTapDown == null && config.onTapCancel == null) { _tap = _ensureDisposed(_tap); - else - _ensureTap().onTap = config.onTap; + } else { + _tap ??= new TapGestureRecognizer(router: _router); + _tap + ..onTap = config.onTap + ..onTapDown = config.onTapDown + ..onTapCancel = config.onTapCancel; + } } void _syncDoubleTap() { @@ -160,24 +126,29 @@ class GestureDetectorState extends State { } void _syncShowPress() { - if (config.onShowPress == null) + if (config.onShowPress == null) { _showPress = _ensureDisposed(_showPress); - else - _ensureShowPress().onShowPress = config.onShowPress; + } else { + _showPress ??= new ShowPressGestureRecognizer(router: _router); + _showPress.onShowPress = config.onShowPress; + } } void _syncLongPress() { - if (config.onLongPress == null) + if (config.onLongPress == null) { _longPress = _ensureDisposed(_longPress); - else - _ensureLongPress().onLongPress = config.onLongPress; + } else { + _longPress ??= new LongPressGestureRecognizer(router: _router); + _longPress.onLongPress = config.onLongPress; + } } void _syncVerticalDrag() { if (config.onVerticalDragStart == null && config.onVerticalDragUpdate == null && config.onVerticalDragEnd == null) { _verticalDrag = _ensureDisposed(_verticalDrag); } else { - _ensureVerticalDrag() + _verticalDrag ??= new VerticalDragGestureRecognizer(router: _router); + _verticalDrag ..onStart = config.onVerticalDragStart ..onUpdate = config.onVerticalDragUpdate ..onEnd = config.onVerticalDragEnd; @@ -188,7 +159,8 @@ class GestureDetectorState extends State { if (config.onHorizontalDragStart == null && config.onHorizontalDragUpdate == null && config.onHorizontalDragEnd == null) { _horizontalDrag = _ensureDisposed(_horizontalDrag); } else { - _ensureHorizontalDrag() + _horizontalDrag ??= new HorizontalDragGestureRecognizer(router: _router); + _horizontalDrag ..onStart = config.onHorizontalDragStart ..onUpdate = config.onHorizontalDragUpdate ..onEnd = config.onHorizontalDragEnd; @@ -199,7 +171,9 @@ class GestureDetectorState extends State { if (config.onPanStart == null && config.onPanUpdate == null && config.onPanEnd == null) { _pan = _ensureDisposed(_pan); } else { - _ensurePan() + assert(_scale == null); // Scale is a superset of pan; just use scale + _pan ??= new PanGestureRecognizer(router: _router); + _pan ..onStart = config.onPanStart ..onUpdate = config.onPanUpdate ..onEnd = config.onPanEnd; @@ -208,9 +182,11 @@ class GestureDetectorState extends State { void _syncScale() { if (config.onScaleStart == null && config.onScaleUpdate == null && config.onScaleEnd == null) { - _scale = _ensureDisposed(_pan); + _scale = _ensureDisposed(_scale); } else { - _ensureScale() + assert(_pan == null); // Scale is a superset of pan; just use scale + _scale ??= new ScaleGestureRecognizer(router: _router); + _scale ..onStart = config.onScaleStart ..onUpdate = config.onScaleUpdate ..onEnd = config.onScaleEnd; diff --git a/packages/flutter/lib/src/widgets/icon_button.dart b/packages/flutter/lib/src/widgets/icon_button.dart index fcb42fd327..662ee5fb52 100644 --- a/packages/flutter/lib/src/widgets/icon_button.dart +++ b/packages/flutter/lib/src/widgets/icon_button.dart @@ -4,6 +4,7 @@ import 'dart:sky' as sky; +import 'package:sky/gestures.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/icon.dart'; import 'package:sky/src/widgets/framework.dart'; @@ -13,8 +14,8 @@ class IconButton extends StatelessComponent { const IconButton({ Key key, this.icon, this.onPressed, this.color }) : super(key: key); final String icon; - final Function onPressed; final Color color; + final GestureTapCallback onPressed; Widget build(BuildContext context) { Widget child = new Icon(type: icon, size: 24); diff --git a/packages/flutter/lib/src/widgets/ink_well.dart b/packages/flutter/lib/src/widgets/ink_well.dart index 7f2b2e0e13..9abe7745da 100644 --- a/packages/flutter/lib/src/widgets/ink_well.dart +++ b/packages/flutter/lib/src/widgets/ink_well.dart @@ -2,16 +2,18 @@ // 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:math' as math; import 'dart:sky' as sky; import 'package:sky/animation.dart'; +import 'package:sky/gestures.dart'; import 'package:sky/rendering.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/framework.dart'; const int _kSplashInitialOpacity = 0x30; -const double _kSplashCancelledVelocity = 0.7; +const double _kSplashCanceledVelocity = 0.7; const double _kSplashConfirmedVelocity = 0.7; const double _kSplashInitialSize = 0.0; const double _kSplashUnconfirmedVelocity = 0.2; @@ -25,40 +27,60 @@ double _getSplashTargetSize(Size bounds, Point position) { } class InkSplash { - InkSplash(this.pointer, this.position, this.well) { + InkSplash(this.position, this.well) { _targetRadius = _getSplashTargetSize(well.size, position); _radius = new AnimatedValue( _kSplashInitialSize, end: _targetRadius, curve: easeOut); - _performance = new ValueAnimation( + _performance = new ValuePerformance( variable: _radius, duration: new Duration(milliseconds: (_targetRadius / _kSplashUnconfirmedVelocity).floor()) - )..addListener(_handleRadiusChange) - ..play(); + )..addListener(_handleRadiusChange); + + // Wait kTapTimeout to avoid creating tiny splashes during scrolls. + _startTimer = new Timer(kTapTimeout, _play); } - final int pointer; final Point position; final RenderInkWell well; double _targetRadius; double _pinnedRadius; AnimatedValue _radius; - AnimationPerformance _performance; + Performance _performance; + Timer _startTimer; + + bool _cancelStartTimer() { + if (_startTimer != null) { + _startTimer.cancel(); + _startTimer = null; + return true; + } + return false; + } + + void _play() { + _cancelStartTimer(); + _performance.play(); + } void _updateVelocity(double velocity) { int duration = (_targetRadius / velocity).floor(); - _performance - ..duration = new Duration(milliseconds: duration) - ..play(); + _performance.duration = new Duration(milliseconds: duration); + _play(); } void confirm() { + if (_cancelStartTimer()) + return; _updateVelocity(_kSplashConfirmedVelocity); + _pinnedRadius = null; } void cancel() { - _updateVelocity(_kSplashCancelledVelocity); + if (_cancelStartTimer()) + return; + _updateVelocity(_kSplashCanceledVelocity); _pinnedRadius = _radius.value; } @@ -77,38 +99,95 @@ class InkSplash { } class RenderInkWell extends RenderProxyBox { - RenderInkWell({ RenderBox child }) : super(child); + RenderInkWell({ + RenderBox child, + GestureTapCallback onTap, + GestureLongPressCallback onLongPress + }) : super(child) { + this.onTap = onTap; + this.onLongPress = onLongPress; + } + + GestureTapCallback get onTap => _onTap; + GestureTapCallback _onTap; + void set onTap (GestureTapCallback value) { + _onTap = value; + _syncTapRecognizer(); + } + + GestureTapCallback get onLongPress => _onLongPress; + GestureTapCallback _onLongPress; + void set onLongPress (GestureTapCallback value) { + _onLongPress = value; + _syncLongPressRecognizer(); + } final List _splashes = new List(); + TapGestureRecognizer _tap; + LongPressGestureRecognizer _longPress; + void handleEvent(sky.Event event, BoxHitTestEntry entry) { - // TODO(abarth): We should trigger these effects based on gestures. - // https://github.com/flutter/engine/issues/1271 - if (event is sky.PointerEvent) { - switch (event.type) { - case 'pointerdown': - _startSplash(event.pointer, entry.localPosition); - break; - case 'pointerup': - _confirmSplash(event.pointer); - break; - } + if (event.type == 'pointerdown' && (_tap != null || _longPress != null)) { + _tap?.addPointer(event); + _longPress?.addPointer(event); + _splashes.add(new InkSplash(entry.localPosition, this)); } } - void _startSplash(int pointer, Point position) { - _splashes.add(new InkSplash(pointer, position, this)); - markNeedsPaint(); + void attach() { + super.attach(); + _syncTapRecognizer(); + _syncLongPressRecognizer(); } - void _forEachSplash(int pointer, Function callback) { - _splashes.where((splash) => splash.pointer == pointer) - .forEach(callback); + void detach() { + _disposeTapRecognizer(); + _disposeLongPressRecognizer(); + super.detach(); } - void _confirmSplash(int pointer) { - _forEachSplash(pointer, (splash) { splash.confirm(); }); - markNeedsPaint(); + void _syncTapRecognizer() { + if (onTap == null) { + _disposeTapRecognizer(); + } else { + _tap ??= new TapGestureRecognizer(router: FlutterBinding.instance.pointerRouter) + ..onTap = _handleTap + ..onTapCancel = _handleTapCancel; + } + } + + void _disposeTapRecognizer() { + _tap?.dispose(); + _tap = null; + } + + void _syncLongPressRecognizer() { + if (onLongPress == null) { + _disposeLongPressRecognizer(); + } else { + _longPress ??= new LongPressGestureRecognizer(router: FlutterBinding.instance.pointerRouter) + ..onLongPress = _handleLongPress; + } + } + + void _disposeLongPressRecognizer() { + _longPress?.dispose(); + _longPress = null; + } + + void _handleTap() { + _splashes.last?.confirm(); + onTap(); + } + + void _handleTapCancel() { + _splashes.last?.cancel(); + } + + void _handleLongPress() { + _splashes.last?.confirm(); + onLongPress(); } void paint(PaintingContext context, Offset offset) { @@ -126,6 +205,20 @@ class RenderInkWell extends RenderProxyBox { } class InkWell extends OneChildRenderObjectWidget { - InkWell({ Key key, Widget child }) : super(key: key, child: child); - RenderInkWell createRenderObject() => new RenderInkWell(); + InkWell({ + Key key, + Widget child, + this.onTap, + this.onLongPress + }) : super(key: key, child: child); + + final GestureTapCallback onTap; + final GestureLongPressCallback onLongPress; + + RenderInkWell createRenderObject() => new RenderInkWell(onTap: onTap, onLongPress: onLongPress); + + void updateRenderObject(RenderInkWell renderObject, InkWell oldWidget) { + renderObject.onTap = onTap; + renderObject.onLongPress = onLongPress; + } } diff --git a/packages/flutter/lib/src/widgets/input.dart b/packages/flutter/lib/src/widgets/input.dart index b9bcfbaf92..d892944b1d 100644 --- a/packages/flutter/lib/src/widgets/input.dart +++ b/packages/flutter/lib/src/widgets/input.dart @@ -28,7 +28,7 @@ class Input extends Scrollable { this.placeholder, this.onChanged, this.keyboardType: KeyboardType.TEXT - }): super( + }) : super( key: key, initialScrollOffset: 0.0, scrollDirection: ScrollDirection.horizontal diff --git a/packages/flutter/lib/src/widgets/material.dart b/packages/flutter/lib/src/widgets/material.dart index 02ea4ce80e..eb79290ec9 100644 --- a/packages/flutter/lib/src/widgets/material.dart +++ b/packages/flutter/lib/src/widgets/material.dart @@ -2,8 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:sky/animation.dart'; import 'package:sky/painting.dart'; import 'package:sky/material.dart'; +import 'package:sky/src/widgets/animated_container.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/framework.dart'; import 'package:sky/src/widgets/theme.dart'; @@ -61,10 +63,11 @@ class Material extends StatelessComponent { ); } } - // TODO(abarth): This should use AnimatedContainer. return new DefaultTextStyle( style: Theme.of(context).text.body1, - child: new Container( + child: new AnimatedContainer( + curve: ease, + duration: const Duration(milliseconds: 200), decoration: new BoxDecoration( backgroundColor: getBackgroundColor(context), borderRadius: edges[type], diff --git a/packages/flutter/lib/src/widgets/material_button.dart b/packages/flutter/lib/src/widgets/material_button.dart index c5dfb0a7be..b18cc313c1 100644 --- a/packages/flutter/lib/src/widgets/material_button.dart +++ b/packages/flutter/lib/src/widgets/material_button.dart @@ -2,10 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:sky/gestures.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/button_state.dart'; import 'package:sky/src/widgets/framework.dart'; -import 'package:sky/src/widgets/gesture_detector.dart'; import 'package:sky/src/widgets/ink_well.dart'; import 'package:sky/src/widgets/material.dart'; @@ -22,7 +22,7 @@ abstract class MaterialButton extends StatefulComponent { final Widget child; final bool enabled; - final Function onPressed; + final GestureTapCallback onPressed; } abstract class MaterialButtonState extends ButtonState { @@ -37,17 +37,17 @@ abstract class MaterialButtonState extends ButtonState child: config.child // TODO(ianh): figure out a way to compell the child to have gray text when disabled... ) ); - return new GestureDetector( - onTap: config.enabled ? config.onPressed : null, - child: new Container( - height: 36.0, - constraints: new BoxConstraints(minWidth: 88.0), - margin: new EdgeDims.all(8.0), - child: new Material( - type: MaterialType.button, - child: config.enabled ? new InkWell(child: contents) : contents, - level: level, - color: getColor(context) + return new Container( + height: 36.0, + constraints: new BoxConstraints(minWidth: 88.0), + margin: new EdgeDims.all(8.0), + child: new Material( + type: MaterialType.button, + level: level, + color: getColor(context), + child: new InkWell( + onTap: config.enabled ? config.onPressed : null, + child: contents ) ) ); diff --git a/packages/flutter/lib/src/widgets/mixed_viewport.dart b/packages/flutter/lib/src/widgets/mixed_viewport.dart index 505aa7a988..5336e80c71 100644 --- a/packages/flutter/lib/src/widgets/mixed_viewport.dart +++ b/packages/flutter/lib/src/widgets/mixed_viewport.dart @@ -22,7 +22,7 @@ class MixedViewport extends RenderObjectWidget { this.token, this.onExtentsUpdate, this.onInvalidatorAvailable - }): super(key: key); + }) : super(key: key); final double startOffset; final ScrollDirection direction; diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index 55abf1be2c..d69d75a3a6 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -8,7 +8,14 @@ import 'package:sky/src/widgets/focus.dart'; import 'package:sky/src/widgets/framework.dart'; import 'package:sky/src/widgets/transitions.dart'; -typedef Widget RouteBuilder(NavigatorState navigator, Route route); +class RouteArguments { + const RouteArguments({ this.navigator, this.previousPerformance, this.nextPerformance }); + final NavigatorState navigator; + final PerformanceView previousPerformance; + final PerformanceView nextPerformance; +} + +typedef Widget RouteBuilder(RouteArguments args); typedef RouteBuilder RouteGenerator(String name); typedef void StateRouteCallback(StateRoute route); typedef void NotificationCallback(); @@ -118,7 +125,7 @@ class NavigatorState extends State { Widget build(BuildContext context) { List visibleRoutes = new List(); bool alreadyInsertModalBarrier = false; - WatchableAnimationPerformance nextPerformance; + PerformanceView nextPerformance; for (int i = _history.length-1; i >= 0; i -= 1) { Route route = _history[i]; if (!route.hasContent) { @@ -126,11 +133,16 @@ class NavigatorState extends State { continue; } route.ensurePerformance( - direction: (i <= _currentPosition) ? Direction.forward : Direction.reverse + direction: (i <= _currentPosition) ? AnimationDirection.forward : AnimationDirection.reverse ); route._onDismissed = () { + assert(_history.contains(route)); + if (_history.lastIndexOf(route) <= _currentPosition) + popRoute(route); + }; + route._onRemoveRoute = () { + assert(_history.contains(route)); setState(() { - assert(_history.contains(route)); _history.remove(route); }); }; @@ -154,33 +166,39 @@ class NavigatorState extends State { } return new Focus(child: new Stack(visibleRoutes.reversed.toList())); } + } abstract class Route { - WatchableAnimationPerformance get performance => _performance?.view; - AnimationPerformance _performance; + PerformanceView get performance => _performance?.view; + Performance _performance; NotificationCallback _onDismissed; + NotificationCallback _onRemoveRoute; - AnimationPerformance createPerformance() { + Performance createPerformance() { Duration duration = transitionDuration; if (duration > Duration.ZERO) { - return new AnimationPerformance(duration: duration) - ..addStatusListener((AnimationStatus status) { - if (status == AnimationStatus.dismissed && _onDismissed != null) - _onDismissed(); + return new Performance(duration: duration) + ..addStatusListener((PerformanceStatus status) { + if (status == PerformanceStatus.dismissed) { + if (_onDismissed != null) + _onDismissed(); + if (_onRemoveRoute != null) + _onRemoveRoute(); + } }); } return null; } - void ensurePerformance({ Direction direction }) { + void ensurePerformance({ AnimationDirection direction }) { assert(direction != null); if (_performance == null) _performance = createPerformance(); if (_performance != null) { - AnimationStatus desiredStatus = direction == Direction.forward ? AnimationStatus.forward : AnimationStatus.reverse; + PerformanceStatus desiredStatus = direction == AnimationDirection.forward ? PerformanceStatus.forward : PerformanceStatus.reverse; if (_performance.status != desiredStatus) _performance.play(direction); } @@ -236,17 +254,17 @@ abstract class Route { /// cover the entire application surface or are in any way semi-transparent. bool get opaque => false; - /// If this is set to a non-zero [Duration], then an [AnimationPerformance] + /// If this is set to a non-zero [Duration], then an [Performance] /// object, available via the performance field, will be created when the /// route is first built, using the duration described here. Duration get transitionDuration => Duration.ZERO; bool get isActuallyOpaque => (performance == null || _performance.isCompleted) && opaque; - Widget build(NavigatorState navigator, WatchableAnimationPerformance nextRoutePerformance); + Widget build(NavigatorState navigator, PerformanceView nextRoutePerformance); void didPop([dynamic result]) { - if (performance == null && _onDismissed != null) - _onDismissed(); + if (performance == null && _onRemoveRoute != null) + _onRemoveRoute(); } String toString() => '$runtimeType()'; @@ -263,7 +281,7 @@ class PageRoute extends Route { bool get opaque => true; Duration get transitionDuration => _kTransitionDuration; - Widget build(NavigatorState navigator, WatchableAnimationPerformance nextRoutePerformance) { + Widget build(NavigatorState navigator, PerformanceView nextRoutePerformance) { // TODO(jackson): Hit testing should ignore transform // TODO(jackson): Block input unless content is interactive return new SlideTransition( @@ -272,7 +290,7 @@ class PageRoute extends Route { child: new FadeTransition( performance: performance, opacity: new AnimatedValue(0.0, end: 1.0, curve: easeOut), - child: builder(navigator, this) + child: builder(new RouteArguments(navigator: navigator, previousPerformance: this.performance, nextPerformance: nextRoutePerformance)) ) ); } @@ -296,5 +314,5 @@ class StateRoute extends Route { super.didPop(result); } - Widget build(NavigatorState navigator, WatchableAnimationPerformance nextRoutePerformance) => null; + Widget build(NavigatorState navigator, PerformanceView nextRoutePerformance) => null; } diff --git a/packages/flutter/lib/src/widgets/popup_menu.dart b/packages/flutter/lib/src/widgets/popup_menu.dart index f66f1007c7..c6af624244 100644 --- a/packages/flutter/lib/src/widgets/popup_menu.dart +++ b/packages/flutter/lib/src/widgets/popup_menu.dart @@ -11,7 +11,7 @@ import 'package:sky/painting.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/focus.dart'; import 'package:sky/src/widgets/framework.dart'; -import 'package:sky/src/widgets/gesture_detector.dart'; +import 'package:sky/src/widgets/ink_well.dart'; import 'package:sky/src/widgets/navigator.dart'; import 'package:sky/src/widgets/popup_menu_item.dart'; import 'package:sky/src/widgets/scrollable.dart'; @@ -29,7 +29,7 @@ const double _kMenuVerticalPadding = 8.0; typedef List PopupMenuItemsBuilder(NavigatorState navigator); -class PopupMenu extends StatefulComponent { +class PopupMenu extends StatelessComponent { PopupMenu({ Key key, this.items, @@ -44,78 +44,48 @@ class PopupMenu extends StatefulComponent { final List items; final int level; final NavigatorState navigator; - final WatchableAnimationPerformance performance; - - PopupMenuState createState() => new PopupMenuState(); -} - -class PopupMenuState extends State { - void initState() { - super.initState(); - config.performance.addListener(_performanceChanged); - } - - void didUpdateConfig(PopupMenu oldConfig) { - if (config.performance != oldConfig.performance) { - oldConfig.performance.removeListener(_performanceChanged); - config.performance.addListener(_performanceChanged); - } - } - - void dispose() { - config.performance.removeListener(_performanceChanged); - super.dispose(); - } - - void _performanceChanged() { - setState(() { - // the performance changed, and our state is tied up with the performance - }); - } - - BoxPainter _painter; - - void _updateBoxPainter(BoxDecoration decoration) { - if (_painter == null || _painter.decoration != decoration) - _painter = new BoxPainter(decoration); - } + final PerformanceView performance; Widget build(BuildContext context) { - _updateBoxPainter(new BoxDecoration( + final BoxPainter painter = new BoxPainter(new BoxDecoration( backgroundColor: Theme.of(context).canvasColor, borderRadius: 2.0, - boxShadow: shadows[config.level] + boxShadow: shadows[level] )); - double unit = 1.0 / (config.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade. + + double unit = 1.0 / (items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade. List children = []; - for (int i = 0; i < config.items.length; ++i) { + + for (int i = 0; i < items.length; ++i) { double start = (i + 1) * unit; double end = (start + 1.5 * unit).clamp(0.0, 1.0); children.add(new FadeTransition( - performance: config.performance, - opacity: new AnimatedValue(0.0, end: 1.0, interval: new Interval(start, end)), - child: new GestureDetector( - onTap: () { config.navigator.pop(config.items[i].value); }, - child: config.items[i] + performance: performance, + opacity: new AnimatedValue(0.0, end: 1.0, curve: new Interval(start, end)), + child: new InkWell( + onTap: () { navigator.pop(items[i].value); }, + child: items[i] )) ); } - final width = new AnimatedValue(0.0, end: 1.0, interval: new Interval(0.0, unit)); - final height = new AnimatedValue(0.0, end: 1.0, interval: new Interval(0.0, unit * config.items.length)); + + final width = new AnimatedValue(0.0, end: 1.0, curve: new Interval(0.0, unit)); + final height = new AnimatedValue(0.0, end: 1.0, curve: new Interval(0.0, unit * items.length)); + return new FadeTransition( - performance: config.performance, - opacity: new AnimatedValue(0.0, end: 1.0, interval: new Interval(0.0, 1.0 / 3.0)), + performance: performance, + opacity: new AnimatedValue(0.0, end: 1.0, curve: new Interval(0.0, 1.0 / 3.0)), child: new Container( margin: new EdgeDims.all(_kMenuMargin), child: new BuilderTransition( - performance: config.performance, + performance: performance, variables: [width, height], builder: (BuildContext context) { return new CustomPaint( callback: (sky.Canvas canvas, Size size) { double widthValue = width.value * size.width; double heightValue = height.value * size.height; - _painter.paint(canvas, new Rect.fromLTWH(size.width - widthValue, 0.0, widthValue, heightValue)); + painter.paint(canvas, new Rect.fromLTWH(size.width - widthValue, 0.0, widthValue, heightValue)); }, child: new ConstrainedBox( constraints: new BoxConstraints( @@ -159,10 +129,10 @@ class MenuRoute extends Route { final PopupMenuItemsBuilder builder; final int level; - AnimationPerformance createPerformance() { - AnimationPerformance result = super.createPerformance(); + Performance createPerformance() { + Performance result = super.createPerformance(); AnimationTiming timing = new AnimationTiming(); - timing.reverseInterval = new Interval(0.0, _kMenuCloseIntervalEnd); + timing.reverseCurve = new Interval(0.0, _kMenuCloseIntervalEnd); result.timing = timing; return result; } @@ -172,7 +142,7 @@ class MenuRoute extends Route { bool get opaque => false; Duration get transitionDuration => _kMenuDuration; - Widget build(NavigatorState navigator, WatchableAnimationPerformance nextRoutePerformance) { + Widget build(NavigatorState navigator, PerformanceView nextRoutePerformance) { return new Positioned( top: position?.top, right: position?.right, diff --git a/packages/flutter/lib/src/widgets/popup_menu_item.dart b/packages/flutter/lib/src/widgets/popup_menu_item.dart index 1e44c23c2d..d52d8e26ab 100644 --- a/packages/flutter/lib/src/widgets/popup_menu_item.dart +++ b/packages/flutter/lib/src/widgets/popup_menu_item.dart @@ -4,7 +4,6 @@ import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/framework.dart'; -import 'package:sky/src/widgets/ink_well.dart'; import 'package:sky/src/widgets/theme.dart'; const double _kMenuItemHeight = 48.0; @@ -21,15 +20,13 @@ class PopupMenuItem extends StatelessComponent { final dynamic value; Widget build(BuildContext context) { - return new InkWell( - child: new Container( - height: _kMenuItemHeight, - child: new DefaultTextStyle( - style: Theme.of(context).text.subhead, - child: new Baseline( - baseline: _kMenuItemHeight - _kBaselineOffsetFromBottom, - child: child - ) + return new Container( + height: _kMenuItemHeight, + child: new DefaultTextStyle( + style: Theme.of(context).text.subhead, + child: new Baseline( + baseline: _kMenuItemHeight - _kBaselineOffsetFromBottom, + child: child ) ) ); diff --git a/packages/flutter/lib/src/widgets/progress_indicator.dart b/packages/flutter/lib/src/widgets/progress_indicator.dart index d6c86ef72f..c661fced9e 100644 --- a/packages/flutter/lib/src/widgets/progress_indicator.dart +++ b/packages/flutter/lib/src/widgets/progress_indicator.dart @@ -36,16 +36,16 @@ abstract class ProgressIndicator extends StatefulComponent { class ProgressIndicatorState extends State { - ValueAnimation _performance; + ValuePerformance _performance; void initState() { super.initState(); - _performance = new ValueAnimation( + _performance = new ValuePerformance( variable: new AnimatedValue(0.0, end: 1.0, curve: ease), duration: const Duration(milliseconds: 1500) ); - _performance.addStatusListener((AnimationStatus status) { - if (status == AnimationStatus.completed) + _performance.addStatusListener((PerformanceStatus status) { + if (status == PerformanceStatus.completed) _restartAnimation(); }); _performance.play(); diff --git a/packages/flutter/lib/src/widgets/raised_button.dart b/packages/flutter/lib/src/widgets/raised_button.dart index bf666f2783..22c596511c 100644 --- a/packages/flutter/lib/src/widgets/raised_button.dart +++ b/packages/flutter/lib/src/widgets/raised_button.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:sky/gestures.dart'; import 'package:sky/material.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/framework.dart'; @@ -13,7 +14,7 @@ class RaisedButton extends MaterialButton { Key key, Widget child, bool enabled: true, - Function onPressed + GestureTapCallback onPressed }) : super(key: key, child: child, enabled: enabled, diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index f5ac199d25..da4d940a96 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -51,16 +51,10 @@ abstract class ScrollableState extends State { super.initState(); if (config.initialScrollOffset is double) _scrollOffset = config.initialScrollOffset; - _toEndAnimation = new AnimatedSimulation(_setScrollOffset); - _toOffsetAnimation = new ValueAnimation() - ..addListener(() { - AnimatedValue offset = _toOffsetAnimation.variable; - _setScrollOffset(offset.value); - }); + _animation = new SimulationStepper(_setScrollOffset); } - AnimatedSimulation _toEndAnimation; // See _startToEndAnimation() - ValueAnimation _toOffsetAnimation; // Started by scrollTo() + SimulationStepper _animation; double _scrollOffset = 0.0; double get scrollOffset => _scrollOffset; @@ -106,23 +100,10 @@ abstract class ScrollableState extends State { Widget buildContent(BuildContext context); - Future _startToOffsetAnimation(double newScrollOffset, Duration duration, Curve curve) { - _stopAnimations(); - _toOffsetAnimation - ..variable = new AnimatedValue(scrollOffset, - end: newScrollOffset, - curve: curve - ) - ..progress = 0.0 - ..duration = duration; - return _toOffsetAnimation.play(); - } - - void _stopAnimations() { - if (_toOffsetAnimation.isAnimating) - _toOffsetAnimation.stop(); - if (_toEndAnimation.isAnimating) - _toEndAnimation.stop(); + Future _animateTo(double newScrollOffset, Duration duration, Curve curve) { + _animation.stop(); + _animation.value = scrollOffset; + return _animation.animateTo(newScrollOffset, duration: duration, curve: curve); } bool _scrollOffsetIsInBounds(double offset) { @@ -165,16 +146,16 @@ abstract class ScrollableState extends State { } Future _startToEndAnimation({ double velocity }) { - _stopAnimations(); + _animation.stop(); Simulation simulation = _createSnapSimulation(velocity) ?? _createFlingSimulation(velocity ?? 0.0); if (simulation == null) return new Future.value(); - return _toEndAnimation.start(simulation); + return _animation.animateWith(simulation); } void dispose() { - _stopAnimations(); + _animation.stop(); super.dispose(); } @@ -193,12 +174,12 @@ abstract class ScrollableState extends State { return new Future.value(); if (duration == null) { - _stopAnimations(); + _animation.stop(); _setScrollOffset(newScrollOffset); return new Future.value(); } - return _startToOffsetAnimation(newScrollOffset, duration, curve); + return _animateTo(newScrollOffset, duration, curve); } Future scrollBy(double scrollDelta, { Duration duration, Curve curve }) { @@ -209,7 +190,7 @@ abstract class ScrollableState extends State { Future fling(Offset velocity) { if (velocity != Offset.zero) return _startToEndAnimation(velocity: _scrollVelocity(velocity)); - if (!_toEndAnimation.isAnimating && (_toOffsetAnimation == null || !_toOffsetAnimation.isAnimating)) + if (!_animation.isAnimating) return settleScrollOffset(); return new Future.value(); } @@ -226,7 +207,7 @@ abstract class ScrollableState extends State { } void _handlePointerDown(_) { - _stopAnimations(); + _animation.stop(); } void _handleDragUpdate(double delta) { @@ -337,7 +318,7 @@ class ScrollableViewportState extends ScrollableState { }); } void _updateScrollBehaviour() { - // if you don't call this from build() or syncConstructorArguments(), you must call it from setState(). + // if you don't call this from build(), you must call it from setState(). scrollTo(scrollBehavior.updateExtents( contentExtent: _childSize, containerExtent: _viewportSize, diff --git a/packages/flutter/lib/src/widgets/snack_bar.dart b/packages/flutter/lib/src/widgets/snack_bar.dart index 82ff38feb3..757d9a683a 100644 --- a/packages/flutter/lib/src/widgets/snack_bar.dart +++ b/packages/flutter/lib/src/widgets/snack_bar.dart @@ -2,10 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - import 'package:sky/animation.dart'; -import 'package:sky/painting.dart'; +import 'package:sky/gestures.dart'; import 'package:sky/material.dart'; +import 'package:sky/painting.dart'; import 'package:sky/src/widgets/animated_component.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/framework.dart'; @@ -28,7 +28,7 @@ class SnackBarAction extends StatelessComponent { } final String label; - final Function onPressed; + final GestureTapCallback onPressed; Widget build(BuildContext) { return new GestureDetector( @@ -49,7 +49,7 @@ class SnackBar extends AnimatedComponent { this.actions, bool showing, this.onDismissed - }) : super(key: key, direction: showing ? Direction.forward : Direction.reverse, duration: _kSlideInDuration) { + }) : super(key: key, direction: showing ? AnimationDirection.forward : AnimationDirection.reverse, duration: _kSlideInDuration) { assert(content != null); } diff --git a/packages/flutter/lib/src/widgets/statistics_overlay.dart b/packages/flutter/lib/src/widgets/statistics_overlay.dart new file mode 100644 index 0000000000..e3d62fc556 --- /dev/null +++ b/packages/flutter/lib/src/widgets/statistics_overlay.dart @@ -0,0 +1,60 @@ +// 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:sky/src/widgets/framework.dart'; +import 'package:sky/src/rendering/statistics_box.dart'; + +/// The options that control whether the statistics overlay displays certain +/// aspects of the compositor +enum StatisticsOption { + /// Display the frame time and FPS of the last frame rendered. This field is + /// updated every frame. + /// + /// This is the time spent by the rasterizer as it tries + /// to convert the layer tree obtained from the widgets into OpenGL commands + /// and tries to flush them onto the screen. When the total time taken by this + /// step exceeds the frame slice, a frame is lost. + displayRasterizerStatistics, + /// Display the rasterizer frame times as they change over a set period of + /// time in the form of a graph. The y axis of the graph denotes the total + /// time spent by the rasterizer as a fraction of the total frame slice. When + /// the bar turns red, a frame is lost. + visualizeRasterizerStatistics, + /// Display the frame time and FPS at which the interface can construct a + /// layer tree for the rasterizer (whose behavior is described above) to + /// consume. + /// + /// This involves all layout, animations, etc. When the total time taken by + /// this step exceeds the frame slice, a frame is lost. + displayEngineStatistics, + /// Display the engine frame times as they change over a set period of time + /// in the form of a graph. The y axis of the graph denotes the total time + /// spent by the eninge as a fraction of the total frame slice. When the bar + /// turns red, a frame is lost. + visualizeEngineStatistics, +} + +class StatisticsOverlay extends LeafRenderObjectWidget { + + /// Create a statistics overlay that only displays specific statistics. The + /// mask is created by shifting 1 by the index of the specific StatisticOption + /// to enable. + StatisticsOverlay({ this.optionsMask, Key key }) : super(key: key); + + /// Create a statistics overaly that displays all available statistics + StatisticsOverlay.allEnabled({ Key key }) : super(key: key), optionsMask = ( + 1 << StatisticsOption.displayRasterizerStatistics.index | + 1 << StatisticsOption.visualizeRasterizerStatistics.index | + 1 << StatisticsOption.displayEngineStatistics.index | + 1 << StatisticsOption.visualizeEngineStatistics.index + ); + + final int optionsMask; + + StatisticsBox createRenderObject() => new StatisticsBox(optionsMask: optionsMask); + + void updateRenderObject(StatisticsBox renderObject, RenderObjectWidget oldWidget) { + renderObject.optionsMask = optionsMask; + } +} diff --git a/packages/flutter/lib/src/widgets/tabs.dart b/packages/flutter/lib/src/widgets/tabs.dart index d65957c433..bcee5e115e 100644 --- a/packages/flutter/lib/src/widgets/tabs.dart +++ b/packages/flutter/lib/src/widgets/tabs.dart @@ -7,12 +7,12 @@ import 'dart:sky' as sky; import 'package:newton/newton.dart'; import 'package:sky/animation.dart'; +import 'package:sky/gestures.dart'; +import 'package:sky/material.dart'; import 'package:sky/painting.dart'; import 'package:sky/rendering.dart'; -import 'package:sky/material.dart'; import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/framework.dart'; -import 'package:sky/src/widgets/gesture_detector.dart'; import 'package:sky/src/widgets/icon.dart'; import 'package:sky/src/widgets/ink_well.dart'; import 'package:sky/src/widgets/scrollable.dart'; @@ -307,6 +307,7 @@ class TabLabel { class Tab extends StatelessComponent { Tab({ Key key, + this.onSelected, this.label, this.color, this.selected: false, @@ -315,6 +316,7 @@ class Tab extends StatelessComponent { assert(label.text != null || label.icon != null); } + final GestureTapCallback onSelected; final TabLabel label; final Color color; final bool selected; @@ -359,7 +361,10 @@ class Tab extends StatelessComponent { padding: _kTabLabelPadding ); - return new InkWell(child: centeredLabel); + return new InkWell( + onTap: onSelected, + child: centeredLabel + ); } } @@ -403,16 +408,16 @@ class TabBar extends Scrollable { class TabBarState extends ScrollableState { void initState() { super.initState(); - _indicatorAnimation = new ValueAnimation() + _indicatorAnimation = new ValuePerformance() ..duration = _kTabBarScroll - ..variable = new AnimatedRect(null, curve: ease); + ..variable = new AnimatedRectValue(null, curve: ease); scrollBehavior.isScrollable = config.isScrollable; } Size _tabBarSize; Size _viewportSize = Size.zero; List _tabWidths; - ValueAnimation _indicatorAnimation; + ValuePerformance _indicatorAnimation; void didUpdateConfig(TabBar oldConfig) { super.didUpdateConfig(oldConfig); @@ -420,7 +425,7 @@ class TabBarState extends ScrollableState { scrollTo(0.0); } - AnimatedRect get _indicatorRect => _indicatorAnimation.variable; + AnimatedRectValue get _indicatorRect => _indicatorAnimation.variable; void _startIndicatorAnimation(int fromTabIndex, int toTabIndex) { _indicatorRect @@ -458,7 +463,7 @@ class TabBarState extends ScrollableState { .clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset); } - void _handleTap(int tabIndex) { + void _handleTabSelected(int tabIndex) { if (tabIndex != config.selectedIndex) { if (_tabWidths != null) { if (config.isScrollable) @@ -471,14 +476,12 @@ class TabBarState extends ScrollableState { } Widget _toTab(TabLabel label, int tabIndex, Color color, Color selectedColor) { - return new GestureDetector( - onTap: () => _handleTap(tabIndex), - child: new Tab( - label: label, - color: color, - selected: tabIndex == config.selectedIndex, - selectedColor: selectedColor - ) + return new Tab( + onSelected: () => _handleTabSelected(tabIndex), + label: label, + color: color, + selected: tabIndex == config.selectedIndex, + selectedColor: selectedColor ); } diff --git a/packages/flutter/lib/src/widgets/transitions.dart b/packages/flutter/lib/src/widgets/transitions.dart index be14f5d30d..4f00c3694d 100644 --- a/packages/flutter/lib/src/widgets/transitions.dart +++ b/packages/flutter/lib/src/widgets/transitions.dart @@ -7,7 +7,7 @@ import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/framework.dart'; import 'package:vector_math/vector_math_64.dart'; -export 'package:sky/animation.dart' show Direction; +export 'package:sky/animation.dart' show AnimationDirection; abstract class TransitionComponent extends StatefulComponent { TransitionComponent({ @@ -17,7 +17,7 @@ abstract class TransitionComponent extends StatefulComponent { assert(performance != null); } - final WatchableAnimationPerformance performance; + final PerformanceView performance; Widget build(BuildContext context); @@ -57,7 +57,7 @@ abstract class TransitionWithChild extends TransitionComponent { TransitionWithChild({ Key key, this.child, - WatchableAnimationPerformance performance + PerformanceView performance }) : super(key: key, performance: performance); final Widget child; @@ -71,7 +71,7 @@ class SlideTransition extends TransitionWithChild { SlideTransition({ Key key, this.position, - WatchableAnimationPerformance performance, + PerformanceView performance, Widget child }) : super(key: key, performance: performance, @@ -91,7 +91,7 @@ class FadeTransition extends TransitionWithChild { FadeTransition({ Key key, this.opacity, - WatchableAnimationPerformance performance, + PerformanceView performance, Widget child }) : super(key: key, performance: performance, @@ -109,7 +109,7 @@ class ColorTransition extends TransitionWithChild { ColorTransition({ Key key, this.color, - WatchableAnimationPerformance performance, + PerformanceView performance, Widget child }) : super(key: key, performance: performance, @@ -131,7 +131,7 @@ class SquashTransition extends TransitionWithChild { Key key, this.width, this.height, - WatchableAnimationPerformance performance, + PerformanceView performance, Widget child }) : super(key: key, performance: performance, @@ -156,7 +156,7 @@ class BuilderTransition extends TransitionComponent { Key key, this.variables, this.builder, - WatchableAnimationPerformance performance + PerformanceView performance }) : super(key: key, performance: performance); diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index 3e7f257d57..df63f53c19 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -6,6 +6,7 @@ library widgets; export 'src/widgets/animated_component.dart'; +export 'src/widgets/animated_container.dart'; export 'src/widgets/app.dart'; export 'src/widgets/basic.dart'; export 'src/widgets/binding.dart'; @@ -43,6 +44,7 @@ export 'src/widgets/radio.dart'; export 'src/widgets/raised_button.dart'; export 'src/widgets/scaffold.dart'; export 'src/widgets/scrollable.dart'; +export 'src/widgets/statistics_overlay.dart'; export 'src/widgets/snack_bar.dart'; export 'src/widgets/switch.dart'; export 'src/widgets/tabs.dart'; diff --git a/packages/flutter_sprites/lib/node.dart b/packages/flutter_sprites/lib/node.dart index 9d51195a92..0896b9c123 100644 --- a/packages/flutter_sprites/lib/node.dart +++ b/packages/flutter_sprites/lib/node.dart @@ -131,6 +131,19 @@ class Node { void set rotation(double rotation) { assert(rotation != null); + + if (_physicsBody != null && parent is PhysicsNode) { + PhysicsNode physicsNode = parent; + physicsNode._updateRotation(this.physicsBody, rotation); + return; + } + + _rotation = rotation; + invalidateTransformMatrix(); + } + + void _setRotationFromPhysics(double rotation) { + assert(rotation != null); _rotation = rotation; invalidateTransformMatrix(); } @@ -142,6 +155,19 @@ class Node { void set position(Point position) { assert(position != null); + + if (_physicsBody != null && parent is PhysicsNode) { + PhysicsNode physicsNode = parent; + physicsNode._updatePosition(this.physicsBody, position); + return; + } + + _position = position; + invalidateTransformMatrix(); + } + + void _setPositionFromPhysics(Point position) { + assert(position != null); _position = position; invalidateTransformMatrix(); } @@ -609,4 +635,24 @@ class Node { bool handleEvent(SpriteBoxEvent event) { return false; } + + // Physics + + PhysicsBody _physicsBody; + + PhysicsBody get physicsBody => _physicsBody; + + set physicsBody(PhysicsBody physicsBody) { + if (parent != null) { + assert(parent is PhysicsNode); + + if (physicsBody == null) { + physicsBody._detach(); + } else { + physicsBody._attach(parent, this); + } + } + + _physicsBody = physicsBody; + } } diff --git a/packages/flutter_sprites/lib/physics_body.dart b/packages/flutter_sprites/lib/physics_body.dart new file mode 100644 index 0000000000..e9446b65e1 --- /dev/null +++ b/packages/flutter_sprites/lib/physics_body.dart @@ -0,0 +1,138 @@ +part of skysprites; + +enum PhysicsBodyType { + static, + dynamic +} + +class PhysicsBody { + PhysicsBody(this.shape, { + this.tag: null, + this.type: PhysicsBodyType.dynamic, + this.density: 1.0, + this.friction: 0.0, + this.restitution: 0.0, + this.isSensor: false, + this.linearVelocity: Offset.zero, + this.angularVelocity: 0.0, + this.linearDampening: 0.0, + this.angularDampening: 0.0, + this.allowSleep: true, + this.awake: true, + this.fixedRotation: false, + this.bullet: false, + this.active: true, + this.gravityScale: 1.0 + }); + + Object tag; + + PhysicsShape shape; + + PhysicsBodyType type; + + double density; + double friction; + double restitution; + bool isSensor; + + Offset linearVelocity; + double angularVelocity; + + double linearDampening; + + double angularDampening; + + bool allowSleep; + + bool awake; + + bool fixedRotation; + + bool bullet; + + bool active; + + double gravityScale; + + PhysicsNode _physicsNode; + Node _node; + + box2d.Body _body; + + bool _attached = false; + + void _attach(PhysicsNode physicsNode, Node node) { + assert(_attached == false); + + // Create BodyDef + box2d.BodyDef bodyDef = new box2d.BodyDef(); + bodyDef.linearVelocity = new Vector2(linearVelocity.dx, linearVelocity.dy); + bodyDef.angularVelocity = angularVelocity; + bodyDef.linearDamping = linearDampening; + bodyDef.angularDamping = angularDampening; + bodyDef.allowSleep = allowSleep; + bodyDef.awake = awake; + bodyDef.fixedRotation = fixedRotation; + bodyDef.bullet = bullet; + bodyDef.active = active; + bodyDef.gravityScale = gravityScale; + if (type == PhysicsBodyType.dynamic) + bodyDef.type = box2d.BodyType.DYNAMIC; + else + bodyDef.type = box2d.BodyType.STATIC; + + double conv = physicsNode.b2WorldToNodeConversionFactor; + bodyDef.position = new Vector2(node.position.x / conv, node.position.y / conv); + bodyDef.angle = radians(node.rotation); + + // Create Body + _body = physicsNode.b2World.createBody(bodyDef); + + // Create FixtureDef + box2d.FixtureDef fixtureDef = new box2d.FixtureDef(); + fixtureDef.friction = friction; + fixtureDef.restitution = restitution; + fixtureDef.density = density; + fixtureDef.isSensor = isSensor; + + // Get shapes + List b2Shapes = []; + List physicsShapes = []; + _addB2Shapes(physicsNode, shape, b2Shapes, physicsShapes); + + // Create fixtures + for (int i = 0; i < b2Shapes.length; i++) { + box2d.Shape b2Shape = b2Shapes[i]; + PhysicsShape physicsShape = physicsShapes[i]; + + fixtureDef.shape = b2Shape; + box2d.Fixture fixture = _body.createFixtureFromFixtureDef(fixtureDef); + fixture.userData = physicsShape; + } + _body.userData = this; + + _physicsNode = physicsNode; + _node = node; + + _attached = true; + } + + void _detach() { + if (_attached) { + _physicsNode._bodiesScheduledForDestruction.add(_body); + _attached = false; + } + } + + void _addB2Shapes(PhysicsNode physicsNode, PhysicsShape shape, List b2Shapes, List physicsShapes) { + if (shape is PhysicsShapeGroup) { + for (PhysicsShape child in shape.shapes) { + _addB2Shapes(physicsNode, child, b2Shapes, physicsShapes); + } + } else { + b2Shapes.add(shape.getB2Shape(physicsNode)); + physicsShapes.add(shape); + } + } +} diff --git a/packages/flutter_sprites/lib/physics_node.dart b/packages/flutter_sprites/lib/physics_node.dart new file mode 100644 index 0000000000..12ea4f8531 --- /dev/null +++ b/packages/flutter_sprites/lib/physics_node.dart @@ -0,0 +1,303 @@ +part of skysprites; + +enum PhysicsContactType { + preSolve, + postSolve, + begin, + end +} + +typedef void PhysicsContactCallback(PhysicsContactType type, PhysicsContact contact); + +class PhysicsNode extends Node { + PhysicsNode(Offset gravity) { + b2World = new box2d.World.withGravity( + new Vector2( + gravity.dx / b2WorldToNodeConversionFactor, + gravity.dy / b2WorldToNodeConversionFactor)); + _init(); + } + + PhysicsNode.fromB2World(this.b2World, this.b2WorldToNodeConversionFactor) { + _init(); + } + + void _init() { + _contactHandler = new _ContactHandler(this); + b2World.setContactListener(_contactHandler); + } + + box2d.World b2World; + + _ContactHandler _contactHandler; + + List _bodiesScheduledForDestruction = []; + + double b2WorldToNodeConversionFactor = 500.0; + + Offset get gravity { + Vector2 g = b2World.getGravity(); + return new Offset(g.x, g.y); + } + + set gravity(Offset gravity) { + // Convert from points/s^2 to m/s^2 + b2World.setGravity(new Vector2(gravity.dx / b2WorldToNodeConversionFactor, + gravity.dy / b2WorldToNodeConversionFactor)); + } + + bool get allowSleep => b2World.isAllowSleep(); + + set allowSleep(bool allowSleep) { + b2World.setAllowSleep(allowSleep); + } + + bool get subStepping => b2World.isSubStepping(); + + set subStepping(bool subStepping) { + b2World.setSubStepping(subStepping); + } + + void _stepPhysics(double dt) { + // Remove bodies that were marked for destruction during the update phase + _removeBodiesScheduledForDestruction(); + + // Calculate a step in the simulation + b2World.stepDt(dt, 10, 10); + + // Iterate over the bodies + for (box2d.Body b2Body = b2World.bodyList; b2Body != null; b2Body = b2Body.getNext()) { + // Update visual position and rotation + PhysicsBody body = b2Body.userData; + body._node._setPositionFromPhysics(new Point( + b2Body.position.x * b2WorldToNodeConversionFactor, + b2Body.position.y * b2WorldToNodeConversionFactor + )); + + body._node._setRotationFromPhysics(degrees(b2Body.getAngle())); + } + + // Remove bodies that were marked for destruction during the simulation + _removeBodiesScheduledForDestruction(); + } + + void _removeBodiesScheduledForDestruction() { + for (box2d.Body b2Body in _bodiesScheduledForDestruction) { + b2World.destroyBody(b2Body); + } + _bodiesScheduledForDestruction.clear(); + } + + void _updatePosition(PhysicsBody body, Point position) { + Vector2 newPos = new Vector2( + position.x / b2WorldToNodeConversionFactor, + position.y / b2WorldToNodeConversionFactor + ); + double angle = body._body.getAngle(); + body._body.setTransform(newPos, angle); + body._body.setAwake(true); + } + + void _updateRotation(PhysicsBody body, double rotation) { + Vector2 pos = body._body.position; + double newAngle = radians(rotation); + body._body.setTransform(pos, newAngle); + body._body.setAwake(true); + } + + void addChild(Node node) { + super.addChild(node); + if (node.physicsBody != null) { + node.physicsBody._attach(this, node); + } + } + + void removeChild(Node node) { + super.removeChild(node); + if (node.physicsBody != null) { + node.physicsBody._detach(); + } + } + + void addContactCallback(PhysicsContactCallback callback, Object tagA, Object tagB, [PhysicsContactType type]) { + _contactHandler.addContactCallback(callback, tagA, tagB, type); + } + + void paint(PaintingCanvas canvas) { + super.paint(canvas); + paintDebug(canvas); + } + + void paintDebug(PaintingCanvas canvas) { + Paint shapePaint = new Paint(); + shapePaint.setStyle(sky.PaintingStyle.stroke); + shapePaint.strokeWidth = 1.0; + + for (box2d.Body body = b2World.bodyList; body != null; body = body.getNext()) { + canvas.save(); + + Point point = new Point( + body.position.x * b2WorldToNodeConversionFactor, + body.position.y * b2WorldToNodeConversionFactor); + + canvas.translate(point.x, point.y); + canvas.rotate(body.getAngle()); + + if (body.getType() == box2d.BodyType.DYNAMIC) { + if (body.isAwake()) + shapePaint.color = new Color(0xff00ff00); + else + shapePaint.color = new Color(0xff666666); + } + else if (body.getType() == box2d.BodyType.STATIC) + shapePaint.color = new Color(0xffff0000); + else if (body.getType() == box2d.BodyType.KINEMATIC) + shapePaint.color = new Color(0xffff9900); + + for (box2d.Fixture fixture = body.getFixtureList(); fixture != null; fixture = fixture.getNext()) { + box2d.Shape shape = fixture.getShape(); + + if (shape.shapeType == box2d.ShapeType.CIRCLE) { + box2d.CircleShape circle = shape; + Point cp = new Point( + circle.p.x * b2WorldToNodeConversionFactor, + circle.p.y * b2WorldToNodeConversionFactor + ); + double radius = circle.radius * b2WorldToNodeConversionFactor; + canvas.drawCircle(cp, radius, shapePaint); + } else if (shape.shapeType == box2d.ShapeType.POLYGON) { + box2d.PolygonShape poly = shape; + List points = []; + for (int i = 0; i < poly.getVertexCount(); i++) { + Vector2 vertex = poly.getVertex(i); + Point pt = new Point( + vertex.x * b2WorldToNodeConversionFactor, + vertex.y * b2WorldToNodeConversionFactor + ); + points.add(pt); + } + if (points.length >= 2) { + for (int i = 0; i < points.length; i++) { + canvas.drawLine(points[i], points[(i + 1) % points.length], shapePaint); + } + } + } + } + canvas.restore(); + } + } +} + +class PhysicsContact { + PhysicsContact( + this.nodeA, + this.nodeB, + this.shapeA, + this.shapeB, + this.isTouching, + this.isEnabled + ); + + final Node nodeA; + final Node nodeB; + final PhysicsShape shapeA; + final PhysicsShape shapeB; + final isTouching; + bool isEnabled; +} + +class _ContactCallbackInfo { + _ContactCallbackInfo(this.callback, this.tagA, this.tagB, this.type); + + PhysicsContactCallback callback; + Object tagA; + Object tagB; + PhysicsContactType type; +} + +class _ContactHandler extends box2d.ContactListener { + _ContactHandler(this.physicsNode); + + PhysicsNode physicsNode; + + List<_ContactCallbackInfo> callbackInfos = []; + + void addContactCallback(PhysicsContactCallback callback, Object tagA, Object tagB, PhysicsContactType type) { + callbackInfos.add(new _ContactCallbackInfo(callback, tagA, tagB, type)); + } + + void handleCallback(PhysicsContactType type, box2d.Contact b2Contact, box2d.Manifold oldManifold, box2d.ContactImpulse impulse) { + // Get info about the contact + PhysicsBody bodyA = b2Contact.fixtureA.getBody().userData; + PhysicsBody bodyB = b2Contact.fixtureB.getBody().userData; + box2d.Fixture fixtureA = b2Contact.fixtureA; + box2d.Fixture fixtureB = b2Contact.fixtureB; + + // Match callback with added callbacks + for (_ContactCallbackInfo info in callbackInfos) { + // Check that type is matching + if (info.type != null && info.type != type) + continue; + + // Check if there is a match + bool matchA = (info.tagA == null) || info.tagA == bodyA.tag; + bool matchB = (info.tagB == null) || info.tagB == bodyB.tag; + + bool match = (matchA && matchB); + if (!match) { + // Check if there is a match if we swap a & b + bool matchA = (info.tagA == null) || info.tagA == bodyB.tag; + bool matchB = (info.tagB == null) || info.tagB == bodyA.tag; + + match = (matchA && matchB); + if (match) { + // Swap a & b + PhysicsBody tempBody = bodyA; + bodyA = bodyB; + bodyB = tempBody; + + box2d.Fixture tempFixture = fixtureA; + fixtureA = fixtureB; + fixtureB = tempFixture; + } + } + + if (match) { + // We have contact and a matched callback, setup contact info + PhysicsContact contact = new PhysicsContact( + bodyA._node, + bodyB._node, + fixtureA.userData, + fixtureB.userData, + b2Contact.isTouching(), + b2Contact.isEnabled() + ); + + if (type == PhysicsContactType.postSolve) { + + } + + // Make callback + info.callback(type, contact); + + // Update Box2D contact + b2Contact.setEnabled(contact.isEnabled); + } + } + } + + void beginContact(box2d.Contact contact) { + handleCallback(PhysicsContactType.begin, contact, null, null); + } + + void endContact(box2d.Contact contact) { + handleCallback(PhysicsContactType.end, contact, null, null); + } + + void preSolve(box2d.Contact contact, box2d.Manifold oldManifold) { + handleCallback(PhysicsContactType.preSolve, contact, oldManifold, null); + } + void postSolve(box2d.Contact contact, box2d.ContactImpulse impulse) { + handleCallback(PhysicsContactType.postSolve, contact, null, impulse); + } +} diff --git a/packages/flutter_sprites/lib/physics_shape.dart b/packages/flutter_sprites/lib/physics_shape.dart new file mode 100644 index 0000000000..657b85d1ec --- /dev/null +++ b/packages/flutter_sprites/lib/physics_shape.dart @@ -0,0 +1,65 @@ +part of skysprites; + +abstract class PhysicsShape { + + box2d.Shape _b2Shape; + + Object userObject; + + box2d.Shape getB2Shape(PhysicsNode node) { + if (_b2Shape == null) { + _b2Shape = _createB2Shape(node); + } + return _b2Shape; + } + + box2d.Shape _createB2Shape(PhysicsNode node); +} + +class PhysicsShapeCircle extends PhysicsShape { + PhysicsShapeCircle(this.point, this.radius); + + final Point point; + final double radius; + + box2d.Shape _createB2Shape(PhysicsNode node) { + box2d.CircleShape shape = new box2d.CircleShape(); + shape.p.x = point.x / node.b2WorldToNodeConversionFactor; + shape.p.y = point.y / node.b2WorldToNodeConversionFactor; + shape.radius = radius / node.b2WorldToNodeConversionFactor; + return shape; + } +} + +class PhysicsShapePolygon extends PhysicsShape { + + PhysicsShapePolygon(this.points); + + final List points; + + box2d.Shape _createB2Shape(PhysicsNode node) { + List vectors = []; + for (Point point in points) { + Vector2 vec = new Vector2( + point.x / node.b2WorldToNodeConversionFactor, + point.y / node.b2WorldToNodeConversionFactor + ); + vectors.add(vec); + } + + box2d.PolygonShape shape = new box2d.PolygonShape(); + shape.set(vectors, vectors.length); + return shape; + } +} + +class PhysicsShapeGroup extends PhysicsShape { + + PhysicsShapeGroup(this.shapes); + + final List shapes; + + box2d.Shape _createB2Shape(PhysicsNode node) { + return null; + } +} diff --git a/packages/flutter_sprites/lib/skysprites.dart b/packages/flutter_sprites/lib/skysprites.dart index 08fd7cc2df..ad662c65d0 100644 --- a/packages/flutter_sprites/lib/skysprites.dart +++ b/packages/flutter_sprites/lib/skysprites.dart @@ -10,6 +10,7 @@ import 'dart:math' as math; import 'dart:typed_data'; import 'dart:sky' as sky; +import 'package:box2d/box2d.dart' as box2d; import 'package:mojo/core.dart'; import 'package:sky_services/media/media.mojom.dart'; import 'package:sky/animation.dart'; @@ -31,6 +32,9 @@ part 'node.dart'; part 'node3d.dart'; part 'node_with_size.dart'; part 'particle_system.dart'; +part 'physics_body.dart'; +part 'physics_node.dart'; +part 'physics_shape.dart'; part 'sound.dart'; part 'sound_manager.dart'; part 'sprite.dart'; diff --git a/packages/flutter_sprites/lib/sprite_box.dart b/packages/flutter_sprites/lib/sprite_box.dart index aeecfd0168..90f88c3b0e 100644 --- a/packages/flutter_sprites/lib/sprite_box.dart +++ b/packages/flutter_sprites/lib/sprite_box.dart @@ -47,7 +47,7 @@ class SpriteBox extends RenderBox { } // Tracking of frame rate and updates - double _lastTimeStamp; + Duration _lastTimeStamp; double _frameRate = 0.0; double get frameRate => _frameRate; @@ -76,6 +76,8 @@ class SpriteBox extends RenderBox { List _constrainedNodes; + List _physicsNodes; + Rect _visibleArea; Rect get visibleArea { @@ -84,6 +86,8 @@ class SpriteBox extends RenderBox { return _visibleArea; } + bool _initialized = false; + // Setup /// Creates a new SpriteBox with a node as its content, by default uses letterboxing. @@ -134,19 +138,22 @@ class SpriteBox extends RenderBox { size = constraints.biggest; _invalidateTransformMatrix(); _callSpriteBoxPerformedLayout(_rootNode); + _initialized = true; } // Adding and removing nodes - _registerNode(Node node) { + void _registerNode(Node node) { _actionControllers = null; _eventTargets = null; + _physicsNodes = null; if (node == null || node.constraints != null) _constrainedNodes = null; } - _deregisterNode(Node node) { + void _deregisterNode(Node node) { _actionControllers = null; _eventTargets = null; + _physicsNodes = null; if (node == null || node.constraints != null) _constrainedNodes = null; } @@ -345,21 +352,25 @@ class SpriteBox extends RenderBox { scheduler.requestAnimationFrame(_tick); } - void _tick(double timeStamp) { + void _tick(Duration timeStamp) { if (!attached) return; // Calculate delta and frame rate - if (_lastTimeStamp == null) _lastTimeStamp = timeStamp; - double delta = (timeStamp - _lastTimeStamp) / 1000; + if (_lastTimeStamp == null) + _lastTimeStamp = timeStamp; + double delta = (timeStamp - _lastTimeStamp).inMicroseconds.toDouble() / Duration.MICROSECONDS_PER_SECOND; _lastTimeStamp = timeStamp; _frameRate = 1.0/delta; - _callConstraintsPreUpdate(delta); - _runActions(delta); - _callUpdate(_rootNode, delta); - _callConstraintsConstrain(delta); + if (_initialized) { + _callConstraintsPreUpdate(delta); + _runActions(delta); + _callUpdate(_rootNode, delta); + _callStepPhysics(delta); + _callConstraintsConstrain(delta); + } // Schedule next update _scheduleTick(); @@ -370,20 +381,26 @@ class SpriteBox extends RenderBox { void _runActions(double dt) { if (_actionControllers == null) { - _actionControllers = []; - _addActionControllers(_rootNode, _actionControllers); + _rebuildActionControllersAndPhysicsNodes(); } for (ActionController actions in _actionControllers) { actions.step(dt); } } - void _addActionControllers(Node node, List controllers) { - if (node._actions != null) controllers.add(node._actions); + void _rebuildActionControllersAndPhysicsNodes() { + _actionControllers = []; + _physicsNodes = []; + _addActionControllersAndPhysicsNodes(_rootNode); + } + + void _addActionControllersAndPhysicsNodes(Node node) { + if (node._actions != null) _actionControllers.add(node._actions); + if (node is PhysicsNode) _physicsNodes.add(node); for (int i = node.children.length - 1; i >= 0; i--) { Node child = node.children[i]; - _addActionControllers(child, controllers); + _addActionControllersAndPhysicsNodes(child); } } @@ -397,6 +414,15 @@ class SpriteBox extends RenderBox { } } + void _callStepPhysics(double dt) { + if (_physicsNodes == null) + _rebuildActionControllersAndPhysicsNodes(); + + for (PhysicsNode physicsNode in _physicsNodes) { + physicsNode._stepPhysics(dt); + } + } + void _callConstraintsPreUpdate(double dt) { if (_constrainedNodes == null) { _constrainedNodes = []; diff --git a/packages/flutter_sprites/pubspec.yaml b/packages/flutter_sprites/pubspec.yaml index a557cc8dd8..b2fbf54410 100644 --- a/packages/flutter_sprites/pubspec.yaml +++ b/packages/flutter_sprites/pubspec.yaml @@ -6,6 +6,7 @@ homepage: http://flutter.io dependencies: sky: ">=0.0.36 < 0.1.0" sky_tools: ">=0.0.10 < 0.1.0" + box2d: any dependency_overrides: sky: path: ../sky/packages/sky diff --git a/packages/unit/test/animation/scheduler_test.dart b/packages/unit/test/animation/scheduler_test.dart index 380de02bb3..62f9379c3d 100644 --- a/packages/unit/test/animation/scheduler_test.dart +++ b/packages/unit/test/animation/scheduler_test.dart @@ -8,18 +8,18 @@ void main() { bool firstCallbackRan = false; bool secondCallbackRan = false; - void firstCallback(double timeStamp) { + void firstCallback(Duration timeStamp) { expect(firstCallbackRan, isFalse); expect(secondCallbackRan, isFalse); - expect(timeStamp, equals(16.0)); + expect(timeStamp.inMilliseconds, equals(16)); firstCallbackRan = true; scheduler.cancelAnimationFrame(secondId); } - void secondCallback(double timeStamp) { + void secondCallback(Duration timeStamp) { expect(firstCallbackRan, isTrue); expect(secondCallbackRan, isFalse); - expect(timeStamp, equals(16.0)); + expect(timeStamp.inMilliseconds, equals(16)); secondCallbackRan = true; } diff --git a/packages/unit/test/widget/animated_container_test.dart b/packages/unit/test/widget/animated_container_test.dart new file mode 100644 index 0000000000..b98cee45e9 --- /dev/null +++ b/packages/unit/test/widget/animated_container_test.dart @@ -0,0 +1,48 @@ +import 'package:sky/rendering.dart'; +import 'package:sky/widgets.dart'; +import 'package:test/test.dart'; + +import 'widget_tester.dart'; + +void main() { + test('AnimatedContainer control test', () { + testWidgets((WidgetTester tester) { + GlobalKey key = new GlobalKey(); + + BoxDecoration decorationA = new BoxDecoration( + backgroundColor: new Color(0xFF00FF00) + ); + + BoxDecoration decorationB = new BoxDecoration( + backgroundColor: new Color(0xFF0000FF) + ); + + tester.pumpWidget( + new AnimatedContainer( + key: key, + duration: const Duration(milliseconds: 200), + decoration: decorationA + ) + ); + + RenderDecoratedBox box = key.currentState.context.findRenderObject(); + expect(box.decoration.backgroundColor, equals(decorationA.backgroundColor)); + + tester.pumpWidget( + new AnimatedContainer( + key: key, + duration: const Duration(milliseconds: 200), + decoration: decorationB + ) + ); + + expect(key.currentState.context.findRenderObject(), equals(box)); + expect(box.decoration.backgroundColor, equals(decorationA.backgroundColor)); + + tester.pump(const Duration(seconds: 1)); + + expect(box.decoration.backgroundColor, equals(decorationB.backgroundColor)); + + }); + }); +} diff --git a/packages/unit/test/widget/dismissable_test.dart b/packages/unit/test/widget/dismissable_test.dart index 8d436dd1d2..bb4086e65f 100644 --- a/packages/unit/test/widget/dismissable_test.dart +++ b/packages/unit/test/widget/dismissable_test.dart @@ -1,3 +1,4 @@ +import 'package:sky/rendering.dart'; import 'package:sky/widgets.dart'; import 'package:test/test.dart'; @@ -44,13 +45,11 @@ Widget widgetBuilder() { ); } -void dismissItem(WidgetTester tester, int item, { DismissDirection gestureDirection }) { +void dismissElement(WidgetTester tester, Element itemElement, { DismissDirection gestureDirection }) { + assert(itemElement != null); assert(gestureDirection != DismissDirection.horizontal); assert(gestureDirection != DismissDirection.vertical); - Element itemElement = tester.findText(item.toString()); - expect(itemElement, isNotNull); - Point downLocation; Point upLocation; switch(gestureDirection) { @@ -84,12 +83,35 @@ void dismissItem(WidgetTester tester, int item, { DismissDirection gestureDirect tester.dispatchEvent(pointer.down(downLocation), downLocation); tester.dispatchEvent(pointer.move(upLocation), downLocation); tester.dispatchEvent(pointer.up(), downLocation); +} + +void dismissItem(WidgetTester tester, int item, { DismissDirection gestureDirection }) { + assert(gestureDirection != DismissDirection.horizontal); + assert(gestureDirection != DismissDirection.vertical); + + Element itemElement = tester.findText(item.toString()); + expect(itemElement, isNotNull); + + dismissElement(tester, itemElement, gestureDirection: gestureDirection); tester.pumpWidget(widgetBuilder()); // start the resize animation tester.pumpWidget(widgetBuilder(), const Duration(seconds: 1)); // finish the resize animation tester.pumpWidget(widgetBuilder(), const Duration(seconds: 1)); // dismiss } +class Test1215DismissableComponent extends StatelessComponent { + Test1215DismissableComponent(this.text); + final String text; + Widget build(BuildContext context) { + return new Dismissable( + child: new AspectRatio( + aspectRatio: 1.0, + child: new Text(this.text) + ) + ); + } +} + void main() { test('Horizontal drag triggers dismiss scrollDirection=vertical', () { testWidgets((WidgetTester tester) { @@ -230,4 +252,31 @@ void main() { tester.pumpWidget(widgetBuilder()); }); }); + + // This one is for + // https://github.com/flutter/engine/issues/1215 + test('dismissing bottom then top (smoketest)', () { + testWidgets((WidgetTester tester) { + tester.pumpWidget(new Center( + child: new Container( + width: 100.0, + height: 1000.0, + child: new Column([ + new Test1215DismissableComponent('1'), + new Test1215DismissableComponent('2') + ]) + ) + )); + expect(tester.findText('1'), isNotNull); + expect(tester.findText('2'), isNotNull); + dismissElement(tester, tester.findText('2'), gestureDirection: DismissDirection.right); + tester.pump(new Duration(seconds: 1)); + expect(tester.findText('1'), isNotNull); + expect(tester.findText('2'), isNull); + dismissElement(tester, tester.findText('1'), gestureDirection: DismissDirection.right); + tester.pump(new Duration(seconds: 1)); + expect(tester.findText('1'), isNull); + expect(tester.findText('2'), isNull); + }); + }); } diff --git a/packages/unit/test/widget/draggable_test.dart b/packages/unit/test/widget/draggable_test.dart index 31120bc376..8e8b224d2a 100644 --- a/packages/unit/test/widget/draggable_test.dart +++ b/packages/unit/test/widget/draggable_test.dart @@ -13,9 +13,9 @@ void main() { tester.pumpWidget(new Navigator( routes: { - '/': (NavigatorState navigator, Route route) { return new Column([ + '/': (RouteArguments args) { return new Column([ new Draggable( - navigator: navigator, + navigator: args.navigator, data: 1, child: new Text('Source'), feedback: new Text('Dragging') diff --git a/packages/unit/test/widget/fractionally_sized_box_test.dart b/packages/unit/test/widget/fractionally_sized_box_test.dart index 4343f5a950..6ab3d21d5d 100644 --- a/packages/unit/test/widget/fractionally_sized_box_test.dart +++ b/packages/unit/test/widget/fractionally_sized_box_test.dart @@ -1,3 +1,4 @@ +import 'package:sky/rendering.dart'; import 'package:sky/widgets.dart'; import 'package:test/test.dart'; @@ -29,7 +30,8 @@ void main() { ) )); expect(detectedSize, equals(const Size(50.0, 25.0))); - expect(inner.currentContext.findRenderObject().localToGlobal(Point.origin), equals(const Point(25.0, 37.5))); + RenderBox box = inner.currentContext.findRenderObject(); + expect(box.localToGlobal(Point.origin), equals(const Point(25.0, 37.5))); }); }); } diff --git a/packages/unit/test/widget/gesture_detector_test.dart b/packages/unit/test/widget/gesture_detector_test.dart index 26d45ecc08..206b8e866f 100644 --- a/packages/unit/test/widget/gesture_detector_test.dart +++ b/packages/unit/test/widget/gesture_detector_test.dart @@ -88,4 +88,38 @@ void main() { tester.pumpWidget(new Container()); }); }); + + test('Pan doesn\'t crash', () { + testWidgets((WidgetTester tester) { + bool didStartPan = false; + Offset panDelta; + bool didEndPan = false; + + tester.pumpWidget( + new GestureDetector( + onPanStart: () { + didStartPan = true; + }, + onPanUpdate: (Offset delta) { + panDelta = delta; + }, + onPanEnd: (_) { + didEndPan = true; + }, + child: new Container() + ) + ); + + expect(didStartPan, isFalse); + expect(panDelta, isNull); + expect(didEndPan, isFalse); + + tester.scrollAt(new Point(10.0, 10.0), new Offset(20.0, 30.0)); + + expect(didStartPan, isTrue); + expect(panDelta.dx, 20.0); + expect(panDelta.dy, 30.0); + expect(didEndPan, isTrue); + }); + }); } diff --git a/packages/unit/test/widget/navigator_test.dart b/packages/unit/test/widget/navigator_test.dart index 5da23d03ef..a9bfcd5e63 100644 --- a/packages/unit/test/widget/navigator_test.dart +++ b/packages/unit/test/widget/navigator_test.dart @@ -49,8 +49,8 @@ void main() { test('Can navigator navigate to and from a stateful component', () { testWidgets((WidgetTester tester) { final Map routes = { - '/': (navigator, route) => new FirstComponent(navigator), - '/second': (navigator, route) => new SecondComponent(navigator), + '/': (RouteArguments args) => new FirstComponent(args.navigator), + '/second': (RouteArguments args) => new SecondComponent(args.navigator), }; tester.pumpWidget(new Navigator(routes: routes)); diff --git a/packages/unit/test/widget/shader_mask_test.dart b/packages/unit/test/widget/shader_mask_test.dart new file mode 100644 index 0000000000..c92122c663 --- /dev/null +++ b/packages/unit/test/widget/shader_mask_test.dart @@ -0,0 +1,27 @@ +import 'dart:sky' as sky; + +import 'package:sky/painting.dart'; +import 'package:sky/widgets.dart'; +import 'package:test/test.dart'; + +import 'widget_tester.dart'; + +sky.Shader createShader(Rect bounds) { + return new LinearGradient( + begin: Point.origin, + end: new Point(0.0, bounds.height), + colors: [const Color(0x00FFFFFF), const Color(0xFFFFFFFF)], + stops: [0.1, 0.35] + ) + .createShader(); +} + + +void main() { + test('Can be constructed', () { + testWidgets((WidgetTester tester) { + Widget child = new Container(width: 100.0, height: 100.0); + tester.pumpWidget(new ShaderMask(child: child, shaderCallback: createShader)); + }); + }); +} diff --git a/packages/unit/test/widget/widget_tester.dart b/packages/unit/test/widget/widget_tester.dart index 465a14094e..cf8f127ee1 100644 --- a/packages/unit/test/widget/widget_tester.dart +++ b/packages/unit/test/widget/widget_tester.dart @@ -145,7 +145,10 @@ class WidgetTester { } void scroll(Element element, Offset offset, { int pointer: 1 }) { - Point startLocation = getCenter(element); + scrollAt(getCenter(element), offset, pointer: pointer); + } + + void scrollAt(Point startLocation, Offset offset, { int pointer: 1 }) { Point endLocation = startLocation + offset; TestPointer p = new TestPointer(pointer); // Events for the entire press-drag-release gesture are dispatched