diff --git a/examples/catalog/.gitignore b/examples/catalog/.gitignore new file mode 100644 index 0000000000..52bc6fbc1a --- /dev/null +++ b/examples/catalog/.gitignore @@ -0,0 +1,3 @@ +.generated/ +test_driver/screenshot.dart +test_driver/screenshot_test.dart diff --git a/examples/catalog/README.md b/examples/catalog/README.md new file mode 100644 index 0000000000..f841b15093 --- /dev/null +++ b/examples/catalog/README.md @@ -0,0 +1,31 @@ +Samples Catalog +======= + +A collection of sample apps that demonstrate how Flutter can be used. + +Each sample app is contained in a single `.dart` file and they're all found in +the lib directory. + +The apps are intended to be short and easily understood. Classes that represent +the sample's focus are at the top of the file, data and support classes follow. + +Each sample app contains a comment (usually at the end) which provides some +standard documentation that also appears in the web view of the catalog. +See the "Generating..." section below. + +Generating the web view of the catalog +--------- + +Markdown and a screenshot of each app are produced by `bin/sample_page.dart` +and saved in the `.generated` directory. The markdown file contains +the text taken from the Sample Catalog comment found in the app's source +file, followed by the source code itself. + +This sample_page.dart command line app must be run from the examples/catalog +directory. It relies on templates also found in the bin directory and it +generates and executes `test_driver` apps to collect the screenshots: + +``` +cd examples/catalog +dart bin/sample_page.dart +``` diff --git a/examples/catalog/bin/sample_page.dart b/examples/catalog/bin/sample_page.dart new file mode 100644 index 0000000000..cc28273488 --- /dev/null +++ b/examples/catalog/bin/sample_page.dart @@ -0,0 +1,203 @@ +// Copyright 2017 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. + +// This application generates markdown pages and screenshots for each +// sample app. For more information see ../README.md. + +import 'dart:io'; + +class SampleError extends Error { + SampleError(this.message); + final String message; + @override + String toString() => message; +} + +// Sample apps are .dart files in the lib directory which contain a block +// comment that begins with a '/* Sample Catalog' line, and ends with a line +// that just contains '*/'. The following keywords may appear at the +// beginning of lines within the comment. A keyword's value is all of +// the following text up to the next keyword or the end of the comment, +// sans leading and trailing whitespace. +const String sampleCatalogKeywords = r'^Title:|^Summary:|^Description:|^Classes:|^Sample:|^See also:'; + +Directory outputDirectory; +Directory sampleDirectory; +Directory testDirectory; +Directory driverDirectory; +String sampleTemplate; +String screenshotTemplate; +String screenshotDriverTemplate; + +void logMessage(String s) { print(s); } +void logError(String s) { print(s); } + +File inputFile(String dir, String name) { + return new File(dir + Platform.pathSeparator + name); +} + +File outputFile(String name, [Directory directory]) { + return new File((directory ?? outputDirectory).path + Platform.pathSeparator + name); +} + +void initialize() { + final File sampleTemplateFile = inputFile('bin', 'sample_page.md.template'); + final File screenshotTemplateFile = inputFile('bin', 'screenshot.dart.template'); + final File screenshotDriverTemplateFile = inputFile('bin', 'screenshot_test.dart.template'); + + outputDirectory = new Directory('.generated'); + sampleDirectory = new Directory('lib'); + testDirectory = new Directory('test'); + driverDirectory = new Directory('test_driver'); + sampleTemplate = sampleTemplateFile.readAsStringSync(); + screenshotTemplate = screenshotTemplateFile.readAsStringSync(); + screenshotDriverTemplate = screenshotDriverTemplateFile.readAsStringSync(); +} + +// Return a copy of template with each occurrence of @(foo) replaced +// by values[foo]. +String expandTemplate(String template, Map values) { + // Matches @(foo), match[1] == 'foo' + final RegExp tokenRE = new RegExp(r'@\(([\w ]+)\)', multiLine: true); + return template.replaceAllMapped(tokenRE, (Match match) { + if (match.groupCount != 1) + throw new SampleError('bad template keyword $match[0]'); + final String keyword = match[1]; + return (values[keyword] ?? ""); + }); +} + +void writeExpandedTemplate(File output, String template, Map values) { + output.writeAsStringSync(expandTemplate(template, values)); + logMessage('wrote $output'); +} + +class SampleGenerator { + SampleGenerator(this.sourceFile); + + final File sourceFile; + String sourceCode; + Map commentValues; + + // If sourceFile is lib/foo.dart then sourceName is foo. The sourceName + // is used to create derived filenames like foo.md or foo.png. + String get sourceName { + // In /foo/bar/baz.dart, matches baz.dart, match[1] == 'baz' + final RegExp nameRE = new RegExp(r'(\w+)\.dart$'); + final Match nameMatch = nameRE.firstMatch(sourceFile.path); + if (nameMatch.groupCount != 1) + throw new SampleError('bad source file name ${sourceFile.path}'); + return nameMatch[1]; + } + + // The name of the widget class that defines this sample app, like 'FooSample'. + String get sampleClass => commentValues["sample"]; + + // The relative import path for this sample, like '../lib/foo.dart'. + String get importPath => '..' + Platform.pathSeparator + sourceFile.path; + + // Return true if we're able to find the "Sample Catalog" comment in the + // sourceFile, and we're able to load its keyword/value pairs into + // the commentValues Map. The rest of the file's contents are saved + // in sourceCode. + bool initialize() { + final String contents = sourceFile.readAsStringSync(); + + final RegExp startRE = new RegExp(r'^/\*\s+^Sample\s+Catalog', multiLine: true); + final RegExp endRE = new RegExp(r'^\*/', multiLine: true); + final Match startMatch = startRE.firstMatch(contents); + if (startMatch == null) + return false; + + final int startIndex = startMatch.end; + final Match endMatch = endRE.firstMatch(contents.substring(startIndex)); + if (endMatch == null) + return false; + + final String comment = contents.substring(startIndex, startIndex + endMatch.start); + sourceCode = contents.substring(0, startMatch.start) + contents.substring(startIndex + endMatch.end); + if (sourceCode.trim().isEmpty) + throw new SampleError('did not find any source code in $sourceFile'); + + final RegExp keywordsRE = new RegExp(sampleCatalogKeywords, multiLine: true); + final List keywordMatches = keywordsRE.allMatches(comment).toList(); + // TBD: fix error generation + if (keywordMatches.isEmpty) + throw new SampleError('did not find any keywords in the Sample Catalog comment in $sourceFile'); + + commentValues = {}; + for (int i = 0; i < keywordMatches.length; i += 1) { + final String keyword = comment.substring(keywordMatches[i].start, keywordMatches[i].end - 1); + final String value = comment.substring( + keywordMatches[i].end, + i == keywordMatches.length - 1 ? null : keywordMatches[i + 1].start, + ); + commentValues[keyword.toLowerCase()] = value.trim(); + } + commentValues['source'] = sourceCode.trim(); + + return true; + } +} + +void generate() { + initialize(); + + final List samples = []; + sampleDirectory.listSync().forEach((FileSystemEntity entity) { + if (entity is File && entity.path.endsWith('.dart')) { + final SampleGenerator sample = new SampleGenerator(entity); + if (sample.initialize()) { // skip files that lack the Sample Catalog comment + writeExpandedTemplate( + outputFile(sample.sourceName + '.md'), + sampleTemplate, + sample.commentValues, + ); + samples.add(sample); + } + } + }); + + writeExpandedTemplate( + outputFile('screenshot.dart', driverDirectory), + screenshotTemplate, + { + 'imports': samples.map((SampleGenerator page) { + return "import '${page.importPath}' show ${page.sampleClass};\n"; + }).toList().join(), + 'widgets': samples.map((SampleGenerator sample) { + return 'new ${sample.sampleClass}(),\n'; + }).toList().join(), + }, + ); + + writeExpandedTemplate( + outputFile('screenshot_test.dart', driverDirectory), + screenshotDriverTemplate, + { + 'paths': samples.map((SampleGenerator sample) { + return "'${outputFile(sample.sourceName + '.png').path}'"; + }).toList().join(',\n'), + }, + ); + + final List flutterDriveArgs = ['drive', 'test_driver/screenshot.dart']; + logMessage('Generating screenshots with: flutter ${flutterDriveArgs.join(" ")}'); + Process.runSync('flutter', flutterDriveArgs); +} + +void main(List args) { + try { + generate(); + } catch (error) { + logError( + 'Error: sample_page.dart failed: $error\n' + 'This sample_page.dart app expects to be run from the examples/catalog directory. ' + 'More information can be found in examples/catalog/README.md.' + ); + exit(255); + } + + exit(0); +} diff --git a/examples/catalog/bin/sample_page.md.template b/examples/catalog/bin/sample_page.md.template new file mode 100644 index 0000000000..cc937f1c6c --- /dev/null +++ b/examples/catalog/bin/sample_page.md.template @@ -0,0 +1,13 @@ +@(title) +============= + +@(summary) + +@(description) + +See also: +@(see also) + +``` +@(source) +``` diff --git a/examples/catalog/bin/screenshot.dart.template b/examples/catalog/bin/screenshot.dart.template new file mode 100644 index 0000000000..b513f85ca8 --- /dev/null +++ b/examples/catalog/bin/screenshot.dart.template @@ -0,0 +1,40 @@ +// This file was generated using bin/screenshot.dart.template and +// bin/sample_page.dart. For more information see README.md. + +import 'package:flutter_driver/driver_extension.dart'; +import 'package:flutter/material.dart'; + +@(imports) + +class SampleScreenshots extends StatefulWidget { + @override + SampleScreenshotsState createState() => new SampleScreenshotsState(); +} + +class SampleScreenshotsState extends State { + final List samples = [ + @(widgets) + ]; + int sampleIndex = 0; + + @override + Widget build(BuildContext context) { + return new GestureDetector( + key: const ValueKey('screenshotGestureDetector'), + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + sampleIndex += 1; + }); + }, + child: new IgnorePointer( + child: samples[sampleIndex % samples.length], + ), + ); + } +} + +void main() { + enableFlutterDriverExtension(); + runApp(new SampleScreenshots()); +} diff --git a/examples/catalog/bin/screenshot_test.dart.template b/examples/catalog/bin/screenshot_test.dart.template new file mode 100644 index 0000000000..d25ceae2fd --- /dev/null +++ b/examples/catalog/bin/screenshot_test.dart.template @@ -0,0 +1,35 @@ +// This file was generated using bin/screenshot_test.dart.template and +// bin/sample_page.dart. For more information see README.md. + +import 'dart:io'; + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:test/test.dart'; + +void main() { + group('sample screenshots', () async { + FlutterDriver driver; + + setUpAll(() async { + driver = await FlutterDriver.connect(); + }); + + tearDownAll(() async { + await driver?.close(); + }); + + test('take sample screenshots', () async { + final List paths = [ + @(paths) + ]; + for (String path in paths) { + final List pixels = await driver.screenshot(); + final File file = new File(path); + await file.writeAsBytes(pixels); + print('wrote $file'); + await driver.tap(find.byValueKey('screenshotGestureDetector')); + await driver.waitUntilNoTransientCallbacks(); + } + }); + }); +} diff --git a/examples/catalog/lib/animated_list.dart b/examples/catalog/lib/animated_list.dart index 4cddd4284e..5742ed511a 100644 --- a/examples/catalog/lib/animated_list.dart +++ b/examples/catalog/lib/animated_list.dart @@ -2,113 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// A sample app that demonstrates using an AnimatedList. -// -// Tap an item to select it, tap it again to unselect. Tap '+' to insert at the -// selected item, '-' to remove the selected item. -// -// This app includes a ListModel class, a simple encapsulation of List -// that keeps an AnimatedList in sync. - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -/// Displays its integer item as 'item N' on a Card whose color is based on -/// the item's value. The text is displayed in bright green if selected is true. -/// This widget's height is based on the animation parameter, it varies -/// from 0 to 128 as the animation varies from 0.0 to 1.0. -class CardItem extends StatelessWidget { - CardItem({ - Key key, - @required this.animation, - this.onTap, - @required this.item, - this.selected: false - }) : super(key: key) { - assert(animation != null); - assert(item != null && item >= 0); - assert(selected != null); - } - - final Animation animation; - final VoidCallback onTap; - final int item; - final bool selected; - - @override - Widget build(BuildContext context) { - TextStyle textStyle = Theme.of(context).textTheme.display1; - if (selected) - textStyle = textStyle.copyWith(color: Colors.lightGreenAccent[400]); - return new Padding( - padding: const EdgeInsets.all(2.0), - child: new SizeTransition( - axis: Axis.vertical, - sizeFactor: animation, - child: new GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onTap, - child: new SizedBox( - height: 128.0, - child: new Card( - color: Colors.primaries[item % Colors.primaries.length], - child: new Center( - child: new Text('Item $item', style: textStyle), - ), - ), - ), - ), - ), - ); - } -} - -/// Keeps a Dart List in sync with an AnimatedList. -/// -/// The [insert] and [removeAt] methods apply to both the internal list and the -/// animated list that belongs to [listKey]. -/// -/// This class only exposes as much of the Dart List API as is needed by the -/// sample app. More list methods are easily added, however methods that mutate the -/// list must make the same changes to the animated list in terms of -/// [AnimatedListState.insertItem] and [AnimatedList.removeItem]. -class ListModel { - ListModel({ - @required this.listKey, - @required this.removedItemBuilder, - Iterable initialItems, - }) : _items = new List.from(initialItems ?? []) { - assert(listKey != null); - assert(removedItemBuilder != null); - } - - final GlobalKey listKey; - final dynamic removedItemBuilder; - final List _items; - - AnimatedListState get _animatedList => listKey.currentState; - - void insert(int index, E item) { - _items.insert(index, item); - _animatedList.insertItem(index); - } - - E removeAt(int index) { - final E removedItem = _items.removeAt(index); - if (removedItem != null) { - _animatedList.removeItem(index, (BuildContext context, Animation animation) { - return removedItemBuilder(removedItem, context, animation); - }); - } - return removedItem; - } - - int get length => _items.length; - E operator [](int index) => _items[index]; - int indexOf(E item) => _items.indexOf(item); -} - - class AnimatedListSample extends StatefulWidget { @override _AnimatedListSampleState createState() => new _AnimatedListSampleState(); @@ -177,28 +73,125 @@ class _AnimatedListSampleState extends State { @override Widget build(BuildContext context) { - return new Scaffold( - appBar: new AppBar( - title: const Text('AnimatedList'), - actions: [ - new IconButton( - icon: const Icon(Icons.add_circle), - onPressed: _insert, - tooltip: 'insert a new item', + return new MaterialApp( + home: new Scaffold( + appBar: new AppBar( + title: const Text('AnimatedList'), + actions: [ + new IconButton( + icon: const Icon(Icons.add_circle), + onPressed: _insert, + tooltip: 'insert a new item', + ), + new IconButton( + icon: const Icon(Icons.remove_circle), + onPressed: _remove, + tooltip: 'remove the selected item', + ), + ], + ), + body: new Padding( + padding: const EdgeInsets.all(16.0), + child: new AnimatedList( + key: _listKey, + initialItemCount: _list.length, + itemBuilder: _buildItem, ), - new IconButton( - icon: const Icon(Icons.remove_circle), - onPressed: _remove, - tooltip: 'remove the selected item', - ), - ], + ), ), - body: new Padding( - padding: const EdgeInsets.all(16.0), - child: new AnimatedList( - key: _listKey, - initialItemCount: _list.length, - itemBuilder: _buildItem, + ); + } +} + +/// Keeps a Dart List in sync with an AnimatedList. +/// +/// The [insert] and [removeAt] methods apply to both the internal list and the +/// animated list that belongs to [listKey]. +/// +/// This class only exposes as much of the Dart List API as is needed by the +/// sample app. More list methods are easily added, however methods that mutate the +/// list must make the same changes to the animated list in terms of +/// [AnimatedListState.insertItem] and [AnimatedList.removeItem]. +class ListModel { + ListModel({ + @required this.listKey, + @required this.removedItemBuilder, + Iterable initialItems, + }) : _items = new List.from(initialItems ?? []) { + assert(listKey != null); + assert(removedItemBuilder != null); + } + + final GlobalKey listKey; + final dynamic removedItemBuilder; + final List _items; + + AnimatedListState get _animatedList => listKey.currentState; + + void insert(int index, E item) { + _items.insert(index, item); + _animatedList.insertItem(index); + } + + E removeAt(int index) { + final E removedItem = _items.removeAt(index); + if (removedItem != null) { + _animatedList.removeItem(index, (BuildContext context, Animation animation) { + return removedItemBuilder(removedItem, context, animation); + }); + } + return removedItem; + } + + int get length => _items.length; + E operator [](int index) => _items[index]; + int indexOf(E item) => _items.indexOf(item); +} + +/// Displays its integer item as 'item N' on a Card whose color is based on +/// the item's value. The text is displayed in bright green if selected is true. +/// This widget's height is based on the animation parameter, it varies +/// from 0 to 128 as the animation varies from 0.0 to 1.0. +class CardItem extends StatelessWidget { + CardItem({ + Key key, + @required this.animation, + this.onTap, + @required this.item, + this.selected: false + }) : super(key: key) { + assert(animation != null); + assert(item != null && item >= 0); + assert(selected != null); + } + + final Animation animation; + final VoidCallback onTap; + final int item; + final bool selected; + + @override + Widget build(BuildContext context) { + TextStyle textStyle = Theme.of(context).textTheme.display1; + if (selected) + textStyle = textStyle.copyWith(color: Colors.lightGreenAccent[400]); + return new Padding( + padding: const EdgeInsets.all(2.0), + child: new SizeTransition( + axis: Axis.vertical, + sizeFactor: animation, + child: new GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: new SizedBox( + height: 128.0, + child: new Card( + color: Colors.primaries[item % Colors.primaries.length], + child: new Center( + child: new Text('Item $item', style: textStyle), + ), + ), + ), ), ), ); @@ -206,5 +199,32 @@ class _AnimatedListSampleState extends State { } void main() { - runApp(new MaterialApp(home: new AnimatedListSample())); + runApp(new AnimatedListSample()); } + +/* +Sample Catalog + +Title: AnimatedList + +Summary: In this app an AnimatedList displays a list of cards which stays +in sync with an app-specific ListModel. When an item is added to or removed +from the model, a corresponding card items animate in or out of view +in the animated list. + +Description: +Tap an item to select it, tap it again to unselect. Tap '+' to insert at the +selected item, '-' to remove the selected item. The tap handlers add or +remove items from a `ListModel`, a simple encapsulation of `List` +that keeps the AnimatedList in sync. The list model has a GlobalKey for +its animated list. It uses the key to call the insertItem and removeItem +methods defined by AnimatedListState. + +Classes: AnimatedList, AnimatedListState + +Sample: AnimatedListSample + +See also: + - The "Components-Lists: Controls" section of the material design specification: + +*/ diff --git a/examples/catalog/lib/app_bar_bottom.dart b/examples/catalog/lib/app_bar_bottom.dart index b30cd04354..308ec837b5 100644 --- a/examples/catalog/lib/app_bar_bottom.dart +++ b/examples/catalog/lib/app_bar_bottom.dart @@ -4,17 +4,76 @@ import 'package:flutter/material.dart'; -/// Sample Catalog: AppBar with a custom bottom widget. -/// -/// The bottom widget's TabPageSelector displays the relative position of the -/// selected choice. The arrow buttons in the toolbar part of the app bar -/// select the previous or the next choice. -/// -/// Sample classes: [AppBar], [TabController], [TabPageSelector], [Scaffold], [TabBarView]. -/// -/// See also: -/// * The "Components-Tabs" section of the material design specification: -/// +class AppBarBottomSample extends StatefulWidget { + @override + _AppBarBottomSampleState createState() => new _AppBarBottomSampleState(); +} + +class _AppBarBottomSampleState extends State with SingleTickerProviderStateMixin { + TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = new TabController(vsync: this, length: choices.length); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + void _nextPage(int delta) { + final int newIndex = _tabController.index + delta; + if (newIndex < 0 || newIndex >= _tabController.length) + return; + _tabController.animateTo(newIndex); + } + + @override + Widget build(BuildContext context) { + return new MaterialApp( + home: new Scaffold( + appBar: new AppBar( + title: const Text('AppBar Bottom Widget'), + leading: new IconButton( + tooltip: 'Previous choice', + icon: const Icon(Icons.arrow_back), + onPressed: () { _nextPage(-1); }, + ), + actions: [ + new IconButton( + icon: const Icon(Icons.arrow_forward), + tooltip: 'Next choice', + onPressed: () { _nextPage(1); }, + ), + ], + bottom: new PreferredSize( + preferredSize: const Size.fromHeight(48.0), + child: new Theme( + data: Theme.of(context).copyWith(accentColor: Colors.white), + child: new Container( + height: 48.0, + alignment: FractionalOffset.center, + child: new TabPageSelector(controller: _tabController), + ), + ), + ), + ), + body: new TabBarView( + controller: _tabController, + children: choices.map((Choice choice) { + return new Padding( + padding: const EdgeInsets.all(16.0), + child: new ChoiceCard(choice: choice), + ); + }).toList(), + ), + ), + ); + } +} class Choice { const Choice({ this.title, this.icon }); @@ -55,75 +114,29 @@ class ChoiceCard extends StatelessWidget { } } -class AppBarBottomSample extends StatefulWidget { - @override - _AppBarBottomSampleState createState() => new _AppBarBottomSampleState(); -} - -class _AppBarBottomSampleState extends State with SingleTickerProviderStateMixin { - TabController _tabController; - - @override - void initState() { - super.initState(); - _tabController = new TabController(vsync: this, length: choices.length); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - void _nextPage(int delta) { - final int newIndex = _tabController.index + delta; - if (newIndex < 0 || newIndex >= _tabController.length) - return; - _tabController.animateTo(newIndex); - } - - @override - Widget build(BuildContext context) { - return new Scaffold( - appBar: new AppBar( - title: const Text('AppBar Bottom Widget'), - leading: new IconButton( - tooltip: 'Previous choice', - icon: const Icon(Icons.arrow_back), - onPressed: () { _nextPage(-1); }, - ), - actions: [ - new IconButton( - icon: const Icon(Icons.arrow_forward), - tooltip: 'Next choice', - onPressed: () { _nextPage(1); }, - ), - ], - bottom: new PreferredSize( - preferredSize: const Size.fromHeight(48.0), - child: new Theme( - data: Theme.of(context).copyWith(accentColor: Colors.white), - child: new Container( - height: 48.0, - alignment: FractionalOffset.center, - child: new TabPageSelector(controller: _tabController), - ), - ), - ), - ), - body: new TabBarView( - controller: _tabController, - children: choices.map((Choice choice) { - return new Padding( - padding: const EdgeInsets.all(16.0), - child: new ChoiceCard(choice: choice), - ); - }).toList(), - ), - ); - } -} - void main() { - runApp(new MaterialApp(home: new AppBarBottomSample())); + runApp(new AppBarBottomSample()); } + +/* +Sample Catalog + +Title: AppBar with a custom bottom widget. + +Summary: The AppBar's bottom widget is often a TabBar however any widget with a +PreferredSize can be used. + +Description: +In this app, the app bar's bottom widget is a TabPageSelector +that displays the relative position of the selected page in the app's +TabBarView. The arrow buttons in the toolbar part of the app bar select +the previous or the next choice. + +Classes: AppBar, PreferredSize, TabBarView, TabController + +Sample: AppBarBottomSample + +See also: + - The "Components-Tabs" section of the material design specification: + +*/ diff --git a/examples/catalog/lib/basic_app_bar.dart b/examples/catalog/lib/basic_app_bar.dart index 34823b94d3..ed15fd9f90 100644 --- a/examples/catalog/lib/basic_app_bar.dart +++ b/examples/catalog/lib/basic_app_bar.dart @@ -4,17 +4,57 @@ import 'package:flutter/material.dart'; -/// Sample Catalog: Basic AppBar -/// -/// An AppBar with a title, actions, and an overflow menu. One of the app's -/// choices can be selected action buttons or the menu. -/// -/// Sample classes: [AppBar], [IconButton], [PopupMenuButton], [Scaffold]. -/// -/// See also: -/// -/// * The "Layout-Structure" section of the material design specification: -/// +// This app is a stateful, it tracks the user's current choice. +class BasicAppBarSample extends StatefulWidget { + @override + _BasicAppBarSampleState createState() => new _BasicAppBarSampleState(); +} + +class _BasicAppBarSampleState extends State { + Choice _selectedChoice = choices[0]; // The app's "state". + + void _select(Choice choice) { + setState(() { // Causes the app to rebuild with the new _selectedChoice. + _selectedChoice = choice; + }); + } + + @override + Widget build(BuildContext context) { + return new MaterialApp( + home: new Scaffold( + appBar: new AppBar( + title: const Text('Basic AppBar'), + actions: [ + new IconButton( // action button + icon: new Icon(choices[0].icon), + onPressed: () { _select(choices[0]); }, + ), + new IconButton( // action button + icon: new Icon(choices[1].icon), + onPressed: () { _select(choices[1]); }, + ), + new PopupMenuButton( // overflow menu + onSelected: _select, + itemBuilder: (BuildContext context) { + return choices.skip(2).map((Choice choice) { + return new PopupMenuItem( + value: choice, + child: new Text(choice.title), + ); + }).toList(); + }, + ), + ], + ), + body: new Padding( + padding: const EdgeInsets.all(16.0), + child: new ChoiceCard(choice: _selectedChoice), + ), + ), + ); + } +} class Choice { const Choice({ this.title, this.icon }); @@ -55,55 +95,28 @@ class ChoiceCard extends StatelessWidget { } } -class BasicAppBarSample extends StatefulWidget { - @override - _BasicAppBarSampleState createState() => new _BasicAppBarSampleState(); -} - -class _BasicAppBarSampleState extends State { - Choice _selectedChoice = choices[0]; - - void _select(Choice choice) { - setState(() { - _selectedChoice = choice; - }); - } - - @override - Widget build(BuildContext context) { - return new Scaffold( - appBar: new AppBar( - title: const Text('Basic AppBar'), - actions: [ - new IconButton( - icon: new Icon(choices[0].icon), - onPressed: () { _select(choices[0]); }, - ), - new IconButton( - icon: new Icon(choices[1].icon), - onPressed: () { _select(choices[1]); }, - ), - new PopupMenuButton( - onSelected: _select, - itemBuilder: (BuildContext context) { - return choices.skip(2).map((Choice choice) { - return new PopupMenuItem( - value: choice, - child: new Text(choice.title), - ); - }).toList(); - }, - ), - ], - ), - body: new Padding( - padding: const EdgeInsets.all(16.0), - child: new ChoiceCard(choice: _selectedChoice), - ), - ); - } -} - void main() { - runApp(new MaterialApp(home: new BasicAppBarSample())); + runApp(new BasicAppBarSample()); } + +/* +Sample Catalog + +Title: AppBar Basics + +Summary: An AppBar with a title, actions, and an overflow dropdown menu. +One of the app's choices can be selected with an action button or the menu. + +Description: +An app that displays one of a half dozen choices with an icon and a title. +The two most common choices are available as action buttons and the remaining +choices are included in the overflow dropdow menu. + +Classes: AppBar, IconButton, PopupMenuButton, Scaffold + +Sample: BasicAppBarSample + +See also: + - The "Layout-Structure" section of the material design specification: + +*/ diff --git a/examples/catalog/lib/expansion_tile_sample.dart b/examples/catalog/lib/expansion_tile_sample.dart index 3e639a06cf..2213ee18c5 100644 --- a/examples/catalog/lib/expansion_tile_sample.dart +++ b/examples/catalog/lib/expansion_tile_sample.dart @@ -4,12 +4,31 @@ import 'package:flutter/material.dart'; +class ExpansionTileSample extends StatelessWidget { + @override + Widget build(BuildContext context) { + return new MaterialApp( + home: new Scaffold( + appBar: new AppBar( + title: const Text('ExpansionTile'), + ), + body: new ListView.builder( + itemBuilder: (BuildContext context, int index) => new EntryItem(data[index]), + itemCount: data.length, + ), + ), + ); + } +} + +// One entry in the multilevel list displayed by this app. class Entry { Entry(this.title, [this.children = const []]); final String title; final List children; } +// The entire multilevel list displayed by this app. final List data = [ new Entry('Chapter A', [ @@ -46,6 +65,8 @@ final List data = [ ), ]; +// Displays one Entry. If the entry has children then it's displayed +// with an ExpansionTile. class EntryItem extends StatelessWidget { EntryItem(this.entry); @@ -67,21 +88,31 @@ class EntryItem extends StatelessWidget { } } -class ExpansionTileSample extends StatelessWidget { - @override - Widget build(BuildContext context) { - return new Scaffold( - appBar: new AppBar( - title: const Text('ExpansionTile'), - ), - body: new ListView.builder( - itemBuilder: (BuildContext context, int index) => new EntryItem(data[index]), - itemCount: data.length, - ), - ); - } +void main() { + runApp(new ExpansionTileSample()); } -void main() { - runApp(new MaterialApp(home: new ExpansionTileSample())); -} +/* +Sample Catalog + +Title: ExpansionTile + +Summary: ExpansionTiles can used to produce two-level or multi-level lists. +When displayed within a scrollable that creates its list items lazily, +like a scrollable list created with `ListView.builder()`, they can be quite +efficient, particularly for material design "expand/collapse" lists. + +Description: +This app displays hierarchical data with ExpansionTiles. Tapping a tile +expands or collapses the view of its children. When a tile is collapsed +its children are disposed so that the widget footprint of the list only +reflects what's visible. + +Classes: ExpansionTile, ListView + +Sample: ExpansionTileSample + +See also: + - The "expand/collapse" part of the material design specification: + +*/ diff --git a/examples/catalog/lib/tabbed_app_bar.dart b/examples/catalog/lib/tabbed_app_bar.dart index 9c860fa328..ab0cb6d7ca 100644 --- a/examples/catalog/lib/tabbed_app_bar.dart +++ b/examples/catalog/lib/tabbed_app_bar.dart @@ -4,16 +4,38 @@ import 'package:flutter/material.dart'; -/// Sample Catalog: Tabbed AppBar -/// -/// A basic app bar with a tab bar at the bottom. One of the app's choices can be -/// selected by tapping the tabs or by swiping the tab bar view. -/// -/// Sample classes: [AppBar], [DefaultTabController], [TabBar], [Scaffold], [TabBarView]. -/// -/// See also: -/// * The "Components-Tabs" section of the material design specification: -/// +class TabbedAppBarSample extends StatelessWidget { + @override + Widget build(BuildContext context) { + return new MaterialApp( + home: new DefaultTabController( + length: choices.length, + child: new Scaffold( + appBar: new AppBar( + title: const Text('Tabbed AppBar'), + bottom: new TabBar( + isScrollable: true, + tabs: choices.map((Choice choice) { + return new Tab( + text: choice.title, + icon: new Icon(choice.icon), + ); + }).toList(), + ), + ), + body: new TabBarView( + children: choices.map((Choice choice) { + return new Padding( + padding: const EdgeInsets.all(16.0), + child: new ChoiceCard(choice: choice), + ); + }).toList(), + ), + ), + ), + ); + } +} class Choice { const Choice({ this.title, this.icon }); @@ -54,37 +76,27 @@ class ChoiceCard extends StatelessWidget { } } -class TabbedAppBarSample extends StatelessWidget { - @override - Widget build(BuildContext context) { - return new DefaultTabController( - length: choices.length, - child: new Scaffold( - appBar: new AppBar( - title: const Text('Tabbed AppBar'), - bottom: new TabBar( - isScrollable: true, - tabs: choices.map((Choice choice) { - return new Tab( - text: choice.title, - icon: new Icon(choice.icon), - ); - }).toList(), - ), - ), - body: new TabBarView( - children: choices.map((Choice choice) { - return new Padding( - padding: const EdgeInsets.all(16.0), - child: new ChoiceCard(choice: choice), - ); - }).toList(), - ), - ), - ); - } +void main() { + runApp(new TabbedAppBarSample()); } -void main() { - runApp(new MaterialApp(home: new TabbedAppBarSample())); -} +/* +Sample Catalog + +Title: Tabbed AppBar + +Summary: An AppBar can include a TabBar as its bottom widget. + +Description: +A TabBar can be used to navigate among the pages displayed in a TabBarView. +Although a TabBar is an ordinary widget that can appear, it's most often +included in the application's AppBar. + +Classes: AppBar, DefaultTabController, TabBar, Scaffold, TabBarView + +Sample: TabbedAppBarSample + +See also: + - The "Components-Tabs" section of the material design specification: + +*/ diff --git a/examples/catalog/test/smoke_test.dart b/examples/catalog/test/animated_list_test.dart similarity index 100% rename from examples/catalog/test/smoke_test.dart rename to examples/catalog/test/animated_list_test.dart diff --git a/examples/catalog/test_driver/README.md b/examples/catalog/test_driver/README.md new file mode 100644 index 0000000000..fe96af377a --- /dev/null +++ b/examples/catalog/test_driver/README.md @@ -0,0 +1 @@ +The screenshot_test.dart file was generated by ../bin/sample_page.dart. It should not be checked in.