Commit 2a140a77 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Sample Catalog page/screenshot production (#10212)

parent ee345164
.generated/
test_driver/screenshot.dart
test_driver/screenshot_test.dart
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
```
// 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<String, String> 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<String, String> values) {
output.writeAsStringSync(expandTemplate(template, values));
logMessage('wrote $output');
}
class SampleGenerator {
SampleGenerator(this.sourceFile);
final File sourceFile;
String sourceCode;
Map<String, String> 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<Match> 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 = <String, String>{};
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<SampleGenerator> samples = <SampleGenerator>[];
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,
<String, String>{
'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,
<String, String>{
'paths': samples.map((SampleGenerator sample) {
return "'${outputFile(sample.sourceName + '.png').path}'";
}).toList().join(',\n'),
},
);
final List<String> flutterDriveArgs = <String>['drive', 'test_driver/screenshot.dart'];
logMessage('Generating screenshots with: flutter ${flutterDriveArgs.join(" ")}');
Process.runSync('flutter', flutterDriveArgs);
}
void main(List<String> 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);
}
@(title)
=============
@(summary)
@(description)
See also:
@(see also)
```
@(source)
```
// 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<SampleScreenshots> {
final List<Widget> samples = <Widget>[
@(widgets)
];
int sampleIndex = 0;
@override
Widget build(BuildContext context) {
return new GestureDetector(
key: const ValueKey<String>('screenshotGestureDetector'),
behavior: HitTestBehavior.opaque,
onTap: () {
setState(() {
sampleIndex += 1;
});
},
child: new IgnorePointer(
child: samples[sampleIndex % samples.length],
),
);
}
}
void main() {
enableFlutterDriverExtension();
runApp(new SampleScreenshots());
}
// 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<String> paths = <String>[
@(paths)
];
for (String path in paths) {
final List<int> 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();
}
});
});
}
......@@ -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<E> class, a simple encapsulation of List<E>
// 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<double> 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<E> {
ListModel({
@required this.listKey,
@required this.removedItemBuilder,
Iterable<E> initialItems,
}) : _items = new List<E>.from(initialItems ?? <E>[]) {
assert(listKey != null);
assert(removedItemBuilder != null);
}
final GlobalKey<AnimatedListState> listKey;
final dynamic removedItemBuilder;
final List<E> _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<double> 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<AnimatedListSample> {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: const Text('AnimatedList'),
actions: <Widget>[
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',
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('AnimatedList'),
actions: <Widget>[
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,
),
],
),
),
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<E> {
ListModel({
@required this.listKey,
@required this.removedItemBuilder,
Iterable<E> initialItems,
}) : _items = new List<E>.from(initialItems ?? <E>[]) {
assert(listKey != null);
assert(removedItemBuilder != null);
}
final GlobalKey<AnimatedListState> listKey;
final dynamic removedItemBuilder;
final List<E> _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<double> 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<double> 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<AnimatedListSample> {
}
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<E>`, a simple encapsulation of `List<E>`
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:
<https://material.io/guidelines/components/lists-controls.html#>
*/
......@@ -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:
/// <https://material.io/guidelines/components/tabs.html>
class AppBarBottomSample extends StatefulWidget {
@override
_AppBarBottomSampleState createState() => new _AppBarBottomSampleState();
}
class _AppBarBottomSampleState extends State<AppBarBottomSample> 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: <Widget>[
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();
void main() {
runApp(new AppBarBottomSample());
}
class _AppBarBottomSampleState extends State<AppBarBottomSample> with SingleTickerProviderStateMixin {
TabController _tabController;
/*
Sample Catalog
@override
void initState() {
super.initState();
_tabController = new TabController(vsync: this, length: choices.length);
}
Title: AppBar with a custom bottom widget.
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
Summary: The AppBar's bottom widget is often a TabBar however any widget with a
PreferredSize can be used.
void _nextPage(int delta) {
final int newIndex = _tabController.index + delta;
if (newIndex < 0 || newIndex >= _tabController.length)
return;
_tabController.animateTo(newIndex);
}
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.
@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: <Widget>[
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(),
),
);
}
}
Classes: AppBar, PreferredSize, TabBarView, TabController
void main() {
runApp(new MaterialApp(home: new AppBarBottomSample()));
}
Sample: AppBarBottomSample
See also:
- The "Components-Tabs" section of the material design specification:
<https://material.io/guidelines/components/tabs.html>
*/
......@@ -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:
/// <https://material.io/guidelines/layout/structure.html#structure-app-bar>
// 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<BasicAppBarSample> {
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: <Widget>[
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<Choice>( // overflow menu
onSelected: _select,
itemBuilder: (BuildContext context) {
return choices.skip(2).map((Choice choice) {
return new PopupMenuItem<Choice>(
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();
void main() {
runApp(new BasicAppBarSample());
}
class _BasicAppBarSampleState extends State<BasicAppBarSample> {
Choice _selectedChoice = choices[0];
/*
Sample Catalog
void _select(Choice choice) {
setState(() {
_selectedChoice = choice;
});
}
Title: AppBar Basics
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: const Text('Basic AppBar'),
actions: <Widget>[
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<Choice>(
onSelected: _select,
itemBuilder: (BuildContext context) {
return choices.skip(2).map((Choice choice) {
return new PopupMenuItem<Choice>(
value: choice,
child: new Text(choice.title),
);
}).toList();
},
),
],
),
body: new Padding(
padding: const EdgeInsets.all(16.0),
child: new ChoiceCard(choice: _selectedChoice),
),
);
}
}
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.
void main() {
runApp(new MaterialApp(home: new BasicAppBarSample()));
}
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:
<https://material.io/guidelines/layout/structure.html#structure-app-bar>
*/
......@@ -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 <Entry>[]]);
final String title;
final List<Entry> children;
}
// The entire multilevel list displayed by this app.
final List<Entry> data = <Entry>[
new Entry('Chapter A',
<Entry>[
......@@ -46,6 +65,8 @@ final List<Entry> data = <Entry>[
),
];
// 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 MaterialApp(home: new ExpansionTileSample()));
runApp(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:
<https://material.io/guidelines/components/lists-controls.html#lists-controls-types-of-list-controls>
*/
......@@ -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:
/// <https://material.io/guidelines/components/tabs.html>
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 MaterialApp(home: new TabbedAppBarSample()));
runApp(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:
<https://material.io/guidelines/components/tabs.html>
*/
The screenshot_test.dart file was generated by ../bin/sample_page.dart. It should not be checked in.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment