Sample Catalog page/screenshot production (#10212)
This commit is contained in:
parent
ee34516491
commit
2a140a7752
3
examples/catalog/.gitignore
vendored
Normal file
3
examples/catalog/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.generated/
|
||||
test_driver/screenshot.dart
|
||||
test_driver/screenshot_test.dart
|
31
examples/catalog/README.md
Normal file
31
examples/catalog/README.md
Normal file
@ -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
|
||||
```
|
203
examples/catalog/bin/sample_page.dart
Normal file
203
examples/catalog/bin/sample_page.dart
Normal file
@ -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<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);
|
||||
}
|
13
examples/catalog/bin/sample_page.md.template
Normal file
13
examples/catalog/bin/sample_page.md.template
Normal file
@ -0,0 +1,13 @@
|
||||
@(title)
|
||||
=============
|
||||
|
||||
@(summary)
|
||||
|
||||
@(description)
|
||||
|
||||
See also:
|
||||
@(see also)
|
||||
|
||||
```
|
||||
@(source)
|
||||
```
|
40
examples/catalog/bin/screenshot.dart.template
Normal file
40
examples/catalog/bin/screenshot.dart.template
Normal file
@ -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<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());
|
||||
}
|
35
examples/catalog/bin/screenshot_test.dart.template
Normal file
35
examples/catalog/bin/screenshot_test.dart.template
Normal file
@ -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<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',
|
||||
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,
|
||||
),
|
||||
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<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();
|
||||
}
|
||||
|
||||
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 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
<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();
|
||||
}
|
||||
|
||||
class _BasicAppBarSampleState extends State<BasicAppBarSample> {
|
||||
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: <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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
<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 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:
|
||||
<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 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:
|
||||
<https://material.io/guidelines/components/tabs.html>
|
||||
*/
|
||||
|
1
examples/catalog/test_driver/README.md
Normal file
1
examples/catalog/test_driver/README.md
Normal file
@ -0,0 +1 @@
|
||||
The screenshot_test.dart file was generated by ../bin/sample_page.dart. It should not be checked in.
|
Loading…
x
Reference in New Issue
Block a user