diff --git a/examples/catalog/README.md b/examples/catalog/README.md
new file mode 100644
index 00000000000..69848daf430
--- /dev/null
+++ b/examples/catalog/README.md
@@ -0,0 +1,39 @@
+Samples Catalog
+=======
+
+A collection of sample apps that demonstrate how Flutter can be used.
+
+Each sample app is contained in a single `.dart` file located in the `lib`
+directory. To run each sample app, specify the corresponding file on the
+`flutter run` command line, for example:
+
+```
+flutter run lib/animated_list.dart
+flutter run lib/app_bar_bottom.dart
+flutter run lib/basic_app_bar.dart
+...
+```
+
+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/android/app/build.gradle b/examples/catalog/android/app/build.gradle
new file mode 100644
index 00000000000..852e5cd9490
--- /dev/null
+++ b/examples/catalog/android/app/build.gradle
@@ -0,0 +1,57 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+ throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+ compileSdkVersion 29
+
+ lintOptions {
+ disable 'InvalidPackage'
+ }
+
+ defaultConfig {
+ applicationId "io.flutter.examples.catalog"
+ minSdkVersion 16
+ targetSdkVersion 29
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig signingConfigs.debug
+ }
+ }
+}
+
+flutter {
+ source '../..'
+}
diff --git a/examples/catalog/android/app/src/main/AndroidManifest.xml b/examples/catalog/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..e25ddbe7c72
--- /dev/null
+++ b/examples/catalog/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/catalog/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/catalog/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000000..db77bb4b7b0
Binary files /dev/null and b/examples/catalog/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/examples/catalog/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/catalog/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000000..17987b79bb8
Binary files /dev/null and b/examples/catalog/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/examples/catalog/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/catalog/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000000..09d4391482b
Binary files /dev/null and b/examples/catalog/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/examples/catalog/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/catalog/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000000..d5f1c8d34e7
Binary files /dev/null and b/examples/catalog/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/examples/catalog/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/catalog/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000000..4d6372eebdb
Binary files /dev/null and b/examples/catalog/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/examples/catalog/android/build.gradle b/examples/catalog/android/build.gradle
new file mode 100644
index 00000000000..5df71fcf8bb
--- /dev/null
+++ b/examples/catalog/android/build.gradle
@@ -0,0 +1,33 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+buildscript {
+ repositories {
+ google()
+ jcenter()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.5.0'
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+ project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/examples/catalog/android/gradle.properties b/examples/catalog/android/gradle.properties
new file mode 100644
index 00000000000..a6738207fd1
--- /dev/null
+++ b/examples/catalog/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
+android.enableR8=true
diff --git a/examples/catalog/android/gradle/wrapper/gradle-wrapper.properties b/examples/catalog/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000000..296b146b731
--- /dev/null
+++ b/examples/catalog/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
diff --git a/examples/catalog/android/settings.gradle b/examples/catalog/android/settings.gradle
new file mode 100644
index 00000000000..d3b6a4013d7
--- /dev/null
+++ b/examples/catalog/android/settings.gradle
@@ -0,0 +1,15 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+include ':app'
+
+def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
+def properties = new Properties()
+
+assert localPropertiesFile.exists()
+localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
+
+def flutterSdkPath = properties.getProperty("flutter.sdk")
+assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
diff --git a/examples/catalog/bin/class_index.md.template b/examples/catalog/bin/class_index.md.template
new file mode 100644
index 00000000000..7411af3ba01
--- /dev/null
+++ b/examples/catalog/bin/class_index.md.template
@@ -0,0 +1,11 @@
+---
+layout: page
+title: "@(class) Sample Apps"
+permalink: /catalog/samples/@(link)/
+---
+
+All of the sample apps listed here use the Flutter @(class) class in an interesting way. The Sample App Catalog page lists all of the sample apps.
+
+
+@(entries)
+
diff --git a/examples/catalog/bin/entry.md.template b/examples/catalog/bin/entry.md.template
new file mode 100644
index 00000000000..33bd322cc46
--- /dev/null
+++ b/examples/catalog/bin/entry.md.template
@@ -0,0 +1,18 @@
+
+
+
+
)
+
+
+
+
+ @(summary)
+
+
+ This app features the following classes: @(classes).
+
+
+ Learn more.
+
+
+
diff --git a/examples/catalog/bin/index.md.template b/examples/catalog/bin/index.md.template
new file mode 100644
index 00000000000..a10d905ad77
--- /dev/null
+++ b/examples/catalog/bin/index.md.template
@@ -0,0 +1,13 @@
+---
+layout: page
+title: "Sample App Catalog"
+permalink: /catalog/samples/
+---
+
+This catalog lists applications that demonstrate how to implement common mobile design patterns with Flutter. Each sample demonstrates how a few Flutter widgets can be put together to implement a meaningful user interface. The samples are short - just one Dart file - but they're complete applications. They should be easy to try out and tweak with your favorite IDE/code editor.
+
+If there are other sample apps that you'd like to see we'd appreciate hearing from you on our [Gitter channel](https://gitter.im/flutter/flutter) or [mailing list](https://groups.google.com/d/forum/flutter-dev).
+
+
+@(entries)
+
diff --git a/examples/catalog/bin/sample_page.dart b/examples/catalog/bin/sample_page.dart
new file mode 100644
index 00000000000..6e1ae655a3b
--- /dev/null
+++ b/examples/catalog/bin/sample_page.dart
@@ -0,0 +1,264 @@
+// Copyright 2014 The Flutter 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';
+
+import 'package:path/path.dart';
+
+class SampleError extends Error {
+ SampleError(this.message);
+ final String message;
+ @override
+ String toString() => 'SampleError($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;
+
+void logMessage(String s) { print(s); }
+void logError(String s) { print(s); }
+
+File inputFile(String dir, String name) {
+ return File(dir + Platform.pathSeparator + name);
+}
+
+File outputFile(String name, [Directory directory]) {
+ return File((directory ?? outputDirectory).path + Platform.pathSeparator + name);
+}
+
+void initialize() {
+ outputDirectory = Directory('.generated');
+ sampleDirectory = Directory('lib');
+ testDirectory = Directory('test');
+ driverDirectory = Directory('test_driver');
+ outputDirectory.createSync();
+}
+
+// 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 = RegExp(r'@\(([\w ]+)\)', multiLine: true);
+ return template.replaceAllMapped(tokenRE, (Match match) {
+ if (match.groupCount != 1)
+ throw 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 SampleInfo {
+ SampleInfo(this.sourceFile, this.commit);
+
+ final File sourceFile;
+ final String commit;
+ 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 => basenameWithoutExtension(sourceFile.path);
+
+ // The website's link to this page will be /catalog/samples/@(link)/.
+ String get link => sourceName.replaceAll('_', '-');
+
+ // The name of the widget class that defines this sample app, like 'FooSample'.
+ String get sampleClass => commentValues['sample'];
+
+ // The value of the 'Classes:' comment as a list of class names.
+ Iterable get highlightedClasses {
+ final String classNames = commentValues['classes'];
+ if (classNames == null)
+ return const [];
+ return classNames.split(',').map((String s) => s.trim()).where((String s) => s.isNotEmpty);
+ }
+
+ // 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 = RegExp(r'^/\*\s+^Sample\s+Catalog', multiLine: true);
+ final RegExp endRE = 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 SampleError('did not find any source code in $sourceFile');
+
+ final RegExp keywordsRE = RegExp(sampleCatalogKeywords, multiLine: true);
+ final List keywordMatches = keywordsRE.allMatches(comment).toList();
+ if (keywordMatches.isEmpty)
+ throw 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['name'] = sourceName;
+ commentValues['path'] = 'examples/catalog/${sourceFile.path}';
+ commentValues['source'] = sourceCode.trim();
+ commentValues['link'] = link;
+ commentValues['android screenshot'] = 'https://storage.googleapis.com/flutter-catalog/$commit/${sourceName}_small.png';
+
+ return true;
+ }
+}
+
+void generate(String commit) {
+ initialize();
+
+ final List samples = [];
+ for (final FileSystemEntity entity in sampleDirectory.listSync()) {
+ if (entity is File && entity.path.endsWith('.dart')) {
+ final SampleInfo sample = SampleInfo(entity, commit);
+ if (sample.initialize()) // skip files that lack the Sample Catalog comment
+ samples.add(sample);
+ }
+ }
+
+ // Causes the generated imports to appear in alphabetical order.
+ // Avoid complaints from flutter lint.
+ samples.sort((SampleInfo a, SampleInfo b) {
+ return a.sourceName.compareTo(b.sourceName);
+ });
+
+ final String entryTemplate = inputFile('bin', 'entry.md.template').readAsStringSync();
+
+ // Write the sample catalog's home page: index.md
+ final Iterable entries = samples.map((SampleInfo sample) {
+ return expandTemplate(entryTemplate, sample.commentValues);
+ });
+ writeExpandedTemplate(
+ outputFile('index.md'),
+ inputFile('bin', 'index.md.template').readAsStringSync(),
+ {
+ 'entries': entries.join('\n'),
+ },
+ );
+
+ // Write the sample app files, like animated_list.md
+ for (final SampleInfo sample in samples) {
+ writeExpandedTemplate(
+ outputFile(sample.sourceName + '.md'),
+ inputFile('bin', 'sample_page.md.template').readAsStringSync(),
+ sample.commentValues,
+ );
+ }
+
+ // For each unique class listened in a sample app's "Classes:" list, generate
+ // a file that's structurally the same as index.md but only contains samples
+ // that feature one class. For example AnimatedList_index.md would only
+ // include samples that had AnimatedList in their "Classes:" list.
+ final Map> classToSamples = >{};
+ for (final SampleInfo sample in samples) {
+ for (final String className in sample.highlightedClasses) {
+ classToSamples[className] ??= [];
+ classToSamples[className].add(sample);
+ }
+ }
+ for (final String className in classToSamples.keys) {
+ final Iterable entries = classToSamples[className].map((SampleInfo sample) {
+ return expandTemplate(entryTemplate, sample.commentValues);
+ });
+ writeExpandedTemplate(
+ outputFile('${className}_index.md'),
+ inputFile('bin', 'class_index.md.template').readAsStringSync(),
+ {
+ 'class': className,
+ 'entries': entries.join('\n'),
+ 'link': '${className}_index',
+ },
+ );
+ }
+
+ // Write screenshot.dart, a "test" app that displays each sample
+ // app in turn when the app is tapped.
+ writeExpandedTemplate(
+ outputFile('screenshot.dart', driverDirectory),
+ inputFile('bin', 'screenshot.dart.template').readAsStringSync(),
+ {
+ 'imports': samples.map((SampleInfo page) {
+ return "import '${page.importPath}' show ${page.sampleClass};\n";
+ }).join(),
+ 'widgets': samples.map((SampleInfo sample) {
+ return 'new ${sample.sampleClass}(),\n';
+ }).join(),
+ },
+ );
+
+ // Write screenshot_test.dart, a test driver for screenshot.dart
+ // that collects screenshots of each app and saves them.
+ writeExpandedTemplate(
+ outputFile('screenshot_test.dart', driverDirectory),
+ inputFile('bin', 'screenshot_test.dart.template').readAsStringSync(),
+ {
+ 'paths': samples.map((SampleInfo sample) {
+ return "'${outputFile(sample.sourceName + '.png').path}'";
+ }).join(',\n'),
+ },
+ );
+
+ // For now, the website's index.json file must be updated by hand.
+ logMessage('The following entries must appear in _data/catalog/widgets.json');
+ for (final String className in classToSamples.keys)
+ logMessage('"sample": "${className}_index"');
+}
+
+void main(List args) {
+ if (args.length != 1) {
+ logError(
+ 'Usage (cd examples/catalog/; dart bin/sample_page.dart commit)\n'
+ 'The flutter commit hash locates screenshots on storage.googleapis.com/flutter-catalog/'
+ );
+ exit(255);
+ }
+ try {
+ generate(args[0]);
+ } 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 00000000000..f9470caf653
--- /dev/null
+++ b/examples/catalog/bin/sample_page.md.template
@@ -0,0 +1,36 @@
+---
+layout: page
+title: "@(title)"
+permalink: /catalog/samples/@(link)/
+---
+
+@(summary)
+
+
+
+
+
+
+
+
)
+
+
+
+
+
+
+
+
+@(description)
+
+Try this app out by creating a new project with `flutter create` and replacing the contents of `lib/main.dart` with the code that follows.
+
+```dart
+@(source)
+```
+
+See also:
+@(see also)
+- The source code in [@(path)](https://github.com/flutter/flutter/blob/master/@(path)).
diff --git a/examples/catalog/bin/screenshot.dart.template b/examples/catalog/bin/screenshot.dart.template
new file mode 100644
index 00000000000..4e8c57909a2
--- /dev/null
+++ b/examples/catalog/bin/screenshot.dart.template
@@ -0,0 +1,41 @@
+// 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();
+ WidgetsApp.debugAllowBannerOverride = false; // No "debug" banner.
+ 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 00000000000..50220b87601
--- /dev/null
+++ b/examples/catalog/bin/screenshot_test.dart.template
@@ -0,0 +1,36 @@
+// This file was generated using bin/screenshot_test.dart.template and
+// bin/sample_page.dart. For more information see README.md.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:flutter_driver/flutter_driver.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('sample screenshots', () {
+ 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) {
+ await driver.waitUntilNoTransientCallbacks();
+ 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'));
+ }
+ });
+ });
+}
diff --git a/examples/catalog/ios/Flutter/AppFrameworkInfo.plist b/examples/catalog/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 00000000000..f2872cf474e
--- /dev/null
+++ b/examples/catalog/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ MinimumOSVersion
+ 9.0
+
+
diff --git a/examples/catalog/ios/Flutter/Debug.xcconfig b/examples/catalog/ios/Flutter/Debug.xcconfig
new file mode 100644
index 00000000000..592ceee85b8
--- /dev/null
+++ b/examples/catalog/ios/Flutter/Debug.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/examples/catalog/ios/Flutter/Release.xcconfig b/examples/catalog/ios/Flutter/Release.xcconfig
new file mode 100644
index 00000000000..592ceee85b8
--- /dev/null
+++ b/examples/catalog/ios/Flutter/Release.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/examples/catalog/ios/Runner.xcodeproj/project.pbxproj b/examples/catalog/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 00000000000..35e0caba853
--- /dev/null
+++ b/examples/catalog/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,503 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 74970F651EDBF3AE000507F3 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 74970F641EDBF3AE000507F3 /* GeneratedPluginRegistrant.m */; };
+ 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
+ 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 74970F631EDBF3AE000507F3 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 74970F641EDBF3AE000507F3 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; };
+ 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 840012C8B5EDBCF56B0E4AC1 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Pods;
+ sourceTree = "";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 840012C8B5EDBCF56B0E4AC1 /* Pods */,
+ CF3B75C9A7D2FA2A4C99F110 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 74970F631EDBF3AE000507F3 /* GeneratedPluginRegistrant.h */,
+ 74970F641EDBF3AE000507F3 /* GeneratedPluginRegistrant.m */,
+ 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
+ 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 97C146F11CF9000F007C117D /* Supporting Files */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ 97C146F11CF9000F007C117D /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146F21CF9000F007C117D /* main.m */,
+ );
+ name = "Supporting Files";
+ sourceTree = "";
+ };
+ CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 1020;
+ ORGANIZATIONNAME = "The Flutter Authors";
+ TargetAttributes = {
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
+ 97C146F31CF9000F007C117D /* main.m in Sources */,
+ 74970F651EDBF3AE000507F3 /* GeneratedPluginRegistrant.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 24FC0D0321828CE100FD135A /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Profile;
+ };
+ 24FC0D0421828CE100FD135A /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ENABLE_BITCODE = NO;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.animatedList;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ENABLE_BITCODE = NO;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.animatedList;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ENABLE_BITCODE = NO;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.animatedList;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 24FC0D0321828CE100FD135A /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 24FC0D0421828CE100FD135A /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/examples/catalog/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/catalog/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000000..21a3cc14c74
--- /dev/null
+++ b/examples/catalog/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/examples/catalog/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/catalog/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000000..18d981003d6
--- /dev/null
+++ b/examples/catalog/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/examples/catalog/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/examples/catalog/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 00000000000..04a869b91e0
--- /dev/null
+++ b/examples/catalog/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/catalog/ios/Runner.xcworkspace/contents.xcworkspacedata b/examples/catalog/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000000..1d526a16ed0
--- /dev/null
+++ b/examples/catalog/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/examples/catalog/ios/Runner/AppDelegate.h b/examples/catalog/ios/Runner/AppDelegate.h
new file mode 100644
index 00000000000..a78a945cd2e
--- /dev/null
+++ b/examples/catalog/ios/Runner/AppDelegate.h
@@ -0,0 +1,10 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import
+#import
+
+@interface AppDelegate : FlutterAppDelegate
+
+@end
diff --git a/examples/catalog/ios/Runner/AppDelegate.m b/examples/catalog/ios/Runner/AppDelegate.m
new file mode 100644
index 00000000000..202284911ac
--- /dev/null
+++ b/examples/catalog/ios/Runner/AppDelegate.m
@@ -0,0 +1,15 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "AppDelegate.h"
+#import "GeneratedPluginRegistrant.h"
+
+@implementation AppDelegate
+
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+ [GeneratedPluginRegistrant registerWithRegistry:self];
+ // Override point for customization after application launch.
+ return [super application:application didFinishLaunchingWithOptions:launchOptions];
+}
+@end
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000000..d22f10b2ab6
--- /dev/null
+++ b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,116 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 00000000000..28c6bf03016
Binary files /dev/null and b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 00000000000..2ccbfd967d9
Binary files /dev/null and b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 00000000000..f091b6b0bca
Binary files /dev/null and b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 00000000000..4cde12118dd
Binary files /dev/null and b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 00000000000..d0ef06e7edb
Binary files /dev/null and b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 00000000000..dcdc2306c28
Binary files /dev/null and b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 00000000000..2ccbfd967d9
Binary files /dev/null and b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 00000000000..c8f9ed8f5ce
Binary files /dev/null and b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 00000000000..a6d6b8609df
Binary files /dev/null and b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 00000000000..a6d6b8609df
Binary files /dev/null and b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 00000000000..75b2d164a5a
Binary files /dev/null and b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 00000000000..c4df70d39da
Binary files /dev/null and b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 00000000000..6a84f41e14e
Binary files /dev/null and b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 00000000000..d0e1f585360
Binary files /dev/null and b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/examples/catalog/ios/Runner/Base.lproj/LaunchScreen.storyboard b/examples/catalog/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 00000000000..ebf48f60397
--- /dev/null
+++ b/examples/catalog/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/catalog/ios/Runner/Base.lproj/Main.storyboard b/examples/catalog/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 00000000000..f3c28516fb3
--- /dev/null
+++ b/examples/catalog/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/catalog/ios/Runner/Info.plist b/examples/catalog/ios/Runner/Info.plist
new file mode 100644
index 00000000000..81e0505f069
--- /dev/null
+++ b/examples/catalog/ios/Runner/Info.plist
@@ -0,0 +1,45 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ animated_list
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+
+
diff --git a/examples/catalog/ios/Runner/main.m b/examples/catalog/ios/Runner/main.m
new file mode 100644
index 00000000000..86070722733
--- /dev/null
+++ b/examples/catalog/ios/Runner/main.m
@@ -0,0 +1,14 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import
+#import
+#import "AppDelegate.h"
+
+int main(int argc, char * argv[]) {
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil,
+ NSStringFromClass([AppDelegate class]));
+ }
+}
diff --git a/examples/catalog/lib/animated_list.dart b/examples/catalog/lib/animated_list.dart
new file mode 100644
index 00000000000..4a2a29254db
--- /dev/null
+++ b/examples/catalog/lib/animated_list.dart
@@ -0,0 +1,227 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+class AnimatedListSample extends StatefulWidget {
+ @override
+ _AnimatedListSampleState createState() => _AnimatedListSampleState();
+}
+
+class _AnimatedListSampleState extends State {
+ final GlobalKey _listKey = GlobalKey();
+ ListModel _list;
+ int _selectedItem;
+ int _nextItem; // The next item inserted when the user presses the '+' button.
+
+ @override
+ void initState() {
+ super.initState();
+ _list = ListModel(
+ listKey: _listKey,
+ initialItems: [0, 1, 2],
+ removedItemBuilder: _buildRemovedItem,
+ );
+ _nextItem = 3;
+ }
+
+ // Used to build list items that haven't been removed.
+ Widget _buildItem(BuildContext context, int index, Animation animation) {
+ return CardItem(
+ animation: animation,
+ item: _list[index],
+ selected: _selectedItem == _list[index],
+ onTap: () {
+ setState(() {
+ _selectedItem = _selectedItem == _list[index] ? null : _list[index];
+ });
+ },
+ );
+ }
+
+ // Used to build an item after it has been removed from the list. This method is
+ // needed because a removed item remains visible until its animation has
+ // completed (even though it's gone as far this ListModel is concerned).
+ // The widget will be used by the [AnimatedListState.removeItem] method's
+ // [AnimatedListRemovedItemBuilder] parameter.
+ Widget _buildRemovedItem(int item, BuildContext context, Animation animation) {
+ return CardItem(
+ animation: animation,
+ item: item,
+ selected: false,
+ // No gesture detector here: we don't want removed items to be interactive.
+ );
+ }
+
+ // Insert the "next item" into the list model.
+ void _insert() {
+ final int index = _selectedItem == null ? _list.length : _list.indexOf(_selectedItem);
+ _list.insert(index, _nextItem++);
+ }
+
+ // Remove the selected item from the list model.
+ void _remove() {
+ if (_selectedItem != null) {
+ _list.removeAt(_list.indexOf(_selectedItem));
+ setState(() {
+ _selectedItem = null;
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text('AnimatedList'),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.add_circle),
+ onPressed: _insert,
+ tooltip: 'insert a new item',
+ ),
+ IconButton(
+ icon: const Icon(Icons.remove_circle),
+ onPressed: _remove,
+ tooltip: 'remove the selected item',
+ ),
+ ],
+ ),
+ body: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: 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,
+ }) : assert(listKey != null),
+ assert(removedItemBuilder != null),
+ _items = initialItems?.toList() ?? [];
+
+ final GlobalKey listKey;
+ final Widget Function(E item, BuildContext context, Animation animation) 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 {
+ const CardItem({
+ Key key,
+ @required this.animation,
+ this.onTap,
+ @required this.item,
+ this.selected = false,
+ }) : assert(animation != null),
+ assert(item != null && item >= 0),
+ assert(selected != null),
+ super(key: key);
+
+ final Animation animation;
+ final VoidCallback onTap;
+ final int item;
+ final bool selected;
+
+ @override
+ Widget build(BuildContext context) {
+ TextStyle textStyle = Theme.of(context).textTheme.headline4;
+ if (selected)
+ textStyle = textStyle.copyWith(color: Colors.lightGreenAccent[400]);
+ return Padding(
+ padding: const EdgeInsets.all(2.0),
+ child: SizeTransition(
+ axis: Axis.vertical,
+ sizeFactor: animation,
+ child: GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: onTap,
+ child: SizedBox(
+ height: 128.0,
+ child: Card(
+ color: Colors.primaries[item % Colors.primaries.length],
+ child: Center(
+ child: Text('Item $item', style: textStyle),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+void main() {
+ runApp(AnimatedListSample());
+}
+
+/*
+Sample Catalog
+
+Title: AnimatedList
+
+Summary: An AnimatedList for displaying a list of cards that stay
+in sync with an app-specific ListModel. When an item is added to or removed
+from the model, the corresponding card animates in or out of view.
+
+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
new file mode 100644
index 00000000000..1435b505940
--- /dev/null
+++ b/examples/catalog/lib/app_bar_bottom.dart
@@ -0,0 +1,145 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+class AppBarBottomSample extends StatefulWidget {
+ @override
+ _AppBarBottomSampleState createState() => _AppBarBottomSampleState();
+}
+
+class _AppBarBottomSampleState extends State with SingleTickerProviderStateMixin {
+ TabController _tabController;
+
+ @override
+ void initState() {
+ super.initState();
+ _tabController = 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 MaterialApp(
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text('AppBar Bottom Widget'),
+ leading: IconButton(
+ tooltip: 'Previous choice',
+ icon: const Icon(Icons.arrow_back),
+ onPressed: () { _nextPage(-1); },
+ ),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.arrow_forward),
+ tooltip: 'Next choice',
+ onPressed: () { _nextPage(1); },
+ ),
+ ],
+ bottom: PreferredSize(
+ preferredSize: const Size.fromHeight(48.0),
+ child: Theme(
+ data: Theme.of(context).copyWith(accentColor: Colors.white),
+ child: Container(
+ height: 48.0,
+ alignment: Alignment.center,
+ child: TabPageSelector(controller: _tabController),
+ ),
+ ),
+ ),
+ ),
+ body: TabBarView(
+ controller: _tabController,
+ children: choices.map((Choice choice) {
+ return Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: ChoiceCard(choice: choice),
+ );
+ }).toList(),
+ ),
+ ),
+ );
+ }
+}
+
+class Choice {
+ const Choice({ this.title, this.icon });
+ final String title;
+ final IconData icon;
+}
+
+const List choices = [
+ Choice(title: 'CAR', icon: Icons.directions_car),
+ Choice(title: 'BICYCLE', icon: Icons.directions_bike),
+ Choice(title: 'BOAT', icon: Icons.directions_boat),
+ Choice(title: 'BUS', icon: Icons.directions_bus),
+ Choice(title: 'TRAIN', icon: Icons.directions_railway),
+ Choice(title: 'WALK', icon: Icons.directions_walk),
+];
+
+class ChoiceCard extends StatelessWidget {
+ const ChoiceCard({ Key key, this.choice }) : super(key: key);
+
+ final Choice choice;
+
+ @override
+ Widget build(BuildContext context) {
+ final TextStyle textStyle = Theme.of(context).textTheme.headline4;
+ return Card(
+ color: Colors.white,
+ child: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Icon(choice.icon, size: 128.0, color: textStyle.color),
+ Text(choice.title, style: textStyle),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+void main() {
+ runApp(AppBarBottomSample());
+}
+
+/*
+Sample Catalog
+
+Title: AppBar with a custom bottom widget.
+
+Summary: An AppBar that includes a bottom widget. Any widget
+with a PreferredSize can appear at the bottom of an AppBar.
+
+Summary: Any widget with a PreferredSize can appear at the bottom of an AppBar.
+
+Description:
+Typically an AppBar's bottom widget is a TabBar however any widget with a
+PreferredSize can be used. 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 and they select the previous or the next page.
+
+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
new file mode 100644
index 00000000000..9c4f8ffeeac
--- /dev/null
+++ b/examples/catalog/lib/basic_app_bar.dart
@@ -0,0 +1,121 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+// This app is a stateful, it tracks the user's current choice.
+class BasicAppBarSample extends StatefulWidget {
+ @override
+ _BasicAppBarSampleState createState() => _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 MaterialApp(
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text('Basic AppBar'),
+ actions: [
+ IconButton( // action button
+ icon: Icon(choices[0].icon),
+ onPressed: () { _select(choices[0]); },
+ ),
+ IconButton( // action button
+ icon: Icon(choices[1].icon),
+ onPressed: () { _select(choices[1]); },
+ ),
+ PopupMenuButton( // overflow menu
+ onSelected: _select,
+ itemBuilder: (BuildContext context) {
+ return choices.skip(2).map>((Choice choice) {
+ return PopupMenuItem(
+ value: choice,
+ child: Text(choice.title),
+ );
+ }).toList();
+ },
+ ),
+ ],
+ ),
+ body: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: ChoiceCard(choice: _selectedChoice),
+ ),
+ ),
+ );
+ }
+}
+
+class Choice {
+ const Choice({ this.title, this.icon });
+ final String title;
+ final IconData icon;
+}
+
+const List choices = [
+ Choice(title: 'Car', icon: Icons.directions_car),
+ Choice(title: 'Bicycle', icon: Icons.directions_bike),
+ Choice(title: 'Boat', icon: Icons.directions_boat),
+ Choice(title: 'Bus', icon: Icons.directions_bus),
+ Choice(title: 'Train', icon: Icons.directions_railway),
+ Choice(title: 'Walk', icon: Icons.directions_walk),
+];
+
+class ChoiceCard extends StatelessWidget {
+ const ChoiceCard({ Key key, this.choice }) : super(key: key);
+
+ final Choice choice;
+
+ @override
+ Widget build(BuildContext context) {
+ final TextStyle textStyle = Theme.of(context).textTheme.headline4;
+ return Card(
+ color: Colors.white,
+ child: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Icon(choice.icon, size: 128.0, color: textStyle.color),
+ Text(choice.title, style: textStyle),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+void main() {
+ runApp(BasicAppBarSample());
+}
+
+/*
+Sample Catalog
+
+Title: AppBar Basics
+
+Summary: A basic AppBar with a title, actions, and an overflow dropdown 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 dropdown 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/custom_a11y_traversal.dart b/examples/catalog/lib/custom_a11y_traversal.dart
new file mode 100644
index 00000000000..efb5f423701
--- /dev/null
+++ b/examples/catalog/lib/custom_a11y_traversal.dart
@@ -0,0 +1,320 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter/semantics.dart';
+
+/// This example shows a set of widgets for changing data fields arranged in a
+/// column of rows but, in accessibility mode, are traversed in a custom order.
+///
+/// This demonstrates how Flutter's accessibility system describes custom
+/// traversal orders using sort keys.
+///
+/// The example app here has three fields that have a title and up/down spinner
+/// buttons above and below. The traversal order should allow the user to focus
+/// on each title, the input field next, the up spinner next, and the down
+/// spinner last before moving to the next input title.
+///
+/// Users that do not use a screen reader (e.g. TalkBack on Android and
+/// VoiceOver on iOS) will just see a regular app with controls.
+///
+/// The example's [RowColumnTraversal] widget sets up two [Semantics] objects
+/// that wrap the given [Widget] child, providing the traversal order they
+/// should have in the "row" direction, and then the traversal order they should
+/// have in the "column" direction.
+///
+/// Since widgets are globally sorted by their sort key, the order does not have
+/// to conform to the widget hierarchy. Indeed, in this example, we traverse
+/// vertically first, but the widget hierarchy is a column of rows.
+///
+/// See also:
+///
+/// * [Semantics] for an object that annotates widgets with accessibility semantics
+/// (including traversal order).
+/// * [SemanticSortKey] for the base class of all semantic sort keys.
+/// * [OrdinalSortKey] for a concrete sort key that sorts based on the given ordinal.
+class RowColumnTraversal extends StatelessWidget {
+ const RowColumnTraversal({this.rowOrder, this.columnOrder, this.child});
+
+ final int rowOrder;
+ final int columnOrder;
+ final Widget child;
+
+ /// Builds a widget hierarchy that wraps [child].
+ ///
+ /// This function expresses the sort keys as a hierarchy.
+ @override
+ Widget build(BuildContext context) {
+ return Semantics(
+ sortKey: OrdinalSortKey(columnOrder.toDouble()),
+ child: Semantics(
+ sortKey: OrdinalSortKey(rowOrder.toDouble()),
+ child: child,
+ ),
+ );
+ }
+}
+
+// --------------- Component widgets ---------------------
+
+/// A Button class that wraps an [IconButton] with a [RowColumnTraversal] to
+/// set its traversal order.
+class SpinnerButton extends StatelessWidget {
+ const SpinnerButton({
+ Key key,
+ this.onPressed,
+ this.icon,
+ this.rowOrder,
+ this.columnOrder,
+ this.field,
+ this.increment,
+ }) : super(key: key);
+
+ final VoidCallback onPressed;
+ final IconData icon;
+ final int rowOrder;
+ final int columnOrder;
+ final Field field;
+ final bool increment;
+
+ @override
+ Widget build(BuildContext context) {
+ final String label = '${increment ? 'Increment' : 'Decrement'} ${_fieldToName(field)}';
+
+ return RowColumnTraversal(
+ rowOrder: rowOrder,
+ columnOrder: columnOrder,
+ child: Center(
+ child: IconButton(
+ icon: Icon(icon),
+ onPressed: onPressed,
+ tooltip: label,
+ ),
+ ),
+ );
+ }
+}
+
+/// A text entry field that wraps a [TextField] with a [RowColumnTraversal] to
+/// set its traversal order.
+class FieldWidget extends StatelessWidget {
+ const FieldWidget({
+ Key key,
+ this.rowOrder,
+ this.columnOrder,
+ this.onIncrease,
+ this.onDecrease,
+ this.value,
+ this.field,
+ }) : super(key: key);
+
+ final int rowOrder;
+ final int columnOrder;
+ final VoidCallback onDecrease;
+ final VoidCallback onIncrease;
+ final int value;
+ final Field field;
+
+ @override
+ Widget build(BuildContext context) {
+ final String stringValue = '${_fieldToName(field)} $value';
+ final String increasedValue = '${_fieldToName(field)} ${value + 1}';
+ final String decreasedValue = '${_fieldToName(field)} ${value - 1}';
+
+ return RowColumnTraversal(
+ rowOrder: rowOrder,
+ columnOrder: columnOrder,
+ child: Center(
+ child: Semantics(
+ onDecrease: onDecrease,
+ onIncrease: onIncrease,
+ value: stringValue,
+ increasedValue: increasedValue,
+ decreasedValue: decreasedValue,
+ child: ExcludeSemantics(child: Text(value.toString())),
+ ),
+ ),
+ );
+ }
+}
+
+// --------------- Field manipulation functions ---------------------
+
+/// An enum that describes which column we're referring to.
+enum Field { DOGS, CATS, FISH }
+
+String _fieldToName(Field field) {
+ switch (field) {
+ case Field.DOGS:
+ return 'Dogs';
+ case Field.CATS:
+ return 'Cats';
+ case Field.FISH:
+ return 'Fish';
+ }
+ return null;
+}
+
+// --------------- Main app ---------------------
+
+/// The top-level example widget that serves as the body of the app.
+class CustomTraversalExample extends StatefulWidget {
+ @override
+ CustomTraversalExampleState createState() => CustomTraversalExampleState();
+}
+
+/// The state object for the top level example widget.
+class CustomTraversalExampleState extends State {
+ /// The fields that we are manipulating. List indices correspond to
+ /// the entries in the [Field] enum.
+ List fields = [0, 0, 0];
+
+ void _addToField(Field field, int delta) {
+ setState(() {
+ fields[field.index] += delta;
+ });
+ }
+
+ Widget _makeFieldHeader(int rowOrder, int columnOrder, Field field) {
+ return RowColumnTraversal(
+ rowOrder: rowOrder,
+ columnOrder: columnOrder,
+ child: Text(_fieldToName(field)),
+ );
+ }
+
+ Widget _makeSpinnerButton(int rowOrder, int columnOrder, Field field, {bool increment = true}) {
+ return SpinnerButton(
+ rowOrder: rowOrder,
+ columnOrder: columnOrder,
+ icon: increment ? Icons.arrow_upward : Icons.arrow_downward,
+ onPressed: () => _addToField(field, increment ? 1 : -1),
+ field: field,
+ increment: increment,
+ );
+ }
+
+ Widget _makeEntryField(int rowOrder, int columnOrder, Field field) {
+ return FieldWidget(
+ rowOrder: rowOrder,
+ columnOrder: columnOrder,
+ onIncrease: () => _addToField(field, 1),
+ onDecrease: () => _addToField(field, -1),
+ value: fields[field.index],
+ field: field,
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text('Pet Inventory'),
+ ),
+ body: Builder(
+ builder: (BuildContext context) {
+ return DefaultTextStyle(
+ style: DefaultTextStyle.of(context).style.copyWith(fontSize: 21.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Semantics(
+ // Since this is the only sort key that the text has, it
+ // will be compared with the 'column' OrdinalSortKeys of all the
+ // fields, because the column sort keys are first in the other fields.
+ //
+ // An ordinal of "0.0" means that it will be traversed before column 1.
+ sortKey: const OrdinalSortKey(0.0),
+ child: const Text(
+ 'How many pets do you own?',
+ ),
+ ),
+ const Padding(padding: EdgeInsets.symmetric(vertical: 10.0)),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _makeFieldHeader(1, 0, Field.DOGS),
+ _makeFieldHeader(1, 1, Field.CATS),
+ _makeFieldHeader(1, 2, Field.FISH),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _makeSpinnerButton(3, 0, Field.DOGS, increment: true),
+ _makeSpinnerButton(3, 1, Field.CATS, increment: true),
+ _makeSpinnerButton(3, 2, Field.FISH, increment: true),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _makeEntryField(2, 0, Field.DOGS),
+ _makeEntryField(2, 1, Field.CATS),
+ _makeEntryField(2, 2, Field.FISH),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _makeSpinnerButton(4, 0, Field.DOGS, increment: false),
+ _makeSpinnerButton(4, 1, Field.CATS, increment: false),
+ _makeSpinnerButton(4, 2, Field.FISH, increment: false),
+ ],
+ ),
+ const Padding(padding: EdgeInsets.symmetric(vertical: 10.0)),
+ Semantics(
+ // Since this is the only sort key that the reset button has, it
+ // will be compared with the 'column' OrdinalSortKeys of all the
+ // fields, because the column sort keys are first in the other fields.
+ //
+ // an ordinal of "5.0" means that it will be traversed after column 4.
+ sortKey: const OrdinalSortKey(5.0),
+ child: MaterialButton(
+ child: const Text('RESET'),
+ textTheme: ButtonTextTheme.normal,
+ textColor: Colors.blue,
+ onPressed: () {
+ setState(() {
+ fields = [0, 0, 0];
+ });
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ ),
+ ),
+ );
+ }
+}
+
+void main() {
+ runApp(CustomTraversalExample());
+}
+
+/*
+Sample Catalog
+
+Title: CustomTraversalExample
+
+Summary: An app that demonstrates a custom semantics traversal order.
+
+Description:
+This app presents a value selection interface where the fields can be
+incremented or decremented using spinner arrows. In accessibility mode, the
+widgets are traversed in a custom order from one column to the next, starting
+with the column title, moving to the input field, then to the "up" increment
+button, and lastly to the "down" decrement button.
+
+When not in accessibility mode, the app works as one would expect.
+
+Classes: Semantics
+
+Sample: CustomTraversalExample
+*/
diff --git a/examples/catalog/lib/custom_semantics.dart b/examples/catalog/lib/custom_semantics.dart
new file mode 100644
index 00000000000..efe9e8afb90
--- /dev/null
+++ b/examples/catalog/lib/custom_semantics.dart
@@ -0,0 +1,152 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+/// A [ListTile] containing a dropdown menu that exposes itself as an
+/// "Adjustable" to screen readers (e.g. TalkBack on Android and VoiceOver on
+/// iOS).
+///
+/// This allows screen reader users to swipe up/down (on iOS) or use the volume
+/// keys (on Android) to switch between the values in the dropdown menu.
+/// Depending on what the values in the dropdown menu are this can be a more
+/// intuitive way of switching values compared to exposing the content of the
+/// drop down menu as a screen overlay from which the user can select.
+///
+/// Users that do not use a screen reader will just see a regular dropdown menu.
+class AdjustableDropdownListTile extends StatelessWidget {
+ const AdjustableDropdownListTile({
+ this.label,
+ this.value,
+ this.items,
+ this.onChanged,
+ });
+
+ final String label;
+ final String value;
+ final List items;
+ final ValueChanged onChanged;
+
+ @override
+ Widget build(BuildContext context) {
+ final int indexOfValue = items.indexOf(value);
+ assert(indexOfValue != -1);
+
+ final bool canIncrease = indexOfValue < items.length - 1;
+ final bool canDecrease = indexOfValue > 0;
+
+ return Semantics(
+ container: true,
+ label: label,
+ value: value,
+ increasedValue: canIncrease ? _increasedValue : null,
+ decreasedValue: canDecrease ? _decreasedValue : null,
+ onIncrease: canIncrease ? _performIncrease : null,
+ onDecrease: canDecrease ? _performDecrease : null,
+ child: ExcludeSemantics(
+ child: ListTile(
+ title: Text(label),
+ trailing: DropdownButton(
+ value: value,
+ onChanged: onChanged,
+ items: items.map>((String item) {
+ return DropdownMenuItem(
+ value: item,
+ child: Text(item),
+ );
+ }).toList(),
+ ),
+ ),
+ ),
+ );
+ }
+
+ String get _increasedValue {
+ final int indexOfValue = items.indexOf(value);
+ assert(indexOfValue < items.length - 1);
+ return items[indexOfValue + 1];
+ }
+
+ String get _decreasedValue {
+ final int indexOfValue = items.indexOf(value);
+ assert(indexOfValue > 0);
+ return items[indexOfValue - 1];
+ }
+
+ void _performIncrease() => onChanged(_increasedValue);
+
+ void _performDecrease() => onChanged(_decreasedValue);
+}
+
+class AdjustableDropdownExample extends StatefulWidget {
+ @override
+ AdjustableDropdownExampleState createState() => AdjustableDropdownExampleState();
+}
+
+class AdjustableDropdownExampleState extends State {
+
+ final List items = [
+ '1 second',
+ '5 seconds',
+ '15 seconds',
+ '30 seconds',
+ '1 minute',
+ ];
+ String timeout;
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text('Adjustable DropDown'),
+ ),
+ body: ListView(
+ children: [
+ AdjustableDropdownListTile(
+ label: 'Timeout',
+ value: timeout ?? items[2],
+ items: items,
+ onChanged: (String value) {
+ setState(() {
+ timeout = value;
+ });
+ },
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+void main() {
+ runApp(AdjustableDropdownExample());
+}
+
+/*
+Sample Catalog
+
+Title: AdjustableDropdownListTile
+
+Summary: A dropdown menu that exposes itself as an "Adjustable" to screen
+readers.
+
+Description:
+This app presents a dropdown menu to the user that exposes itself as an
+"Adjustable" to screen readers (e.g. TalkBack on Android and VoiceOver on iOS).
+This allows users of screen readers to cycle through the values of the dropdown
+menu by swiping up or down on the screen with one finger (on iOS) or by using
+the volume keys (on Android). Depending on the values in the dropdown this
+behavior may be more intuitive to screen reader users compared to showing the
+classical dropdown overlay on screen to choose a value.
+
+When the screen reader is turned off, the dropdown menu behaves like any
+dropdown menu would.
+
+Classes: Semantics
+
+Sample: AdjustableDropdownListTile
+
+*/
diff --git a/examples/catalog/lib/expansion_tile_sample.dart b/examples/catalog/lib/expansion_tile_sample.dart
new file mode 100644
index 00000000000..96c024be592
--- /dev/null
+++ b/examples/catalog/lib/expansion_tile_sample.dart
@@ -0,0 +1,121 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+class ExpansionTileSample extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text('ExpansionTile'),
+ ),
+ body: ListView.builder(
+ itemBuilder: (BuildContext context, int index) => 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 = [
+ Entry('Chapter A',
+ [
+ Entry('Section A0',
+ [
+ Entry('Item A0.1'),
+ Entry('Item A0.2'),
+ Entry('Item A0.3'),
+ ],
+ ),
+ Entry('Section A1'),
+ Entry('Section A2'),
+ ],
+ ),
+ Entry('Chapter B',
+ [
+ Entry('Section B0'),
+ Entry('Section B1'),
+ ],
+ ),
+ Entry('Chapter C',
+ [
+ Entry('Section C0'),
+ Entry('Section C1'),
+ Entry('Section C2',
+ [
+ Entry('Item C2.0'),
+ Entry('Item C2.1'),
+ Entry('Item C2.2'),
+ Entry('Item C2.3'),
+ ],
+ ),
+ ],
+ ),
+];
+
+// Displays one Entry. If the entry has children then it's displayed
+// with an ExpansionTile.
+class EntryItem extends StatelessWidget {
+ const EntryItem(this.entry);
+
+ final Entry entry;
+
+ Widget _buildTiles(Entry root) {
+ if (root.children.isEmpty)
+ return ListTile(title: Text(root.title));
+ return ExpansionTile(
+ key: PageStorageKey(root),
+ title: Text(root.title),
+ children: root.children.map(_buildTiles).toList(),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return _buildTiles(entry);
+ }
+}
+
+void main() {
+ runApp(ExpansionTileSample());
+}
+
+/*
+Sample Catalog
+
+Title: ExpansionTile
+
+Summary: An ExpansionTile for building nested lists, with two or more levels.
+
+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.
+
+When displayed within a scrollable that creates its list items lazily,
+like a scrollable list created with `ListView.builder()`, ExpansionTiles
+can be quite efficient, particularly for material design "expand/collapse"
+lists.
+
+
+Classes: ExpansionTile, ListView
+
+Sample: ExpansionTileSample
+
+See also:
+ - The "expand/collapse" part of the material design specification:
+
+*/
diff --git a/examples/catalog/lib/main.dart b/examples/catalog/lib/main.dart
new file mode 100644
index 00000000000..c3fb7d315cf
--- /dev/null
+++ b/examples/catalog/lib/main.dart
@@ -0,0 +1,16 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/widgets.dart';
+
+void main() {
+ runApp(
+ const Center(
+ child: Text(
+ 'Instead run:\nflutter run lib/xxx.dart',
+ textDirection: TextDirection.ltr,
+ ),
+ ),
+ );
+}
diff --git a/examples/catalog/lib/tabbed_app_bar.dart b/examples/catalog/lib/tabbed_app_bar.dart
new file mode 100644
index 00000000000..f5890a1d9cb
--- /dev/null
+++ b/examples/catalog/lib/tabbed_app_bar.dart
@@ -0,0 +1,102 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+class TabbedAppBarSample extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: DefaultTabController(
+ length: choices.length,
+ child: Scaffold(
+ appBar: AppBar(
+ title: const Text('Tabbed AppBar'),
+ bottom: TabBar(
+ isScrollable: true,
+ tabs: choices.map((Choice choice) {
+ return Tab(
+ text: choice.title,
+ icon: Icon(choice.icon),
+ );
+ }).toList(),
+ ),
+ ),
+ body: TabBarView(
+ children: choices.map((Choice choice) {
+ return Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: ChoiceCard(choice: choice),
+ );
+ }).toList(),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class Choice {
+ const Choice({ this.title, this.icon });
+ final String title;
+ final IconData icon;
+}
+
+const List choices = [
+ Choice(title: 'CAR', icon: Icons.directions_car),
+ Choice(title: 'BICYCLE', icon: Icons.directions_bike),
+ Choice(title: 'BOAT', icon: Icons.directions_boat),
+ Choice(title: 'BUS', icon: Icons.directions_bus),
+ Choice(title: 'TRAIN', icon: Icons.directions_railway),
+ Choice(title: 'WALK', icon: Icons.directions_walk),
+];
+
+class ChoiceCard extends StatelessWidget {
+ const ChoiceCard({ Key key, this.choice }) : super(key: key);
+
+ final Choice choice;
+
+ @override
+ Widget build(BuildContext context) {
+ final TextStyle textStyle = Theme.of(context).textTheme.headline4;
+ return Card(
+ color: Colors.white,
+ child: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Icon(choice.icon, size: 128.0, color: textStyle.color),
+ Text(choice.title, style: textStyle),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+void main() {
+ runApp(TabbedAppBarSample());
+}
+
+/*
+Sample Catalog
+
+Title: Tabbed AppBar
+
+Summary: An AppBar with a TabBar for navigating pages just below it.
+
+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 anywhere, 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/pubspec.yaml b/examples/catalog/pubspec.yaml
new file mode 100644
index 00000000000..dece40d1069
--- /dev/null
+++ b/examples/catalog/pubspec.yaml
@@ -0,0 +1,84 @@
+name: sample_catalog
+description: A collection of Flutter sample apps
+
+environment:
+ # The pub client defaults to an <2.0.0 sdk constraint which we need to explicitly overwrite.
+ sdk: ">=2.0.0-dev.68.0 <3.0.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+ path: 1.8.0-nullsafety.1
+
+ characters: 1.1.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ collection: 1.15.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ meta: 1.3.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ typed_data: 1.3.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ vector_math: 2.1.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+ flutter_driver:
+ sdk: flutter
+ test: 1.16.0-nullsafety.5
+
+ _fe_analyzer_shared: 7.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ analyzer: 0.39.17 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ archive: 2.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ args: 1.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ async: 2.5.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ boolean_selector: 2.1.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ charcode: 1.2.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ cli_util: 0.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ clock: 1.1.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ coverage: 0.14.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ crypto: 2.1.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ csslib: 0.16.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ fake_async: 1.2.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ file: 6.0.0-nullsafety.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ glob: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ html: 0.14.0+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ http: 0.12.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ http_multi_server: 2.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ http_parser: 3.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ io: 0.3.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ js: 0.6.3-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ json_rpc_2: 2.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ logging: 0.11.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ matcher: 0.12.10-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ mime: 0.9.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ node_interop: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ node_io: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ node_preamble: 1.4.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ package_config: 1.9.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ pedantic: 1.10.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ pool: 1.5.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ pub_semver: 1.4.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ shelf: 0.7.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ shelf_packages_handler: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ shelf_static: 0.2.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ shelf_web_socket: 0.2.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ source_map_stack_trace: 2.1.0-nullsafety.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ source_maps: 0.10.10-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ source_span: 1.8.0-nullsafety.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ stack_trace: 1.10.0-nullsafety.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ stream_channel: 2.1.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ string_scanner: 1.1.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ sync_http: 0.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ term_glyph: 1.2.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ test_api: 0.2.19-nullsafety.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ test_core: 0.3.12-nullsafety.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ vm_service: 4.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ watcher: 0.9.7+15 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ web_socket_channel: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ webdriver: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ webkit_inspection_protocol: 0.7.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ yaml: 2.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+
+flutter:
+ uses-material-design: true
+
+# PUBSPEC CHECKSUM: 5ffe
diff --git a/examples/catalog/test/animated_list_test.dart b/examples/catalog/test/animated_list_test.dart
new file mode 100644
index 00000000000..c4eb8964bb4
--- /dev/null
+++ b/examples/catalog/test/animated_list_test.dart
@@ -0,0 +1,64 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:sample_catalog/animated_list.dart' as animated_list_sample;
+
+void main() {
+ testWidgets('animated_list sample app smoke test', (WidgetTester tester) async {
+ animated_list_sample.main();
+ await tester.pump();
+
+ expect(find.text('Item 0'), findsOneWidget);
+ expect(find.text('Item 1'), findsOneWidget);
+ expect(find.text('Item 2'), findsOneWidget);
+
+ final Finder insertButton = find.byTooltip('insert a new item');
+ final Finder removeButton = find.byTooltip('remove the selected item');
+ expect(insertButton, findsOneWidget);
+ expect(removeButton, findsOneWidget);
+
+ // Remove items 0, 1, 2.
+ await tester.tap(find.text('Item 0'));
+ await tester.tap(removeButton);
+ await tester.pumpAndSettle();
+ await tester.tap(find.text('Item 1'));
+ await tester.tap(removeButton);
+ await tester.pumpAndSettle();
+ await tester.tap(find.text('Item 2'));
+ await tester.tap(removeButton);
+ await tester.pumpAndSettle();
+
+ // Append items 3, 4, 5, 6.
+ await tester.tap(insertButton);
+ await tester.tap(insertButton);
+ await tester.tap(insertButton);
+ await tester.tap(insertButton);
+ await tester.pumpAndSettle();
+
+ expect(find.text('Item 0'), findsNothing);
+ expect(find.text('Item 1'), findsNothing);
+ expect(find.text('Item 2'), findsNothing);
+ expect(find.text('Item 3'), findsOneWidget);
+ expect(find.text('Item 4'), findsOneWidget);
+ expect(find.text('Item 5'), findsOneWidget);
+ expect(find.text('Item 6'), findsOneWidget);
+
+ // Insert items 7, 8 at item 3's position (at the top)
+ await tester.tap(find.text('Item 3'));
+ await tester.tap(insertButton);
+ await tester.tap(insertButton);
+ await tester.pumpAndSettle();
+
+ expect(find.text('Item 7'), findsOneWidget);
+ expect(find.text('Item 8'), findsOneWidget);
+
+ // Scroll to the end.
+ await tester.fling(find.text('Item 7'), const Offset(0.0, -200.0), 1000.0);
+ await tester.pumpAndSettle();
+ expect(find.text('Item 6'), findsOneWidget);
+ expect(find.text('Item 8'), findsNothing);
+ });
+}
diff --git a/examples/catalog/test/app_bar_bottom_test.dart b/examples/catalog/test/app_bar_bottom_test.dart
new file mode 100644
index 00000000000..2e4c763121a
--- /dev/null
+++ b/examples/catalog/test/app_bar_bottom_test.dart
@@ -0,0 +1,37 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:sample_catalog/app_bar_bottom.dart' as app_bar_bottom_sample;
+
+final int choiceCount = app_bar_bottom_sample.choices.length;
+IconData iconAt(int index) => app_bar_bottom_sample.choices[index].icon;
+
+Finder findChoiceCard(IconData icon) {
+ return find.descendant(of: find.byType(Card), matching: find.byIcon(icon));
+}
+
+void main() {
+ testWidgets('app_bar_bottom sample smoke test', (WidgetTester tester) async {
+ app_bar_bottom_sample.main();
+ await tester.pump();
+
+ // Cycle through the choices using the forward and backwards arrows.
+
+ final Finder nextChoice = find.byTooltip('Next choice');
+ for (int i = 0; i < choiceCount; i += 1) {
+ expect(findChoiceCard(iconAt(i)), findsOneWidget);
+ await tester.tap(nextChoice);
+ await tester.pumpAndSettle();
+ }
+
+ final Finder previousChoice = find.byTooltip('Previous choice');
+ for (int i = choiceCount - 1; i >= 0; i -= 1) {
+ expect(findChoiceCard(iconAt(i)), findsOneWidget);
+ await tester.tap(previousChoice);
+ await tester.pumpAndSettle();
+ }
+ });
+}
diff --git a/examples/catalog/test/basic_app_bar_test.dart b/examples/catalog/test/basic_app_bar_test.dart
new file mode 100644
index 00000000000..321f73cb3ea
--- /dev/null
+++ b/examples/catalog/test/basic_app_bar_test.dart
@@ -0,0 +1,45 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:sample_catalog/basic_app_bar.dart' as basic_app_bar_sample;
+
+int choiceCount = basic_app_bar_sample.choices.length;
+IconData iconAt(int index) => basic_app_bar_sample.choices[index].icon;
+String titleAt(int index) => basic_app_bar_sample.choices[index].title;
+
+Finder findAppBarIcon(IconData icon) {
+ return find.descendant(of: find.byType(AppBar), matching: find.byIcon(icon));
+}
+
+Finder findChoiceCard(IconData icon) {
+ return find.descendant(of: find.byType(Card), matching: find.byIcon(icon));
+}
+
+void main() {
+ testWidgets('basic_app_bar sample smoke test', (WidgetTester tester) async {
+ basic_app_bar_sample.main();
+ await tester.pump();
+
+ // Tap on the two action buttons and all of the overflow menu items.
+ // Verify that a Card with the expected icon appears.
+
+ await tester.tap(findAppBarIcon(iconAt(0)));
+ await tester.pumpAndSettle();
+ expect(findChoiceCard(iconAt(0)), findsOneWidget);
+
+ await tester.tap(findAppBarIcon(iconAt(1)));
+ await tester.pumpAndSettle();
+ expect(findChoiceCard(iconAt(1)), findsOneWidget);
+
+ for (int i = 2; i < choiceCount; i += 1) {
+ await tester.tap(findAppBarIcon(Icons.more_vert));
+ await tester.pumpAndSettle();
+ await tester.tap(find.text(titleAt(i)));
+ await tester.pumpAndSettle();
+ expect(findChoiceCard(iconAt(i)), findsOneWidget);
+ }
+ });
+}
diff --git a/examples/catalog/test/custom_semantics_test.dart b/examples/catalog/test/custom_semantics_test.dart
new file mode 100644
index 00000000000..dd8217679d6
--- /dev/null
+++ b/examples/catalog/test/custom_semantics_test.dart
@@ -0,0 +1,113 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/rendering.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:sample_catalog/custom_semantics.dart' as custom_semantics show main;
+import 'package:sample_catalog/custom_semantics.dart';
+
+void main() {
+ testWidgets('custom_semantics sample smoke test', (WidgetTester tester) async {
+ // Turn on Semantics
+ final SemanticsHandle semanticsHandler = tester.binding.pipelineOwner.ensureSemantics();
+ final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;
+
+ // Build the sample app
+ custom_semantics.main();
+ await tester.pump();
+
+ // Verify it correctly exposes its semantics.
+ final SemanticsNode semantics = tester.getSemantics(find.byType(AdjustableDropdownListTile));
+
+ expectAdjustable(semantics,
+ hasIncreaseAction: true,
+ hasDecreaseAction: true,
+ label: 'Timeout',
+ decreasedValue: '5 seconds',
+ value: '15 seconds',
+ increasedValue: '30 seconds',
+ );
+
+ // Increase
+ semanticsOwner.performAction(semantics.id, SemanticsAction.increase);
+ await tester.pump();
+
+ expectAdjustable(semantics,
+ hasIncreaseAction: true,
+ hasDecreaseAction: true,
+ label: 'Timeout',
+ decreasedValue: '15 seconds',
+ value: '30 seconds',
+ increasedValue: '1 minute',
+ );
+
+ // Increase all the way to highest value
+ semanticsOwner.performAction(semantics.id, SemanticsAction.increase);
+ await tester.pump();
+
+ expectAdjustable(semantics,
+ hasIncreaseAction: false,
+ hasDecreaseAction: true,
+ label: 'Timeout',
+ decreasedValue: '30 seconds',
+ value: '1 minute',
+ );
+
+ // Decrease
+ semanticsOwner.performAction(semantics.id, SemanticsAction.decrease);
+ await tester.pump();
+
+ expectAdjustable(semantics,
+ hasIncreaseAction: true,
+ hasDecreaseAction: true,
+ label: 'Timeout',
+ decreasedValue: '15 seconds',
+ value: '30 seconds',
+ increasedValue: '1 minute',
+ );
+
+ // Decrease all the way to lowest value
+ semanticsOwner.performAction(semantics.id, SemanticsAction.decrease);
+ await tester.pump();
+ semanticsOwner.performAction(semantics.id, SemanticsAction.decrease);
+ await tester.pump();
+ semanticsOwner.performAction(semantics.id, SemanticsAction.decrease);
+ await tester.pump();
+
+ expectAdjustable(semantics,
+ hasIncreaseAction: true,
+ hasDecreaseAction: false,
+ label: 'Timeout',
+ value: '1 second',
+ increasedValue: '5 seconds',
+ );
+
+ // Clean-up
+ semanticsHandler.dispose();
+ });
+}
+
+void expectAdjustable(
+ SemanticsNode node, {
+ bool hasIncreaseAction = true,
+ bool hasDecreaseAction = true,
+ String label = '',
+ String decreasedValue = '',
+ String value = '',
+ String increasedValue = '',
+}) {
+ final SemanticsData semanticsData = node.getSemanticsData();
+
+ int actions = 0;
+ if (hasIncreaseAction)
+ actions |= SemanticsAction.increase.index;
+ if (hasDecreaseAction)
+ actions |= SemanticsAction.decrease.index;
+
+ expect(semanticsData.actions, actions);
+ expect(semanticsData.label, label);
+ expect(semanticsData.decreasedValue, decreasedValue);
+ expect(semanticsData.value, value);
+ expect(semanticsData.increasedValue, increasedValue);
+}
diff --git a/examples/catalog/test/expansion_tile_sample_test.dart b/examples/catalog/test/expansion_tile_sample_test.dart
new file mode 100644
index 00000000000..9188652da23
--- /dev/null
+++ b/examples/catalog/test/expansion_tile_sample_test.dart
@@ -0,0 +1,90 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:sample_catalog/expansion_tile_sample.dart' as expansion_tile_sample;
+import 'package:sample_catalog/expansion_tile_sample.dart' show Entry;
+
+void main() {
+ testWidgets('expansion_tile sample smoke test', (WidgetTester tester) async {
+ expansion_tile_sample.main();
+ await tester.pump();
+
+ // Initially only the top level EntryItems (the "chapters") are present.
+ for (final Entry chapter in expansion_tile_sample.data) {
+ expect(find.text(chapter.title), findsOneWidget);
+ for (final Entry section in chapter.children) {
+ expect(find.text(section.title), findsNothing);
+ for (final Entry item in section.children)
+ expect(find.text(item.title), findsNothing);
+ }
+ }
+
+ Future scrollUpOneEntry() async {
+ await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, -88.00));
+ await tester.pumpAndSettle();
+ }
+
+ Future tapEntry(String title) async {
+ await tester.tap(find.text(title));
+ await tester.pumpAndSettle();
+ }
+
+ // Expand the chapters. Now the chapter and sections, but not the
+ // items, should be present.
+ for (final Entry chapter in expansion_tile_sample.data.reversed)
+ await tapEntry(chapter.title);
+
+ for (final Entry chapter in expansion_tile_sample.data) {
+ expect(find.text(chapter.title), findsOneWidget);
+ for (final Entry section in chapter.children) {
+ expect(find.text(section.title), findsOneWidget);
+ await scrollUpOneEntry();
+ for (final Entry item in section.children)
+ expect(find.text(item.title), findsNothing);
+ }
+ await scrollUpOneEntry();
+ }
+
+ // - scroll to the top -
+ await tester.flingFrom(const Offset(200.0, 200.0), const Offset(0.0, 100.0), 5000.0);
+ await tester.pumpAndSettle();
+
+ // Expand the sections. Now Widgets for all three levels should be present.
+ for (final Entry chapter in expansion_tile_sample.data) {
+ for (final Entry section in chapter.children) {
+ await tapEntry(section.title);
+ await scrollUpOneEntry();
+ }
+ await scrollUpOneEntry();
+ }
+
+ // We're scrolled to the bottom so the very last item is visible.
+ // Working in reverse order, so we don't need to do anymore scrolling,
+ // check that everything is visible and close the sections and
+ // chapters as we go up.
+ for (final Entry chapter in expansion_tile_sample.data.reversed) {
+ expect(find.text(chapter.title), findsOneWidget);
+ for (final Entry section in chapter.children.reversed) {
+ expect(find.text(section.title), findsOneWidget);
+ for (final Entry item in section.children.reversed)
+ expect(find.text(item.title), findsOneWidget);
+ await tapEntry(section.title); // close the section
+ }
+ await tapEntry(chapter.title); // close the chapter
+ }
+
+ // Finally only the top level EntryItems (the "chapters") are present.
+ for (final Entry chapter in expansion_tile_sample.data) {
+ expect(find.text(chapter.title), findsOneWidget);
+ for (final Entry section in chapter.children) {
+ expect(find.text(section.title), findsNothing);
+ for (final Entry item in section.children)
+ expect(find.text(item.title), findsNothing);
+ }
+ }
+
+ });
+}
diff --git a/examples/catalog/test/tabbed_app_bar_test.dart b/examples/catalog/test/tabbed_app_bar_test.dart
new file mode 100644
index 00000000000..59d33cc2556
--- /dev/null
+++ b/examples/catalog/test/tabbed_app_bar_test.dart
@@ -0,0 +1,35 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:sample_catalog/tabbed_app_bar.dart' as tabbed_app_bar_sample;
+
+final int choiceCount = tabbed_app_bar_sample.choices.length;
+IconData iconAt(int index) => tabbed_app_bar_sample.choices[index].icon;
+
+Finder findChoiceCard(IconData icon) {
+ return find.descendant(of: find.byType(Card), matching: find.byIcon(icon));
+}
+
+Finder findTab(IconData icon) {
+ return find.descendant(of: find.byType(Tab), matching: find.byIcon(icon));
+}
+
+void main() {
+ testWidgets('tabbed_app_bar sample smoke test', (WidgetTester tester) async {
+ tabbed_app_bar_sample.main();
+ await tester.pump();
+
+ // Tap on each tab, verify that a Card with the expected icon appears.
+ for (int i = 0; i < choiceCount; i += 1) {
+ await tester.tap(findTab(iconAt(i)));
+ await tester.pumpAndSettle();
+ expect(findChoiceCard(iconAt(i)), findsOneWidget);
+ // Scroll the tabBar by about one tab width
+ await tester.drag(find.byType(TabBar), const Offset(-24.0, 0.0));
+ await tester.pumpAndSettle();
+ }
+ });
+}
diff --git a/examples/catalog/test_driver/README.md b/examples/catalog/test_driver/README.md
new file mode 100644
index 00000000000..8622c89978e
--- /dev/null
+++ b/examples/catalog/test_driver/README.md
@@ -0,0 +1 @@
+The screenshot.dart and screenshot_test.dart files were generated by ../bin/sample_page.dart. They should not be checked in.