diff --git a/.analysis_options b/.analysis_options index 2a1fece32f7e2..38cff14072e3e 100644 --- a/.analysis_options +++ b/.analysis_options @@ -7,15 +7,19 @@ # See the configuration guide for more # https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer # -# There are three similar analysis options files in the flutter repo: +# There are four similar analysis options files in the flutter repos: # - .analysis_options (this file) # - .analysis_options_repo # - packages/flutter/lib/analysis_options_user.yaml +# - https://github.com/flutter/plugins/blob/master/.analysis_options # # This file contains the analysis options used by Flutter editors, such as Atom. # It is very similar to the .analysis_options_repo file in this same directory; # the only difference (currently) is the public_member_api_docs option, # which triggers too many messages to be used in editors. +# +# The flutter/plugins repo contains a copy of this file, which should be kept +# in sync with this file. analyzer: language: diff --git a/VERSION b/VERSION index 61958cf4d74bf..321598098eff4 100644 --- a/VERSION +++ b/VERSION @@ -6,4 +6,4 @@ # incompatible way, this version number might not change. Instead, the version # number for package:flutter will update to reflect that change. -0.0.7-dev +0.0.10-dev diff --git a/bin/flutter b/bin/flutter index 046e5a474ab20..87708ccdb64c1 100755 --- a/bin/flutter +++ b/bin/flutter @@ -46,8 +46,8 @@ DART_SDK_PATH="$FLUTTER_ROOT/bin/cache/dart-sdk" DART="$DART_SDK_PATH/bin/dart" PUB="$DART_SDK_PATH/bin/pub" -# Test if running as superuser -if [[ "$EUID" == "0" ]]; then +# Test if running as superuser – but don't warn if running within Docker +if [[ "$EUID" == "0" ]] && ! [[ -f /.dockerenv ]]; then echo " Woah! You appear to be trying to run flutter as root." echo " We strongly recommend running the flutter tool without superuser privileges." echo " /" diff --git a/bin/internal/README.md b/bin/internal/README.md new file mode 100644 index 0000000000000..e051a56be09da --- /dev/null +++ b/bin/internal/README.md @@ -0,0 +1,10 @@ +Dart SDK dependency +=================== + +The Dart SDK is downloaded from one of [the supported channels](https://www.dartlang.org/install/archive), +cached in `bin/cache/dart-sdk` and is used to run Flutter Dart code. + +The file `bin/internal/dart-sdk.version` determines the version of Dart SDK +that will be downloaded. Normally it points to the `dev` channel (for example, +`1.24.0-dev.6.7`), but it can also point to particular bleeding edge build +of Dart (for example, `hash/c0617d20158955d99d6447036237fe2639ba088c`). \ No newline at end of file diff --git a/bin/internal/dart-sdk.version b/bin/internal/dart-sdk.version index 7856c3dc9fd35..a93ad5a024906 100644 --- a/bin/internal/dart-sdk.version +++ b/bin/internal/dart-sdk.version @@ -1 +1 @@ -1.24.0-dev.3.0 +1.24.0-dev.6.7 diff --git a/bin/internal/engine.version b/bin/internal/engine.version index f41ba1f4193fd..c63693caabebd 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -75c74dc463d56e17be10315cfde409010fd8f90b +18fdfb86bb3876fcbb4e1d25e5b2aad0c5cd669f diff --git a/bin/internal/update_dart_sdk.ps1 b/bin/internal/update_dart_sdk.ps1 index 8767d1d98f372..1f16cb6ebb685 100644 --- a/bin/internal/update_dart_sdk.ps1 +++ b/bin/internal/update_dart_sdk.ps1 @@ -29,7 +29,7 @@ if ((Test-Path $dartSdkStampPath) -and ($dartSdkVersion -eq (Get-Content $dartSd Write-Host "Downloading Dart SDK $dartSdkVersion..." $dartZipName = "dartsdk-windows-x64-release.zip" -$dartChannel = if ($dartSdkVersion.Contains("-dev.")) {"dev"} else {"stable"} +$dartChannel = if ($dartSdkVersion.Contains("-dev.")) {"dev"} else {if ($dartSdkVersion.Contains("hash/")) {"be"} else {"stable"}} $dartSdkUrl = "https://storage.googleapis.com/dart-archive/channels/$dartChannel/raw/$dartSdkVersion/sdk/$dartZipName" if (Test-Path $dartSdkPath) { diff --git a/bin/internal/update_dart_sdk.sh b/bin/internal/update_dart_sdk.sh index cbff31ca74bf7..80e8f6e4ed701 100755 --- a/bin/internal/update_dart_sdk.sh +++ b/bin/internal/update_dart_sdk.sh @@ -41,6 +41,9 @@ if [ ! -f "$DART_SDK_STAMP_PATH" ] || [ "$DART_SDK_VERSION" != `cat "$DART_SDK_S if [[ $DART_SDK_VERSION == *"-dev."* ]] then DART_CHANNEL="dev" + elif [[ $DART_SDK_VERSION == "hash/"* ]] + then + DART_CHANNEL="be" fi DART_SDK_URL="https://storage.googleapis.com/dart-archive/channels/$DART_CHANNEL/raw/$DART_SDK_VERSION/sdk/$DART_ZIP_NAME" diff --git a/dev/benchmarks/complex_layout/android/app/build.gradle b/dev/benchmarks/complex_layout/android/app/build.gradle index 114a0ee1094c7..4a8ab5e563fbf 100644 --- a/dev/benchmarks/complex_layout/android/app/build.gradle +++ b/dev/benchmarks/complex_layout/android/app/build.gradle @@ -33,6 +33,14 @@ android { signingConfig signingConfigs.debug } } + + aaptOptions { + // TODO(goderbauer): remove when https://github.com/flutter/flutter/issues/8986 is resolved. + if(System.getenv("FLUTTER_CI_WIN")) { + println "AAPT cruncher disabled when running on CI, see https://github.com/flutter/flutter/issues/8986" + cruncherEnabled false + } + } } flutter { diff --git a/dev/benchmarks/complex_layout/lib/main.dart b/dev/benchmarks/complex_layout/lib/main.dart index 1d78c37ffdd33..27bd8c1a2402c 100644 --- a/dev/benchmarks/complex_layout/lib/main.dart +++ b/dev/benchmarks/complex_layout/lib/main.dart @@ -111,9 +111,9 @@ class ComplexLayoutState extends State { key: const Key('complex-scroll'), // this key is used by the driver test itemBuilder: (BuildContext context, int index) { if (index % 2 == 0) - return new FancyImageItem(index, key: new ValueKey(index)); + return new FancyImageItem(index, key: new PageStorageKey(index)); else - return new FancyGalleryItem(index, key: new ValueKey(index)); + return new FancyGalleryItem(index, key: new PageStorageKey(index)); }, ) ), @@ -496,7 +496,7 @@ class ItemGalleryBox extends StatelessWidget { child: new TabBarView( children: tabNames.map((String tabName) { return new Container( - key: new Key(tabName), + key: new PageStorageKey(tabName), child: new Padding( padding: const EdgeInsets.all(8.0), child: new Card( @@ -611,6 +611,7 @@ class GalleryDrawer extends StatelessWidget { final ScrollMode currentMode = ComplexLayoutApp.of(context).scrollMode; return new Drawer( child: new ListView( + key: const PageStorageKey('gallery-drawer'), children: [ new FancyDrawerHeader(), new ListTile( diff --git a/dev/benchmarks/complex_layout/test_driver/semantics_perf.dart b/dev/benchmarks/complex_layout/test_driver/semantics_perf.dart new file mode 100644 index 0000000000000..df4ca137b5cf4 --- /dev/null +++ b/dev/benchmarks/complex_layout/test_driver/semantics_perf.dart @@ -0,0 +1,11 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_driver/driver_extension.dart'; +import 'package:complex_layout/main.dart' as app; + +void main() { + enableFlutterDriverExtension(); + app.main(); +} diff --git a/dev/benchmarks/complex_layout/test_driver/semantics_perf_test.dart b/dev/benchmarks/complex_layout/test_driver/semantics_perf_test.dart new file mode 100644 index 0000000000000..28b1ed026cf77 --- /dev/null +++ b/dev/benchmarks/complex_layout/test_driver/semantics_perf_test.dart @@ -0,0 +1,43 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + group('semantics performance test', () { + FlutterDriver driver; + + setUpAll(() async { + driver = await FlutterDriver.connect(printCommunication: true); + }); + + tearDownAll(() async { + if (driver != null) + driver.close(); + }); + + test('inital tree creation', () async { + // Let app become fully idle. + await new Future.delayed(const Duration(seconds: 1)); + + final Timeline timeline = await driver.traceAction(() async { + expect(await driver.setSemantics(true), isTrue); + }); + + final Iterable semanticsEvents = timeline.events.where((TimelineEvent event) => event.name == "Semantics"); + if (semanticsEvents.length != 1) + fail('Expected exactly one semantics event, got ${semanticsEvents.length}'); + final Duration semanticsTreeCreation = semanticsEvents.first.duration; + + final String json = JSON.encode({'initialSemanticsTreeCreation': semanticsTreeCreation.inMilliseconds}); + new File(p.join(testOutputsDirectory, 'complex_layout_semantics_perf.json')).writeAsStringSync(json); + }); + }); +} diff --git a/dev/bots/analyze-sample-code.dart b/dev/bots/analyze-sample-code.dart new file mode 100644 index 0000000000000..7aff93c54a730 --- /dev/null +++ b/dev/bots/analyze-sample-code.dart @@ -0,0 +1,268 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as path; + +final String _flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script)))); +final String _flutter = path.join(_flutterRoot, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter'); + +class Line { + const Line(this.filename, this.line, this.indent); + final String filename; + final int line; + final int indent; + Line get next => this + 1; + Line operator +(int count) { + if (count == 0) + return this; + return new Line(filename, line + count, indent); + } + @override + String toString([int column]) { + if (column != null) + return '$filename:$line:${column + indent}'; + return '$filename:$line'; + } +} + +class Section { + const Section(this.start, this.preamble, this.code, this.postamble); + final Line start; + final String preamble; + final List code; + final String postamble; + Iterable get strings sync* { + if (preamble != null) + yield preamble; + yield* code; + if (postamble != null) + yield postamble; + } + List get lines { + final List result = new List.generate(code.length, (int index) => start + index); + if (preamble != null) + result.insert(0, null); + if (postamble != null) + result.add(null); + return result; + } +} + +const String kDartDocPrefix = '///'; +const String kDartDocPrefixWithSpace = '$kDartDocPrefix '; + +/// To run this: bin/cache/dart-sdk/bin/dart dev/bots/analyze-sample-code.dart +Future main() async { + final Directory temp = Directory.systemTemp.createTempSync('analyze_sample_code_'); + int exitCode = 1; + bool keepMain = false; + try { + final File mainDart = new File(path.join(temp.path, 'main.dart')); + final File pubSpec = new File(path.join(temp.path, 'pubspec.yaml')); + final Directory flutterPackage = new Directory(path.join(_flutterRoot, 'packages', 'flutter', 'lib')); + final List
sections =
[]; + int sampleCodeSections = 0; + for (FileSystemEntity file in flutterPackage.listSync(recursive: true, followLinks: false)) { + if (file is File && path.extension(file.path) == '.dart') { + final List lines = file.readAsLinesSync(); + bool inPreamble = false; + bool inSampleSection = false; + bool inDart = false; + bool foundDart = false; + int lineNumber = 0; + final List block = []; + Line startLine; + for (String line in lines) { + lineNumber += 1; + final String trimmedLine = line.trim(); + if (inPreamble) { + if (line.isEmpty) { + inPreamble = false; + processBlock(startLine, block, sections); + } else if (!line.startsWith('// ')) { + throw '${file.path}:$lineNumber: Unexpected content in sample code preamble.'; + } else { + block.add(line.substring(3)); + } + } else if (inSampleSection) { + if (!trimmedLine.startsWith(kDartDocPrefix) || trimmedLine.startsWith('/// ## ')) { + if (inDart) + throw '${file.path}:$lineNumber: Dart section inexplicably unterminated.'; + if (!foundDart) + throw '${file.path}:$lineNumber: No dart block found in sample code section'; + inSampleSection = false; + } else { + if (inDart) { + if (trimmedLine == '/// ```') { + inDart = false; + processBlock(startLine, block, sections); + } else if (trimmedLine == kDartDocPrefix) { + block.add(''); + } else { + final int index = line.indexOf(kDartDocPrefixWithSpace); + if (index < 0) + throw '${file.path}:$lineNumber: Dart section inexplicably did not contain "$kDartDocPrefixWithSpace" prefix.'; + block.add(line.substring(index + 4)); + } + } else if (trimmedLine == '/// ```dart') { + assert(block.isEmpty); + startLine = new Line(file.path, lineNumber + 1, line.indexOf(kDartDocPrefixWithSpace) + kDartDocPrefixWithSpace.length); + inDart = true; + foundDart = true; + } + } + } else if (line == '// Examples can assume:') { + assert(block.isEmpty); + startLine = new Line(file.path, lineNumber + 1, 3); + inPreamble = true; + } else if (trimmedLine == '/// ## Sample code') { + inSampleSection = true; + foundDart = false; + sampleCodeSections += 1; + } + } + } + } + final List buffer = []; + buffer.add('// generated code'); + buffer.add('import \'dart:math\' as math;'); + buffer.add('import \'dart:ui\' as ui;'); + for (FileSystemEntity file in flutterPackage.listSync(recursive: false, followLinks: false)) { + if (file is File && path.extension(file.path) == '.dart') { + buffer.add(''); + buffer.add('// ${file.path}'); + buffer.add('import \'package:flutter/${path.basename(file.path)}\';'); + } + } + buffer.add(''); + final List lines = new List.filled(buffer.length, null, growable: true); + for (Section section in sections) { + buffer.addAll(section.strings); + lines.addAll(section.lines); + } + mainDart.writeAsStringSync(buffer.join('\n')); + pubSpec.writeAsStringSync(''' +name: analyze_sample_code +dependencies: + flutter: + sdk: flutter +'''); + print('Found $sampleCodeSections sample code sections.'); + final Process process = await Process.start( + _flutter, + ['analyze', '--no-preamble', mainDart.path], + workingDirectory: temp.path, + ); + stderr.addStream(process.stderr); + final List errors = await process.stdout.transform(UTF8.decoder).transform(const LineSplitter()).toList(); + if (errors.first == 'Building flutter tool...') + errors.removeAt(0); + if (errors.first.startsWith('Running "flutter packages get" in ')) + errors.removeAt(0); + if (errors.first.startsWith('Analyzing ')) + errors.removeAt(0); + if (errors.last.endsWith(' issues found.') || errors.last.endsWith(' issue found.')) + errors.removeLast(); + int errorCount = 0; + for (String error in errors) { + const String kBullet = ' • '; + const String kAt = ' at main.dart:'; + const String kColon = ':'; + final int start = error.indexOf(kBullet); + final int end = error.indexOf(kAt); + if (start >= 0 && end >= 0) { + final String message = error.substring(start + kBullet.length, end); + final int colon2 = error.indexOf(kColon, end + kAt.length); + if (colon2 < 0) + throw 'failed to parse error message: $error'; + final String line = error.substring(end + kAt.length, colon2); + final int bullet2 = error.indexOf(kBullet, colon2); + if (bullet2 < 0) + throw 'failed to parse error message: $error'; + final String column = error.substring(colon2 + kColon.length, bullet2); + final int lineNumber = int.parse(line, radix: 10, onError: (String source) => throw 'failed to parse error message: $error'); + final int columnNumber = int.parse(column, radix: 10, onError: (String source) => throw 'failed to parse error message: $error'); + if (lineNumber < 0 || lineNumber >= lines.length) + throw 'failed to parse error message: $error'; + final Line actualLine = lines[lineNumber - 1]; + final String errorCode = error.substring(bullet2 + kBullet.length); + if (errorCode == 'unused_element') { + // We don't really care if sample code isn't used! + } else if (actualLine == null) { + if (errorCode == 'missing_identifier' && lineNumber > 1 && buffer[lineNumber - 2].endsWith(',')) { + final Line actualLine = lines[lineNumber - 2]; + print('${actualLine.toString(buffer[lineNumber - 2].length - 1)}: unexpected comma at end of sample code'); + errorCount += 1; + } else { + print('${mainDart.path}:${lineNumber - 1}:$columnNumber: $message'); + keepMain = true; + errorCount += 1; + } + } else { + print('${actualLine.toString(columnNumber)}: $message ($errorCode)'); + errorCount += 1; + } + } else { + print('?? $error'); + errorCount += 1; + } + } + exitCode = await process.exitCode; + if (exitCode == 1 && errorCount == 0) + exitCode = 0; + if (exitCode == 0) + print('No errors!'); + } finally { + if (keepMain) { + print('Kept ${temp.path} because it had errors (see above).'); + } else { + temp.deleteSync(recursive: true); + } + } + exit(exitCode); +} + +int _expressionId = 0; + +void processBlock(Line line, List block, List
sections) { + if (block.isEmpty) + throw '$line: Empty ```dart block in sample code.'; + if (block.first.startsWith('new ') || block.first.startsWith('const ')) { + _expressionId += 1; + sections.add(new Section(line, 'dynamic expression$_expressionId = ', block.toList(), ';')); + } else if (block.first.startsWith('class ') || block.first.startsWith('const ')) { + sections.add(new Section(line, null, block.toList(), null)); + } else { + final List buffer = []; + int subblocks = 0; + Line subline; + for (int index = 0; index < block.length; index += 1) { + if (block[index] == '' || block[index] == '// ...') { + if (subline == null) + throw '${line + index}: Unexpected blank line or "// ..." line near start of subblock in sample code.'; + subblocks += 1; + processBlock(subline, buffer, sections); + assert(buffer.isEmpty); + subline = null; + } else if (block[index].startsWith('// ')) { + if (buffer.length > 1) // don't include leading comments + buffer.add('/${block[index]}'); // so that it doesn't start with "// " and get caught in this again + } else { + subline ??= line + index; + buffer.add(block[index]); + } + } + if (subblocks > 0) { + if (subline != null) + processBlock(subline, buffer, sections); + } else { + sections.add(new Section(line, null, block.toList(), null)); + } + } + block.clear(); +} diff --git a/dev/bots/docs.sh b/dev/bots/docs.sh index 508a092d31a75..4d9de3f85bae6 100755 --- a/dev/bots/docs.sh +++ b/dev/bots/docs.sh @@ -19,13 +19,24 @@ FLUTTER_ROOT=$PWD bin/cache/dart-sdk/bin/dart dev/tools/javadoc.dart # Ensure google webmaster tools can verify our site. cp dev/docs/google2ed1af765c529f57.html dev/docs/doc -# Upload new API docs when on Travis and branch is master. -if [ "$TRAVIS_PULL_REQUEST" = "false" ] && [ "$TRAVIS_BRANCH" = "master" ]; then - cd dev/docs - firebase deploy --project docs-flutter-io - exit_code=$? - if [[ $exit_code -ne 0 ]]; then +# Upload new API docs when on Travis +if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then + if [ "$TRAVIS_BRANCH" == "master" -o "$TRAVIS_BRANCH" == "alpha" ]; then + cd dev/docs + + if [ "$TRAVIS_BRANCH" == "master" ]; then + echo -e "User-agent: *\nDisallow: /" > doc/robots.txt + firebase deploy --project master-docs-flutter-io + fi + + if [ "$TRAVIS_BRANCH" == "alpha" ]; then + firebase deploy --project docs-flutter-io + fi + + exit_code=$? + if [[ $exit_code -ne 0 ]]; then >&2 echo "Error deploying docs via firebase ($exit_code)" exit $exit_code + fi fi fi diff --git a/dev/bots/test.dart b/dev/bots/test.dart index 4c19d12ec391a..dd62802334aff 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -1,3 +1,7 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -37,6 +41,11 @@ Future main() async { options: ['--flutter-repo'], ); + // Analyze all the sample code in the repo + await _runCommand(dart, [path.join(flutterRoot, 'dev', 'bots', 'analyze-sample-code.dart')], + workingDirectory: flutterRoot, + ); + // Try with the --watch analyzer, to make sure it returns success also. // The --benchmark argument exits after one run. await _runFlutterAnalyze(flutterRoot, @@ -118,7 +127,7 @@ Future _pubRunTest( String workingDirectory, { String testPath, }) { - final List args = ['run', 'test', '-rexpanded']; + final List args = ['run', 'test', '-j1', '-rexpanded']; if (testPath != null) args.add(testPath); return _runCommand(pub, args, workingDirectory: workingDirectory); diff --git a/dev/devicelab/bin/tasks/analyzer_benchmark.dart b/dev/devicelab/bin/tasks/analyzer_benchmark.dart new file mode 100644 index 0000000000000..d705eedace0ab --- /dev/null +++ b/dev/devicelab/bin/tasks/analyzer_benchmark.dart @@ -0,0 +1,12 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/tasks/analysis.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; + +Future main() async { + await task(analyzerBenchmarkTask); +} diff --git a/dev/devicelab/bin/tasks/analyzer_cli__analysis_time.dart b/dev/devicelab/bin/tasks/analyzer_cli__analysis_time.dart deleted file mode 100644 index 50ee992e050ec..0000000000000 --- a/dev/devicelab/bin/tasks/analyzer_cli__analysis_time.dart +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2016 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter_devicelab/tasks/analysis.dart'; -import 'package:flutter_devicelab/framework/framework.dart'; -import 'package:flutter_devicelab/framework/utils.dart'; - -Future main() async { - final String revision = await getCurrentFlutterRepoCommit(); - final DateTime revisionTimestamp = await getFlutterRepoCommitTimestamp(revision); - final String dartSdkVersion = await getDartVersion(); - await task(createAnalyzerCliTest( - sdk: dartSdkVersion, - commit: revision, - timestamp: revisionTimestamp, - )); -} diff --git a/dev/devicelab/bin/tasks/analyzer_server__analysis_time.dart b/dev/devicelab/bin/tasks/analyzer_server__analysis_time.dart deleted file mode 100644 index 922c8ce85e0ce..0000000000000 --- a/dev/devicelab/bin/tasks/analyzer_server__analysis_time.dart +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2016 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter_devicelab/tasks/analysis.dart'; -import 'package:flutter_devicelab/framework/framework.dart'; -import 'package:flutter_devicelab/framework/utils.dart'; - -Future main() async { - final String revision = await getCurrentFlutterRepoCommit(); - final DateTime revisionTimestamp = await getFlutterRepoCommitTimestamp(revision); - final String dartSdkVersion = await getDartVersion(); - await task(createAnalyzerServerTest( - sdk: dartSdkVersion, - commit: revision, - timestamp: revisionTimestamp, - )); -} diff --git a/dev/devicelab/bin/tasks/android_sample_catalog_generator.dart b/dev/devicelab/bin/tasks/android_sample_catalog_generator.dart index b9550b36c6447..0fa9d4fb26bb3 100644 --- a/dev/devicelab/bin/tasks/android_sample_catalog_generator.dart +++ b/dev/devicelab/bin/tasks/android_sample_catalog_generator.dart @@ -6,9 +6,10 @@ import 'dart:async'; import 'package:flutter_devicelab/framework/adb.dart'; import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; import 'package:flutter_devicelab/tasks/sample_catalog_generator.dart'; -Future main() async { +Future main(List args) async { deviceOperatingSystem = DeviceOperatingSystem.android; - await task(samplePageCatalogGenerator); + await task(() => samplePageCatalogGenerator(extractCloudAuthTokenArg(args))); } diff --git a/dev/devicelab/bin/tasks/complex_layout_semantics_perf.dart b/dev/devicelab/bin/tasks/complex_layout_semantics_perf.dart new file mode 100644 index 0000000000000..028447a75d716 --- /dev/null +++ b/dev/devicelab/bin/tasks/complex_layout_semantics_perf.dart @@ -0,0 +1,38 @@ +// Copyright (c) 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_devicelab/framework/adb.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; +import 'package:path/path.dart' as p; + +void main() { + task(() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + + final Device device = await devices.workingDevice; + await device.unlock(); + final String deviceId = device.deviceId; + await flutter('packages', options: ['get']); + + final String complexLayoutPath = p.join(flutterDirectory.path, 'dev', 'benchmarks', 'complex_layout'); + + await inDirectory(complexLayoutPath, () async { + await flutter('drive', options: [ + '-v', + '--profile', + '--trace-startup', // Enables "endless" timeline event buffering. + '-t', + p.join(complexLayoutPath, 'test_driver', 'semantics_perf.dart'), + '-d', + deviceId, + ]); + }); + + final String dataPath = p.join(complexLayoutPath, 'build', 'complex_layout_semantics_perf.json'); + return new TaskResult.successFromFile(file(dataPath), benchmarkScoreKeys: [ + 'initialSemanticsTreeCreation', + ]); + }); +} diff --git a/dev/devicelab/bin/tasks/flutter_gallery__transition_perf_with_semantics.dart b/dev/devicelab/bin/tasks/flutter_gallery__transition_perf_with_semantics.dart new file mode 100644 index 0000000000000..27c61713cd5bb --- /dev/null +++ b/dev/devicelab/bin/tasks/flutter_gallery__transition_perf_with_semantics.dart @@ -0,0 +1,29 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_devicelab/tasks/gallery.dart'; +import 'package:flutter_devicelab/framework/adb.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + await task(() async { + final TaskResult withoutSemantics = await createGalleryTransitionTest()(); + final TaskResult withSemantics = await createGalleryTransitionTest(semanticsEnabled: true)(); + + final List benchmarkScoreKeys = []; + final Map data = {}; + for (String key in withSemantics.benchmarkScoreKeys) { + final String deltaKey = 'delta_$key'; + data[deltaKey] = withSemantics.data[key] - withoutSemantics.data[key]; + data['semantics_$key'] = withSemantics.data[key]; + data[key] = withoutSemantics.data[key]; + benchmarkScoreKeys.add(deltaKey); + } + + return new TaskResult.success(data, benchmarkScoreKeys: benchmarkScoreKeys); + }); +} diff --git a/dev/devicelab/bin/tasks/ios_sample_catalog_generator.dart b/dev/devicelab/bin/tasks/ios_sample_catalog_generator.dart index 94f14641c1935..20efb83307cdb 100644 --- a/dev/devicelab/bin/tasks/ios_sample_catalog_generator.dart +++ b/dev/devicelab/bin/tasks/ios_sample_catalog_generator.dart @@ -6,9 +6,10 @@ import 'dart:async'; import 'package:flutter_devicelab/framework/adb.dart'; import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; import 'package:flutter_devicelab/tasks/sample_catalog_generator.dart'; -Future main() async { +Future main(List args) async { deviceOperatingSystem = DeviceOperatingSystem.ios; - await task(samplePageCatalogGenerator); + await task(() => samplePageCatalogGenerator(extractCloudAuthTokenArg(args))); } diff --git a/dev/devicelab/lib/framework/benchmarks.dart b/dev/devicelab/lib/framework/benchmarks.dart deleted file mode 100644 index b4bddfc665cbd..0000000000000 --- a/dev/devicelab/lib/framework/benchmarks.dart +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2016 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'framework.dart'; - -/// A benchmark harness used to run a benchmark multiple times and report the -/// best result. -abstract class Benchmark { - Benchmark(this.name); - - final String name; - - TaskResult bestResult; - - Future init() => new Future.value(); - - Future run(); - TaskResult get lastResult; - - @override - String toString() => name; -} - -/// Runs a [benchmark] [iterations] times and reports the best result. -/// -/// Use [warmUpBenchmark] to discard cold performance results. -Future runBenchmark(Benchmark benchmark, { - int iterations: 1, - bool warmUpBenchmark: false -}) async { - await benchmark.init(); - - final List allRuns = []; - - num minValue; - - if (warmUpBenchmark) - await benchmark.run(); - - while (iterations > 0) { - iterations--; - - print(''); - - try { - final num result = await benchmark.run(); - allRuns.add(result); - - if (minValue == null || result < minValue) { - benchmark.bestResult = benchmark.lastResult; - minValue = result; - } - } catch (error) { - print('benchmark failed with error: $error'); - } - } - - return minValue; -} diff --git a/dev/devicelab/lib/framework/utils.dart b/dev/devicelab/lib/framework/utils.dart index 06de108b5f9b0..5b0c77e9d8ae8 100644 --- a/dev/devicelab/lib/framework/utils.dart +++ b/dev/devicelab/lib/framework/utils.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:args/args.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'package:process/process.dart'; @@ -379,35 +380,6 @@ void checkNotNull(Object o1, throw 'o10 is null'; } -/// Add benchmark values to a JSON results file. -/// -/// If the file contains information about how long the benchmark took to run -/// (a `time` field), then return that info. -// TODO(yjbanov): move this data to __metadata__ -num addBuildInfo(File jsonFile, - {num expected, String sdk, String commit, DateTime timestamp}) { - Map json; - - if (jsonFile.existsSync()) - json = JSON.decode(jsonFile.readAsStringSync()); - else - json = {}; - - if (expected != null) - json['expected'] = expected; - if (sdk != null) - json['sdk'] = sdk; - if (commit != null) - json['commit'] = commit; - if (timestamp != null) - json['timestamp'] = timestamp.millisecondsSinceEpoch; - - jsonFile.writeAsStringSync(jsonEncode(json)); - - // Return the elapsed time of the benchmark (if any). - return json['time']; -} - /// Splits [from] into lines and selects those that contain [pattern]. Iterable grep(Pattern pattern, {@required String from}) { return from.split('\n').where((String line) { @@ -452,3 +424,23 @@ Future findAvailablePort() async { } bool canRun(String path) => _processManager.canRun(path); + +String extractCloudAuthTokenArg(List rawArgs) { + final ArgParser argParser = new ArgParser()..addOption('cloud-auth-token'); + ArgResults args; + try { + args = argParser.parse(rawArgs); + } on FormatException catch(error) { + stderr.writeln('${error.message}\n'); + stderr.writeln('Usage:\n'); + stderr.writeln(argParser.usage); + return null; + } + + final String token = args['cloud-auth-token']; + if (token == null) { + stderr.writeln('Required option --cloud-auth-token not found'); + return null; + } + return token; +} diff --git a/dev/devicelab/lib/tasks/analysis.dart b/dev/devicelab/lib/tasks/analysis.dart index b02d316026f93..fb681394a24cd 100644 --- a/dev/devicelab/lib/tasks/analysis.dart +++ b/dev/devicelab/lib/tasks/analysis.dart @@ -5,112 +5,98 @@ import 'dart:async'; import 'dart:io'; -import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; -import '../framework/benchmarks.dart'; import '../framework/framework.dart'; import '../framework/utils.dart'; -TaskFunction createAnalyzerCliTest({ - @required String sdk, - @required String commit, - @required DateTime timestamp, -}) { - return new AnalyzerCliTask(sdk, commit, timestamp); -} +/// Run each benchmark this many times and compute average. +const int _kRunsPerBenchmark = 3; -TaskFunction createAnalyzerServerTest({ - @required String sdk, - @required String commit, - @required DateTime timestamp, -}) { - return new AnalyzerServerTask(sdk, commit, timestamp); -} +/// Runs a benchmark once and reports the result as a lower-is-better numeric +/// value. +typedef Future _Benchmark(); -abstract class AnalyzerTask { - Benchmark benchmark; +/// Path to the generated "mega gallery" app. +Directory get _megaGalleryDirectory => dir(path.join(Directory.systemTemp.path, 'mega_gallery')); - Future call() async { - section(benchmark.name); - await runBenchmark(benchmark, iterations: 3, warmUpBenchmark: true); - return benchmark.bestResult; - } -} +Future analyzerBenchmarkTask() async { + await inDirectory(flutterDirectory, () async { + rmTree(_megaGalleryDirectory); + mkdirs(_megaGalleryDirectory); + await dart(['dev/tools/mega_gallery.dart', '--out=${_megaGalleryDirectory.path}']); + }); -class AnalyzerCliTask extends AnalyzerTask { - AnalyzerCliTask(String sdk, String commit, DateTime timestamp) { - benchmark = new FlutterAnalyzeBenchmark(sdk, commit, timestamp); - } -} + final Map data = { + 'flutter_repo_batch': await _run(new _FlutterRepoBenchmark()), + 'flutter_repo_watch': await _run(new _FlutterRepoBenchmark(watch: true)), + 'mega_gallery_batch': await _run(new _MegaGalleryBenchmark()), + 'mega_gallery_watch': await _run(new _MegaGalleryBenchmark(watch: true)), + }; -class AnalyzerServerTask extends AnalyzerTask { - AnalyzerServerTask(String sdk, String commit, DateTime timestamp) { - benchmark = new FlutterAnalyzeAppBenchmark(sdk, commit, timestamp); - } + return new TaskResult.success(data, benchmarkScoreKeys: data.keys.toList()); } -class FlutterAnalyzeBenchmark extends Benchmark { - FlutterAnalyzeBenchmark(this.sdk, this.commit, this.timestamp) - : super('flutter analyze --flutter-repo'); - - final String sdk; - final String commit; - final DateTime timestamp; +/// Times how long it takes to analyze the Flutter repository. +class _FlutterRepoBenchmark { + _FlutterRepoBenchmark({ this.watch = false }); - File get benchmarkFile => - file(path.join(flutterDirectory.path, 'analysis_benchmark.json')); + final bool watch; - @override - TaskResult get lastResult => new TaskResult.successFromFile(benchmarkFile); - - @override - Future run() async { - rm(benchmarkFile); + Future call() async { + section('Analyze Flutter repo ${watch ? 'with watcher' : ''}'); + final Stopwatch stopwatch = new Stopwatch(); await inDirectory(flutterDirectory, () async { - await flutter('analyze', options: [ + final List options = [ '--flutter-repo', '--benchmark', - ]); + ]; + + if (watch) + options.add('--watch'); + + stopwatch.start(); + await flutter('analyze', options: options); + stopwatch.stop(); }); - return addBuildInfo(benchmarkFile, - timestamp: timestamp, expected: 25.0, sdk: sdk, commit: commit); + return stopwatch.elapsedMilliseconds / 1000; } } -class FlutterAnalyzeAppBenchmark extends Benchmark { - FlutterAnalyzeAppBenchmark(this.sdk, this.commit, this.timestamp) - : super('analysis server mega_gallery'); +/// Times how long it takes to analyze the generated "mega_gallery" app. +class _MegaGalleryBenchmark { + _MegaGalleryBenchmark({ this.watch = false }); - final String sdk; - final String commit; - final DateTime timestamp; + final bool watch; - @override - TaskResult get lastResult => new TaskResult.successFromFile(benchmarkFile); + Future call() async { + section('Analyze mega gallery ${watch ? 'with watcher' : ''}'); + final Stopwatch stopwatch = new Stopwatch(); + await inDirectory(_megaGalleryDirectory, () async { + final List options = [ + '--benchmark', + ]; - Directory get megaDir => dir( - path.join(flutterDirectory.path, 'dev/benchmarks/mega_gallery')); - File get benchmarkFile => - file(path.join(megaDir.path, 'analysis_benchmark.json')); + if (watch) + options.add('--watch'); - @override - Future init() { - return inDirectory(flutterDirectory, () async { - await dart(['dev/tools/mega_gallery.dart']); + stopwatch.start(); + await flutter('analyze', options: options); + stopwatch.stop(); }); + return stopwatch.elapsedMilliseconds / 1000; } +} - @override - Future run() async { - rm(benchmarkFile); - await inDirectory(megaDir, () async { - await flutter('analyze', options: [ - '--watch', - '--benchmark', - ]); - }); - return addBuildInfo(benchmarkFile, - timestamp: timestamp, expected: 10.0, sdk: sdk, commit: commit); +/// Runs a [benchmark] several times and reports the average result. +Future _run(_Benchmark benchmark) async { + double total = 0.0; + for (int i = 0; i < _kRunsPerBenchmark; i++) { + // Delete cached analysis results. + rmTree(dir('${Platform.environment['HOME']}/.dartServer')); + + total += await benchmark(); } + final double average = total / _kRunsPerBenchmark; + return average; } diff --git a/dev/devicelab/lib/tasks/gallery.dart b/dev/devicelab/lib/tasks/gallery.dart index 6a36e00e6ab9d..b0730033bd35e 100644 --- a/dev/devicelab/lib/tasks/gallery.dart +++ b/dev/devicelab/lib/tasks/gallery.dart @@ -12,12 +12,16 @@ import '../framework/framework.dart'; import '../framework/ios.dart'; import '../framework/utils.dart'; -TaskFunction createGalleryTransitionTest() { - return new GalleryTransitionTest(); +TaskFunction createGalleryTransitionTest({ bool semanticsEnabled: false }) { + return new GalleryTransitionTest(semanticsEnabled: semanticsEnabled); } class GalleryTransitionTest { + GalleryTransitionTest({ this.semanticsEnabled: false }); + + final bool semanticsEnabled; + Future call() async { final Device device = await devices.workingDevice; await device.unlock(); @@ -33,11 +37,15 @@ class GalleryTransitionTest { await flutter('build', options: ['ios', '--profile']); } + final String testDriver = semanticsEnabled + ? 'transitions_perf_with_semantics.dart' + : 'transitions_perf.dart'; + await flutter('drive', options: [ '--profile', '--trace-startup', '-t', - 'test_driver/transitions_perf.dart', + 'test_driver/$testDriver', '-d', deviceId, ]); diff --git a/dev/devicelab/lib/tasks/sample_catalog_generator.dart b/dev/devicelab/lib/tasks/sample_catalog_generator.dart index ecd4ce78c6b20..c3a73d698cdfa 100644 --- a/dev/devicelab/lib/tasks/sample_catalog_generator.dart +++ b/dev/devicelab/lib/tasks/sample_catalog_generator.dart @@ -9,8 +9,10 @@ import '../framework/adb.dart'; import '../framework/framework.dart'; import '../framework/ios.dart'; import '../framework/utils.dart'; +import 'save_catalog_screenshots.dart' show saveCatalogScreenshots; -Future samplePageCatalogGenerator() async { + +Future samplePageCatalogGenerator(String authorizationToken) async { final Device device = await devices.workingDevice; await device.unlock(); final String deviceId = device.deviceId; @@ -19,7 +21,8 @@ Future samplePageCatalogGenerator() async { await inDirectory(catalogDirectory, () async { await flutter('packages', options: ['get']); - if (deviceOperatingSystem == DeviceOperatingSystem.ios) + final bool isIosDevice = deviceOperatingSystem == DeviceOperatingSystem.ios; + if (isIosDevice) await prepareProvisioningCertificates(catalogDirectory.path); await dart(['bin/sample_page.dart']); @@ -30,6 +33,13 @@ Future samplePageCatalogGenerator() async { '--device-id', deviceId, ]); + + await saveCatalogScreenshots( + directory: dir('${flutterDirectory.path}/examples/catalog/.generated'), + commit: await getCurrentFlutterRepoCommit(), + token: authorizationToken, + prefix: isIosDevice ? 'ios_' : '', + ); }); return new TaskResult.success(null); diff --git a/dev/devicelab/lib/tasks/save_catalog_screenshots.dart b/dev/devicelab/lib/tasks/save_catalog_screenshots.dart new file mode 100644 index 0000000000000..661266699f22c --- /dev/null +++ b/dev/devicelab/lib/tasks/save_catalog_screenshots.dart @@ -0,0 +1,140 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math' as math; + +import 'package:image/image.dart'; +import 'package:path/path.dart'; + +String authorizationToken; + +class UploadError extends Error { + UploadError(this.message); + final String message; + @override + String toString() => 'UploadError($message)'; +} + +void logMessage(String s) { print(s); } + +class Upload { + Upload(this.fromPath, this.largeName, this.smallName); + + static math.Random random; + static const String uriAuthority = 'www.googleapis.com'; + static const String uriPath = 'upload/storage/v1/b/flutter-catalog/o'; + + final String fromPath; + final String largeName; + final String smallName; + + List largeImage; + List smallImage; + bool largeImageSaved; + int retryCount = 0; + bool isComplete = false; + + // Exponential backoff per https://cloud.google.com/storage/docs/exponential-backoff + Duration get timeLimit { + if (retryCount == 0) + return const Duration(milliseconds: 1000); + random ??= new math.Random(); + return new Duration(milliseconds: random.nextInt(1000) + math.pow(2, retryCount) * 1000); + } + + Future save(HttpClient client, String name, List content) async { + try { + final Uri uri = new Uri.https(uriAuthority, uriPath, { + 'uploadType': 'media', + 'name': name, + }); + final HttpClientRequest request = await client.postUrl(uri); + request + ..headers.contentType = new ContentType('image', 'png') + ..headers.add('Authorization', 'Bearer $authorizationToken') + ..add(content); + + final HttpClientResponse response = await request.close().timeout(timeLimit); + if (response.statusCode == HttpStatus.OK) { + logMessage('Saved $name'); + await response.drain(); + } else { + // TODO(hansmuller): only retry on 5xx and 429 responses + logMessage('Request to save "$name" (length ${content.length}) failed with status ${response.statusCode}, will retry'); + logMessage(await response.transform(UTF8.decoder).join()); + } + return response.statusCode == HttpStatus.OK; + } on TimeoutException catch (_) { + logMessage('Request to save "$name" (length ${content.length}) timed out, will retry'); + return false; + } + } + + Future run(HttpClient client) async { + assert(!isComplete); + if (retryCount > 2) + throw new UploadError('upload of "$fromPath" to "$largeName" and "$smallName" failed after 2 retries'); + + largeImage ??= await new File(fromPath).readAsBytes(); + smallImage ??= encodePng(copyResize(decodePng(largeImage), 400)); + + if (!largeImageSaved) + largeImageSaved = await save(client, largeName, largeImage); + isComplete = largeImageSaved && await save(client, smallName, smallImage); + + retryCount += 1; + return isComplete; + } + + static bool isNotComplete(Upload upload) => !upload.isComplete; +} + +Future saveScreenshots(List fromPaths, List largeNames, List smallNames) async { + assert(fromPaths.length == largeNames.length); + assert(fromPaths.length == smallNames.length); + + List uploads = new List(fromPaths.length); + for (int index = 0; index < uploads.length; index += 1) + uploads[index] = new Upload(fromPaths[index], largeNames[index], smallNames[index]); + + while(uploads.any(Upload.isNotComplete)) { + final HttpClient client = new HttpClient(); + uploads = uploads.where(Upload.isNotComplete).toList(); + await Future.wait(uploads.map((Upload upload) => upload.run(client))); + client.close(force: true); + } +} + + +// If path is lib/foo.png then screenshotName is foo. +String screenshotName(String path) => basenameWithoutExtension(path); + +Future saveCatalogScreenshots({ + Directory directory, // Where the *.png screenshots are. + String commit, // The commit hash to be used as a cloud storage "directory". + String token, // Cloud storage authorization token. + String prefix, // Prefix for all file names. + }) async { + final List screenshots = []; + directory.listSync().forEach((FileSystemEntity entity) { + if (entity is File && entity.path.endsWith('.png')) { + final File file = entity; + screenshots.add(file.path); + } + }); + + final List largeNames = []; // Cloud storage names for the full res screenshots. + final List smallNames = []; // Likewise for the scaled down screenshots. + for (String path in screenshots) { + final String name = screenshotName(path); + largeNames.add('$commit/$prefix$name.png'); + smallNames.add('$commit/$prefix${name}_small.png'); + } + + authorizationToken = token; + await saveScreenshots(screenshots, largeNames, smallNames); +} diff --git a/dev/devicelab/manifest.yaml b/dev/devicelab/manifest.yaml index d42e025e936d6..5eb0fd94a4fae 100644 --- a/dev/devicelab/manifest.yaml +++ b/dev/devicelab/manifest.yaml @@ -37,13 +37,6 @@ tasks: # TODO: make these not require "has-android-device"; it is only there to # ensure we have the Android SDK. - flutter_gallery__build: - description: > - Collects various performance metrics from AOT builds of the Flutter - Gallery. - stage: devicelab - required_agent_capabilities: ["has-android-device"] - complex_layout__build: description: > Collects various performance metrics from AOT builds of the Complex @@ -57,18 +50,6 @@ tasks: stage: devicelab required_agent_capabilities: ["has-android-device"] - analyzer_cli__analysis_time: - description: > - Measures the speed of analyzing Flutter itself in batch mode. - stage: devicelab - required_agent_capabilities: ["has-android-device"] - - analyzer_server__analysis_time: - description: > - Measures the speed of analyzing Flutter itself in server mode. - stage: devicelab - required_agent_capabilities: ["has-android-device"] - # Android on-device tests complex_layout_scroll_perf__timeline_summary: @@ -90,25 +71,12 @@ tasks: stage: devicelab required_agent_capabilities: ["has-android-device"] - flutter_gallery__start_up: - description: > - Measures the startup time of the Flutter Gallery app on Android. - stage: devicelab - required_agent_capabilities: ["has-android-device"] - complex_layout__start_up: description: > Measures the startup time of the Complex Layout sample app on Android. stage: devicelab required_agent_capabilities: ["has-android-device"] - flutter_gallery__transition_perf: - description: > - Measures the performance of screen transitions in Flutter Gallery on - Android. - stage: devicelab - required_agent_capabilities: ["has-android-device"] - hot_mode_dev_cycle__benchmark: description: > Measures the performance of Dart VM hot patching feature. @@ -127,18 +95,6 @@ tasks: stage: devicelab required_agent_capabilities: ["has-android-device"] - flutter_gallery__memory_nav: - description: > - Measures memory usage after repeated navigation in Gallery. - stage: devicelab - required_agent_capabilities: ["has-android-device"] - - flutter_gallery__back_button_memory: - description: > - Measures memory usage after Android app suspend and resume. - stage: devicelab - required_agent_capabilities: ["has-android-device"] - microbenchmarks: description: > Runs benchmarks from dev/benchmarks/microbenchmarks. @@ -165,6 +121,13 @@ tasks: required_agent_capabilities: ["has-android-device"] flaky: true + complex_layout_semantics_perf: + description: > + Measures duration of building the initial semantics tree. + stage: devicelab + required_agent_capabilities: ["linux/android"] + flaky: true + # iOS on-device tests channels_integration_test_ios: @@ -172,12 +135,14 @@ tasks: Checks that platform channels work on iOS. stage: devicelab_ios required_agent_capabilities: ["has-ios-device"] + flaky: true platform_channel_sample_test_ios: description: > Runs a driver test on the Platform Channel sample app on iOS. stage: devicelab_ios required_agent_capabilities: ["has-ios-device"] + flaky: true complex_layout_scroll_perf_ios__timeline_summary: description: > @@ -185,6 +150,7 @@ tasks: iOS. stage: devicelab_ios required_agent_capabilities: ["has-ios-device"] + flaky: true # flutter_gallery_ios__start_up: # description: > @@ -206,12 +172,14 @@ tasks: iOS. stage: devicelab_ios required_agent_capabilities: ["has-ios-device"] + flaky: true basic_material_app_ios__size: description: > Measures the IPA size of a basic material app. stage: devicelab_ios required_agent_capabilities: ["has-ios-device"] + flaky: true # microbenchmarks_ios: # description: > @@ -233,6 +201,7 @@ tasks: Runs end-to-end Flutter tests on iOS. stage: devicelab_ios required_agent_capabilities: ["has-ios-device"] + flaky: true ios_sample_catalog_generator: description: > @@ -269,18 +238,61 @@ tasks: Measures the performance of Dart VM hot patching feature on a Linux host. stage: devicelab required_agent_capabilities: ["linux/android"] - flaky: true dartdocs: description: > Tracks how many members are still lacking documentation. stage: devicelab required_agent_capabilities: ["linux/android"] - flaky: true technical_debt__cost: description: > Estimates our technical debt (TODOs, analyzer ignores, etc). stage: devicelab required_agent_capabilities: ["linux/android"] + + flutter_gallery__build: + description: > + Collects various performance metrics from AOT builds of the Flutter + Gallery. + stage: devicelab + required_agent_capabilities: ["linux/android"] + + flutter_gallery__start_up: + description: > + Measures the startup time of the Flutter Gallery app on Android. + stage: devicelab + required_agent_capabilities: ["linux/android"] + + flutter_gallery__transition_perf: + description: > + Measures the performance of screen transitions in Flutter Gallery on + Android. + stage: devicelab + required_agent_capabilities: ["linux/android"] + + flutter_gallery__transition_perf_with_semantics: + description: > + Measures the delta in performance of screen transitions without and + with semantics enabled. + stage: devicelab + required_agent_capabilities: ["linux/android"] flaky: true + + flutter_gallery__memory_nav: + description: > + Measures memory usage after repeated navigation in Gallery. + stage: devicelab + required_agent_capabilities: ["linux/android"] + + flutter_gallery__back_button_memory: + description: > + Measures memory usage after Android app suspend and resume. + stage: devicelab + required_agent_capabilities: ["linux/android"] + + analyzer_benchmark: + description: > + Measures the speed of Dart analyzer. + stage: devicelab + required_agent_capabilities: ["linux/android"] diff --git a/dev/devicelab/pubspec.yaml b/dev/devicelab/pubspec.yaml index 12815963c0908..d2a2397605d72 100644 --- a/dev/devicelab/pubspec.yaml +++ b/dev/devicelab/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: args: ^0.13.4 + image: ^1.1.27 meta: ^1.0.5 path: ^1.4.0 process: 2.0.3 @@ -17,4 +18,4 @@ dependencies: dev_dependencies: # See packages/flutter_test/pubspec.yaml for why we're pinning this version. - test: 0.12.20 + test: 0.12.21 diff --git a/dev/devicelab/test/manifest_test.dart b/dev/devicelab/test/manifest_test.dart index eb1e39874ede5..12185569d0022 100644 --- a/dev/devicelab/test/manifest_test.dart +++ b/dev/devicelab/test/manifest_test.dart @@ -15,7 +15,7 @@ void main() { final ManifestTask task = manifest.tasks.firstWhere((ManifestTask task) => task.name == 'flutter_gallery__start_up'); expect(task.description, 'Measures the startup time of the Flutter Gallery app on Android.\n'); expect(task.stage, 'devicelab'); - expect(task.requiredAgentCapabilities, ['has-android-device']); + expect(task.requiredAgentCapabilities, ['linux/android']); }); }); diff --git a/dev/manual_tests/lib/material_arc.dart b/dev/manual_tests/lib/material_arc.dart index 19aa04a2bb515..b6d71f671f28f 100644 --- a/dev/manual_tests/lib/material_arc.dart +++ b/dev/manual_tests/lib/material_arc.dart @@ -189,10 +189,13 @@ class _PointDemoState extends State<_PointDemo> { return new RawGestureDetector( behavior: _dragTarget == null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque, gestures: { - ImmediateMultiDragGestureRecognizer: (ImmediateMultiDragGestureRecognizer recognizer) { // ignore: map_value_type_not_assignable, https://github.com/flutter/flutter/issues/5771 - return (recognizer ??= new ImmediateMultiDragGestureRecognizer()) - ..onStart = _handleOnStart; - } + ImmediateMultiDragGestureRecognizer: new GestureRecognizerFactoryWithHandlers( + () => new ImmediateMultiDragGestureRecognizer(), + (ImmediateMultiDragGestureRecognizer instance) { + instance + ..onStart = _handleOnStart; + }, + ), }, child: new ClipRect( child: new CustomPaint( @@ -359,10 +362,13 @@ class _RectangleDemoState extends State<_RectangleDemo> { return new RawGestureDetector( behavior: _dragTarget == null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque, gestures: { - ImmediateMultiDragGestureRecognizer: (ImmediateMultiDragGestureRecognizer recognizer) { // ignore: map_value_type_not_assignable, https://github.com/flutter/flutter/issues/5771 - return (recognizer ??= new ImmediateMultiDragGestureRecognizer()) - ..onStart = _handleOnStart; - } + ImmediateMultiDragGestureRecognizer: new GestureRecognizerFactoryWithHandlers( + () => new ImmediateMultiDragGestureRecognizer(), + (ImmediateMultiDragGestureRecognizer instance) { + instance + ..onStart = _handleOnStart; + }, + ), }, child: new ClipRect( child: new CustomPaint( diff --git a/examples/catalog/android/app/src/main/java/com/yourcompany/animated_list/MainActivity.java b/examples/catalog/android/app/src/main/java/com/yourcompany/animated_list/MainActivity.java index 7ba5d182f6d14..4b45672f6ac72 100644 --- a/examples/catalog/android/app/src/main/java/com/yourcompany/animated_list/MainActivity.java +++ b/examples/catalog/android/app/src/main/java/com/yourcompany/animated_list/MainActivity.java @@ -2,15 +2,12 @@ import android.os.Bundle; import io.flutter.app.FlutterActivity; -import io.flutter.plugins.PluginRegistry; +import io.flutter.plugins.GeneratedPluginRegistrant; public class MainActivity extends FlutterActivity { - PluginRegistry pluginRegistry; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - pluginRegistry = new PluginRegistry(); - pluginRegistry.registerAll(this); - } + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + GeneratedPluginRegistrant.registerWith(this); + } } diff --git a/examples/catalog/bin/sample_page.dart b/examples/catalog/bin/sample_page.dart index 2ebbb471e1f38..b790330737921 100644 --- a/examples/catalog/bin/sample_page.dart +++ b/examples/catalog/bin/sample_page.dart @@ -7,11 +7,13 @@ import 'dart:io'; +import 'package:path/path.dart'; + class SampleError extends Error { SampleError(this.message); final String message; @override - String toString() => message; + String toString() => 'SampleError($message)'; } // Sample apps are .dart files in the lib directory which contain a block @@ -83,14 +85,7 @@ class SampleGenerator { // If sourceFile is lib/foo.dart then sourceName is foo. The sourceName // is used to create derived filenames like foo.md or foo.png. - String get sourceName { - // In /foo/bar/baz.dart, matches baz.dart, match[1] == 'baz' - final RegExp nameRE = new RegExp(r'(\w+)\.dart$'); - final Match nameMatch = nameRE.firstMatch(sourceFile.path); - if (nameMatch.groupCount != 1) - throw new SampleError('bad source file name ${sourceFile.path}'); - return nameMatch[1]; - } + String get sourceName => basenameWithoutExtension(sourceFile.path); // The name of the widget class that defines this sample app, like 'FooSample'. String get sampleClass => commentValues["sample"]; @@ -161,6 +156,8 @@ void generate() { } }); + // Causes the generated imports to appear in alphabetical order. + // Avoid complaints from flutter lint. samples.sort((SampleGenerator a, SampleGenerator b) { return a.sourceName.compareTo(b.sourceName); }); @@ -183,7 +180,7 @@ void generate() { screenshotDriverTemplate, { 'paths': samples.map((SampleGenerator sample) { - return "'${outputFile('\${prefix}' + sample.sourceName + '.png').path}'"; + return "'${outputFile(sample.sourceName + '.png').path}'"; }).toList().join(',\n'), }, ); diff --git a/examples/catalog/bin/screenshot_test.dart.template b/examples/catalog/bin/screenshot_test.dart.template index 93f5f73f8f54e..2b283773d3d0e 100644 --- a/examples/catalog/bin/screenshot_test.dart.template +++ b/examples/catalog/bin/screenshot_test.dart.template @@ -19,7 +19,6 @@ void main() { }); test('take sample screenshots', () async { - final String prefix = Platform.isMacOS ? 'ios_' : ""; final List paths = [ @(paths) ]; diff --git a/examples/catalog/ios/Runner.xcodeproj/project.pbxproj b/examples/catalog/ios/Runner.xcodeproj/project.pbxproj index e6291935a57a6..43c2147621e94 100644 --- a/examples/catalog/ios/Runner.xcodeproj/project.pbxproj +++ b/examples/catalog/ios/Runner.xcodeproj/project.pbxproj @@ -7,10 +7,10 @@ objects = { /* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* PluginRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* PluginRegistry.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 74970F651EDBF3AE000507F3 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 74970F641EDBF3AE000507F3 /* GeneratedPluginRegistrant.m */; }; 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; @@ -39,10 +39,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* PluginRegistry.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PluginRegistry.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* PluginRegistry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PluginRegistry.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; 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 = ""; }; @@ -114,6 +114,8 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 74970F631EDBF3AE000507F3 /* GeneratedPluginRegistrant.h */, + 74970F641EDBF3AE000507F3 /* GeneratedPluginRegistrant.m */, 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 97C146FA1CF9000F007C117D /* Main.storyboard */, @@ -121,8 +123,6 @@ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* PluginRegistry.h */, - 1498D2331E8E89220040F4C2 /* PluginRegistry.m */, ); path = Runner; sourceTree = ""; @@ -252,7 +252,7 @@ files = ( 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* PluginRegistry.m in Sources */, + 74970F651EDBF3AE000507F3 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/examples/catalog/ios/Runner/AppDelegate.m b/examples/catalog/ios/Runner/AppDelegate.m index 3b6ab51b9e117..d4277583c2430 100644 --- a/examples/catalog/ios/Runner/AppDelegate.m +++ b/examples/catalog/ios/Runner/AppDelegate.m @@ -1,38 +1,11 @@ #include "AppDelegate.h" -#include "PluginRegistry.h" +#include "GeneratedPluginRegistrant.h" -@implementation AppDelegate { - PluginRegistry *plugins; -} +@implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - // Override point for customization after application launch. - FlutterViewController *flutterController = - (FlutterViewController *)self.window.rootViewController; - plugins = [[PluginRegistry alloc] initWithController:flutterController]; - return YES; -} - -- (void)applicationWillResignActive:(UIApplication *)application { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. -} - -- (void)applicationDidEnterBackground:(UIApplication *)application { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. -} - -- (void)applicationWillEnterForeground:(UIApplication *)application { - // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. -} - -- (void)applicationDidBecomeActive:(UIApplication *)application { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; } - -- (void)applicationWillTerminate:(UIApplication *)application { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. -} - @end diff --git a/examples/catalog/ios/Runner/PluginRegistry.h b/examples/catalog/ios/Runner/PluginRegistry.h deleted file mode 100644 index df039db5157cd..0000000000000 --- a/examples/catalog/ios/Runner/PluginRegistry.h +++ /dev/null @@ -1,18 +0,0 @@ -// -// Generated file. Do not edit. -// - -#ifndef PluginRegistry_h -#define PluginRegistry_h - -#import - - -@interface PluginRegistry : NSObject - - -- (instancetype)initWithController:(FlutterViewController *)controller; - -@end - -#endif /* PluginRegistry_h */ diff --git a/examples/catalog/ios/Runner/PluginRegistry.m b/examples/catalog/ios/Runner/PluginRegistry.m deleted file mode 100644 index 0a3472994685d..0000000000000 --- a/examples/catalog/ios/Runner/PluginRegistry.m +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -#import "PluginRegistry.h" - -@implementation PluginRegistry - -- (instancetype)initWithController:(FlutterViewController *)controller { - if (self = [super init]) { - } - return self; -} - -@end diff --git a/examples/catalog/lib/animated_list.dart b/examples/catalog/lib/animated_list.dart index 5742ed511a6c7..85101a74292fc 100644 --- a/examples/catalog/lib/animated_list.dart +++ b/examples/catalog/lib/animated_list.dart @@ -117,10 +117,9 @@ class ListModel { @required this.listKey, @required this.removedItemBuilder, Iterable initialItems, - }) : _items = new List.from(initialItems ?? []) { - assert(listKey != null); - assert(removedItemBuilder != null); - } + }) : assert(listKey != null), + assert(removedItemBuilder != null), + _items = new List.from(initialItems ?? []); final GlobalKey listKey; final dynamic removedItemBuilder; @@ -153,17 +152,16 @@ class ListModel { /// This widget's height is based on the animation parameter, it varies /// from 0 to 128 as the animation varies from 0.0 to 1.0. class CardItem extends StatelessWidget { - CardItem({ + const CardItem({ Key key, @required this.animation, this.onTap, @required this.item, this.selected: false - }) : super(key: key) { - assert(animation != null); - assert(item != null && item >= 0); - assert(selected != null); - } + }) : assert(animation != null), + assert(item != null && item >= 0), + assert(selected != null), + super(key: key); final Animation animation; final VoidCallback onTap; diff --git a/examples/catalog/lib/expansion_tile_sample.dart b/examples/catalog/lib/expansion_tile_sample.dart index 2213ee18c5520..01b75c7917fe5 100644 --- a/examples/catalog/lib/expansion_tile_sample.dart +++ b/examples/catalog/lib/expansion_tile_sample.dart @@ -68,7 +68,7 @@ final List data = [ // Displays one Entry. If the entry has children then it's displayed // with an ExpansionTile. class EntryItem extends StatelessWidget { - EntryItem(this.entry); + const EntryItem(this.entry); final Entry entry; @@ -76,7 +76,7 @@ class EntryItem extends StatelessWidget { if (root.children.isEmpty) return new ListTile(title: new Text(root.title)); return new ExpansionTile( - key: new ValueKey(root), + key: new PageStorageKey(root), title: new Text(root.title), children: root.children.map(_buildTiles).toList(), ); diff --git a/examples/catalog/lib/main.dart b/examples/catalog/lib/main.dart new file mode 100644 index 0000000000000..af45e3cd425c7 --- /dev/null +++ b/examples/catalog/lib/main.dart @@ -0,0 +1,7 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; + +void main() => runApp(const Center(child: const Text('flutter run -t lib/xxx.dart'))); diff --git a/examples/catalog/pubspec.yaml b/examples/catalog/pubspec.yaml index b8219d79a554d..bca57f4a43d3e 100644 --- a/examples/catalog/pubspec.yaml +++ b/examples/catalog/pubspec.yaml @@ -1,8 +1,9 @@ -name: animated_list -description: A sample app for AnimatedList +name: sample_catalog +description: A collection of Flutter sample apps dependencies: flutter: sdk: flutter + path: ^1.4.0 dev_dependencies: flutter_test: diff --git a/examples/flutter_gallery/lib/demo/animation/home.dart b/examples/flutter_gallery/lib/demo/animation/home.dart index e617c8411d57c..503b766a93013 100644 --- a/examples/flutter_gallery/lib/demo/animation/home.dart +++ b/examples/flutter_gallery/lib/demo/animation/home.dart @@ -35,10 +35,10 @@ class _RenderStatusBarPaddingSliver extends RenderSliver { _RenderStatusBarPaddingSliver({ @required double maxHeight, @required double scrollFactor, - }) : _maxHeight = maxHeight, _scrollFactor = scrollFactor { - assert(maxHeight != null && maxHeight >= 0.0); - assert(scrollFactor != null && scrollFactor >= 1.0); - } + }) : assert(maxHeight != null && maxHeight >= 0.0), + assert(scrollFactor != null && scrollFactor >= 1.0), + _maxHeight = maxHeight, + _scrollFactor = scrollFactor; // The height of the status bar double get maxHeight => _maxHeight; @@ -75,14 +75,13 @@ class _RenderStatusBarPaddingSliver extends RenderSliver { } class _StatusBarPaddingSliver extends SingleChildRenderObjectWidget { - _StatusBarPaddingSliver({ + const _StatusBarPaddingSliver({ Key key, @required this.maxHeight, this.scrollFactor: 5.0, - }) : super(key: key) { - assert(maxHeight != null && maxHeight >= 0.0); - assert(scrollFactor != null && scrollFactor >= 1.0); - } + }) : assert(maxHeight != null && maxHeight >= 0.0), + assert(scrollFactor != null && scrollFactor >= 1.0), + super(key: key); final double maxHeight; final double scrollFactor; @@ -272,14 +271,13 @@ class _AllSectionsView extends AnimatedWidget { this.midHeight, this.maxHeight, this.sectionCards: const [], - }) : super(key: key, listenable: selectedIndex) { - assert(sections != null); - assert(sectionCards != null); - assert(sectionCards.length == sections.length); - assert(sectionIndex >= 0 && sectionIndex < sections.length); - assert(selectedIndex != null); - assert(selectedIndex.value >= 0.0 && selectedIndex.value < sections.length.toDouble()); - } + }) : assert(sections != null), + assert(sectionCards != null), + assert(sectionCards.length == sections.length), + assert(sectionIndex >= 0 && sectionIndex < sections.length), + assert(selectedIndex != null), + assert(selectedIndex.value >= 0.0 && selectedIndex.value < sections.length.toDouble()), + super(key: key, listenable: selectedIndex); final int sectionIndex; final List
sections; @@ -371,7 +369,7 @@ class _AllSectionsView extends AnimatedWidget { // app bar's height is _kAppBarMidHeight and only one section heading is // visible. class _SnappingScrollPhysics extends ClampingScrollPhysics { - _SnappingScrollPhysics({ + const _SnappingScrollPhysics({ ScrollPhysics parent, @required this.midScrollOffset, }) : assert(midScrollOffset != null), diff --git a/examples/flutter_gallery/lib/demo/animation/widgets.dart b/examples/flutter_gallery/lib/demo/animation/widgets.dart index bc0d8c6222ab7..1207acc13b49e 100644 --- a/examples/flutter_gallery/lib/demo/animation/widgets.dart +++ b/examples/flutter_gallery/lib/demo/animation/widgets.dart @@ -11,9 +11,9 @@ const double kSectionIndicatorWidth = 32.0; // The card for a single section. Displays the section's gradient and background image. class SectionCard extends StatelessWidget { - SectionCard({ Key key, @required this.section }) : super(key: key) { - assert(section != null); - } + const SectionCard({ Key key, @required this.section }) + : assert(section != null), + super(key: key); final Section section; @@ -60,16 +60,15 @@ class SectionTitle extends StatelessWidget { color: const Color(0x19000000), ); - SectionTitle({ + const SectionTitle({ Key key, @required this.section, @required this.scale, @required this.opacity, - }) : super(key: key) { - assert(section != null); - assert(scale != null); - assert(opacity != null && opacity >= 0.0 && opacity <= 1.0); - } + }) : assert(section != null), + assert(scale != null), + assert(opacity != null && opacity >= 0.0 && opacity <= 1.0), + super(key: key); final Section section; final double scale; @@ -118,10 +117,10 @@ class SectionIndicator extends StatelessWidget { // Display a single SectionDetail. class SectionDetailView extends StatelessWidget { - SectionDetailView({ Key key, @required this.detail }) : super(key: key) { - assert(detail != null && detail.imageAsset != null); - assert((detail.imageAsset ?? detail.title) != null); - } + SectionDetailView({ Key key, @required this.detail }) + : assert(detail != null && detail.imageAsset != null), + assert((detail.imageAsset ?? detail.title) != null), + super(key: key); final SectionDetail detail; diff --git a/examples/flutter_gallery/lib/demo/colors_demo.dart b/examples/flutter_gallery/lib/demo/colors_demo.dart index b84f0e673f7e1..35a1be8a5d9e0 100644 --- a/examples/flutter_gallery/lib/demo/colors_demo.dart +++ b/examples/flutter_gallery/lib/demo/colors_demo.dart @@ -42,16 +42,15 @@ final List allPalettes = [ class ColorItem extends StatelessWidget { - ColorItem({ + const ColorItem({ Key key, @required this.index, @required this.color, this.prefix: '', - }) : super(key: key) { - assert(index != null); - assert(color != null); - assert(prefix != null); - } + }) : assert(index != null), + assert(color != null), + assert(prefix != null), + super(key: key); final int index; final Color color; @@ -84,9 +83,8 @@ class PaletteTabView extends StatelessWidget { PaletteTabView({ Key key, @required this.colors, - }) : super(key: key) { - assert(colors != null && colors.isValid); - } + }) : assert(colors != null && colors.isValid), + super(key: key); final Palette colors; diff --git a/examples/flutter_gallery/lib/demo/contacts_demo.dart b/examples/flutter_gallery/lib/demo/contacts_demo.dart index 5a34a344723ce..3cd48317fff3d 100644 --- a/examples/flutter_gallery/lib/demo/contacts_demo.dart +++ b/examples/flutter_gallery/lib/demo/contacts_demo.dart @@ -37,9 +37,9 @@ class _ContactCategory extends StatelessWidget { } class _ContactItem extends StatelessWidget { - _ContactItem({ Key key, this.icon, this.lines, this.tooltip, this.onPressed }) : super(key: key) { - assert(lines.length > 1); - } + _ContactItem({ Key key, this.icon, this.lines, this.tooltip, this.onPressed }) + : assert(lines.length > 1), + super(key: key); final IconData icon; final List lines; diff --git a/examples/flutter_gallery/lib/demo/cupertino/cupertino_buttons_demo.dart b/examples/flutter_gallery/lib/demo/cupertino/cupertino_buttons_demo.dart index cbc6d7bccfa65..d9165900d1e82 100644 --- a/examples/flutter_gallery/lib/demo/cupertino/cupertino_buttons_demo.dart +++ b/examples/flutter_gallery/lib/demo/cupertino/cupertino_buttons_demo.dart @@ -5,8 +5,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -const Color _kBlue = const Color(0xFF007AFF); - class CupertinoButtonsDemo extends StatefulWidget { static const String routeName = '/cupertino/buttons'; @@ -27,15 +25,17 @@ class _CupertinoButtonDemoState extends State { children: [ const Padding( padding: const EdgeInsets.all(16.0), - child: const Text('iOS themed buttons are flat. They can have borders or backgrounds but ' - 'only when necessary.'), + child: const Text( + 'iOS themed buttons are flat. They can have borders or backgrounds but ' + 'only when necessary.' + ), ), new Expanded( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: [ new Text(_pressedCount > 0 - ? 'Button pressed $_pressedCount time${_pressedCount == 1 ? '' : 's'}' + ? 'Button pressed $_pressedCount time${_pressedCount == 1 ? "" : "s"}' : ' '), const Padding(padding: const EdgeInsets.all(12.0)), new Align( @@ -46,7 +46,7 @@ class _CupertinoButtonDemoState extends State { new CupertinoButton( child: const Text('Cupertino Button'), onPressed: () { - setState(() {_pressedCount++;}); + setState(() { _pressedCount += 1; }); } ), const CupertinoButton( @@ -59,15 +59,15 @@ class _CupertinoButtonDemoState extends State { const Padding(padding: const EdgeInsets.all(12.0)), new CupertinoButton( child: const Text('With Background'), - color: _kBlue, + color: CupertinoColors.activeBlue, onPressed: () { - setState(() {_pressedCount++;}); + setState(() { _pressedCount += 1; }); } ), const Padding(padding: const EdgeInsets.all(12.0)), const CupertinoButton( child: const Text('Disabled'), - color: _kBlue, + color: CupertinoColors.activeBlue, onPressed: null, ), ], diff --git a/examples/flutter_gallery/lib/demo/cupertino/cupertino_dialog_demo.dart b/examples/flutter_gallery/lib/demo/cupertino/cupertino_dialog_demo.dart index cadff8aafdb3c..d16158c6add54 100644 --- a/examples/flutter_gallery/lib/demo/cupertino/cupertino_dialog_demo.dart +++ b/examples/flutter_gallery/lib/demo/cupertino/cupertino_dialog_demo.dart @@ -5,8 +5,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -const Color _kBlue = const Color(0xFF007AFF); - class CupertinoDialogDemo extends StatefulWidget { static const String routeName = '/cupertino/dialog'; @@ -44,7 +42,7 @@ class _CupertinoDialogDemoState extends State { children: [ new CupertinoButton( child: const Text('Alert'), - color: _kBlue, + color: CupertinoColors.activeBlue, onPressed: () { showDemoDialog( context: context, @@ -69,7 +67,7 @@ class _CupertinoDialogDemoState extends State { const Padding(padding: const EdgeInsets.all(8.0)), new CupertinoButton( child: const Text('Alert with Title'), - color: _kBlue, + color: CupertinoColors.activeBlue, padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 36.0), onPressed: () { showDemoDialog( diff --git a/examples/flutter_gallery/lib/demo/material/cards_demo.dart b/examples/flutter_gallery/lib/demo/material/cards_demo.dart index b02b0ee46a084..4beaacb78c198 100644 --- a/examples/flutter_gallery/lib/demo/material/cards_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/cards_demo.dart @@ -37,9 +37,9 @@ final List destinations = [ ]; class TravelDestinationItem extends StatelessWidget { - TravelDestinationItem({ Key key, @required this.destination }) : super(key: key) { - assert(destination != null && destination.isValid); - } + TravelDestinationItem({ Key key, @required this.destination }) + : assert(destination != null && destination.isValid), + super(key: key); static final double height = 366.0; final TravelDestination destination; diff --git a/examples/flutter_gallery/lib/demo/material/full_screen_dialog_demo.dart b/examples/flutter_gallery/lib/demo/material/full_screen_dialog_demo.dart index 1709e99a067e2..e2876e3f74cce 100644 --- a/examples/flutter_gallery/lib/demo/material/full_screen_dialog_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/full_screen_dialog_demo.dart @@ -19,11 +19,10 @@ enum DismissDialogAction { class DateTimeItem extends StatelessWidget { DateTimeItem({ Key key, DateTime dateTime, @required this.onChanged }) - : date = new DateTime(dateTime.year, dateTime.month, dateTime.day), + : assert(onChanged != null), + date = new DateTime(dateTime.year, dateTime.month, dateTime.day), time = new TimeOfDay(hour: dateTime.hour, minute: dateTime.minute), - super(key: key) { - assert(onChanged != null); - } + super(key: key); final DateTime date; final TimeOfDay time; diff --git a/examples/flutter_gallery/lib/demo/material/grid_list_demo.dart b/examples/flutter_gallery/lib/demo/material/grid_list_demo.dart index 87bba384631ec..fe06de77827bd 100644 --- a/examples/flutter_gallery/lib/demo/material/grid_list_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/grid_list_demo.dart @@ -143,11 +143,10 @@ class GridDemoPhotoItem extends StatelessWidget { @required this.photo, @required this.tileStyle, @required this.onBannerTap - }) : super(key: key) { - assert(photo != null && photo.isValid); - assert(tileStyle != null); - assert(onBannerTap != null); - } + }) : assert(photo != null && photo.isValid), + assert(tileStyle != null), + assert(onBannerTap != null), + super(key: key); final Photo photo; final GridDemoTileStyle tileStyle; diff --git a/examples/flutter_gallery/lib/demo/shrine/shrine_home.dart b/examples/flutter_gallery/lib/demo/shrine/shrine_home.dart index a61ee5c902168..4383bedc305d5 100644 --- a/examples/flutter_gallery/lib/demo/shrine/shrine_home.dart +++ b/examples/flutter_gallery/lib/demo/shrine/shrine_home.dart @@ -116,9 +116,9 @@ class _ShrineGridDelegate extends SliverGridDelegate { // Displays the Vendor's name and avatar. class _VendorItem extends StatelessWidget { - _VendorItem({ Key key, @required this.vendor }) : super(key: key) { - assert(vendor != null); - } + const _VendorItem({ Key key, @required this.vendor }) + : assert(vendor != null), + super(key: key); final Vendor vendor; @@ -240,11 +240,11 @@ class _HeadingLayout extends MultiChildLayoutDelegate { // A card that highlights the "featured" catalog item. class _Heading extends StatelessWidget { - _Heading({ Key key, @required this.product }) : super(key: key) { - assert(product != null); - assert(product.featureTitle != null); - assert(product.featureDescription != null); - } + _Heading({ Key key, @required this.product }) + : assert(product != null), + assert(product.featureTitle != null), + assert(product.featureDescription != null), + super(key: key); final Product product; @@ -294,9 +294,9 @@ class _Heading extends StatelessWidget { // A card that displays a product's image, price, and vendor. The _ProductItem // cards appear in a grid below the heading. class _ProductItem extends StatelessWidget { - _ProductItem({ Key key, @required this.product, this.onPressed }) : super(key: key) { - assert(product != null); - } + const _ProductItem({ Key key, @required this.product, this.onPressed }) + : assert(product != null), + super(key: key); final Product product; final VoidCallback onPressed; diff --git a/examples/flutter_gallery/lib/demo/shrine/shrine_order.dart b/examples/flutter_gallery/lib/demo/shrine/shrine_order.dart index 052bfa0ccd2d9..a55f37b08ea64 100644 --- a/examples/flutter_gallery/lib/demo/shrine/shrine_order.dart +++ b/examples/flutter_gallery/lib/demo/shrine/shrine_order.dart @@ -12,16 +12,15 @@ import 'shrine_types.dart'; // Displays the product title's, description, and order quantity dropdown. class _ProductItem extends StatelessWidget { - _ProductItem({ + const _ProductItem({ Key key, @required this.product, @required this.quantity, @required this.onChanged, - }) : super(key: key) { - assert(product != null); - assert(quantity != null); - assert(onChanged != null); - } + }) : assert(product != null), + assert(quantity != null), + assert(onChanged != null), + super(key: key); final Product product; final int quantity; @@ -70,9 +69,9 @@ class _ProductItem extends StatelessWidget { // Vendor name and description class _VendorItem extends StatelessWidget { - _VendorItem({ Key key, @required this.vendor }) : super(key: key) { - assert(vendor != null); - } + const _VendorItem({ Key key, @required this.vendor }) + : assert(vendor != null), + super(key: key); final Vendor vendor; @@ -141,15 +140,14 @@ class _HeadingLayout extends MultiChildLayoutDelegate { // Describes a product and vendor in detail, supports specifying // a order quantity (0-5). Appears at the top of the OrderPage. class _Heading extends StatelessWidget { - _Heading({ + const _Heading({ Key key, @required this.product, @required this.quantity, this.quantityChanged, - }) : super(key: key) { - assert(product != null); - assert(quantity != null && quantity >= 0 && quantity <= 5); - } + }) : assert(product != null), + assert(quantity != null && quantity >= 0 && quantity <= 5), + super(key: key); final Product product; final int quantity; @@ -213,11 +211,10 @@ class OrderPage extends StatefulWidget { @required this.order, @required this.products, @required this.shoppingCart, - }) : super(key: key) { - assert(order != null); - assert(products != null && products.isNotEmpty); - assert(shoppingCart != null); - } + }) : assert(order != null), + assert(products != null && products.isNotEmpty), + assert(shoppingCart != null), + super(key: key); final Order order; final List products; @@ -328,9 +325,8 @@ class ShrineOrderRoute extends ShrinePageRoute { @required this.order, WidgetBuilder builder, RouteSettings settings: const RouteSettings(), - }) : super(builder: builder, settings: settings) { - assert(order != null); - } + }) : assert(order != null), + super(builder: builder, settings: settings); Order order; diff --git a/examples/flutter_gallery/lib/demo/shrine/shrine_page.dart b/examples/flutter_gallery/lib/demo/shrine/shrine_page.dart index eb073feda3e45..d6c401860af97 100644 --- a/examples/flutter_gallery/lib/demo/shrine/shrine_page.dart +++ b/examples/flutter_gallery/lib/demo/shrine/shrine_page.dart @@ -15,17 +15,16 @@ enum ShrineAction { } class ShrinePage extends StatefulWidget { - ShrinePage({ + const ShrinePage({ Key key, @required this.scaffoldKey, @required this.body, this.floatingActionButton, this.products, this.shoppingCart - }) : super(key: key) { - assert(body != null); - assert(scaffoldKey != null); - } + }) : assert(body != null), + assert(scaffoldKey != null), + super(key: key); final GlobalKey scaffoldKey; final Widget body; diff --git a/examples/flutter_gallery/lib/demo/shrine/shrine_theme.dart b/examples/flutter_gallery/lib/demo/shrine/shrine_theme.dart index e51bbb7430be7..8dc5cf5e73254 100644 --- a/examples/flutter_gallery/lib/demo/shrine/shrine_theme.dart +++ b/examples/flutter_gallery/lib/demo/shrine/shrine_theme.dart @@ -27,9 +27,9 @@ TextStyle abrilFatfaceRegular34(Color color) => new ShrineStyle.abrilFatface(34. /// InheritedWidget is shared by all of the routes and widgets created for /// the Shrine app. class ShrineTheme extends InheritedWidget { - ShrineTheme({ Key key, @required Widget child }) : super(key: key, child: child) { - assert(child != null); - } + ShrineTheme({ Key key, @required Widget child }) + : assert(child != null), + super(key: key, child: child); final Color cardBackgroundColor = Colors.white; final Color appBarBackgroundColor = Colors.white; diff --git a/examples/flutter_gallery/lib/demo/shrine/shrine_types.dart b/examples/flutter_gallery/lib/demo/shrine/shrine_types.dart index 13673f85f3142..42827d0f13de5 100644 --- a/examples/flutter_gallery/lib/demo/shrine/shrine_types.dart +++ b/examples/flutter_gallery/lib/demo/shrine/shrine_types.dart @@ -66,11 +66,10 @@ class Product { } class Order { - Order({ @required this.product, this.quantity: 1, this.inCart: false }) { - assert(product != null); - assert(quantity != null && quantity >= 0); - assert(inCart != null); - } + Order({ @required this.product, this.quantity: 1, this.inCart: false }) + : assert(product != null), + assert(quantity != null && quantity >= 0), + assert(inCart != null); final Product product; final int quantity; diff --git a/examples/flutter_gallery/lib/demo/typography_demo.dart b/examples/flutter_gallery/lib/demo/typography_demo.dart index da6e34f5d6ea9..b39f6328dffcd 100644 --- a/examples/flutter_gallery/lib/demo/typography_demo.dart +++ b/examples/flutter_gallery/lib/demo/typography_demo.dart @@ -6,16 +6,15 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class TextStyleItem extends StatelessWidget { - TextStyleItem({ + const TextStyleItem({ Key key, @required this.name, @required this.style, @required this.text, - }) : super(key: key) { - assert(name != null); - assert(style != null); - assert(text != null); - } + }) : assert(name != null), + assert(style != null), + assert(text != null), + super(key: key); final String name; final TextStyle style; diff --git a/examples/flutter_gallery/lib/gallery/drawer.dart b/examples/flutter_gallery/lib/gallery/drawer.dart index e0ff3e3e6a457..992b6a700fba6 100644 --- a/examples/flutter_gallery/lib/gallery/drawer.dart +++ b/examples/flutter_gallery/lib/gallery/drawer.dart @@ -11,6 +11,20 @@ import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; class LinkTextSpan extends TextSpan { + + // Beware! + // + // This class is only safe because the TapGestureRecognizer is not + // given a deadline and therefore never allocates any resources. + // + // In any other situation -- setting a deadline, using any of the less trivial + // recognizers, etc -- you would have to manage the gesture recognizer's + // lifetime and call dispose() when the TextSpan was no longer being rendered. + // + // Since TextSpan itself is @immutable, this means that you would have to + // manage the recognizer from outside the TextSpan, e.g. in the State of a + // stateful widget that then hands the recognizer to the TextSpan. + LinkTextSpan({ TextStyle style, String url, String text }) : super( style: style, text: text ?? url, @@ -88,7 +102,7 @@ class _GalleryDrawerHeaderState extends State { } class GalleryDrawer extends StatelessWidget { - GalleryDrawer({ + const GalleryDrawer({ Key key, this.useLightTheme, @required this.onThemeChanged, @@ -102,10 +116,9 @@ class GalleryDrawer extends StatelessWidget { this.onCheckerboardOffscreenLayersChanged, this.onPlatformChanged, this.onSendFeedback, - }) : super(key: key) { - assert(onThemeChanged != null); - assert(onTimeDilationChanged != null); - } + }) : assert(onThemeChanged != null), + assert(onTimeDilationChanged != null), + super(key: key); final bool useLightTheme; final ValueChanged onThemeChanged; diff --git a/examples/flutter_gallery/lib/gallery/home.dart b/examples/flutter_gallery/lib/gallery/home.dart index 76f9994e8af4d..03a766e27358d 100644 --- a/examples/flutter_gallery/lib/gallery/home.dart +++ b/examples/flutter_gallery/lib/gallery/home.dart @@ -66,7 +66,7 @@ class _AppBarBackground extends StatelessWidget { } class GalleryHome extends StatefulWidget { - GalleryHome({ + const GalleryHome({ Key key, this.useLightTheme, @required this.onThemeChanged, @@ -80,10 +80,9 @@ class GalleryHome extends StatefulWidget { this.onCheckerboardOffscreenLayersChanged, this.onPlatformChanged, this.onSendFeedback, - }) : super(key: key) { - assert(onThemeChanged != null); - assert(onTimeDilationChanged != null); - } + }) : assert(onThemeChanged != null), + assert(onTimeDilationChanged != null), + super(key: key); final bool useLightTheme; final ValueChanged onThemeChanged; diff --git a/examples/flutter_gallery/lib/gallery/item.dart b/examples/flutter_gallery/lib/gallery/item.dart index aa9b74dd4a007..dafa548b4cfe5 100644 --- a/examples/flutter_gallery/lib/gallery/item.dart +++ b/examples/flutter_gallery/lib/gallery/item.dart @@ -12,18 +12,16 @@ import '../demo/all.dart'; typedef Widget GalleryDemoBuilder(); class GalleryItem extends StatelessWidget { - GalleryItem({ + const GalleryItem({ @required this.title, this.subtitle, @required this.category, @required this.routeName, @required this.buildRoute, - }) { - assert(title != null); - assert(category != null); - assert(routeName != null); - assert(buildRoute != null); - } + }) : assert(title != null), + assert(category != null), + assert(routeName != null), + assert(buildRoute != null); final String title; final String subtitle; diff --git a/examples/flutter_gallery/lib/gallery/updates.dart b/examples/flutter_gallery/lib/gallery/updates.dart index 021dd3db217e4..fabafe0998e1c 100644 --- a/examples/flutter_gallery/lib/gallery/updates.dart +++ b/examples/flutter_gallery/lib/gallery/updates.dart @@ -12,9 +12,9 @@ import 'package:url_launcher/url_launcher.dart'; typedef Future UpdateUrlFetcher(); class Updater extends StatefulWidget { - Updater({ @required this.updateUrlFetcher, this.child, Key key }) : super(key: key) { - assert(updateUrlFetcher != null); - } + const Updater({ @required this.updateUrlFetcher, this.child, Key key }) + : assert(updateUrlFetcher != null), + super(key: key); final UpdateUrlFetcher updateUrlFetcher; final Widget child; diff --git a/examples/flutter_gallery/test_driver/transitions_perf_test.dart b/examples/flutter_gallery/test_driver/transitions_perf_test.dart index 827ee70055782..e2017d45b3b54 100644 --- a/examples/flutter_gallery/test_driver/transitions_perf_test.dart +++ b/examples/flutter_gallery/test_driver/transitions_perf_test.dart @@ -174,11 +174,15 @@ Future runDemos(Iterable demos, FlutterDriver driver) async { } } -void main() { +void main([List args = const []]) { group('flutter gallery transitions', () { FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); + if (args.contains('--with_semantics')) { + print('Enabeling semantics...'); + await driver.setSemantics(true); + } }); tearDownAll(() async { diff --git a/examples/flutter_gallery/test_driver/transitions_perf_with_semantics.dart b/examples/flutter_gallery/test_driver/transitions_perf_with_semantics.dart new file mode 100644 index 0000000000000..02879b4871129 --- /dev/null +++ b/examples/flutter_gallery/test_driver/transitions_perf_with_semantics.dart @@ -0,0 +1,9 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'transitions_perf.dart' as transitions_perf; + +void main() { + transitions_perf.main(); +} diff --git a/examples/flutter_gallery/test_driver/transitions_perf_with_semantics_test.dart b/examples/flutter_gallery/test_driver/transitions_perf_with_semantics_test.dart new file mode 100644 index 0000000000000..afa8235eddd3d --- /dev/null +++ b/examples/flutter_gallery/test_driver/transitions_perf_with_semantics_test.dart @@ -0,0 +1,9 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'transitions_perf_test.dart' as transitions_perf_test; + +void main() { + transitions_perf_test.main(['--with_semantics']); +} diff --git a/examples/hello_world/hello_world.iml b/examples/hello_world/hello_world.iml index 9d5dae19540c2..c82504f3177c9 100644 --- a/examples/hello_world/hello_world.iml +++ b/examples/hello_world/hello_world.iml @@ -7,6 +7,7 @@ + diff --git a/examples/layers/services/isolate.dart b/examples/layers/services/isolate.dart index b325d7d9138d1..395875780faf4 100644 --- a/examples/layers/services/isolate.dart +++ b/examples/layers/services/isolate.dart @@ -18,12 +18,11 @@ typedef void OnResultListener(String result); // in real-world applications. class Calculator { Calculator({ @required this.onProgressListener, @required this.onResultListener, String data }) - // In order to keep the example files smaller, we "cheat" a little and - // replicate our small json string into a 10,000-element array. - : _data = _replicateJson(data, 10000) { - assert(onProgressListener != null); - assert(onResultListener != null); - } + : assert(onProgressListener != null), + assert(onResultListener != null), + // In order to keep the example files smaller, we "cheat" a little and + // replicate our small json string into a 10,000-element array. + _data = _replicateJson(data, 10000); final OnProgressListener onProgressListener; final OnResultListener onResultListener; @@ -87,9 +86,9 @@ class CalculationMessage { // progress of the background computation. class CalculationManager { CalculationManager({ @required this.onProgressListener, @required this.onResultListener }) - : _receivePort = new ReceivePort() { - assert(onProgressListener != null); - assert(onResultListener != null); + : assert(onProgressListener != null), + assert(onResultListener != null), + _receivePort = new ReceivePort() { _receivePort.listen(_handleMessage); } diff --git a/examples/platform_view/lib/main.dart b/examples/platform_view/lib/main.dart index b6426b5eff3a6..a1ab83a17118a 100644 --- a/examples/platform_view/lib/main.dart +++ b/examples/platform_view/lib/main.dart @@ -19,13 +19,13 @@ class PlatformView extends StatelessWidget { theme: new ThemeData( primarySwatch: Colors.grey, ), - home: new MyHomePage(title: 'Platform View'), + home: const MyHomePage(title: 'Platform View'), ); } } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + const MyHomePage({Key key, this.title}) : super(key: key); final String title; diff --git a/examples/stocks/lib/stock_types.dart b/examples/stocks/lib/stock_types.dart index f3d356b406a01..163628e7a8803 100644 --- a/examples/stocks/lib/stock_types.dart +++ b/examples/stocks/lib/stock_types.dart @@ -19,18 +19,16 @@ class StockConfiguration { @required this.debugShowRainbow, @required this.showPerformanceOverlay, @required this.showSemanticsDebugger - }) { - assert(stockMode != null); - assert(backupMode != null); - assert(debugShowGrid != null); - assert(debugShowSizes != null); - assert(debugShowBaselines != null); - assert(debugShowLayers != null); - assert(debugShowPointers != null); - assert(debugShowRainbow != null); - assert(showPerformanceOverlay != null); - assert(showSemanticsDebugger != null); - } + }) : assert(stockMode != null), + assert(backupMode != null), + assert(debugShowGrid != null), + assert(debugShowSizes != null), + assert(debugShowBaselines != null), + assert(debugShowLayers != null), + assert(debugShowPointers != null), + assert(debugShowRainbow != null), + assert(showPerformanceOverlay != null), + assert(showSemanticsDebugger != null); final StockMode stockMode; final BackupMode backupMode; diff --git a/packages/flutter/lib/cupertino.dart b/packages/flutter/lib/cupertino.dart index 684c2fe4dbf4b..6ec803a7f4f64 100644 --- a/packages/flutter/lib/cupertino.dart +++ b/packages/flutter/lib/cupertino.dart @@ -12,7 +12,10 @@ export 'src/cupertino/bottom_tab_bar.dart'; export 'src/cupertino/button.dart'; export 'src/cupertino/colors.dart'; export 'src/cupertino/dialog.dart'; +export 'src/cupertino/nav_bar.dart'; export 'src/cupertino/page.dart'; +export 'src/cupertino/scaffold.dart'; export 'src/cupertino/slider.dart'; export 'src/cupertino/switch.dart'; export 'src/cupertino/thumb_painter.dart'; +export 'widgets.dart'; diff --git a/packages/flutter/lib/foundation.dart b/packages/flutter/lib/foundation.dart index 6221f086f631e..704a8b755f7be 100644 --- a/packages/flutter/lib/foundation.dart +++ b/packages/flutter/lib/foundation.dart @@ -16,6 +16,16 @@ export 'package:meta/meta.dart' show protected, required; +// Examples can assume: +// String _name; +// bool _first; +// bool _lights; +// bool _visible; +// double _volume; +// dynamic _calculation; +// dynamic _last; +// dynamic _selection; + export 'src/foundation/assertions.dart'; export 'src/foundation/basic_types.dart'; export 'src/foundation/binding.dart'; diff --git a/packages/flutter/lib/src/animation/animation_controller.dart b/packages/flutter/lib/src/animation/animation_controller.dart index 9619ed4faf9c2..26c629ea63937 100644 --- a/packages/flutter/lib/src/animation/animation_controller.dart +++ b/packages/flutter/lib/src/animation/animation_controller.dart @@ -15,6 +15,9 @@ import 'listener_helpers.dart'; export 'package:flutter/scheduler.dart' show TickerFuture, TickerCanceled; +// Examples can assume: +// AnimationController _controller; + /// The direction in which an animation is running. enum _AnimationDirection { /// The animation is running from beginning to end. @@ -44,17 +47,20 @@ const Tolerance _kFlingTolerance = const Tolerance( /// * Define the [upperBound] and [lowerBound] values of an animation. /// * Create a [fling] animation effect using a physics simulation. /// -/// By default, an [AnimationController] linearly produces values that range from 0.0 to 1.0, during -/// a given duration. The animation controller generates a new value whenever the device running -/// your app is ready to display a new frame (typically, this rate is around 60 values per second). +/// By default, an [AnimationController] linearly produces values that range +/// from 0.0 to 1.0, during a given duration. The animation controller generates +/// a new value whenever the device running your app is ready to display a new +/// frame (typically, this rate is around 60 values per second). /// -/// An AnimationController needs a [TickerProvider], which is configured using the `vsync` argument -/// on the constructor. If you are creating an AnimationController from a [State], then you can use -/// the [TickerProviderStateMixin] and [SingleTickerProviderStateMixin] classes to obtain a suitable -/// [TickerProvider]. The widget test framework [WidgetTester] object can be used as a ticker provider -/// in the context of tests. In other contexts, you will have to either pass a [TickerProvider] from -/// a higher level (e.g. indirectly from a [State] that mixes in [TickerProviderStateMixin]), or -/// create a custom [TickerProvider] subclass. +/// An AnimationController needs a [TickerProvider], which is configured using +/// the `vsync` argument on the constructor. If you are creating an +/// AnimationController from a [State], then you can use the +/// [TickerProviderStateMixin] and [SingleTickerProviderStateMixin] classes to +/// obtain a suitable [TickerProvider]. The widget test framework [WidgetTester] +/// object can be used as a ticker provider in the context of tests. In other +/// contexts, you will have to either pass a [TickerProvider] from a higher +/// level (e.g. indirectly from a [State] that mixes in +/// [TickerProviderStateMixin]), or create a custom [TickerProvider] subclass. /// /// The methods that start animations return a [TickerFuture] object which /// completes when the animation completes successfully, and never throws an @@ -115,12 +121,11 @@ class AnimationController extends Animation this.lowerBound: 0.0, this.upperBound: 1.0, @required TickerProvider vsync, - }) { - assert(lowerBound != null); - assert(upperBound != null); - assert(upperBound >= lowerBound); - assert(vsync != null); - _direction = _AnimationDirection.forward; + }) : assert(lowerBound != null), + assert(upperBound != null), + assert(upperBound >= lowerBound), + assert(vsync != null), + _direction = _AnimationDirection.forward { _ticker = vsync.createTicker(_tick); _internalSetValue(value ?? lowerBound); } @@ -146,11 +151,11 @@ class AnimationController extends Animation this.duration, this.debugLabel, @required TickerProvider vsync, - }) : lowerBound = double.NEGATIVE_INFINITY, - upperBound = double.INFINITY { - assert(value != null); - assert(vsync != null); - _direction = _AnimationDirection.forward; + }) : assert(value != null), + assert(vsync != null), + lowerBound = double.NEGATIVE_INFINITY, + upperBound = double.INFINITY, + _direction = _AnimationDirection.forward { _ticker = vsync.createTicker(_tick); _internalSetValue(value); } @@ -496,11 +501,10 @@ class AnimationController extends Animation class _InterpolationSimulation extends Simulation { _InterpolationSimulation(this._begin, this._end, Duration duration, this._curve) - : _durationInSeconds = duration.inMicroseconds / Duration.MICROSECONDS_PER_SECOND { - assert(_durationInSeconds > 0.0); - assert(_begin != null); - assert(_end != null); - } + : assert(_begin != null), + assert(_end != null), + assert(duration != null && duration.inMicroseconds > 0), + _durationInSeconds = duration.inMicroseconds / Duration.MICROSECONDS_PER_SECOND; final double _durationInSeconds; final double _begin; diff --git a/packages/flutter/lib/src/animation/animations.dart b/packages/flutter/lib/src/animation/animations.dart index f0fdcd90dd63f..82e104a977e60 100644 --- a/packages/flutter/lib/src/animation/animations.dart +++ b/packages/flutter/lib/src/animation/animations.dart @@ -115,6 +115,10 @@ class AlwaysStoppedAnimation extends Animation { /// given [parent] Animation. To implement an [Animation] that proxies to a /// parent, this class plus implementing "T get value" is all that is necessary. abstract class AnimationWithParentMixin { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory AnimationWithParentMixin._() => null; + /// The animation whose value this animation will proxy. /// /// This animation must remain the same for the lifetime of this object. If @@ -233,19 +237,19 @@ class ProxyAnimation extends Animation /// An animation that is the reverse of another animation. /// /// If the parent animation is running forward from 0.0 to 1.0, this animation -/// is running in reverse from 1.0 to 0.0. Notice that using a ReverseAnimation -/// is different from simply using a [Tween] with a begin of 1.0 and an end of -/// 0.0 because the tween does not change the status or direction of the -/// animation. +/// is running in reverse from 1.0 to 0.0. +/// +/// Using a [ReverseAnimation] is different from simply using a [Tween] with a +/// begin of 1.0 and an end of 0.0 because the tween does not change the status +/// or direction of the animation. class ReverseAnimation extends Animation with AnimationLazyListenerMixin, AnimationLocalStatusListenersMixin { /// Creates a reverse animation. /// /// The parent argument must not be null. - ReverseAnimation(this.parent) { - assert(parent != null); - } + ReverseAnimation(this.parent) + : assert(parent != null); /// The animation whose value and direction this animation is reversing. final Animation parent; @@ -327,9 +331,8 @@ class CurvedAnimation extends Animation with AnimationWithParentMixin /// /// The current train argument must not be null but the next train argument /// can be null. - TrainHoppingAnimation(this._currentTrain, this._nextTrain, { this.onSwitchedTrain }) { - assert(_currentTrain != null); + TrainHoppingAnimation(this._currentTrain, this._nextTrain, { this.onSwitchedTrain }) + : assert(_currentTrain != null) { if (_nextTrain != null) { if (_currentTrain.value > _nextTrain.value) { _mode = _TrainHoppingMode.maximize; @@ -548,10 +551,8 @@ abstract class CompoundAnimation extends Animation CompoundAnimation({ @required this.first, @required this.next, - }) { - assert(first != null); - assert(next != null); - } + }) : assert(first != null), + assert(next != null); /// The first sub-animation. Its status takes precedence if neither are /// animating. diff --git a/packages/flutter/lib/src/animation/curves.dart b/packages/flutter/lib/src/animation/curves.dart index cecba6ba4d7df..f0c74528d7342 100644 --- a/packages/flutter/lib/src/animation/curves.dart +++ b/packages/flutter/lib/src/animation/curves.dart @@ -27,6 +27,13 @@ abstract class Curve { /// Returns a new curve that is the reversed inversion of this one. /// This is often useful as the reverseCurve of an [Animation]. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_bounce_in.png) + /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_flipped.png) + /// + /// See also: + /// + /// * [FlippedCurve], the class that is used to implement this getter. Curve get flipped => new FlippedCurve(this); @override @@ -49,6 +56,8 @@ class _Linear extends Curve { /// /// The curve rises linearly from 0.0 to 1.0 and then falls discontinuously back /// to 0.0 each iteration. +/// +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_sawtooth.png) class SawTooth extends Curve { /// Creates a sawtooth curve. /// @@ -80,6 +89,8 @@ class SawTooth extends Curve { /// animation that uses an [Interval] with its [begin] set to 0.5 and its [end] /// set to 1.0 will essentially become a three-second animation that starts /// three seconds later. +/// +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_interval.png) class Interval extends Curve { /// Creates an interval curve. /// @@ -127,6 +138,8 @@ class Interval extends Curve { } /// A curve that is 0.0 until it hits the threshold, then it jumps to 1.0. +/// +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_threshold.png) class Threshold extends Curve { /// Creates a threshold curve. /// @@ -151,14 +164,17 @@ class Threshold extends Curve { /// A cubic polynomial mapping of the unit interval. /// -/// See [Curves] for a number of commonly used cubic curves. -/// -/// See also: +/// The [Curves] class contains some commonly used cubic curves: /// /// * [Curves.ease] /// * [Curves.easeIn] /// * [Curves.easeOut] /// * [Curves.easeInOut] +/// +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_ease.png) +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_ease_in.png) +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_ease_out.png) +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_ease_in_out.png) class Cubic extends Curve { /// Creates a cubic curve. /// @@ -232,6 +248,11 @@ class Cubic extends Curve { /// This curve evalutes the given curve in reverse (i.e., from 1.0 to 0.0 as t /// increases from 0.0 to 1.0) and returns the inverse of the given curve's value /// (i.e., 1.0 minus the given curve's value). +/// +/// This is the class used to implement the [flipped] getter on curves. +/// +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_bounce_in.png) +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_flipped_curve.png) class FlippedCurve extends Curve { /// Creates a flipped curve. /// @@ -334,6 +355,11 @@ class _BounceInOutCurve extends Curve { // ELASTIC CURVES /// An oscillating curve that grows in magnitude while overshooting its bounds. +/// +/// An instance of this class using the default period of 0.4 is available as +/// [Curves.elasticIn]. +/// +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_elastic_in.png) class ElasticInCurve extends Curve { /// Creates an elastic-in curve. /// @@ -358,6 +384,11 @@ class ElasticInCurve extends Curve { } /// An oscillating curve that shrinks in magnitude while overshooting its bounds. +/// +/// An instance of this class using the default period of 0.4 is available as +/// [Curves.elasticOut]. +/// +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_elastic_out.png) class ElasticOutCurve extends Curve { /// Creates an elastic-out curve. /// @@ -380,7 +411,13 @@ class ElasticOutCurve extends Curve { } } -/// An oscillating curve that grows and then shrinks in magnitude while overshooting its bounds. +/// An oscillating curve that grows and then shrinks in magnitude while +/// overshooting its bounds. +/// +/// An instance of this class using the default period of 0.4 is available as +/// [Curves.elasticInOut]. +/// +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_elastic_in_out.png) class ElasticInOutCurve extends Curve { /// Creates an elastic-in-out curve. /// @@ -411,6 +448,25 @@ class ElasticInOutCurve extends Curve { // PREDEFINED CURVES /// A collection of common animation curves. +/// +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_bounce_in.png) +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_bounce_in_out.png) +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_bounce_out.png) +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_decelerate.png) +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_ease.png) +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_ease_in.png) +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_ease_in_out.png) +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_ease_out.png) +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_elastic_in.png) +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_elastic_in_out.png) +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_elastic_out.png) +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_fast_out_slow_in.png) +/// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_linear.png) +/// +/// See also: +/// +/// * [Curve], the interface implemented by the constants available from the +/// [Curves] class. class Curves { Curves._(); @@ -419,6 +475,8 @@ class Curves { /// This is the identity map over the unit interval: its [Curve.transform] /// method returns its input unmodified. This is useful as a default curve for /// cases where a [Curve] is required but no actual curve is desired. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_linear.png) static const Curve linear = const _Linear._(); /// A curve where the rate of change starts out quickly and then decelerates; an @@ -426,18 +484,28 @@ class Curves { /// /// This is equivalent to the Android `DecelerateInterpolator` class with a unit /// factor (the default factor). + /// + /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_decelerate.png) static const Curve decelerate = const _DecelerateCurve._(); /// A cubic animation curve that speeds up quickly and ends slowly. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_ease.png) static const Cubic ease = const Cubic(0.25, 0.1, 0.25, 1.0); /// A cubic animation curve that starts slowly and ends quickly. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_ease_in.png) static const Cubic easeIn = const Cubic(0.42, 0.0, 1.0, 1.0); /// A cubic animation curve that starts quickly and ends slowly. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_ease_out.png) static const Cubic easeOut = const Cubic(0.0, 0.0, 0.58, 1.0); /// A cubic animation curve that starts slowly, speeds up, and then and ends slowly. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_ease_in_out.png) static const Cubic easeInOut = const Cubic(0.42, 0.0, 0.58, 1.0); /// A curve that starts quickly and eases into its final position. @@ -445,23 +513,37 @@ class Curves { /// Over the course of the animation, the object spends more time near its /// final destination. As a result, the user isn’t left waiting for the /// animation to finish, and the negative effects of motion are minimized. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_fast_out_slow_in.png) static const Cubic fastOutSlowIn = const Cubic(0.4, 0.0, 0.2, 1.0); /// An oscillating curve that grows in magnitude. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_bounce_in.png) static const Curve bounceIn = const _BounceInCurve._(); /// An oscillating curve that first grows and then shrink in magnitude. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_bounce_out.png) static const Curve bounceOut = const _BounceOutCurve._(); /// An oscillating curve that first grows and then shrink in magnitude. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_bounce_in_out.png) static const Curve bounceInOut = const _BounceInOutCurve._(); /// An oscillating curve that grows in magnitude while overshootings its bounds. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_elastic_in.png) static const ElasticInCurve elasticIn = const ElasticInCurve(); /// An oscillating curve that shrinks in magnitude while overshootings its bounds. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_elastic_out.png) static const ElasticOutCurve elasticOut = const ElasticOutCurve(); /// An oscillating curve that grows and then shrinks in magnitude while overshootings its bounds. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/animation/curve_elastic_in_out.png) static const ElasticInOutCurve elasticInOut = const ElasticInOutCurve(); } diff --git a/packages/flutter/lib/src/animation/listener_helpers.dart b/packages/flutter/lib/src/animation/listener_helpers.dart index 570c83a584b9d..d75d99ad0007b 100644 --- a/packages/flutter/lib/src/animation/listener_helpers.dart +++ b/packages/flutter/lib/src/animation/listener_helpers.dart @@ -9,12 +9,20 @@ import 'package:flutter/foundation.dart'; import 'animation.dart'; abstract class _ListenerMixin { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory _ListenerMixin._() => null; + void didRegisterListener(); void didUnregisterListener(); } /// A mixin that helps listen to another object only when this object has registered listeners. -abstract class AnimationLazyListenerMixin implements _ListenerMixin { +abstract class AnimationLazyListenerMixin extends _ListenerMixin { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory AnimationLazyListenerMixin._() => null; + int _listenerCounter = 0; @override @@ -47,7 +55,11 @@ abstract class AnimationLazyListenerMixin implements _ListenerMixin { /// A mixin that replaces the didRegisterListener/didUnregisterListener contract /// with a dispose contract. -abstract class AnimationEagerListenerMixin implements _ListenerMixin { +abstract class AnimationEagerListenerMixin extends _ListenerMixin { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory AnimationEagerListenerMixin._() => null; + @override void didRegisterListener() { } @@ -60,9 +72,13 @@ abstract class AnimationEagerListenerMixin implements _ListenerMixin { void dispose() { } } -/// A mixin that implements the addListener/removeListener protocol and notifies -/// all the registered listeners when notifyListeners is called. +/// A mixin that implements the [addListener]/[removeListener] protocol and notifies +/// all the registered listeners when [notifyListeners] is called. abstract class AnimationLocalListenersMixin extends _ListenerMixin { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory AnimationLocalListenersMixin._() => null; + final ObserverList _listeners = new ObserverList(); /// Calls the listener every time the value of the animation changes. @@ -111,6 +127,10 @@ abstract class AnimationLocalListenersMixin extends _ListenerMixin { /// and notifies all the registered listeners when notifyStatusListeners is /// called. abstract class AnimationLocalStatusListenersMixin extends _ListenerMixin { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory AnimationLocalStatusListenersMixin._() => null; + final ObserverList _statusListeners = new ObserverList(); /// Calls listener every time the status of the animation changes. diff --git a/packages/flutter/lib/src/animation/tween.dart b/packages/flutter/lib/src/animation/tween.dart index e993a003db2dd..6a43000545511 100644 --- a/packages/flutter/lib/src/animation/tween.dart +++ b/packages/flutter/lib/src/animation/tween.dart @@ -97,7 +97,7 @@ class _ChainedEvaluation extends Animatable { /// `_animation`: /// /// ```dart -/// _animation = new Tween( +/// Animation _animation = new Tween( /// begin: const Offset(100.0, 50.0), /// end: const Offset(200.0, 300.0), /// ).animate(_controller); @@ -291,9 +291,8 @@ class CurveTween extends Animatable { /// Creates a curve tween. /// /// The [curve] argument must not be null. - CurveTween({ @required this.curve }) { - assert(curve != null); - } + CurveTween({ @required this.curve }) + : assert(curve != null); /// The curve to use when transforming the value of the animation. Curve curve; diff --git a/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart b/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart index 8a0ddded09cd0..e28125d9a18dd 100644 --- a/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart +++ b/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart @@ -13,8 +13,26 @@ import 'colors.dart'; const double _kTabBarHeight = 50.0; const Color _kDefaultTabBarBackgroundColor = const Color(0xCCF8F8F8); - -class CupertinoTabBar extends StatelessWidget { +const Color _kDefaultTabBarBorderColor = const Color(0x4C000000); + +/// An iOS-styled bottom navigation tab bar. +/// +/// Displays multiple tabs using [BottomNavigationBarItem] with one tab being +/// active, the first tab by default. +/// +/// This [StatelessWidget] doesn't store the active tab itself. You must +/// listen to the [onTap] callbacks and call `setState` with a new [currentIndex] +/// for the new selection to reflect. +/// +/// Tab changes typically trigger a switch between [Navigator]s, each with its +/// own navigation stack, per standard iOS design. +/// +/// If the given [backgroundColor]'s opacity is not 1.0 (which is the case by +/// default), it will produce a blurring effect to the content behind it. +// +// TODO(xster): document using with a CupertinoScaffold. +class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { + /// Creates a tab bar in the iOS style. CupertinoTabBar({ Key key, @required this.items, @@ -24,12 +42,11 @@ class CupertinoTabBar extends StatelessWidget { this.activeColor: CupertinoColors.activeBlue, this.inactiveColor: CupertinoColors.inactiveGray, this.iconSize: 24.0, - }) : super(key: key) { - assert(items != null); - assert(items.length >= 2); - assert(0 <= currentIndex && currentIndex < items.length); - assert(iconSize != null); - } + }) : assert(items != null), + assert(items.length >= 2), + assert(0 <= currentIndex && currentIndex < items.length), + assert(iconSize != null), + super(key: key); /// The interactive items laid out within the bottom navigation bar. final List items; @@ -64,15 +81,19 @@ class CupertinoTabBar extends StatelessWidget { /// should configure itself to match the icon theme's size and color. final double iconSize; + /// True if the tab bar's background color has no transparency. + bool get opaque => backgroundColor.alpha == 0xFF; + @override - Widget build(BuildContext context) { - final bool addBlur = backgroundColor.alpha != 0xFF; + Size get preferredSize => const Size.fromHeight(_kTabBarHeight); + @override + Widget build(BuildContext context) { Widget result = new DecoratedBox( decoration: new BoxDecoration( border: const Border( top: const BorderSide( - color: const Color(0x4C000000), + color: _kDefaultTabBarBorderColor, width: 0.0, // One physical pixel. style: BorderStyle.solid, ), @@ -103,7 +124,7 @@ class CupertinoTabBar extends StatelessWidget { ), ); - if (addBlur) { + if (!opaque) { // For non-opaque backgrounds, apply a blur effect. result = new ClipRect( child: new BackdropFilter( @@ -124,6 +145,7 @@ class CupertinoTabBar extends StatelessWidget { _wrapActiveItem( new Expanded( child: new GestureDetector( + behavior: HitTestBehavior.opaque, onTap: onTap == null ? null : () { onTap(index); }, child: new Padding( padding: const EdgeInsets.only(bottom: 4.0), @@ -158,4 +180,28 @@ class CupertinoTabBar extends StatelessWidget { ), ); } + + /// Create a clone of the current [CupertinoTabBar] but with provided + /// parameters overriden. + CupertinoTabBar copyWith({ + Key key, + List items, + Color backgroundColor, + Color activeColor, + Color inactiveColor, + Size iconSize, + int currentIndex, + ValueChanged onTap, + }) { + return new CupertinoTabBar( + key: key ?? this.key, + items: items ?? this.items, + backgroundColor: backgroundColor ?? this.backgroundColor, + activeColor: activeColor ?? this.activeColor, + inactiveColor: inactiveColor ?? this.inactiveColor, + iconSize: iconSize ?? this.iconSize, + currentIndex: currentIndex ?? this.currentIndex, + onTap: onTap ?? this.onTap, + ); + } } diff --git a/packages/flutter/lib/src/cupertino/colors.dart b/packages/flutter/lib/src/cupertino/colors.dart index cb546cb56427e..8765b3e6ee4e9 100644 --- a/packages/flutter/lib/src/cupertino/colors.dart +++ b/packages/flutter/lib/src/cupertino/colors.dart @@ -4,6 +4,8 @@ import 'dart:ui' show Color; +/// A palette of [Color] constants that describe colors commonly used when +/// matching the iOS platform aesthetics. class CupertinoColors { CupertinoColors._(); @@ -17,9 +19,18 @@ class CupertinoColors { static const Color activeGreen = const Color(0xFF4CD964); /// Opaque white color. Used for backgrounds and fonts against dark backgrounds. + /// + /// See also: + /// + /// * [Colors.white], the same color, in the material design palette. + /// * [black], opaque black in the [CupertinoColors] palette. static const Color white = const Color(0xFFFFFFFF); /// Opaque black color. Used for texts against light backgrounds. + /// + /// See also: + /// + /// * [Colors.black], the same color, in the material design palette. static const Color black = const Color(0xFF000000); /// Used in iOS 10 for light background fills such as the chat bubble background. diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart new file mode 100644 index 0000000000000..fe9bd9b9ef215 --- /dev/null +++ b/packages/flutter/lib/src/cupertino/nav_bar.dart @@ -0,0 +1,148 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' show ImageFilter; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import 'colors.dart'; + +// Standard iOS 10 nav bar height without the status bar. +const double _kNavBarHeight = 44.0; + +const Color _kDefaultNavBarBackgroundColor = const Color(0xCCF8F8F8); +const Color _kDefaultNavBarBorderColor = const Color(0x4C000000); + +/// An iOS-styled navigation bar. +/// +/// The navigation bar is a toolbar that minimally consists of a widget, normally +/// a page title, in the [middle] of the toolbar. +/// +/// It also supports a [leading] and [trailing] widget before and after the +/// [middle] widget while keeping the [middle] widget centered. +/// +/// It should be placed at top of the screen and automatically accounts for +/// the OS's status bar. +/// +/// If the given [backgroundColor]'s opacity is not 1.0 (which is the case by +/// default), it will produce a blurring effect to the content behind it. +// +// TODO(xster): document automatic addition of a CupertinoBackButton. +// TODO(xster): add sample code using icons. +// TODO(xster): document integration into a CupertinoScaffold. +class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWidget { + /// Creates a navigation bar in the iOS style. + const CupertinoNavigationBar({ + Key key, + this.leading, + @required this.middle, + this.trailing, + this.backgroundColor: _kDefaultNavBarBackgroundColor, + this.actionsForegroundColor: CupertinoColors.activeBlue, + }) : assert(middle != null, 'There must be a middle widget, usually a title'), + super(key: key); + + /// Widget to place at the start of the nav bar. Normally a back button + /// for a normal page or a cancel button for full page dialogs. + final Widget leading; + + /// Widget to place in the middle of the nav bar. Normally a title or + /// a segmented control. + final Widget middle; + + /// Widget to place at the end of the nav bar. Normally additional actions + /// taken on the page such as a search or edit function. + final Widget trailing; + + // TODO(xster): implement support for double row nav bars. + + /// The background color of the nav bar. If it contains transparency, the + /// tab bar will automatically produce a blurring effect to the content + /// behind it. + final Color backgroundColor; + + /// Default color used for text and icons of the [leading] and [trailing] + /// widgets in the nav bar. + /// + /// The [title] remains black if it's a text as per iOS standard design. + final Color actionsForegroundColor; + + /// True if the nav bar's background color has no transparency. + bool get opaque => backgroundColor.alpha == 0xFF; + + @override + Size get preferredSize => const Size.fromHeight(_kNavBarHeight); + + @override + Widget build(BuildContext context) { + Widget styledMiddle = middle; + if (styledMiddle.runtimeType == Text || styledMiddle.runtimeType == DefaultTextStyle) { + // Let the middle be black rather than `actionsForegroundColor` in case + // it's a plain text title. + styledMiddle = DefaultTextStyle.merge( + style: const TextStyle(color: CupertinoColors.black), + child: middle, + ); + } + + // TODO(xster): automatically build a CupertinoBackButton. + + Widget result = new DecoratedBox( + decoration: new BoxDecoration( + border: const Border( + bottom: const BorderSide( + color: _kDefaultNavBarBorderColor, + width: 0.0, // One physical pixel. + style: BorderStyle.solid, + ), + ), + color: backgroundColor, + ), + child: new SizedBox( + height: _kNavBarHeight + MediaQuery.of(context).padding.top, + child: IconTheme.merge( + data: new IconThemeData( + color: actionsForegroundColor, + size: 22.0, + ), + child: DefaultTextStyle.merge( + style: new TextStyle( + fontSize: 17.0, + letterSpacing: -0.24, + color: actionsForegroundColor, + ), + child: new Padding( + padding: new EdgeInsets.only( + top: MediaQuery.of(context).padding.top, + // TODO(xster): dynamically reduce padding when an automatic + // CupertinoBackButton is present. + left: 16.0, + right: 16.0, + ), + child: new NavigationToolbar( + leading: leading, + middle: styledMiddle, + trailing: trailing, + centerMiddle: true, + ), + ), + ), + ), + ), + ); + + if (!opaque) { + // For non-opaque backgrounds, apply a blur effect. + result = new ClipRect( + child: new BackdropFilter( + filter: new ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: result, + ), + ); + } + + return result; + } +} diff --git a/packages/flutter/lib/src/cupertino/page.dart b/packages/flutter/lib/src/cupertino/page.dart index 59c27d347a641..b2aa609bb0db3 100644 --- a/packages/flutter/lib/src/cupertino/page.dart +++ b/packages/flutter/lib/src/cupertino/page.dart @@ -25,7 +25,7 @@ final FractionalOffsetTween _kBottomUpTween = new FractionalOffsetTween( end: FractionalOffset.topLeft, ); -// Custom decoration from no shadow to page shadow mimicking iOS page +// Custom decoration from no shadow to page shadow mimicking iOS page // transitions using gradients. final DecorationTween _kGradientShadowTween = new DecorationTween( begin: _CupertinoEdgeShadowDecoration.none, // No decoration initially. @@ -36,105 +36,160 @@ final DecorationTween _kGradientShadowTween = new DecorationTween( end: FractionalOffset.topRight, // Eyeballed gradient used to mimic a drop shadow on the left side only. colors: const [ - const Color(0x00000000), + const Color(0x00000000), const Color(0x04000000), const Color(0x12000000), const Color(0x38000000) ], stops: const [0.0, 0.3, 0.6, 1.0], - ), + ), ), ); -/// A custom [Decoration] used to paint an extra shadow on the left edge of the -/// box it's decorating. It's like a [BoxDecoration] with only a gradient except -/// it paints to the left of the box instead of behind the box. -class _CupertinoEdgeShadowDecoration extends Decoration { - const _CupertinoEdgeShadowDecoration({ this.edgeGradient }); - - /// A Decoration with no decorating properties. - static const _CupertinoEdgeShadowDecoration none = - const _CupertinoEdgeShadowDecoration(); +/// A modal route that replaces the entire screen with an iOS transition. +/// +/// The page slides in from the right and exits in reverse. +/// The page also shifts to the left in parallax when another page enters to cover it. +/// +/// The page slides in from the bottom and exits in reverse with no parallax effect +/// for fullscreen dialogs. +/// +/// By default, when a modal route is replaced by another, the previous route +/// remains in memory. To free all the resources when this is not necessary, set +/// [maintainState] to false. +/// +/// See also: +/// +/// * [MaterialPageRoute] for an adaptive [PageRoute] that uses a platform appropriate transition. +class CupertinoPageRoute extends PageRoute { + /// Creates a page route for use in an iOS designed app. + CupertinoPageRoute({ + @required this.builder, + RouteSettings settings: const RouteSettings(), + this.maintainState: true, + bool fullscreenDialog: false, + }) : assert(builder != null), + assert(opaque), + super(settings: settings, fullscreenDialog: fullscreenDialog); + + /// Builds the primary contents of the route. + final WidgetBuilder builder; - /// A gradient to draw to the left of the box being decorated. - /// FractionalOffsets are relative to the original box translated one box - /// width to the left. - final LinearGradient edgeGradient; + @override + final bool maintainState; - /// Linearly interpolate between two edge shadow decorations decorations. - /// - /// See also [Decoration.lerp]. - static _CupertinoEdgeShadowDecoration lerp( - _CupertinoEdgeShadowDecoration a, - _CupertinoEdgeShadowDecoration b, - double t - ) { - if (a == null && b == null) - return null; - return new _CupertinoEdgeShadowDecoration( - edgeGradient: LinearGradient.lerp(a?.edgeGradient, b?.edgeGradient, t), - ); - } + @override + Duration get transitionDuration => const Duration(milliseconds: 350); @override - _CupertinoEdgeShadowDecoration lerpFrom(Decoration a, double t) { - if (a is! _CupertinoEdgeShadowDecoration) - return _CupertinoEdgeShadowDecoration.lerp(null, this, t); - return _CupertinoEdgeShadowDecoration.lerp(a, this, t); - } + Color get barrierColor => null; @override - _CupertinoEdgeShadowDecoration lerpTo(Decoration b, double t) { - if (b is! _CupertinoEdgeShadowDecoration) - return _CupertinoEdgeShadowDecoration.lerp(this, null, t); - return _CupertinoEdgeShadowDecoration.lerp(this, b, t); + bool canTransitionFrom(TransitionRoute nextRoute) { + return nextRoute is CupertinoPageRoute; } - + @override - _CupertinoEdgeShadowPainter createBoxPainter([VoidCallback onChanged]) { - return new _CupertinoEdgeShadowPainter(this, onChanged); + bool canTransitionTo(TransitionRoute nextRoute) { + // Don't perform outgoing animation if the next route is a fullscreen dialog. + return nextRoute is CupertinoPageRoute && !nextRoute.fullscreenDialog; } @override - bool operator ==(dynamic other) { - if (identical(this, other)) - return true; - if (other.runtimeType != _CupertinoEdgeShadowDecoration) - return false; - final _CupertinoEdgeShadowDecoration typedOther = other; - return edgeGradient == typedOther.edgeGradient; + void dispose() { + _backGestureController?.dispose(); + // If the route is never installed (i.e. pushed into a Navigator) such as the + // case when [MaterialPageRoute] delegates transition building to [CupertinoPageRoute], + // don't dispose super. + if (overlayEntries.isNotEmpty) + super.dispose(); } + CupertinoBackGestureController _backGestureController; + + /// Support for dismissing this route with a horizontal swipe. + /// + /// Swiping will be disabled if the page is a fullscreen dialog or if + /// dismissals can be overriden because a [WillPopCallback] was + /// defined for the route. + /// + /// See also: + /// + /// * [hasScopedWillPopCallback], which is true if a `willPop` callback + /// is defined for this route. @override - int get hashCode { - return edgeGradient.hashCode; + NavigationGestureController startPopGesture() { + return startPopGestureForRoute(this); } -} -/// A [BoxPainter] used to draw the page transition shadow using gradients. -class _CupertinoEdgeShadowPainter extends BoxPainter { - _CupertinoEdgeShadowPainter( - @required this._decoration, - VoidCallback onChange - ) : assert(_decoration != null), - super(onChange); + /// Create a CupertinoBackGestureController using a specific PageRoute. + /// + /// Used when [MaterialPageRoute] delegates the back gesture to [CupertinoPageRoute] + /// since the [CupertinoPageRoute] is not actually inserted into the Navigator. + NavigationGestureController startPopGestureForRoute(PageRoute hostRoute) { + // If attempts to dismiss this route might be vetoed such as in a page + // with forms, then do not allow the user to dismiss the route with a swipe. + if (hostRoute.hasScopedWillPopCallback) + return null; + // Fullscreen dialogs aren't dismissable by back swipe. + if (fullscreenDialog) + return null; + if (hostRoute.controller.status != AnimationStatus.completed) + return null; + assert(_backGestureController == null); + _backGestureController = new CupertinoBackGestureController( + navigator: hostRoute.navigator, + controller: hostRoute.controller, + ); - final _CupertinoEdgeShadowDecoration _decoration; + Function handleBackGestureEnded; + handleBackGestureEnded = (AnimationStatus status) { + if (status == AnimationStatus.completed) { + _backGestureController?.dispose(); + _backGestureController = null; + hostRoute.controller.removeStatusListener(handleBackGestureEnded); + } + }; + + hostRoute.controller.addStatusListener(handleBackGestureEnded); + return _backGestureController; + } @override - void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { - final LinearGradient gradient = _decoration.edgeGradient; - if (gradient == null) - return; - // The drawable space for the gradient is a rect with the same size as - // its parent box one box width to the left of the box. - final Rect rect = - (offset & configuration.size).translate(-configuration.size.width, 0.0); - final Paint paint = new Paint() - ..shader = gradient.createShader(rect); + Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { + final Widget result = builder(context); + assert(() { + if (result == null) { + throw new FlutterError( + 'The builder for route "${settings.name}" returned null.\n' + 'Route builders must never return null.' + ); + } + return true; + }); + return result; + } - canvas.drawRect(rect, paint); + @override + Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { + if (fullscreenDialog) + return new CupertinoFullscreenDialogTransition( + animation: animation, + child: child, + ); + else + return new CupertinoPageTransition( + primaryRouteAnimation: animation, + secondaryRouteAnimation: secondaryAnimation, + child: child, + // In the middle of a back gesture drag, let the transition be linear to match finger + // motions. + linearTransition: _backGestureController != null, + ); } + + @override + String get debugLabel => '${super.debugLabel}(${settings.name})'; } /// Provides an iOS-style page transition animation. @@ -249,9 +304,8 @@ class CupertinoBackGestureController extends NavigationGestureController { CupertinoBackGestureController({ @required NavigatorState navigator, @required this.controller, - }) : super(navigator) { - assert(controller != null); - } + }) : assert(controller != null), + super(navigator); /// The animation controller that the route uses to drive its transition /// animation. @@ -304,3 +358,94 @@ class CupertinoBackGestureController extends NavigationGestureController { navigator.pop(); } } + +/// A custom [Decoration] used to paint an extra shadow on the left edge of the +/// box it's decorating. It's like a [BoxDecoration] with only a gradient except +/// it paints to the left of the box instead of behind the box. +class _CupertinoEdgeShadowDecoration extends Decoration { + const _CupertinoEdgeShadowDecoration({ this.edgeGradient }); + + /// A Decoration with no decorating properties. + static const _CupertinoEdgeShadowDecoration none = + const _CupertinoEdgeShadowDecoration(); + + /// A gradient to draw to the left of the box being decorated. + /// FractionalOffsets are relative to the original box translated one box + /// width to the left. + final LinearGradient edgeGradient; + + /// Linearly interpolate between two edge shadow decorations decorations. + /// + /// See also [Decoration.lerp]. + static _CupertinoEdgeShadowDecoration lerp( + _CupertinoEdgeShadowDecoration a, + _CupertinoEdgeShadowDecoration b, + double t + ) { + if (a == null && b == null) + return null; + return new _CupertinoEdgeShadowDecoration( + edgeGradient: LinearGradient.lerp(a?.edgeGradient, b?.edgeGradient, t), + ); + } + + @override + _CupertinoEdgeShadowDecoration lerpFrom(Decoration a, double t) { + if (a is! _CupertinoEdgeShadowDecoration) + return _CupertinoEdgeShadowDecoration.lerp(null, this, t); + return _CupertinoEdgeShadowDecoration.lerp(a, this, t); + } + + @override + _CupertinoEdgeShadowDecoration lerpTo(Decoration b, double t) { + if (b is! _CupertinoEdgeShadowDecoration) + return _CupertinoEdgeShadowDecoration.lerp(this, null, t); + return _CupertinoEdgeShadowDecoration.lerp(this, b, t); + } + + @override + _CupertinoEdgeShadowPainter createBoxPainter([VoidCallback onChanged]) { + return new _CupertinoEdgeShadowPainter(this, onChanged); + } + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (other.runtimeType != _CupertinoEdgeShadowDecoration) + return false; + final _CupertinoEdgeShadowDecoration typedOther = other; + return edgeGradient == typedOther.edgeGradient; + } + + @override + int get hashCode { + return edgeGradient.hashCode; + } +} + +/// A [BoxPainter] used to draw the page transition shadow using gradients. +class _CupertinoEdgeShadowPainter extends BoxPainter { + _CupertinoEdgeShadowPainter( + this._decoration, + VoidCallback onChange + ) : assert(_decoration != null), + super(onChange); + + final _CupertinoEdgeShadowDecoration _decoration; + + @override + void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { + final LinearGradient gradient = _decoration.edgeGradient; + if (gradient == null) + return; + // The drawable space for the gradient is a rect with the same size as + // its parent box one box width to the left of the box. + final Rect rect = + (offset & configuration.size).translate(-configuration.size.width, 0.0); + final Paint paint = new Paint() + ..shader = gradient.createShader(rect); + + canvas.drawRect(rect, paint); + } +} diff --git a/packages/flutter/lib/src/cupertino/scaffold.dart b/packages/flutter/lib/src/cupertino/scaffold.dart new file mode 100644 index 0000000000000..621ee019d215c --- /dev/null +++ b/packages/flutter/lib/src/cupertino/scaffold.dart @@ -0,0 +1,229 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'bottom_tab_bar.dart'; +import 'nav_bar.dart'; + +/// Implements a basic iOS application's layout and behavior structure. +/// +/// The scaffold lays out the navigation bar on top, the tab bar at the bottom +/// and tabbed or untabbed content between or behind the bars. +/// +/// For tabbed scaffolds, the tab's active item and the actively showing tab +/// in the content area are automatically connected. +// TODO(xster): describe navigator handlings. +// TODO(xster): add an example. +class CupertinoScaffold extends StatefulWidget { + /// Construct a [CupertinoScaffold] without tabs. + /// + /// The [tabBar] and [rootTabPageBuilder] fields are not used in a [CupertinoScaffold] + /// without tabs. + // TODO(xster): document that page transitions will happen behind the navigation + // bar. + const CupertinoScaffold({ + Key key, + this.navigationBar, + @required this.child, + }) : assert(child != null), + tabBar = null, + rootTabPageBuilder = null, + super(key: key); + + /// Construct a [CupertinoScaffold] with tabs. + /// + /// A [tabBar] and a [rootTabPageBuilder] are required. The [CupertinoScaffold] + /// will automatically listen to the provided [CupertinoTabBar]'s tap callbacks + /// to change the active tab. + /// + /// Tabs' contents are built with the provided [rootTabPageBuilder] at the active + /// tab index. [rootTabPageBuilder] must be able to build the same number of + /// pages as the [tabBar.items.length]. Inactive tabs will be moved [Offstage] + /// and its animations disabled. + /// + /// The [child] field is not used in a [CupertinoScaffold] with tabs. + const CupertinoScaffold.tabbed({ + Key key, + this.navigationBar, + @required this.tabBar, + @required this.rootTabPageBuilder, + }) : assert(tabBar != null), + assert(rootTabPageBuilder != null), + child = null, + super(key: key); + + /// The [navigationBar], typically a [CupertinoNavigationBar], is drawn at the + /// top of the screen. + /// + /// If translucent, the main content may slide behind it. + /// Otherwise, the main content's top margin will be offset by its height. + // TODO(xster): document its page transition animation when ready + final PreferredSizeWidget navigationBar; + + /// The [tabBar] is a [CupertinoTabBar] drawn at the bottom of the screen + /// that lets the user switch between different tabs in the main content area + /// when present. + /// + /// This parameter is required and must be non-null when the [new CupertinoScaffold.tabbed] + /// constructor is used. + /// + /// When provided, [CupertinoTabBar.currentIndex] will be ignored and will + /// be managed by the [CupertinoScaffold] to show the currently selected page + /// as the active item index. If [CupertinoTabBar.onTap] is provided, it will + /// still be called. [CupertinoScaffold] automatically also listen to the + /// [CupertinoTabBar]'s `onTap` to change the [CupertinoTabBar]'s `currentIndex` + /// and change the actively displayed tab in [CupertinoScaffold]'s own + /// main content area. + /// + /// If translucent, the main content may slide behind it. + /// Otherwise, the main content's bottom margin will be offset by its height. + final CupertinoTabBar tabBar; + + /// An [IndexedWidgetBuilder] that's called when tabs become active. + /// + /// Used when a tabbed scaffold is constructed via the [new CupertinoScaffold.tabbed] + /// constructor and must be non-null. + /// + /// When the tab becomes inactive, its content is still cached in the widget + /// tree [Offstage] and its animations disabled. + /// + /// Content can slide under the [navigationBar] or the [tabBar] when they're + /// translucent. + final IndexedWidgetBuilder rootTabPageBuilder; + + /// Widget to show in the main content area when the scaffold is used without + /// tabs. + /// + /// Used when the default [new CupertinoScaffold] constructor is used and must + /// be non-null. + /// + /// Content can slide under the [navigationBar] or the [tabBar] when they're + /// translucent. + final Widget child; + + @override + _CupertinoScaffoldState createState() => new _CupertinoScaffoldState(); +} + +class _CupertinoScaffoldState extends State { + int _currentPage = 0; + + /// Pad the given middle widget with or without top and bottom offsets depending + /// on whether the middle widget should slide behind translucent bars. + Widget _padMiddle(Widget middle) { + double topPadding = MediaQuery.of(context).padding.top; + if (widget.navigationBar is CupertinoNavigationBar) { + final CupertinoNavigationBar top = widget.navigationBar; + if (top.opaque) + topPadding += top.preferredSize.height; + } + + double bottomPadding = 0.0; + if (widget.tabBar?.opaque ?? false) + bottomPadding = widget.tabBar.preferredSize.height; + + return new Padding( + padding: new EdgeInsets.only(top: topPadding, bottom: bottomPadding), + child: middle, + ); + } + + @override + Widget build(BuildContext context) { + final List stacked = []; + + // The main content being at the bottom is added to the stack first. + if (widget.child != null) { + stacked.add(_padMiddle(widget.child)); + } else if (widget.rootTabPageBuilder != null) { + stacked.add(_padMiddle(new _TabView( + currentTabIndex: _currentPage, + tabNumber: widget.tabBar.items.length, + rootTabPageBuilder: widget.rootTabPageBuilder, + ))); + } + + if (widget.navigationBar != null) { + stacked.add(new Align( + alignment: FractionalOffset.topCenter, + child: widget.navigationBar, + )); + } + + if (widget.tabBar != null) { + stacked.add(new Align( + alignment: FractionalOffset.bottomCenter, + // Override the tab bar's currentIndex to the current tab and hook in + // our own listener to update the _currentPage on top of a possibly user + // provided callback. + child: widget.tabBar.copyWith( + currentIndex: _currentPage, + onTap: (int newIndex) { + setState(() { + _currentPage = newIndex; + }); + // Chain the user's original callback. + if (widget.tabBar.onTap != null) + widget.tabBar.onTap(newIndex); + } + ), + )); + } + + return new Stack( + children: stacked, + ); + } +} + +/// An widget laying out multiple tabs with only one active tab being built +/// at a time and on stage. Off stage tabs' animations are stopped. +class _TabView extends StatefulWidget { + _TabView({ + @required this.currentTabIndex, + @required this.tabNumber, + @required this.rootTabPageBuilder, + }) : assert(currentTabIndex != null), + assert(tabNumber != null && tabNumber > 0), + assert(rootTabPageBuilder != null); + + final int currentTabIndex; + final int tabNumber; + final IndexedWidgetBuilder rootTabPageBuilder; + + @override + _TabViewState createState() => new _TabViewState(); +} + +class _TabViewState extends State<_TabView> { + List tabs; + + @override + void initState() { + super.initState(); + tabs = new List(widget.tabNumber); + } + + @override + Widget build(BuildContext context) { + return new Stack( + children: new List.generate(widget.tabNumber, (int index) { + final bool active = index == widget.currentTabIndex; + + // TODO(xster): lazily replace empty tabs with Navigators instead. + if (active || tabs[index] != null) + tabs[index] = widget.rootTabPageBuilder(context, index); + + return new Offstage( + offstage: !active, + child: new TickerMode( + enabled: active, + child: tabs[index] ?? new Container(), + ), + ); + }), + ); + } +} diff --git a/packages/flutter/lib/src/cupertino/slider.dart b/packages/flutter/lib/src/cupertino/slider.dart index e23ea3ab33075..8ed550ad23b19 100644 --- a/packages/flutter/lib/src/cupertino/slider.dart +++ b/packages/flutter/lib/src/cupertino/slider.dart @@ -192,11 +192,11 @@ class _RenderCupertinoSlider extends RenderConstrainedBox implements SemanticsAc Color activeColor, this.onChanged, TickerProvider vsync, - }) : _value = value, + }) : assert(value != null && value >= 0.0 && value <= 1.0), + _value = value, _divisions = divisions, _activeColor = activeColor, super(additionalConstraints: const BoxConstraints.tightFor(width: _kSliderWidth, height: _kSliderHeight)) { - assert(value != null && value >= 0.0 && value <= 1.0); _drag = new HorizontalDragGestureRecognizer() ..onStart = _handleDragStart ..onUpdate = _handleDragUpdate diff --git a/packages/flutter/lib/src/cupertino/switch.dart b/packages/flutter/lib/src/cupertino/switch.dart index 28d1da9339f8f..9d5501e7f3e96 100644 --- a/packages/flutter/lib/src/cupertino/switch.dart +++ b/packages/flutter/lib/src/cupertino/switch.dart @@ -22,8 +22,28 @@ import 'thumb_painter.dart'; /// that use a switch will listen for the [onChanged] callback and rebuild the /// switch with a new [value] to update the visual appearance of the switch. /// +/// ## Sample code +/// +/// This sample shows how to use a [CupertinoSwitch] in a [ListTile]. The +/// [MergeSemantics] is used to turn the entire [ListTile] into a single item +/// for accessibility tools. +/// +/// ```dart +/// new MergeSemantics( +/// child: new ListTile( +/// title: new Text('Lights'), +/// trailing: new CupertinoSwitch( +/// value: _lights, +/// onChanged: (bool value) { setState(() { _lights = value; }); }, +/// ), +/// onTap: () { setState(() { _lights = !_lights; }); }, +/// ), +/// ) +/// ``` +/// /// See also: /// +/// * [Switch], the material design equivalent. /// * class CupertinoSwitch extends StatefulWidget { /// Creates an iOS-style switch. @@ -141,14 +161,14 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsAc @required Color activeColor, ValueChanged onChanged, @required TickerProvider vsync, - }) : _value = value, - _activeColor = activeColor, - _onChanged = onChanged, - _vsync = vsync, - super(additionalConstraints: const BoxConstraints.tightFor(width: _kSwitchWidth, height: _kSwitchHeight)) { - assert(value != null); - assert(activeColor != null); - assert(vsync != null); + }) : assert(value != null), + assert(activeColor != null), + assert(vsync != null), + _value = value, + _activeColor = activeColor, + _onChanged = onChanged, + _vsync = vsync, + super(additionalConstraints: const BoxConstraints.tightFor(width: _kSwitchWidth, height: _kSwitchHeight)) { _tap = new TapGestureRecognizer() ..onTapDown = _handleTapDown ..onTap = _handleTap diff --git a/packages/flutter/lib/src/foundation/basic_types.dart b/packages/flutter/lib/src/foundation/basic_types.dart index 5d63bf244f67c..7e0fe50260af6 100644 --- a/packages/flutter/lib/src/foundation/basic_types.dart +++ b/packages/flutter/lib/src/foundation/basic_types.dart @@ -79,9 +79,9 @@ class BitField { /// Creates a bit field of all zeros. /// /// The given length must be at most 62. - BitField(this._length) : _bits = _kAllZeros { - assert(_length <= _kSMIBits); - } + BitField(this._length) + : assert(_length <= _kSMIBits), + _bits = _kAllZeros; /// Creates a bit field filled with a particular value. /// @@ -89,9 +89,10 @@ class BitField { /// the bits are filled with zeros. /// /// The given length must be at most 62. - BitField.filled(this._length, bool value) : _bits = value ? _kAllOnes : _kAllZeros { - assert(_length <= _kSMIBits); - } + BitField.filled(this._length, bool value) + : assert(_length <= _kSMIBits), + _bits = value ? _kAllOnes : _kAllZeros; + final int _length; int _bits; diff --git a/packages/flutter/lib/src/foundation/change_notifier.dart b/packages/flutter/lib/src/foundation/change_notifier.dart index 953296c11497f..06dd9ea97d4af 100644 --- a/packages/flutter/lib/src/foundation/change_notifier.dart +++ b/packages/flutter/lib/src/foundation/change_notifier.dart @@ -20,7 +20,7 @@ abstract class Listenable { /// The list must not be changed after this method has been called. Doing so /// will lead to memory leaks or exceptions. /// - /// The list may contain `null`s; they are ignored. + /// The list may contain nulls; they are ignored. factory Listenable.merge(List listenables) = _MergingListenable; /// Register a closure to be called when the object notifies its listeners. diff --git a/packages/flutter/lib/src/foundation/serialization.dart b/packages/flutter/lib/src/foundation/serialization.dart index 753de146e704d..822d916aa0184 100644 --- a/packages/flutter/lib/src/foundation/serialization.dart +++ b/packages/flutter/lib/src/foundation/serialization.dart @@ -103,9 +103,8 @@ class WriteBuffer { /// The byte order used is [Endianness.HOST_ENDIAN] throughout. class ReadBuffer { /// Creates a [ReadBuffer] for reading from the specified [data]. - ReadBuffer(this.data) { - assert(data != null); - } + ReadBuffer(this.data) + : assert(data != null); /// The underlying data being read. final ByteData data; diff --git a/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart b/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart index d1054277c3c37..3c634144d308e 100644 --- a/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart +++ b/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart @@ -6,6 +6,10 @@ import 'package:meta/meta.dart'; /// A mixin that helps dump string representations of trees. abstract class TreeDiagnosticsMixin { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory TreeDiagnosticsMixin._() => null; + @override String toString() => '$runtimeType#$hashCode'; diff --git a/packages/flutter/lib/src/gestures/binding.dart b/packages/flutter/lib/src/gestures/binding.dart index 849b999487eed..09945d7d3adc8 100644 --- a/packages/flutter/lib/src/gestures/binding.dart +++ b/packages/flutter/lib/src/gestures/binding.dart @@ -15,7 +15,10 @@ import 'hit_test.dart'; import 'pointer_router.dart'; /// A binding for the gesture subsystem. -abstract class GestureBinding extends BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget { +abstract class GestureBinding extends BindingBase with HitTestable, HitTestDispatcher, HitTestTarget { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory GestureBinding._() => null; @override void initInstances() { diff --git a/packages/flutter/lib/src/gestures/drag_details.dart b/packages/flutter/lib/src/gestures/drag_details.dart index 9cdfdb0131be6..7cd2ef2ce36f1 100644 --- a/packages/flutter/lib/src/gestures/drag_details.dart +++ b/packages/flutter/lib/src/gestures/drag_details.dart @@ -20,9 +20,8 @@ class DragDownDetails { /// Creates details for a [GestureDragDownCallback]. /// /// The [globalPosition] argument must not be null. - DragDownDetails({ this.globalPosition: Offset.zero }) { - assert(globalPosition != null); - } + DragDownDetails({ this.globalPosition: Offset.zero }) + : assert(globalPosition != null); /// The global position at which the pointer contacted the screen. /// @@ -53,9 +52,8 @@ class DragStartDetails { /// Creates details for a [GestureDragStartCallback]. /// /// The [globalPosition] argument must not be null. - DragStartDetails({ this.globalPosition: Offset.zero }) { - assert(globalPosition != null); - } + DragStartDetails({ this.globalPosition: Offset.zero }) + : assert(globalPosition != null); /// The global position at which the pointer contacted the screen. /// @@ -99,12 +97,10 @@ class DragUpdateDetails { this.delta: Offset.zero, this.primaryDelta, @required this.globalPosition - }) { - assert(delta != null); - assert(primaryDelta == null - || (primaryDelta == delta.dx && delta.dy == 0.0) - || (primaryDelta == delta.dy && delta.dx == 0.0)); - } + }) : assert(delta != null), + assert(primaryDelta == null + || (primaryDelta == delta.dx && delta.dy == 0.0) + || (primaryDelta == delta.dy && delta.dx == 0.0)); /// The amount the pointer has moved since the previous update. /// @@ -158,12 +154,10 @@ class DragEndDetails { DragEndDetails({ this.velocity: Velocity.zero, this.primaryVelocity, - }) { - assert(velocity != null); - assert(primaryVelocity == null - || primaryVelocity == velocity.pixelsPerSecond.dx - || primaryVelocity == velocity.pixelsPerSecond.dy); - } + }) : assert(velocity != null), + assert(primaryVelocity == null + || primaryVelocity == velocity.pixelsPerSecond.dx + || primaryVelocity == velocity.pixelsPerSecond.dy); /// The velocity the pointer was moving when it stopped contacting the screen. /// diff --git a/packages/flutter/lib/src/gestures/hit_test.dart b/packages/flutter/lib/src/gestures/hit_test.dart index 4cbd64bdaaa5d..1c380bef1ffb6 100644 --- a/packages/flutter/lib/src/gestures/hit_test.dart +++ b/packages/flutter/lib/src/gestures/hit_test.dart @@ -6,6 +6,10 @@ import 'events.dart'; /// An object that can hit-test pointers. abstract class HitTestable { // ignore: one_member_abstracts + // This class is intended to be used as an interface with the implements + // keyword, and should not be extended directly. + factory HitTestable._() => null; + /// Check whether the given position hits this object. /// /// If this given position hits this object, consider adding a [HitTestEntry] @@ -15,12 +19,20 @@ abstract class HitTestable { // ignore: one_member_abstracts /// An object that can dispatch events. abstract class HitTestDispatcher { // ignore: one_member_abstracts + // This class is intended to be used as an interface with the implements + // keyword, and should not be extended directly. + factory HitTestDispatcher._() => null; + /// Override this method to dispatch events. void dispatchEvent(PointerEvent event, HitTestResult result); } /// An object that can handle events. abstract class HitTestTarget { // ignore: one_member_abstracts + // This class is intended to be used as an interface with the implements + // keyword, and should not be extended directly. + factory HitTestTarget._() => null; + /// Override this method to receive events. void handleEvent(PointerEvent event, HitTestEntry entry); } diff --git a/packages/flutter/lib/src/gestures/lsq_solver.dart b/packages/flutter/lib/src/gestures/lsq_solver.dart index 71ffe7af77196..904263b4397db 100644 --- a/packages/flutter/lib/src/gestures/lsq_solver.dart +++ b/packages/flutter/lib/src/gestures/lsq_solver.dart @@ -75,10 +75,9 @@ class LeastSquaresSolver { /// Creates a least-squares solver. /// /// The [x], [y], and [w] arguments must not be null. - LeastSquaresSolver(this.x, this.y, this.w) { - assert(x.length == y.length); - assert(y.length == w.length); - } + LeastSquaresSolver(this.x, this.y, this.w) + : assert(x.length == y.length), + assert(y.length == w.length); /// The x-coordinates of each data point. final List x; diff --git a/packages/flutter/lib/src/gestures/multidrag.dart b/packages/flutter/lib/src/gestures/multidrag.dart index d40b81e673356..1bab27bfeb57f 100644 --- a/packages/flutter/lib/src/gestures/multidrag.dart +++ b/packages/flutter/lib/src/gestures/multidrag.dart @@ -27,9 +27,8 @@ abstract class MultiDragPointerState { /// Creates per-pointer state for a [MultiDragGestureRecognizer]. /// /// The [initialPosition] argument must not be null. - MultiDragPointerState(this.initialPosition) { - assert(initialPosition != null); - } + MultiDragPointerState(this.initialPosition) + : assert(initialPosition != null); /// The global coordinates of the pointer when the pointer contacted the screen. final Offset initialPosition; @@ -93,7 +92,7 @@ abstract class MultiDragPointerState { /// Called when the gesture was rejected. /// - /// [dispose()] will be called immediately following this. + /// The [dispose] method will be called immediately following this. @protected @mustCallSuper void rejected() { @@ -396,8 +395,9 @@ class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Ver } class _DelayedPointerState extends MultiDragPointerState { - _DelayedPointerState(Offset initialPosition, Duration delay) : super(initialPosition) { - assert(delay != null); + _DelayedPointerState(Offset initialPosition, Duration delay) + : assert(delay != null), + super(initialPosition) { _timer = new Timer(delay, _delayPassed); } @@ -481,9 +481,7 @@ class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Dela /// can be changed for specific behaviors. DelayedMultiDragGestureRecognizer({ this.delay: kLongPressTimeout - }) { - assert(delay != null); - } + }) : assert(delay != null); /// The amount of time the pointer must remain in the same place for the drag /// to be recognized. diff --git a/packages/flutter/lib/src/gestures/recognizer.dart b/packages/flutter/lib/src/gestures/recognizer.dart index 93cb880aef4b0..c9107e0bda019 100644 --- a/packages/flutter/lib/src/gestures/recognizer.dart +++ b/packages/flutter/lib/src/gestures/recognizer.dart @@ -147,7 +147,9 @@ abstract class OneSequenceGestureRecognizer extends GestureRecognizer { /// is shortly after creating the recognizer. GestureArenaTeam get team => _team; GestureArenaTeam _team; + /// The [team] can only be set once. set team(GestureArenaTeam value) { + assert(value != null); assert(_entries.isEmpty); assert(_trackedPointers.isEmpty); assert(_team == null); diff --git a/packages/flutter/lib/src/gestures/scale.dart b/packages/flutter/lib/src/gestures/scale.dart index 2c459a0647c30..5540cd11e4842 100644 --- a/packages/flutter/lib/src/gestures/scale.dart +++ b/packages/flutter/lib/src/gestures/scale.dart @@ -32,9 +32,8 @@ class ScaleStartDetails { /// Creates details for [GestureScaleStartCallback]. /// /// The [focalPoint] argument must not be null. - ScaleStartDetails({ this.focalPoint: Offset.zero }) { - assert(focalPoint != null); - } + ScaleStartDetails({ this.focalPoint: Offset.zero }) + : assert(focalPoint != null); /// The initial focal point of the pointers in contact with the screen. /// Reported in global coordinates. @@ -50,10 +49,11 @@ class ScaleUpdateDetails { /// /// The [focalPoint] and [scale] arguments must not be null. The [scale] /// argument must be greater than or equal to zero. - ScaleUpdateDetails({ this.focalPoint: Offset.zero, this.scale: 1.0 }) { - assert(focalPoint != null); - assert(scale != null && scale >= 0.0); - } + ScaleUpdateDetails({ + this.focalPoint: Offset.zero, + this.scale: 1.0, + }) : assert(focalPoint != null), + assert(scale != null && scale >= 0.0); /// The focal point of the pointers in contact with the screen. Reported in /// global coordinates. @@ -72,9 +72,8 @@ class ScaleEndDetails { /// Creates details for [GestureScaleEndCallback]. /// /// The [velocity] argument must not be null. - ScaleEndDetails({ this.velocity: Velocity.zero }) { - assert(velocity != null); - } + ScaleEndDetails({ this.velocity: Velocity.zero }) + : assert(velocity != null); /// The velocity of the last pointer to be lifted off of the screen. final Velocity velocity; diff --git a/packages/flutter/lib/src/gestures/tap.dart b/packages/flutter/lib/src/gestures/tap.dart index 97fbf79f52a3a..bbe170ae39b4f 100644 --- a/packages/flutter/lib/src/gestures/tap.dart +++ b/packages/flutter/lib/src/gestures/tap.dart @@ -12,9 +12,8 @@ class TapDownDetails { /// Creates details for a [GestureTapDownCallback]. /// /// The [globalPosition] argument must not be null. - TapDownDetails({ this.globalPosition: Offset.zero }) { - assert(globalPosition != null); - } + TapDownDetails({ this.globalPosition: Offset.zero }) + : assert(globalPosition != null); /// The global position at which the pointer contacted the screen. final Offset globalPosition; @@ -32,9 +31,8 @@ class TapUpDetails { /// Creates details for a [GestureTapUpCallback]. /// /// The [globalPosition] argument must not be null. - TapUpDetails({ this.globalPosition: Offset.zero }) { - assert(globalPosition != null); - } + TapUpDetails({ this.globalPosition: Offset.zero }) + : assert(globalPosition != null); /// The global position at which the pointer contacted the screen. final Offset globalPosition; diff --git a/packages/flutter/lib/src/gestures/team.dart b/packages/flutter/lib/src/gestures/team.dart index fbef0b85b1be3..b23f3e743ee22 100644 --- a/packages/flutter/lib/src/gestures/team.dart +++ b/packages/flutter/lib/src/gestures/team.dart @@ -80,15 +80,33 @@ class _CombiningGestureArenaMember extends GestureArenaMember { } } -/// A group of [GestureArenaMember] objects that are competing as a unit in the [GestureArenaManager]. +/// A group of [GestureArenaMember] objects that are competing as a unit in the +/// [GestureArenaManager]. /// /// Normally, a recognizer competes directly in the [GestureArenaManager] to /// recognize a sequence of pointer events as a gesture. With a /// [GestureArenaTeam], recognizers can compete in the arena in a group with /// other recognizers. /// -/// To assign a gesture recognizer to a team, see -/// [OneSequenceGestureRecognizer.team]. +/// When gesture recognizers are in a team together, then once there are no +/// other competing gestures in the arena, the first gesture to have been added +/// to the team automatically wins, instead of the gestures continuing to +/// compete against each other. +/// +/// For example, [Slider] uses this to support both a +/// [HorizontalDragGestureRecognizer] and a [TapGestureRecognizer], but without +/// the drag recognizer having to wait until the user has dragged outside the +/// slop region of the tap gesture before triggering. Since they compete as a +/// team, as soon as any other recognizers are out of the arena, the drag +/// recognizer wins, even if the user has not actually dragged yet. On the other +/// hand, if the tap can win outright, before the other recognizers are taken +/// out of the arena (e.g. if the slider is in a vertical scrolling list and the +/// user places their finger on the touch surface then lifts it, so that neither +/// the horizontal nor vertical drag recognizers can claim victory) the tap +/// recognizer still actually wins, despite being in the team. +/// +/// To assign a gesture recognizer to a team, set +/// [OneSequenceGestureRecognizer.team] to an instance of [GestureArenaTeam]. class GestureArenaTeam { final Map _combiners = {}; diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart index 17b34c1697894..a860860d86a23 100644 --- a/packages/flutter/lib/src/material/app.dart +++ b/packages/flutter/lib/src/material/app.dart @@ -42,8 +42,9 @@ class MaterialApp extends StatefulWidget { /// Creates a MaterialApp. /// /// At least one of [home], [routes], or [onGenerateRoute] must be - /// given. If only [routes] is given, it must include an entry for - /// the [Navigator.defaultRouteName] (`'/'`). + /// given. If only [routes] is given, it must include an entry for the + /// [initialRoute], which defaults to [Navigator.defaultRouteName] + /// (`'/'`). /// /// This class creates an instance of [WidgetsApp]. MaterialApp({ @@ -53,7 +54,7 @@ class MaterialApp extends StatefulWidget { this.theme, this.home, this.routes: const {}, - this.initialRoute, + this.initialRoute: Navigator.defaultRouteName, this.onGenerateRoute, this.onLocaleChanged, this.navigatorObservers: const [], @@ -63,12 +64,11 @@ class MaterialApp extends StatefulWidget { this.checkerboardOffscreenLayers: false, this.showSemanticsDebugger: false, this.debugShowCheckedModeBanner: true - }) : super(key: key) { - assert(debugShowMaterialGrid != null); - assert(routes != null); - assert(!routes.containsKey(Navigator.defaultRouteName) || (home == null)); - assert(routes.containsKey(Navigator.defaultRouteName) || (home != null) || (onGenerateRoute != null)); - } + }) : assert(debugShowMaterialGrid != null), + assert(routes != null), + assert(!routes.containsKey(initialRoute) || (home == null)), + assert(routes.containsKey(initialRoute) || (home != null) || (onGenerateRoute != null)), + super(key: key); /// A one-line description of this app for use in the window manager. final String title; diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index 7c7ab7b8a5062..aecefbda92a09 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -21,70 +21,12 @@ import 'tabs.dart'; import 'theme.dart'; import 'typography.dart'; -enum _ToolbarSlot { - leading, - title, - actions, -} - -class _ToolbarLayout extends MultiChildLayoutDelegate { - _ToolbarLayout({ this.centerTitle }); - - // If false the title should be left or right justified within the space bewteen - // the leading and actions widgets, depending on the locale's writing direction. - // If true the title is centered within the toolbar (not within the horizontal - // space bewteen the leading and actions widgets). - final bool centerTitle; - - static const double kLeadingWidth = 56.0; // So it's square with kToolbarHeight. - static const double kTitleLeftWithLeading = 72.0; // As per https://material.io/guidelines/layout/metrics-keylines.html#metrics-keylines-keylines-spacing. - static const double kTitleLeftWithoutLeading = 16.0; - - @override - void performLayout(Size size) { - double actionsWidth = 0.0; - - if (hasChild(_ToolbarSlot.leading)) { - final BoxConstraints constraints = new BoxConstraints.tight(new Size(kLeadingWidth, size.height)); - layoutChild(_ToolbarSlot.leading, constraints); - positionChild(_ToolbarSlot.leading, Offset.zero); - } - - if (hasChild(_ToolbarSlot.actions)) { - final BoxConstraints constraints = new BoxConstraints.loose(size); - final Size actionsSize = layoutChild(_ToolbarSlot.actions, constraints); - final double actionsLeft = size.width - actionsSize.width; - final double actionsTop = (size.height - actionsSize.height) / 2.0; - actionsWidth = actionsSize.width; - positionChild(_ToolbarSlot.actions, new Offset(actionsLeft, actionsTop)); - } - - if (hasChild(_ToolbarSlot.title)) { - final double titleLeftMargin = - hasChild(_ToolbarSlot.leading) ? kTitleLeftWithLeading : kTitleLeftWithoutLeading; - final double maxWidth = math.max(size.width - titleLeftMargin - actionsWidth, 0.0); - final BoxConstraints constraints = new BoxConstraints.loose(size).copyWith(maxWidth: maxWidth); - final Size titleSize = layoutChild(_ToolbarSlot.title, constraints); - final double titleY = (size.height - titleSize.height) / 2.0; - double titleX = titleLeftMargin; - - // If the centered title will not fit between the leading and actions - // widgets, then align its left or right edge with the adjacent boundary. - if (centerTitle) { - titleX = (size.width - titleSize.width) / 2.0; - if (titleX + titleSize.width > size.width - actionsWidth) - titleX = size.width - actionsWidth - titleSize.width; - else if (titleX < titleLeftMargin) - titleX = titleLeftMargin; - } - - positionChild(_ToolbarSlot.title, new Offset(titleX, titleY)); - } - } +// Examples can assume: +// void _airDress() { } +// void _restitchDress() { } +// void _repairDress() { } - @override - bool shouldRelayout(_ToolbarLayout oldDelegate) => centerTitle != oldDelegate.centerTitle; -} +const double _kLeadingWidth = kToolbarHeight; // So the leading button is square. // Bottom justify the kToolbarHeight child which may overflow the top. class _ToolbarContainerLayout extends SingleChildLayoutDelegate { @@ -201,13 +143,12 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { this.centerTitle, this.toolbarOpacity: 1.0, this.bottomOpacity: 1.0, - }) : preferredSize = new Size.fromHeight(kToolbarHeight + (bottom?.preferredSize?.height ?? 0.0)), - super(key: key) { - assert(elevation != null); - assert(primary != null); - assert(toolbarOpacity != null); - assert(bottomOpacity != null); - } + }) : assert(elevation != null), + assert(primary != null), + assert(toolbarOpacity != null), + assert(bottomOpacity != null), + preferredSize = new Size.fromHeight(kToolbarHeight + (bottom?.preferredSize?.height ?? 0.0)), + super(key: key); /// A widget to display before the [title]. /// @@ -390,7 +331,6 @@ class _AppBarState extends State { ); } - final List toolbarChildren = []; Widget leading = widget.leading; if (leading == null) { if (hasDrawer) { @@ -405,47 +345,38 @@ class _AppBarState extends State { } } if (leading != null) { - toolbarChildren.add( - new LayoutId( - id: _ToolbarSlot.leading, - child: leading - ) + leading = new ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: _kLeadingWidth), + child: leading, ); } - if (widget.title != null) { - toolbarChildren.add( - new LayoutId( - id: _ToolbarSlot.title, - child: new DefaultTextStyle( - style: centerStyle, - softWrap: false, - overflow: TextOverflow.ellipsis, - child: widget.title, - ), - ), + Widget title = widget.title; + if (title != null) { + title = new DefaultTextStyle( + style: centerStyle, + softWrap: false, + overflow: TextOverflow.ellipsis, + child: title, ); } + + Widget actions; if (widget.actions != null && widget.actions.isNotEmpty) { - toolbarChildren.add( - new LayoutId( - id: _ToolbarSlot.actions, - child: new Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: widget.actions, - ), - ), + actions = new Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: widget.actions, ); } final Widget toolbar = new Padding( padding: const EdgeInsets.only(right: 4.0), - child: new CustomMultiChildLayout( - delegate: new _ToolbarLayout( - centerTitle: widget._getEffectiveCenterTitle(themeData), - ), - children: toolbarChildren, + child: new NavigationToolbar( + leading: leading, + middle: title, + trailing: actions, + centerMiddle: widget._getEffectiveCenterTitle(themeData), ), ); @@ -586,9 +517,8 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { @required this.floating, @required this.pinned, @required this.snapConfiguration, - }) : _bottomHeight = bottom?.preferredSize?.height ?? 0.0 { - assert(primary || topPadding == 0.0); - } + }) : assert(primary || topPadding == 0.0), + _bottomHeight = bottom?.preferredSize?.height ?? 0.0; final Widget leading; final Widget title; diff --git a/packages/flutter/lib/src/material/back_button.dart b/packages/flutter/lib/src/material/back_button.dart index d6326a1d0fd86..671bfeeb8721f 100644 --- a/packages/flutter/lib/src/material/back_button.dart +++ b/packages/flutter/lib/src/material/back_button.dart @@ -10,6 +10,8 @@ import 'theme.dart'; /// A "back" icon that's appropriate for the current [TargetPlatform]. /// +/// The current platform is determined by querying for the ambient [Theme]. +/// /// See also: /// /// * [BackButton], an [IconButton] with a [BackButtonIcon] that calls @@ -17,7 +19,10 @@ import 'theme.dart'; /// * [IconButton], which is a more general widget for creating buttons /// with icons. /// * [Icon], a material design icon. +/// * [ThemeData.platform], which specifies the current platform. class BackButtonIcon extends StatelessWidget { + /// Creates an icon that shows the appropriate "back" image for + /// the current platform (as obtained from the [Theme]). const BackButtonIcon({ Key key }) : super(key: key); /// Returns tha appropriate "back" icon for the given `platform`. diff --git a/packages/flutter/lib/src/material/bottom_navigation_bar.dart b/packages/flutter/lib/src/material/bottom_navigation_bar.dart index 5fba48526818e..011848688fcc8 100644 --- a/packages/flutter/lib/src/material/bottom_navigation_bar.dart +++ b/packages/flutter/lib/src/material/bottom_navigation_bar.dart @@ -69,14 +69,13 @@ class BottomNavigationBar extends StatefulWidget { this.type: BottomNavigationBarType.fixed, this.fixedColor, this.iconSize: 24.0, - }) : super(key: key) { - assert(items != null); - assert(items.length >= 2); - assert(0 <= currentIndex && currentIndex < items.length); - assert(type != null); - assert(type == BottomNavigationBarType.fixed || fixedColor == null); - assert(iconSize != null); - } + }) : assert(items != null), + assert(items.length >= 2), + assert(0 <= currentIndex && currentIndex < items.length), + assert(type != null), + assert(type == BottomNavigationBarType.fixed || fixedColor == null), + assert(iconSize != null), + super(key: key); /// The interactive items laid out within the bottom navigation bar. final List items; @@ -450,11 +449,9 @@ class _Circle { @required this.index, @required this.color, @required TickerProvider vsync, - }) { - assert(state != null); - assert(index != null); - assert(color != null); - + }) : assert(state != null), + assert(index != null), + assert(color != null) { controller = new AnimationController( duration: kThemeAnimationDuration, vsync: vsync, diff --git a/packages/flutter/lib/src/material/checkbox_list_tile.dart b/packages/flutter/lib/src/material/checkbox_list_tile.dart index 02b217b7e0476..25ec516c4d2d2 100644 --- a/packages/flutter/lib/src/material/checkbox_list_tile.dart +++ b/packages/flutter/lib/src/material/checkbox_list_tile.dart @@ -31,6 +31,9 @@ import 'theme.dart'; /// [secondary] widget is placed on the opposite side. This maps to the /// [ListTile.leading] and [ListTile.trailing] properties of [ListTile]. /// +/// To show the [CheckboxListTile] as disabled, pass null as the [onChanged] +/// callback. +/// /// ## Sample code /// /// This widget shows a checkbox that, when checked, slows down all animations diff --git a/packages/flutter/lib/src/material/circle_avatar.dart b/packages/flutter/lib/src/material/circle_avatar.dart index cd45511f91fe6..10872846f5f42 100644 --- a/packages/flutter/lib/src/material/circle_avatar.dart +++ b/packages/flutter/lib/src/material/circle_avatar.dart @@ -9,6 +9,9 @@ import 'constants.dart'; import 'theme.dart'; import 'typography.dart'; +// Examples can assume: +// String userAvatarUrl; + /// A circle that represents a user. /// /// Typically used with a user's profile image, or, in the absence of diff --git a/packages/flutter/lib/src/material/colors.dart b/packages/flutter/lib/src/material/colors.dart index 7186c0b567570..f11c210791594 100644 --- a/packages/flutter/lib/src/material/colors.dart +++ b/packages/flutter/lib/src/material/colors.dart @@ -2,41 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show Color, hashValues; +import 'dart:ui' show Color; -/// A color that has a small table of related colors called a "swatch". -/// -/// See also: -/// -/// * [MaterialColor] and [MaterialAccentColor], which define material design -/// primary and accent color swatches. -/// * [Colors], which defines all of the standard material design colors. -class ColorSwatch extends Color { - /// Creates a color that has a small table of related colors called a "swatch". - const ColorSwatch(int primary, this._swatch) : super(primary); - - final Map _swatch; - - /// Returns an element of the swatch table. - Color operator [](int index) => _swatch[index]; - - @override - bool operator ==(dynamic other) { - if (identical(this, other)) - return true; - if (other.runtimeType != runtimeType) - return false; - final ColorSwatch typedOther = other; - return super==(other) && _swatch == typedOther._swatch; - } - - @override - int get hashCode => hashValues(runtimeType, value, _swatch); - - @override - String toString() => '$runtimeType(primary value: ${super.toString()})'; - -} +import 'package:flutter/painting.dart'; /// Defines a single color as well a color swatch with ten shades of the color. /// @@ -47,39 +15,39 @@ class ColorSwatch extends Color { /// See also: /// /// * [Colors], which defines all of the standard material colors. -class MaterialColor extends ColorSwatch { +class MaterialColor extends ColorSwatch { /// Creates a color swatch with a variety of shades. const MaterialColor(int primary, Map swatch) : super(primary, swatch); /// The lightest shade. - Color get shade50 => _swatch[50]; + Color get shade50 => this[50]; /// The second lightest shade. - Color get shade100 => _swatch[100]; + Color get shade100 => this[100]; /// The third lightest shade. - Color get shade200 => _swatch[200]; + Color get shade200 => this[200]; /// The fourth lightest shade. - Color get shade300 => _swatch[300]; + Color get shade300 => this[300]; /// The fifth lightest shade. - Color get shade400 => _swatch[400]; + Color get shade400 => this[400]; /// The default shade. - Color get shade500 => _swatch[500]; + Color get shade500 => this[500]; /// The fourth darkest shade. - Color get shade600 => _swatch[600]; + Color get shade600 => this[600]; /// The third darkest shade. - Color get shade700 => _swatch[700]; + Color get shade700 => this[700]; /// The second darkest shade. - Color get shade800 => _swatch[800]; + Color get shade800 => this[800]; /// The darkest shade. - Color get shade900 => _swatch[900]; + Color get shade900 => this[900]; } /// Defines a single accent color as well a swatch of four shades of the @@ -94,25 +62,25 @@ class MaterialColor extends ColorSwatch { /// /// * [Colors], which defines all of the standard material colors. /// * -class MaterialAccentColor extends ColorSwatch { +class MaterialAccentColor extends ColorSwatch { /// Creates a color swatch with a variety of shades appropriate for accent /// colors. const MaterialAccentColor(int primary, Map swatch) : super(primary, swatch); /// The lightest shade. - Color get shade50 => _swatch[50]; + Color get shade50 => this[50]; /// The second lightest shade. - Color get shade100 => _swatch[100]; + Color get shade100 => this[100]; /// The default shade. - Color get shade200 => _swatch[200]; + Color get shade200 => this[200]; /// The second darkest shade. - Color get shade400 => _swatch[400]; + Color get shade400 => this[400]; /// The darkest shade. - Color get shade700 => _swatch[700]; + Color get shade700 => this[700]; } /// [Color] and [ColorSwatch] constants which represent Material design's @@ -123,14 +91,24 @@ class MaterialAccentColor extends ColorSwatch { /// colors selected for the current theme, such as [ThemeData.primaryColor] and /// [ThemeData.accentColor] (among many others). /// +/// Most swatches have colors from 100 to 900 in increments of one hundred, plus +/// the color 50. The smaller the number, the more pale the color. The greater +/// the number, the darker the color. The accent swatches (e.g. [redAccent]) only +/// have the values 100, 200, 400, and 700. +/// +/// In addition, a series of blacks and whites with common opacities are +/// available. For example, [black54] is a pure black with 54% opacity. +/// +/// ## Sample code +/// /// To select a specific color from one of the swatches, index into the swatch /// using an integer for the specific color desired, as follows: /// /// ```dart -/// Colors.green[400] // Selects a mid-range green. +/// Color selection = Colors.green[400]; // Selects a mid-range green. /// ``` /// -/// Each ColorSwatch constant is a color and can used directly. For example +/// Each [ColorSwatch] constant is a color and can used directly. For example: /// /// ```dart /// new Container( @@ -138,13 +116,73 @@ class MaterialAccentColor extends ColorSwatch { /// ) /// ``` /// -/// Most swatches have colors from 100 to 900 in increments of one hundred, plus -/// the color 50. The smaller the number, the more pale the color. The greater -/// the number, the darker the color. The accent swatches (e.g. [redAccent]) only -/// have the values 100, 200, 400, and 700. +/// ## Color palettes /// -/// In addition, a series of blacks and whites with common opacities are -/// available. For example, [black54] is a pure black with 54% opacity. +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.pink.png) +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.pinkAccent.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.red.png) +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.redAccent.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepOrange.png) +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepOrangeAccent.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.orange.png) +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.orangeAccent.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.amber.png) +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.amberAccent.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.yellow.png) +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.yellowAccent.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lime.png) +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.limeAccent.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightGreen.png) +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightGreenAccent.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.green.png) +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.greenAccent.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.teal.png) +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.tealAccent.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.cyan.png) +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.cyanAccent.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightBlue.png) +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightBlueAccent.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blue.png) +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blueAccent.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.indigo.png) +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.indigoAccent.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.purple.png) +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.purpleAccent.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepPurple.png) +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepPurpleAccent.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blueGrey.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.brown.png) +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.grey.png) +/// +/// ## Blacks and whites +/// +/// These colors are identified by their transparency. The low transparency +/// levels (e.g. [Colors.white12] and [Colors.white10]) are very hard to see and +/// should be avoided in general. They are intended for very subtle effects. +/// +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blacks.png) +/// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.whites.png) +/// +/// The [Colors.transparent] color isn't shown here because it is entirely +/// invisible! class Colors { Colors._(); @@ -152,54 +190,97 @@ class Colors { static const Color transparent = const Color(0x00000000); /// Completely opaque black. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blacks.png) + /// + /// See also: + /// + /// * [black87], [black54], [black45], [black38], [black26], [black12], which + /// are variants on this color but with different opacities. + /// * [white], a solid white color. + /// * [transparent], a fully-transparent color. static const Color black = const Color(0xFF000000); /// Black with 87% opacity. /// /// This is a good contrasting color for text in light themes. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blacks.png) + /// /// See also: /// - /// * [Typography.black], which uses this color for its text styles. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [Typography.black], which uses this color for its text styles. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. + /// * [black], [black54], [black45], [black38], [black26], [black12], which + /// are variants on this color but with different opacities. static const Color black87 = const Color(0xDD000000); /// Black with 54% opacity. /// - /// This is a color commonly used for headings in light themes. + /// This is a color commonly used for headings in light themes. It's also used + /// as the mask color behind dialogs. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blacks.png) /// /// See also: /// - /// * [Typography.black], which uses this color for its text styles. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [Typography.black], which uses this color for its text styles. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. + /// * [black], [black87], [black45], [black38], [black26], [black12], which + /// are variants on this color but with different opacities. static const Color black54 = const Color(0x8A000000); + /// Black with 45% opacity. + /// + /// Used for disabled icons. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blacks.png) + /// + /// See also: + /// + /// * [black], [black87], [black54], [black38], [black26], [black12], which + /// are variants on this color but with different opacities. + static const Color black45 = const Color(0x73000000); + /// Black with 38% opacity. /// /// Used for the placeholder text in data tables in light themes. - static const Color black38 = const Color(0x61000000); - - /// Black with 45% opacity. /// - /// Used for modal barriers. - static const Color black45 = const Color(0x73000000); + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blacks.png) + /// + /// See also: + /// + /// * [black], [black87], [black54], [black45], [black26], [black12], which + /// are variants on this color but with different opacities. + static const Color black38 = const Color(0x61000000); /// Black with 26% opacity. /// /// Used for disabled radio buttons and the text of disabled flat buttons in light themes. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blacks.png) + /// /// See also: /// - /// * [ThemeData.disabledColor], which uses this color by default in light themes. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [ThemeData.disabledColor], which uses this color by default in light themes. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. + /// * [black], [black87], [black54], [black45], [black38], [black12], which + /// are variants on this color but with different opacities. static const Color black26 = const Color(0x42000000); /// Black with 12% opacity. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blacks.png) + /// /// Used for the background of disabled raised buttons in light themes. + /// + /// See also: + /// + /// * [black], [black87], [black54], [black45], [black38], [black26], which + /// are variants on this color but with different opacities. static const Color black12 = const Color(0x1F000000); /// Completely opaque white. @@ -207,57 +288,98 @@ class Colors { /// This is a good contrasting color for the [ThemeData.primaryColor] in the /// dark theme. See [ThemeData.brightness]. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.whites.png) + /// /// See also: /// - /// * [Typography.white], which uses this color for its text styles. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [Typography.white], which uses this color for its text styles. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. + /// * [white70, white30, white12, white10], which are variants on this color + /// but with different opacities. + /// * [black], a solid black color. + /// * [transparent], a fully-transparent color. static const Color white = const Color(0xFFFFFFFF); /// White with 70% opacity. /// /// This is a color commonly used for headings in dark themes. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.whites.png) + /// /// See also: /// - /// * [Typography.white], which uses this color for its text styles. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [Typography.white], which uses this color for its text styles. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. + /// * [white, white30, white12, white10], which are variants on this color + /// but with different opacities. static const Color white70 = const Color(0xB3FFFFFF); /// White with 32% opacity. /// /// Used for disabled radio buttons and the text of disabled flat buttons in dark themes. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.whites.png) + /// /// See also: /// - /// * [ThemeData.disabledColor], which uses this color by default in dark themes. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [ThemeData.disabledColor], which uses this color by default in dark themes. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. + /// * [white, white70, white12, white10], which are variants on this color + /// but with different opacities. static const Color white30 = const Color(0x4DFFFFFF); /// White with 12% opacity. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.whites.png) + /// /// Used for the background of disabled raised buttons in dark themes. + /// + /// See also: + /// + /// * [white, white70, white30, white10], which are variants on this color + /// but with different opacities. static const Color white12 = const Color(0x1FFFFFFF); /// White with 10% opacity. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.whites.png) + /// + /// See also: + /// + /// * [white, white70, white30, white12], which are variants on this color + /// but with different opacities. + /// * [transparent], a fully-transparent color, not far from this one. static const Color white10 = const Color(0x1AFFFFFF); /// The red primary color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.red.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.redAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepOrange.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepOrangeAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.pink.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.pinkAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.red[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.red[400], + /// ) /// ``` /// /// See also: /// - /// * [redAccent], the corresponding accent colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [redAccent], the corresponding accent colors. + /// * [deepOrange] and [pink], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor red = const MaterialColor( _redPrimaryValue, const { @@ -277,18 +399,30 @@ class Colors { /// The red accent swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.red.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.redAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepOrange.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepOrangeAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.pink.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.pinkAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.redAccent[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.redAccent[400], + /// ) /// ``` /// /// See also: /// - /// * [red], the corresponding primary colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [red], the corresponding primary colors. + /// * [deepOrangeAccent] and [pinkAccent], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialAccentColor redAccent = const MaterialAccentColor( _redAccentValue, const { @@ -302,18 +436,30 @@ class Colors { /// The pink primary color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.pink.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.pinkAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.red.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.redAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.purple.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.purpleAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.pink[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.pink[400], + /// ) /// ``` /// /// See also: /// - /// * [pinkAccent], the corresponding accent colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [pinkAccent], the corresponding accent colors. + /// * [red] and [purple], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor pink = const MaterialColor( _pinkPrimaryValue, const { @@ -333,18 +479,30 @@ class Colors { /// The pink accent color swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.pink.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.pinkAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.red.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.redAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.purple.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.purpleAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.pinkAccent[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.pinkAccent[400], + /// ) /// ``` /// /// See also: /// - /// * [pink], the corresponding primary colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [pink], the corresponding primary colors. + /// * [redAccent] and [purpleAccent], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialAccentColor pinkAccent = const MaterialAccentColor( _pinkAccentPrimaryValue, const { @@ -358,18 +516,30 @@ class Colors { /// The purple primary color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.purple.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.purpleAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepPurple.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepPurpleAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.pink.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.pinkAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.purple[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.purple[400], + /// ) /// ``` /// /// See also: /// - /// * [purpleAccent], the corresponding accent colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [purpleAccent], the corresponding accent colors. + /// * [deepPurple] and [pink], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor purple = const MaterialColor( _purplePrimaryValue, const { @@ -389,18 +559,30 @@ class Colors { /// The purple accent color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.purple.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.purpleAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepPurple.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepPurpleAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.pink.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.pinkAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.purpleAccent[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.purpleAccent[400], + /// ) /// ``` /// /// See also: /// - /// * [purple], the corresponding primary colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [purple], the corresponding primary colors. + /// * [deepPurpleAccent] and [pinkAccent], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialAccentColor purpleAccent = const MaterialAccentColor( _purpleAccentPrimaryValue, const { @@ -414,18 +596,30 @@ class Colors { /// The deep purple primary color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepPurple.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepPurpleAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.purple.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.purpleAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.indigo.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.indigoAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.deepPurple[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.deepPurple[400], + /// ) /// ``` /// /// See also: /// - /// * [deepPurpleAccent], the corresponding accent colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [deepPurpleAccent], the corresponding accent colors. + /// * [purple] and [indigo], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor deepPurple = const MaterialColor( _deepPurplePrimaryValue, const { @@ -445,18 +639,30 @@ class Colors { /// The deep purple accent color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepPurple.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepPurpleAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.purple.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.purpleAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.indigo.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.indigoAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.deepPurpleAccent[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.deepPurpleAccent[400], + /// ) /// ``` /// /// See also: /// - /// * [deepPurple], the corresponding primary colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [deepPurple], the corresponding primary colors. + /// * [purpleAccent] and [indigoAccent], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialAccentColor deepPurpleAccent = const MaterialAccentColor( _deepPurpleAccentPrimaryValue, const { @@ -470,18 +676,30 @@ class Colors { /// The indigo primary color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.indigo.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.indigoAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blue.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blueAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepPurple.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepPurpleAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.indigo[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.indigo[400], + /// ) /// ``` /// /// See also: /// - /// * [indigoAccent], the corresponding accent colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [indigoAccent], the corresponding accent colors. + /// * [blue] and [deepPurple], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor indigo = const MaterialColor( _indigoPrimaryValue, const { @@ -501,18 +719,30 @@ class Colors { /// The indigo accent color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.indigo.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.indigoAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blue.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blueAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepPurple.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepPurpleAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.indigoAccent[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.indigoAccent[400], + /// ) /// ``` /// /// See also: /// - /// * [indigo], the corresponding primary colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [indigo], the corresponding primary colors. + /// * [blueAccent] and [deepPurpleAccent], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialAccentColor indigoAccent = const MaterialAccentColor( _indigoAccentPrimaryValue, const { @@ -526,18 +756,32 @@ class Colors { /// The blue primary color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blue.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blueAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.indigo.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.indigoAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightBlue.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightBlueAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blueGrey.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.blue[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.blue[400], + /// ) /// ``` /// /// See also: /// - /// * [blueAccent], the corresponding accent colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [blueAccent], the corresponding accent colors. + /// * [indigo], [lightBlue], and [blueGrey], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor blue = const MaterialColor( _bluePrimaryValue, const { @@ -557,18 +801,30 @@ class Colors { /// The blue accent color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blue.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blueAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.indigo.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.indigoAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightBlue.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightBlueAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.blueAccent[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.blueAccent[400], + /// ) /// ``` /// /// See also: /// - /// * [blue], the corresponding primary colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [blue], the corresponding primary colors. + /// * [indigoAccent] and [lightBlueAccent], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialAccentColor blueAccent = const MaterialAccentColor( _blueAccentPrimaryValue, const { @@ -582,18 +838,30 @@ class Colors { /// The light blue primary color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightBlue.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightBlueAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blue.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blueAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.cyan.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.cyanAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.lightBlue[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.lightBlue[400], + /// ) /// ``` /// /// See also: /// - /// * [lightBlueAccent], the corresponding accent colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [lightBlueAccent], the corresponding accent colors. + /// * [blue] and [cyan], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor lightBlue = const MaterialColor( _lightBluePrimaryValue, const { @@ -613,18 +881,30 @@ class Colors { /// The light blue accent swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightBlue.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightBlueAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blue.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blueAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.cyan.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.cyanAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.lightBlueAccent[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.lightBlueAccent[400], + /// ) /// ``` /// /// See also: /// - /// * [lightBlue], the corresponding primary colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [lightBlue], the corresponding primary colors. + /// * [blueAccent] and [cyanAccent], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialAccentColor lightBlueAccent = const MaterialAccentColor( _lightBlueAccentPrimaryValue, const { @@ -638,18 +918,32 @@ class Colors { /// The cyan primary color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.cyan.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.cyanAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightBlue.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightBlueAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.teal.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.tealAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blueGrey.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.cyan[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.cyan[400], + /// ) /// ``` /// /// See also: /// - /// * [cyanAccent], the corresponding accent colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [cyanAccent], the corresponding accent colors. + /// * [lightBlue], [teal], and [blueGrey], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor cyan = const MaterialColor( _cyanPrimaryValue, const { @@ -669,18 +963,30 @@ class Colors { /// The cyan accent color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.cyan.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.cyanAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightBlue.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightBlueAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.teal.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.tealAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.cyanAccent[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.cyanAccent[400], + /// ) /// ``` /// /// See also: /// - /// * [cyan], the corresponding primary colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [cyan], the corresponding primary colors. + /// * [lightBlueAccent] and [tealAccent], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialAccentColor cyanAccent = const MaterialAccentColor( _cyanAccentPrimaryValue, const { @@ -694,18 +1000,30 @@ class Colors { /// The teal primary color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.teal.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.tealAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.green.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.greenAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.cyan.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.cyanAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.teal[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.teal[400], + /// ) /// ``` /// /// See also: /// - /// * [tealAccent], the corresponding accent colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [tealAccent], the corresponding accent colors. + /// * [green] and [cyan], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor teal = const MaterialColor( _tealPrimaryValue, const { @@ -725,18 +1043,30 @@ class Colors { /// The teal accent color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.teal.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.tealAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.green.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.greenAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.cyan.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.cyanAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.tealAccent[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.tealAccent[400], + /// ) /// ``` /// /// See also: /// - /// * [teal], the corresponding primary colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [teal], the corresponding primary colors. + /// * [greenAccent] and [cyanAccent], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialAccentColor tealAccent = const MaterialAccentColor( _tealAccentPrimaryValue, const { @@ -750,18 +1080,33 @@ class Colors { /// The green primary color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.green.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.greenAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.teal.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.tealAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightGreen.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightGreenAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lime.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.limeAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.green[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.green[400], + /// ) /// ``` /// /// See also: /// - /// * [greenAccent], the corresponding accent colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [greenAccent], the corresponding accent colors. + /// * [teal], [lightGreen], and [lime], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor green = const MaterialColor( _greenPrimaryValue, const { @@ -781,18 +1126,33 @@ class Colors { /// The green accent color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.green.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.greenAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.teal.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.tealAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightGreen.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightGreenAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lime.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.limeAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.greenAccent[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.greenAccent[400], + /// ) /// ``` /// /// See also: /// - /// * [green], the corresponding primary colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [green], the corresponding primary colors. + /// * [tealAccent], [lightGreenAccent], and [limeAccent], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialAccentColor greenAccent = const MaterialAccentColor( _greenAccentPrimaryValue, const { @@ -806,18 +1166,30 @@ class Colors { /// The light green primary color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightGreen.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightGreenAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.green.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.greenAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lime.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.limeAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.lightGreen[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.lightGreen[400], + /// ) /// ``` /// /// See also: /// - /// * [lightGreenAccent], the corresponding accent colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [lightGreenAccent], the corresponding accent colors. + /// * [green] and [lime], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor lightGreen = const MaterialColor( _lightGreenPrimaryValue, const { @@ -837,18 +1209,30 @@ class Colors { /// The light green accent color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightGreen.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightGreenAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.green.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.greenAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lime.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.limeAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.lightGreenAccent[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.lightGreenAccent[400], + /// ) /// ``` /// /// See also: /// - /// * [lightGreen], the corresponding primary colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [lightGreen], the corresponding primary colors. + /// * [greenAccent] and [limeAccent], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialAccentColor lightGreenAccent = const MaterialAccentColor( _lightGreenAccentPrimaryValue, const { @@ -862,18 +1246,30 @@ class Colors { /// The lime primary color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lime.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.limeAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightGreen.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightGreenAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.yellow.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.yellowAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.lime[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.lime[400], + /// ) /// ``` /// /// See also: /// - /// * [limeAccent], the corresponding accent colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [limeAccent], the corresponding accent colors. + /// * [lightGreen] and [yellow], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor lime = const MaterialColor( _limePrimaryValue, const { @@ -893,18 +1289,30 @@ class Colors { /// The lime accent primary color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lime.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.limeAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightGreen.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lightGreenAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.yellow.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.yellowAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.limeAccent[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.limeAccent[400], + /// ) /// ``` /// /// See also: /// - /// * [lime], the corresponding primary colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [lime], the corresponding primary colors. + /// * [lightGreenAccent] and [yellowAccent], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialAccentColor limeAccent = const MaterialAccentColor( _limeAccentPrimaryValue, const { @@ -918,18 +1326,30 @@ class Colors { /// The yellow primary color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.yellow.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.yellowAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lime.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.limeAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.amber.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.amberAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.yellow[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.yellow[400], + /// ) /// ``` /// /// See also: /// - /// * [yellowAccent], the corresponding accent colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [yellowAccent], the corresponding accent colors. + /// * [lime] and [amber], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor yellow = const MaterialColor( _yellowPrimaryValue, const { @@ -949,18 +1369,30 @@ class Colors { /// The yellow accent color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.yellow.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.yellowAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.lime.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.limeAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.amber.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.amberAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.yellowAccent[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.yellowAccent[400], + /// ) /// ``` /// /// See also: /// - /// * [yellow], the corresponding primary colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [yellow], the corresponding primary colors. + /// * [limeAccent] and [amberAccent], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialAccentColor yellowAccent = const MaterialAccentColor( _yellowAccentPrimaryValue, const { @@ -974,18 +1406,30 @@ class Colors { /// The amber primary color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.amber.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.amberAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.yellow.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.yellowAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.orange.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.orangeAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.amber[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.amber[400], + /// ) /// ``` /// /// See also: /// - /// * [amberAccent], the corresponding accent colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [amberAccent], the corresponding accent colors. + /// * [yellow] and [orange], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor amber = const MaterialColor( _amberPrimaryValue, const { @@ -1005,18 +1449,30 @@ class Colors { /// The amber accent color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.amber.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.amberAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.yellow.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.yellowAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.orange.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.orangeAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.amberAccent[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.amberAccent[400], + /// ) /// ``` /// /// See also: /// - /// * [amber], the corresponding primary colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [amber], the corresponding primary colors. + /// * [yellowAccent] and [orangeAccent], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialAccentColor amberAccent = const MaterialAccentColor( _amberAccentPrimaryValue, const { @@ -1030,18 +1486,32 @@ class Colors { /// The orange primary color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.orange.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.orangeAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.amber.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.amberAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepOrange.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepOrangeAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.brown.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.orange[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.orange[400], + /// ) /// ``` /// /// See also: /// - /// * [orangeAccent], the corresponding accent colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [orangeAccent], the corresponding accent colors. + /// * [amber], [deepOrange], and [brown], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor orange = const MaterialColor( _orangePrimaryValue, const { @@ -1061,18 +1531,30 @@ class Colors { /// The orange accent color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.orange.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.orangeAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.amber.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.amberAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepOrange.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepOrangeAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.orangeAccent[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.orangeAccent[400], + /// ) /// ``` /// /// See also: /// - /// * [orange], the corresponding primary colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [orange], the corresponding primary colors. + /// * [amberAccent] and [deepOrangeAccent], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialAccentColor orangeAccent = const MaterialAccentColor( _orangeAccentPrimaryValue, const { @@ -1086,18 +1568,32 @@ class Colors { /// The deep orange primary color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepOrange.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepOrangeAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.orange.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.orangeAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.red.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.redAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.brown.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.deepOrange[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.deepOrange[400], + /// ) /// ``` /// /// See also: /// - /// * [deepOrangeAccent], the corresponding accent colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [deepOrangeAccent], the corresponding accent colors. + /// * [orange], [red], and [brown], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor deepOrange = const MaterialColor( _deepOrangePrimaryValue, const { @@ -1117,18 +1613,30 @@ class Colors { /// The deep orange accent color and swatch. /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepOrange.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.deepOrangeAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.orange.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.orangeAccent.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.red.png) + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.redAccent.png) + /// + /// ## Sample code + /// /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.deepOrangeAccent[400], - /// ), + /// new Icon( + /// Icons.widgets, + /// color: Colors.deepOrangeAccent[400], + /// ) /// ``` /// /// See also: /// - /// * [deepOrange], the corresponding primary colors. - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [deepOrange], the corresponding primary colors. + /// * [orangeAccent] [redAccent], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialAccentColor deepOrangeAccent = const MaterialAccentColor( _deepOrangeAccentPrimaryValue, const { @@ -1142,19 +1650,28 @@ class Colors { /// The brown primary color and swatch. /// - /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.brown[400], - /// ), - /// ``` + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.brown.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.orange.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blueGrey.png) /// /// This swatch has no corresponding accent color and swatch. /// + /// ## Sample code + /// + /// ```dart + /// new Icon( + /// Icons.widgets, + /// color: Colors.brown[400], + /// ) + /// ``` + /// /// See also: /// - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [orange] and [blueGrey], vaguely similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor brown = const MaterialColor( _brownPrimaryValue, const { @@ -1174,12 +1691,11 @@ class Colors { /// The grey primary color and swatch. /// - /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.grey[400], - /// ), - /// ``` + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.grey.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blueGrey.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.brown.png) /// /// This swatch has no corresponding accent swatch. /// @@ -1188,10 +1704,22 @@ class Colors { /// used for raised button while pressed in light themes, and 850 is used for /// the background color of the dark theme. See [ThemeData.brightness]. /// + /// ## Sample code + /// + /// ```dart + /// new Icon( + /// Icons.widgets, + /// color: Colors.grey[400], + /// ) + /// ``` + /// /// See also: /// - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [blueGrey] and [brown], somewhat similar colors. + /// * [black], [black87], [black54], [black45], [black38], [black26], [black12], which + /// provide a different approach to showing shades of grey. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor grey = const MaterialColor( _greyPrimaryValue, const { @@ -1213,19 +1741,30 @@ class Colors { /// The blue-grey primary color and swatch. /// - /// ```dart - /// new Icon( - /// icon: Icons.widgets, - /// color: Colors.blueGrey[400], - /// ), - /// ``` + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blueGrey.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.grey.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.cyan.png) + /// + /// ![](https://flutter.github.io/assets-for-api-docs/material/Colors.blue.png) /// /// This swatch has no corresponding accent swatch. /// + /// ## Sample code + /// + /// ```dart + /// new Icon( + /// Icons.widgets, + /// color: Colors.blueGrey[400], + /// ) + /// ``` + /// /// See also: /// - /// * [Theme.of], which allows you to select colors from the current theme - /// rather than hard-coding colors in your build methods. + /// * [grey], [cyan], and [blue], similar colors. + /// * [Theme.of], which allows you to select colors from the current theme + /// rather than hard-coding colors in your build methods. static const MaterialColor blueGrey = const MaterialColor( _blueGreyPrimaryValue, const { @@ -1243,7 +1782,7 @@ class Colors { ); static const int _blueGreyPrimaryValue = 0xFF607D8B; - /// The material design primary color swatches (except grey). + /// The material design primary color swatches, excluding grey. static const List primaries = const [ red, pink, @@ -1262,7 +1801,9 @@ class Colors { orange, deepOrange, brown, - // grey intentionally omitted + // The grey swatch is intentionally omitted because when picking a color + // randomly from this list to colorize an application, picking grey suddenly + // makes the app look disabled. blueGrey, ]; diff --git a/packages/flutter/lib/src/material/constants.dart b/packages/flutter/lib/src/material/constants.dart index 2558975639124..22288167fbc42 100644 --- a/packages/flutter/lib/src/material/constants.dart +++ b/packages/flutter/lib/src/material/constants.dart @@ -28,5 +28,8 @@ const int kRadialReactionAlpha = 0x33; /// The duration of the horizontal scroll animation that occurs when a tab is tapped. const Duration kTabScrollDuration = const Duration(milliseconds: 300); +/// The horizontal padding included by [Tab]s. +const EdgeInsets kTabLabelPadding = const EdgeInsets.symmetric(horizontal: 12.0); + /// The padding added around material list items. const EdgeInsets kMaterialListPadding = const EdgeInsets.symmetric(vertical: 8.0); diff --git a/packages/flutter/lib/src/material/data_table.dart b/packages/flutter/lib/src/material/data_table.dart index 2730acb78e79a..040d0dcfcf12d 100644 --- a/packages/flutter/lib/src/material/data_table.dart +++ b/packages/flutter/lib/src/material/data_table.dart @@ -259,14 +259,14 @@ class DataTable extends StatelessWidget { this.sortAscending: true, this.onSelectAll, @required this.rows - }) : _onlyTextColumn = _initOnlyTextColumn(columns), super(key: key) { - assert(columns != null); - assert(columns.isNotEmpty); - assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)); - assert(sortAscending != null); - assert(rows != null); - assert(!rows.any((DataRow row) => row.cells.length != columns.length)); - } + }) : assert(columns != null), + assert(columns.isNotEmpty), + assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)), + assert(sortAscending != null), + assert(rows != null), + assert(!rows.any((DataRow row) => row.cells.length != columns.length)), + _onlyTextColumn = _initOnlyTextColumn(columns), + super(key: key); /// The configuration and labels for the columns in the table. final List columns; diff --git a/packages/flutter/lib/src/material/date_picker.dart b/packages/flutter/lib/src/material/date_picker.dart index c60a738c6dd03..14cb4552770dc 100644 --- a/packages/flutter/lib/src/material/date_picker.dart +++ b/packages/flutter/lib/src/material/date_picker.dart @@ -181,14 +181,13 @@ class DayPicker extends StatelessWidget { @required this.lastDate, @required this.displayedMonth, this.selectableDayPredicate, - }) : super(key: key) { - assert(selectedDate != null); - assert(currentDate != null); - assert(onChanged != null); - assert(displayedMonth != null); - assert(!firstDate.isAfter(lastDate)); - assert(selectedDate.isAfter(firstDate) || selectedDate.isAtSameMomentAs(firstDate)); - } + }) : assert(selectedDate != null), + assert(currentDate != null), + assert(onChanged != null), + assert(displayedMonth != null), + assert(!firstDate.isAfter(lastDate)), + assert(selectedDate.isAfter(firstDate) || selectedDate.isAtSameMomentAs(firstDate)), + super(key: key); /// The currently selected date. /// @@ -331,12 +330,11 @@ class MonthPicker extends StatefulWidget { @required this.firstDate, @required this.lastDate, this.selectableDayPredicate, - }) : super(key: key) { - assert(selectedDate != null); - assert(onChanged != null); - assert(!firstDate.isAfter(lastDate)); - assert(selectedDate.isAfter(firstDate) || selectedDate.isAtSameMomentAs(firstDate)); - } + }) : assert(selectedDate != null), + assert(onChanged != null), + assert(!firstDate.isAfter(lastDate)), + assert(selectedDate.isAfter(firstDate) || selectedDate.isAtSameMomentAs(firstDate)), + super(key: key); /// The currently selected date. /// @@ -519,11 +517,10 @@ class YearPicker extends StatefulWidget { @required this.onChanged, @required this.firstDate, @required this.lastDate, - }) : super(key: key) { - assert(selectedDate != null); - assert(onChanged != null); - assert(!firstDate.isAfter(lastDate)); - } + }) : assert(selectedDate != null), + assert(onChanged != null), + assert(!firstDate.isAfter(lastDate)), + super(key: key); /// The currently selected date. /// diff --git a/packages/flutter/lib/src/material/dialog.dart b/packages/flutter/lib/src/material/dialog.dart index 33b568d802e33..c4bc60fc0116e 100644 --- a/packages/flutter/lib/src/material/dialog.dart +++ b/packages/flutter/lib/src/material/dialog.dart @@ -319,9 +319,8 @@ class _DialogRoute extends PopupRoute { @required this.theme, bool barrierDismissible: true, @required this.child, - }) : _barrierDismissible = barrierDismissible { - assert(barrierDismissible != null); - } + }) : assert(barrierDismissible != null), + _barrierDismissible = barrierDismissible; final Widget child; final ThemeData theme; diff --git a/packages/flutter/lib/src/material/divider.dart b/packages/flutter/lib/src/material/divider.dart index 317190ee7b00b..c8dc1b58a41ef 100644 --- a/packages/flutter/lib/src/material/divider.dart +++ b/packages/flutter/lib/src/material/divider.dart @@ -6,7 +6,7 @@ import 'package:flutter/widgets.dart'; import 'theme.dart'; -/// A one logical pixel thick horizontal line, with padding on either +/// A one device pixel thick horizontal line, with padding on either /// side. /// /// In the material design language, this represents a divider. @@ -26,19 +26,22 @@ import 'theme.dart'; class Divider extends StatelessWidget { /// Creates a material design divider. /// - /// The height must be at least 1.0 logical pixels. + /// The height must be positive. const Divider({ Key key, this.height: 16.0, this.indent: 0.0, this.color - }) : assert(height >= 1.0), + }) : assert(height >= 0.0), super(key: key); /// The divider's vertical extent. /// - /// The divider itself is always drawn as one logical pixel thick horizontal + /// The divider itself is always drawn as one device pixel thick horizontal /// line that is centered within the height specified by this value. + /// + /// A divider with a height of 0.0 is always drawn as a line with a height of + /// exactly one device pixel, without any padding around it. final double height; /// The amount of empty space to the left of the divider. @@ -58,19 +61,22 @@ class Divider extends StatelessWidget { @override Widget build(BuildContext context) { - final double bottom = (height ~/ 2.0).toDouble(); - return new Container( - height: 0.0, - margin: new EdgeInsets.only( - top: height - bottom - 1.0, - left: indent, - bottom: bottom + return new SizedBox( + height: height, + child: new Center( + child: new Container( + height: 0.0, + margin: new EdgeInsets.only(left: indent), + decoration: new BoxDecoration( + border: new Border( + bottom: new BorderSide( + color: color ?? Theme.of(context).dividerColor, + width: 0.0, + ), + ), + ), + ), ), - decoration: new BoxDecoration( - border: new Border( - bottom: new BorderSide(color: color ?? Theme.of(context).dividerColor) - ) - ) ); } } diff --git a/packages/flutter/lib/src/material/drawer.dart b/packages/flutter/lib/src/material/drawer.dart index dde0a504e1fe1..b9f32ec699f2f 100644 --- a/packages/flutter/lib/src/material/drawer.dart +++ b/packages/flutter/lib/src/material/drawer.dart @@ -270,10 +270,12 @@ class DrawerControllerState extends State with SingleTickerPro child: new RepaintBoundary( child: new Stack( children: [ - new GestureDetector( - onTap: close, - child: new Container( - color: _color.evaluate(_controller) + new BlockSemantics( + child: new GestureDetector( + onTap: close, + child: new Container( + color: _color.evaluate(_controller) + ), ), ), new Align( diff --git a/packages/flutter/lib/src/material/drawer_header.dart b/packages/flutter/lib/src/material/drawer_header.dart index b94f68eafaa8e..2167ce57c2d63 100644 --- a/packages/flutter/lib/src/material/drawer_header.dart +++ b/packages/flutter/lib/src/material/drawer_header.dart @@ -82,7 +82,7 @@ class DrawerHeader extends StatelessWidget { border: new Border( bottom: new BorderSide( color: theme.dividerColor, - width: 1.0 + width: 0.0 ) ) ), diff --git a/packages/flutter/lib/src/material/dropdown.dart b/packages/flutter/lib/src/material/dropdown.dart index 627f8dcd2adb8..a4a22b545539e 100644 --- a/packages/flutter/lib/src/material/dropdown.dart +++ b/packages/flutter/lib/src/material/dropdown.dart @@ -266,9 +266,7 @@ class _DropdownRoute extends PopupRoute<_DropdownRouteResult> { this.elevation: 8, this.theme, @required this.style, - }) { - assert(style != null); - } + }) : assert(style != null); final List> items; final Rect buttonRect; @@ -400,10 +398,16 @@ class DropdownButtonHideUnderline extends InheritedWidget { /// shows the currently selected item as well as an arrow that opens a menu for /// selecting another item. /// +/// The type `T` is the type of the values the dropdown menu represents. All the +/// entries in a given menu must represent values with consistent types. +/// Typically, an enum is used. Each [DropdownMenuItem] in [items] must be +/// specialized with that same type argument. +/// /// Requires one of its ancestors to be a [Material] widget. /// /// See also: /// +/// * [DropdownMenuItem], the class used to represent the [items]. /// * [DropdownButtonHideUnderline], which prevents its descendant dropdown buttons /// from displaying their underlines. /// * [RaisedButton], [FlatButton], ordinary buttons that trigger a single action. @@ -425,11 +429,9 @@ class DropdownButton extends StatefulWidget { this.style, this.iconSize: 24.0, this.isDense: false, - }) : super(key: key) { - assert(items != null); - assert(value == null || - items.where((DropdownMenuItem item) => item.value == value).length == 1); - } + }) : assert(items != null), + assert(value == null || items.where((DropdownMenuItem item) => item.value == value).length == 1), + super(key: key); /// The list of possible items to select among. final List> items; diff --git a/packages/flutter/lib/src/material/expansion_panel.dart b/packages/flutter/lib/src/material/expansion_panel.dart index 2b8a40aa57708..39ace458818d5 100644 --- a/packages/flutter/lib/src/material/expansion_panel.dart +++ b/packages/flutter/lib/src/material/expansion_panel.dart @@ -43,11 +43,9 @@ class ExpansionPanel { @required this.headerBuilder, @required this.body, this.isExpanded: false - }) { - assert(headerBuilder != null); - assert(body != null); - assert(isExpanded != null); - } + }) : assert(headerBuilder != null), + assert(body != null), + assert(isExpanded != null); /// The widget builder that builds the expansion panels' header. final ExpansionPanelHeaderBuilder headerBuilder; diff --git a/packages/flutter/lib/src/material/ink_highlight.dart b/packages/flutter/lib/src/material/ink_highlight.dart index 29cb6912f1b99..3130b3d8a94c3 100644 --- a/packages/flutter/lib/src/material/ink_highlight.dart +++ b/packages/flutter/lib/src/material/ink_highlight.dart @@ -43,13 +43,13 @@ class InkHighlight extends InkFeature { BorderRadius borderRadius, RectCallback rectCallback, VoidCallback onRemoved, - }) : _color = color, + }) : assert(color != null), + assert(shape != null), + _color = color, _shape = shape, _borderRadius = borderRadius ?? BorderRadius.zero, _rectCallback = rectCallback, super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) { - assert(color != null); - assert(shape != null); _alphaController = new AnimationController(duration: _kHighlightFadeDuration, vsync: controller.vsync) ..addListener(controller.markNeedsPaint) ..addStatusListener(_handleAlphaStatusChanged) diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index b3416ae9f1c87..70e25030f74fe 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -294,7 +294,7 @@ class ListTile extends StatelessWidget { position: DecorationPosition.foreground, decoration: new BoxDecoration( border: new Border( - bottom: new BorderSide(color: dividerColor), + bottom: new BorderSide(color: dividerColor, width: 0.0), ), ), child: tile, diff --git a/packages/flutter/lib/src/material/material.dart b/packages/flutter/lib/src/material/material.dart index 283b5550f0cd1..fc92a7e14255c 100644 --- a/packages/flutter/lib/src/material/material.dart +++ b/packages/flutter/lib/src/material/material.dart @@ -275,9 +275,12 @@ class _MaterialState extends State with TickerProviderStateMixin { const Duration _kHighlightFadeDuration = const Duration(milliseconds: 200); class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController { - _RenderInkFeatures({ RenderBox child, @required this.vsync, this.color }) : super(child) { - assert(vsync != null); - } + _RenderInkFeatures({ + RenderBox child, + @required this.vsync, + this.color, + }) : assert(vsync != null), + super(child); // This class should exist in a 1:1 relationship with a MaterialState object, // since there's no current support for dynamically changing the ticker @@ -366,10 +369,9 @@ abstract class InkFeature { @required MaterialInkController controller, @required this.referenceBox, this.onRemoved - }) : _controller = controller { - assert(_controller != null); - assert(referenceBox != null); - } + }) : assert(controller != null), + assert(referenceBox != null), + _controller = controller; /// The [MaterialInkController] associated with this [InkFeature]. /// diff --git a/packages/flutter/lib/src/material/page.dart b/packages/flutter/lib/src/material/page.dart index 2a2fb7d442f77..4d9a371a66a1b 100644 --- a/packages/flutter/lib/src/material/page.dart +++ b/packages/flutter/lib/src/material/page.dart @@ -54,26 +54,34 @@ class _MountainViewPageTransition extends StatelessWidget { /// /// Specify whether the incoming page is a fullscreen modal dialog. On iOS, those /// pages animate bottom->up rather than right->left. +/// +/// See also: +/// +/// * [CupertinoPageRoute], that this [PageRoute] delegates transition animations to for iOS. class MaterialPageRoute extends PageRoute { /// Creates a page route for use in a material design app. MaterialPageRoute({ @required this.builder, RouteSettings settings: const RouteSettings(), this.maintainState: true, - this.fullscreenDialog: false, - }) : super(settings: settings) { - assert(builder != null); - assert(opaque); - } + bool fullscreenDialog: false, + }) : assert(builder != null), + assert(opaque), + super(settings: settings, fullscreenDialog: fullscreenDialog); /// Builds the primary contents of the route. final WidgetBuilder builder; - /// Whether this route is a full-screen dialog. - /// - /// Prevents [startPopGesture] from poping the route using an edge swipe on - /// iOS. - final bool fullscreenDialog; + /// A delegate PageRoute to which iOS themed page operations are delegated to. + /// It's lazily created on first use. + CupertinoPageRoute _internalCupertinoPageRoute; + CupertinoPageRoute get _cupertinoPageRoute { + _internalCupertinoPageRoute ??= new CupertinoPageRoute( + builder: builder, // Not used. + fullscreenDialog: fullscreenDialog, + ); + return _internalCupertinoPageRoute; + } @override final bool maintainState; @@ -86,23 +94,22 @@ class MaterialPageRoute extends PageRoute { @override bool canTransitionFrom(TransitionRoute nextRoute) { - return nextRoute is MaterialPageRoute; + return nextRoute is MaterialPageRoute || nextRoute is CupertinoPageRoute; } @override bool canTransitionTo(TransitionRoute nextRoute) { // Don't perform outgoing animation if the next route is a fullscreen dialog. - return nextRoute is MaterialPageRoute && !nextRoute.fullscreenDialog; + return (nextRoute is MaterialPageRoute && !nextRoute.fullscreenDialog) + || (nextRoute is CupertinoPageRoute && !nextRoute.fullscreenDialog); } @override void dispose() { - _backGestureController?.dispose(); + _internalCupertinoPageRoute?.dispose(); super.dispose(); } - CupertinoBackGestureController _backGestureController; - /// Support for dismissing this route with a horizontal swipe is enabled /// for [TargetPlatform.iOS]. If attempts to dismiss this route might be /// vetoed because a [WillPopCallback] was defined for the route then the @@ -110,35 +117,14 @@ class MaterialPageRoute extends PageRoute { /// /// See also: /// + /// * [CupertinoPageRoute] that backs the gesture for iOS. /// * [hasScopedWillPopCallback], which is true if a `willPop` callback /// is defined for this route. @override NavigationGestureController startPopGesture() { - // If attempts to dismiss this route might be vetoed, then do not - // allow the user to dismiss the route with a swipe. - if (hasScopedWillPopCallback) - return null; - // Fullscreen dialogs aren't dismissable by back swipe. - if (fullscreenDialog) - return null; - if (controller.status != AnimationStatus.completed) - return null; - assert(_backGestureController == null); - _backGestureController = new CupertinoBackGestureController( - navigator: navigator, - controller: controller, - ); - - controller.addStatusListener(_handleBackGestureEnded); - return _backGestureController; - } - - void _handleBackGestureEnded(AnimationStatus status) { - if (status == AnimationStatus.completed) { - _backGestureController?.dispose(); - _backGestureController = null; - controller.removeStatusListener(_handleBackGestureEnded); - } + return Theme.of(navigator.context).platform == TargetPlatform.iOS + ? _cupertinoPageRoute.startPopGestureForRoute(this) + : null; } @override @@ -159,20 +145,7 @@ class MaterialPageRoute extends PageRoute { @override Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { if (Theme.of(context).platform == TargetPlatform.iOS) { - if (fullscreenDialog) - return new CupertinoFullscreenDialogTransition( - animation: animation, - child: child, - ); - else - return new CupertinoPageTransition( - primaryRouteAnimation: animation, - secondaryRouteAnimation: secondaryAnimation, - child: child, - // In the middle of a back gesture drag, let the transition be linear to match finger - // motions. - linearTransition: _backGestureController != null, - ); + return _cupertinoPageRoute.buildTransitions(context, animation, secondaryAnimation, child); } else { return new _MountainViewPageTransition( routeAnimation: animation, diff --git a/packages/flutter/lib/src/material/paginated_data_table.dart b/packages/flutter/lib/src/material/paginated_data_table.dart index c2bee851eb299..1c260627abc36 100644 --- a/packages/flutter/lib/src/material/paginated_data_table.dart +++ b/packages/flutter/lib/src/material/paginated_data_table.dart @@ -73,21 +73,20 @@ class PaginatedDataTable extends StatefulWidget { this.availableRowsPerPage: const [defaultRowsPerPage, defaultRowsPerPage * 2, defaultRowsPerPage * 5, defaultRowsPerPage * 10], this.onRowsPerPageChanged, @required this.source - }) : super(key: key) { - assert(header != null); - assert(columns != null); - assert(columns.isNotEmpty); - assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)); - assert(sortAscending != null); - assert(rowsPerPage != null); - assert(rowsPerPage > 0); - assert(() { - if (onRowsPerPageChanged != null) - assert(availableRowsPerPage != null && availableRowsPerPage.contains(rowsPerPage)); - return true; - }); - assert(source != null); - } + }) : assert(header != null), + assert(columns != null), + assert(columns.isNotEmpty), + assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)), + assert(sortAscending != null), + assert(rowsPerPage != null), + assert(rowsPerPage > 0), + assert(() { + if (onRowsPerPageChanged != null) + assert(availableRowsPerPage != null && availableRowsPerPage.contains(rowsPerPage)); + return true; + }), + assert(source != null), + super(key: key); /// The table card's header. /// diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index 01b64d3fb387d..f0200708014f5 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -16,6 +16,10 @@ import 'list_tile.dart'; import 'material.dart'; import 'theme.dart'; +// Examples can assume: +// enum Commands { heroAndScholar, hurricaneCame } +// dynamic _heroAndScholar; + const Duration _kMenuDuration = const Duration(milliseconds: 300); const double _kBaselineOffsetFromBottom = 20.0; const double _kMenuCloseIntervalEnd = 2.0 / 3.0; @@ -133,7 +137,7 @@ class _PopupMenuDividerState extends State { /// const PopupMenuItem( /// value: WhyFarther.harder, /// child: const Text('Working a lot harder'), -/// ), +/// ) /// ``` /// /// See the example at [PopupMenuButton] for how this example could be used in a @@ -616,6 +620,8 @@ typedef List> PopupMenuItemBuilder(BuildContext context); /// the selected menu item. If child is null then a standard 'navigation/more_vert' /// icon is created. /// +/// ## Sample code +/// /// This example shows a menu with four items, selecting between an enum's /// values and setting a `_selection` field based on the selection. /// diff --git a/packages/flutter/lib/src/material/radio_list_tile.dart b/packages/flutter/lib/src/material/radio_list_tile.dart index 969d312283c01..b1a34844e67ab 100644 --- a/packages/flutter/lib/src/material/radio_list_tile.dart +++ b/packages/flutter/lib/src/material/radio_list_tile.dart @@ -34,6 +34,9 @@ import 'theme.dart'; /// [secondary] widget is placed on the opposite side. This maps to the /// [ListTile.leading] and [ListTile.trailing] properties of [ListTile]. /// +/// To show the [RadioListTile] as disabled, pass null as the [onChanged] +/// callback. +/// /// ## Sample code /// /// This widget shows a pair of radio buttons that control the `_character` diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index 72ab0f7a8dd65..72e72d46ecb36 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -721,18 +721,22 @@ class ScaffoldState extends State with TickerProviderStateMixin { NavigationGestureController _backGestureController; bool _shouldHandleBackGesture() { + assert(mounted); return Theme.of(context).platform == TargetPlatform.iOS && Navigator.canPop(context); } void _handleDragStart(DragStartDetails details) { + assert(mounted); _backGestureController = Navigator.of(context).startPopGesture(); } void _handleDragUpdate(DragUpdateDetails details) { + assert(mounted); _backGestureController?.dragUpdate(details.primaryDelta / context.size.width); } void _handleDragEnd(DragEndDetails details) { + assert(mounted); final bool willPop = _backGestureController?.dragEnd(details.velocity.pixelsPerSecond.dx / context.size.width) ?? false; if (willPop) _currentBottomSheet?.close(); @@ -740,6 +744,7 @@ class ScaffoldState extends State with TickerProviderStateMixin { } void _handleDragCancel() { + assert(mounted); final bool willPop = _backGestureController?.dragEnd(0.0) ?? false; if (willPop) _currentBottomSheet?.close(); @@ -867,6 +872,8 @@ class ScaffoldState extends State with TickerProviderStateMixin { child: new GestureDetector( behavior: HitTestBehavior.opaque, onTap: _handleStatusBarTap, + // iOS accessibility automatically adds scroll-to-top to the clock in the status bar + excludeFromSemantics: true, ) )); } diff --git a/packages/flutter/lib/src/material/scrollbar.dart b/packages/flutter/lib/src/material/scrollbar.dart index 6c0859f170027..748030761ddab 100644 --- a/packages/flutter/lib/src/material/scrollbar.dart +++ b/packages/flutter/lib/src/material/scrollbar.dart @@ -84,8 +84,8 @@ class _ScrollbarState extends State with TickerProviderStateMixin { } class _ScrollbarPainter extends ChangeNotifier implements CustomPainter { - _ScrollbarPainter(TickerProvider vsync) { - assert(vsync != null); + _ScrollbarPainter(TickerProvider vsync) + : assert(vsync != null) { _fadeController = new AnimationController(duration: _kThumbFadeDuration, vsync: vsync); _opacity = new CurvedAnimation(parent: _fadeController, curve: Curves.fastOutSlowIn) ..addListener(notifyListeners); diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart index e6a8a6826aede..213884cdd7bff 100644 --- a/packages/flutter/lib/src/material/slider.dart +++ b/packages/flutter/lib/src/material/slider.dart @@ -280,12 +280,12 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler { TextTheme textTheme, this.onChanged, TickerProvider vsync, - }) : _value = value, + }) : assert(value != null && value >= 0.0 && value <= 1.0), + _value = value, _divisions = divisions, _activeColor = activeColor, _thumbOpenAtMin = thumbOpenAtMin, _textTheme = textTheme { - assert(value != null && value >= 0.0 && value <= 1.0); this.label = label; final GestureArenaTeam team = new GestureArenaTeam(); _drag = new HorizontalDragGestureRecognizer() diff --git a/packages/flutter/lib/src/material/stepper.dart b/packages/flutter/lib/src/material/stepper.dart index c2e964c5f5f88..e83c47e91429d 100644 --- a/packages/flutter/lib/src/material/stepper.dart +++ b/packages/flutter/lib/src/material/stepper.dart @@ -140,12 +140,11 @@ class Stepper extends StatefulWidget { this.onStepTapped, this.onStepContinue, this.onStepCancel, - }) : super(key: key) { - assert(steps != null); - assert(type != null); - assert(currentStep != null); - assert(0 <= currentStep && currentStep < steps.length); - } + }) : assert(steps != null), + assert(type != null), + assert(currentStep != null), + assert(0 <= currentStep && currentStep < steps.length), + super(key: key); /// The steps of the stepper whose titles, subtitles, icons always get shown. /// diff --git a/packages/flutter/lib/src/material/switch_list_tile.dart b/packages/flutter/lib/src/material/switch_list_tile.dart index 1504f6d7a94a4..eb12eb61fe512 100644 --- a/packages/flutter/lib/src/material/switch_list_tile.dart +++ b/packages/flutter/lib/src/material/switch_list_tile.dart @@ -32,6 +32,9 @@ import 'theme.dart'; /// [ListTile.leading] slot. This cannot be changed; there is not sufficient /// space in a [ListTile]'s [ListTile.leading] slot for a [Switch]. /// +/// To show the [SwitchListTile] as disabled, pass null as the [onChanged] +/// callback. +/// /// ## Sample code /// /// This widget shows a switch that, when toggled, changes the state of a [bool] diff --git a/packages/flutter/lib/src/material/tab_controller.dart b/packages/flutter/lib/src/material/tab_controller.dart index beafdd72995e9..1c7b8eb10ebcf 100644 --- a/packages/flutter/lib/src/material/tab_controller.dart +++ b/packages/flutter/lib/src/material/tab_controller.dart @@ -14,10 +14,24 @@ import 'constants.dart'; /// The selected tab's index can be changed with [animateTo]. /// /// A stateful widget that builds a [TabBar] or a [TabBarView] can create -/// a TabController and share it directly. +/// a [TabController] and share it directly. +/// +/// When the [TabBar] and [TabBarView] don't have a convenient stateful +/// ancestor, a [TabController] can be shared with the [DefaultTabController] +/// inherited widget. +/// +/// ## Sample code +/// +/// This widget introduces a [Scaffold] with an [AppBar] and a [TabBar]. /// /// ```dart -/// class _MyDemoState extends State with SingleTickerProviderStateMixin { +/// class MyTabbedPage extends StatefulWidget { +/// const MyTabbedPage({ Key key }) : super(key: key); +/// @override +/// _MyTabbedPageState createState() => new _MyTabbedPageState(); +/// } +/// +/// class _MyTabbedPageState extends State with SingleTickerProviderStateMixin { /// final List myTabs = [ /// new Tab(text: 'LEFT'), /// new Tab(text: 'RIGHT'), @@ -56,23 +70,24 @@ import 'constants.dart'; /// } /// } /// ``` -/// -/// When the [TabBar] and [TabBarView] don't have a convenient stateful -/// ancestor, a TabController can be shared with the [DefaultTabController] -/// inherited widget. class TabController extends ChangeNotifier { /// Creates an object that manages the state required by [TabBar] and a [TabBarView]. + /// + /// The [length] cannot be null or negative. Typically its a value greater than one, i.e. + /// typically there are two or more tabs. + /// + /// The `initialIndex` must be valid given [length] and cannot be null. If [length] is + /// zero, then `initialIndex` must be 0 (the default). TabController({ int initialIndex: 0, @required this.length, @required TickerProvider vsync }) - : _index = initialIndex, + : assert(length != null && length >= 0), + assert(initialIndex != null && initialIndex >= 0 && (length == 0 || initialIndex < length)), + _index = initialIndex, _previousIndex = initialIndex, - _animationController = new AnimationController( + _animationController = length < 2 ? null : new AnimationController( value: initialIndex.toDouble(), upperBound: (length - 1).toDouble(), vsync: vsync - ) { - assert(length != null && length > 1); - assert(initialIndex != null && initialIndex >= 0 && initialIndex < length); - } + ); /// An animation whose value represents the current position of the [TabBar]'s /// selected tab indicator as well as the scrollOffsets of the [TabBar] @@ -82,18 +97,21 @@ class TabController extends ChangeNotifier { /// selected tab is changed, the animation's value equals [index]. The /// animation's value can be [offset] by +/- 1.0 to reflect [TabBarView] /// drag scrolling. - Animation get animation => _animationController.view; + /// + /// If length is zero or one, [index] animations don't happen and the value + /// of this property is [kAlwaysCompleteAnimation]. + Animation get animation => _animationController?.view ?? kAlwaysCompleteAnimation; final AnimationController _animationController; - /// The total number of tabs. Must be greater than one. + /// The total number of tabs. Typically greater than one. final int length; void _changeIndex(int value, { Duration duration, Curve curve }) { assert(value != null); - assert(value >= 0 && value < length); + assert(value >= 0 && (value < length || length == 0)); assert(duration == null ? curve == null : true); assert(_indexIsChangingCount >= 0); - if (value == _index) + if (value == _index || length < 2) return; _previousIndex = index; _index = value; @@ -119,6 +137,9 @@ class TabController extends ChangeNotifier { /// [indexIsChanging] to false, and notifies listeners. /// /// To change the currently selected tab and play the [animation] use [animateTo]. + /// + /// The value of [index] must be valid given [length]. If [length] is zero, + /// then [index] will also be zero. int get index => _index; int _index; set index(int value) { @@ -149,8 +170,9 @@ class TabController extends ChangeNotifier { /// drags left or right. A value between -1.0 and 0.0 implies that the /// TabBarView has been dragged to the left. Similarly a value between /// 0.0 and 1.0 implies that the TabBarView has been dragged to the right. - double get offset => _animationController.value - _index.toDouble(); + double get offset => length > 1 ? _animationController.value - _index.toDouble() : 0.0; set offset(double value) { + assert(length > 1); assert(value != null); assert(value >= -1.0 && value <= 1.0); assert(!indexIsChanging); @@ -161,7 +183,7 @@ class TabController extends ChangeNotifier { @override void dispose() { - _animationController.dispose(); + _animationController?.dispose(); super.dispose(); } } @@ -221,7 +243,7 @@ class _TabControllerScope extends InheritedWidget { class DefaultTabController extends StatefulWidget { /// Creates a default tab controller for the given [child] widget. /// - /// The [length] argument must be great than one. + /// The [length] argument is typically greater than one. /// /// The [initialIndex] argument must not be null. const DefaultTabController({ @@ -232,7 +254,7 @@ class DefaultTabController extends StatefulWidget { }) : assert(initialIndex != null), super(key: key); - /// The total number of tabs. Must be greater than one. + /// The total number of tabs. Typically greater than one. final int length; /// The initial index of the selected tab. diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index c6ed44bb6ea09..796ca9ebc4f3d 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:math' as math; import 'dart:ui' show lerpDouble; import 'package:flutter/foundation.dart'; @@ -20,10 +21,8 @@ import 'theme.dart'; const double _kTabHeight = 46.0; const double _kTextAndIconTabHeight = 72.0; -const double _kTabIndicatorHeight = 2.0; const double _kMinTabWidth = 72.0; const double _kMaxTabWidth = 264.0; -const EdgeInsets _kTabLabelPadding = const EdgeInsets.symmetric(horizontal: 12.0); /// A material design [TabBar] tab. If both [icon] and [text] are /// provided, the text is displayed below the icon. @@ -82,7 +81,7 @@ class Tab extends StatelessWidget { } return new Container( - padding: _kTabLabelPadding, + padding: kTabLabelPadding, height: height, constraints: const BoxConstraints(minWidth: _kMinTabWidth), child: new Center(child: label), @@ -155,16 +154,15 @@ class _TabLabelBarRenderer extends RenderFlex { CrossAxisAlignment crossAxisAlignment, TextBaseline textBaseline, @required this.onPerformLayout, - }) : super( - children: children, - direction: direction, - mainAxisSize: mainAxisSize, - mainAxisAlignment: mainAxisAlignment, - crossAxisAlignment: crossAxisAlignment, - textBaseline: textBaseline, - ) { - assert(onPerformLayout != null); - } + }) : assert(onPerformLayout != null), + super( + children: children, + direction: direction, + mainAxisSize: mainAxisSize, + mainAxisAlignment: mainAxisAlignment, + crossAxisAlignment: crossAxisAlignment, + textBaseline: textBaseline, + ); ValueChanged> onPerformLayout; @@ -239,30 +237,39 @@ double _indexChangeProgress(TabController controller) { } class _IndicatorPainter extends CustomPainter { - _IndicatorPainter(this.controller) : super(repaint: controller.animation); + _IndicatorPainter({ + this.controller, + this.indicatorWeight, + this.indicatorPadding, + List initialTabOffsets, + }) : _tabOffsets = initialTabOffsets, super(repaint: controller.animation); - TabController controller; - List tabOffsets; - Color color; - Rect currentRect; + final TabController controller; + final double indicatorWeight; + final EdgeInsets indicatorPadding; + List _tabOffsets; + Color _color; + Rect _currentRect; - // tabOffsets[index] is the offset of the left edge of the tab at index, and - // tabOffsets[tabOffsets.length] is the right edge of the last tab. - int get maxTabIndex => tabOffsets.length - 2; + // _tabOffsets[index] is the offset of the left edge of the tab at index, and + // _tabOffsets[_tabOffsets.length] is the right edge of the last tab. + int get maxTabIndex => _tabOffsets.length - 2; Rect indicatorRect(Size tabBarSize, int tabIndex) { - assert(tabOffsets != null && tabIndex >= 0 && tabIndex <= maxTabIndex); - final double tabLeft = tabOffsets[tabIndex]; - final double tabRight = tabOffsets[tabIndex + 1]; - final double tabTop = tabBarSize.height - _kTabIndicatorHeight; - return new Rect.fromLTWH(tabLeft, tabTop, tabRight - tabLeft, _kTabIndicatorHeight); + assert(_tabOffsets != null && tabIndex >= 0 && tabIndex <= maxTabIndex); + double tabLeft = _tabOffsets[tabIndex]; + double tabRight = _tabOffsets[tabIndex + 1]; + tabLeft = math.min(tabLeft + indicatorPadding.left, tabRight); + tabRight = math.max(tabRight - indicatorPadding.right, tabLeft); + final double tabTop = tabBarSize.height - indicatorWeight; + return new Rect.fromLTWH(tabLeft, tabTop, tabRight - tabLeft, indicatorWeight); } @override void paint(Canvas canvas, Size size) { if (controller.indexIsChanging) { final Rect targetRect = indicatorRect(size, controller.index); - currentRect = Rect.lerp(targetRect, currentRect ?? targetRect, _indexChangeProgress(controller)); + _currentRect = Rect.lerp(targetRect, _currentRect ?? targetRect, _indexChangeProgress(controller)); } else { final int currentIndex = controller.index; final Rect left = currentIndex > 0 ? indicatorRect(size, currentIndex - 1) : null; @@ -272,21 +279,21 @@ class _IndicatorPainter extends CustomPainter { final double index = controller.index.toDouble(); final double value = controller.animation.value; if (value == index - 1.0) - currentRect = left ?? middle; + _currentRect = left ?? middle; else if (value == index + 1.0) - currentRect = right ?? middle; + _currentRect = right ?? middle; else if (value == index) - currentRect = middle; + _currentRect = middle; else if (value < index) - currentRect = left == null ? middle : Rect.lerp(middle, left, index - value); + _currentRect = left == null ? middle : Rect.lerp(middle, left, index - value); else - currentRect = right == null ? middle : Rect.lerp(middle, right, value - index); + _currentRect = right == null ? middle : Rect.lerp(middle, right, value - index); } - assert(currentRect != null); - canvas.drawRect(currentRect, new Paint()..color = color); + assert(_currentRect != null); + canvas.drawRect(_currentRect, new Paint()..color = _color); } - static bool tabOffsetsNotEqual(List a, List b) { + static bool _tabOffsetsNotEqual(List a, List b) { assert(a != null && b != null && a.length == b.length); for(int i = 0; i < a.length; i++) { if (a[i] != b[i]) @@ -298,9 +305,9 @@ class _IndicatorPainter extends CustomPainter { @override bool shouldRepaint(_IndicatorPainter old) { return controller != old.controller || - tabOffsets?.length != old.tabOffsets?.length || - tabOffsetsNotEqual(tabOffsets, old.tabOffsets) || - currentRect != old.currentRect; + _tabOffsets?.length != old._tabOffsets?.length || + _tabOffsetsNotEqual(_tabOffsets, old._tabOffsets) || + _currentRect != old._currentRect; } } @@ -382,41 +389,51 @@ class _TabBarScrollController extends ScrollController { /// A material design widget that displays a horizontal row of tabs. /// -/// Typically created as part of an [AppBar] and in conjuction with a -/// [TabBarView]. +/// Typically created as the [AppBar.bottom] part of an [AppBar] and in +/// conjuction with a [TabBarView]. /// /// If a [TabController] is not provided, then there must be a -/// [DefaultTabController] ancestor. +/// [DefaultTabController] ancestor. The tab controller's [TabController.length] +/// must equal the length of the [tabs] list. /// /// Requires one of its ancestors to be a [Material] widget. /// /// See also: /// -/// * [TabBarView], which displays the contents that the tab bar is selecting -/// between. +/// * [TabBarView], which displays page views that correspond to each tab. class TabBar extends StatefulWidget implements PreferredSizeWidget { /// Creates a material design tab bar. /// - /// The [tabs] argument must not be null and must have more than one widget. + /// The [tabs] argument cannot be null and its length must match the [controller]'s + /// [TabController.length]. /// /// If a [TabController] is not provided, then there must be a /// [DefaultTabController] ancestor. + /// + /// The [indicatorWeight] parameter defaults to 2, and cannot be null. + /// + /// The [indicatorPadding] parameter defaults to [EdgeInsets.zero], and cannot be null. TabBar({ Key key, @required this.tabs, this.controller, this.isScrollable: false, this.indicatorColor, + this.indicatorWeight: 2.0, + this.indicatorPadding: EdgeInsets.zero, this.labelColor, this.labelStyle, this.unselectedLabelColor, this.unselectedLabelStyle, - }) : super(key: key) { - assert(tabs != null && tabs.length > 1); - assert(isScrollable != null); - } + }) : assert(tabs != null), + assert(isScrollable != null), + assert(indicatorWeight != null && indicatorWeight > 0.0), + assert(indicatorPadding != null), + super(key: key); - /// Typically a list of [Tab] widgets. + /// Typically a list of two or more [Tab] widgets. + /// + /// The length of this list must match the [controller]'s [TabController.length]. final List tabs; /// This widget's selection and animation state. @@ -436,6 +453,20 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { /// is null then the value of the Theme's indicatorColor property is used. final Color indicatorColor; + /// The thickness of the line that appears below the selected tab. The value + /// of this parameter must be greater than zero. + /// + /// The default value of [indicatorWeight] is 2.0. + final double indicatorWeight; + + /// The horizontal padding for the line that appears below the selected tab. + /// For [isScrollable] tab bars, specifying [kDefaultTabLabelPadding] will align + /// the indicator with the tab's text for [Tab] widgets and all but the + /// shortest [Tab.text] values. + /// + /// The default value of [indicatorPadding] is [EdgeInsets.zero]. + final EdgeInsets indicatorPadding; + /// The color of selected tab labels. /// /// Unselected tab labels are rendered with the same color rendered at 70% @@ -474,10 +505,10 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { if (item is Tab) { final Tab tab = item; if (tab.text != null && tab.icon != null) - return const Size.fromHeight(_kTextAndIconTabHeight + _kTabIndicatorHeight); + return new Size.fromHeight(_kTextAndIconTabHeight + indicatorWeight); } } - return const Size.fromHeight(_kTabHeight + _kTabIndicatorHeight); + return new Size.fromHeight(_kTabHeight + indicatorWeight); } @override @@ -517,8 +548,13 @@ class _TabBarState extends State { _controller.animation.addListener(_handleTabControllerAnimationTick); _controller.addListener(_handleTabControllerTick); _currentIndex = _controller.index; - final List offsets = _indicatorPainter?.tabOffsets; - _indicatorPainter = new _IndicatorPainter(_controller)..tabOffsets = offsets; + final List offsets = _indicatorPainter?._tabOffsets; + _indicatorPainter = new _IndicatorPainter( + controller: _controller, + indicatorWeight: widget.indicatorWeight, + indicatorPadding: widget.indicatorPadding, + initialTabOffsets: offsets, + ); } } @@ -545,14 +581,14 @@ class _TabBarState extends State { super.dispose(); } - // tabOffsets[index] is the offset of the left edge of the tab at index, and - // tabOffsets[tabOffsets.length] is the right edge of the last tab. - int get maxTabIndex => _indicatorPainter.tabOffsets.length - 2; + // _tabOffsets[index] is the offset of the left edge of the tab at index, and + // _tabOffsets[_tabOffsets.length] is the right edge of the last tab. + int get maxTabIndex => _indicatorPainter._tabOffsets.length - 2; double _tabScrollOffset(int index, double viewportWidth, double minExtent, double maxExtent) { if (!widget.isScrollable) return 0.0; - final List tabOffsets = _indicatorPainter.tabOffsets; + final List tabOffsets = _indicatorPainter._tabOffsets; assert(tabOffsets != null && index >= 0 && index <= maxTabIndex); final double tabCenter = (tabOffsets[index] + tabOffsets[index + 1]) / 2.0; return (tabCenter - viewportWidth / 2.0).clamp(minExtent, maxExtent); @@ -612,7 +648,7 @@ class _TabBarState extends State { // Called each time layout completes. void _saveTabOffsets(List tabOffsets) { - _indicatorPainter?.tabOffsets = tabOffsets; + _indicatorPainter?._tabOffsets = tabOffsets; } void _handleTap(int index) { @@ -634,14 +670,20 @@ class _TabBarState extends State { @override Widget build(BuildContext context) { + if (_controller.length == 0) { + return new Container( + height: _kTabHeight + widget.indicatorWeight, + ); + } + final List wrappedTabs = new List.from(widget.tabs, growable: false); // If the controller was provided by DefaultTabController and we're part // of a Hero (typically the AppBar), then we will not be able to find the // controller during a Hero transition. See https://github.com/flutter/flutter/issues/213. if (_controller != null) { - _indicatorPainter.color = widget.indicatorColor ?? Theme.of(context).indicatorColor; - if (_indicatorPainter.color == Material.of(context).color) { + _indicatorPainter._color = widget.indicatorColor ?? Theme.of(context).indicatorColor; + if (_indicatorPainter._color == Material.of(context).color) { // ThemeData tries to avoid this by having indicatorColor avoid being the // primaryColor. However, it's possible that the tab bar is on a // Material that isn't the primaryColor. In that case, if the indicator @@ -649,7 +691,7 @@ class _TabBarState extends State { // automatic transitions of the theme will likely look ugly as the // indicator color suddenly snaps to white at one end, but it's not clear // how to avoid that any further. - _indicatorPainter.color = Colors.white; + _indicatorPainter._color = Colors.white; } if (_controller.index != _currentIndex) { @@ -687,10 +729,22 @@ class _TabBarState extends State { // Add the tap handler to each tab. If the tab bar is scrollable // then give all of the tabs equal flexibility so that their widths // reflect the intrinsic width of their labels. - for (int index = 0; index < widget.tabs.length; index++) { - wrappedTabs[index] = new InkWell( - onTap: () { _handleTap(index); }, - child: wrappedTabs[index], + final int tabCount = widget.tabs.length; + for (int index = 0; index < tabCount; index++) { + wrappedTabs[index] = new MergeSemantics( + child: new Stack( + children: [ + new InkWell( + onTap: () { _handleTap(index); }, + child: wrappedTabs[index], + ), + new Semantics( + selected: index == _currentIndex, + // TODO(goderbauer): I10N-ify + label: 'Tab ${index + 1} of $tabCount', + ), + ], + ), ); if (!widget.isScrollable) wrappedTabs[index] = new Expanded(child: wrappedTabs[index]); @@ -699,7 +753,7 @@ class _TabBarState extends State { Widget tabBar = new CustomPaint( painter: _indicatorPainter, child: new Padding( - padding: const EdgeInsets.only(bottom: _kTabIndicatorHeight), + padding: new EdgeInsets.only(bottom: widget.indicatorWeight), child: new _TabStyle( animation: kAlwaysDismissedAnimation, selected: false, @@ -741,9 +795,7 @@ class TabBarView extends StatefulWidget { Key key, @required this.children, this.controller, - }) : super(key: key) { - assert(children != null && children.length > 1); - } + }) : assert(children != null), super(key: key); /// This widget's selection and animation state. /// @@ -909,12 +961,19 @@ class _TabBarViewState extends State { } } -/// Displays a single 12x12 circle with the specified border and background colors. +/// Displays a single circle with the specified border and background colors. /// /// Used by [TabPageSelector] to indicate the selected page. class TabPageSelectorIndicator extends StatelessWidget { /// Creates an indicator used by [TabPageSelector]. - const TabPageSelectorIndicator({ Key key, this.backgroundColor, this.borderColor }) : super(key: key); + /// + /// The [backgroundColor], [borderColor], and [size] parameters cannot be null. + const TabPageSelectorIndicator({ + Key key, + @required this.backgroundColor, + @required this.borderColor, + @required this.size, + }) : assert(backgroundColor != null), assert(borderColor != null), assert(size != null), super(key: key); /// The indicator circle's background color. final Color backgroundColor; @@ -922,11 +981,14 @@ class TabPageSelectorIndicator extends StatelessWidget { /// The indicator circle's border color. final Color borderColor; + /// The indicator circle's diameter. + final double size; + @override Widget build(BuildContext context) { return new Container( - width: 12.0, - height: 12.0, + width: size, + height: size, margin: const EdgeInsets.all(4.0), decoration: new BoxDecoration( color: backgroundColor, @@ -944,7 +1006,13 @@ class TabPageSelectorIndicator extends StatelessWidget { /// ancestor. class TabPageSelector extends StatelessWidget { /// Creates a compact widget that indicates which tab has been selected. - const TabPageSelector({ Key key, this.controller }) : super(key: key); + const TabPageSelector({ + Key key, + this.controller, + this.indicatorSize: 12.0, + this.color, + this.selectedColor, + }) : assert(indicatorSize != null && indicatorSize > 0.0), super(key: key); /// This widget's selection and animation state. /// @@ -952,47 +1020,64 @@ class TabPageSelector extends StatelessWidget { /// will be used. final TabController controller; + /// The indicator circle's diameter (the default value is 12.0). + final double indicatorSize; + + /// The indicator cicle's fill color for unselected pages. + /// + /// If this parameter is null then the indicator is filled with [Colors.transparent]. + final Color color; + + /// The indicator cicle's fill color for selected pages and border color + /// for all indicator circles. + /// + /// If this parameter is null then the indicator is filled with the theme's + /// accent color, [ThemeData.accentColor]. + final Color selectedColor; + Widget _buildTabIndicator( int tabIndex, TabController tabController, - ColorTween selectedColor, - ColorTween previousColor, + ColorTween selectedColorTween, + ColorTween previousColorTween, ) { Color background; if (tabController.indexIsChanging) { // The selection's animation is animating from previousValue to value. final double t = 1.0 - _indexChangeProgress(tabController); if (tabController.index == tabIndex) - background = selectedColor.lerp(t); + background = selectedColorTween.lerp(t); else if (tabController.previousIndex == tabIndex) - background = previousColor.lerp(t); + background = previousColorTween.lerp(t); else - background = selectedColor.begin; + background = selectedColorTween.begin; } else { // The selection's offset reflects how far the TabBarView has /// been dragged to the left (-1.0 to 0.0) or the right (0.0 to 1.0). final double offset = tabController.offset; if (tabController.index == tabIndex) { - background = selectedColor.lerp(1.0 - offset.abs()); + background = selectedColorTween.lerp(1.0 - offset.abs()); } else if (tabController.index == tabIndex - 1 && offset > 0.0) { - background = selectedColor.lerp(offset); + background = selectedColorTween.lerp(offset); } else if (tabController.index == tabIndex + 1 && offset < 0.0) { - background = selectedColor.lerp(-offset); + background = selectedColorTween.lerp(-offset); } else { - background = selectedColor.begin; + background = selectedColorTween.begin; } } return new TabPageSelectorIndicator( backgroundColor: background, - borderColor: selectedColor.end, + borderColor: selectedColorTween.end, + size: indicatorSize, ); } @override Widget build(BuildContext context) { - final Color color = Theme.of(context).accentColor; - final ColorTween selectedColor = new ColorTween(begin: Colors.transparent, end: color); - final ColorTween previousColor = new ColorTween(begin: color, end: Colors.transparent); + final Color fixColor = color ?? Colors.transparent; + final Color fixSelectedColor = selectedColor ?? Theme.of(context).accentColor; + final ColorTween selectedColorTween = new ColorTween(begin: fixColor, end: fixSelectedColor); + final ColorTween previousColorTween = new ColorTween(begin: fixSelectedColor, end: fixColor); final TabController tabController = controller ?? DefaultTabController.of(context); assert(() { if (tabController == null) { @@ -1018,7 +1103,7 @@ class TabPageSelector extends StatelessWidget { child: new Row( mainAxisSize: MainAxisSize.min, children: new List.generate(tabController.length, (int tabIndex) { - return _buildTabIndicator(tabIndex, tabController, selectedColor, previousColor); + return _buildTabIndicator(tabIndex, tabController, selectedColorTween, previousColorTween); }).toList(), ), ); diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index e9a7bc8090260..46fb6fe34c0cc 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -62,6 +62,12 @@ class TextField extends StatefulWidget { /// To remove the decoration entirely (including the extra padding introduced /// by the decoration to save space for the labels), set the [decoration] to /// null. + /// + /// The [maxLines] property can be set to null to remove the restriction on + /// the number of lines. By default, it is 1, meaning this is a single-line + /// text field. If it is not null, it must be greater than zero. + /// + /// The [keyboardType], [autofocus], and [obscureText] arguments must not be null. const TextField({ Key key, this.controller, @@ -76,7 +82,11 @@ class TextField extends StatefulWidget { this.onChanged, this.onSubmitted, this.inputFormatters, - }) : super(key: key); + }) : assert(keyboardType != null), + assert(autofocus != null), + assert(obscureText != null), + assert(maxLines == null || maxLines > 0), + super(key: key); /// Controls the text being edited. /// @@ -98,6 +108,8 @@ class TextField extends StatefulWidget { final InputDecoration decoration; /// The type of keyboard to use for editing the text. + /// + /// Defaults to [TextInputType.text]. Cannot be null. final TextInputType keyboardType; /// The style to use for the text being edited. @@ -116,7 +128,7 @@ class TextField extends StatefulWidget { /// If true, the keyboard will open as soon as this text field obtains focus. /// Otherwise, the keyboard is only shown after the user taps the text field. /// - /// Defaults to false. + /// Defaults to false. Cannot be null. // See https://github.com/flutter/flutter/issues/7035 for the rationale for this // keyboard behavior. final bool autofocus; @@ -126,13 +138,16 @@ class TextField extends StatefulWidget { /// When this is set to true, all the characters in the text field are /// replaced by U+2022 BULLET characters (•). /// - /// Defaults to false. + /// Defaults to false. Cannot be null. final bool obscureText; /// The maximum number of lines for the text to span, wrapping if necessary. /// /// If this is 1 (the default), the text will not wrap, but will scroll /// horizontally instead. + /// + /// If this is null, there is no limit to the number of lines. If it is not + /// null, the value must be greater than zero. final int maxLines; /// Called when the text being edited changes. diff --git a/packages/flutter/lib/src/material/text_form_field.dart b/packages/flutter/lib/src/material/text_form_field.dart index 8ef46d915f59f..49726958c6b7d 100644 --- a/packages/flutter/lib/src/material/text_form_field.dart +++ b/packages/flutter/lib/src/material/text_form_field.dart @@ -30,7 +30,8 @@ import 'text_field.dart'; class TextFormField extends FormField { /// Creates a [FormField] that contains a [TextField]. /// - /// For a documentation about the various parameters, see [TextField]. + /// For documentation about the various parameters, see the [TextField] class + /// and [new TextField], the constructor. TextFormField({ Key key, TextEditingController controller, @@ -44,7 +45,11 @@ class TextFormField extends FormField { FormFieldSetter onSaved, FormFieldValidator validator, List inputFormatters, - }) : super( + }) : assert(keyboardType != null), + assert(autofocus != null), + assert(obscureText != null), + assert(maxLines == null || maxLines > 0), + super( key: key, initialValue: controller != null ? controller.value.text : '', onSaved: onSaved, diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index cc134fbb1c83b..591e9edcaf93a 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -43,32 +43,6 @@ const Color _kLightThemeSplashColor = const Color(0x66C8C8C8); const Color _kDarkThemeHighlightColor = const Color(0x40CCCCCC); const Color _kDarkThemeSplashColor = const Color(0x40CCCCCC); -// See -double _linearizeColorComponent(double component) { - if (component <= 0.03928) - return component / 12.92; - return math.pow((component + 0.055) / 1.055, 2.4); -} - -Brightness _estimateBrightnessForColor(Color color) { - // See - final double R = _linearizeColorComponent(color.red / 0xFF); - final double G = _linearizeColorComponent(color.green / 0xFF); - final double B = _linearizeColorComponent(color.blue / 0xFF); - final double L = 0.2126 * R + 0.7152 * G + 0.0722 * B; - - // See - // The spec says to use kThreshold=0.0525, but Material Design appears to bias - // more towards using light text than WCAG20 recommends. Material Design spec - // doesn't say what value to use, but 0.15 seemed close to what the Material - // Design spec shows for its color palette on - // . - const double kThreshold = 0.15; - if ((L + 0.05) * (L + 0.05) > kThreshold ) - return Brightness.light; - return Brightness.dark; -} - /// Holds the color and typography values for a material design theme. /// /// Use this class to configure a [Theme] widget. @@ -134,10 +108,10 @@ class ThemeData { final bool isDark = brightness == Brightness.dark; primarySwatch ??= Colors.blue; primaryColor ??= isDark ? Colors.grey[900] : primarySwatch[500]; - primaryColorBrightness ??= _estimateBrightnessForColor(primaryColor); + primaryColorBrightness ??= estimateBrightnessForColor(primaryColor); final bool primaryIsDark = primaryColorBrightness == Brightness.dark; accentColor ??= isDark ? Colors.tealAccent[200] : primarySwatch[500]; - accentColorBrightness ??= _estimateBrightnessForColor(accentColor); + accentColorBrightness ??= estimateBrightnessForColor(accentColor); final bool accentIsDark = accentColorBrightness == Brightness.dark; canvasColor ??= isDark ? Colors.grey[850] : Colors.grey[50]; scaffoldBackgroundColor ??= canvasColor; @@ -467,6 +441,37 @@ class ThemeData { ); } + // See + static double _linearizeColorComponent(double component) { + if (component <= 0.03928) + return component / 12.92; + return math.pow((component + 0.055) / 1.055, 2.4); + } + + /// Determines whether the given [Color] is [Brightness.light] or + /// [Brightness.dark]. + /// + /// This compares the luminosity of the given color to a threshold value that + /// matches the material design specification. + static Brightness estimateBrightnessForColor(Color color) { + // See + final double R = _linearizeColorComponent(color.red / 0xFF); + final double G = _linearizeColorComponent(color.green / 0xFF); + final double B = _linearizeColorComponent(color.blue / 0xFF); + final double L = 0.2126 * R + 0.7152 * G + 0.0722 * B; + + // See + // The spec says to use kThreshold=0.0525, but Material Design appears to bias + // more towards using light text than WCAG20 recommends. Material Design spec + // doesn't say what value to use, but 0.15 seemed close to what the Material + // Design spec shows for its color palette on + // . + const double kThreshold = 0.15; + if ((L + 0.05) * (L + 0.05) > kThreshold ) + return Brightness.light; + return Brightness.dark; + } + /// Linearly interpolate between two themes. /// /// The arguments must not be null. diff --git a/packages/flutter/lib/src/material/toggleable.dart b/packages/flutter/lib/src/material/toggleable.dart index 08f17461693a2..6d65025eebf2d 100644 --- a/packages/flutter/lib/src/material/toggleable.dart +++ b/packages/flutter/lib/src/material/toggleable.dart @@ -30,16 +30,16 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic @required Color inactiveColor, ValueChanged onChanged, @required TickerProvider vsync, - }) : _value = value, + }) : assert(value != null), + assert(activeColor != null), + assert(inactiveColor != null), + assert(vsync != null), + _value = value, _activeColor = activeColor, _inactiveColor = inactiveColor, _onChanged = onChanged, _vsync = vsync, super(additionalConstraints: new BoxConstraints.tight(size)) { - assert(value != null); - assert(activeColor != null); - assert(inactiveColor != null); - assert(vsync != null); _tap = new TapGestureRecognizer() ..onTapDown = _handleTapDown ..onTap = _handleTap diff --git a/packages/flutter/lib/src/material/typography.dart b/packages/flutter/lib/src/material/typography.dart index 92e4bcad0f824..eb388a187dff2 100644 --- a/packages/flutter/lib/src/material/typography.dart +++ b/packages/flutter/lib/src/material/typography.dart @@ -9,7 +9,7 @@ import 'colors.dart'; // TODO(eseidel): Font weights are supposed to be language relative. // TODO(jackson): Baseline should be language relative. -// These values are for English-like text. +// TODO(ianh): These values are for English-like text. /// Material design text theme. /// @@ -20,11 +20,21 @@ import 'colors.dart'; /// To obtain the current text theme, call [Theme.of] with the current /// [BuildContext] and read the [ThemeData.textTheme] property. /// +/// The following image [from the material design +/// specification](https://material.io/guidelines/style/typography.html#typography-styles) +/// shows the recommended styles for each of the properties of a [TextTheme]. +/// This image uses the `Roboto` font, which is the font used on Android. On +/// iOS, the [San Francisco +/// font](https://developer.apple.com/ios/human-interface-guidelines/visual-design/typography/) +/// is automatically used instead. +/// +/// ![To see the image, visit the typography site referenced below.](https://storage.googleapis.com/material-design/publish/material_v_11/assets/0Bzhp5Z4wHba3alhXZ2pPWGk3Zjg/style_typography_styles_scale.png) +/// /// See also: /// -/// * [Typography] -/// * [Theme] -/// * [ThemeData] +/// * [Typography], the class that generates [TextTheme]s appropriate for a platform. +/// * [Theme], for other aspects of a material design application that can be +/// globally adjusted, such as the color scheme. /// * @immutable class TextTheme { @@ -49,34 +59,34 @@ class TextTheme { this.body2, this.body1, this.caption, - this.button + this.button, }); const TextTheme._blackMountainView() - : display4 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 112.0, fontWeight: FontWeight.w100, color: Colors.black54, textBaseline: TextBaseline.alphabetic), - display3 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 56.0, fontWeight: FontWeight.w400, color: Colors.black54, textBaseline: TextBaseline.alphabetic), - display2 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 45.0, fontWeight: FontWeight.w400, color: Colors.black54, textBaseline: TextBaseline.alphabetic), - display1 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 34.0, fontWeight: FontWeight.w400, color: Colors.black54, textBaseline: TextBaseline.alphabetic), - headline = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 24.0, fontWeight: FontWeight.w400, color: Colors.black87, textBaseline: TextBaseline.alphabetic), - title = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 20.0, fontWeight: FontWeight.w500, color: Colors.black87, textBaseline: TextBaseline.alphabetic), - subhead = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 16.0, fontWeight: FontWeight.w400, color: Colors.black87, textBaseline: TextBaseline.alphabetic), - body2 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.black87, textBaseline: TextBaseline.alphabetic), - body1 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w400, color: Colors.black87, textBaseline: TextBaseline.alphabetic), - caption = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 12.0, fontWeight: FontWeight.w400, color: Colors.black54, textBaseline: TextBaseline.alphabetic), - button = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.black87, textBaseline: TextBaseline.alphabetic); + : display4 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 112.0, fontWeight: FontWeight.w100, color: Colors.black54, textBaseline: TextBaseline.alphabetic), + display3 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 56.0, fontWeight: FontWeight.w400, color: Colors.black54, textBaseline: TextBaseline.alphabetic), + display2 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 45.0, fontWeight: FontWeight.w400, color: Colors.black54, textBaseline: TextBaseline.alphabetic), + display1 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 34.0, fontWeight: FontWeight.w400, color: Colors.black54, textBaseline: TextBaseline.alphabetic), + headline = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 24.0, fontWeight: FontWeight.w400, color: Colors.black87, textBaseline: TextBaseline.alphabetic), + title = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 20.0, fontWeight: FontWeight.w500, color: Colors.black87, textBaseline: TextBaseline.alphabetic), + subhead = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 16.0, fontWeight: FontWeight.w400, color: Colors.black87, textBaseline: TextBaseline.alphabetic), + body2 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.black87, textBaseline: TextBaseline.alphabetic), + body1 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w400, color: Colors.black87, textBaseline: TextBaseline.alphabetic), + caption = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 12.0, fontWeight: FontWeight.w400, color: Colors.black54, textBaseline: TextBaseline.alphabetic), + button = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.black87, textBaseline: TextBaseline.alphabetic); const TextTheme._whiteMountainView() - : display4 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 112.0, fontWeight: FontWeight.w100, color: Colors.white70, textBaseline: TextBaseline.alphabetic), - display3 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 56.0, fontWeight: FontWeight.w400, color: Colors.white70, textBaseline: TextBaseline.alphabetic), - display2 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 45.0, fontWeight: FontWeight.w400, color: Colors.white70, textBaseline: TextBaseline.alphabetic), - display1 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 34.0, fontWeight: FontWeight.w400, color: Colors.white70, textBaseline: TextBaseline.alphabetic), - headline = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 24.0, fontWeight: FontWeight.w400, color: Colors.white, textBaseline: TextBaseline.alphabetic), - title = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 20.0, fontWeight: FontWeight.w500, color: Colors.white, textBaseline: TextBaseline.alphabetic), - subhead = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 16.0, fontWeight: FontWeight.w400, color: Colors.white, textBaseline: TextBaseline.alphabetic), - body2 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.white, textBaseline: TextBaseline.alphabetic), - body1 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w400, color: Colors.white, textBaseline: TextBaseline.alphabetic), - caption = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 12.0, fontWeight: FontWeight.w400, color: Colors.white70, textBaseline: TextBaseline.alphabetic), - button = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.white, textBaseline: TextBaseline.alphabetic); + : display4 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 112.0, fontWeight: FontWeight.w100, color: Colors.white70, textBaseline: TextBaseline.alphabetic), + display3 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 56.0, fontWeight: FontWeight.w400, color: Colors.white70, textBaseline: TextBaseline.alphabetic), + display2 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 45.0, fontWeight: FontWeight.w400, color: Colors.white70, textBaseline: TextBaseline.alphabetic), + display1 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 34.0, fontWeight: FontWeight.w400, color: Colors.white70, textBaseline: TextBaseline.alphabetic), + headline = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 24.0, fontWeight: FontWeight.w400, color: Colors.white, textBaseline: TextBaseline.alphabetic), + title = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 20.0, fontWeight: FontWeight.w500, color: Colors.white, textBaseline: TextBaseline.alphabetic), + subhead = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 16.0, fontWeight: FontWeight.w400, color: Colors.white, textBaseline: TextBaseline.alphabetic), + body2 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.white, textBaseline: TextBaseline.alphabetic), + body1 = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w400, color: Colors.white, textBaseline: TextBaseline.alphabetic), + caption = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 12.0, fontWeight: FontWeight.w400, color: Colors.white70, textBaseline: TextBaseline.alphabetic), + button = const TextStyle(fontFamily: 'Roboto', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.white, textBaseline: TextBaseline.alphabetic); const TextTheme._blackCupertino() : display4 = const TextStyle(fontFamily: '.SF UI Display', inherit: false, fontSize: 112.0, fontWeight: FontWeight.w100, color: Colors.black54, textBaseline: TextBaseline.alphabetic), @@ -85,11 +95,11 @@ class TextTheme { display1 = const TextStyle(fontFamily: '.SF UI Display', inherit: false, fontSize: 34.0, fontWeight: FontWeight.w400, color: Colors.black54, textBaseline: TextBaseline.alphabetic), headline = const TextStyle(fontFamily: '.SF UI Display', inherit: false, fontSize: 24.0, fontWeight: FontWeight.w400, color: Colors.black87, textBaseline: TextBaseline.alphabetic), title = const TextStyle(fontFamily: '.SF UI Display', inherit: false, fontSize: 20.0, fontWeight: FontWeight.w500, color: Colors.black87, textBaseline: TextBaseline.alphabetic), - subhead = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 16.0, fontWeight: FontWeight.w400, color: Colors.black87, textBaseline: TextBaseline.alphabetic), - body2 = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.black87, textBaseline: TextBaseline.alphabetic), - body1 = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w400, color: Colors.black87, textBaseline: TextBaseline.alphabetic), - caption = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 12.0, fontWeight: FontWeight.w400, color: Colors.black54, textBaseline: TextBaseline.alphabetic), - button = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.black87, textBaseline: TextBaseline.alphabetic); + subhead = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 16.0, fontWeight: FontWeight.w400, color: Colors.black87, textBaseline: TextBaseline.alphabetic), + body2 = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.black87, textBaseline: TextBaseline.alphabetic), + body1 = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w400, color: Colors.black87, textBaseline: TextBaseline.alphabetic), + caption = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 12.0, fontWeight: FontWeight.w400, color: Colors.black54, textBaseline: TextBaseline.alphabetic), + button = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.black87, textBaseline: TextBaseline.alphabetic); const TextTheme._whiteCupertino() : display4 = const TextStyle(fontFamily: '.SF UI Display', inherit: false, fontSize: 112.0, fontWeight: FontWeight.w100, color: Colors.white70, textBaseline: TextBaseline.alphabetic), @@ -98,11 +108,11 @@ class TextTheme { display1 = const TextStyle(fontFamily: '.SF UI Display', inherit: false, fontSize: 34.0, fontWeight: FontWeight.w400, color: Colors.white70, textBaseline: TextBaseline.alphabetic), headline = const TextStyle(fontFamily: '.SF UI Display', inherit: false, fontSize: 24.0, fontWeight: FontWeight.w400, color: Colors.white, textBaseline: TextBaseline.alphabetic), title = const TextStyle(fontFamily: '.SF UI Display', inherit: false, fontSize: 20.0, fontWeight: FontWeight.w500, color: Colors.white, textBaseline: TextBaseline.alphabetic), - subhead = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 16.0, fontWeight: FontWeight.w400, color: Colors.white, textBaseline: TextBaseline.alphabetic), - body2 = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.white, textBaseline: TextBaseline.alphabetic), - body1 = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w400, color: Colors.white, textBaseline: TextBaseline.alphabetic), - caption = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 12.0, fontWeight: FontWeight.w400, color: Colors.white70, textBaseline: TextBaseline.alphabetic), - button = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.white, textBaseline: TextBaseline.alphabetic); + subhead = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 16.0, fontWeight: FontWeight.w400, color: Colors.white, textBaseline: TextBaseline.alphabetic), + body2 = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.white, textBaseline: TextBaseline.alphabetic), + body1 = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w400, color: Colors.white, textBaseline: TextBaseline.alphabetic), + caption = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 12.0, fontWeight: FontWeight.w400, color: Colors.white70, textBaseline: TextBaseline.alphabetic), + button = const TextStyle(fontFamily: '.SF UI Text', inherit: false, fontSize: 14.0, fontWeight: FontWeight.w500, color: Colors.white, textBaseline: TextBaseline.alphabetic); /// Extremely large text. /// @@ -173,7 +183,7 @@ class TextTheme { body2: body2 ?? this.body2, body1: body1 ?? this.body1, caption: caption ?? this.caption, - button: button ?? this.button + button: button ?? this.button, ); } @@ -199,67 +209,67 @@ class TextTheme { color: displayColor, fontFamily: fontFamily, fontSizeFactor: fontSizeFactor, - fontSizeDelta: fontSizeDelta + fontSizeDelta: fontSizeDelta, ), display3: display3.apply( color: displayColor, fontFamily: fontFamily, fontSizeFactor: fontSizeFactor, - fontSizeDelta: fontSizeDelta + fontSizeDelta: fontSizeDelta, ), display2: display2.apply( color: displayColor, fontFamily: fontFamily, fontSizeFactor: fontSizeFactor, - fontSizeDelta: fontSizeDelta + fontSizeDelta: fontSizeDelta, ), display1: display1.apply( color: displayColor, fontFamily: fontFamily, fontSizeFactor: fontSizeFactor, - fontSizeDelta: fontSizeDelta + fontSizeDelta: fontSizeDelta, ), headline: headline.apply( color: bodyColor, fontFamily: fontFamily, fontSizeFactor: fontSizeFactor, - fontSizeDelta: fontSizeDelta + fontSizeDelta: fontSizeDelta, ), title: title.apply( color: bodyColor, fontFamily: fontFamily, fontSizeFactor: fontSizeFactor, - fontSizeDelta: fontSizeDelta + fontSizeDelta: fontSizeDelta, ), subhead: subhead.apply( color: bodyColor, fontFamily: fontFamily, fontSizeFactor: fontSizeFactor, - fontSizeDelta: fontSizeDelta + fontSizeDelta: fontSizeDelta, ), body2: body2.apply( color: bodyColor, fontFamily: fontFamily, fontSizeFactor: fontSizeFactor, - fontSizeDelta: fontSizeDelta + fontSizeDelta: fontSizeDelta, ), body1: body1.apply( color: bodyColor, fontFamily: fontFamily, fontSizeFactor: fontSizeFactor, - fontSizeDelta: fontSizeDelta + fontSizeDelta: fontSizeDelta, ), caption: caption.apply( color: displayColor, fontFamily: fontFamily, fontSizeFactor: fontSizeFactor, - fontSizeDelta: fontSizeDelta + fontSizeDelta: fontSizeDelta, ), button: button.apply( color: bodyColor, fontFamily: fontFamily, fontSizeFactor: fontSizeFactor, - fontSizeDelta: fontSizeDelta + fontSizeDelta: fontSizeDelta, ), ); } @@ -277,7 +287,7 @@ class TextTheme { body2: TextStyle.lerp(begin.body2, end.body2, t), body1: TextStyle.lerp(begin.body1, end.body1, t), caption: TextStyle.lerp(begin.caption, end.caption, t), - button: TextStyle.lerp(begin.button, end.button, t) + button: TextStyle.lerp(begin.button, end.button, t), ); } @@ -285,7 +295,7 @@ class TextTheme { bool operator ==(dynamic other) { if (identical(this, other)) return true; - if (other is! TextTheme) + if (other.runtimeType != runtimeType) return false; final TextTheme typedOther = other; return display4 == typedOther.display4 && @@ -331,8 +341,9 @@ class TextTheme { /// /// See also: /// -/// * [Theme] -/// * [ThemeData] +/// * [TextTheme], which shows what the text styles in a theme look like. +/// * [Theme], for other aspects of a material design application that can be +/// globally adjusted, such as the color scheme. /// * class Typography { /// Creates the default typography for the specified platform. diff --git a/packages/flutter/lib/src/painting/box_fit.dart b/packages/flutter/lib/src/painting/box_fit.dart index 595e9058354b8..99ec3f3147745 100644 --- a/packages/flutter/lib/src/painting/box_fit.dart +++ b/packages/flutter/lib/src/painting/box_fit.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:math' as math; -import 'dart:ui' show Image; // to disambiguate mentions of Image in the dartdocs import 'package:flutter/foundation.dart'; @@ -13,22 +12,6 @@ import 'basic_types.dart'; /// /// See also [applyBoxFit], which applies the sizing semantics of these values /// (though not the alignment semantics). -/// -/// The following diagrams show the effects of each value: -/// -/// ![`fill`: Fill the target box by distorting the source's aspect ratio.](https://flutter.github.io/assets-for-api-docs/painting/box_fit_fill.png) -/// -/// ![`contain`: As large as possible while still containing the source entirely within the target box.](https://flutter.github.io/assets-for-api-docs/painting/box_fit_contain.png) -/// -/// ![`cover`: As small as possible while still covering the entire target box.](https://flutter.github.io/assets-for-api-docs/painting/box_fit_cover.png) -/// -/// ![`fitWidth`: Make sure the full width of the source is shown.](https://flutter.github.io/assets-for-api-docs/painting/box_fit_fitWidth.png) -/// -/// ![`fitHeight`: Make sure the full height of the source is shown.](https://flutter.github.io/assets-for-api-docs/painting/box_fit_fitHeight.png) -/// -/// ![`none`: Do not resize the source.](https://flutter.github.io/assets-for-api-docs/painting/box_fit_none.png) -/// -/// ![`scaleDown`: Same as `contain` if that would shrink the image, otherwise same as `none`.](https://flutter.github.io/assets-for-api-docs/painting/box_fit_scaleDown.png) enum BoxFit { /// Fill the target box by distorting the source's aspect ratio. /// @@ -121,16 +104,18 @@ class FittedSizes { /// /// ## Sample code /// -/// This example paints an [Image] `image` onto the [Rect] `outputRect` on a -/// [Canvas] `canvas`, using a [Paint] paint, applying the [BoxFit] algorithm +/// This function paints a [dart:ui.Image] `image` onto the [Rect] `outputRect` on a +/// [Canvas] `canvas`, using a [Paint] `paint`, applying the [BoxFit] algorithm /// `fit`: /// /// ```dart -/// final Size imageSize = new Size(image.width.toDouble(), image.height.toDouble()); -/// final FittedSizes sizes = applyBoxFit(fit, imageSize, outputRect.size); -/// final Rect inputSubrect = FractionalOffset.center.inscribe(sizes.source, Offset.zero & imageSize); -/// final Rect outputSubrect = FractionalOffset.center.inscribe(sizes.destination, outputRect); -/// canvas.drawImageRect(image, inputSubrect, outputSubrect, paint); +/// void paintImage(ui.Image image, Rect outputRect, Canvas canvas, Paint paint, BoxFit fit) { +/// final Size imageSize = new Size(image.width.toDouble(), image.height.toDouble()); +/// final FittedSizes sizes = applyBoxFit(fit, imageSize, outputRect.size); +/// final Rect inputSubrect = FractionalOffset.center.inscribe(sizes.source, Offset.zero & imageSize); +/// final Rect outputSubrect = FractionalOffset.center.inscribe(sizes.destination, outputRect); +/// canvas.drawImageRect(image, inputSubrect, outputSubrect, paint); +/// } /// ``` /// /// See also: diff --git a/packages/flutter/lib/src/painting/box_painter.dart b/packages/flutter/lib/src/painting/box_painter.dart index 4600841df914b..6df8d4f45b40d 100644 --- a/packages/flutter/lib/src/painting/box_painter.dart +++ b/packages/flutter/lib/src/painting/box_painter.dart @@ -328,18 +328,21 @@ class BorderSide { /// /// ## Sample code /// +/// All four borders the same, two-pixel wide solid white: +/// /// ```dart -/// // All four borders the same, two-pixel wide solid white: /// new Border.all(width: 2.0, color: const Color(0xFFFFFFFF)) /// ``` /// +/// The border for a material design divider: +/// /// ```dart -/// // The border for a material design divider: /// new Border(bottom: new BorderSide(color: Theme.of(context).dividerColor)) /// ``` /// +/// A 1990s-era "OK" button: +/// /// ```dart -/// // A 1990s-era "OK" button: /// new Container( /// decoration: const BoxDecoration( /// border: const Border( @@ -1013,21 +1016,24 @@ class LinearGradient extends Gradient { /// /// ## Sample code /// +/// This function draws a gradient that looks like a sun in a blue sky. +/// /// ```dart -/// // This gradient looks like a sun in a blue sky. -/// var gradient = new RadialGradient( -/// center: const FractionalOffset(0.7, 0.2), // near the top right -/// radius: 0.2, -/// colors: [ -/// const Color(0xFFFFFF00), // yellow sun -/// const Color(0xFF0099FF), // blue sky -/// ], -/// stops: [0.4, 1.0], -/// ); -/// // rect is the area we are painting over -/// var paint = new Paint() -/// ..shader = gradient.createShader(rect); -/// canvas.drawRect(rect, paint); +/// void paintSky(Canvas canvas, Rect rect) { +/// var gradient = new RadialGradient( +/// center: const FractionalOffset(0.7, 0.2), // near the top right +/// radius: 0.2, +/// colors: [ +/// const Color(0xFFFFFF00), // yellow sun +/// const Color(0xFF0099FF), // blue sky +/// ], +/// stops: [0.4, 1.0], +/// ); +/// // rect is the area we are painting over +/// var paint = new Paint() +/// ..shader = gradient.createShader(rect); +/// canvas.drawRect(rect, paint); +/// } /// ``` /// /// See also: @@ -1411,7 +1417,7 @@ class DecorationImage { /// /// The [BoxDecoration] class provides a variety of ways to draw a box. /// -/// The box has a [border], a body, and may cast a [shadow]. +/// The box has a [border], a body, and may cast a [boxShadow]. /// /// The [shape] of the box can be a circle or a rectangle. If it is a rectangle, /// then the [borderRadius] property controls the roundness of the corners. @@ -1421,7 +1427,7 @@ class DecorationImage { /// the box. Finally there is the [image], the precise alignment of which is /// controlled by the [DecorationImage] class. /// -/// The [border] paints over the body; the [shadow], naturally, paints below it. +/// The [border] paints over the body; the [boxShadow], naturally, paints below it. /// /// ## Sample code /// @@ -1659,9 +1665,9 @@ class BoxDecoration extends Decoration { /// An object that paints a [BoxDecoration] into a canvas. class _BoxDecorationPainter extends BoxPainter { - _BoxDecorationPainter(@required this._decoration, VoidCallback onChange) : super(onChange) { - assert(_decoration != null); - } + _BoxDecorationPainter(this._decoration, VoidCallback onChange) + : assert(_decoration != null), + super(onChange); final BoxDecoration _decoration; diff --git a/packages/flutter/lib/src/painting/colors.dart b/packages/flutter/lib/src/painting/colors.dart index b76a4ae213366..be9748dc41fcd 100644 --- a/packages/flutter/lib/src/painting/colors.dart +++ b/packages/flutter/lib/src/painting/colors.dart @@ -172,3 +172,39 @@ class HSVColor { @override String toString() => "HSVColor($alpha, $hue, $saturation, $value)"; } + +/// A color that has a small table of related colors called a "swatch". +/// +/// The table is indexed by values of type `T`. +/// +/// See also: +/// +/// * [MaterialColor] and [MaterialAccentColor], which define material design +/// primary and accent color swatches. +/// * [Colors], which defines all of the standard material design colors. +class ColorSwatch extends Color { + /// Creates a color that has a small table of related colors called a "swatch". + const ColorSwatch(int primary, this._swatch) : super(primary); + + @protected + final Map _swatch; + + /// Returns an element of the swatch table. + Color operator [](T index) => _swatch[index]; + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + final ColorSwatch typedOther = other; + return super==(other) && _swatch == typedOther._swatch; + } + + @override + int get hashCode => hashValues(runtimeType, value, _swatch); + + @override + String toString() => '$runtimeType(primary value: ${super.toString()})'; +} diff --git a/packages/flutter/lib/src/painting/edge_insets.dart b/packages/flutter/lib/src/painting/edge_insets.dart index 43de4f51312f0..b6b013bd60435 100644 --- a/packages/flutter/lib/src/painting/edge_insets.dart +++ b/packages/flutter/lib/src/painting/edge_insets.dart @@ -26,14 +26,21 @@ enum Axis { /// /// Here are some examples of how to create [EdgeInsets] instances: /// +/// Typical eight-pixel margin on all sides: +/// /// ```dart -/// // typical 8-pixel margin on all sides /// const EdgeInsets.all(8.0) +/// ``` +/// +/// Eight pixel margin above and below, no horizontal margins: /// -/// // 8-pixel margin above and below, no horizontal margins +/// ```dart /// const EdgeInsets.symmetric(vertical: 8.0) +/// ``` /// -/// // left-margin indent of 40 pixels +/// Left margin indent of 40 pixels: +/// +/// ```dart /// const EdgeInsets.only(left: 40.0) /// ``` /// @@ -49,8 +56,9 @@ class EdgeInsets { /// /// ## Sample code /// + /// Typical eight-pixel margin on all sides: + /// /// ```dart - /// // typical 8-pixel margin on all sides /// const EdgeInsets.all(8.0) /// ``` const EdgeInsets.all(double value) @@ -60,8 +68,9 @@ class EdgeInsets { /// /// ## Sample code /// + /// Left margin indent of 40 pixels: + /// /// ```dart - /// // left-margin indent of 40 pixels /// const EdgeInsets.only(left: 40.0) /// ``` const EdgeInsets.only({ @@ -75,8 +84,9 @@ class EdgeInsets { /// /// ## Sample code /// + /// Eight pixel margin above and below, no horizontal margins: + /// /// ```dart - /// // 8-pixel margin above and below, no horizontal margins /// const EdgeInsets.symmetric(vertical: 8.0) /// ``` const EdgeInsets.symmetric({ double vertical: 0.0, diff --git a/packages/flutter/lib/src/painting/flutter_logo.dart b/packages/flutter/lib/src/painting/flutter_logo.dart index 97ba120a0fb3f..3652febb51ca2 100644 --- a/packages/flutter/lib/src/painting/flutter_logo.dart +++ b/packages/flutter/lib/src/painting/flutter_logo.dart @@ -219,9 +219,10 @@ class FlutterLogoDecoration extends Decoration { /// An object that paints a [BoxDecoration] into a canvas. class _FlutterLogoPainter extends BoxPainter { - _FlutterLogoPainter(this._config) : super(null) { - assert(_config != null); - assert(_config.debugAssertIsValid()); + _FlutterLogoPainter(this._config) + : assert(_config != null), + assert(_config.debugAssertIsValid()), + super(null) { _prepareText(); } diff --git a/packages/flutter/lib/src/painting/fractional_offset.dart b/packages/flutter/lib/src/painting/fractional_offset.dart index e0b0c725e23e1..90b4549986a02 100644 --- a/packages/flutter/lib/src/painting/fractional_offset.dart +++ b/packages/flutter/lib/src/painting/fractional_offset.dart @@ -16,6 +16,12 @@ import 'basic_types.dart'; /// /// `FractionalOffset(0.5, 2.0)` represents a point half way across the [Size], /// below the bottom of the rectangle by the height of the [Size]. +/// +/// A variety of widgets use [FractionalOffset] in their configuration, most +/// notably: +/// +/// * [Align] positions a child according to a [FractionalOffset]. +/// * [FractionalTranslation] moves a child according to a [FractionalOffset]. @immutable class FractionalOffset { /// Creates a fractional offset. diff --git a/packages/flutter/lib/src/painting/text_painter.dart b/packages/flutter/lib/src/painting/text_painter.dart index a3e77426e42c8..03a3ca9355fc3 100644 --- a/packages/flutter/lib/src/painting/text_painter.dart +++ b/packages/flutter/lib/src/painting/text_painter.dart @@ -35,16 +35,22 @@ class TextPainter { /// /// The text argument is optional but [text] must be non-null before calling /// [layout]. + /// + /// The [maxLines] property, if non-null, must be greater than zero. TextPainter({ TextSpan text, TextAlign textAlign, double textScaleFactor: 1.0, int maxLines, String ellipsis, - }) : _text = text, _textAlign = textAlign, _textScaleFactor = textScaleFactor, _maxLines = maxLines, _ellipsis = ellipsis { - assert(text == null || text.debugAssertIsValid()); - assert(textScaleFactor != null); - } + }) : assert(text == null || text.debugAssertIsValid()), + assert(textScaleFactor != null), + assert(maxLines == null || maxLines > 0), + _text = text, + _textAlign = textAlign, + _textScaleFactor = textScaleFactor, + _maxLines = maxLines, + _ellipsis = ellipsis; ui.Paragraph _paragraph; bool _needsLayout = true; @@ -106,6 +112,11 @@ class TextPainter { /// passed to [layout]. /// /// After this is set, you must call [layout] before the next call to [paint]. + /// + /// The higher layers of the system, such as the [Text] widget, represent + /// overflow effects using the [TextOverflow] enum. The + /// [TextOverflow.ellipsis] value corresponds to setting this property to + /// U+2026 HORIZONTAL ELLIPSIS (…). String get ellipsis => _ellipsis; String _ellipsis; set ellipsis(String value) { @@ -126,7 +137,9 @@ class TextPainter { /// After this is set, you must call [layout] before the next call to [paint]. int get maxLines => _maxLines; int _maxLines; + /// The value may be null. If it is not null, then it must be greater than zero. set maxLines(int value) { + assert(value == null || value > 0); if (_maxLines == value) return; _maxLines = value; diff --git a/packages/flutter/lib/src/painting/text_span.dart b/packages/flutter/lib/src/painting/text_span.dart index 0b5f824d632f4..a7f068d7d809e 100644 --- a/packages/flutter/lib/src/painting/text_span.dart +++ b/packages/flutter/lib/src/painting/text_span.dart @@ -11,13 +11,13 @@ import 'package:flutter/services.dart'; import 'basic_types.dart'; import 'text_style.dart'; -// TODO(abarth): Should this be somewhere more general? +// TODO(ianh): This should be on List itself. bool _deepEquals(List a, List b) { if (a == null) return b == null; if (b == null || a.length != b.length) return false; - for (int i = 0; i < a.length; ++i) { + for (int i = 0; i < a.length; i += 1) { if (a[i] != b[i]) return false; } @@ -40,11 +40,25 @@ bool _deepEquals(List a, List b) { /// span in a widget, use a [RichText]. For text with a single style, consider /// using the [Text] widget. /// +/// ## Sample code +/// +/// The text "Hello world!", in black: +/// +/// ```dart +/// new TextSpan( +/// text: 'Hello world!', +/// style: new TextStyle(color: Colors.black), +/// ) +/// ``` +/// +/// _There is some more detailed sample code in the documentation for the +/// [recognizer] property._ +/// /// See also: /// -/// * [Text] -/// * [RichText] -/// * [TextPainter] +/// * [Text], a widget for showing uniformly-styled text. +/// * [RichText], a widget for finer control of text rendering. +/// * [TextPainter], a class for painting [TextSpan] objects on a [Canvas]. @immutable class TextSpan { /// Creates a [TextSpan] with the given values. @@ -55,7 +69,7 @@ class TextSpan { this.style, this.text, this.children, - this.recognizer + this.recognizer, }); /// The style to apply to the [text] and the [children]. @@ -80,11 +94,77 @@ class TextSpan { /// A gesture recognizer that will receive events that hit this text span. /// - /// [TextSpan] itself does not implement hit testing or event - /// dispatch. The owner of the [TextSpan] tree to which the object - /// belongs is responsible for dispatching events. + /// [TextSpan] itself does not implement hit testing or event dispatch. The + /// object that manages the [TextSpan] painting is also responsible for + /// dispatching events. In the rendering library, that is the + /// [RenderParagraph] object, which corresponds to the [RichText] widget in + /// the widgets layer; these objects do not bubble events in [TextSpan]s, so a + /// [recognizer] is only effective for events that directly hit the [text] of + /// that [TextSpan], not any of its [children]. + /// + /// [TextSpan] also does not manage the lifetime of the gesture recognizer. + /// The code that owns the [GestureRecognizer] object must call + /// [GestureRecognizer.dispose] when the [TextSpan] object is no longer used. + /// + /// ## Sample code + /// + /// This example shows how to manage the lifetime of a gesture recognizer + /// provided to a [TextSpan] object. It defines a [BuzzingText] widget which + /// uses the [HapticFeedback] class to vibrate the device when the user + /// long-presses the "find the" span, which is underlined in wavy green. The + /// hit-testing is handled by the [RichText] widget. + /// + /// ```dart + /// class BuzzingText extends StatefulWidget { + /// @override + /// _BuzzingTextState createState() => new _BuzzingTextState(); + /// } + /// + /// class _BuzzingTextState extends State { + /// LongPressGestureRecognizer _longPressRecognizer; + /// + /// @override + /// void initState() { + /// super.initState(); + /// _longPressRecognizer = new LongPressGestureRecognizer() + /// ..onLongPress = _handlePress; + /// } + /// + /// @override + /// void dispose() { + /// _longPressRecognizer.dispose(); + /// super.dispose(); + /// } /// - /// For an example, see [RenderParagraph] in the Flutter rendering library. + /// void _handlePress() { + /// HapticFeedback.vibrate(); + /// } + /// + /// @override + /// Widget build(BuildContext context) { + /// return new RichText( + /// text: new TextSpan( + /// text: 'Can you ', + /// style: new TextStyle(color: Colors.black), + /// children: [ + /// new TextSpan( + /// text: 'find the', + /// style: new TextStyle( + /// color: Colors.green, + /// decoration: TextDecoration.underline, + /// decorationStyle: TextDecorationStyle.wavy, + /// ), + /// recognizer: _longPressRecognizer, + /// ), + /// new TextSpan( + /// text: ' secret?', + /// ), + /// ], + /// ), + /// ); + /// } + /// } + /// ``` final GestureRecognizer recognizer; /// Apply the [style], [text], and [children] of this object to the @@ -111,7 +191,8 @@ class TextSpan { builder.pop(); } - /// Walks this text span and its decendants in pre-order and calls [visitor] for each span that has text. + /// Walks this text span and its decendants in pre-order and calls [visitor] + /// for each span that has text. bool visitTextSpan(bool visitor(TextSpan span)) { if (text != null) { if (!visitor(this)) @@ -162,6 +243,7 @@ class TextSpan { } /// Returns the UTF-16 code unit at the given index in the flattened string. + /// /// Returns null if the index is out of bounds. int codeUnitAt(int index) { if (index < 0) @@ -208,8 +290,9 @@ class TextSpan { /// valid configuration. Otherwise, returns true. /// /// This is intended to be used as follows: + /// /// ```dart - /// assert(myTextSpan.debugAssertIsValid()); + /// assert(myTextSpan.debugAssertIsValid()); /// ``` bool debugAssertIsValid() { assert(() { @@ -238,7 +321,7 @@ class TextSpan { bool operator ==(dynamic other) { if (identical(this, other)) return true; - if (other is! TextSpan) + if (other.runtimeType != runtimeType) return false; final TextSpan typedOther = other; return typedOther.text == text diff --git a/packages/flutter/lib/src/painting/text_style.dart b/packages/flutter/lib/src/painting/text_style.dart index 5fcabe8d7bbb1..c7595b8fc1552 100644 --- a/packages/flutter/lib/src/painting/text_style.dart +++ b/packages/flutter/lib/src/painting/text_style.dart @@ -9,6 +9,127 @@ import 'package:flutter/foundation.dart'; import 'basic_types.dart'; /// An immutable style in which paint text. +/// +/// ## Sample code +/// +/// ### Bold +/// +/// Here, a single line of text in a [Text] widget is given a specific style +/// override. The style is mixed with the ambient [DefaultTextStyle] by the +/// [Text] widget. +/// +/// ```dart +/// new Text( +/// 'No, we need bold strokes. We need this plan.', +/// style: new TextStyle(fontWeight: FontWeight.bold), +/// ) +/// ``` +/// +/// ### Italics +/// +/// As in the previous example, the [Text] widget is given a specific style +/// override which is implicitly mixed with the ambient [DefaultTextStyle]. +/// +/// ```dart +/// new Text( +/// 'Welcome to the present, we\'re running a real nation.', +/// style: new TextStyle(fontStyle: FontStyle.italic), +/// ) +/// ``` +/// +/// ### Opacity +/// +/// Each line here is progressively more opaque. The base color is +/// [Colors.black], and [Color.withOpacity] is used to create a derivative color +/// with the desired opacity. The root [TextSpan] for this [RichText] widget is +/// explicitly given the ambient [DefaultTextStyle], since [RichText] does not +/// do that automatically. The inner [TextStyle] objects are implicitly mixed +/// with the parent [TextSpan]'s [TextSpan.style]. +/// +/// ```dart +/// new RichText( +/// text: new TextSpan( +/// style: DefaultTextStyle.of(context).style, +/// children: [ +/// new TextSpan( +/// text: 'You don\'t have the votes.\n', +/// style: new TextStyle(color: Colors.black.withOpacity(0.6)), +/// ), +/// new TextSpan( +/// text: 'You don\'t have the votes!\n', +/// style: new TextStyle(color: Colors.black.withOpacity(0.8)), +/// ), +/// new TextSpan( +/// text: 'You\'re gonna need congressional approval and you don\'t have the votes!\n', +/// style: new TextStyle(color: Colors.black.withOpacity(1.0)), +/// ), +/// ], +/// ), +/// ) +/// ``` +/// +/// ### Size +/// +/// In this example, the ambient [DefaultTextStyle] is explicitly manipulated to +/// obtain a [TextStyle] that doubles the default font size. +/// +/// ```dart +/// new Text( +/// 'These are wise words, enterprising men quote \'em.', +/// style: DefaultTextStyle.of(context).style.apply(fontSizeFactor: 2.0), +/// ) +/// ``` +/// +/// ### Line height +/// +/// The [height] property can be used to change the line height. Here, the line +/// height is set to 100 logical pixels, so that the text is very spaced out. +/// +/// ```dart +/// new Text( +/// 'Don\'t act surprised, you guys, cuz I wrote \'em!', +/// style: new TextStyle(height: 100.0), +/// ) +/// ``` +/// +/// ### Wavy red underline with black text +/// +/// Styles can be combined. In this example, the misspelt word is drawn in black +/// text and underlined with a wavy red line to indicate a spelling error. (The +/// remainder is styled according to the Flutter default text styles, not the +/// ambient [DefaultTextStyle], since no explicit style is given and [RichText] +/// does not automatically use the ambient [DefaultTextStyle].) +/// +/// ```dart +/// new RichText( +/// text: new TextSpan( +/// text: 'Don\'t tax the South ', +/// children: [ +/// new TextSpan( +/// text: 'cuz', +/// style: new TextStyle( +/// color: Colors.black, +/// decoration: TextDecoration.underline, +/// decorationColor: Colors.red, +/// decorationStyle: TextDecorationStyle.wavy, +/// ), +/// ), +/// new TextSpan( +/// text: ' we got it made in the shade', +/// ), +/// ], +/// ), +/// ) +/// ``` +/// +/// See also: +/// +/// * [Text], the widget for showing text in a single style. +/// * [DefaultTextStyle], the widget that specifies the default text styles for +/// [Text] widgets, configured using a [TextStyle]. +/// * [RichText], the widget for showing a paragraph of mix-style text. +/// * [TextSpan], the class that wraps a [TextStyle] for the purposes of +/// passing it to a [RichText]. @immutable class TextStyle { /// Creates a text style. @@ -25,10 +146,15 @@ class TextStyle { this.height, this.decoration, this.decorationColor, - this.decorationStyle + this.decorationStyle, }) : assert(inherit != null); - /// Whether null values are replaced with their value in an ancestor text style (e.g., in a [TextSpan] tree). + /// Whether null values are replaced with their value in an ancestor text + /// style (e.g., in a [TextSpan] tree). + /// + /// If this is false, properties that don't have explicit values will revert + /// to the defaults: white in color, a font size of 10 pixels, in a sans-serif + /// font face. final bool inherit; /// The color to use when painting the text. @@ -54,11 +180,13 @@ class TextStyle { /// A negative value can be used to bring the letters closer. final double letterSpacing; - /// The amount of space (in logical pixels) to add at each sequence of white-space (i.e. between each word). - /// A negative value can be used to bring the words closer. + /// The amount of space (in logical pixels) to add at each sequence of + /// white-space (i.e. between each word). A negative value can be used to + /// bring the words closer. final double wordSpacing; - /// The common baseline that should be aligned between this text span and its parent text span, or, for the root text spans, with the line box. + /// The common baseline that should be aligned between this text span and its + /// parent text span, or, for the root text spans, with the line box. final TextBaseline textBaseline; /// The height of this text span, as a multiple of the font size. @@ -77,7 +205,8 @@ class TextStyle { /// The style in which to paint the text decorations (e.g., dashed). final TextDecorationStyle decorationStyle; - /// Creates a copy of this text style but with the given fields replaced with the new values. + /// Creates a copy of this text style but with the given fields replaced with + /// the new values. TextStyle copyWith({ Color color, String fontFamily, @@ -90,7 +219,7 @@ class TextStyle { double height, TextDecoration decoration, Color decorationColor, - TextDecorationStyle decorationStyle + TextDecorationStyle decorationStyle, }) { return new TextStyle( inherit: inherit, @@ -105,7 +234,7 @@ class TextStyle { height: height ?? this.height, decoration: decoration ?? this.decoration, decorationColor: decorationColor ?? this.decorationColor, - decorationStyle: decorationStyle ?? this.decorationStyle + decorationStyle: decorationStyle ?? this.decorationStyle, ); } @@ -234,12 +363,18 @@ class TextStyle { } /// The style information for paragraphs, encoded for use by `dart:ui`. + /// + /// The `textScaleFactor` argument must not be null. If omitted, it defaults + /// to 1.0. The other arguments may be null. The `maxLines` argument, if + /// specified and non-null, must be greater than zero. ui.ParagraphStyle getParagraphStyle({ TextAlign textAlign, double textScaleFactor: 1.0, String ellipsis, int maxLines, - }) { + }) { + assert(textScaleFactor != null); + assert(maxLines == null || maxLines > 0); return new ui.ParagraphStyle( textAlign: textAlign, fontWeight: fontWeight, diff --git a/packages/flutter/lib/src/physics/clamped_simulation.dart b/packages/flutter/lib/src/physics/clamped_simulation.dart index 82fe080310fe1..854936f5ccbb7 100644 --- a/packages/flutter/lib/src/physics/clamped_simulation.dart +++ b/packages/flutter/lib/src/physics/clamped_simulation.dart @@ -26,11 +26,9 @@ class ClampedSimulation extends Simulation { this.xMax: double.INFINITY, this.dxMin: double.NEGATIVE_INFINITY, this.dxMax: double.INFINITY - }) { - assert(simulation != null); - assert(xMax >= xMin); - assert(dxMax >= dxMin); - } + }) : assert(simulation != null), + assert(xMax >= xMin), + assert(dxMax >= dxMin); /// The simulation being clamped. Calls to [x], [dx], and [isDone] are /// forwarded to the simulation. diff --git a/packages/flutter/lib/src/physics/friction_simulation.dart b/packages/flutter/lib/src/physics/friction_simulation.dart index 10cf766e22c10..8fa5cf18d8f66 100644 --- a/packages/flutter/lib/src/physics/friction_simulation.dart +++ b/packages/flutter/lib/src/physics/friction_simulation.dart @@ -105,9 +105,8 @@ class BoundedFrictionSimulation extends FrictionSimulation { double velocity, this._minX, this._maxX - ) : super(drag, position, velocity) { - assert(position.clamp(_minX, _maxX) == position); - } + ) : assert(position.clamp(_minX, _maxX) == position), + super(drag, position, velocity); final double _minX; final double _maxX; diff --git a/packages/flutter/lib/src/physics/gravity_simulation.dart b/packages/flutter/lib/src/physics/gravity_simulation.dart index b75eb93bded24..2873d65d62d09 100644 --- a/packages/flutter/lib/src/physics/gravity_simulation.dart +++ b/packages/flutter/lib/src/physics/gravity_simulation.dart @@ -8,6 +8,30 @@ import 'simulation.dart'; /// /// Models a particle that follows Newton's second law of motion. The simulation /// ends when the position reaches a defined point. +/// +/// ## Sample code +/// +/// This method triggers an [AnimationController] (a previously constructed +/// `_controller` field) to simulate a fall of 300 pixels. +/// +/// ```dart +/// void _startFall() { +/// _controller.animateWith(new GravitySimulation( +/// 10.0, // acceleration, pixels per second per second +/// 0.0, // starting position, pixels +/// 300.0, // ending position, pixels +/// 0.0, // starting velocity, pixels per second +/// )); +/// } +/// ``` +/// +/// This [AnimationController] could be used with an [AnimatedBuilder] to +/// animate the position of a child as if it was falling. +/// +/// See also: +/// +/// * [Curves.bounceOut], a [Curve] that has a similar aesthetics but includes +/// a bouncing effect. class GravitySimulation extends Simulation { /// Creates a [GravitySimulation] using the given arguments, which are, /// respectively: an acceleration that is to be applied continually over time; @@ -28,16 +52,15 @@ class GravitySimulation extends Simulation { double distance, double endDistance, double velocity - ) : _a = acceleration, + ) : assert(acceleration != null), + assert(distance != null), + assert(velocity != null), + assert(endDistance != null), + assert(endDistance >= 0), + _a = acceleration, _x = distance, _v = velocity, - _end = endDistance { - assert(acceleration != null); - assert(distance != null); - assert(velocity != null); - assert(endDistance != null); - assert(endDistance >= 0); - } + _end = endDistance; final double _x; final double _v; diff --git a/packages/flutter/lib/src/rendering/README.md b/packages/flutter/lib/src/rendering/README.md deleted file mode 100644 index 540ac8ef7fa18..0000000000000 --- a/packages/flutter/lib/src/rendering/README.md +++ /dev/null @@ -1,112 +0,0 @@ -Flutter Rendering Layer -======================= - -This document is intended to describe some of the core designs of the -Flutter rendering layer. - -Layout ------- - -Paint ------ - -Compositing ------------ - -Semantics ---------- - -The last phase of a frame is the Semantics phase. This only occurs if -a semantics server has been installed, for example if the user is -using an accessibility tool. - -Each frame, the semantics phase starts with a call to the -`PipelineOwner.flushSemantics()` method from the `Renderer` binding's -`beginFrame()` method. - -Each node marked as needing semantics (which initially is just the -root node, as scheduled by `scheduleInitialSemantics()`), in depth -order, has its semantics updated by calling `_updateSemantics()`. - -The `_updateSemantics()` method calls `_getSemantics()` to obtain an -`_InterestingSemanticsFragment`, and then calls `compile()` on that -fragment to obtain a `SemanticsNode` which becomes the value of the -`RenderObject`'s `_semantics` field. **This is essentially a two-pass -walk of the render tree. The first pass determines the shape of the -output tree, and the second creates the nodes of this tree and hooks -them together.** The second walk is a sparse walk; it only walks the -nodes that are interesting for the purpose of semantics. - -`_getSemantics()` is the core function that walks the render tree to -obtain the semantics. It collects semantic annotators for this -`RenderObject`, then walks its children collecting -`_SemanticsFragment`s for them, and then returns an appropriate -`_SemanticsFragment` object that describes the `RenderObject`'s -semantics. - -Semantic annotators are functions that, given a `SemanticsNode`, set -some flags or strings on the object. They are obtained from -`getSemanticsAnnotators()`. For example, here is how `RenderParagraph` -annotates the `SemanticsNode` with its text: - -```dart - Iterable getSemanticsAnnotators() sync* { - yield (SemanticsNode node) { - node.label = text.toPlainText(); - }; - } -``` - -A `_SemanticsFragment` object is a node in a short-lived tree which is -used to create the final `SemanticsNode` tree that is sent to the -semantics server. These objects have a list of semantic annotators, -and a list of `_SemanticsFragment` children. - -There are several `_SemanticsFragment` classes. The `_getSemantics()` -method picks its return value as follows: - -* `_CleanSemanticsFragment` is used to represent a `RenderObject` that - has a `SemanticsNode` and which is in no way dirty. This class has - no children and no annotators, and when compiled, it returns the - `SemanticsNode` that the `RenderObject` already has. - -* `_RootSemanticsFragment`* is used to represent the `RenderObject` - found at the top of the render tree. This class always compiles to a - `SemanticsNode` with ID 0. - -* `_ConcreteSemanticsFragment`* is used to represent a `RenderObject` - that has `hasSemantics` set to true. It returns the `SemanticsNode` - for that `RenderObject`. - -* `_ImplicitSemanticsFragment`* is used to represent a `RenderObject` - that does not have `hasSemantics` set to true, but which does have - some semantic annotators. When it is compiled, if the nearest - ancestor `_SemanticsFragment` that isn't also an - `_ImplicitSemanticsFragment` is a `_RootSemanticsFragment` or a - `_ConcreteSemanticsFragment`, then the `SemanticsNode` from that - object is reused. Otherwise, a new one is created. - -* `_ForkingSemanticsFragment` is used to represent a `RenderObject` - that introduces no semantics of its own, but which has two or more - descendants that do introduce semantics (and which are not ancestors - or descendants of each other). - -* For `RenderObject` nodes that introduce no semantics but which have - a (single) child that does, the `_SemanticsFragment` of the child is - returned. - -* For `RenderObject` nodes that introduce no semantics and have no - descendants that introduce semantics, `null` is returned. - -The classes marked with an asterisk * above are the -`_InterestingSemanticsFragment` classes. - -When the `_SemanticsFragment` tree is then compiled, the -`SemanticsNode` objects are created (if necessary), the semantic -annotators are run on each `SemanticsNode`, the geometry (matrix, -size, and clip) is applied, and the children are updated. - -As part of this, the code clears out the `_semantics` field of any -`RenderObject` that previously had a `SemanticsNode` but no longer -does. This is done as part of the first walk where possible, and as -part of the second otherwise. diff --git a/packages/flutter/lib/src/rendering/animated_size.dart b/packages/flutter/lib/src/rendering/animated_size.dart index 2e707e8ca4d4c..788404dddca07 100644 --- a/packages/flutter/lib/src/rendering/animated_size.dart +++ b/packages/flutter/lib/src/rendering/animated_size.dart @@ -39,10 +39,11 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { Curve curve: Curves.linear, FractionalOffset alignment: FractionalOffset.center, RenderBox child, - }) : _vsync = vsync, super(child: child, alignment: alignment) { - assert(vsync != null); - assert(duration != null); - assert(curve != null); + }) : assert(vsync != null), + assert(duration != null), + assert(curve != null), + _vsync = vsync, + super(child: child, alignment: alignment) { _controller = new AnimationController( vsync: vsync, duration: duration, diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index aad9dd228d951..6faf2d72f52f8 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -20,7 +20,11 @@ import 'view.dart'; export 'package:flutter/gestures.dart' show HitTestResult; /// The glue between the render tree and the Flutter engine. -abstract class RendererBinding extends BindingBase implements SchedulerBinding, ServicesBinding, HitTestable { +abstract class RendererBinding extends BindingBase with SchedulerBinding, ServicesBinding, HitTestable { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory RendererBinding._() => null; + @override void initInstances() { super.initInstances(); @@ -258,8 +262,17 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding, /// likely to be quite expensive) gets a few extra milliseconds to run. void scheduleWarmUpFrame() { // We use timers here to ensure that microtasks flush in between. + // + // We call resetEpoch after this frame so that, in the hot reload case, the + // very next frame pretends to have occurred immediately after this warm-up + // frame. The warm-up frame's timestamp will typically be far in the past + // (the time of the last real frame), so if we didn't reset the epoch we + // would see a sudden jump from the old time in the warm-up frame to the new + // time in the "real" frame. The biggest problem with this is that implicit + // animations end up being triggered at the old time and then skipping every + // frame and finishing in the new time. Timer.run(() { handleBeginFrame(null); }); - Timer.run(() { handleDrawFrame(); }); + Timer.run(() { handleDrawFrame(); resetEpoch(); }); } @override diff --git a/packages/flutter/lib/src/rendering/block.dart b/packages/flutter/lib/src/rendering/block.dart index 6521f0d3a213c..98c4bda2e4de0 100644 --- a/packages/flutter/lib/src/rendering/block.dart +++ b/packages/flutter/lib/src/rendering/block.dart @@ -8,7 +8,7 @@ import 'box.dart'; import 'object.dart'; /// Parent data for use with [RenderListBody]. -class ListBodyParentData extends ContainerBoxParentDataMixin { } +class ListBodyParentData extends ContainerBoxParentData { } typedef double _ChildSizingFunction(RenderBox child); diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index 7806abdecbf5c..82afc4d3b14e2 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.dart @@ -23,18 +23,63 @@ class _DebugSize extends Size { /// Immutable layout constraints for [RenderBox] layout. /// -/// A size respects a [BoxConstraints] if, and only if, all of the following +/// A [Size] respects a [BoxConstraints] if, and only if, all of the following /// relations hold: /// -/// * `minWidth <= size.width <= maxWidth` -/// * `minHeight <= size.height <= maxHeight` +/// * [minWidth] <= [Size.width] <= [maxWidth] +/// * [minHeight] <= [Size.height] <= [maxHeight] /// /// The constraints themselves must satisfy these relations: /// -/// * `0.0 <= minWidth <= maxWidth <= double.INFINITY` -/// * `0.0 <= minHeight <= maxHeight <= double.INFINITY` +/// * 0.0 <= [minWidth] <= [maxWidth] <= [double.INFINITY] +/// * 0.0 <= [minHeight] <= [maxHeight] <= [double.INFINITY] /// /// [double.INFINITY] is a legal value for each constraint. +/// +/// ## The box layout model +/// +/// Render objects in the Flutter framework are laid out by a one-pass layout +/// model which walks down the render tree passing constraints, then walks back +/// up the render tree passing concrete geometry. +/// +/// For boxes, the constraints are [BoxConstraints], which, as described herein, +/// consist of four numbers: a minimum width [minWidth], a maximum width +/// [maxWidth], a minimum height [minHeight], and a maximum height [maxHeight]. +/// +/// The geometry for boxes consists of a [Size], which must satisfy the +/// constraints described above. +/// +/// Each [RenderBox] (the objects that provide the layout models for box +/// widgets) receives [BoxConstraints] from its parent, then lays out each of +/// its children, then picks a [Size] that satisfies the [BoxConstraints]. +/// +/// Render objects position their children independently of laying them out. +/// Frequently, the parent will use the children's sizes to determine their +/// position. A child does not know its position and will not necessarily be +/// laid out again, or repainted, if its position changes. +/// +/// ## Terminology +/// +/// When the minimum constraints and the maximum constraint in an axis are the +/// same, that axis is _tightly_ constrained. See: [new +/// BoxConstraints.tightFor], [new BoxConstraints.tightForFinite], [tighten], +/// [hasTightWidth], [hasTightHeight], [isTight]. +/// +/// An axis with a minimum constraint of 0.0 is _loose_ (regardless of the +/// maximum constraint; if it is also 0.0, then the axis is simultaneously tight +/// and loose!). See: [new BoxConstraints.loose], [loosen]. +/// +/// An axis whose maximum constraint is not infinite is _bounded_. See: +/// [hasBoundedWidth], [hasBoundedHeight]. +/// +/// An axis whose maximum constraint is infinite is _unbounded_. An axis is +/// _expanding_ if it is tightly infinite (its minimum and maximum constraints +/// are both infinite). See: [new BoxConstraints.expand]. +/// +/// A size is _constrained_ when it satisfies a [BoxConstraints] description. +/// See: [constrain], [constrainWidth], [constrainHeight], +/// [constrainDimensions], [constrainSizeAndAttemptToPreserveAspectRatio], +/// [isSatisfiedBy]. class BoxConstraints extends Constraints { /// Creates box constraints with the given constraints. const BoxConstraints({ @@ -68,6 +113,12 @@ class BoxConstraints extends Constraints { maxHeight = size.height; /// Creates box constraints that require the given width or height. + /// + /// See also: + /// + /// * [new BoxConstraints.tightForFinite], which is similar but instead of + /// being tight if the value is non-null, is tight if the value is not + /// infinite. const BoxConstraints.tightFor({ double width, double height @@ -76,7 +127,13 @@ class BoxConstraints extends Constraints { minHeight = height != null ? height : 0.0, maxHeight = height != null ? height : double.INFINITY; - /// Creates box constraints that require the given width or height, except if they are infinite. + /// Creates box constraints that require the given width or height, except if + /// they are infinite. + /// + /// See also: + /// + /// * [new BoxConstraints.tightFor], which is similar but instead of being + /// tight if the value is not infinite, is tight if the value is non-null. const BoxConstraints.tightForFinite({ double width: double.INFINITY, double height: double.INFINITY @@ -230,10 +287,10 @@ class BoxConstraints extends Constraints { /// Returns a size that attempts to meet the following conditions, in order: /// - /// - The size must satisfy these constraints. - /// - The aspect ratio of the returned size matches the aspect ratio of the + /// * The size must satisfy these constraints. + /// * The aspect ratio of the returned size matches the aspect ratio of the /// given size. - /// - The returned size as big as possible while still being equal to or + /// * The returned size as big as possible while still being equal to or /// smaller than the given size. Size constrainSizeAndAttemptToPreserveAspectRatio(Size size) { if (isTight) { @@ -530,7 +587,10 @@ class BoxParentData extends ParentData { /// Abstract ParentData subclass for RenderBox subclasses that want the /// ContainerRenderObjectMixin. -abstract class ContainerBoxParentDataMixin extends BoxParentData with ContainerParentDataMixin { } +/// +/// This is a convenience class that mixes in the relevant classes with +/// the relevant type arguments. +abstract class ContainerBoxParentData extends BoxParentData with ContainerParentDataMixin { } enum _IntrinsicDimension { minWidth, maxWidth, minHeight, maxHeight } @@ -714,13 +774,13 @@ class _IntrinsicDimensionsCacheEntry { /// [parentData]. The class used for [parentData] must itself have the /// [ContainerParentDataMixin] class mixed into it; this is where /// [ContainerRenderObjectMixin] stores the linked list. A [ParentData] class -/// can extend [ContainerBoxParentDataMixin]; this is essentially +/// can extend [ContainerBoxParentData]; this is essentially /// [BoxParentData] mixed with [ContainerParentDataMixin]. For example, if a /// `RenderFoo` class wanted to have a linked list of [RenderBox] children, one /// might create a `FooParentData` class as follows: /// /// ```dart -/// class FooParentData extends ContainerBoxParentDataMixin { +/// class FooParentData extends ContainerBoxParentData { /// // (any fields you might need for these children) /// } /// ``` @@ -1695,9 +1755,10 @@ abstract class RenderBox extends RenderObject { /// Determines the set of render objects located at the given position. /// - /// Returns true if the given point is contained in this render object or one - /// of its descendants. Adds any render objects that contain the point to the - /// given hit test result. + /// Returns true, and adds any render objects that contain the point to the + /// given hit test result, if this render object or one of its descendants + /// absorbs the hit (preventing objects below this one from being hit). + /// Returns false if the hit can continue to other objects below this one. /// /// The caller is responsible for transforming [position] into the local /// coordinate space of the callee. The callee is responsible for checking @@ -2024,7 +2085,10 @@ abstract class RenderBox extends RenderObject { /// By convention, this class doesn't override any members of the superclass. /// Instead, it provides helpful functions that subclasses can call as /// appropriate. -abstract class RenderBoxContainerDefaultsMixin> implements ContainerRenderObjectMixin { +abstract class RenderBoxContainerDefaultsMixin> implements ContainerRenderObjectMixin { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory RenderBoxContainerDefaultsMixin._() => null; /// Returns the baseline of the first child with a baseline. /// diff --git a/packages/flutter/lib/src/rendering/custom_layout.dart b/packages/flutter/lib/src/rendering/custom_layout.dart index 7b90806259189..c5a3e0eef6814 100644 --- a/packages/flutter/lib/src/rendering/custom_layout.dart +++ b/packages/flutter/lib/src/rendering/custom_layout.dart @@ -10,7 +10,7 @@ import 'object.dart'; // For SingleChildLayoutDelegate and RenderCustomSingleChildLayoutBox, see shifted_box.dart /// [ParentData] used by [RenderCustomMultiChildLayoutBox]. -class MultiChildLayoutParentData extends ContainerBoxParentDataMixin { +class MultiChildLayoutParentData extends ContainerBoxParentData { /// An object representing the identity of this child. Object id; @@ -268,8 +268,8 @@ class RenderCustomMultiChildLayoutBox extends RenderBox RenderCustomMultiChildLayoutBox({ List children, @required MultiChildLayoutDelegate delegate - }) : _delegate = delegate { - assert(delegate != null); + }) : assert(delegate != null), + _delegate = delegate { addAll(children); } diff --git a/packages/flutter/lib/src/rendering/debug.dart b/packages/flutter/lib/src/rendering/debug.dart index fdd137177a1e8..e605d73e52bd9 100644 --- a/packages/flutter/lib/src/rendering/debug.dart +++ b/packages/flutter/lib/src/rendering/debug.dart @@ -108,7 +108,11 @@ bool debugPrintMarkNeedsLayoutStacks = false; /// Check the intrinsic sizes of each [RenderBox] during layout. bool debugCheckIntrinsicSizes = false; -/// Adds [dart:developer.Timeline] events for every RenderObject painted. +/// Adds [dart:developer.Timeline] events for every [RenderObject] painted. +/// +/// This is only enabled in debug builds. The timing information this exposes is +/// not representative of actual paints. However, it can expose unexpected +/// painting in the timeline. /// /// For details on how to use [dart:developer.Timeline] events in the Dart /// Observatory to optimize your app, see: diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index d3d0922e1b67a..cde9cc98293d7 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -58,7 +58,41 @@ class TextSelectionPoint { } } +/// Displays some text in a scrollable container with a potentially blinking +/// cursor and with gesture recognizers. +/// +/// This is the renderer for an editable text field. It does not directly +/// provide affordances for editing the text, but it does handle text selection +/// and manipulation of the text cursor. +/// +/// The [text] is displayed, scrolled by the given [offset], aligned according +/// to [textAlign]. The [maxLines] property controls whether the text displays +/// on one line or many. The [selection], if it is not collapsed, is painted in +/// the [selectionColor]. If it _is_ collapsed, then it represents the cursor +/// position. The cursor is shown while [showCursor] is true. It is painted in +/// the [cursorColor]. +/// +/// If, when the render object paints, the caret is found to have changed +/// location, [onCaretChanged] is called. +/// +/// The user may interact with the render object by tapping or long-pressing. +/// When the user does so, the selection is updated, and [onSelectionChanged] is +/// called. +/// +/// Keyboard handling, IME handling, scrolling, toggling the [showCursor] value +/// to actually blink the cursor, and other features not mentioned above are the +/// responsibility of higher layers and not handled by this object. class RenderEditable extends RenderBox { + /// Creates a render object that implements the visual aspects of a text field. + /// + /// If [showCursor] is not specified, then it defaults to hiding the cursor. + /// + /// The [maxLines] property can be set to null to remove the restriction on + /// the number of lines. By default, it is 1, meaning this is a single-line + /// text field. If it is not null, it must be greater than zero. + /// + /// The [offset] is required and must not be null. You can use [new + /// ViewportOffset.zero] if you have no need for scrolling. RenderEditable({ TextSpan text, TextAlign textAlign, @@ -71,16 +105,16 @@ class RenderEditable extends RenderBox { @required ViewportOffset offset, this.onSelectionChanged, this.onCaretChanged, - }) : _textPainter = new TextPainter(text: text, textAlign: textAlign, textScaleFactor: textScaleFactor), + }) : assert(maxLines == null || maxLines > 0), + assert(textScaleFactor != null), + assert(offset != null), + _textPainter = new TextPainter(text: text, textAlign: textAlign, textScaleFactor: textScaleFactor), _cursorColor = cursorColor, _showCursor = showCursor ?? new ValueNotifier(false), _maxLines = maxLines, _selection = selection, _offset = offset { assert(_showCursor != null); - assert(maxLines != null); - assert(textScaleFactor != null); - assert(offset != null); assert(!_showCursor.value || cursorColor != null); _tap = new TapGestureRecognizer() ..onTapDown = _handleTapDown @@ -155,13 +189,21 @@ class RenderEditable extends RenderBox { } /// The maximum number of lines for the text to span, wrapping if necessary. + /// /// If this is 1 (the default), the text will not wrap, but will extend /// indefinitely instead. + /// + /// If this is null, there is no limit to the number of lines. + /// + /// When this is not null, the intrinsic height of the render object is the + /// height of one line of text multiplied by this value. In other words, this + /// also controls the height of the actual editing widget. int get maxLines => _maxLines; int _maxLines; + /// The value may be null. If it is not null, then it must be greater than zero. set maxLines(int value) { - assert(value != null); - if (_maxLines == value) + assert(value == null || value > 0); + if (maxLines == value) return; _maxLines = value; markNeedsTextLayout(); @@ -236,7 +278,7 @@ class RenderEditable extends RenderBox { super.detach(); } - bool get _isMultiline => maxLines > 1; + bool get _isMultiline => maxLines != 1; Axis get _viewportAxis => _isMultiline ? Axis.vertical : Axis.horizontal; @@ -334,14 +376,30 @@ class RenderEditable extends RenderBox { // This does not required the layout to be updated. double get _preferredLineHeight => _textPainter.preferredLineHeight; + double _preferredHeight(double width) { + if (maxLines != null) + return _preferredLineHeight * maxLines; + if (width == double.INFINITY) { + final String text = _textPainter.text.toPlainText(); + int lines = 1; + for (int index = 0; index < text.length; index += 1) { + if (text.codeUnitAt(index) == 0x0A) // count explicit line breaks + lines += 1; + } + return _preferredLineHeight * lines; + } + _layoutText(width); + return math.max(_preferredLineHeight, _textPainter.height); + } + @override double computeMinIntrinsicHeight(double width) { - return _preferredLineHeight; + return _preferredHeight(width); } @override double computeMaxIntrinsicHeight(double width) { - return _preferredLineHeight * maxLines; + return _preferredHeight(width); } @override @@ -409,7 +467,7 @@ class RenderEditable extends RenderBox { return; final double caretMargin = _kCaretGap + _kCaretWidth; final double availableWidth = math.max(0.0, constraintWidth - caretMargin); - final double maxWidth = _maxLines > 1 ? availableWidth : double.INFINITY; + final double maxWidth = _isMultiline ? availableWidth : double.INFINITY; _textPainter.layout(minWidth: availableWidth, maxWidth: maxWidth); _textLayoutLastWidth = constraintWidth; } @@ -419,9 +477,7 @@ class RenderEditable extends RenderBox { _layoutText(constraints.maxWidth); _caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, _preferredLineHeight - 2.0 * _kCaretHeightOffset); _selectionRects = null; - size = new Size(constraints.maxWidth, constraints.constrainHeight( - _textPainter.height.clamp(_preferredLineHeight, _preferredLineHeight * _maxLines) - )); + size = new Size(constraints.maxWidth, constraints.constrainHeight(_preferredHeight(constraints.maxWidth))); final Size contentSize = new Size(_textPainter.width + _kCaretGap + _kCaretWidth, _textPainter.height); final double _maxScrollExtent = _getMaxScrollExtent(contentSize); _hasVisualOverflow = _maxScrollExtent > 0.0; @@ -481,13 +537,13 @@ class RenderEditable extends RenderBox { @override void debugFillDescription(List description) { super.debugFillDescription(description); - description.add('cursorColor: $_cursorColor'); - description.add('showCursor: $_showCursor'); - description.add('maxLines: $_maxLines'); - description.add('selectionColor: $_selectionColor'); + description.add('cursorColor: $cursorColor'); + description.add('showCursor: $showCursor'); + description.add('maxLines: $maxLines'); + description.add('selectionColor: $selectionColor'); description.add('textScaleFactor: $textScaleFactor'); - description.add('selection: $_selection'); - description.add('offset: $_offset'); + description.add('selection: $selection'); + description.add('offset: $offset'); } @override diff --git a/packages/flutter/lib/src/rendering/flex.dart b/packages/flutter/lib/src/rendering/flex.dart index db5e15658208d..e85650a5cdd9d 100644 --- a/packages/flutter/lib/src/rendering/flex.dart +++ b/packages/flutter/lib/src/rendering/flex.dart @@ -29,7 +29,7 @@ enum FlexFit { } /// Parent data for use with [RenderFlex]. -class FlexParentData extends ContainerBoxParentDataMixin { +class FlexParentData extends ContainerBoxParentData { /// The flex factor to use for this child /// /// If null or zero, the child is inflexible and determines its own size. If @@ -48,7 +48,7 @@ class FlexParentData extends ContainerBoxParentDataMixin { FlexFit fit; @override - String toString() => '${super.toString()}; flex=$flex'; + String toString() => '${super.toString()}; flex=$flex; fit=$fit'; } /// How much space should be occupied in the main axis. @@ -201,15 +201,15 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin (opacity * 255).round(); /// transformation matrix, which is private to the [RenderFlow]. To set the /// matrix, use the [FlowPaintingContext.paintChild] function from an override /// of the [FlowDelegate.paintChildren] function. -class FlowParentData extends ContainerBoxParentDataMixin { +class FlowParentData extends ContainerBoxParentData { Matrix4 _transform; } @@ -182,8 +182,8 @@ class RenderFlow extends RenderBox RenderFlow({ List children, @required FlowDelegate delegate - }) : _delegate = delegate { - assert(delegate != null); + }) : assert(delegate != null), + _delegate = delegate { addAll(children); } diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart index 582fb25315ed8..da25813b60800 100644 --- a/packages/flutter/lib/src/rendering/layer.dart +++ b/packages/flutter/lib/src/rendering/layer.dart @@ -643,11 +643,9 @@ class PhysicalModelLayer extends ContainerLayer { @required this.clipRRect, @required this.elevation, @required this.color, - }) { - assert(clipRRect != null); - assert(elevation != null); - assert(color != null); - } + }) : assert(clipRRect != null), + assert(elevation != null), + assert(color != null); /// The rounded-rect to clip in the parent's coordinate system. /// diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 9b7dd0b26b1af..6eb23ead8b21a 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'dart:developer'; -import 'dart:ui' as ui show ImageFilter, PictureRecorder; +import 'dart:ui' as ui show PictureRecorder; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -46,8 +46,8 @@ typedef void PaintingContextCallback(PaintingContext context, Offset offset); /// A place to paint. /// -/// Rather than holding a canvas directly, render objects paint using a painting -/// context. The painting context has a canvas, which receives the +/// Rather than holding a canvas directly, [RenderObject]s paint using a painting +/// context. The painting context has a [Canvas], which receives the /// individual draw operations, and also has functions for painting child /// render objects. /// @@ -57,10 +57,9 @@ typedef void PaintingContextCallback(PaintingContext context, Offset offset); /// not hold a reference to the canvas across operations that might paint /// child render objects. class PaintingContext { - PaintingContext._(this._containerLayer, this._paintBounds) { - assert(_containerLayer != null); - assert(_paintBounds != null); - } + PaintingContext._(this._containerLayer, this._paintBounds) + : assert(_containerLayer != null), + assert(_paintBounds != null); final ContainerLayer _containerLayer; final Rect _paintBounds; @@ -243,7 +242,7 @@ class PaintingContext { _currentLayer?.willChangeHint = true; } - /// Adds a composited layer to the recording. + /// Adds a composited leaf layer to the recording. /// /// After calling this function, the [canvas] property will change to refer to /// a new [Canvas] that draws on top of the given layer. @@ -251,12 +250,46 @@ class PaintingContext { /// A [RenderObject] that uses this function is very likely to require its /// [RenderObject.alwaysNeedsCompositing] property to return true. That informs /// ancestor render objects that this render object will include a composited - /// layer, which causes them to use composited clips, for example. + /// layer, which, for example, causes them to use composited clips. + /// + /// See also: + /// + /// * [pushLayer], for adding a layer and using its canvas to paint with that + /// layer. void addLayer(Layer layer) { _stopRecordingIfNeeded(); _appendLayer(layer); } + /// Appends the given layer to the recording, and calls the `painter` callback + /// with that layer, providing the [childPaintBounds] as the paint bounds of + /// the child. Canvas recording commands are not guaranteed to be stored + /// outside of the paint bounds. + /// + /// The given layer must be an unattached orphan. (Providing a newly created + /// object, rather than reusing an existing layer, satisfies that + /// requirement.) + /// + /// The `offset` is the offset to pass to the `painter`. + /// + /// If the `childPaintBounds` are not specified then the current layer's + /// bounds are used. This is appropriate if the child layer does not apply any + /// transformation or clipping to its contents. + /// + /// See also: + /// + /// * [addLayer], for pushing a leaf layer whose canvas is not used. + void pushLayer(Layer childLayer, PaintingContextCallback painter, Offset offset, { Rect childPaintBounds }) { + assert(!childLayer.attached); + assert(childLayer.parent == null); + assert(painter != null); + _stopRecordingIfNeeded(); + _appendLayer(childLayer); + final PaintingContext childContext = new PaintingContext._(childLayer, childPaintBounds ?? _paintBounds); + painter(childContext, offset); + childContext._stopRecordingIfNeeded(); + } + /// Clip further painting using a rectangle. /// /// * `needsCompositing` is whether the child needs compositing. Typically @@ -270,12 +303,7 @@ class PaintingContext { void pushClipRect(bool needsCompositing, Offset offset, Rect clipRect, PaintingContextCallback painter) { final Rect offsetClipRect = clipRect.shift(offset); if (needsCompositing) { - _stopRecordingIfNeeded(); - final ClipRectLayer clipLayer = new ClipRectLayer(clipRect: offsetClipRect); - _appendLayer(clipLayer); - final PaintingContext childContext = new PaintingContext._(clipLayer, offsetClipRect); - painter(childContext, offset); - childContext._stopRecordingIfNeeded(); + pushLayer(new ClipRectLayer(clipRect: offsetClipRect), painter, offset, childPaintBounds: offsetClipRect); } else { canvas.save(); canvas.clipRect(offsetClipRect); @@ -300,12 +328,7 @@ class PaintingContext { final Rect offsetBounds = bounds.shift(offset); final RRect offsetClipRRect = clipRRect.shift(offset); if (needsCompositing) { - _stopRecordingIfNeeded(); - final ClipRRectLayer clipLayer = new ClipRRectLayer(clipRRect: offsetClipRRect); - _appendLayer(clipLayer); - final PaintingContext childContext = new PaintingContext._(clipLayer, offsetBounds); - painter(childContext, offset); - childContext._stopRecordingIfNeeded(); + pushLayer(new ClipRRectLayer(clipRRect: offsetClipRRect), painter, offset, childPaintBounds: offsetBounds); } else { canvas.saveLayer(offsetBounds, _defaultPaint); canvas.clipRRect(offsetClipRRect); @@ -330,12 +353,7 @@ class PaintingContext { final Rect offsetBounds = bounds.shift(offset); final Path offsetClipPath = clipPath.shift(offset); if (needsCompositing) { - _stopRecordingIfNeeded(); - final ClipPathLayer clipLayer = new ClipPathLayer(clipPath: offsetClipPath); - _appendLayer(clipLayer); - final PaintingContext childContext = new PaintingContext._(clipLayer, offsetBounds); - painter(childContext, offset); - childContext._stopRecordingIfNeeded(); + pushLayer(new ClipPathLayer(clipPath: offsetClipPath), painter, offset, childPaintBounds: offsetBounds); } else { canvas.saveLayer(bounds.shift(offset), _defaultPaint); canvas.clipPath(clipPath.shift(offset)); @@ -357,13 +375,12 @@ class PaintingContext { final Matrix4 effectiveTransform = new Matrix4.translationValues(offset.dx, offset.dy, 0.0) ..multiply(transform)..translate(-offset.dx, -offset.dy); if (needsCompositing) { - _stopRecordingIfNeeded(); - final TransformLayer transformLayer = new TransformLayer(transform: effectiveTransform); - _appendLayer(transformLayer); - final Rect transformedPaintBounds = MatrixUtils.inverseTransformRect(effectiveTransform, _paintBounds); - final PaintingContext childContext = new PaintingContext._(transformLayer, transformedPaintBounds); - painter(childContext, offset); - childContext._stopRecordingIfNeeded(); + pushLayer( + new TransformLayer(transform: effectiveTransform), + painter, + offset, + childPaintBounds: MatrixUtils.inverseTransformRect(effectiveTransform, _paintBounds), + ); } else { canvas.save(); canvas.transform(effectiveTransform.storage); @@ -385,112 +402,9 @@ class PaintingContext { /// A [RenderObject] that uses this function is very likely to require its /// [RenderObject.alwaysNeedsCompositing] property to return true. That informs /// ancestor render objects that this render object will include a composited - /// layer, which causes them to use composited clips, for example. + /// layer, which, for example, causes them to use composited clips. void pushOpacity(Offset offset, int alpha, PaintingContextCallback painter) { - _stopRecordingIfNeeded(); - final OpacityLayer opacityLayer = new OpacityLayer(alpha: alpha); - _appendLayer(opacityLayer); - final PaintingContext childContext = new PaintingContext._(opacityLayer, _paintBounds); - painter(childContext, offset); - childContext._stopRecordingIfNeeded(); - } - - /// Apply a mask derived from a shader to further painting. - /// - /// * `offset` is the offset from the origin of the canvas' coordinate system - /// to the origin of the caller's coordinate system. - /// * `shader` is the shader that will generate the mask. The shader operates - /// in the coordinate system of the caller. - /// * `maskRect` is the region of the canvas (in the coodinate system of the - /// caller) in which to apply the mask. - /// * `blendMode` is the [BlendMode] to use when applying the shader to - /// the painting done by `painter`. - /// * `painter` is a callback that will paint with the mask applied. This - /// function calls the `painter` synchronously. - /// - /// A [RenderObject] that uses this function is very likely to require its - /// [RenderObject.alwaysNeedsCompositing] property to return true. That informs - /// ancestor render objects that this render object will include a composited - /// layer, which causes them to use composited clips, for example. - void pushShaderMask(Offset offset, Shader shader, Rect maskRect, BlendMode blendMode, PaintingContextCallback painter) { - _stopRecordingIfNeeded(); - final ShaderMaskLayer shaderLayer = new ShaderMaskLayer( - shader: shader, - maskRect: maskRect, - blendMode: blendMode, - ); - _appendLayer(shaderLayer); - final PaintingContext childContext = new PaintingContext._(shaderLayer, _paintBounds); - painter(childContext, offset); - childContext._stopRecordingIfNeeded(); - } - - /// Push a backdrop filter. - /// - /// This function applies a filter to the existing painted content and then - /// synchronously calls the painter to paint on top of the filtered backdrop. - /// - /// A [RenderObject] that uses this function is very likely to require its - /// [RenderObject.alwaysNeedsCompositing] property to return true. That informs - /// ancestor render objects that this render object will include a composited - /// layer, which causes them to use composited clips, for example. - // TODO(abarth): I don't quite understand how this API is supposed to work. - void pushBackdropFilter(Offset offset, ui.ImageFilter filter, PaintingContextCallback painter) { - _stopRecordingIfNeeded(); - final BackdropFilterLayer backdropFilterLayer = new BackdropFilterLayer(filter: filter); - _appendLayer(backdropFilterLayer); - final PaintingContext childContext = new PaintingContext._(backdropFilterLayer, _paintBounds); - painter(childContext, offset); - childContext._stopRecordingIfNeeded(); - } - - /// Clip using a physical model layer. - /// - /// * `offset` is the offset from the origin of the canvas' coordinate system - /// to the origin of the caller's coordinate system. - /// * `bounds` is the region of the canvas (in the caller's coodinate system) - /// into which `painter` will paint in. - /// * `clipRRect` is the rounded-rectangle (in the caller's coodinate system) - /// to use to clip the painting done by `painter`. - /// * `elevation` is the z-coordinate at which to place this material. - /// * `color` is the background color. - /// * `painter` is a callback that will paint with the `clipRRect` applied. This - /// function calls the `painter` synchronously. - void pushPhysicalModel(bool needsCompositing, Offset offset, Rect bounds, RRect clipRRect, double elevation, Color color, PaintingContextCallback painter) { - final Rect offsetBounds = bounds.shift(offset); - final RRect offsetClipRRect = clipRRect.shift(offset); - if (needsCompositing) { - _stopRecordingIfNeeded(); - final PhysicalModelLayer physicalModel = new PhysicalModelLayer( - clipRRect: offsetClipRRect, - elevation: elevation, - color: color, - ); - _appendLayer(physicalModel); - final PaintingContext childContext = new PaintingContext._(physicalModel, offsetBounds); - painter(childContext, offset); - childContext._stopRecordingIfNeeded(); - } else { - if (elevation != 0) { - // The drawShadow call doesn't add the region of the shadow to the - // picture's bounds, so we draw a hardcoded amount of extra space to - // account for the maximum potential area of the shadow. - // TODO(jsimmons): remove this when Skia does it for us. - canvas.drawRect(offsetBounds.inflate(20.0), - new Paint()..color=const Color(0)); - canvas.drawShadow( - new Path()..addRRect(offsetClipRRect), - const Color(0xFF000000), - elevation.toDouble(), - color.alpha != 0xFF, - ); - } - canvas.drawRRect(offsetClipRRect, new Paint()..color=color); - canvas.saveLayer(offsetBounds, _defaultPaint); - canvas.clipRRect(offsetClipRRect); - painter(this, offset); - canvas.restore(); - } + pushLayer(new OpacityLayer(alpha: alpha), painter, offset); } } @@ -652,26 +566,34 @@ class _SemanticsGeometry { } } +/// Describes the shape of the semantic tree. +/// +/// A [_SemanticsFragment] object is a node in a short-lived tree which is used +/// to create the final [SemanticsNode] tree that is sent to the semantics +/// server. These objects have a [SemanticsAnnotator], and a list of +/// [_SemanticsFragment] children. abstract class _SemanticsFragment { _SemanticsFragment({ @required RenderObject renderObjectOwner, this.annotator, - List<_SemanticsFragment> children - }) { - assert(renderObjectOwner != null); - _ancestorChain = [renderObjectOwner]; - assert(() { - if (children == null) - return true; - final Set<_SemanticsFragment> seenChildren = new Set<_SemanticsFragment>(); - for (_SemanticsFragment child in children) - assert(seenChildren.add(child)); // check for duplicate adds - return true; - }); - _children = children ?? const <_SemanticsFragment>[]; - } + List<_SemanticsFragment> children, + this.dropSemanticsOfPreviousSiblings, + }) : assert(renderObjectOwner != null), + assert(() { + if (children == null) + return true; + final Set<_SemanticsFragment> seenChildren = new Set<_SemanticsFragment>(); + for (_SemanticsFragment child in children) + assert(seenChildren.add(child)); // check for duplicate adds + return true; + }), + _ancestorChain = [renderObjectOwner], + _children = children ?? const <_SemanticsFragment>[]; final SemanticsAnnotator annotator; + bool dropSemanticsOfPreviousSiblings; + + bool get producesSemanticNodes => true; List _ancestorChain; void addAncestor(RenderObject ancestor) { @@ -689,16 +611,35 @@ abstract class _SemanticsFragment { String toString() => '$runtimeType#$hashCode'; } -/// Represents a subtree that doesn't need updating, it already has a -/// SemanticsNode and isn't dirty. (We still update the matrix, since -/// that comes from the (dirty) ancestors.) +/// A SemanticsFragment that doesn't produce any [SemanticsNode]s when compiled. +class _EmptySemanticsFragment extends _SemanticsFragment { + _EmptySemanticsFragment({ + @required RenderObject renderObjectOwner, + bool dropSemanticsOfPreviousSiblings, + }) : super(renderObjectOwner: renderObjectOwner, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); + + @override + Iterable compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics }) sync* { } + + @override + bool get producesSemanticNodes => false; +} + +/// Represents a [RenderObject] which is in no way dirty. +/// +/// This class has no children and no annotators, and when compiled, it returns +/// the [SemanticsNode] that the [RenderObject] already has. (We still update +/// the matrix, since that comes from the (dirty) ancestors.) class _CleanSemanticsFragment extends _SemanticsFragment { _CleanSemanticsFragment({ - @required RenderObject renderObjectOwner - }) : super(renderObjectOwner: renderObjectOwner) { - assert(renderObjectOwner != null); - assert(renderObjectOwner._semantics != null); - } + @required RenderObject renderObjectOwner, + bool dropSemanticsOfPreviousSiblings, + }) : assert(renderObjectOwner != null), + assert(renderObjectOwner._semantics != null), + super( + renderObjectOwner: renderObjectOwner, + dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings + ); @override Iterable compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics }) sync* { @@ -720,8 +661,9 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment { _InterestingSemanticsFragment({ RenderObject renderObjectOwner, SemanticsAnnotator annotator, - Iterable<_SemanticsFragment> children - }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children); + Iterable<_SemanticsFragment> children, + bool dropSemanticsOfPreviousSiblings, + }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); bool get haveConcreteNode => true; @@ -750,12 +692,16 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment { _SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry); } +/// Represents the [RenderObject] found at the top of the render tree. +/// +/// This class always compiles to a [SemanticsNode] with ID 0. class _RootSemanticsFragment extends _InterestingSemanticsFragment { _RootSemanticsFragment({ RenderObject renderObjectOwner, SemanticsAnnotator annotator, - Iterable<_SemanticsFragment> children - }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children); + Iterable<_SemanticsFragment> children, + bool dropSemanticsOfPreviousSiblings, + }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); @override SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) { @@ -780,12 +726,16 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment { } } +/// Represents a RenderObject that has [isSemanticBoundary] set to `true`. +/// +/// It returns the SemanticsNode for that [RenderObject]. class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment { _ConcreteSemanticsFragment({ RenderObject renderObjectOwner, SemanticsAnnotator annotator, - Iterable<_SemanticsFragment> children - }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children); + Iterable<_SemanticsFragment> children, + bool dropSemanticsOfPreviousSiblings, + }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); @override SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) { @@ -808,12 +758,20 @@ class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment { } } +/// Represents a RenderObject that does not have [isSemanticBoundary] set to +/// `true`, but which does have some semantic annotators. +/// +/// When it is compiled, if the nearest ancestor [_SemanticsFragment] that isn't +/// also an [_ImplicitSemanticsFragment] is a [_RootSemanticsFragment] or a +/// [_ConcreteSemanticsFragment], then the [SemanticsNode] from that object is +/// reused. Otherwise, a new one is created. class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment { _ImplicitSemanticsFragment({ RenderObject renderObjectOwner, SemanticsAnnotator annotator, - Iterable<_SemanticsFragment> children - }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children); + Iterable<_SemanticsFragment> children, + bool dropSemanticsOfPreviousSiblings, + }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); @override bool get haveConcreteNode => _haveConcreteNode; @@ -851,14 +809,21 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment { } } +/// Represents a [RenderObject] that introduces no semantics of its own, but +/// which has two or more descendants that do introduce semantics +/// (and which are not ancestors or descendants of each other). class _ForkingSemanticsFragment extends _SemanticsFragment { _ForkingSemanticsFragment({ RenderObject renderObjectOwner, - @required Iterable<_SemanticsFragment> children - }) : super(renderObjectOwner: renderObjectOwner, children: children) { - assert(children != null); - assert(children.length > 1); - } + @required Iterable<_SemanticsFragment> children, + bool dropSemanticsOfPreviousSiblings, + }) : assert(children != null), + assert(children.length > 1), + super( + renderObjectOwner: renderObjectOwner, + children: children, + dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings + ); @override Iterable compile({ @@ -898,8 +863,8 @@ class _ForkingSemanticsFragment extends _SemanticsFragment { /// [PipelineOwner] for the render tree from which you wish to read semantics. /// You can obtain the [PipelineOwner] using the [RenderObject.owner] property. class SemanticsHandle { - SemanticsHandle._(this._owner, this.listener) { - assert(_owner != null); + SemanticsHandle._(this._owner, this.listener) + : assert(_owner != null) { if (listener != null) _owner.semanticsOwner.addListener(listener); } @@ -1180,7 +1145,11 @@ class PipelineOwner { bool _debugDoingSemantics = false; final List _nodesNeedingSemantics = []; - /// Update the semantics for all render objects. + /// Update the semantics for render objects marked as needing a semantics + /// update. + /// + /// Initially, only the root node, as scheduled by + /// [RenderObjectscheduleInitialSemantics], needs a semantics update. /// /// This function is one of the core stages of the rendering pipeline. The /// semantics are compiled after painting and only after @@ -1386,6 +1355,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { super.adoptChild(child); markNeedsLayout(); markNeedsCompositingBitsUpdate(); + markNeedsSemanticsUpdate(); } /// Called by subclasses when they decide a render object is no longer a child. @@ -1403,6 +1373,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { super.dropChild(child); markNeedsLayout(); markNeedsCompositingBitsUpdate(); + markNeedsSemanticsUpdate(); } /// Calls visitor for each immediate child of this render object. @@ -2026,8 +1997,8 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { /// creates at least one composited layer. For example, videos should return /// true if they use hardware decoders. /// - /// You must call markNeedsCompositingBitsUpdate() if the value of this - /// getter changes. + /// You must call [markNeedsCompositingBitsUpdate] if the value of this getter + /// changes. (This is implied when [adoptChild] or [dropChild] are called.) @protected bool get alwaysNeedsCompositing => false; @@ -2345,7 +2316,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { /// the approximate bounding box of the clip rect that would be /// applied to the given child during the paint phase, if any. /// - /// Returns `null` if the child would not be clipped. + /// Returns null if the child would not be clipped. /// /// This is used in the semantics phase to avoid including children /// that are not physically visible. @@ -2380,6 +2351,22 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { /// setting [isSemanticBoundary] to true. bool get isSemanticBoundary => false; + /// Whether this [RenderObject] makes other [RenderObject]s previously painted + /// within the same semantic boundary unreachable for accessibility purposes. + /// + /// If `true` is returned, the [SemanticsNode]s for all siblings and cousins + /// of this node, that are earlier in a depth-first pre-order traversal, are + /// dropped from the semantics tree up until a semantic boundary (as defined + /// by [isSemanticBoundary]) is reached. + /// + /// If [isSemanticBoundary] and [isBlockingSemanticsOfPreviouslyPaintedNodes] + /// is set on the same node, all previously painted siblings and cousins + /// up until the next ancestor that is a semantic boundary are dropped. + /// + /// Paint order as established by [visitChildrenForSemantics] is used to + /// determine if a node is previous to this one. + bool get isBlockingSemanticsOfPreviouslyPaintedNodes => false; + /// The bounding box, in the local coordinate system, of this /// object, for accessibility purposes. Rect get semanticBounds; @@ -2486,6 +2473,13 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { } } + /// Updates the semantic information of the render object. + /// + /// This is essentially a two-pass walk of the render tree. The first pass + /// determines the shape of the output tree (captured in + /// [_SemanticsFragment]s), and the second creates the nodes of this tree and + /// hooks them together. The second walk is a sparse walk; it only walks the + /// nodes that are interesting for the purpose of semantics. void _updateSemantics() { try { assert(_needsSemanticsUpdate); @@ -2500,13 +2494,20 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { } } + /// Core function that walks the render tree to obtain the semantics. + /// + /// It collects semantic annotators for this RenderObject, then walks its + /// children collecting [_SemanticsFragments] for them, and then returns an + /// appropriate [_SemanticsFragment] object that describes the RenderObject's + /// semantics. _SemanticsFragment _getSemanticsFragment() { // early-exit if we're not dirty and have our own semantics if (!_needsSemanticsUpdate && isSemanticBoundary) { assert(_semantics != null); - return new _CleanSemanticsFragment(renderObjectOwner: this); + return new _CleanSemanticsFragment(renderObjectOwner: this, dropSemanticsOfPreviousSiblings: isBlockingSemanticsOfPreviouslyPaintedNodes); } List<_SemanticsFragment> children; + bool dropSemanticsOfPreviousSiblings = isBlockingSemanticsOfPreviouslyPaintedNodes; visitChildrenForSemantics((RenderObject child) { if (_needsSemanticsGeometryUpdate) { // If our geometry changed, make sure the child also does a @@ -2516,34 +2517,47 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { child._needsSemanticsGeometryUpdate = true; } final _SemanticsFragment fragment = child._getSemanticsFragment(); - if (fragment != null) { + assert(fragment != null); + if (fragment.dropSemanticsOfPreviousSiblings) { + children = null; // throw away all left siblings of [child]. + dropSemanticsOfPreviousSiblings = true; + } + if (fragment.producesSemanticNodes) { fragment.addAncestor(this); children ??= <_SemanticsFragment>[]; assert(!children.contains(fragment)); children.add(fragment); } }); + if (isSemanticBoundary && !isBlockingSemanticsOfPreviouslyPaintedNodes) { + // Don't propagate [dropSemanticsOfPreviousSiblings] up through a semantic boundary. + dropSemanticsOfPreviousSiblings = false; + } _needsSemanticsUpdate = false; _needsSemanticsGeometryUpdate = false; final SemanticsAnnotator annotator = semanticsAnnotator; if (parent is! RenderObject) - return new _RootSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children); + return new _RootSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); if (isSemanticBoundary) - return new _ConcreteSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children); + return new _ConcreteSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); if (annotator != null) - return new _ImplicitSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children); + return new _ImplicitSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); _semantics = null; - if (children == null) - return null; + if (children == null) { + // Introduces no semantics and has no descendants that introduce semantics. + return new _EmptySemanticsFragment(renderObjectOwner: this, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); + } if (children.length > 1) - return new _ForkingSemanticsFragment(renderObjectOwner: this, children: children); + return new _ForkingSemanticsFragment(renderObjectOwner: this, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); assert(children.length == 1); - return children.single; + return children.single..dropSemanticsOfPreviousSiblings = dropSemanticsOfPreviousSiblings; } - /// Called when collecting the semantics of this node. Subclasses - /// that have children that are not semantically relevant (e.g. - /// because they are invisible) should skip those children here. + /// Called when collecting the semantics of this node. + /// + /// The implementation has to return the children in paint order skipping all + /// children that are not semantically relevant (e.g. because they are + /// invisible). /// /// The default implementation mirrors the behavior of /// [visitChildren()] (which is supposed to walk all the children). @@ -2682,6 +2696,10 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { description.add('layer: $_layer'); if (_semantics != null) description.add('semantics: $_semantics'); + if (isBlockingSemanticsOfPreviouslyPaintedNodes) + description.add('blocks semantics of earlier render objects below the common boundary'); + if (isSemanticBoundary) + description.add('semantic boundary'); } /// Returns a string describing the current node's descendants. Each line of @@ -2694,7 +2712,11 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { /// Generic mixin for render objects with one child. /// /// Provides a child model for a render object subclass that has a unique child. -abstract class RenderObjectWithChildMixin implements RenderObject { +abstract class RenderObjectWithChildMixin extends RenderObject { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory RenderObjectWithChildMixin._() => null; + /// Checks whether the given render object has the correct [runtimeType] to be /// a child of this render object. /// @@ -2771,7 +2793,11 @@ abstract class RenderObjectWithChildMixin implem } /// Parent data to support a doubly-linked list of children. -abstract class ContainerParentDataMixin implements ParentData { +abstract class ContainerParentDataMixin extends ParentData { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory ContainerParentDataMixin._() => null; + /// The previous sibling in the parent's child list. ChildType previousSibling; /// The next sibling in the parent's child list. @@ -2802,7 +2828,10 @@ abstract class ContainerParentDataMixin implemen /// /// Provides a child model for a render object subclass that has a doubly-linked /// list of children. -abstract class ContainerRenderObjectMixin> implements RenderObject { +abstract class ContainerRenderObjectMixin> extends RenderObject { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory ContainerRenderObjectMixin._() => null; bool _debugUltimatePreviousSiblingOf(ChildType child, { ChildType equals }) { ParentDataType childParentData = child.parentData; diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart index 043f9540b0eb5..5262cdd4df3b4 100644 --- a/packages/flutter/lib/src/rendering/paragraph.dart +++ b/packages/flutter/lib/src/rendering/paragraph.dart @@ -31,14 +31,24 @@ const String _kEllipsis = '\u2026'; class RenderParagraph extends RenderBox { /// Creates a paragraph render object. /// - /// The [text], [overflow], and [softWrap] arguments must not be null. + /// The [text], [overflow], [softWrap], and [textScaleFactor] arguments must + /// not be null. + /// + /// The [maxLines] property may be null (and indeed defaults to null), but if + /// it is not null, it must be greater than zero. RenderParagraph(TextSpan text, { TextAlign textAlign, bool softWrap: true, TextOverflow overflow: TextOverflow.clip, double textScaleFactor: 1.0, int maxLines, - }) : _softWrap = softWrap, + }) : assert(text != null), + assert(text.debugAssertIsValid()), + assert(softWrap != null), + assert(overflow != null), + assert(textScaleFactor != null), + assert(maxLines == null || maxLines > 0), + _softWrap = softWrap, _overflow = overflow, _textPainter = new TextPainter( text: text, @@ -46,13 +56,7 @@ class RenderParagraph extends RenderBox { textScaleFactor: textScaleFactor, maxLines: maxLines, ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null, - ) { - assert(text != null); - assert(text.debugAssertIsValid()); - assert(softWrap != null); - assert(overflow != null); - assert(textScaleFactor != null); - } + ); final TextPainter _textPainter; @@ -78,7 +82,11 @@ class RenderParagraph extends RenderBox { /// Whether the text should break at soft line breaks. /// - /// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space. + /// If false, the glyphs in the text will be positioned as if there was + /// unlimited horizontal space. + /// + /// If [softWrap] is false, [overflow] and [textAlign] may have unexpected + /// effects. bool get softWrap => _softWrap; bool _softWrap; set softWrap(bool value) { @@ -117,9 +125,11 @@ class RenderParagraph extends RenderBox { /// An optional maximum number of lines for the text to span, wrapping if necessary. /// If the text exceeds the given number of lines, it will be truncated according - /// to [overflow]. + /// to [overflow] and [softWrap]. int get maxLines => _textPainter.maxLines; + /// The value may be null. If it is not null, then it must be greater than zero. set maxLines(int value) { + assert(value == null || value > 0); if (_textPainter.maxLines == value) return; _textPainter.maxLines = value; @@ -128,8 +138,7 @@ class RenderParagraph extends RenderBox { } void _layoutText({ double minWidth: 0.0, double maxWidth: double.INFINITY }) { - final bool wrap = _softWrap || (_overflow == TextOverflow.ellipsis && maxLines == null); - _textPainter.layout(minWidth: minWidth, maxWidth: wrap ? maxWidth : double.INFINITY); + _textPainter.layout(minWidth: minWidth, maxWidth: _softWrap ? maxWidth : double.INFINITY); } void _layoutTextWithConstraints(BoxConstraints constraints) { diff --git a/packages/flutter/lib/src/rendering/performance_overlay.dart b/packages/flutter/lib/src/rendering/performance_overlay.dart index 22fd8cf66e49a..8d06080295c1a 100644 --- a/packages/flutter/lib/src/rendering/performance_overlay.dart +++ b/packages/flutter/lib/src/rendering/performance_overlay.dart @@ -66,15 +66,14 @@ class RenderPerformanceOverlay extends RenderBox { int rasterizerThreshold: 0, bool checkerboardRasterCacheImages: false, bool checkerboardOffscreenLayers: false, - }) : _optionsMask = optionsMask, - _rasterizerThreshold = rasterizerThreshold, - _checkerboardRasterCacheImages = checkerboardRasterCacheImages, - _checkerboardOffscreenLayers = checkerboardOffscreenLayers { - assert(optionsMask != null); - assert(rasterizerThreshold != null); - assert(checkerboardRasterCacheImages != null); - assert(checkerboardOffscreenLayers != null); - } + }) : assert(optionsMask != null), + assert(rasterizerThreshold != null), + assert(checkerboardRasterCacheImages != null), + assert(checkerboardOffscreenLayers != null), + _optionsMask = optionsMask, + _rasterizerThreshold = rasterizerThreshold, + _checkerboardRasterCacheImages = checkerboardRasterCacheImages, + _checkerboardOffscreenLayers = checkerboardOffscreenLayers; /// The mask is created by shifting 1 by the index of the specific /// [PerformanceOverlayOption] to enable. diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 47a3a1d3d6cfc..86e59a3ec98dc 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -12,6 +12,7 @@ import 'package:vector_math/vector_math_64.dart'; import 'box.dart'; import 'debug.dart'; +import 'layer.dart'; import 'object.dart'; import 'semantics.dart'; @@ -50,7 +51,11 @@ class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin { +abstract class RenderProxyBoxMixin extends RenderBox with RenderObjectWithChildMixin { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory RenderProxyBoxMixin._() => null; + @override void setupParentData(RenderObject child) { // We don't actually use the offset argument in BoxParentData, so let's @@ -197,10 +202,10 @@ class RenderConstrainedBox extends RenderProxyBox { RenderConstrainedBox({ RenderBox child, @required BoxConstraints additionalConstraints, - }) : _additionalConstraints = additionalConstraints, super(child) { - assert(additionalConstraints != null); - assert(additionalConstraints.debugAssertIsValid()); - } + }) : assert(additionalConstraints != null), + assert(additionalConstraints.debugAssertIsValid()), + _additionalConstraints = additionalConstraints, + super(child); /// Additional constraints to apply to [child] during layout BoxConstraints get additionalConstraints => _additionalConstraints; @@ -307,10 +312,11 @@ class RenderLimitedBox extends RenderProxyBox { RenderBox child, double maxWidth: double.INFINITY, double maxHeight: double.INFINITY - }) : _maxWidth = maxWidth, _maxHeight = maxHeight, super(child) { - assert(maxWidth != null && maxWidth >= 0.0); - assert(maxHeight != null && maxHeight >= 0.0); - } + }) : assert(maxWidth != null && maxWidth >= 0.0), + assert(maxHeight != null && maxHeight >= 0.0), + _maxWidth = maxWidth, + _maxHeight = maxHeight, + super(child); /// The value to use for maxWidth if the incoming maxWidth constraint is infinite. double get maxWidth => _maxWidth; @@ -396,11 +402,11 @@ class RenderAspectRatio extends RenderProxyBox { RenderAspectRatio({ RenderBox child, @required double aspectRatio, - }) : _aspectRatio = aspectRatio, super(child) { - assert(aspectRatio != null); - assert(aspectRatio > 0.0); - assert(aspectRatio.isFinite); - } + }) : assert(aspectRatio != null), + assert(aspectRatio > 0.0), + assert(aspectRatio.isFinite), + _aspectRatio = aspectRatio, + super(child); /// The aspect ratio to attempt to use. /// @@ -538,7 +544,10 @@ class RenderAspectRatio extends RenderProxyBox { /// you would like a child that would otherwise attempt to expand infinitely to /// instead size itself to a more reasonable width. /// -/// This class is relatively expensive. Avoid using it where possible. +/// This class is relatively expensive, because it adds a speculative layout +/// pass before the final layout phase. Avoid using it where possible. In the +/// worst case, this render object can result in a layout that is O(N²) in the +/// depth of the tree. class RenderIntrinsicWidth extends RenderProxyBox { /// Creates a render object that sizes itself to its child's intrinsic width. RenderIntrinsicWidth({ @@ -644,7 +653,10 @@ class RenderIntrinsicWidth extends RenderProxyBox { /// you would like a child that would otherwise attempt to expand infinitely to /// instead size itself to a more reasonable height. /// -/// This class is relatively expensive. Avoid using it where possible. +/// This class is relatively expensive, because it adds a speculative layout +/// pass before the final layout phase. Avoid using it where possible. In the +/// worst case, this render object can result in a layout that is O(N²) in the +/// depth of the tree. class RenderIntrinsicHeight extends RenderProxyBox { /// Creates a render object that sizes itself to its child's intrinsic height. RenderIntrinsicHeight({ @@ -710,10 +722,11 @@ class RenderOpacity extends RenderProxyBox { /// /// The [opacity] argument must be between 0.0 and 1.0, inclusive. RenderOpacity({ double opacity: 1.0, RenderBox child }) - : _opacity = opacity, _alpha = _getAlphaFromOpacity(opacity), super(child) { - assert(opacity != null); - assert(opacity >= 0.0 && opacity <= 1.0); - } + : assert(opacity != null), + assert(opacity >= 0.0 && opacity <= 1.0), + _opacity = opacity, + _alpha = _getAlphaFromOpacity(opacity), + super(child); @override bool get alwaysNeedsCompositing => child != null && (_alpha != 0 && _alpha != 255); @@ -773,7 +786,7 @@ class RenderOpacity extends RenderProxyBox { /// Signature for a function that creates a [Shader] for a given [Rect]. /// -/// Used by [RenderShaderMask]. +/// Used by [RenderShaderMask] and the [ShaderMask] widget. typedef Shader ShaderCallback(Rect bounds); /// Applies a mask generated by a [Shader] to its child. @@ -788,10 +801,11 @@ class RenderShaderMask extends RenderProxyBox { RenderBox child, @required ShaderCallback shaderCallback, BlendMode blendMode: BlendMode.modulate, - }) : _shaderCallback = shaderCallback, _blendMode = blendMode, super(child) { - assert(shaderCallback != null); - assert(blendMode != null); - } + }) : assert(shaderCallback != null), + assert(blendMode != null), + _shaderCallback = shaderCallback, + _blendMode = blendMode, + super(child); /// Called to creates the [Shader] that generates the mask. /// @@ -830,8 +844,15 @@ class RenderShaderMask extends RenderProxyBox { void paint(PaintingContext context, Offset offset) { if (child != null) { assert(needsCompositing); - final Rect rect = Offset.zero & size; - context.pushShaderMask(offset, _shaderCallback(rect), rect, _blendMode, super.paint); + context.pushLayer( + new ShaderMaskLayer( + shader: _shaderCallback(offset & size), + maskRect: offset & size, + blendMode: _blendMode, + ), + super.paint, + offset, + ); } } } @@ -845,9 +866,9 @@ class RenderBackdropFilter extends RenderProxyBox { /// /// The [filter] argument must not be null. RenderBackdropFilter({ RenderBox child, @required ui.ImageFilter filter }) - : _filter = filter, super(child) { - assert(filter != null); - } + : assert(filter != null), + _filter = filter, + super(child); /// The image filter to apply to the existing painted content before painting /// the child. @@ -871,7 +892,7 @@ class RenderBackdropFilter extends RenderProxyBox { void paint(PaintingContext context, Offset offset) { if (child != null) { assert(needsCompositing); - context.pushBackdropFilter(offset, _filter, super.paint); + context.pushLayer(new BackdropFilterLayer(filter: _filter), super.paint, offset); } } } @@ -1217,15 +1238,14 @@ class RenderPhysicalModel extends _RenderCustomClip { BorderRadius borderRadius, double elevation: 0.0, @required Color color, - }) : _shape = shape, + }) : assert(shape != null), + assert(elevation != null), + assert(color != null), + _shape = shape, _borderRadius = borderRadius, _elevation = elevation, _color = color, - super(child: child) { - assert(shape != null); - assert(elevation != null); - assert(color != null); - } + super(child: child); /// The shape of the layer. /// @@ -1302,11 +1322,47 @@ class RenderPhysicalModel extends _RenderCustomClip { return super.hitTest(result, position: position); } + static final Paint _defaultPaint = new Paint(); + static final Paint _transparentPaint = new Paint()..color = const Color(0x00000000); + @override void paint(PaintingContext context, Offset offset) { if (child != null) { _updateClip(); - context.pushPhysicalModel(needsCompositing, offset, _clip.outerRect, _clip, elevation, color, super.paint); + final RRect offsetClipRRect = _clip.shift(offset); + final Rect offsetBounds = offsetClipRRect.outerRect; + if (needsCompositing) { + final PhysicalModelLayer physicalModel = new PhysicalModelLayer( + clipRRect: offsetClipRRect, + elevation: elevation, + color: color, + ); + context.pushLayer(physicalModel, super.paint, offset, childPaintBounds: offsetBounds); + } else { + final Canvas canvas = context.canvas; + if (elevation != 0.0) { + // The drawShadow call doesn't add the region of the shadow to the + // picture's bounds, so we draw a hardcoded amount of extra space to + // account for the maximum potential area of the shadow. + // TODO(jsimmons): remove this when Skia does it for us. + canvas.drawRect( + offsetBounds.inflate(20.0), + _transparentPaint, + ); + canvas.drawShadow( + new Path()..addRRect(offsetClipRRect), + const Color(0xFF000000), + elevation, + color.alpha != 0xFF, + ); + } + canvas.drawRRect(offsetClipRRect, new Paint()..color = color); + canvas.saveLayer(offsetBounds, _defaultPaint); + canvas.clipRRect(offsetClipRRect); + super.paint(context, offset); + canvas.restore(); + assert(context.canvas == canvas, 'canvas changed even though needsCompositing was false'); + } } } @@ -1343,14 +1399,13 @@ class RenderDecoratedBox extends RenderProxyBox { DecorationPosition position: DecorationPosition.background, ImageConfiguration configuration: ImageConfiguration.empty, RenderBox child - }) : _decoration = decoration, + }) : assert(decoration != null), + assert(position != null), + assert(configuration != null), + _decoration = decoration, _position = position, _configuration = configuration, - super(child) { - assert(decoration != null); - assert(position != null); - assert(configuration != null); - } + super(child); BoxPainter _painter; @@ -1464,9 +1519,9 @@ class RenderTransform extends RenderProxyBox { FractionalOffset alignment, this.transformHitTests: true, RenderBox child - }) : super(child) { - assert(transform != null); - assert(alignment == null || (alignment.dx != null && alignment.dy != null)); + }) : assert(transform != null), + assert(alignment == null || (alignment.dx != null && alignment.dy != null)), + super(child) { this.transform = transform; this.alignment = alignment; this.origin = origin; @@ -1504,7 +1559,7 @@ class RenderTransform extends RenderProxyBox { /// child as it is painted. When set to false, hit tests are performed /// ignoring the transformation. /// - /// applyPaintTransform(), and therefore localToGlobal() and globalToLocal(), + /// [applyPaintTransform], and therefore [localToGlobal] and [globalToLocal], /// always honor the transformation, regardless of the value of this property. bool transformHitTests; @@ -1629,10 +1684,11 @@ class RenderFittedBox extends RenderProxyBox { RenderBox child, BoxFit fit: BoxFit.contain, FractionalOffset alignment: FractionalOffset.center - }) : _fit = fit, _alignment = alignment, super(child) { - assert(fit != null); - assert(alignment != null && alignment.dx != null && alignment.dy != null); - } + }) : assert(fit != null), + assert(alignment != null && alignment.dx != null && alignment.dy != null), + _fit = fit, + _alignment = alignment, + super(child); /// How to inscribe the child into the space allocated during layout. BoxFit get fit => _fit; @@ -1767,9 +1823,9 @@ class RenderFractionalTranslation extends RenderProxyBox { FractionalOffset translation, this.transformHitTests: true, RenderBox child - }) : _translation = translation, super(child) { - assert(translation == null || (translation.dx != null && translation.dy != null)); - } + }) : assert(translation == null || (translation.dx != null && translation.dy != null)), + _translation = translation, + super(child); /// The translation to apply to the child, as a multiple of the size. FractionalOffset get translation => _translation; @@ -1868,7 +1924,14 @@ class RenderFractionalTranslation extends RenderProxyBox { /// } /// /// @override -/// bool shouldRepaint(CustomPainter oldDelegate) => false; +/// bool shouldRepaint(Sky oldDelegate) { +/// // Since this Sky painter has no fields, it always paints +/// // the same thing, and therefore we return false here. If +/// // we had fields (set from the constructor) then we would +/// // return true if any of them differed from the same +/// // fields on the oldDelegate. +/// return false; +/// } /// } /// ``` /// @@ -2009,12 +2072,11 @@ class RenderCustomPaint extends RenderProxyBox { CustomPainter foregroundPainter, Size preferredSize: Size.zero, RenderBox child, - }) : _painter = painter, + }) : assert(preferredSize != null), + _painter = painter, _foregroundPainter = foregroundPainter, _preferredSize = preferredSize, - super(child) { - assert(preferredSize != null); - } + super(child); /// The background custom paint delegate. /// @@ -2479,9 +2541,9 @@ class RenderOffstage extends RenderProxyBox { RenderOffstage({ bool offstage: true, RenderBox child - }) : _offstage = offstage, super(child) { - assert(offstage != null); - } + }) : assert(offstage != null), + _offstage = offstage, + super(child); /// Whether the child is hidden from the rest of the tree. /// @@ -2598,9 +2660,8 @@ class RenderAbsorbPointer extends RenderProxyBox { RenderAbsorbPointer({ RenderBox child, this.absorbing: true - }) : super(child) { - assert(absorbing != null); - } + }) : assert(absorbing != null), + super(child); /// Whether this render object absorbs pointers during hit testing. /// @@ -2812,13 +2873,14 @@ class RenderSemanticsAnnotations extends RenderProxyBox { RenderBox child, bool container: false, bool checked, - String label - }) : _container = container, + bool selected, + String label, + }) : assert(container != null), + _container = container, _checked = checked, + _selected = selected, _label = label, - super(child) { - assert(container != null); - } + super(child); /// If 'container' is true, this RenderObject will introduce a new /// node in the semantics tree. Otherwise, the semantics will be @@ -2840,8 +2902,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { markNeedsSemanticsUpdate(); } - /// If non-null, sets the "hasCheckedState" semantic to true and the - /// "isChecked" semantic to the given value. + /// If non-null, sets the [SemanticsNode.hasCheckedState] semantic to true and + /// the [SemanticsNode.isChecked] semantic to the given value. bool get checked => _checked; bool _checked; set checked(bool value) { @@ -2852,7 +2914,19 @@ class RenderSemanticsAnnotations extends RenderProxyBox { markNeedsSemanticsUpdate(onlyChanges: (value != null) == hadValue); } - /// If non-null, sets the "label" semantic to the given value. + /// If non-null, sets the [SemanticsNode.isSelected] semantic to the given + /// value. + bool get selected => _selected; + bool _selected; + set selected(bool value) { + if (selected == value) + return; + final bool hadValue = selected != null; + _selected = value; + markNeedsSemanticsUpdate(onlyChanges: (value != null) == hadValue); + } + + /// If non-null, sets the [SemanticsNode.label] semantic to the given value. String get label => _label; String _label; set label(String value) { @@ -2867,7 +2941,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { bool get isSemanticBoundary => container; @override - SemanticsAnnotator get semanticsAnnotator => checked != null || label != null ? _annotate : null; + SemanticsAnnotator get semanticsAnnotator => checked != null || selected != null || label != null ? _annotate : null; void _annotate(SemanticsNode node) { if (checked != null) { @@ -2875,11 +2949,27 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ..hasCheckedState = true ..isChecked = checked; } + if (selected != null) + node.isSelected = selected; if (label != null) node.label = label; } } +/// Causes the semantics of all earlier render objects below the same semantic +/// boundary to be dropped. +/// +/// This is useful in a stack where an opaque mask should prevent interactions +/// with the render objects painted below the mask. +class RenderBlockSemantics extends RenderProxyBox { + /// Create a render object that blocks semantics for nodes below it in paint + /// order. + RenderBlockSemantics({ RenderBox child }) : super(child); + + @override + bool get isBlockingSemanticsOfPreviouslyPaintedNodes => true; +} + /// Causes the semantics of all descendants to be merged into this /// node such that the entire subtree becomes a single leaf in the /// semantics tree. diff --git a/packages/flutter/lib/src/rendering/rotated_box.dart b/packages/flutter/lib/src/rendering/rotated_box.dart index c52d8222da0af..3db3641319ded 100644 --- a/packages/flutter/lib/src/rendering/rotated_box.dart +++ b/packages/flutter/lib/src/rendering/rotated_box.dart @@ -26,8 +26,8 @@ class RenderRotatedBox extends RenderBox with RenderObjectWithChildMixin (_flags & SemanticsFlags.isChecked.index) != 0; set isChecked(bool value) => _setFlag(SemanticsFlags.isChecked, value); + /// Whether the current node is selected (true) or not (false). + bool get isSelected => (_flags & SemanticsFlags.isSelected.index) != 0; + set isSelected(bool value) => _setFlag(SemanticsFlags.isSelected, value); + /// A textual description of this node. String get label => _label; String _label = ''; @@ -595,6 +599,8 @@ class SemanticsNode extends AbstractNode { else buffer.write('; unchecked'); } + if (isSelected) + buffer.write('; selected'); if (label.isNotEmpty) buffer.write('; "$label"'); buffer.write(')'); diff --git a/packages/flutter/lib/src/rendering/shifted_box.dart b/packages/flutter/lib/src/rendering/shifted_box.dart index a7f1c3ae6c496..0887c39648978 100644 --- a/packages/flutter/lib/src/rendering/shifted_box.dart +++ b/packages/flutter/lib/src/rendering/shifted_box.dart @@ -93,10 +93,10 @@ class RenderPadding extends RenderShiftedBox { RenderPadding({ @required EdgeInsets padding, RenderBox child - }) : _padding = padding, super(child) { - assert(padding != null); - assert(padding.isNonNegative); - } + }) : assert(padding != null), + assert(padding.isNonNegative), + _padding = padding, + super(child); /// The amount to pad the child in each dimension. EdgeInsets get padding => _padding; @@ -192,9 +192,9 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox { RenderAligningShiftedBox({ FractionalOffset alignment: FractionalOffset.center, RenderBox child - }) : _alignment = alignment, super(child) { - assert(alignment != null && alignment.dx != null && alignment.dy != null); - } + }) : assert(alignment != null && alignment.dx != null && alignment.dy != null), + _alignment = alignment, + super(child); /// How to align the child. /// @@ -259,12 +259,11 @@ class RenderPositionedBox extends RenderAligningShiftedBox { double widthFactor, double heightFactor, FractionalOffset alignment: FractionalOffset.center - }) : _widthFactor = widthFactor, + }) : assert(widthFactor == null || widthFactor >= 0.0), + assert(heightFactor == null || heightFactor >= 0.0), + _widthFactor = widthFactor, _heightFactor = heightFactor, - super(child: child, alignment: alignment) { - assert(widthFactor == null || widthFactor >= 0.0); - assert(heightFactor == null || heightFactor >= 0.0); - } + super(child: child, alignment: alignment); /// If non-null, sets its width to the child's width multipled by this factor. /// @@ -499,10 +498,9 @@ class RenderSizedOverflowBox extends RenderAligningShiftedBox { RenderBox child, @required Size requestedSize, FractionalOffset alignment: FractionalOffset.center - }) : _requestedSize = requestedSize, - super(child: child, alignment: alignment) { - assert(requestedSize != null); - } + }) : assert(requestedSize != null), + _requestedSize = requestedSize, + super(child: child, alignment: alignment); /// The size this render box should attempt to be. Size get requestedSize => _requestedSize; @@ -791,9 +789,9 @@ class RenderCustomSingleChildLayoutBox extends RenderShiftedBox { RenderCustomSingleChildLayoutBox({ RenderBox child, @required SingleChildLayoutDelegate delegate - }) : _delegate = delegate, super(child) { - assert(delegate != null); - } + }) : assert(delegate != null), + _delegate = delegate, + super(child); /// A delegate that controls this object's layout. SingleChildLayoutDelegate get delegate => _delegate; @@ -901,12 +899,11 @@ class RenderBaseline extends RenderShiftedBox { RenderBox child, @required double baseline, @required TextBaseline baselineType - }) : _baseline = baseline, + }) : assert(baseline != null), + assert(baselineType != null), + _baseline = baseline, _baselineType = baselineType, - super(child) { - assert(baseline != null); - assert(baselineType != null); - } + super(child); /// The number of logical pixels from the top of this box at which to position /// the child's baseline. diff --git a/packages/flutter/lib/src/rendering/sliver.dart b/packages/flutter/lib/src/rendering/sliver.dart index 4414ee53411ff..b8e2c54ca30fc 100644 --- a/packages/flutter/lib/src/rendering/sliver.dart +++ b/packages/flutter/lib/src/rendering/sliver.dart @@ -485,13 +485,13 @@ class SliverGeometry { double hitTestExtent, bool visible, this.hasVisualOverflow: false, - this.scrollOffsetCorrection: 0.0 + this.scrollOffsetCorrection, }) : assert(scrollExtent != null), assert(paintExtent != null), assert(paintOrigin != null), assert(maxPaintExtent != null), assert(hasVisualOverflow != null), - assert(scrollOffsetCorrection != null), + assert(scrollOffsetCorrection != 0.0), layoutExtent = layoutExtent ?? paintExtent, hitTestExtent = hitTestExtent ?? paintExtent, visible = visible ?? paintExtent > 0.0; @@ -613,7 +613,7 @@ class SliverGeometry { verify(hitTestExtent >= 0.0, 'The "hitTestExtent" is negative.'); verify(visible != null, 'The "visible" property is null.'); verify(hasVisualOverflow != null, 'The "hasVisualOverflow" is null.'); - verify(scrollOffsetCorrection != null, 'The "scrollOffsetCorrection" is null.'); + verify(scrollOffsetCorrection != 0.0, 'The "scrollOffsetCorrection" is zero.'); return true; }); return true; @@ -648,7 +648,8 @@ class SliverGeometry { buffer.write('hitTestExtent: ${hitTestExtent.toStringAsFixed(1)}, '); if (hasVisualOverflow) buffer.write('hasVisualOverflow: true, '); - buffer.write('scrollOffsetCorrection: ${scrollOffsetCorrection.toStringAsFixed(1)}'); + if (scrollOffsetCorrection != null) + buffer.write('scrollOffsetCorrection: ${scrollOffsetCorrection.toStringAsFixed(1)}'); buffer.write(')'); return buffer.toString(); } diff --git a/packages/flutter/lib/src/rendering/sliver_fill.dart b/packages/flutter/lib/src/rendering/sliver_fill.dart index dd3031e71c724..1b3c689714be9 100644 --- a/packages/flutter/lib/src/rendering/sliver_fill.dart +++ b/packages/flutter/lib/src/rendering/sliver_fill.dart @@ -34,10 +34,10 @@ class RenderSliverFillViewport extends RenderSliverFixedExtentBoxAdaptor { RenderSliverFillViewport({ @required RenderSliverBoxChildManager childManager, double viewportFraction: 1.0, - }) : _viewportFraction = viewportFraction, super(childManager: childManager) { - assert(viewportFraction != null); - assert(viewportFraction > 0.0); - } + }) : assert(viewportFraction != null), + assert(viewportFraction > 0.0), + _viewportFraction = viewportFraction, + super(childManager: childManager); @override double get itemExtent => constraints.viewportMainAxisExtent * viewportFraction; diff --git a/packages/flutter/lib/src/rendering/sliver_grid.dart b/packages/flutter/lib/src/rendering/sliver_grid.dart index 039ca12b70c2b..259bc9e47ae40 100644 --- a/packages/flutter/lib/src/rendering/sliver_grid.dart +++ b/packages/flutter/lib/src/rendering/sliver_grid.dart @@ -456,10 +456,9 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor { RenderSliverGrid({ @required RenderSliverBoxChildManager childManager, @required SliverGridDelegate gridDelegate, - }) : _gridDelegate = gridDelegate, - super(childManager: childManager) { - assert(gridDelegate != null); - } + }) : assert(gridDelegate != null), + _gridDelegate = gridDelegate, + super(childManager: childManager); @override void setupParentData(RenderObject child) { diff --git a/packages/flutter/lib/src/rendering/sliver_list.dart b/packages/flutter/lib/src/rendering/sliver_list.dart index 739538142d086..4abff380ca83d 100644 --- a/packages/flutter/lib/src/rendering/sliver_list.dart +++ b/packages/flutter/lib/src/rendering/sliver_list.dart @@ -99,15 +99,23 @@ class RenderSliverList extends RenderSliverMultiBoxAdaptor { earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true); if (earliestUsefulChild == null) { - // We ran out of children before reaching the scroll offset. - // We must inform our parent that this sliver cannot fulfill - // its contract and that we need a scroll offset correction. - geometry = new SliverGeometry( - scrollOffsetCorrection: -scrollOffset, - ); final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData; childParentData.layoutOffset = 0.0; - return; + + if (scrollOffset == 0.0) { + earliestUsefulChild = firstChild; + leadingChildWithLayout = earliestUsefulChild; + trailingChildWithLayout ??= earliestUsefulChild; + break; + } else { + // We ran out of children before reaching the scroll offset. + // We must inform our parent that this sliver cannot fulfill + // its contract and that we need a scroll offset correction. + geometry = new SliverGeometry( + scrollOffsetCorrection: -scrollOffset, + ); + return; + } } final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild); diff --git a/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart b/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart index 642f04b685d6e..09a791ee5c1a0 100644 --- a/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart +++ b/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart @@ -149,9 +149,8 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver /// The [childManager] argument must not be null. RenderSliverMultiBoxAdaptor({ @required RenderSliverBoxChildManager childManager - }) : _childManager = childManager { - assert(childManager != null); - } + }) : assert(childManager != null), + _childManager = childManager; @override void setupParentData(RenderObject child) { diff --git a/packages/flutter/lib/src/rendering/sliver_padding.dart b/packages/flutter/lib/src/rendering/sliver_padding.dart index 00ce8923e656e..c602a59f28a2f 100644 --- a/packages/flutter/lib/src/rendering/sliver_padding.dart +++ b/packages/flutter/lib/src/rendering/sliver_padding.dart @@ -31,9 +31,9 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin { +class StackParentData extends ContainerBoxParentData { /// The distance by which the child's top edge is inset from the top of the stack. double top; @@ -300,12 +300,12 @@ class RenderStack extends RenderBox FractionalOffset alignment: FractionalOffset.topLeft, StackFit fit: StackFit.loose, Overflow overflow: Overflow.clip - }) : _alignment = alignment, + }) : assert(alignment != null), + assert(fit != null), + assert(overflow != null), + _alignment = alignment, _fit = fit, _overflow = overflow { - assert(alignment != null); - assert(fit != null); - assert(overflow != null); addAll(children); } diff --git a/packages/flutter/lib/src/rendering/table.dart b/packages/flutter/lib/src/rendering/table.dart index 82153716bfcd8..ee0dfaebbe136 100644 --- a/packages/flutter/lib/src/rendering/table.dart +++ b/packages/flutter/lib/src/rendering/table.dart @@ -489,12 +489,11 @@ class RenderTable extends RenderBox { TableCellVerticalAlignment defaultVerticalAlignment: TableCellVerticalAlignment.top, TextBaseline textBaseline, List> children - }) { - assert(columns == null || columns >= 0); - assert(rows == null || rows >= 0); - assert(rows == null || children == null); - assert(defaultColumnWidth != null); - assert(configuration != null); + }) : assert(columns == null || columns >= 0), + assert(rows == null || rows >= 0), + assert(rows == null || children == null), + assert(defaultColumnWidth != null), + assert(configuration != null) { _columns = columns ?? (children != null && children.isNotEmpty ? children.first.length : 0); _rows = rows ?? 0; _children = []..length = _columns * _rows; diff --git a/packages/flutter/lib/src/rendering/viewport.dart b/packages/flutter/lib/src/rendering/viewport.dart index 662a93b50258f..89bc9565d40ef 100644 --- a/packages/flutter/lib/src/rendering/viewport.dart +++ b/packages/flutter/lib/src/rendering/viewport.dart @@ -20,7 +20,11 @@ import 'viewport_offset.dart'; /// content, which can be controlled by a [ViewportOffset]. This interface lets /// the framework recognize such render objects and interact with them without /// having specific knowledge of all the various types of viewports. -abstract class RenderAbstractViewport implements RenderObject { +abstract class RenderAbstractViewport extends RenderObject { + // This class is intended to be used as an interface with the implements + // keyword, and should not be extended directly. + factory RenderAbstractViewport._() => null; + /// Returns the [RenderAbstractViewport] that most tightly encloses the given /// render object. /// @@ -76,11 +80,10 @@ abstract class RenderViewportBase children, RenderSliver center, - }) : _anchor = anchor, + }) : assert(anchor != null), + assert(anchor >= 0.0 && anchor <= 1.0), + _anchor = anchor, _center = center, super(axisDirection: axisDirection, offset: offset) { - assert(anchor != null); - assert(anchor >= 0.0 && anchor <= 1.0); addAll(children); if (center == null && firstChild != null) _center = firstChild; diff --git a/packages/flutter/lib/src/rendering/wrap.dart b/packages/flutter/lib/src/rendering/wrap.dart index c365bc4289628..f1ec7b178e09b 100644 --- a/packages/flutter/lib/src/rendering/wrap.dart +++ b/packages/flutter/lib/src/rendering/wrap.dart @@ -59,7 +59,7 @@ class _RunMetrics { } /// Parent data for use with [RenderWrap]. -class WrapParentData extends ContainerBoxParentDataMixin { +class WrapParentData extends ContainerBoxParentData { int _runIndex = 0; } @@ -91,18 +91,18 @@ class RenderWrap extends RenderBox with ContainerRenderObjectMixin null; @override void initInstances() { diff --git a/packages/flutter/lib/src/services/binding.dart b/packages/flutter/lib/src/services/binding.dart index 13eaa6703c170..03df44384797d 100644 --- a/packages/flutter/lib/src/services/binding.dart +++ b/packages/flutter/lib/src/services/binding.dart @@ -17,6 +17,10 @@ import 'platform_messages.dart'; /// the licenses found in the `LICENSE` file stored at the root of the asset /// bundle. abstract class ServicesBinding extends BindingBase { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory ServicesBinding._() => null; + @override void initInstances() { super.initInstances(); diff --git a/packages/flutter/lib/src/services/haptic_feedback.dart b/packages/flutter/lib/src/services/haptic_feedback.dart index cd15407b82f61..fa41878bd9d06 100644 --- a/packages/flutter/lib/src/services/haptic_feedback.dart +++ b/packages/flutter/lib/src/services/haptic_feedback.dart @@ -18,7 +18,7 @@ class HapticFeedback { /// On iOS devices that support haptic feedback, this uses the default system /// vibration value (`kSystemSoundID_Vibrate`). /// - /// On Android, this uses the platform haptic feedback API to simulates a + /// On Android, this uses the platform haptic feedback API to simulate a /// short tap on a virtual keyboard. static Future vibrate() async { await SystemChannels.platform.invokeMethod('HapticFeedback.vibrate'); diff --git a/packages/flutter/lib/src/services/image_provider.dart b/packages/flutter/lib/src/services/image_provider.dart index 306435b7be3bb..6d4644fd93513 100644 --- a/packages/flutter/lib/src/services/image_provider.dart +++ b/packages/flutter/lib/src/services/image_provider.dart @@ -19,6 +19,13 @@ import 'image_stream.dart'; /// Configuration information passed to the [ImageProvider.resolve] method to /// select a specific image. +/// +/// See also: +/// +/// * [createLocalImageConfiguration], which creates an [ImageConfiguration] +/// based on ambient configuration in a [Widget] environment. +/// * [ImageProvider], which uses [ImageConfiguration] objects to determine +/// which image to obtain. @immutable class ImageConfiguration { /// Creates an object holding the configuration information for an [ImageProvider]. @@ -149,6 +156,81 @@ class ImageConfiguration { /// /// The type argument does not have to be specified when using the type as an /// argument (where any image provider is acceptable). +/// +/// ## Sample code +/// +/// The following shows the code required to write a widget that fully conforms +/// to the [ImageProvider] and [Widget] protocols. (It is essentially a +/// bare-bones version of the [Image] widget.) +/// +/// ```dart +/// class MyImage extends StatefulWidget { +/// const MyImage({ +/// Key key, +/// @required this.imageProvider, +/// }) : assert(imageProvider != null), +/// super(key: key); +/// +/// final ImageProvider imageProvider; +/// +/// @override +/// _MyImageState createState() => new _MyImageState(); +/// } +/// +/// class _MyImageState extends State { +/// ImageStream _imageStream; +/// ImageInfo _imageInfo; +/// +/// @override +/// void didChangeDependencies() { +/// super.didChangeDependencies(); +/// // We call _getImage here because createLocalImageConfiguration() needs to +/// // be called again if the dependencies changed, in case the changes relate +/// // to the DefaultAssetBundle, MediaQuery, etc, which that method uses. +/// _getImage(); +/// } +/// +/// @override +/// void didUpdateWidget(MyImage oldWidget) { +/// super.didUpdateWidget(oldWidget); +/// if (widget.imageProvider != oldWidget.imageProvider) +/// _getImage(); +/// } +/// +/// void _getImage() { +/// final ImageStream oldImageStream = _imageStream; +/// _imageStream = widget.imageProvider.resolve(createLocalImageConfiguration(context)); +/// if (_imageStream.key != oldImageStream?.key) { +/// // If the keys are the same, then we got the same image back, and so we don't +/// // need to update the listeners. If the key changed, though, we must make sure +/// // to switch our listeners to the new image stream. +/// oldImageStream?.removeListener(_updateImage); +/// _imageStream.addListener(_updateImage); +/// } +/// } +/// +/// void _updateImage(ImageInfo imageInfo, bool synchronousCall) { +/// setState(() { +/// // Trigger a build whenever the image changes. +/// _imageInfo = imageInfo; +/// }); +/// } +/// +/// @override +/// void dispose() { +/// _imageStream.removeListener(_updateImage); +/// super.dispose(); +/// } +/// +/// @override +/// Widget build(BuildContext context) { +/// return new RawImage( +/// image: _imageInfo?.image, // this is a dart:ui Image object +/// scale: _imageInfo?.scale ?? 1.0, +/// ); +/// } +/// } +/// ``` @optionalTypeArgs abstract class ImageProvider { /// Abstract const constructor. This constructor enables subclasses to provide diff --git a/packages/flutter/lib/src/services/image_stream.dart b/packages/flutter/lib/src/services/image_stream.dart index baa013e04617c..55a3cdfee36ce 100644 --- a/packages/flutter/lib/src/services/image_stream.dart +++ b/packages/flutter/lib/src/services/image_stream.dart @@ -44,10 +44,14 @@ class ImageInfo { /// Signature for callbacks reporting that an image is available. /// -/// synchronousCall is true if the listener is being invoked during the call -/// to addListener. -/// /// Used by [ImageStream]. +/// +/// The `synchronousCall` argument is true if the listener is being invoked +/// during the call to addListener. This can be useful if, for example, +/// [ImageStream.addListener] is invoked during a frame, so that a new rendering +/// frame is requested if the call was asynchronous (after the current frame) +/// and no rendering frame is requested if the call was synchronous (within the +/// same stack frame as the call to [ImageStream.addListener]). typedef void ImageListener(ImageInfo image, bool synchronousCall); /// A handle to an image resource. @@ -61,6 +65,11 @@ typedef void ImageListener(ImageInfo image, bool synchronousCall); /// loading. /// /// ImageStream objects are backed by [ImageStreamCompleter] objects. +/// +/// See also: +/// +/// * [ImageProvider], which has an example that includes the use of an +/// [ImageStream] in a [Widget]. class ImageStream { /// Create an initially unbound image stream. /// @@ -237,8 +246,8 @@ class OneFrameImageStreamCompleter extends ImageStreamCompleter { /// argument on [FlutterErrorDetails] set to true, meaning that by default the /// message is only dumped to the console in debug mode (see [new /// FlutterErrorDetails]). - OneFrameImageStreamCompleter(Future image, { InformationCollector informationCollector }) { - assert(image != null); + OneFrameImageStreamCompleter(Future image, { InformationCollector informationCollector }) + : assert(image != null) { image.then(setImage, onError: (dynamic error, StackTrace stack) { FlutterError.reportError(new FlutterErrorDetails( exception: error, diff --git a/packages/flutter/lib/src/services/message_codec.dart b/packages/flutter/lib/src/services/message_codec.dart index dec6db4c5c85d..7f2aebf3797c2 100644 --- a/packages/flutter/lib/src/services/message_codec.dart +++ b/packages/flutter/lib/src/services/message_codec.dart @@ -35,9 +35,8 @@ abstract class MessageCodec { class MethodCall { /// Creates a [MethodCall] representing the invocation of [method] with the /// specified [arguments]. - MethodCall(this.method, [this.arguments]) { - assert(method != null); - } + const MethodCall(this.method, [this.arguments]) + : assert(method != null); /// The name of the method to be called. final String method; @@ -148,9 +147,7 @@ class PlatformException implements Exception { @required this.code, this.message, this.details, - }) { - assert(code != null); - } + }) : assert(code != null); /// An error code. final String code; diff --git a/packages/flutter/lib/src/services/text_input.dart b/packages/flutter/lib/src/services/text_input.dart index ce1cbf8a988b0..a982d9bd80e15 100644 --- a/packages/flutter/lib/src/services/text_input.dart +++ b/packages/flutter/lib/src/services/text_input.dart @@ -14,18 +14,42 @@ import 'text_editing.dart'; export 'dart:ui' show TextAffinity; /// The type of information for which to optimize the text input control. +/// +/// On Android, behavior may vary across device and keyboard provider. enum TextInputType { /// Optimize for textual information. + /// + /// Requests the default platform keyboard. text, /// Optimize for numerical information. + /// + /// Requests a keyboard with ready access to the decimal point and number + /// keys. number, /// Optimize for telephone numbers. + /// + /// Requests a keyboard with ready access to the number keys, "*", and "#". phone, /// Optimize for date and time information. + /// + /// On iOS, requests the default keyboard. + /// + /// On Android, requests a keyboard with ready access to the number keys, + /// ":", and "-". datetime, + + /// Optimize for email addresses. + /// + /// Requests a keyboard with ready access to the "@" and "." keys. + emailAddress, + + /// Optimize for URLs. + /// + /// Requests a keyboard with ready access to the "/" and "." keys. + url, } /// An action the user has requested the text input control to perform. @@ -200,9 +224,9 @@ abstract class TextInputClient { /// /// * [TextInput.attach] class TextInputConnection { - TextInputConnection._(this._client) : _id = _nextId++ { - assert(_client != null); - } + TextInputConnection._(this._client) + : assert(_client != null), + _id = _nextId++; static int _nextId = 1; final int _id; diff --git a/packages/flutter/lib/src/widgets/animated_cross_fade.dart b/packages/flutter/lib/src/widgets/animated_cross_fade.dart index 8c64b889a4a19..b64447099f179 100644 --- a/packages/flutter/lib/src/widgets/animated_cross_fade.dart +++ b/packages/flutter/lib/src/widgets/animated_cross_fade.dart @@ -29,15 +29,41 @@ enum CrossFadeState { /// /// The animation is controlled through the [crossFadeState] parameter. /// [firstCurve] and [secondCurve] represent the opacity curves of the two -/// children. Note that [firstCurve] is inverted, i.e. it fades out when -/// providing a growing curve like [Curves.linear]. [sizeCurve] is the curve -/// used to animated between the size of the fading out child and the size of -/// the fading in child. +/// children. The [firstCurve] is inverted, i.e. it fades out when providing a +/// growing curve like [Curves.linear]. The [sizeCurve] is the curve used to +/// animated between the size of the fading out child and the size of the fading +/// in child. /// /// This widget is intended to be used to fade a pair of widgets with the same /// width. In the case where the two children have different heights, the /// animation crops overflowing children during the animation by aligning their /// top edge, which means that the bottom will be clipped. +/// +/// The animation is automatically triggered when an existing +/// [AnimatedCrossFade] is rebuilt with a different value for the +/// [crossFadeState] property. +/// +/// ## Sample code +/// +/// This code fades between two representations of the Flutter logo. It depends +/// on a boolean field `_on`; when `_on` is true, the first logo is shown, +/// otherwise the second logo is shown. When the field changes state, the +/// [AnimatedCrossFade] widget cross-fades between the two forms of the logo +/// over three seconds. +/// +/// ```dart +/// new AnimatedCrossFade( +/// duration: const Duration(seconds: 3), +/// firstChild: const FlutterLogo(style: FlutterLogoStyle.horizontal, size: 100.0), +/// secondChild: const FlutterLogo(style: FlutterLogoStyle.stacked, size: 100.0), +/// crossFadeState: _first ? CrossFadeState.showFirst : CrossFadeState.showSecond, +/// ) +/// ``` +/// +/// See also: +/// +/// * [AnimatedSize], the lower-level widget which [AnimatedCrossFade] uses to +/// automatically change size. class AnimatedCrossFade extends StatefulWidget { /// Creates a cross-fade animation widget. /// @@ -99,6 +125,14 @@ class AnimatedCrossFade extends StatefulWidget { @override _AnimatedCrossFadeState createState() => new _AnimatedCrossFadeState(); + + @override + void debugFillDescription(List description) { + super.debugFillDescription(description); + description.add('$crossFadeState'); + if (alignment != FractionalOffset.topCenter) + description.add('alignment: $alignment'); + } } class _AnimatedCrossFadeState extends State with TickerProviderStateMixin { @@ -212,4 +246,13 @@ class _AnimatedCrossFadeState extends State with TickerProvid ), ); } + + @override + void debugFillDescription(List description) { + super.debugFillDescription(description); + description.add('${widget.crossFadeState}'); + description.add('$_controller'); + if (widget.alignment != FractionalOffset.topCenter) + description.add('alignment: ${widget.alignment}'); + } } diff --git a/packages/flutter/lib/src/widgets/animated_list.dart b/packages/flutter/lib/src/widgets/animated_list.dart index 98ef388e41961..2b6cb747d99eb 100644 --- a/packages/flutter/lib/src/widgets/animated_list.dart +++ b/packages/flutter/lib/src/widgets/animated_list.dart @@ -46,7 +46,7 @@ class _ActiveItem implements Comparable<_ActiveItem> { /// This widget is similar to one created by [ListView.builder]. class AnimatedList extends StatefulWidget { /// Creates a scrolling container that animates items when they are inserted or removed. - AnimatedList({ + const AnimatedList({ Key key, @required this.itemBuilder, this.initialItemCount: 0, @@ -57,10 +57,9 @@ class AnimatedList extends StatefulWidget { this.physics, this.shrinkWrap: false, this.padding, - }) : super(key: key) { - assert(itemBuilder != null); - assert(initialItemCount != null && initialItemCount >= 0); - } + }) : assert(itemBuilder != null), + assert(initialItemCount != null && initialItemCount >= 0), + super(key: key); /// Called, as needed, to build list item widgets. /// diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index ecdeed592d9c8..669716d9c136e 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -166,6 +166,15 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv return await navigator.maybePop(); } + @override + Future didPushRoute(String route) async { + assert(mounted); + final NavigatorState navigator = _navigator.currentState; + assert(navigator != null); + navigator.pushNamed(route); + return true; + } + @override void didChangeMetrics() { setState(() { diff --git a/packages/flutter/lib/src/widgets/async.dart b/packages/flutter/lib/src/widgets/async.dart index f64cb8cb87470..da98ac9a866bf 100644 --- a/packages/flutter/lib/src/widgets/async.dart +++ b/packages/flutter/lib/src/widgets/async.dart @@ -12,6 +12,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart' show required; +// Examples can assume: +// dynamic _lot; + /// Base class for widgets that build themselves based on interaction with /// a specified [Stream]. /// @@ -195,7 +198,7 @@ class AsyncSnapshot { const AsyncSnapshot.nothing() : this._(ConnectionState.none, null, null); /// Creates an [AsyncSnapshot] in the specified [state] and with the specified [data]. - AsyncSnapshot.withData(ConnectionState state, T data) : this._(state, data, null); // not const because https://github.com/dart-lang/sdk/issues/29432 + const AsyncSnapshot.withData(ConnectionState state, T data) : this._(state, data, null); /// Creates an [AsyncSnapshot] in the specified [state] and with the specified [error]. const AsyncSnapshot.withError(ConnectionState state, Object error) : this._(state, null, error); diff --git a/packages/flutter/lib/src/widgets/banner.dart b/packages/flutter/lib/src/widgets/banner.dart index ccd08f601b07f..2ca61a86553f9 100644 --- a/packages/flutter/lib/src/widgets/banner.dart +++ b/packages/flutter/lib/src/widgets/banner.dart @@ -47,12 +47,10 @@ class BannerPainter extends CustomPainter { @required this.location, this.color: _kColor, this.textStyle: _kTextStyle, - }) { - assert(message != null); - assert(location != null); - assert(color != null); - assert(textStyle != null); - } + }) : assert(message != null), + assert(location != null), + assert(color != null), + assert(textStyle != null); /// The message to show in the banner. final String message; diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 9776c2e545d48..bf462de3ff5b4 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -61,6 +61,27 @@ export 'package:flutter/rendering.dart' show /// expensive because it requires painting the child into an intermediate /// buffer. For the value 0.0, the child is simply not painted at all. For the /// value 1.0, the child is painted immediately without an intermediate buffer. +/// +/// ## Sample code +/// +/// This example shows some [Text] when the `_visible` member field is true, and +/// hides it when it is false: +/// +/// ```dart +/// new Opacity( +/// opacity: _visible ? 1.0 : 0.0, +/// child: const Text('Now you see me, now you don\'t!'), +/// ) +/// ``` +/// +/// This is more efficient than adding and removing the child widget from the +/// tree on demand. +/// +/// See also: +/// +/// * [ShaderMask], which can apply more elaborate effects to its child. +/// * [Transform], which applies an arbitrary transform to its child widget at +/// paint time. class Opacity extends SingleChildRenderObjectWidget { /// Creates a widget that makes its child partially transparent. /// @@ -69,7 +90,7 @@ class Opacity extends SingleChildRenderObjectWidget { const Opacity({ Key key, @required this.opacity, - Widget child + Widget child, }) : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0), super(key: key, child: child); @@ -104,6 +125,31 @@ class Opacity extends SingleChildRenderObjectWidget { /// /// For example, [ShaderMask] can be used to gradually fade out the edge /// of a child by using a [new ui.Gradient.linear] mask. +/// +/// ## Sample code +/// +/// This example makes the text look like it is on fire: +/// +/// ```dart +/// new ShaderMask( +/// shaderCallback: (Rect bounds) { +/// return new RadialGradient( +/// center: FractionalOffset.topLeft, +/// radius: 1.0, +/// colors: [Colors.yellow, Colors.deepOrange.shade900], +/// tileMode: TileMode.mirror, +/// ).createShader(bounds); +/// }, +/// child: const Text('I’m burning the memories'), +/// ) +/// ``` +/// +/// See also: +/// +/// * [Opacity], which can apply a uniform alpha effect to its child. +/// * [CustomPaint], which lets you draw directly on the canvas. +/// * [DecoratedBox], for another approach at decorating child widgets. +/// * [BackdropFilter], which applies an image filter to the background. class ShaderMask extends SingleChildRenderObjectWidget { /// Creates a widget that applies a mask generated by a [Shader] to its child. /// @@ -117,10 +163,14 @@ class ShaderMask extends SingleChildRenderObjectWidget { assert(blendMode != null), super(key: key, child: child); - /// Called to creates the [Shader] that generates the mask. + /// Called to create the [dart:ui.Shader] that generates the mask. /// /// The shader callback is called with the current size of the child so that /// it can customize the shader to the size and location of the child. + /// + /// Typically this will use a [LinearGradient] or [RadialGradient] to create + /// the [dart:ui.Shader], though the [dart:ui.ImageShader] class could also be + /// used. final ShaderCallback shaderCallback; /// The [BlendMode] to use when applying the shader to the child. @@ -133,7 +183,7 @@ class ShaderMask extends SingleChildRenderObjectWidget { RenderShaderMask createRenderObject(BuildContext context) { return new RenderShaderMask( shaderCallback: shaderCallback, - blendMode: blendMode + blendMode: blendMode, ); } @@ -149,6 +199,11 @@ class ShaderMask extends SingleChildRenderObjectWidget { /// /// This effect is relatively expensive, especially if the filter is non-local, /// such as a blur. +/// +/// See also: +/// +/// * [DecoratedBox], which draws a background under (or over) a widget. +/// * [Opacity], which changes the opacity of the widget itself. class BackdropFilter extends SingleChildRenderObjectWidget { /// Creates a backdrop filter. /// @@ -275,7 +330,7 @@ class CustomPaint extends SingleChildRenderObjectWidget { /// custom [clipper]. /// /// [ClipRect] is commonly used with these widgets, which commonly paint outside -/// their bounds. +/// their bounds: /// /// * [CustomPaint] /// * [CustomSingleChildLayout] @@ -287,15 +342,15 @@ class CustomPaint extends SingleChildRenderObjectWidget { /// /// ## Sample code /// -/// For example, use a clip to show the top half of an [Image], you can use a -/// [ClipRect] combined with an [Align]: +/// For example, by combining a [ClipRect] with an [Align], one can show just +/// the top half of an [Image]: /// /// ```dart /// new ClipRect( /// child: new Align( /// alignment: FractionalOffset.topCenter, /// heightFactor: 0.5, -/// child: new Image(...), +/// child: new Image.network(userAvatarUrl), /// ), /// ) /// ``` @@ -500,6 +555,11 @@ class ClipPath extends SingleChildRenderObjectWidget { /// /// Physical layers cast shadows based on an [elevation] which is nominally in /// logical pixels, coming vertically out of the rendering surface. +/// +/// See also: +/// +/// * [DecoratedBox], which can apply more arbitrary shadow effects. +/// * [ClipRect], which applies a clip to its child. class PhysicalModel extends SingleChildRenderObjectWidget { /// Creates a physical model with a rounded-rectangular clip. /// @@ -560,6 +620,32 @@ class PhysicalModel extends SingleChildRenderObjectWidget { // POSITIONING AND SIZING NODES /// A widget that applies a transformation before painting its child. +/// +/// ## Sample code +/// +/// This example rotates and skews an orange box containing text, keeping the +/// top right corner pinned to its original position. +/// +/// ```dart +/// new Container( +/// color: Colors.black, +/// child: new Transform( +/// alignment: FractionalOffset.topRight, +/// transform: new Matrix4.skewY(0.3)..rotateZ(-math.PI / 12.0), +/// child: new Container( +/// padding: const EdgeInsets.all(8.0), +/// color: const Color(0xFFE8581C), +/// child: const Text('Apartment for rent!'), +/// ), +/// ), +/// ) +/// ``` +/// +/// See also: +/// * [RotatedBox], which rotates the child widget during layout, not just +/// during painting. +/// * [FittedBox], which sizes and positions its child widget to fit the parent +/// according to a given [BoxFit] discipline. class Transform extends SingleChildRenderObjectWidget { /// Creates a widget that transforms its child. /// @@ -570,10 +656,41 @@ class Transform extends SingleChildRenderObjectWidget { this.origin, this.alignment, this.transformHitTests: true, - Widget child + Widget child, }) : assert(transform != null), super(key: key, child: child); + /// Creates a widget that transforms its child using a rotation around the + /// center. + /// + /// The `angle` argument must not be null. It gives the rotation in clockwise + /// radians. + /// + /// ## Sample code + /// + /// This example rotates an orange box containing text around its center by + /// fifteen degrees. + /// + /// ```dart + /// new Transform.rotate( + /// angle: -math.PI / 12.0, + /// child: new Container( + /// padding: const EdgeInsets.all(8.0), + /// color: const Color(0xFFE8581C), + /// child: const Text('Apartment for rent!'), + /// ), + /// ) + /// ``` + Transform.rotate({ + Key key, + @required double angle, + this.origin, + this.alignment: FractionalOffset.center, + this.transformHitTests: true, + Widget child, + }) : transform = new Matrix4.rotationZ(angle), + super(key: key, child: child); + /// The matrix to transform the child by during painting. final Matrix4 transform; @@ -612,6 +729,11 @@ class Transform extends SingleChildRenderObjectWidget { } /// Scales and positions its child within itself according to [fit]. +/// +/// See also: +/// +/// * [Transform], which applies an arbitrary transform to its child widget at +/// paint time. class FittedBox extends SingleChildRenderObjectWidget { /// Creates a widget that scales and positions its child within itself according to [fit]. /// @@ -682,6 +804,23 @@ class FractionalTranslation extends SingleChildRenderObjectWidget { /// Unlike [Transform], which applies a transform just prior to painting, /// this object applies its rotation prior to layout, which means the entire /// rotated box consumes only as much space as required by the rotated child. +/// +/// ## Sample code +/// +/// This snippet rotates the child (some [Text]) so that it renders from bottom +/// to top, like an axis label on a graph: +/// +/// ```dart +/// new RotatedBox( +/// quarterTurns: 3, +/// child: const Text('Hello World!'), +/// ) +/// ``` +/// +/// See also: +/// +/// * [Transform], which is a paint effect that allows you to apply an +/// arbitrary transform to a child. class RotatedBox extends SingleChildRenderObjectWidget { /// A widget that rotates its child. /// @@ -689,7 +828,7 @@ class RotatedBox extends SingleChildRenderObjectWidget { const RotatedBox({ Key key, @required this.quarterTurns, - Widget child + Widget child, }) : assert(quarterTurns != null), super(key: key, child: child); @@ -712,6 +851,17 @@ class RotatedBox extends SingleChildRenderObjectWidget { /// size. Padding then sizes itself to its child's size, inflated by the /// padding, effectively creating empty space around the child. /// +/// ## Sample code +/// +/// This snippet indents the child (a [Card] with some [Text]) by eight pixels in each direction: +/// +/// ```dart +/// new Padding( +/// padding: new EdgeInsets.all(8.0), +/// child: const Card(child: const Text('Hello World!')), +/// ) +/// ``` +/// /// ## Design discussion /// /// ### Why use a [Padding] widget rather than a [Container] with a [Container.padding] property? @@ -731,6 +881,12 @@ class RotatedBox extends SingleChildRenderObjectWidget { /// In fact, the majority of widgets in Flutter are simply combinations of other /// simpler widgets. Composition, rather than inheritance, is the primary /// mechansim for building up widgets. +/// +/// See also: +/// +/// * [EdgeInsets], the class that is used to describe the padding dimensions. +/// * [Center], which positions the child at its natural dimensions, centered +/// in the parent. class Padding extends SingleChildRenderObjectWidget { /// Creates a widget that insets its child. /// @@ -738,7 +894,7 @@ class Padding extends SingleChildRenderObjectWidget { const Padding({ Key key, @required this.padding, - Widget child + Widget child, }) : assert(padding != null), super(key: key, child: child); @@ -1001,6 +1157,30 @@ class CustomMultiChildLayout extends MultiChildRenderObjectWidget { /// The [new SizedBox.expand] constructor can be used to make a [SizedBox] that /// sizes itself to fit the parent. It is equivalent to setting [width] and /// [height] to [double.INFINITY]. +/// +/// ## Sample code +/// +/// This snippet makes the child widget (a [Card] with some [Text]) have the +/// exact size 200x300, parental constraints permitting: +/// +/// ```dart +/// new SizedBox( +/// width: 200.0, +/// height: 300.0, +/// child: const Card(child: const Text('Hello World!')), +/// ) +/// ``` +/// +/// See also: +/// +/// * [ConstrainedBox], a more generic version of this class that takes +/// arbitrary [BoxConstraints] instead of an explicit width and height. +/// * [FractionallySizedBox], a widget that sizes its child to a fraction of +/// the total available space. +/// * [AspectRatio], a widget that attempts to fit within the parent's +/// constraints while also sizing its child to match a given sapect ratio. +/// * [FittedBox], which sizes and positions its child widget to fit the parent +/// according to a given [BoxFit] discipline. class SizedBox extends SingleChildRenderObjectWidget { /// Creates a fixed size box. The [width] and [height] parameters can be null /// to indicate that the size of the box should not be constrained in @@ -1028,7 +1208,7 @@ class SizedBox extends SingleChildRenderObjectWidget { @override RenderConstrainedBox createRenderObject(BuildContext context) => new RenderConstrainedBox( - additionalConstraints: _additionalConstraints + additionalConstraints: _additionalConstraints, ); BoxConstraints get _additionalConstraints { @@ -1062,8 +1242,32 @@ class SizedBox extends SingleChildRenderObjectWidget { /// A widget that imposes additional constraints on its child. /// /// For example, if you wanted [child] to have a minimum height of 50.0 logical -/// pixels, you could use `const BoxConstraints(minHeight: 50.0)`` as the +/// pixels, you could use `const BoxConstraints(minHeight: 50.0)` as the /// [constraints]. +/// +/// ## Sample code +/// +/// This snippet makes the child widget (a [Card] with some [Text]) fill the +/// parent, by applying [BoxConstraints.expand] constraints: +/// +/// ```dart +/// new ConstrainedBox( +/// constraints: const BoxConstraints.expand(), +/// child: const Card(child: const Text('Hello World!')), +/// ) +/// ``` +/// +/// The same behaviour can be obtained using the [new SizedBox.expand] widget. +/// +/// See also: +/// +/// * [BoxConstraints], the class that describes constraints. +/// * [SizedBox], which lets you specify tight constraints by explicitly +/// specifying the height or width. +/// * [FractionallySizedBox], a widget that sizes its child to a fraction of +/// the total available space. +/// * [AspectRatio], a widget that attempts to fit within the parent's +/// constraints while also sizing its child to match a given sapect ratio. class ConstrainedBox extends SingleChildRenderObjectWidget { /// Creates a widget that imposes additional constraints on its child. /// @@ -1073,9 +1277,8 @@ class ConstrainedBox extends SingleChildRenderObjectWidget { @required this.constraints, Widget child }) : assert(constraints != null), - super(key: key, child: child) { - assert(constraints.debugAssertIsValid()); - } + assert(constraints.debugAssertIsValid()), + super(key: key, child: child); /// The additional constraints to impose on the child. final BoxConstraints constraints; @@ -1190,6 +1393,13 @@ class FractionallySizedBox extends SingleChildRenderObjectWidget { /// This is useful when composing widgets that normally try to match their /// parents' size, so that they behave reasonably in lists (which are /// unbounded). +/// +/// See also: +/// +/// * [ConstrainedBox], which applies its constraints in all cases, not just +/// when the incoming constraints are unbounded. +/// * [SizedBox], which lets you specify tight constraints by explicitly +/// specifying the height or width. class LimitedBox extends SingleChildRenderObjectWidget { /// Creates a box that limits its size only when it's unconstrained. /// @@ -1199,7 +1409,7 @@ class LimitedBox extends SingleChildRenderObjectWidget { Key key, this.maxWidth: double.INFINITY, this.maxHeight: double.INFINITY, - Widget child + Widget child, }) : assert(maxWidth != null && maxWidth >= 0.0), assert(maxHeight != null && maxHeight >= 0.0), super(key: key, child: child); @@ -1483,7 +1693,10 @@ class AspectRatio extends SingleChildRenderObjectWidget { /// you would like a child that would otherwise attempt to expand infinitely to /// instead size itself to a more reasonable width. /// -/// This class is relatively expensive. Avoid using it where possible. +/// This class is relatively expensive, because it adds a speculative layout +/// pass before the final layout phase. Avoid using it where possible. In the +/// worst case, this widget can result in a layout that is O(N²) in the depth of +/// the tree. class IntrinsicWidth extends SingleChildRenderObjectWidget { /// Creates a widget that sizes its child to the child's intrinsic width. /// @@ -1514,7 +1727,10 @@ class IntrinsicWidth extends SingleChildRenderObjectWidget { /// you would like a child that would otherwise attempt to expand infinitely to /// instead size itself to a more reasonable height. /// -/// This class is relatively expensive. Avoid using it where possible. +/// This class is relatively expensive, because it adds a speculative layout +/// pass before the final layout phase. Avoid using it where possible. In the +/// worst case, this widget can result in a layout that is O(N²) in the depth of +/// the tree. class IntrinsicHeight extends SingleChildRenderObjectWidget { /// Creates a widget that sizes its child to the child's intrinsic height. /// @@ -1704,12 +1920,13 @@ class ListBody extends MultiChildRenderObjectWidget { /// placed relative to the stack according to their top, right, bottom, and left /// properties. /// -/// The stack paints its children in order. If you want to change the order in -/// which the children paint, you can rebuild the stack with the children in -/// the new order. If you reorder the children in this way, consider giving the -/// children non-null keys. These keys will cause the framework to move the -/// underlying objects for the children to their new locations rather than -/// recreate them at their new location. +/// The stack paints its children in order with the first child being at the +/// bottom. If you want to change the order in which the children paint, you +/// can rebuild the stack with the children in the new order. If you reorder +/// the children in this way, consider giving the children non-null keys. +/// These keys will cause the framework to move the underlying objects for +/// the children to their new locations rather than recreate them at their +/// new location. /// /// For more details about the stack layout algorithm, see [RenderStack]. /// @@ -2025,6 +2242,7 @@ class Positioned extends ParentDataWidget { /// ## Layout algorithm /// /// _This section describes how a [Flex] is rendered by the framework._ +/// _See [BoxConstraints] for an introduction to box layout models._ /// /// Layout for a [Flex] proceeds in six steps: /// @@ -2086,9 +2304,8 @@ class Flex extends MultiChildRenderObjectWidget { assert(mainAxisAlignment != null), assert(mainAxisSize != null), assert(crossAxisAlignment != null), - super(key: key, children: children) { - assert(crossAxisAlignment != CrossAxisAlignment.baseline || textBaseline != null); // https://github.com/dart-lang/sdk/issues/29278 - } + assert(crossAxisAlignment != CrossAxisAlignment.baseline || textBaseline != null),// https://github.com/dart-lang/sdk/issues/29278 + super(key: key, children: children); /// The direction to use as the main axis. /// @@ -2188,9 +2405,68 @@ class Flex extends MultiChildRenderObjectWidget { /// ) /// ``` /// +/// ## Troubleshooting +/// +/// ### Why is my row turning red? +/// +/// If the contents of the row overflow, meaning that together they are wider +/// than the row, then the row runs out of space to give its [Expanded] and +/// [Flexible] children, and reports this by drawing a red warning box on the +/// edge that is overflowing. +/// +/// #### Story time +/// +/// Suppose, for instance, that you had this code: +/// +/// ```dart +/// new Row( +/// children: [ +/// const FlutterLogo(), +/// const Text('Flutter\'s hot reload helps you quickly and easily experiment, build UIs, add features, and fix bug faster. Experience sub-second reload times, without losing state, on emulators, simulators, and hardware for iOS and Android.'), +/// const Icon(Icons.sentiment_very_satisfied), +/// ], +/// ) +/// ``` +/// +/// The row first asks its first child, the [FlutterLogo], to lay out, at +/// whatever size the logo would like. The logo is friendly and happily decides +/// to be 24 pixels to a side. This leaves lots of room for the next child. The +/// row then asks that next child, the text, to lay out, at whatever size it +/// thinks is best. +/// +/// At this point, the text, not knowing how wide is too wide, says "Ok, I will +/// be thiiiiiiiiiiiiiiiiiiiis wide.", and goes well beyond the space that the +/// row has available, not wrapping. The row responds, "That's not fair, now I +/// have no more room available for my other children!", and gets angry and +/// turns red. +/// +/// The fix is to wrap the second child in an [Expanded] widget, which tells the +/// row that the child should be given the remaining room: +/// +/// ```dart +/// new Row( +/// children: [ +/// const FlutterLogo(), +/// const Expanded( +/// child: const Text('Flutter\'s hot reload helps you quickly and easily experiment, build UIs, add features, and fix bug faster. Experience sub-second reload times, without losing state, on emulators, simulators, and hardware for iOS and Android.'), +/// ), +/// const Icon(Icons.sentiment_very_satisfied), +/// ], +/// ) +/// ``` +/// +/// Now, the row first asks the logo to lay out, and then asks the _icon_ to lay +/// out. The [Icon], like the logo, is happy to take on a reasonable size (also +/// 24 pixels, not coincidentally, since both [FlutterLogo] and [Icon] honor the +/// ambient [IconTheme]). This leaves some room left over, and now the row tells +/// the text exactly how wide to be: the exact width of the remaining space. The +/// text, now happy to comply to a reasonable request, wraps the text within +/// that width, and you end up with a paragraph split over several lines. +/// /// ## Layout algorithm /// /// _This section describes how a [Row] is rendered by the framework._ +/// _See [BoxConstraints] for an introduction to box layout models._ /// /// Layout for a [Row] proceeds in six steps: /// @@ -2315,6 +2591,7 @@ class Row extends Flex { /// ## Layout algorithm /// /// _This section describes how a [Column] is rendered by the framework._ +/// _See [BoxConstraints] for an introduction to box layout models._ /// /// Layout for a [Column] proceeds in six steps: /// @@ -2706,9 +2983,8 @@ class Flow extends MultiChildRenderObjectWidget { Key key, @required this.delegate, List children: const [], - }) : super(key: key, children: children) { - assert(delegate != null); - } + }) : assert(delegate != null), + super(key: key, children: children); /// The delegate that controls the transformation matrices of the children. final FlowDelegate delegate; @@ -2733,13 +3009,14 @@ class Flow extends MultiChildRenderObjectWidget { /// /// Text displayed in a [RichText] widget must be explicitly styled. When /// picking which style to use, consider using [DefaultTextStyle.of] the current -/// [BuildContext] to provide defaults. +/// [BuildContext] to provide defaults. For more details on how to style text in +/// a [RichText] widget, see the documentation for [TextStyle]. /// /// When all the text uses the same style, consider using the [Text] widget, /// which is less verbose and integrates with [DefaultTextStyle] for default /// styling. /// -/// Example: +/// ## Sample code /// /// ```dart /// new RichText( @@ -2756,13 +3033,18 @@ class Flow extends MultiChildRenderObjectWidget { /// /// See also: /// -/// * [Text] -/// * [TextSpan] -/// * [DefaultTextStyle] +/// * [TextStyle], which discusses how to style text. +/// * [TextSpan], which is used to describe the text in a paragraph. +/// * [Text], which automatically applies the ambient styles described by a +/// [DefaultTextStyle] to a single string. class RichText extends LeafRenderObjectWidget { /// Creates a paragraph of rich text. /// - /// The [text], [softWrap], and [overflow] arguments must not be null. + /// The [text], [softWrap], [overflow], nad [textScaleFactor] arguments must + /// not be null. + /// + /// The [maxLines] property may be null (and indeed defaults to null), but if + /// it is not null, it must be greater than zero. const RichText({ Key key, @required this.text, @@ -2775,6 +3057,7 @@ class RichText extends LeafRenderObjectWidget { assert(softWrap != null), assert(overflow != null), assert(textScaleFactor != null), + assert(maxLines == null || maxLines > 0), super(key: key); /// The text to display in this widget. @@ -2800,6 +3083,9 @@ class RichText extends LeafRenderObjectWidget { /// An optional maximum number of lines for the text to span, wrapping if necessary. /// If the text exceeds the given number of lines, it will be truncated according /// to [overflow]. + /// + /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the + /// edge of the box. final int maxLines; @override @@ -3048,6 +3334,10 @@ class WidgetToRenderBoxAdapter extends LeafRenderObjectWidget { /// Rather than listening for raw pointer events, consider listening for /// higher-level gestures using [GestureDetector]. /// +/// ## Layout behavior +/// +/// _See [BoxConstraints] for an introduction to box layout models._ +/// /// If it has a child, this widget defers to the child for sizing behavior. If /// it does not have a child, it grows to fit the parent instead. class Listener extends SingleChildRenderObjectWidget { @@ -3345,7 +3635,8 @@ class Semantics extends SingleChildRenderObjectWidget { Widget child, this.container: false, this.checked, - this.label + this.selected, + this.label, }) : assert(container != null), super(key: key, child: child); @@ -3366,6 +3657,13 @@ class Semantics extends SingleChildRenderObjectWidget { /// state is. final bool checked; + /// If non-null indicates that this subtree represents something that can be + /// in a selected or unselected state, and what its current state is. + /// + /// The active tab in a tab bar for example is considered "selected", whereas + /// all other tabs are unselected. + final bool selected; + /// Provides a textual description of the widget. final String label; @@ -3373,7 +3671,8 @@ class Semantics extends SingleChildRenderObjectWidget { RenderSemanticsAnnotations createRenderObject(BuildContext context) => new RenderSemanticsAnnotations( container: container, checked: checked, - label: label + selected: selected, + label: label, ); @override @@ -3381,6 +3680,7 @@ class Semantics extends SingleChildRenderObjectWidget { renderObject ..container = container ..checked = checked + ..selected = selected ..label = label; } @@ -3390,6 +3690,8 @@ class Semantics extends SingleChildRenderObjectWidget { description.add('container: $container'); if (checked != null) description.add('checked: $checked'); + if (selected != null) + description.add('selected: $selected'); if (label != null) description.add('label: "$label"'); } @@ -3422,6 +3724,27 @@ class MergeSemantics extends SingleChildRenderObjectWidget { RenderMergeSemantics createRenderObject(BuildContext context) => new RenderMergeSemantics(); } +/// A widget that drops the semantics of all widget that were painted before it +/// in the same semantic container. +/// +/// This is useful to hide widgets from accessibility tools that are painted +/// behind a certain widget, e.g. an alert should usually disallow interaction +/// with any widget located "behind" the alert (even when they are still +/// partially visible). Similarly, an open [Drawer] blocks interactions with +/// any widget outside the drawer. +/// +/// See also: +/// +/// * [ExcludeSemantics] which drops all semantics of its descendants. +class BlockSemantics extends SingleChildRenderObjectWidget { + /// Creates a widget that excludes the semantics of all widgets painted before + /// it in the same semantic container. + const BlockSemantics({ Key key, Widget child }) : super(key: key, child: child); + + @override + RenderBlockSemantics createRenderObject(BuildContext context) => new RenderBlockSemantics(); +} + /// A widget that drops all the semantics of its descendants. /// /// When [excluding] is true, this widget (and its subtree) is excluded from @@ -3431,6 +3754,10 @@ class MergeSemantics extends SingleChildRenderObjectWidget { /// reported but that would only be confusing. For example, the /// material library's [Chip] widget hides the avatar since it is /// redundant with the chip label. +/// +/// See also: +/// +/// * [BlockSemantics] which drops semantics of widgets earlier in the tree. class ExcludeSemantics extends SingleChildRenderObjectWidget { /// Creates a widget that drops all the semantics of its descendants. const ExcludeSemantics({ diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index db80dcc676cc7..53bbc07c72be8 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -22,6 +22,55 @@ export 'dart:ui' show AppLifecycleState, Locale; /// Interface for classes that register with the Widgets layer binding. /// /// See [WidgetsBinding.addObserver] and [WidgetsBinding.removeObserver]. +/// +/// This class can be extended directly, to get default behaviors for all of the +/// handlers, or can used with the `implements` keyword, in which case all the +/// handlers must be implemented (and the analyzer will list those that have +/// been omitted). +/// +/// ## Sample code +/// +/// This [StatefulWidget] implements the parts of the [State] and +/// [WidgetsBindingObserver] protocols necessary to react to application +/// lifecycle messages. See [didChangeAppLifecycleState]. +/// +/// ```dart +/// class AppLifecycleReactor extends StatefulWidget { +/// const AppLifecycleReactor({ Key key }) : super(key: key); +/// +/// @override +/// _AppLifecycleReactorState createState() => new _AppLifecycleReactorState(); +/// } +/// +/// class _AppLifecycleReactorState extends State with WidgetsBindingObserver { +/// @override +/// void initState() { +/// super.initState(); +/// WidgetsBinding.instance.addObserver(this); +/// } +/// +/// @override +/// void dispose() { +/// WidgetsBinding.instance.removeObserver(this); +/// super.dispose(); +/// } +/// +/// AppLifecycleState _notification; +/// +/// @override +/// void didChangeAppLifecycleState(AppLifecycleState state) { +/// setState(() { _notification = state; }); +/// } +/// +/// @override +/// Widget build(BuildContext context) { +/// return new Text('Last notification: $_notification'); +/// } +/// } +/// ``` +/// +/// To respond to other notifications, replace the [didChangeAppLifecycleState] +/// method above with other methods from this class. abstract class WidgetsBindingObserver { /// Called when the system tells the app to pop the current route. /// For example, on Android, this is called when the user presses @@ -37,8 +86,66 @@ abstract class WidgetsBindingObserver { /// its current route if possible. Future didPopRoute() => new Future.value(false); + /// Called when the host tells the app to push a new route onto the + /// navigator. + /// + /// Observers are expected to return true if they were able to + /// handle the notification. Observers are notified in registration + /// order until one returns true. + Future didPushRoute(String route) => new Future.value(false); + /// Called when the application's dimensions change. For example, /// when a phone is rotated. + /// + /// ## Sample code + /// + /// This [StatefulWidget] implements the parts of the [State] and + /// [WidgetsBindingObserver] protocols necessary to react when the device is + /// rotated (or otherwise changes dimensions). + /// + /// ```dart + /// class MetricsReactor extends StatefulWidget { + /// const MetricsReactor({ Key key }) : super(key: key); + /// + /// @override + /// _MetricsReactorState createState() => new _MetricsReactorState(); + /// } + /// + /// class _MetricsReactorState extends State with WidgetsBindingObserver { + /// @override + /// void initState() { + /// super.initState(); + /// WidgetsBinding.instance.addObserver(this); + /// } + /// + /// @override + /// void dispose() { + /// WidgetsBinding.instance.removeObserver(this); + /// super.dispose(); + /// } + /// + /// Size _lastSize; + /// + /// @override + /// void didChangeMetrics() { + /// setState(() { _lastSize = ui.window.physicalSize; }); + /// } + /// + /// @override + /// Widget build(BuildContext context) { + /// return new Text('Last size: $_lastSize'); + /// } + /// } + /// ``` + /// + /// In general, this is unnecessary as the layout system takes care of + /// automatically recomputing the application geometry when the application + /// size changes. + /// + /// See also: + /// + /// * [MediaQuery.of], which provides a similar service with less + /// boilerplate. void didChangeMetrics() { } /// Called when the system tells the app that the user's locale has @@ -48,6 +155,9 @@ abstract class WidgetsBindingObserver { /// Called when the system puts the app in the background or returns /// the app to the foreground. + /// + /// An example of implementing this method is provided in the class-level + /// documentation for the [WidgetsBindingObserver] class. void didChangeAppLifecycleState(AppLifecycleState state) { } /// Called when the system is running low on memory. @@ -55,7 +165,11 @@ abstract class WidgetsBindingObserver { } /// The glue between the widgets layer and the Flutter engine. -abstract class WidgetsBinding extends BindingBase implements GestureBinding, RendererBinding { +abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererBinding { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory WidgetsBinding._() => null; + @override void initInstances() { super.initInstances(); @@ -142,11 +256,21 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren /// [MediaQuery.of] static method and (implicitly) the /// [InheritedWidget] mechanism to be notified whenever the screen /// size changes (e.g. whenever the screen rotates). + /// + /// See also: + /// + /// * [removeObserver], to release the resources reserved by this method. + /// * [WidgetsBindingObserver], which has an example of using this method. void addObserver(WidgetsBindingObserver observer) => _observers.add(observer); /// Unregisters the given observer. This should be used sparingly as /// it is relatively expensive (O(N) in the number of registered /// observers). + /// + /// See also: + /// + /// * [addObserver], for the method that adds observers in the first place. + /// * [WidgetsBindingObserver], which has an example of using this method. bool removeObserver(WidgetsBindingObserver observer) => _observers.remove(observer); /// Called when the system metrics change. @@ -198,10 +322,23 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren SystemNavigator.pop(); } - Future _handleNavigationInvocation(MethodCall methodCall) async { - if (methodCall.method == 'popRoute') - handlePopRoute(); - // TODO(abarth): Handle 'pushRoute'. + /// Called when the host tells the app to push a new route onto the + /// navigator. + Future handlePushRoute(String route) async { + for (WidgetsBindingObserver observer in new List.from(_observers)) { + if (await observer.didPushRoute(route)) + return; + } + } + + Future _handleNavigationInvocation(MethodCall methodCall) { + switch (methodCall.method) { + case 'popRoute': + return handlePopRoute(); + case 'pushRoute': + return handlePushRoute(methodCall.arguments); + } + return new Future.value(); } /// Called when the application lifecycle state changes. diff --git a/packages/flutter/lib/src/widgets/container.dart b/packages/flutter/lib/src/widgets/container.dart index ad1d96d9e2bd6..89bf1409b42ec 100644 --- a/packages/flutter/lib/src/widgets/container.dart +++ b/packages/flutter/lib/src/widgets/container.dart @@ -124,6 +124,50 @@ class DecoratedBox extends SingleChildRenderObjectWidget { /// `width`, `height`, and [constraints] arguments to the constructor override /// this. /// +/// ## Layout behavior +/// +/// _See [BoxConstraints] for an introduction to box layout models._ +/// +/// Since [Container] combines a number of other widgets each with their own +/// layout behavior, [Container]'s layout behaviour is somewhat complicated. +/// +/// tl;dr: [Container] tries, in order: to honor [alignment], to size itself to +/// the [child], to honor the `width`, `height`, and [constraints], to expand to +/// fit the parent, to be as small as possible. +/// +/// More specifically: +/// +/// If the widget has no child, no `height`, no `width`, no [constraints], +/// and the parent provides unbounded constraints, then [Container] tries to +/// size as small as possible. +/// +/// If the widget has no child and no [alignment], but a `height`, `width`, or +/// [constraints] are provided, then the [Container] tries to be as small as +/// possible given the combination of those constraints and the parent's +/// constraints. +/// +/// If the widget has no child, no `height`, no `width`, no [constraints], and +/// no [alignment], but the parent provides bounded constraints, then +/// [Container] expands to fit the constraints provided by the parent. +/// +/// If the widget has an [alignment], and the parent provides unbounded +/// constraints, then the [Container] tries to size itself around the child. +/// +/// If the widget has an [alignment], and the parent provides bounded +/// constraints, then the [Container] tries to expand to fit the parent, and +/// then positions the child within itself as per the [alignment]. +/// +/// Otherwise, the widget has a [child] but no `height`, no `width`, no +/// [constraints], and no [alignment], and the [Container] passes the +/// constraints from the parent to the child and sizes itself to match the +/// child. +/// +/// The [margin] and [padding] properties also affect the layout, as described +/// in the documentation for those properties. (Their effects merely augment the +/// rules described above.) The [decoration] can implicitly increase the +/// [padding] (e.g. borders in a [BoxDecoration] contribute to the [padding]); +/// see [Decoration.padding]. +/// /// ## Sample code /// /// This example shows a 48x48 green square (placed inside a [Center] widget in @@ -146,7 +190,7 @@ class DecoratedBox extends SingleChildRenderObjectWidget { /// The [constraints] are set to fit the font size plus ample headroom /// vertically, while expanding horizontally to fit the parent. The [padding] is /// used to make sure there is space between the contents and the text. The -/// [color] makes the box teal. The [alignment] causes the [child] to be +/// `color` makes the box teal. The [alignment] causes the [child] to be /// centered in the box. The [foregroundDecoration] overlays a nine-patch image /// onto the text. Finally, the [transform] applies a slight rotation to the /// entire contraption to complete the effect. @@ -198,22 +242,21 @@ class Container extends StatelessWidget { this.margin, this.transform, this.child, - }) : decoration = decoration ?? (color != null ? new BoxDecoration(color: color) : null), + }) : assert(margin == null || margin.isNonNegative), + assert(padding == null || padding.isNonNegative), + assert(decoration == null || decoration.debugAssertIsValid()), + assert(constraints == null || constraints.debugAssertIsValid()), + assert(color == null || decoration == null, + 'Cannot provide both a color and a decoration\n' + 'The color argument is just a shorthand for "decoration: new BoxDecoration(color: color)".' + ), + decoration = decoration ?? (color != null ? new BoxDecoration(color: color) : null), constraints = (width != null || height != null) ? constraints?.tighten(width: width, height: height) ?? new BoxConstraints.tightFor(width: width, height: height) : constraints, - super(key: key) { - assert(margin == null || margin.isNonNegative); - assert(padding == null || padding.isNonNegative); - assert(decoration == null || decoration.debugAssertIsValid()); - assert(constraints == null || constraints.debugAssertIsValid()); - assert(color == null || decoration == null, - 'Cannot provide both a color and a decoration\n' - 'The color argument is just a shorthand for "decoration: new BoxDecoration(color: color)".' - ); - } + super(key: key); /// The [child] contained by the container. /// diff --git a/packages/flutter/lib/src/widgets/dismissible.dart b/packages/flutter/lib/src/widgets/dismissible.dart index 79724b3e3d837..1df7adf979b8d 100644 --- a/packages/flutter/lib/src/widgets/dismissible.dart +++ b/packages/flutter/lib/src/widgets/dismissible.dart @@ -129,10 +129,9 @@ class _DismissibleClipper extends CustomClipper { _DismissibleClipper({ @required this.axis, @required this.moveAnimation - }) : super(reclip: moveAnimation) { - assert(axis != null); - assert(moveAnimation != null); - } + }) : assert(axis != null), + assert(moveAnimation != null), + super(reclip: moveAnimation); final Axis axis; final Animation moveAnimation; diff --git a/packages/flutter/lib/src/widgets/drag_target.dart b/packages/flutter/lib/src/widgets/drag_target.dart index 288252265ab19..2b289fbea7536 100644 --- a/packages/flutter/lib/src/widgets/drag_target.dart +++ b/packages/flutter/lib/src/widgets/drag_target.dart @@ -94,7 +94,8 @@ class Draggable extends StatefulWidget { this.affinity, this.maxSimultaneousDrags, this.onDragStarted, - this.onDraggableCanceled + this.onDraggableCanceled, + this.onDragCompleted, }) : assert(child != null), assert(feedback != null), assert(maxSimultaneousDrags == null || maxSimultaneousDrags >= 0), @@ -182,6 +183,16 @@ class Draggable extends StatefulWidget { /// callback is still in the tree. final DraggableCanceledCallback onDraggableCanceled; + /// Called when the draggable is dropped and accepted by a [DragTarget]. + /// + /// This function might be called after this widget has been removed from the + /// tree. For example, if a drag was in progress when this widget was removed + /// from the tree and the drag ended up completing, this callback will + /// still be called. For this reason, implementations of this callback might + /// need to check [State.mounted] to check whether the state receiving the + /// callback is still in the tree. + final VoidCallback onDragCompleted; + /// Creates a gesture recognizer that recognizes the start of the drag. /// /// Subclasses can override this function to customize when they start @@ -313,6 +324,8 @@ class _DraggableState extends State> { _activeCount -= 1; _disposeRecognizerIfInactive(); } + if (wasAccepted && widget.onDragCompleted != null) + widget.onDragCompleted(); if (!wasAccepted && widget.onDraggableCanceled != null) widget.onDraggableCanceled(velocity, offset); } @@ -444,10 +457,9 @@ class _DragAvatar extends Drag { this.feedback, this.feedbackOffset: Offset.zero, this.onDragEnd - }) { - assert(overlayState != null); - assert(dragStartPoint != null); - assert(feedbackOffset != null); + }) : assert(overlayState != null), + assert(dragStartPoint != null), + assert(feedbackOffset != null) { _entry = new OverlayEntry(builder: _build); overlayState.insert(_entry); _position = initialPosition; diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index dddff7ef4b279..a4c8e7d0a7b40 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -128,6 +128,10 @@ class TextEditingController extends ValueNotifier { class EditableText extends StatefulWidget { /// Creates a basic text input control. /// + /// The [maxLines] property can be set to null to remove the restriction on + /// the number of lines. By default, it is 1, meaning this is a single-line + /// text field. If it is not null, it must be greater than zero. + /// /// The [controller], [focusNode], [style], and [cursorColor] arguments must /// not be null. EditableText({ @@ -152,9 +156,9 @@ class EditableText extends StatefulWidget { assert(obscureText != null), assert(style != null), assert(cursorColor != null), - assert(maxLines != null), + assert(maxLines == null || maxLines > 0), assert(autofocus != null), - inputFormatters = maxLines == 1 + inputFormatters = maxLines == 1 ? ( [BlacklistingTextInputFormatter.singleLineFormatter] ..addAll(inputFormatters ?? const Iterable.empty()) @@ -192,8 +196,12 @@ class EditableText extends StatefulWidget { final Color cursorColor; /// The maximum number of lines for the text to span, wrapping if necessary. + /// /// If this is 1 (the default), the text will not wrap, but will scroll /// horizontally instead. + /// + /// If this is null, there is no limit to the number of lines. If it is not + /// null, the value must be greater than zero. final int maxLines; /// Whether this input field should focus itself if nothing else is already focused. @@ -218,7 +226,7 @@ class EditableText extends StatefulWidget { /// Called when the user indicates that they are done editing the text in the field. final ValueChanged onSubmitted; - /// Optional input validation and formatting overrides. Formatters are run + /// Optional input validation and formatting overrides. Formatters are run /// in the provided order when the text input changes. final List inputFormatters; @@ -340,7 +348,7 @@ class EditableTextState extends State implements TextInputClient { } bool get _hasFocus => widget.focusNode.hasFocus; - bool get _isMultiline => widget.maxLines > 1; + bool get _isMultiline => widget.maxLines != 1; // Calculate the new scroll offset so the cursor remains visible. double _getScrollOffsetForCaret(Rect caretRect) { @@ -417,7 +425,7 @@ class EditableTextState extends State implements TextInputClient { void _handleSelectionChanged(TextSelection selection, RenderEditable renderObject, bool longPress) { widget.controller.selection = selection; - // Note that this will show the keyboard for all selection changes on the + // This will show the keyboard for all selection changes on the // EditableWidget, not just changes triggered by user gestures. requestKeyboard(); diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 0e0d1ce395fb6..2a64d1adda025 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -17,6 +17,15 @@ export 'package:flutter/foundation.dart' show FlutterError, debugPrint, debugPri export 'package:flutter/foundation.dart' show VoidCallback, ValueChanged, ValueGetter, ValueSetter; export 'package:flutter/rendering.dart' show RenderObject, RenderBox, debugDumpRenderTree, debugDumpLayerTree; +// Examples can assume: +// BuildContext context; +// void setState(VoidCallback fn) { } + +// Examples can assume: +// abstract class RenderFrogJar extends RenderObject { } +// abstract class FrogJar extends RenderObjectWidget { } +// abstract class FrogJarParentData extends ParentData { Size size; } + // KEYS /// A [Key] is an identifier for [Widget]s and [Element]s. @@ -313,19 +322,19 @@ class LabeledGlobalKey> extends GlobalKey { /// Creates a global key with a debugging label. /// /// The label does not affect the key's identity. - const LabeledGlobalKey(this._debugLabel) : super.constructor(); + LabeledGlobalKey(this._debugLabel) : super.constructor(); // Used for forwarding the constructor from GlobalKey. - const LabeledGlobalKey._({ String debugLabel }) : _debugLabel = debugLabel, super.constructor(); + LabeledGlobalKey._({ String debugLabel }) : _debugLabel = debugLabel, super.constructor(); final String _debugLabel; @override String toString() { - final String tag = _debugLabel != null ? ' $_debugLabel' : '#$hashCode'; + final String label = _debugLabel != null ? ' $_debugLabel' : ''; if (runtimeType == LabeledGlobalKey) - return '[GlobalKey$tag]'; - return '[$runtimeType$tag]'; + return '[GlobalKey#$hashCode$label]'; + return '[$runtimeType#$hashCode$label]'; } } @@ -643,20 +652,20 @@ abstract class StatelessWidget extends Widget { /// /// ## Sample code /// -/// The following is a skeleton of a stateful widget subclass called `GreenFrog`: +/// The following is a skeleton of a stateful widget subclass called `YellowBird`: /// /// ```dart -/// class GreenFrog extends StatefulWidget { -/// const GreenFrog({ Key key }) : super(key: key); +/// class YellowBird extends StatefulWidget { +/// const YellowBird({ Key key }) : super(key: key); /// /// @override -/// _GreenFrogState createState() => new _GreenFrogState(); +/// _YellowBirdState createState() => new _YellowBirdState(); /// } /// -/// class _GreenFrogState extends State { +/// class _YellowBirdState extends State { /// @override /// Widget build(BuildContext context) { -/// return new Container(color: const Color(0xFF2DBD3A)); +/// return new Container(color: const Color(0xFFFFE306)); /// } /// } /// ``` @@ -665,15 +674,15 @@ abstract class StatelessWidget extends Widget { /// represented as private member fields. Also, normally widgets have more /// constructor arguments, each of which corresponds to a `final` property. /// -/// The next example shows the more generic widget `Frog` which can be given a +/// The next example shows the more generic widget `Bird` which can be given a /// color and a child, and which has some internal state with a method that /// can be called to mutate it: /// /// ```dart -/// class Frog extends StatefulWidget { -/// const Frog({ +/// class Bird extends StatefulWidget { +/// const Bird({ /// Key key, -/// this.color: const Color(0xFF2DBD3A), +/// this.color: const Color(0xFFFFE306), /// this.child, /// }) : super(key: key); /// @@ -681,10 +690,10 @@ abstract class StatelessWidget extends Widget { /// /// final Widget child; /// -/// _FrogState createState() => new _FrogState(); +/// _BirdState createState() => new _BirdState(); /// } /// -/// class _FrogState extends State { +/// class _BirdState extends State { /// double _size = 1.0; /// /// void grow() { @@ -695,7 +704,7 @@ abstract class StatelessWidget extends Widget { /// Widget build(BuildContext context) { /// return new Container( /// color: widget.color, -/// transform: new Matrix4.diagonalValues(_size, _size, 1.0), +/// transform: new Matrix4.diagonal3Values(_size, _size, 1.0), /// child: widget.child, /// ); /// } @@ -1309,7 +1318,7 @@ abstract class ProxyWidget extends Widget { /// /// ```dart /// class FrogSize extends ParentDataWidget { -/// Pond({ +/// FrogSize({ /// Key key, /// @required this.size, /// @required Widget child, @@ -1425,7 +1434,7 @@ abstract class ParentDataWidget extends ProxyWidge /// /// ```dart /// class FrogColor extends InheritedWidget { -/// const FrogColor( +/// const FrogColor({ /// Key key, /// @required this.color, /// @required Widget child, @@ -1452,7 +1461,7 @@ abstract class ParentDataWidget extends ProxyWidge /// /// Sometimes, the `of` method returns the data rather than the inherited /// widget; for example, in this case it could have returned a [Color] instead -/// of the [FrogColor] widget. +/// of the `FrogColor` widget. /// /// Occasionally, the inherited widget is an implementation detail of another /// class, and is therefore private. The `of` method in that case is typically @@ -1573,9 +1582,8 @@ abstract class MultiChildRenderObjectWidget extends RenderObjectWidget { /// objects. MultiChildRenderObjectWidget({ Key key, this.children: const [] }) : assert(children != null), - super(key: key) { - assert(!children.any((Widget child) => child == null)); // https://github.com/dart-lang/sdk/issues/29276 - } + assert(!children.any((Widget child) => child == null)), // https://github.com/dart-lang/sdk/issues/29276 + super(key: key); /// The widgets below this widget in the tree. /// @@ -2375,9 +2383,9 @@ abstract class Element implements BuildContext { /// Creates an element that uses the given widget as its configuration. /// /// Typically called by an override of [Widget.createElement]. - Element(Widget widget) : _widget = widget { - assert(widget != null); - } + Element(Widget widget) + : assert(widget != null), + _widget = widget; Element _parent; @@ -4363,9 +4371,9 @@ class SingleChildRenderObjectElement extends RenderObjectElement { /// are expected to inherit from [MultiChildRenderObjectWidget]. class MultiChildRenderObjectElement extends RenderObjectElement { /// Creates an element that uses the given widget as its configuration. - MultiChildRenderObjectElement(MultiChildRenderObjectWidget widget) : super(widget) { - assert(!debugChildrenHaveDuplicateKeys(widget, widget.children)); - } + MultiChildRenderObjectElement(MultiChildRenderObjectWidget widget) + : assert(!debugChildrenHaveDuplicateKeys(widget, widget.children)), + super(widget); @override MultiChildRenderObjectWidget get widget => super.widget; diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart index 9f59ec708653c..5c681f35a2015 100644 --- a/packages/flutter/lib/src/widgets/gesture_detector.dart +++ b/packages/flutter/lib/src/widgets/gesture_detector.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; @@ -33,13 +34,59 @@ export 'package:flutter/gestures.dart' show TapUpDetails, Velocity; -/// Signature for creating gesture recognizers. +/// Factory for creating gesture recognizers. /// -/// The `recognizer` argument is the gesture recognizer that currently occupies -/// the slot for which a gesture recognizer is being created. +/// `T` is the type of gesture recognizer this class manages. /// /// Used by [RawGestureDetector.gestures]. -typedef GestureRecognizer GestureRecognizerFactory(GestureRecognizer recognizer); +@optionalTypeArgs +abstract class GestureRecognizerFactory { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const GestureRecognizerFactory(); + + /// Must return an instance of T. + T constructor(); + + /// Must configure the given instance (which will have been created by + /// `constructor`). + /// + /// This normally means setting the callbacks. + void initializer(T instance); + + bool _debugAssertTypeMatches(Type type) { + assert(type == T, 'GestureRecognizerFactory of type $T was used where type $type was specified.'); + return true; + } +} + +/// Signature for closures that implement [GestureRecognizerFactory.constructor]. +typedef T GestureRecognizerFactoryConstructor(); + +/// Signature for closures that implement [GestureRecognizerFactory.initializer]. +typedef void GestureRecognizerFactoryInitializer(T instance); + +/// Factory for creating gesture recognizers that delegates to callbacks. +/// +/// Used by [RawGestureDetector.gestures]. +class GestureRecognizerFactoryWithHandlers extends GestureRecognizerFactory { + /// Creates a gesture recognizer factory with the given callbacks. + /// + /// The arguments must not be null. + const GestureRecognizerFactoryWithHandlers(this._constructor, this._initializer) : + assert(_constructor != null), + assert(_initializer != null); + + final GestureRecognizerFactoryConstructor _constructor; + + final GestureRecognizerFactoryInitializer _initializer; + + @override + T constructor() => _constructor(); + + @override + void initializer(T instance) => _initializer(instance); +} /// A widget that detects gestures. /// @@ -57,6 +104,23 @@ typedef GestureRecognizer GestureRecognizerFactory(GestureRecognizer recognizer) /// Material design applications typically react to touches with ink splash /// effects. The [InkWell] class implements this effect and can be used in place /// of a [GestureDetector] for handling taps. +/// +/// ## Sample code +/// +/// This example makes a rectangle react to being tapped by setting the +/// `_lights` field: +/// +/// ```dart +/// new GestureDetector( +/// onTap: () { +/// setState(() { _lights = true; }); +/// }, +/// child: new Container( +/// color: Colors.yellow, +/// child: new Text('TURN LIGHTS ON'), +/// ), +/// ) +/// ``` class GestureDetector extends StatelessWidget { /// Creates a widget that detects gestures. /// @@ -98,32 +162,31 @@ class GestureDetector extends StatelessWidget { this.onScaleEnd, this.behavior, this.excludeFromSemantics: false - }) : super(key: key) { - assert(excludeFromSemantics != null); - assert(() { - final bool haveVerticalDrag = onVerticalDragStart != null || onVerticalDragUpdate != null || onVerticalDragEnd != null; - final bool haveHorizontalDrag = onHorizontalDragStart != null || onHorizontalDragUpdate != null || onHorizontalDragEnd != null; - final bool havePan = onPanStart != null || onPanUpdate != null || onPanEnd != null; - final bool haveScale = onScaleStart != null || onScaleUpdate != null || onScaleEnd != null; - if (havePan || haveScale) { - if (havePan && haveScale) { - throw new FlutterError( - 'Incorrect GestureDetector arguments.\n' - 'Having both a pan gesture recognizer and a scale gesture recognizer is redundant; scale is a superset of pan. Just use the scale gesture recognizer.' - ); - } - final String recognizer = havePan ? 'pan' : 'scale'; - if (haveVerticalDrag && haveHorizontalDrag) { - throw new FlutterError( - 'Incorrect GestureDetector arguments.\n' - 'Simultaneously having a vertical drag gesture recognizer, a horizontal drag gesture recognizer, and a $recognizer gesture recognizer ' - 'will result in the $recognizer gesture recognizer being ignored, since the other two will catch all drags.' - ); - } - } - return true; - }); - } + }) : assert(excludeFromSemantics != null), + assert(() { + final bool haveVerticalDrag = onVerticalDragStart != null || onVerticalDragUpdate != null || onVerticalDragEnd != null; + final bool haveHorizontalDrag = onHorizontalDragStart != null || onHorizontalDragUpdate != null || onHorizontalDragEnd != null; + final bool havePan = onPanStart != null || onPanUpdate != null || onPanEnd != null; + final bool haveScale = onScaleStart != null || onScaleUpdate != null || onScaleEnd != null; + if (havePan || haveScale) { + if (havePan && haveScale) { + throw new FlutterError( + 'Incorrect GestureDetector arguments.\n' + 'Having both a pan gesture recognizer and a scale gesture recognizer is redundant; scale is a superset of pan. Just use the scale gesture recognizer.' + ); + } + final String recognizer = havePan ? 'pan' : 'scale'; + if (haveVerticalDrag && haveHorizontalDrag) { + throw new FlutterError( + 'Incorrect GestureDetector arguments.\n' + 'Simultaneously having a vertical drag gesture recognizer, a horizontal drag gesture recognizer, and a $recognizer gesture recognizer ' + 'will result in the $recognizer gesture recognizer being ignored, since the other two will catch all drags.' + ); + } + } + return true; + }), + super(key: key); /// The widget below this widget in the tree. final Widget child; @@ -232,27 +295,36 @@ class GestureDetector extends StatelessWidget { final Map gestures = {}; if (onTapDown != null || onTapUp != null || onTap != null || onTapCancel != null) { - gestures[TapGestureRecognizer] = (TapGestureRecognizer recognizer) { // ignore: invalid_assignment, https://github.com/flutter/flutter/issues/5771 - return (recognizer ??= new TapGestureRecognizer()) - ..onTapDown = onTapDown - ..onTapUp = onTapUp - ..onTap = onTap - ..onTapCancel = onTapCancel; - }; + gestures[TapGestureRecognizer] = new GestureRecognizerFactoryWithHandlers( + () => new TapGestureRecognizer(), + (TapGestureRecognizer instance) { + instance + ..onTapDown = onTapDown + ..onTapUp = onTapUp + ..onTap = onTap + ..onTapCancel = onTapCancel; + }, + ); } if (onDoubleTap != null) { - gestures[DoubleTapGestureRecognizer] = (DoubleTapGestureRecognizer recognizer) { // ignore: invalid_assignment, https://github.com/flutter/flutter/issues/5771 - return (recognizer ??= new DoubleTapGestureRecognizer()) - ..onDoubleTap = onDoubleTap; - }; + gestures[DoubleTapGestureRecognizer] = new GestureRecognizerFactoryWithHandlers( + () => new DoubleTapGestureRecognizer(), + (DoubleTapGestureRecognizer instance) { + instance + ..onDoubleTap = onDoubleTap; + }, + ); } if (onLongPress != null) { - gestures[LongPressGestureRecognizer] = (LongPressGestureRecognizer recognizer) { // ignore: invalid_assignment, https://github.com/flutter/flutter/issues/5771 - return (recognizer ??= new LongPressGestureRecognizer()) - ..onLongPress = onLongPress; - }; + gestures[LongPressGestureRecognizer] = new GestureRecognizerFactoryWithHandlers( + () => new LongPressGestureRecognizer(), + (LongPressGestureRecognizer instance) { + instance + ..onLongPress = onLongPress; + }, + ); } if (onVerticalDragDown != null || @@ -260,14 +332,17 @@ class GestureDetector extends StatelessWidget { onVerticalDragUpdate != null || onVerticalDragEnd != null || onVerticalDragCancel != null) { - gestures[VerticalDragGestureRecognizer] = (VerticalDragGestureRecognizer recognizer) { // ignore: invalid_assignment, https://github.com/flutter/flutter/issues/5771 - return (recognizer ??= new VerticalDragGestureRecognizer()) - ..onDown = onVerticalDragDown - ..onStart = onVerticalDragStart - ..onUpdate = onVerticalDragUpdate - ..onEnd = onVerticalDragEnd - ..onCancel = onVerticalDragCancel; - }; + gestures[VerticalDragGestureRecognizer] = new GestureRecognizerFactoryWithHandlers( + () => new VerticalDragGestureRecognizer(), + (VerticalDragGestureRecognizer instance) { + instance + ..onDown = onVerticalDragDown + ..onStart = onVerticalDragStart + ..onUpdate = onVerticalDragUpdate + ..onEnd = onVerticalDragEnd + ..onCancel = onVerticalDragCancel; + }, + ); } if (onHorizontalDragDown != null || @@ -275,14 +350,17 @@ class GestureDetector extends StatelessWidget { onHorizontalDragUpdate != null || onHorizontalDragEnd != null || onHorizontalDragCancel != null) { - gestures[HorizontalDragGestureRecognizer] = (HorizontalDragGestureRecognizer recognizer) { // ignore: invalid_assignment, https://github.com/flutter/flutter/issues/5771 - return (recognizer ??= new HorizontalDragGestureRecognizer()) - ..onDown = onHorizontalDragDown - ..onStart = onHorizontalDragStart - ..onUpdate = onHorizontalDragUpdate - ..onEnd = onHorizontalDragEnd - ..onCancel = onHorizontalDragCancel; - }; + gestures[HorizontalDragGestureRecognizer] = new GestureRecognizerFactoryWithHandlers( + () => new HorizontalDragGestureRecognizer(), + (HorizontalDragGestureRecognizer instance) { + instance + ..onDown = onHorizontalDragDown + ..onStart = onHorizontalDragStart + ..onUpdate = onHorizontalDragUpdate + ..onEnd = onHorizontalDragEnd + ..onCancel = onHorizontalDragCancel; + }, + ); } if (onPanDown != null || @@ -290,23 +368,29 @@ class GestureDetector extends StatelessWidget { onPanUpdate != null || onPanEnd != null || onPanCancel != null) { - gestures[PanGestureRecognizer] = (PanGestureRecognizer recognizer) { // ignore: invalid_assignment, https://github.com/flutter/flutter/issues/5771 - return (recognizer ??= new PanGestureRecognizer()) - ..onDown = onPanDown - ..onStart = onPanStart - ..onUpdate = onPanUpdate - ..onEnd = onPanEnd - ..onCancel = onPanCancel; - }; + gestures[PanGestureRecognizer] = new GestureRecognizerFactoryWithHandlers( + () => new PanGestureRecognizer(), + (PanGestureRecognizer instance) { + instance + ..onDown = onPanDown + ..onStart = onPanStart + ..onUpdate = onPanUpdate + ..onEnd = onPanEnd + ..onCancel = onPanCancel; + }, + ); } if (onScaleStart != null || onScaleUpdate != null || onScaleEnd != null) { - gestures[ScaleGestureRecognizer] = (ScaleGestureRecognizer recognizer) { // ignore: invalid_assignment, https://github.com/flutter/flutter/issues/5771 - return (recognizer ??= new ScaleGestureRecognizer()) - ..onStart = onScaleStart - ..onUpdate = onScaleUpdate - ..onEnd = onScaleEnd; - }; + gestures[ScaleGestureRecognizer] = new GestureRecognizerFactoryWithHandlers( + () => new ScaleGestureRecognizer(), + (ScaleGestureRecognizer instance) { + instance + ..onStart = onScaleStart + ..onUpdate = onScaleUpdate + ..onEnd = onScaleEnd; + }, + ); } return new RawGestureDetector( @@ -322,13 +406,47 @@ class GestureDetector extends StatelessWidget { /// factories. /// /// For common gestures, use a [GestureRecognizer]. -/// RawGestureDetector is useful primarily when developing your +/// [RawGestureDetector] is useful primarily when developing your /// own gesture recognizers. +/// +/// Configuring the gesture recognizers requires a carefully constructed map, as +/// described in [gestures] and as shown in the example below. +/// +/// ## Sample code +/// +/// This example shows how to hook up a [TapGestureRecognizer]. It assumes that +/// the code is being used inside a [State] object with a `_last` field that is +/// then displayed as the child of the gesture detector. +/// +/// ```dart +/// new RawGestureDetector( +/// gestures: { +/// TapGestureRecognizer: new GestureRecognizerFactoryWithHandlers( +/// () => new TapGestureRecognizer(), +/// (TapGestureRecognizer instance) { +/// instance +/// ..onTapDown = (TapDownDetails details) { setState(() { _last = 'down'; }); } +/// ..onTapUp = (TapUpDetails details) { setState(() { _last = 'up'; }); } +/// ..onTap = () { setState(() { _last = 'tap'; }); } +/// ..onTapCancel = () { setState(() { _last = 'cancel'; }); }; +/// }, +/// ), +/// }, +/// child: new Container(width: 300.0, height: 300.0, color: Colors.yellow, child: new Text(_last)), +/// ) +/// ``` +/// +/// See also: +/// +/// * [GestureDetector], a less flexible but much simpler widget that does the same thing. +/// * [PointerListener], a widget that reports raw pointer events. +/// * [GestureRecognizer], the class that you extend to create a custom gesture recognizer. class RawGestureDetector extends StatefulWidget { /// Creates a widget that detects gestures. /// /// By default, gesture detectors contribute semantic information to the tree - /// that is used by assistive technology. + /// that is used by assistive technology. This can be controlled using + /// [excludeFromSemantics]. const RawGestureDetector({ Key key, this.child, @@ -343,6 +461,12 @@ class RawGestureDetector extends StatefulWidget { final Widget child; /// The gestures that this widget will attempt to recognize. + /// + /// This should be a map from [GestureRecognizer] subclasses to + /// [GestureRecognizerFactory] subclasses specialized with the same type. + /// + /// This value can be late-bound at layout time using + /// [RawGestureDetectorState.replaceGestureRecognizers]. final Map gestures; /// How this gesture detector should behave during hit testing. @@ -376,7 +500,7 @@ class RawGestureDetectorState extends State { } /// This method can be called after the build phase, during the - /// layout of the nearest descendant RenderObjectWidget of the + /// layout of the nearest descendant [RenderObjectWidget] of the /// gesture detector, to update the list of active gesture /// recognizers. /// @@ -384,6 +508,10 @@ class RawGestureDetectorState extends State { /// in their gesture detector, and then need to know the dimensions /// of the viewport and the viewport's child to determine whether /// the gesture detector should be enabled. + /// + /// The argument should follow the same conventions as + /// [RawGestureDetector.gestures]. It acts like a temporary replacement for + /// that value until the next build. void replaceGestureRecognizers(Map gestures) { assert(() { if (!context.findRenderObject().owner.debugDoingLayout) { @@ -420,9 +548,12 @@ class RawGestureDetectorState extends State { final Map oldRecognizers = _recognizers; _recognizers = {}; for (Type type in gestures.keys) { + assert(gestures[type] != null); + assert(gestures[type]._debugAssertTypeMatches(type)); assert(!_recognizers.containsKey(type)); - _recognizers[type] = gestures[type](oldRecognizers[type]); - assert(_recognizers[type].runtimeType == type); + _recognizers[type] = oldRecognizers[type] ?? gestures[type].constructor(); + assert(_recognizers[type].runtimeType == type, 'GestureRecognizerFactory of type $type created a GestureRecognizer of type ${_recognizers[type].runtimeType}. The GestureRecognizerFactory must be specialized with the type of the class that it returns from its constructor method.'); + gestures[type].initializer(_recognizers[type]); } for (Type type in oldRecognizers.keys) { if (!_recognizers.containsKey(type)) diff --git a/packages/flutter/lib/src/widgets/heroes.dart b/packages/flutter/lib/src/widgets/heroes.dart index e5eab5e2bf536..ac5bd720d5ed5 100644 --- a/packages/flutter/lib/src/widgets/heroes.dart +++ b/packages/flutter/lib/src/widgets/heroes.dart @@ -180,9 +180,7 @@ class _HeroFlightManifest { @required this.fromHero, @required this.toHero, @required this.createRectTween, - }) { - assert(fromHero.widget.tag == toHero.widget.tag); - } + }) : assert(fromHero.widget.tag == toHero.widget.tag); final _HeroFlightType type; final OverlayState overlay; diff --git a/packages/flutter/lib/src/widgets/image.dart b/packages/flutter/lib/src/widgets/image.dart index 2a3dfac9a3649..ee4011fa44bff 100644 --- a/packages/flutter/lib/src/widgets/image.dart +++ b/packages/flutter/lib/src/widgets/image.dart @@ -25,6 +25,15 @@ export 'package:flutter/services.dart' show /// /// This is the object that must be passed to [BoxPainter.paint] and to /// [ImageProvider.resolve]. +/// +/// If this is not called from a build method, then it should be reinvoked +/// whenever the dependencies change, e.g. by calling it from +/// [State.didChangeDependencies], so that any changes in the environement are +/// picked up (e.g. if the device pixel ratio changes). +/// +/// See also: +/// +/// * [ImageProvider], which has an example showing how this might be used. ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size size }) { return new ImageConfiguration( bundle: DefaultAssetBundle.of(context), diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart index 3753638b93437..4e74a904e3272 100644 --- a/packages/flutter/lib/src/widgets/implicit_animations.dart +++ b/packages/flutter/lib/src/widgets/implicit_animations.dart @@ -318,22 +318,21 @@ class AnimatedContainer extends ImplicitlyAnimatedWidget { this.child, Curve curve: Curves.linear, @required Duration duration, - }) : decoration = decoration ?? (color != null ? new BoxDecoration(color: color) : null), + }) : assert(margin == null || margin.isNonNegative), + assert(padding == null || padding.isNonNegative), + assert(decoration == null || decoration.debugAssertIsValid()), + assert(constraints == null || constraints.debugAssertIsValid()), + assert(color == null || decoration == null, + 'Cannot provide both a color and a decoration\n' + 'The color argument is just a shorthand for "decoration: new BoxDecoration(backgroundColor: color)".' + ), + decoration = decoration ?? (color != null ? new BoxDecoration(color: color) : null), constraints = (width != null || height != null) ? constraints?.tighten(width: width, height: height) ?? new BoxConstraints.tightFor(width: width, height: height) : constraints, - super(key: key, curve: curve, duration: duration) { - assert(margin == null || margin.isNonNegative); - assert(padding == null || padding.isNonNegative); - assert(decoration == null || decoration.debugAssertIsValid()); - assert(constraints == null || constraints.debugAssertIsValid()); - assert(color == null || decoration == null, - 'Cannot provide both a color and a decoration\n' - 'The color argument is just a shorthand for "decoration: new BoxDecoration(backgroundColor: color)".' - ); - } + super(key: key, curve: curve, duration: duration); /// The [child] contained by the container. /// @@ -602,7 +601,8 @@ class _AnimatedPositionedState extends AnimatedWidgetBaseState { } } -/// Animated version of [DefaultTextStyle] which automatically -/// transitions the default text style (the text style to apply to -/// descendant [Text] widgets without explicit style) over a given -/// duration whenever the given style changes. +/// Animated version of [DefaultTextStyle] which automatically transitions the +/// default text style (the text style to apply to descendant [Text] widgets +/// without explicit style) over a given duration whenever the given style +/// changes. class AnimatedDefaultTextStyle extends ImplicitlyAnimatedWidget { /// Creates a widget that animates the default text style implicitly. /// @@ -709,6 +709,15 @@ class _AnimatedDefaultTextStyleState extends AnimatedWidgetBaseState children = []; + + if (leading != null) + children.add(new LayoutId(id: _ToolbarSlot.leading, child: leading)); + + if (middle != null) + children.add(new LayoutId(id: _ToolbarSlot.middle, child: middle)); + + if (trailing != null) + children.add(new LayoutId(id: _ToolbarSlot.trailing, child: trailing)); + + return new CustomMultiChildLayout( + delegate: new _ToolbarLayout( + centerMiddle: centerMiddle, + ), + children: children, + ); + } +} + +enum _ToolbarSlot { + leading, + middle, + trailing, +} + +const double _kMiddleMargin = 16.0; + +// TODO(xster): support RTL. +class _ToolbarLayout extends MultiChildLayoutDelegate { + _ToolbarLayout({ this.centerMiddle }); + + // If false the middle widget should be left justified within the space + // between the leading and trailing widgets. + // If true the middle widget is centered within the toolbar (not within the horizontal + // space between the leading and trailing widgets). + // TODO(xster): document RTL once supported. + final bool centerMiddle; + + @override + void performLayout(Size size) { + double leadingWidth = 0.0; + double trailingWidth = 0.0; + + if (hasChild(_ToolbarSlot.leading)) { + final BoxConstraints constraints = new BoxConstraints( + minWidth: 0.0, + maxWidth: size.width / 3.0, // The leading widget shouldn't take up more than 1/3 of the space. + minHeight: size.height, // The height should be exactly the height of the bar. + maxHeight: size.height, + ); + leadingWidth = layoutChild(_ToolbarSlot.leading, constraints).width; + positionChild(_ToolbarSlot.leading, Offset.zero); + } + + if (hasChild(_ToolbarSlot.trailing)) { + final BoxConstraints constraints = new BoxConstraints.loose(size); + final Size trailingSize = layoutChild(_ToolbarSlot.trailing, constraints); + final double trailingLeft = size.width - trailingSize.width; + final double trailingTop = (size.height - trailingSize.height) / 2.0; + trailingWidth = trailingSize.width; + positionChild(_ToolbarSlot.trailing, new Offset(trailingLeft, trailingTop)); + } + + if (hasChild(_ToolbarSlot.middle)) { + final double maxWidth = math.max(size.width - leadingWidth - trailingWidth - _kMiddleMargin * 2.0, 0.0); + final BoxConstraints constraints = new BoxConstraints.loose(size).copyWith(maxWidth: maxWidth); + final Size middleSize = layoutChild(_ToolbarSlot.middle, constraints); + + final double middleLeftMargin = leadingWidth + _kMiddleMargin; + double middleX = middleLeftMargin; + final double middleY = (size.height - middleSize.height) / 2.0; + // If the centered middle will not fit between the leading and trailing + // widgets, then align its left or right edge with the adjacent boundary. + if (centerMiddle) { + middleX = (size.width - middleSize.width) / 2.0; + if (middleX + middleSize.width > size.width - trailingWidth) + middleX = size.width - trailingWidth - middleSize.width; + else if (middleX < middleLeftMargin) + middleX = middleLeftMargin; + } + + positionChild(_ToolbarSlot.middle, new Offset(middleX, middleY)); + } + } + + @override + bool shouldRelayout(_ToolbarLayout oldDelegate) => centerMiddle != oldDelegate.centerMiddle; +} diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index e6ee219b53c42..94ab3b5ebdc84 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -256,8 +256,8 @@ class NavigatorObserver { abstract class NavigationGestureController { /// Configures the NavigationGestureController and tells the given [Navigator] that /// a gesture has started. - NavigationGestureController(this._navigator) { - assert(_navigator != null); + NavigationGestureController(this._navigator) + : assert(_navigator != null) { // Disable Hero transitions until the gesture is complete. _navigator.didStartUserGesture(); } diff --git a/packages/flutter/lib/src/widgets/nested_scroll_view.dart b/packages/flutter/lib/src/widgets/nested_scroll_view.dart index 388c554c43bba..2b6280ec30bd8 100644 --- a/packages/flutter/lib/src/widgets/nested_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/nested_scroll_view.dart @@ -25,25 +25,29 @@ import 'sliver.dart'; import 'ticker_provider.dart'; /// Signature used by [NestedScrollView] for building its header. +/// +/// The `innerBoxIsScrolled` argument is typically used to control the +/// [SliverAppBar.forceElevated] property to ensure that the app bar shows a +/// shadow, since it would otherwise not necessarily be aware that it had +/// content ostensibly below it. typedef List NestedScrollViewHeaderSliversBuilder(BuildContext context, bool innerBoxIsScrolled); // TODO(abarth): Make this configurable with a controller. const double _kInitialScrollOffset = 0.0; class NestedScrollView extends StatefulWidget { - NestedScrollView({ + const NestedScrollView({ Key key, this.scrollDirection: Axis.vertical, this.reverse: false, this.physics, @required this.headerSliverBuilder, @required this.body, - }) : super(key: key) { - assert(scrollDirection != null); - assert(reverse != null); - assert(headerSliverBuilder != null); - assert(body != null); - } + }) : assert(scrollDirection != null), + assert(reverse != null), + assert(headerSliverBuilder != null), + assert(body != null), + super(key: key); // TODO(ianh): we should expose a controller so you can call animateTo, etc. @@ -74,8 +78,18 @@ class NestedScrollView extends StatefulWidget { /// Defaults to matching platform conventions. final ScrollPhysics physics; + /// A builder for any widgets that are to precede the inner scroll views (as + /// given by [body]). + /// + /// Typically this is used to create a [SliverAppBar] with a [TabBar]. final NestedScrollViewHeaderSliversBuilder headerSliverBuilder; + /// The widget to show inside the [NestedScrollView]. + /// + /// Typically this will be [TabBarView]. + /// + /// The [body] is built in a context that provides a [PrimaryScrollController] + /// that interacts with the [NestedScrollView]'s scroll controller. final Widget body; List _buildSlivers(BuildContext context, ScrollController innerController, bool bodyIsScrolled) { @@ -782,10 +796,9 @@ class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity { this.metrics, Simulation simulation, TickerProvider vsync, - ) : super(position, simulation, vsync) { - assert(metrics.minRange != metrics.maxRange); - assert(metrics.maxRange > metrics.minRange); - } + ) : assert(metrics.minRange != metrics.maxRange), + assert(metrics.maxRange > metrics.minRange), + super(position, simulation, vsync); final _NestedScrollCoordinator coordinator; final _NestedScrollMetrics metrics; diff --git a/packages/flutter/lib/src/widgets/overlay.dart b/packages/flutter/lib/src/widgets/overlay.dart index 9f024dffcb00d..e8465c5024a7a 100644 --- a/packages/flutter/lib/src/widgets/overlay.dart +++ b/packages/flutter/lib/src/widgets/overlay.dart @@ -62,11 +62,11 @@ class OverlayEntry { @required this.builder, bool opaque: false, bool maintainState: false, - }) : _opaque = opaque, _maintainState = maintainState { - assert(builder != null); - assert(opaque != null); - assert(maintainState != null); - } + }) : assert(builder != null), + assert(opaque != null), + assert(maintainState != null), + _opaque = opaque, + _maintainState = maintainState; /// This entry will include the widget built by this builder in the overlay at /// the entry's position. @@ -154,9 +154,9 @@ class OverlayEntry { } class _OverlayEntry extends StatefulWidget { - _OverlayEntry(this.entry) : super(key: entry._key) { - assert(entry != null); - } + _OverlayEntry(this.entry) + : assert(entry != null), + super(key: entry._key); final OverlayEntry entry; @@ -388,10 +388,8 @@ class _Theatre extends RenderObjectWidget { _Theatre({ this.onstage, @required this.offstage, - }) { - assert(offstage != null); - assert(!offstage.any((Widget child) => child == null)); - } + }) : assert(offstage != null), + assert(!offstage.any((Widget child) => child == null)); final Stack onstage; @@ -405,9 +403,9 @@ class _Theatre extends RenderObjectWidget { } class _TheatreElement extends RenderObjectElement { - _TheatreElement(_Theatre widget) : super(widget) { - assert(!debugChildrenHaveDuplicateKeys(widget, widget.offstage)); - } + _TheatreElement(_Theatre widget) + : assert(!debugChildrenHaveDuplicateKeys(widget, widget.offstage)), + super(widget); @override _Theatre get widget => super.widget; diff --git a/packages/flutter/lib/src/widgets/overscroll_indicator.dart b/packages/flutter/lib/src/widgets/overscroll_indicator.dart index c628f96e36aec..be12919d10881 100644 --- a/packages/flutter/lib/src/widgets/overscroll_indicator.dart +++ b/packages/flutter/lib/src/widgets/overscroll_indicator.dart @@ -225,11 +225,11 @@ class _GlowController extends ChangeNotifier { @required TickerProvider vsync, @required Color color, @required Axis axis, - }) : _color = color, + }) : assert(vsync != null), + assert(color != null), + assert(axis != null), + _color = color, _axis = axis { - assert(vsync != null); - assert(color != null); - assert(axis != null); _glowController = new AnimationController(vsync: vsync) ..addStatusListener(_changePhase); final Animation decelerator = new CurvedAnimation( diff --git a/packages/flutter/lib/src/widgets/page_storage.dart b/packages/flutter/lib/src/widgets/page_storage.dart index 7d71ac87dbf74..a9acc70e1b4d0 100644 --- a/packages/flutter/lib/src/widgets/page_storage.dart +++ b/packages/flutter/lib/src/widgets/page_storage.dart @@ -6,43 +6,65 @@ import 'package:flutter/foundation.dart'; import 'framework.dart'; +/// A [ValueKey] that defines where [PageStorage] values will be saved. +/// +/// [Scrollable]s ([ScrollPosition]s really) use [PageStorage] to save their +/// scroll offset. Each time a scroll completes, the scrollable's page +/// storage is updated. +/// +/// [PageStorage] is used to save and restore values that can outlive the widget. +/// The values are stored in a per-route [Map] whose keys are defined by the +/// [PageStorageKey]s for the widget and its ancestors. To make it possible +/// for a saved value to be found when a widget is recreated, the key's values +/// must not be objects whose identity will change each time the widget is created. +/// +/// For example, to ensure that the scroll offsets for the scrollable within +/// each `MyScrollableTabView` below are restored when the [TabBarView] +/// is recreated, we've specified [PageStorageKey]s whose values are the the +/// tabs' string labels. +/// +/// ```dart +/// new TabBarView( +/// children: myTabs.map((Tab tab) { +/// new MyScrollableTabView( +/// key: new PageStorageKey(tab.text), // like 'Tab 1' +/// tab: tab, +/// ), +/// }), +///) +/// ``` +class PageStorageKey extends ValueKey { + /// Creates a [ValueKey] that defines where [PageStorage] values will be saved. + const PageStorageKey(T value) : super(value); +} + class _StorageEntryIdentifier { - Type clientType; - List keys; - - void addKey(Key key) { - assert(key != null); - assert(key is! GlobalKey); - keys ??= []; - keys.add(key); + _StorageEntryIdentifier(this.keys) { + assert(keys != null); } - GlobalKey scopeKey; + final List> keys; + + bool get isNotEmpty => keys.isNotEmpty; @override bool operator ==(dynamic other) { - if (other is! _StorageEntryIdentifier) + if (other.runtimeType != runtimeType) return false; final _StorageEntryIdentifier typedOther = other; - if (clientType != typedOther.clientType || - scopeKey != typedOther.scopeKey || - keys?.length != typedOther.keys?.length) - return false; - if (keys != null) { - for (int index = 0; index < keys.length; index += 1) { - if (keys[index] != typedOther.keys[index]) - return false; - } + for (int index = 0; index < keys.length; index += 1) { + if (keys[index] != typedOther.keys[index]) + return false; } return true; } @override - int get hashCode => hashValues(clientType, scopeKey, hashList(keys)); + int get hashCode => hashList(keys); @override String toString() { - return 'StorageEntryIdentifier($clientType, $scopeKey, ${keys?.join(":")})'; + return 'StorageEntryIdentifier(${keys?.join(":")})'; } } @@ -51,51 +73,64 @@ class _StorageEntryIdentifier { /// Useful for storing per-page state that persists across navigations from one /// page to another. class PageStorageBucket { - _StorageEntryIdentifier _computeStorageIdentifier(BuildContext context) { - final _StorageEntryIdentifier result = new _StorageEntryIdentifier(); - result.clientType = context.widget.runtimeType; - Key lastKey = context.widget.key; - if (lastKey is! GlobalKey) { - if (lastKey != null) - result.addKey(lastKey); + static bool _maybeAddKey(BuildContext context, List> keys) { + final Widget widget = context.widget; + final Key key = widget.key; + if (key is PageStorageKey) + keys.add(key); + return widget is! PageStorage; + } + + List> _allKeys(BuildContext context) { + final List> keys = >[]; + if (_maybeAddKey(context, keys)) { context.visitAncestorElements((Element element) { - if (element.widget.key is GlobalKey) { - lastKey = element.widget.key; - return false; - } else if (element.widget.key != null) { - result.addKey(element.widget.key); - } - return true; + return _maybeAddKey(element, keys); }); - return result; } - assert(lastKey is GlobalKey); - result.scopeKey = lastKey; - return result; + return keys; + } + + _StorageEntryIdentifier _computeIdentifier(BuildContext context) { + return new _StorageEntryIdentifier(_allKeys(context)); } Map _storage; - /// Write the given data into this page storage bucket using an identifier - /// computed from the given context. The identifier is based on the keys - /// found in the path from context to the root of the widget tree for this - /// page. Keys are collected until the widget tree's root is reached or - /// a GlobalKey is found. + /// Write the given data into this page storage bucket using the + /// specified identifier or an identifier computed from the given context. + /// The computed identifier is based on the [PageStorageKey]s + /// found in the path from context to the [PageStorage] widget that + /// owns this page storage bucket. /// - /// An explicit identifier can be used in cases where the list of keys - /// is not stable. For example if the path concludes with a GlobalKey - /// that's created by a stateful widget, if the stateful widget is - /// recreated when it's exposed by [Navigator.pop], then its storage - /// identifier will change. + /// If an explicit identifier is not provided and no [PageStorageKey]s + /// are found, then the `data` is not saved. void writeState(BuildContext context, dynamic data, { Object identifier }) { _storage ??= {}; - _storage[identifier ?? _computeStorageIdentifier(context)] = data; + if (identifier != null) { + _storage[identifier] = data; + } else { + final _StorageEntryIdentifier contextIdentifier = _computeIdentifier(context); + if (contextIdentifier.isNotEmpty) + _storage[contextIdentifier] = data; + } } - /// Read given data from into this page storage bucket using an identifier - /// computed from the given context. More about [identifier] in [writeState]. + /// Read given data from into this page storage bucket using the specified + /// identifier or an identifier computed from the given context. + /// The computed identifier is based on the [PageStorageKey]s + /// found in the path from context to the [PageStorage] widget that + /// owns this page storage bucket. + /// + /// If an explicit identifier is not provided and no [PageStorageKey]s + /// are found, then null is returned. dynamic readState(BuildContext context, { Object identifier }) { - return _storage != null ? _storage[identifier ?? _computeStorageIdentifier(context)] : null; + if (_storage == null) + return null; + if (identifier != null) + return _storage[identifier]; + final _StorageEntryIdentifier contextIdentifier = _computeIdentifier(context); + return contextIdentifier.isNotEmpty ? _storage[contextIdentifier] : null; } } @@ -119,7 +154,7 @@ class PageStorage extends StatelessWidget { /// The bucket from the closest instance of this class that encloses the given context. /// - /// Returns `null` if none exists. + /// Returns null if none exists. /// /// Typical usage is as follows: /// diff --git a/packages/flutter/lib/src/widgets/page_view.dart b/packages/flutter/lib/src/widgets/page_view.dart index 353a200bdb62a..54dba500b886e 100644 --- a/packages/flutter/lib/src/widgets/page_view.dart +++ b/packages/flutter/lib/src/widgets/page_view.dart @@ -38,19 +38,36 @@ import 'viewport.dart'; class PageController extends ScrollController { /// Creates a page controller. /// - /// The [initialPage] and [viewportFraction] arguments must not be null. + /// The [initialPage], [keepPage], and [viewportFraction] arguments must not be null. PageController({ this.initialPage: 0, + this.keepPage: true, this.viewportFraction: 1.0, - }) { - assert(initialPage != null); - assert(viewportFraction != null); - assert(viewportFraction > 0.0); - } + }) : assert(initialPage != null), + assert(keepPage != null), + assert(viewportFraction != null), + assert(viewportFraction > 0.0); /// The page to show when first creating the [PageView]. final int initialPage; + /// Save the current [page] with [PageStorage] and restore it if + /// this controller's scrollable is recreated. + /// + /// If this property is set to false, the current [page] is never saved + /// and [initialPage] is always used to initialize the scroll offset. + /// If true (the default), the initial page is used the first time the + /// controller's scrollable is created, since there's isn't a page to + /// restore yet. Subsequently the saved page is restored and + /// [initialPage] is ignored. + /// + /// See also: + /// + /// * [PageStorageKey], which should be used when more than one + //// scrollable appears in the same route, to distinguish the [PageStorage] + /// locations used to save scroll offsets. + final bool keepPage; + /// The fraction of the viewport that each page should occupy. /// /// Defaults to 1.0, which means each page fills the viewport in the scrolling @@ -118,6 +135,7 @@ class PageController extends ScrollController { physics: physics, context: context, initialPage: initialPage, + keepPage: keepPage, viewportFraction: viewportFraction, oldPosition: oldPosition, ); @@ -152,20 +170,22 @@ class _PagePosition extends ScrollPositionWithSingleContext { ScrollPhysics physics, ScrollContext context, this.initialPage: 0, + bool keepPage: true, double viewportFraction: 1.0, ScrollPosition oldPosition, - }) : _viewportFraction = viewportFraction, + }) : assert(initialPage != null), + assert(keepPage != null), + assert(viewportFraction != null), + assert(viewportFraction > 0.0), + _viewportFraction = viewportFraction, _pageToUseOnStartup = initialPage.toDouble(), super( - physics: physics, - context: context, - initialPixels: null, - oldPosition: oldPosition, - ) { - assert(initialPage != null); - assert(viewportFraction != null); - assert(viewportFraction > 0.0); - } + physics: physics, + context: context, + initialPixels: null, + keepScrollOffset: keepPage, + oldPosition: oldPosition, + ); final int initialPage; double _pageToUseOnStartup; @@ -358,9 +378,9 @@ class PageView extends StatefulWidget { this.physics, this.onPageChanged, @required this.childrenDelegate, - }) : controller = controller ?? _defaultPageController, super(key: key) { - assert(childrenDelegate != null); - } + }) : assert(childrenDelegate != null), + controller = controller ?? _defaultPageController, + super(key: key); /// The axis along which the page view scrolls. /// diff --git a/packages/flutter/lib/src/widgets/pages.dart b/packages/flutter/lib/src/widgets/pages.dart index b34ede2acbb46..a37e8b1c0308d 100644 --- a/packages/flutter/lib/src/widgets/pages.dart +++ b/packages/flutter/lib/src/widgets/pages.dart @@ -13,9 +13,18 @@ import 'routes.dart'; abstract class PageRoute extends ModalRoute { /// Creates a modal route that replaces the entire screen. PageRoute({ - RouteSettings settings: const RouteSettings() + RouteSettings settings: const RouteSettings(), + this.fullscreenDialog: false, }) : super(settings: settings); + /// Whether this page route is a full-screen dialog. + /// + /// In Material and Cupertino, being fullscreen has the effects of making + /// the app bars have a close button instead of a back button. On + /// iOS, dialogs transitions animate differently and are also not closeable + /// with the back swipe gesture. + final bool fullscreenDialog; + @override bool get opaque => true; @@ -76,13 +85,12 @@ class PageRouteBuilder extends PageRoute { this.barrierDismissible: false, this.barrierColor: null, this.maintainState: true, - }) : super(settings: settings) { - assert(pageBuilder != null); - assert(transitionsBuilder != null); - assert(opaque != null); - assert(barrierDismissible != null); - assert(maintainState != null); - } + }) : assert(pageBuilder != null), + assert(transitionsBuilder != null), + assert(opaque != null), + assert(barrierDismissible != null), + assert(maintainState != null), + super(settings: settings); /// Used build the route's primary contents. /// diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index 571d86f0b8e33..ddd68b14aa6f5 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -524,7 +524,7 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute _delegate; @@ -466,12 +466,12 @@ class DrivenScrollActivity extends ScrollActivity { @required Duration duration, @required Curve curve, @required TickerProvider vsync, - }) : super(delegate) { - assert(from != null); - assert(to != null); - assert(duration != null); - assert(duration > Duration.ZERO); - assert(curve != null); + }) : assert(from != null), + assert(to != null), + assert(duration != null), + assert(duration > Duration.ZERO), + assert(curve != null), + super(delegate) { _completer = new Completer(); _controller = new AnimationController.unbounded( value: from, diff --git a/packages/flutter/lib/src/widgets/scroll_controller.dart b/packages/flutter/lib/src/widgets/scroll_controller.dart index 1b711bedfaaf5..4f8b268f0b092 100644 --- a/packages/flutter/lib/src/widgets/scroll_controller.dart +++ b/packages/flutter/lib/src/widgets/scroll_controller.dart @@ -39,22 +39,40 @@ import 'scroll_position_with_single_context.dart'; class ScrollController extends ChangeNotifier { /// Creates a controller for a scrollable widget. /// - /// The [initialScrollOffset] must not be null. + /// The values of `initialScrollOffset` and `keepScrollOffset` must not be null. ScrollController({ this.initialScrollOffset: 0.0, + this.keepScrollOffset: true, this.debugLabel, - }) { - assert(initialScrollOffset != null); - } + }) : assert(initialScrollOffset != null), + assert(keepScrollOffset != null); /// The initial value to use for [offset]. /// /// New [ScrollPosition] objects that are created and attached to this - /// controller will have their offset initialized to this value. + /// controller will have their offset initialized to this value + /// if [keepScrollOffset] is false or a scroll offset hasn't been saved yet. /// /// Defaults to 0.0. final double initialScrollOffset; + /// Each time a scroll completes, save the current scroll [offset] with + /// [PageStorage] and restore it if this controller's scrollable is recreated. + /// + /// If this property is set to false, the scroll offset is never saved + /// and [initialScrollOffset] is always used to initialize the scroll + /// offset. If true (the default), the initial scroll offset is used the + /// first time the controller's scrollable is created, since there's no + /// scroll offset to restore yet. Subsequently the saved offset is + /// restored and [initialScrollOffset] is ignored. + /// + /// See also: + /// + /// * [PageStorageKey], which should be used when more than one + //// scrollable appears in the same route, to distinguish the [PageStorage] + /// locations used to save scroll offsets. + final bool keepScrollOffset; + /// A label that is used in the [toString] output. Intended to aid with /// identifying scroll controller instances in debug output. final String debugLabel; @@ -206,6 +224,7 @@ class ScrollController extends ChangeNotifier { physics: physics, context: context, initialPixels: initialScrollOffset, + keepScrollOffset: keepScrollOffset, oldPosition: oldPosition, debugLabel: debugLabel, ); diff --git a/packages/flutter/lib/src/widgets/scroll_notification.dart b/packages/flutter/lib/src/widgets/scroll_notification.dart index 5b57351cf0ce1..aa216c9f452d3 100644 --- a/packages/flutter/lib/src/widgets/scroll_notification.dart +++ b/packages/flutter/lib/src/widgets/scroll_notification.dart @@ -15,6 +15,10 @@ import 'scroll_metrics.dart'; /// /// This is used by [ScrollNotification] and [OverscrollIndicatorNotification]. abstract class ViewportNotificationMixin extends Notification { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory ViewportNotificationMixin._() => null; + /// The number of viewports that this notification has bubbled through. /// /// Typically listeners only respond to notifications with a [depth] of zero. @@ -171,12 +175,11 @@ class OverscrollNotification extends ScrollNotification { this.dragDetails, @required this.overscroll, this.velocity: 0.0, - }) : super(metrics: metrics, context: context) { - assert(overscroll != null); - assert(overscroll.isFinite); - assert(overscroll != 0.0); - assert(velocity != null); - } + }) : assert(overscroll != null), + assert(overscroll.isFinite), + assert(overscroll != 0.0), + assert(velocity != null), + super(metrics: metrics, context: context); /// If the [Scrollable] overscrolled because of a drag, the details about that /// drag update. diff --git a/packages/flutter/lib/src/widgets/scroll_position.dart b/packages/flutter/lib/src/widgets/scroll_position.dart index 4b4e580183f8f..c96ee6ea2fb93 100644 --- a/packages/flutter/lib/src/widgets/scroll_position.dart +++ b/packages/flutter/lib/src/widgets/scroll_position.dart @@ -61,18 +61,22 @@ export 'scroll_activity.dart' show ScrollHoldController; abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { /// Creates an object that determines which portion of the content is visible /// in a scroll view. + /// + /// The [physics], [context], and [keepScrollOffset] parameters must not be null. ScrollPosition({ @required this.physics, @required this.context, + this.keepScrollOffset: true, ScrollPosition oldPosition, this.debugLabel, - }) { - assert(physics != null); - assert(context != null); - assert(context.vsync != null); + }) : assert(physics != null), + assert(context != null), + assert(context.vsync != null), + assert(keepScrollOffset != null) { if (oldPosition != null) absorb(oldPosition); - restoreScrollOffset(); + if (keepScrollOffset) + restoreScrollOffset(); } /// How the scroll position should respond to user input. @@ -86,6 +90,15 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { /// Typically implemented by [ScrollableState]. final ScrollContext context; + /// Save the current scroll [offset] with [PageStorage] and restore it if + /// this scroll position's scrollable is recreated. + /// + /// See also: + /// + /// * [ScrollController.keepScrollOffset] and [PageController.keepPage], which + /// create scroll positions and initialize this property. + final bool keepScrollOffset; + /// A label that is used in the [toString] output. Intended to aid with /// identifying animation controller instances in debug output. final String debugLabel; @@ -540,7 +553,8 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { /// This also saves the scroll offset using [saveScrollOffset]. void didEndScroll() { activity.dispatchScrollEndNotification(cloneMetrics(), context.notificationContext); - saveScrollOffset(); + if (keepScrollOffset) + saveScrollOffset(); } /// Called by [setPixels] to report overscroll when an attempt is made to diff --git a/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart b/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart index 287f739fdda5c..4d7b81ba6521d 100644 --- a/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart +++ b/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart @@ -46,13 +46,24 @@ class ScrollPositionWithSingleContext extends ScrollPosition implements ScrollAc /// imperative that the value be set, using [correctPixels], as soon as /// [applyNewDimensions] is invoked, before calling the inherited /// implementation of that method. + /// + /// If [keepScrollOffset] is true (the default), the current scroll offset is + /// saved with [PageStorage] and restored it if this scroll position's scrollable + /// is recreated. ScrollPositionWithSingleContext({ @required ScrollPhysics physics, @required ScrollContext context, double initialPixels: 0.0, + bool keepScrollOffset: true, ScrollPosition oldPosition, String debugLabel, - }) : super(physics: physics, context: context, oldPosition: oldPosition, debugLabel: debugLabel) { + }) : super( + physics: physics, + context: context, + keepScrollOffset: keepScrollOffset, + oldPosition: oldPosition, + debugLabel: debugLabel, + ) { // If oldPosition is not null, the superclass will first call absorb(), // which may set _pixels and _activity. if (pixels == null && initialPixels != null) diff --git a/packages/flutter/lib/src/widgets/scroll_simulation.dart b/packages/flutter/lib/src/widgets/scroll_simulation.dart index 03d3e2c3fc1ae..33d851401990b 100644 --- a/packages/flutter/lib/src/widgets/scroll_simulation.dart +++ b/packages/flutter/lib/src/widgets/scroll_simulation.dart @@ -35,14 +35,13 @@ class BouncingScrollSimulation extends Simulation { @required this.trailingExtent, @required this.spring, Tolerance tolerance: Tolerance.defaultTolerance, - }) : super(tolerance: tolerance) { - assert(position != null); - assert(velocity != null); - assert(leadingExtent != null); - assert(trailingExtent != null); - assert(leadingExtent <= trailingExtent); - assert(spring != null); - + }) : assert(position != null), + assert(velocity != null), + assert(leadingExtent != null), + assert(trailingExtent != null), + assert(leadingExtent <= trailingExtent), + assert(spring != null), + super(tolerance: tolerance) { if (position < leadingExtent) { _springSimulation = _underscrollSimulation(position, velocity); _springTime = double.NEGATIVE_INFINITY; @@ -136,8 +135,8 @@ class ClampingScrollSimulation extends Simulation { @required this.velocity, this.friction: 0.015, Tolerance tolerance: Tolerance.defaultTolerance, - }) : super(tolerance: tolerance) { - assert(_flingVelocityPenetration(0.0) == _kInitialVelocityPenetration); + }) : assert(_flingVelocityPenetration(0.0) == _kInitialVelocityPenetration), + super(tolerance: tolerance) { _duration = _flingDuration(velocity); _distance = (velocity * _duration / _kInitialVelocityPenetration).abs(); } diff --git a/packages/flutter/lib/src/widgets/scroll_view.dart b/packages/flutter/lib/src/widgets/scroll_view.dart index 27f3eb875a245..8ff1896254c5b 100644 --- a/packages/flutter/lib/src/widgets/scroll_view.dart +++ b/packages/flutter/lib/src/widgets/scroll_view.dart @@ -51,17 +51,15 @@ abstract class ScrollView extends StatelessWidget { bool primary, ScrollPhysics physics, this.shrinkWrap: false, - }) : primary = primary ?? controller == null && scrollDirection == Axis.vertical, - physics = physics ?? (primary == true || (primary == null && controller == null && scrollDirection == Axis.vertical) ? const AlwaysScrollableScrollPhysics() : null), - super(key: key) { - assert(reverse != null); - assert(shrinkWrap != null); - assert(this.primary != null); - assert(controller == null || !this.primary, + }) : assert(reverse != null), + assert(shrinkWrap != null), + assert(!(controller != null && primary == true), 'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. ' 'You cannot both set primary to true and pass an explicit controller.' - ); - } + ), + primary = primary ?? controller == null && scrollDirection == Axis.vertical, + physics = physics ?? (primary == true || (primary == null && controller == null && scrollDirection == Axis.vertical) ? const AlwaysScrollableScrollPhysics() : null), + super(key: key); /// The axis along which the scroll view scrolls. /// @@ -511,18 +509,17 @@ class ListView extends BoxScrollView { EdgeInsets padding, this.itemExtent, @required this.childrenDelegate, - }) : super( - key: key, - scrollDirection: scrollDirection, - reverse: reverse, - controller: controller, - primary: primary, - physics: physics, - shrinkWrap: shrinkWrap, - padding: padding, - ) { - assert(childrenDelegate != null); - } + }) : assert(childrenDelegate != null), + super( + key: key, + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + ); /// If non-null, forces the children to have the given extent in the scroll /// direction. @@ -607,18 +604,18 @@ class GridView extends BoxScrollView { EdgeInsets padding, @required this.gridDelegate, List children: const [], - }) : childrenDelegate = new SliverChildListDelegate(children), super( - key: key, - scrollDirection: scrollDirection, - reverse: reverse, - controller: controller, - primary: primary, - physics: physics, - shrinkWrap: shrinkWrap, - padding: padding, - ) { - assert(gridDelegate != null); - } + }) : assert(gridDelegate != null), + childrenDelegate = new SliverChildListDelegate(children), + super( + key: key, + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + ); /// Creates a scrollable, 2D array of widgets that are created on demand. /// @@ -645,18 +642,18 @@ class GridView extends BoxScrollView { @required this.gridDelegate, @required IndexedWidgetBuilder itemBuilder, int itemCount, - }) : childrenDelegate = new SliverChildBuilderDelegate(itemBuilder, childCount: itemCount), super( - key: key, - scrollDirection: scrollDirection, - reverse: reverse, - controller: controller, - primary: primary, - physics: physics, - shrinkWrap: shrinkWrap, - padding: padding, - ) { - assert(gridDelegate != null); - } + }) : assert(gridDelegate != null), + childrenDelegate = new SliverChildBuilderDelegate(itemBuilder, childCount: itemCount), + super( + key: key, + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + ); /// Creates a scrollable, 2D array of widgets with both a custom /// [SliverGridDelegate] and a custom [SliverChildDelegate]. @@ -676,19 +673,18 @@ class GridView extends BoxScrollView { EdgeInsets padding, @required this.gridDelegate, @required this.childrenDelegate, - }) : super( - key: key, - scrollDirection: scrollDirection, - reverse: reverse, - controller: controller, - primary: primary, - physics: physics, - shrinkWrap: shrinkWrap, - padding: padding, - ) { - assert(gridDelegate != null); - assert(childrenDelegate != null); - } + }) : assert(gridDelegate != null), + assert(childrenDelegate != null), + super( + key: key, + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + ); /// Creates a scrollable, 2D array of widgets with a fixed number of tiles in /// the cross axis. diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index c317b457fe698..45e06e99cf249 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -327,32 +327,38 @@ class ScrollableState extends State with TickerProviderStateMixin switch (widget.axis) { case Axis.vertical: _gestureRecognizers = { - VerticalDragGestureRecognizer: (VerticalDragGestureRecognizer recognizer) { // ignore: map_value_type_not_assignable, https://github.com/flutter/flutter/issues/7173 - return (recognizer ??= new VerticalDragGestureRecognizer()) - ..onDown = _handleDragDown - ..onStart = _handleDragStart - ..onUpdate = _handleDragUpdate - ..onEnd = _handleDragEnd - ..onCancel = _handleDragCancel - ..minFlingDistance = _physics?.minFlingDistance - ..minFlingVelocity = _physics?.minFlingVelocity - ..maxFlingVelocity = _physics?.maxFlingVelocity; - } + VerticalDragGestureRecognizer: new GestureRecognizerFactoryWithHandlers( + () => new VerticalDragGestureRecognizer(), + (VerticalDragGestureRecognizer instance) { + instance + ..onDown = _handleDragDown + ..onStart = _handleDragStart + ..onUpdate = _handleDragUpdate + ..onEnd = _handleDragEnd + ..onCancel = _handleDragCancel + ..minFlingDistance = _physics?.minFlingDistance + ..minFlingVelocity = _physics?.minFlingVelocity + ..maxFlingVelocity = _physics?.maxFlingVelocity; + }, + ), }; break; case Axis.horizontal: _gestureRecognizers = { - HorizontalDragGestureRecognizer: (HorizontalDragGestureRecognizer recognizer) { // ignore: map_value_type_not_assignable, https://github.com/flutter/flutter/issues/7173 - return (recognizer ??= new HorizontalDragGestureRecognizer()) - ..onDown = _handleDragDown - ..onStart = _handleDragStart - ..onUpdate = _handleDragUpdate - ..onEnd = _handleDragEnd - ..onCancel = _handleDragCancel - ..minFlingDistance = _physics?.minFlingDistance - ..minFlingVelocity = _physics?.minFlingVelocity - ..maxFlingVelocity = _physics?.maxFlingVelocity; - } + HorizontalDragGestureRecognizer: new GestureRecognizerFactoryWithHandlers( + () => new HorizontalDragGestureRecognizer(), + (HorizontalDragGestureRecognizer instance) { + instance + ..onDown = _handleDragDown + ..onStart = _handleDragStart + ..onUpdate = _handleDragUpdate + ..onEnd = _handleDragEnd + ..onCancel = _handleDragCancel + ..minFlingDistance = _physics?.minFlingDistance + ..minFlingVelocity = _physics?.minFlingVelocity + ..maxFlingVelocity = _physics?.maxFlingVelocity; + }, + ), }; break; } diff --git a/packages/flutter/lib/src/widgets/semantics_debugger.dart b/packages/flutter/lib/src/widgets/semantics_debugger.dart index d8de489a5c61c..f6e1c2e7ccacd 100644 --- a/packages/flutter/lib/src/widgets/semantics_debugger.dart +++ b/packages/flutter/lib/src/widgets/semantics_debugger.dart @@ -65,8 +65,13 @@ class _SemanticsDebuggerState extends State with WidgetsBindi void _update() { SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { - // We want the update to take effect next frame, so to make that - // explicit we call setState() in a post-frame callback. + // Semantic information are only available at the end of a frame and our + // only chance to paint them on the screen is the next frame. To achieve + // this, we call setState() in a post-frame callback. THIS PATTERN SHOULD + // NOT BE COPIED. Calling setState() in a post-frame callback is a bad + // idea as it will not schedule a frame and your app may be lagging behind + // by one frame. We manually call scheduleFrame() to force a frame and + // ensure that the semantic information are always painted on the screen. if (mounted) { // If we got disposed this frame, we will still get an update, // because the inactive list is flushed after the semantics updates @@ -74,6 +79,7 @@ class _SemanticsDebuggerState extends State with WidgetsBindi setState(() { // The generation of the _SemanticsDebuggerListener has changed. }); + SchedulerBinding.instance.scheduleFrame(); } }); } @@ -252,6 +258,7 @@ void _paintMessage(Canvas canvas, SemanticsNode node) { canvas.clipRect(rect); final TextPainter textPainter = new TextPainter() ..text = new TextSpan(style: _messageStyle, text: message) + ..textAlign = TextAlign.center ..layout(maxWidth: rect.width); textPainter.paint(canvas, FractionalOffset.center.inscribe(textPainter.size, rect).topLeft); diff --git a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart index 17b4ed6527d9f..e7af006eb435c 100644 --- a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart @@ -49,15 +49,13 @@ class SingleChildScrollView extends StatelessWidget { this.physics, this.controller, this.child, - }) : primary = primary ?? controller == null && scrollDirection == Axis.vertical, - super(key: key) { - assert(scrollDirection != null); - assert(this.primary != null); - assert(controller == null || !this.primary, - 'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. ' - 'You cannot both set primary to true and pass an explicit controller.' - ); - } + }) : assert(scrollDirection != null), + assert(!(controller != null && primary == true), + 'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. ' + 'You cannot both set primary to true and pass an explicit controller.' + ), + primary = primary ?? controller == null && scrollDirection == Axis.vertical, + super(key: key); /// The axis along which the scroll view scrolls. /// @@ -180,10 +178,10 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix AxisDirection axisDirection: AxisDirection.down, @required ViewportOffset offset, RenderBox child, - }) : _axisDirection = axisDirection, + }) : assert(axisDirection != null), + assert(offset != null), + _axisDirection = axisDirection, _offset = offset { - assert(axisDirection != null); - assert(offset != null); this.child = child; } diff --git a/packages/flutter/lib/src/widgets/size_changed_layout_notifier.dart b/packages/flutter/lib/src/widgets/size_changed_layout_notifier.dart index a3d09ac0e9632..32c3bbd75de4f 100644 --- a/packages/flutter/lib/src/widgets/size_changed_layout_notifier.dart +++ b/packages/flutter/lib/src/widgets/size_changed_layout_notifier.dart @@ -10,15 +10,15 @@ import 'package:flutter/widgets.dart'; /// this notification has changed, and that therefore any assumptions about that /// layout are no longer valid. /// -/// For example, sent by [SizeChangedLayoutNotifier] whenever -/// [SizeChangedLayoutNotifier] changes size. +/// For example, sent by the [SizeChangedLayoutNotifier] widget whenever that +/// widget changes size. /// -/// This notification for triggering repaints, but if you use this notification -/// to trigger rebuilds or relayouts, you'll create a backwards dependency in -/// the frame pipeline because [SizeChangedLayoutNotification]s are generated -/// during layout, which is after the build phase and in the middle of the -/// layout phase. This backwards dependency can lead to visual corruption or -/// lags. +/// This notification can be used for triggering repaints, but if you use this +/// notification to trigger rebuilds or relayouts, you'll create a backwards +/// dependency in the frame pipeline because [SizeChangedLayoutNotification]s +/// are generated during layout, which is after the build phase and in the +/// middle of the layout phase. This backwards dependency can lead to visual +/// corruption or lags. /// /// See [LayoutChangedNotification] for additional discussion of layout /// notifications such as this one. @@ -26,16 +26,28 @@ import 'package:flutter/widgets.dart'; /// See also: /// /// * [SizeChangedLayoutNotifier], which sends this notification. +/// * [LayoutChangedNotification], of which this is a subclass. class SizeChangedLayoutNotification extends LayoutChangedNotification { } /// A widget that automatically dispatches a [SizeChangedLayoutNotification] -/// when the layout of its child changes. -/// -/// Useful especially when having some complex, layout-changing animation within -/// [Material] that is also interactive. +/// when the layout dimensions of its child change. /// /// The notification is not sent for the initial layout (since the size doesn't /// change in that case, it's just established). +/// +/// To listen for the notification dispatched by this widget, use a +/// [NotificationListener]. +/// +/// The [Material] class listens for [LayoutChangedNotification]s, including +/// [SizeChangedLayoutNotification]s, to repaint [InkResponse] and [InkWell] ink +/// effects. When a widget is likely to change size, wrapping it in a +/// [SizeChangedLayoutNotifier] will cause the ink effects to correctly repaint +/// when the child changes size. +/// +/// See also: +/// +/// * [Notification], the base class for notifications that bubble through the +/// widget tree. class SizeChangedLayoutNotifier extends SingleChildRenderObjectWidget { /// Creates a [SizeChangedLayoutNotifier] that dispatches layout changed /// notifications when [child] changes layout size. @@ -58,9 +70,8 @@ class _RenderSizeChangedWithCallback extends RenderProxyBox { _RenderSizeChangedWithCallback({ RenderBox child, @required this.onLayoutChangedCallback - }) : super(child) { - assert(onLayoutChangedCallback != null); - } + }) : assert(onLayoutChangedCallback != null), + super(child); // There's a 1:1 relationship between the _RenderSizeChangedWithCallback and // the `context` that is captured by the closure created by createRenderObject @@ -74,6 +85,8 @@ class _RenderSizeChangedWithCallback extends RenderProxyBox { @override void performLayout() { super.performLayout(); + // Don't send the initial notification, or this will be SizeObserver all + // over again! if (_oldSize != null && size != _oldSize) onLayoutChangedCallback(); _oldSize = size; diff --git a/packages/flutter/lib/src/widgets/sliver_persistent_header.dart b/packages/flutter/lib/src/widgets/sliver_persistent_header.dart index 8e90bedffca28..6836268398a4f 100644 --- a/packages/flutter/lib/src/widgets/sliver_persistent_header.dart +++ b/packages/flutter/lib/src/widgets/sliver_persistent_header.dart @@ -140,7 +140,8 @@ class _SliverPersistentHeaderElement extends RenderObjectElement { @override void visitChildren(ElementVisitor visitor) { - visitor(child); + if (child != null) + visitor(child); } } @@ -166,7 +167,11 @@ abstract class _SliverPersistentHeaderRenderObjectWidget extends RenderObjectWid } } -abstract class _RenderSliverPersistentHeaderForWidgetsMixin implements RenderSliverPersistentHeader { +abstract class _RenderSliverPersistentHeaderForWidgetsMixin extends RenderSliverPersistentHeader { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory _RenderSliverPersistentHeaderForWidgetsMixin._() => null; + _SliverPersistentHeaderElement _element; @override diff --git a/packages/flutter/lib/src/widgets/table.dart b/packages/flutter/lib/src/widgets/table.dart index badd984aa7700..6f302b62885e9 100644 --- a/packages/flutter/lib/src/widgets/table.dart +++ b/packages/flutter/lib/src/widgets/table.dart @@ -100,22 +100,44 @@ class Table extends RenderObjectWidget { this.border, this.defaultVerticalAlignment: TableCellVerticalAlignment.top, this.textBaseline - }) : _rowDecorations = children.any((TableRow row) => row.decoration != null) - ? children.map((TableRow row) => row.decoration).toList(growable: false) - : null, + }) : assert(children != null), + assert(defaultColumnWidth != null), + assert(defaultVerticalAlignment != null), + assert(() { + if (children.any((TableRow row) => row.children.any((Widget cell) => cell == null))) { + throw new FlutterError( + 'One of the children of one of the rows of the table was null.\n' + 'The children of a TableRow must not be null.' + ); + } + return true; + }), + assert(() { + if (children.any((TableRow row1) => row1.key != null && children.any((TableRow row2) => row1 != row2 && row1.key == row2.key))) { + throw new FlutterError( + 'Two or more TableRow children of this Table had the same key.\n' + 'All the keyed TableRow children of a Table must have different Keys.' + ); + } + return true; + }), + assert(() { + if (children.isNotEmpty) { + final int cellCount = children.first.children.length; + if (children.any((TableRow row) => row.children.length != cellCount)) { + throw new FlutterError( + 'Table contains irregular row lengths.\n' + 'Every TableRow in a Table must have the same number of children, so that every cell is filled. ' + 'Otherwise, the table will contain holes.' + ); + } + } + return true; + }), + _rowDecorations = children.any((TableRow row) => row.decoration != null) + ? children.map((TableRow row) => row.decoration).toList(growable: false) + : null, super(key: key) { - assert(children != null); - assert(defaultColumnWidth != null); - assert(defaultVerticalAlignment != null); - assert(() { - if (children.any((TableRow row) => row.children.any((Widget cell) => cell == null))) { - throw new FlutterError( - 'One of the children of one of the rows of the table was null.\n' - 'The children of a TableRow must not be null.' - ); - } - return true; - }); assert(() { final List flatChildren = children.expand((TableRow row) => row.children).toList(growable: false); if (debugChildrenHaveDuplicateKeys(this, flatChildren)) { @@ -128,28 +150,6 @@ class Table extends RenderObjectWidget { } return true; }); - assert(() { - if (children.any((TableRow row1) => row1.key != null && children.any((TableRow row2) => row1 != row2 && row1.key == row2.key))) { - throw new FlutterError( - 'Two or more TableRow children of this Table had the same key.\n' - 'All the keyed TableRow children of a Table must have different Keys.' - ); - } - return true; - }); - assert(() { - if (children.isNotEmpty) { - final int cellCount = children.first.children.length; - if (children.any((TableRow row) => row.children.length != cellCount)) { - throw new FlutterError( - 'Table contains irregular row lengths.\n' - 'Every TableRow in a Table must have the same number of children, so that every cell is filled. ' - 'Otherwise, the table will contain holes.' - ); - } - } - return true; - }); } /// The rows of the table. diff --git a/packages/flutter/lib/src/widgets/text.dart b/packages/flutter/lib/src/widgets/text.dart index 5f4097ea44ab9..a9b4313538c3f 100644 --- a/packages/flutter/lib/src/widgets/text.dart +++ b/packages/flutter/lib/src/widgets/text.dart @@ -14,6 +14,14 @@ class DefaultTextStyle extends InheritedWidget { /// /// Consider using [DefaultTextStyle.merge] to inherit styling information /// from the current default text style for a given [BuildContext]. + /// + /// The [style] and [child] arguments are required and must not be null. + /// + /// The [softWrap] and [overflow] arguments must not be null (though they do + /// have default values). + /// + /// The [maxLines] property may be null (and indeed defaults to null), but if + /// it is not null, it must be greater than zero. const DefaultTextStyle({ Key key, @required this.style, @@ -25,6 +33,7 @@ class DefaultTextStyle extends InheritedWidget { }) : assert(style != null), assert(softWrap != null), assert(overflow != null), + assert(maxLines == null || maxLines > 0), assert(child != null), super(key: key, child: child); @@ -48,6 +57,15 @@ class DefaultTextStyle extends InheritedWidget { /// for the [BuildContext] where the widget is inserted, and any of the other /// arguments that are not null replace the corresponding properties on that /// same default text style. + /// + /// This constructor cannot be used to override the [maxLines] property of the + /// ancestor with the value null, since null here is used to mean "defer to + /// ancestor". To replace a non-null [maxLines] from an ancestor with the null + /// value (to remove the restriction on number of lines), manually obtain the + /// ambient [DefaultTextStyle] using [DefaultTextStyle.of], then create a new + /// [DefaultTextStyle] using the [new DefaultTextStyle] constructor directly. + /// See the source below for an example of how to do this (since that's + /// essentially what this constructor does). static Widget merge({ Key key, TextStyle style, @@ -91,6 +109,12 @@ class DefaultTextStyle extends InheritedWidget { /// An optional maximum number of lines for the text to span, wrapping if necessary. /// If the text exceeds the given number of lines, it will be truncated according /// to [overflow]. + /// + /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the + /// edge of the box. + /// + /// If this is non-null, it will override even explicit null values of + /// [Text.maxLines]. final int maxLines; /// The closest instance of this class that encloses the given context. @@ -139,10 +163,34 @@ class DefaultTextStyle extends InheritedWidget { /// To display text that uses multiple styles (e.g., a paragraph with some bold /// words), use [RichText]. /// +/// ## Sample code +/// +/// ```dart +/// new Text( +/// 'Hello, $_name! How are you?', +/// textAlign: TextAlign.center, +/// overflow: TextOverflow.ellipsis, +/// style: new TextStyle(fontWeight: FontWeight.bold), +/// ) +/// ``` +/// +/// ## Interactivity +/// +/// To make [Text] react to touch events, wrap it in a [GestureDetector] widget +/// with a [GestureDetector.onTap] handler. +/// +/// In a material design application, consider using a [FlatButton] instead, or +/// if that isn't appropriate, at least using an [InkWell] instead of +/// [GestureDetector]. +/// +/// To make sections of the text interactive, use [RichText] and specify a +/// [TapGestureRecognizer] as the [TextSpan.recognizer] of the relevant part of +/// the text. +/// /// See also: /// -/// * [RichText] -/// * [DefaultTextStyle] +/// * [RichText], which gives you more control over the text styles. +/// * [DefaultTextStyle], which sets default styles for [Text] widgets. class Text extends StatelessWidget { /// Creates a text widget. /// @@ -189,9 +237,17 @@ class Text extends StatelessWidget { /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope. final double textScaleFactor; - /// An optional maximum number of lines the text is allowed to take up. + /// An optional maximum number of lines for the text to span, wrapping if necessary. /// If the text exceeds the given number of lines, it will be truncated according /// to [overflow]. + /// + /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the + /// edge of the box. + /// + /// If this is null, but there is an ambient [DefaultTextStyle] that specifies + /// an explicit number for its [DefaultTextStyle.maxLines], then the + /// [DefaultTextStyle] value will take precedence. You can use a [RichText] + /// widget directly to entirely override the [DefaultTextStyle]. final int maxLines; @override @@ -208,7 +264,7 @@ class Text extends StatelessWidget { maxLines: maxLines ?? defaultTextStyle.maxLines, text: new TextSpan( style: effectiveTextStyle, - text: data + text: data, ) ); } diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index 35e18d5b756b6..d589eb5f34a9f 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -95,9 +95,9 @@ class TextSelectionOverlay implements TextSelectionDelegate { this.renderObject, this.onSelectionOverlayChanged, this.selectionControls, - }): _value = value { - assert(value != null); - assert(context != null); + }): assert(value != null), + assert(context != null), + _value = value { final OverlayState overlay = Overlay.of(context); assert(overlay != null); _handleController = new AnimationController(duration: _kFadeDuration, vsync: overlay); diff --git a/packages/flutter/lib/src/widgets/ticker_provider.dart b/packages/flutter/lib/src/widgets/ticker_provider.dart index 4850c37fe12be..190ce3d404a92 100644 --- a/packages/flutter/lib/src/widgets/ticker_provider.dart +++ b/packages/flutter/lib/src/widgets/ticker_provider.dart @@ -73,7 +73,10 @@ class TickerMode extends InheritedWidget { /// This mixin only supports vending a single ticker. If you might have multiple /// [AnimationController] objects over the lifetime of the [State], use a full /// [TickerProviderStateMixin] instead. -abstract class SingleTickerProviderStateMixin implements State, TickerProvider { // ignore: TYPE_ARGUMENT_NOT_MATCHING_BOUNDS, https://github.com/dart-lang/sdk/issues/25232 +abstract class SingleTickerProviderStateMixin extends State implements TickerProvider { // ignore: TYPE_ARGUMENT_NOT_MATCHING_BOUNDS, https://github.com/dart-lang/sdk/issues/25232 + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory SingleTickerProviderStateMixin._() => null; Ticker _ticker; @@ -150,7 +153,10 @@ abstract class SingleTickerProviderStateMixin implements State, TickerP /// If you only have a single [Ticker] (for example only a single /// [AnimationController]) for the lifetime of your [State], then using a /// [SingleTickerProviderStateMixin] is more efficient. This is the common case. -abstract class TickerProviderStateMixin implements State, TickerProvider { // ignore: TYPE_ARGUMENT_NOT_MATCHING_BOUNDS, https://github.com/dart-lang/sdk/issues/25232 +abstract class TickerProviderStateMixin extends State implements TickerProvider { // ignore: TYPE_ARGUMENT_NOT_MATCHING_BOUNDS, https://github.com/dart-lang/sdk/issues/25232 + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory TickerProviderStateMixin._() => null; Set _tickers; diff --git a/packages/flutter/lib/src/widgets/title.dart b/packages/flutter/lib/src/widgets/title.dart index 001edc92232f4..c13e8a379fbb3 100644 --- a/packages/flutter/lib/src/widgets/title.dart +++ b/packages/flutter/lib/src/widgets/title.dart @@ -14,9 +14,8 @@ class Title extends StatelessWidget { this.title, this.color, @required this.child, - }) : super(key: key) { - assert(color == null || color.alpha == 0xFF); - } + }) : assert(color == null || color.alpha == 0xFF), + super(key: key); /// A one-line description of this app for use in the window manager. final String title; diff --git a/packages/flutter/lib/src/widgets/transitions.dart b/packages/flutter/lib/src/widgets/transitions.dart index 800fb3b0e2419..bfd4ecc7a1858 100644 --- a/packages/flutter/lib/src/widgets/transitions.dart +++ b/packages/flutter/lib/src/widgets/transitions.dart @@ -15,7 +15,7 @@ export 'package:flutter/rendering.dart' show RelativeRect; /// A widget that rebuilds when the given [Listenable] changes value. /// -/// [AnimatedWidget] is most common used with [Animation] objects, which are +/// [AnimatedWidget] is most commonly used with [Animation] objects, which are /// [Listenable], but it can be used with any [Listenable], including /// [ChangeNotifier] and [ValueNotifier]. /// @@ -450,6 +450,11 @@ typedef Widget TransitionBuilder(BuildContext context, Widget child); /// an animation as part of a larger build function. To use AnimatedBuilder, /// simply construct the widget and pass it a builder function. /// +/// For simple cases without additional state, consider using +/// [AnimatedWidget]. +/// +/// ## Performance optimisations +/// /// If your [builder] function contains a subtree that does not depend on the /// animation, it's more efficient to build that subtree once instead of /// rebuilding it on every animation tick. @@ -461,8 +466,51 @@ typedef Widget TransitionBuilder(BuildContext context, Widget child); /// Using this pre-built child is entirely optional, but can improve /// performance significantly in some cases and is therefore a good practice. /// -/// For simple cases without additional state, consider using -/// [AnimatedWidget]. +/// ## Sample code +/// +/// This code defines a widget called `Spinner` that spins a green square +/// continually. It is built with an [AnimatedBuilder] and makes use of the +/// [child] feature to avoid having to rebuild the [Container] each time. +/// +/// ```dart +/// class Spinner extends StatefulWidget { +/// @override +/// _SpinnerState createState() => new _SpinnerState(); +/// } +/// +/// class _SpinnerState extends State with SingleTickerProviderStateMixin { +/// AnimationController _controller; +/// +/// @override +/// void initState() { +/// super.initState(); +/// _controller = new AnimationController( +/// duration: const Duration(seconds: 10), +/// vsync: this, +/// )..repeat(); +/// } +/// +/// @override +/// void dispose() { +/// _controller.dispose(); +/// super.dispose(); +/// } +/// +/// @override +/// Widget build(BuildContext context) { +/// return new AnimatedBuilder( +/// animation: _controller, +/// child: new Container(width: 200.0, height: 200.0, color: Colors.green), +/// builder: (BuildContext context, Widget child) { +/// return new Transform.rotate( +/// angle: _controller.value * 2.0 * math.PI, +/// child: child, +/// ); +/// }, +/// ); +/// } +/// } +/// ``` class AnimatedBuilder extends AnimatedWidget { /// Creates an animated builder. /// diff --git a/packages/flutter/lib/src/widgets/viewport.dart b/packages/flutter/lib/src/widgets/viewport.dart index d39b5aa0fd0b6..71bc9156491e5 100644 --- a/packages/flutter/lib/src/widgets/viewport.dart +++ b/packages/flutter/lib/src/widgets/viewport.dart @@ -56,10 +56,10 @@ class Viewport extends MultiChildRenderObjectWidget { @required this.offset, this.center, List slivers: const [], - }) : super(key: key, children: slivers) { - assert(offset != null); - assert(center == null || children.where((Widget child) => child.key == center).length == 1); - } + }) : assert(offset != null), + assert(slivers != null), + assert(center == null || slivers.where((Widget child) => child.key == center).length == 1), + super(key: key, children: slivers); /// The direction in which the [offset]'s [ViewportOffset.pixels] increases. /// @@ -203,9 +203,8 @@ class ShrinkWrappingViewport extends MultiChildRenderObjectWidget { this.axisDirection: AxisDirection.down, @required this.offset, List slivers: const [], - }) : super(key: key, children: slivers) { - assert(offset != null); - } + }) : assert(offset != null), + super(key: key, children: slivers); /// The direction in which the [offset]'s [ViewportOffset.pixels] increases. /// diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index a411ff948bfab..6811b8e747938 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -46,6 +46,7 @@ export 'src/widgets/layout_builder.dart'; export 'src/widgets/locale_query.dart'; export 'src/widgets/media_query.dart'; export 'src/widgets/modal_barrier.dart'; +export 'src/widgets/navigation_toolbar.dart'; export 'src/widgets/navigator.dart'; export 'src/widgets/nested_scroll_view.dart'; export 'src/widgets/notification_listener.dart'; diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml index 2bc6e197ef4be..095456c136a82 100644 --- a/packages/flutter/pubspec.yaml +++ b/packages/flutter/pubspec.yaml @@ -1,5 +1,5 @@ name: flutter -version: 0.0.27-dev +version: 0.0.30-dev author: Flutter Authors description: A framework for writing Flutter applications homepage: http://flutter.io diff --git a/packages/flutter/test/cupertino/activity_indicator_test.dart b/packages/flutter/test/cupertino/activity_indicator_test.dart index 5f84af36a28ec..642f90e08f6de 100644 --- a/packages/flutter/test/cupertino/activity_indicator_test.dart +++ b/packages/flutter/test/cupertino/activity_indicator_test.dart @@ -4,7 +4,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { diff --git a/packages/flutter/test/cupertino/bottom_tab_bar_test.dart b/packages/flutter/test/cupertino/bottom_tab_bar_test.dart index ad1bdf15a3a3b..9005414c86df0 100644 --- a/packages/flutter/test/cupertino/bottom_tab_bar_test.dart +++ b/packages/flutter/test/cupertino/bottom_tab_bar_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:flutter/cupertino.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import '../services/mocks_for_image_cache.dart'; diff --git a/packages/flutter/test/cupertino/button_test.dart b/packages/flutter/test/cupertino/button_test.dart index 5e4c02604a2c1..75f12590eba6a 100644 --- a/packages/flutter/test/cupertino/button_test.dart +++ b/packages/flutter/test/cupertino/button_test.dart @@ -4,7 +4,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; const TextStyle testStyle = const TextStyle( diff --git a/packages/flutter/test/cupertino/nav_bar_test.dart b/packages/flutter/test/cupertino/nav_bar_test.dart new file mode 100644 index 0000000000000..ff65491610319 --- /dev/null +++ b/packages/flutter/test/cupertino/nav_bar_test.dart @@ -0,0 +1,70 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Middle still in center with asymmetrical actions', (WidgetTester tester) async { + await tester.pumpWidget( + new WidgetsApp( + color: const Color(0xFFFFFFFF), + onGenerateRoute: (RouteSettings settings) { + return new PageRouteBuilder( + settings: settings, + pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { + return const CupertinoNavigationBar( + leading: const CupertinoButton(child: const Text('Something'), onPressed: null,), + middle: const Text('Title'), + ); + }, + ); + }, + ), + ); + + // Expect the middle of the title to be exactly in the middle of the screen. + expect(tester.getCenter(find.text('Title')).dx, 400.0); + }); + + testWidgets('Opaque background does not add blur effects', (WidgetTester tester) async { + await tester.pumpWidget( + new WidgetsApp( + color: const Color(0xFFFFFFFF), + onGenerateRoute: (RouteSettings settings) { + return new PageRouteBuilder( + settings: settings, + pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { + return const CupertinoNavigationBar( + middle: const Text('Title'), + backgroundColor: const Color(0xFFE5E5E5), + ); + }, + ); + }, + ), + ); + expect(find.byType(BackdropFilter), findsNothing); + }); + + testWidgets('Non-opaque background adds blur effects', (WidgetTester tester) async { + await tester.pumpWidget( + new WidgetsApp( + color: const Color(0xFFFFFFFF), + onGenerateRoute: (RouteSettings settings) { + return new PageRouteBuilder( + settings: settings, + pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { + return const CupertinoNavigationBar( + middle: const Text('Title'), + ); + }, + ); + }, + ), + ); + expect(find.byType(BackdropFilter), findsOneWidget); + }); +} \ No newline at end of file diff --git a/packages/flutter/test/cupertino/page_test.dart b/packages/flutter/test/cupertino/page_test.dart new file mode 100644 index 0000000000000..56ea3da1b395e --- /dev/null +++ b/packages/flutter/test/cupertino/page_test.dart @@ -0,0 +1,144 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('test iOS page transition', (WidgetTester tester) async { + await tester.pumpWidget( + new WidgetsApp( + color: const Color(0xFFFFFFFF), + onGenerateRoute: (RouteSettings settings) { + return new CupertinoPageRoute( + settings: settings, + builder: (BuildContext context) { + final String pageNumber = settings.name == '/' ? "1" : "2"; + return new Center(child: new Text('Page $pageNumber')); + } + ); + }, + ), + ); + + final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1')); + + tester.state(find.byType(Navigator)).pushNamed('/next'); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 150)); + + Offset widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1')); + Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2')); + + // Page 1 is moving to the left. + expect(widget1TransientTopLeft.dx < widget1InitialTopLeft.dx, true); + // Page 1 isn't moving vertically. + expect(widget1TransientTopLeft.dy == widget1InitialTopLeft.dy, true); + // iOS transition is horizontal only. + expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true); + // Page 2 is coming in from the right. + expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, true); + + await tester.pumpAndSettle(); + + // Page 2 covers page 1. + expect(find.text('Page 1'), findsNothing); + expect(find.text('Page 2'), isOnstage); + + tester.state(find.byType(Navigator)).pop(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1')); + widget2TopLeft = tester.getTopLeft(find.text('Page 2')); + + // Page 1 is coming back from the left. + expect(widget1TransientTopLeft.dx < widget1InitialTopLeft.dx, true); + // Page 1 isn't moving vertically. + expect(widget1TransientTopLeft.dy == widget1InitialTopLeft.dy, true); + // iOS transition is horizontal only. + expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true); + // Page 2 is leaving towards the right. + expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, true); + + await tester.pumpAndSettle(); + + expect(find.text('Page 1'), isOnstage); + expect(find.text('Page 2'), findsNothing); + + widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1')); + + // Page 1 is back where it started. + expect(widget1InitialTopLeft == widget1TransientTopLeft, true); + }); + + testWidgets('test iOS fullscreen dialog transition', (WidgetTester tester) async { + await tester.pumpWidget( + new WidgetsApp( + color: const Color(0xFFFFFFFF), + onGenerateRoute: (RouteSettings settings) { + return new CupertinoPageRoute( + settings: settings, + builder: (BuildContext context) { + return const Center(child: const Text('Page 1')); + } + ); + }, + ), + ); + + final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1')); + + tester.state(find.byType(Navigator)).push(new CupertinoPageRoute( + builder: (BuildContext context) { + return const Center(child: const Text('Page 2')); + }, + fullscreenDialog: true, + )); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + Offset widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1')); + Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2')); + + // Page 1 doesn't move. + expect(widget1TransientTopLeft == widget1InitialTopLeft, true); + // Fullscreen dialogs transitions vertically only. + expect(widget1InitialTopLeft.dx == widget2TopLeft.dx, true); + // Page 2 is coming in from the bottom. + expect(widget2TopLeft.dy > widget1InitialTopLeft.dy, true); + + await tester.pumpAndSettle(); + + // Page 2 covers page 1. + expect(find.text('Page 1'), findsNothing); + expect(find.text('Page 2'), isOnstage); + + tester.state(find.byType(Navigator)).pop(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1')); + widget2TopLeft = tester.getTopLeft(find.text('Page 2')); + + // Page 1 doesn't move. + expect(widget1TransientTopLeft == widget1InitialTopLeft, true); + // Fullscreen dialogs transitions vertically only. + expect(widget1InitialTopLeft.dx == widget2TopLeft.dx, true); + // Page 2 is leaving towards the bottom. + expect(widget2TopLeft.dy > widget1InitialTopLeft.dy, true); + + await tester.pumpAndSettle(); + + expect(find.text('Page 1'), isOnstage); + expect(find.text('Page 2'), findsNothing); + + widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1')); + + // Page 1 is back where it started. + expect(widget1InitialTopLeft == widget1TransientTopLeft, true); + }); +} \ No newline at end of file diff --git a/packages/flutter/test/cupertino/scaffold_test.dart b/packages/flutter/test/cupertino/scaffold_test.dart new file mode 100644 index 0000000000000..34482ff4b3740 --- /dev/null +++ b/packages/flutter/test/cupertino/scaffold_test.dart @@ -0,0 +1,202 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../rendering/rendering_tester.dart'; +import '../services/mocks_for_image_cache.dart'; + +List selectedTabs; + +void main() { + setUp(() { + selectedTabs = []; + }); + + testWidgets('Contents are behind translucent bar', (WidgetTester tester) async { + await tester.pumpWidget( + new WidgetsApp( + color: const Color(0xFFFFFFFF), + onGenerateRoute: (RouteSettings settings) { + // TODO(xster): change to a CupertinoPageRoute. + return new PageRouteBuilder( + settings: settings, + pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { + return const CupertinoScaffold( + // Default nav bar is translucent. + navigationBar: const CupertinoNavigationBar( + middle: const Text('Title'), + ), + child: const Center(), + ); + }, + ); + }, + ), + ); + + expect(tester.getTopLeft(find.byType(Center)), const Offset(0.0, 0.0)); + }); + + testWidgets('Contents are between opaque bars', (WidgetTester tester) async { + final Center page1Center = const Center(); + + await tester.pumpWidget( + new WidgetsApp( + color: const Color(0xFFFFFFFF), + onGenerateRoute: (RouteSettings settings) { + // TODO(xster): change to a CupertinoPageRoute. + return new PageRouteBuilder( + settings: settings, + pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { + return new CupertinoScaffold.tabbed( + navigationBar: const CupertinoNavigationBar( + backgroundColor: CupertinoColors.white, + middle: const Text('Title'), + ), + tabBar: _buildTabBar(), + rootTabPageBuilder: (BuildContext context, int index) { + return index == 0 ? page1Center : new Stack(); + } + ); + }, + ); + }, + ), + ); + + expect(tester.getSize(find.byWidget(page1Center)).height, 600.0 - 44.0 - 50.0); + }); + + testWidgets('Tab switching', (WidgetTester tester) async { + final List tabsPainted = []; + + await tester.pumpWidget( + new WidgetsApp( + color: const Color(0xFFFFFFFF), + onGenerateRoute: (RouteSettings settings) { + // TODO(xster): change to a CupertinoPageRoute. + return new PageRouteBuilder( + settings: settings, + pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { + return new CupertinoScaffold.tabbed( + navigationBar: const CupertinoNavigationBar( + backgroundColor: CupertinoColors.white, + middle: const Text('Title'), + ), + tabBar: _buildTabBar(), + rootTabPageBuilder: (BuildContext context, int index) { + return new CustomPaint( + child: new Text('Page ${index + 1}'), + painter: new TestCallbackPainter( + onPaint: () { tabsPainted.add(index); } + ) + ); + } + ); + }, + ); + }, + ), + ); + + expect(tabsPainted, [0]); + RichText tab1 = tester.widget(find.descendant( + of: find.text('Tab 1'), + matching: find.byType(RichText), + )); + expect(tab1.text.style.color, CupertinoColors.activeBlue); + RichText tab2 = tester.widget(find.descendant( + of: find.text('Tab 2'), + matching: find.byType(RichText), + )); + expect(tab2.text.style.color, CupertinoColors.inactiveGray); + + await tester.tap(find.text('Tab 2')); + await tester.pump(); + + expect(tabsPainted, [0, 1]); + tab1 = tester.widget(find.descendant( + of: find.text('Tab 1'), + matching: find.byType(RichText), + )); + expect(tab1.text.style.color, CupertinoColors.inactiveGray); + tab2 = tester.widget(find.descendant( + of: find.text('Tab 2'), + matching: find.byType(RichText), + )); + expect(tab2.text.style.color, CupertinoColors.activeBlue); + + await tester.tap(find.text('Tab 1')); + await tester.pump(); + + expect(tabsPainted, [0, 1, 0]); + }); + + testWidgets('Tabs are lazy built and moved offstage when inactive', (WidgetTester tester) async { + final List tabsBuilt = []; + + await tester.pumpWidget( + new WidgetsApp( + color: const Color(0xFFFFFFFF), + onGenerateRoute: (RouteSettings settings) { + // TODO(xster): change to a CupertinoPageRoute. + return new PageRouteBuilder( + settings: settings, + pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { + return new CupertinoScaffold.tabbed( + navigationBar: const CupertinoNavigationBar( + backgroundColor: CupertinoColors.white, + middle: const Text('Title'), + ), + tabBar: _buildTabBar(), + rootTabPageBuilder: (BuildContext context, int index) { + tabsBuilt.add(index); + return new Text('Page ${index + 1}'); + } + ); + }, + ); + }, + ), + ); + + expect(tabsBuilt, [0]); + expect(find.text('Page 1'), findsOneWidget); + expect(find.text('Page 2'), findsNothing); + + await tester.tap(find.text('Tab 2')); + await tester.pump(); + + // Both tabs are built but only one is onstage. + expect(tabsBuilt, [0, 0, 1]); + expect(find.text('Page 1', skipOffstage: false), isOffstage); + expect(find.text('Page 2'), findsOneWidget); + + await tester.tap(find.text('Tab 1')); + await tester.pump(); + + expect(tabsBuilt, [0, 0, 1, 0, 1]); + expect(find.text('Page 1'), findsOneWidget); + expect(find.text('Page 2', skipOffstage: false), isOffstage); + }); +} + +CupertinoTabBar _buildTabBar() { + return new CupertinoTabBar( + items: [ + const BottomNavigationBarItem( + icon: const ImageIcon(const TestImageProvider(24, 24)), + title: const Text('Tab 1'), + ), + const BottomNavigationBarItem( + icon: const ImageIcon(const TestImageProvider(24, 24)), + title: const Text('Tab 2'), + ), + ], + backgroundColor: CupertinoColors.white, + onTap: (int newTab) => selectedTabs.add(newTab), + ); +} diff --git a/packages/flutter/test/cupertino/switch_test.dart b/packages/flutter/test/cupertino/switch_test.dart index 61e6a720e59bf..0e16ab485955a 100644 --- a/packages/flutter/test/cupertino/switch_test.dart +++ b/packages/flutter/test/cupertino/switch_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:flutter/cupertino.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { diff --git a/packages/flutter/test/material/app_bar_test.dart b/packages/flutter/test/material/app_bar_test.dart index 293ffbfc559ca..36ac6f744e846 100644 --- a/packages/flutter/test/material/app_bar_test.dart +++ b/packages/flutter/test/material/app_bar_test.dart @@ -216,8 +216,12 @@ void main() { final Finder title = find.byKey(titleKey); expect(tester.getTopLeft(title).dx, 72.0); - // The toolbar's contents are padded on the right by 4.0 - expect(tester.getSize(title).width, equals(800.0 - 72.0 - 4.0)); + expect(tester.getSize(title).width, equals( + 800.0 // Screen width. + - 4.0 // Left margin before the leading button. + - 56.0 // Leading button width. + - 16.0 // Leading button to title padding. + - 16.0)); // Title right side padding. actions = [ const SizedBox(width: 100.0), @@ -227,13 +231,19 @@ void main() { expect(tester.getTopLeft(title).dx, 72.0); // The title shrinks by 200.0 to allow for the actions widgets. - expect(tester.getSize(title).width, equals(800.0 - 72.0 - 4.0 - 200.0)); + expect(tester.getSize(title).width, equals( + 800.0 // Screen width. + - 4.0 // Left margin before the leading button. + - 56.0 // Leading button width. + - 16.0 // Leading button to title padding. + - 16.0 // Title to actions padding + - 200.0)); // Actions' width. leading = new Container(); // AppBar will constrain the width to 24.0 await tester.pumpWidget(buildApp()); expect(tester.getTopLeft(title).dx, 72.0); // Adding a leading widget shouldn't effect the title's size - expect(tester.getSize(title).width, equals(800.0 - 72.0 - 4.0 - 200.0)); + expect(tester.getSize(title).width, equals(800.0 - 4.0 - 56.0 - 16.0 - 16.0 - 200.0)); }); testWidgets('AppBar centerTitle:true title overflow OK ', (WidgetTester tester) async { diff --git a/packages/flutter/test/material/app_test.dart b/packages/flutter/test/material/app_test.dart index e9b2662214a1f..92249a5ae1244 100644 --- a/packages/flutter/test/material/app_test.dart +++ b/packages/flutter/test/material/app_test.dart @@ -143,4 +143,76 @@ void main() { expect(find.text('Home'), findsOneWidget); }); + + testWidgets('Default initialRoute', (WidgetTester tester) async { + await tester.pumpWidget(new MaterialApp(routes: { + '/': (BuildContext context) => const Text('route "/"'), + })); + + expect(find.text('route "/"'), findsOneWidget); + }); + + testWidgets('Custom initialRoute only', (WidgetTester tester) async { + await tester.pumpWidget( + new MaterialApp( + initialRoute: '/a', + routes: { + '/a': (BuildContext context) => const Text('route "/a"'), + }, + ) + ); + + expect(find.text('route "/a"'), findsOneWidget); + }); + + testWidgets('Custom initialRoute along with Navigator.defaultRouteName', (WidgetTester tester) async { + final Map routes = { + '/': (BuildContext context) => const Text('route "/"'), + '/a': (BuildContext context) => const Text('route "/a"'), + '/b': (BuildContext context) => const Text('route "/b"'), + }; + + await tester.pumpWidget( + new MaterialApp( + initialRoute: '/a', + routes: routes, + ) + ); + expect(find.text('route "/"'), findsNothing); + expect(find.text('route "/a"'), findsOneWidget); + expect(find.text('route "/b"'), findsNothing); + }); + + testWidgets('Make sure initialRoute is only used the first time', (WidgetTester tester) async { + final Map routes = { + '/': (BuildContext context) => const Text('route "/"'), + '/a': (BuildContext context) => const Text('route "/a"'), + '/b': (BuildContext context) => const Text('route "/b"'), + }; + + await tester.pumpWidget( + new MaterialApp( + initialRoute: '/a', + routes: routes, + ) + ); + expect(find.text('route "/"'), findsNothing); + expect(find.text('route "/a"'), findsOneWidget); + expect(find.text('route "/b"'), findsNothing); + + await tester.pumpWidget( + new MaterialApp( + initialRoute: '/b', + routes: routes, + ) + ); + expect(find.text('route "/"'), findsNothing); + expect(find.text('route "/a"'), findsOneWidget); + expect(find.text('route "/b"'), findsNothing); + + await tester.pumpWidget(new MaterialApp(routes: routes)); + expect(find.text('route "/"'), findsNothing); + expect(find.text('route "/a"'), findsOneWidget); + expect(find.text('route "/b"'), findsNothing); + }); } diff --git a/packages/flutter/test/material/dialog_test.dart b/packages/flutter/test/material/dialog_test.dart index 83a8f28b3961f..d2e4f8dd74a72 100644 --- a/packages/flutter/test/material/dialog_test.dart +++ b/packages/flutter/test/material/dialog_test.dart @@ -4,6 +4,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:matcher/matcher.dart'; + +import '../widgets/semantics_tester.dart'; void main() { testWidgets('Dialog is scrollable', (WidgetTester tester) async { @@ -192,4 +195,38 @@ void main() { expect(find.text('Dialog2'), findsOneWidget); }); + + testWidgets('Dialog hides underlying semantics tree', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + const String buttonText = 'A button covered by dialog overlay'; + await tester.pumpWidget( + new MaterialApp( + home: const Material( + child: const Center( + child: const RaisedButton( + onPressed: null, + child: const Text(buttonText), + ), + ), + ), + ), + ); + + expect(semantics, includesNodeWithLabel(buttonText)); + + final BuildContext context = tester.element(find.text(buttonText)); + + const String alertText = 'A button in an overlay alert'; + showDialog( + context: context, + child: const AlertDialog(title: const Text(alertText)), + ); + + await tester.pumpAndSettle(const Duration(seconds: 1)); + + expect(semantics, includesNodeWithLabel(alertText)); + expect(semantics, isNot(includesNodeWithLabel(buttonText))); + + semantics.dispose(); + }); } diff --git a/packages/flutter/test/material/divider_test.dart b/packages/flutter/test/material/divider_test.dart index 72e52bacf7364..096a06571c888 100644 --- a/packages/flutter/test/material/divider_test.dart +++ b/packages/flutter/test/material/divider_test.dart @@ -4,11 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../rendering/mock_canvas.dart'; void main() { testWidgets('Divider control test', (WidgetTester tester) async { await tester.pumpWidget(const Center(child: const Divider())); final RenderBox box = tester.firstRenderObject(find.byType(Divider)); - expect(box.size.height, 15.0); + expect(box.size.height, 16.0); + expect(find.byType(Divider), paints..path(strokeWidth: 0.0)); }); } diff --git a/packages/flutter/test/material/drawer_test.dart b/packages/flutter/test/material/drawer_test.dart index eebac15071b68..50d483ae996e4 100644 --- a/packages/flutter/test/material/drawer_test.dart +++ b/packages/flutter/test/material/drawer_test.dart @@ -48,7 +48,7 @@ void main() { box = tester.renderObject(find.byKey(containerKey)); expect(box.size.width, equals(drawerWidth - 2 * 16.0)); - expect(box.size.height, equals(drawerHeight - 2 * 16.0 - 1.0)); // bottom edge + expect(box.size.height, equals(drawerHeight - 2 * 16.0)); expect(find.text('header'), findsOneWidget); }); diff --git a/packages/flutter/test/material/page_selector_test.dart b/packages/flutter/test/material/page_selector_test.dart index 814b7a89a284e..ca1a65e7c44cc 100644 --- a/packages/flutter/test/material/page_selector_test.dart +++ b/packages/flutter/test/material/page_selector_test.dart @@ -5,12 +5,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; -const Color selectedColor = const Color(0xFF00FF00); -const Color unselectedColor = Colors.transparent; +const Color kSelectedColor = const Color(0xFF00FF00); +const Color kUnselectedColor = Colors.transparent; -Widget buildFrame(TabController tabController) { +Widget buildFrame(TabController tabController, { Color color, Color selectedColor, double indicatorSize: 12.0 }) { return new Theme( - data: new ThemeData(accentColor: selectedColor), + data: new ThemeData(accentColor: kSelectedColor), child: new SizedBox.expand( child: new Center( child: new SizedBox( @@ -18,7 +18,12 @@ Widget buildFrame(TabController tabController) { height: 400.0, child: new Column( children: [ - new TabPageSelector(controller: tabController), + new TabPageSelector( + controller: tabController, + color: color, + selectedColor: selectedColor, + indicatorSize: indicatorSize, + ), new Flexible( child: new TabBarView( controller: tabController, @@ -56,17 +61,17 @@ void main() { await tester.pumpWidget(buildFrame(tabController)); expect(tabController.index, 0); - expect(indicatorColors(tester), const [selectedColor, unselectedColor, unselectedColor]); + expect(indicatorColors(tester), const [kSelectedColor, kUnselectedColor, kUnselectedColor]); tabController.index = 1; await tester.pump(); expect(tabController.index, 1); - expect(indicatorColors(tester), const [unselectedColor, selectedColor, unselectedColor]); + expect(indicatorColors(tester), const [kUnselectedColor, kSelectedColor, kUnselectedColor]); tabController.index = 2; await tester.pump(); expect(tabController.index, 2); - expect(indicatorColors(tester), const [unselectedColor, unselectedColor, selectedColor]); + expect(indicatorColors(tester), const [kUnselectedColor, kUnselectedColor, kSelectedColor]); }); testWidgets('PageSelector responds correctly to TabController.animateTo()', (WidgetTester tester) async { @@ -77,7 +82,7 @@ void main() { await tester.pumpWidget(buildFrame(tabController)); expect(tabController.index, 0); - expect(indicatorColors(tester), const [selectedColor, unselectedColor, unselectedColor]); + expect(indicatorColors(tester), const [kSelectedColor, kUnselectedColor, kUnselectedColor]); tabController.animateTo(1, duration: const Duration(milliseconds: 200)); await tester.pump(); @@ -87,14 +92,14 @@ void main() { await tester.pump(const Duration(milliseconds: 10)); List colors = indicatorColors(tester); expect(colors[0].alpha, greaterThan(colors[1].alpha)); - expect(colors[2], unselectedColor); + expect(colors[2], kUnselectedColor); await tester.pump(const Duration(milliseconds: 175)); colors = indicatorColors(tester); expect(colors[0].alpha, lessThan(colors[1].alpha)); - expect(colors[2], unselectedColor); + expect(colors[2], kUnselectedColor); await tester.pumpAndSettle(); expect(tabController.index, 1); - expect(indicatorColors(tester), const [unselectedColor, selectedColor, unselectedColor]); + expect(indicatorColors(tester), const [kUnselectedColor, kSelectedColor, kUnselectedColor]); tabController.animateTo(2, duration: const Duration(milliseconds: 200)); await tester.pump(); @@ -102,14 +107,14 @@ void main() { await tester.pump(const Duration(milliseconds: 10)); colors = indicatorColors(tester); expect(colors[1].alpha, greaterThan(colors[2].alpha)); - expect(colors[0], unselectedColor); + expect(colors[0], kUnselectedColor); await tester.pump(const Duration(milliseconds: 175)); colors = indicatorColors(tester); expect(colors[1].alpha, lessThan(colors[2].alpha)); - expect(colors[0], unselectedColor); + expect(colors[0], kUnselectedColor); await tester.pumpAndSettle(); expect(tabController.index, 2); - expect(indicatorColors(tester), const [unselectedColor, unselectedColor, selectedColor]); + expect(indicatorColors(tester), const [kUnselectedColor, kUnselectedColor, kSelectedColor]); }); testWidgets('PageSelector responds correctly to TabBarView drags', (WidgetTester tester) async { @@ -121,7 +126,7 @@ void main() { await tester.pumpWidget(buildFrame(tabController)); expect(tabController.index, 1); - expect(indicatorColors(tester), const [unselectedColor, selectedColor, unselectedColor]); + expect(indicatorColors(tester), const [kUnselectedColor, kSelectedColor, kUnselectedColor]); final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0)); @@ -131,13 +136,13 @@ void main() { await tester.pumpAndSettle(); List colors = indicatorColors(tester); expect(colors[1].alpha, greaterThan(colors[2].alpha)); - expect(colors[0], unselectedColor); + expect(colors[0], kUnselectedColor); // Drag back to where we started. await gesture.moveBy(const Offset(100.0, 0.0)); await tester.pumpAndSettle(); colors = indicatorColors(tester); - expect(indicatorColors(tester), const [unselectedColor, selectedColor, unselectedColor]); + expect(indicatorColors(tester), const [kUnselectedColor, kSelectedColor, kUnselectedColor]); // Drag to the left moving the selection towards indicator 0. Indicator 0's // opacity should increase and Indicator 1's opacity should decrease. @@ -145,30 +150,69 @@ void main() { await tester.pumpAndSettle(); colors = indicatorColors(tester); expect(colors[1].alpha, greaterThan(colors[0].alpha)); - expect(colors[2], unselectedColor); + expect(colors[2], kUnselectedColor); // Drag back to where we started. await gesture.moveBy(const Offset(-100.0, 0.0)); await tester.pumpAndSettle(); colors = indicatorColors(tester); - expect(indicatorColors(tester), const [unselectedColor, selectedColor, unselectedColor]); + expect(indicatorColors(tester), const [kUnselectedColor, kSelectedColor, kUnselectedColor]); // Completing the gesture doesn't change anything await gesture.up(); await tester.pumpAndSettle(); colors = indicatorColors(tester); - expect(indicatorColors(tester), const [unselectedColor, selectedColor, unselectedColor]); + expect(indicatorColors(tester), const [kUnselectedColor, kSelectedColor, kUnselectedColor]); // Fling to the left, selects indicator 2 await tester.fling(find.byType(TabBarView), const Offset(-100.0, 0.0), 1000.0); await tester.pumpAndSettle(); - expect(indicatorColors(tester), const [unselectedColor, unselectedColor, selectedColor]); + expect(indicatorColors(tester), const [kUnselectedColor, kUnselectedColor, kSelectedColor]); // Fling to the right, selects indicator 1 await tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 1000.0); await tester.pumpAndSettle(); - expect(indicatorColors(tester), const [unselectedColor, selectedColor, unselectedColor]); + expect(indicatorColors(tester), const [kUnselectedColor, kSelectedColor, kUnselectedColor]); + + }); + + testWidgets('PageSelector indicatorColors', (WidgetTester tester) async { + const Color kRed = const Color(0xFFFF0000); + const Color kBlue = const Color(0xFF0000FF); + + final TabController tabController = new TabController( + vsync: const TestVSync(), + initialIndex: 1, + length: 3, + ); + await tester.pumpWidget(buildFrame(tabController, color: kRed, selectedColor: kBlue)); + + expect(tabController.index, 1); + expect(indicatorColors(tester), const [kRed, kBlue, kRed]); + + tabController.index = 0; + await tester.pumpAndSettle(); + expect(indicatorColors(tester), const [kBlue, kRed, kRed]); + }); + + testWidgets('PageSelector indicatorSize', (WidgetTester tester) async { + final TabController tabController = new TabController( + vsync: const TestVSync(), + initialIndex: 1, + length: 3, + ); + await tester.pumpWidget(buildFrame(tabController, indicatorSize: 16.0)); + + final Iterable indicatorElements = find.descendant( + of: find.byType(TabPageSelector), + matching: find.byType(TabPageSelectorIndicator), + ).evaluate(); + + // Indicators get an 8 pixel margin, 16 + 8 = 24. + for (Element indicatorElement in indicatorElements) + expect(indicatorElement.size, const Size(24.0, 24.0)); + expect(tester.getSize(find.byType(TabPageSelector)).height, 24.0); }); } diff --git a/packages/flutter/test/material/page_test.dart b/packages/flutter/test/material/page_test.dart index 9dbb93ac8704c..239f0c5ced4bb 100644 --- a/packages/flutter/test/material/page_test.dart +++ b/packages/flutter/test/material/page_test.dart @@ -38,7 +38,7 @@ void main() { // Animation begins from the top of the page. expect(widget2TopLeft.dy < widget2Size.height, true); - await tester.pumpAndSettle(); + await tester.pump(const Duration(milliseconds: 300)); // Page 2 covers page 1. expect(find.text('Page 1'), findsNothing); @@ -53,7 +53,7 @@ void main() { // Page 2 starts to move down. expect(widget1TopLeft.dy < widget2TopLeft.dy, true); - await tester.pumpAndSettle(); + await tester.pump(const Duration(milliseconds: 300)); expect(find.text('Page 1'), isOnstage); expect(find.text('Page 2'), findsNothing); @@ -96,10 +96,10 @@ void main() { expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true); // Page 2 is coming in from the right. expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, true); - // The shadow should be drawn to one screen width to the left of where + // The shadow should be drawn to one screen width to the left of where // the page 2 box is. `paints` tests relative to the painter's given canvas // rather than relative to the screen so assert that it's one screen - // width to the left of 0 offset box rect and nothing is drawn inside the + // width to the left of 0 offset box rect and nothing is drawn inside the // box's rect. expect(box, paints..rect( rect: new Rect.fromLTWH(-800.0, 0.0, 800.0, 600.0) @@ -302,4 +302,79 @@ void main() { // Page 2 didn't move expect(tester.getTopLeft(find.text('Page 2')), Offset.zero); }); + + testWidgets('test adaptable transitions switch during execution', (WidgetTester tester) async { + await tester.pumpWidget( + new MaterialApp( + theme: new ThemeData(platform: TargetPlatform.android), + home: const Material(child: const Text('Page 1')), + routes: { + '/next': (BuildContext context) { + return const Material(child: const Text('Page 2')); + }, + }, + ) + ); + + final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1')); + + tester.state(find.byType(Navigator)).pushNamed('/next'); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2')); + final Size widget2Size = tester.getSize(find.text('Page 2')); + + // Android transition is vertical only. + expect(widget1InitialTopLeft.dx == widget2TopLeft.dx, true); + // Page 1 is above page 2 mid-transition. + expect(widget1InitialTopLeft.dy < widget2TopLeft.dy, true); + // Animation begins from the top of the page. + expect(widget2TopLeft.dy < widget2Size.height, true); + + await tester.pump(const Duration(milliseconds: 300)); + + // Page 2 covers page 1. + expect(find.text('Page 1'), findsNothing); + expect(find.text('Page 2'), isOnstage); + + // Re-pump the same app but with iOS instead of Android. + await tester.pumpWidget( + new MaterialApp( + theme: new ThemeData(platform: TargetPlatform.iOS), + home: const Material(child: const Text('Page 1')), + routes: { + '/next': (BuildContext context) { + return const Material(child: const Text('Page 2')); + }, + }, + ) + ); + + tester.state(find.byType(Navigator)).pop(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + Offset widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1')); + widget2TopLeft = tester.getTopLeft(find.text('Page 2')); + + // Page 1 is coming back from the left. + expect(widget1TransientTopLeft.dx < widget1InitialTopLeft.dx, true); + // Page 1 isn't moving vertically. + expect(widget1TransientTopLeft.dy == widget1InitialTopLeft.dy, true); + // iOS transition is horizontal only. + expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true); + // Page 2 is leaving towards the right. + expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, true); + + await tester.pump(const Duration(milliseconds: 300)); + + expect(find.text('Page 1'), isOnstage); + expect(find.text('Page 2'), findsNothing); + + widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1')); + + // Page 1 is back where it started. + expect(widget1InitialTopLeft == widget1TransientTopLeft, true); + }); } diff --git a/packages/flutter/test/material/scaffold_test.dart b/packages/flutter/test/material/scaffold_test.dart index 219abe6c43ec9..395589f030ca4 100644 --- a/packages/flutter/test/material/scaffold_test.dart +++ b/packages/flutter/test/material/scaffold_test.dart @@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import '../widgets/semantics_tester.dart'; + void main() { testWidgets('Scaffold control test', (WidgetTester tester) async { final Key bodyKey = new UniqueKey(); @@ -440,4 +442,40 @@ void main() { expect(tester.renderObject(find.byKey(testKey)).localToGlobal(Offset.zero), const Offset(0.0, 0.0)); }); }); + + testWidgets('Open drawer hides underlying semantics tree', (WidgetTester tester) async { + const String bodyLabel = 'I am the body'; + const String persistentFooterButtonLabel = 'a button on the bottom'; + const String bottomNavigationBarLabel = 'a bar in an app'; + const String floatingActionButtonLabel = 'I float in space'; + const String drawerLabel = 'I am the reason for this test'; + + final SemanticsTester semantics = new SemanticsTester(tester); + await tester.pumpWidget(new MaterialApp(home: new Scaffold( + body: new Semantics(label: bodyLabel, child: new Container()), + persistentFooterButtons: [new Semantics(label: persistentFooterButtonLabel, child: new Container())], + bottomNavigationBar: new Semantics(label: bottomNavigationBarLabel, child: new Container()), + floatingActionButton: new Semantics(label: floatingActionButtonLabel, child: new Container()), + drawer: new Drawer(child:new Semantics(label: drawerLabel, child: new Container())), + ))); + + expect(semantics, includesNodeWithLabel(bodyLabel)); + expect(semantics, includesNodeWithLabel(persistentFooterButtonLabel)); + expect(semantics, includesNodeWithLabel(bottomNavigationBarLabel)); + expect(semantics, includesNodeWithLabel(floatingActionButtonLabel)); + expect(semantics, isNot(includesNodeWithLabel(drawerLabel))); + + final ScaffoldState state = tester.firstState(find.byType(Scaffold)); + state.openDrawer(); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(semantics, isNot(includesNodeWithLabel(bodyLabel))); + expect(semantics, isNot(includesNodeWithLabel(persistentFooterButtonLabel))); + expect(semantics, isNot(includesNodeWithLabel(bottomNavigationBarLabel))); + expect(semantics, isNot(includesNodeWithLabel(floatingActionButtonLabel))); + expect(semantics, includesNodeWithLabel(drawerLabel)); + + semantics.dispose(); + }); } diff --git a/packages/flutter/test/material/tabbed_scrollview_warp_test.dart b/packages/flutter/test/material/tabbed_scrollview_warp_test.dart new file mode 100644 index 0000000000000..49824231cfa1d --- /dev/null +++ b/packages/flutter/test/material/tabbed_scrollview_warp_test.dart @@ -0,0 +1,82 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/material.dart'; + +// This is a regression test for https://github.com/flutter/flutter/issues/10549 +// which was failing because _SliverPersistentHeaderElement.visitChildren() +// didn't check child != null before visiting its child. + +class MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate { + @override + double get minExtent => 50.0; + + @override + double get maxExtent => 150.0; + + @override + Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => const Placeholder(color: Colors.teal); + + @override + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => false; +} + +class MyHomePage extends StatefulWidget { + @override + _MyHomePageState createState() => new _MyHomePageState(); +} + +class _MyHomePageState extends State with TickerProviderStateMixin { + static const int tabCount = 3; + TabController tabController; + + @override + void initState() { + super.initState(); + tabController = new TabController(initialIndex: 0, length: tabCount, vsync: this); + } + + @override + void dispose() { + tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return new Scaffold( + appBar: new AppBar( + bottom: new TabBar( + controller: tabController, + tabs: new List.generate(tabCount, (int index) => new Tab(text: 'Tab $index')).toList(), + ), + ), + body: new TabBarView( + controller: tabController, + children: new List.generate(tabCount, (int index) { + return new CustomScrollView( + // The bug only occurs when this key is included + key: new ValueKey('Page $index'), + slivers: [ + new SliverPersistentHeader( + delegate: new MySliverPersistentHeaderDelegate(), + ), + ], + ); + }).toList(), + ), + ); + } +} + +void main() { + testWidgets('Tabbed CustomScrollViews, warp from tab 1 to 3', (WidgetTester tester) async { + await tester.pumpWidget(new MaterialApp(home: new MyHomePage())); + + // should not crash. + await tester.tap(find.text('Tab 2')); + await tester.pumpAndSettle(); + }); +} diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index 9961768f2cc66..7e9c0221a3ef9 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -2,11 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' show SemanticsFlags, SemanticsAction; + import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import '../rendering/mock_canvas.dart'; import '../rendering/recording_canvas.dart'; +import '../widgets/semantics_tester.dart'; class StateMarker extends StatefulWidget { const StateMarker({ Key key, this.child }) : super(key: key); @@ -835,4 +839,219 @@ void main() { expect(find.text('TAB #19'), findsOneWidget); expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB #19')).dx, 800.0); }); + + testWidgets('TabBar with indicatorWeight, indicatorPadding', (WidgetTester tester) async { + const Color color = const Color(0xFF00FF00); + const double height = 100.0; + const double weight = 8.0; + const double padLeft = 8.0; + const double padRight = 4.0; + + final List tabs = new List.generate(4, (int index) { + return new Container( + key: new ValueKey(index), + height: height, + ); + }); + + final TabController controller = new TabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + await tester.pumpWidget( + new Material( + child: new Column( + children: [ + new TabBar( + indicatorWeight: 8.0, + indicatorColor: color, + indicatorPadding: const EdgeInsets.only(left: padLeft, right: padRight), + controller: controller, + tabs: tabs, + ), + new Flexible(child: new Container()), + ], + ), + ), + ); + + final RenderBox tabBarBox = tester.firstRenderObject(find.byType(TabBar)); + + // Selected tab dimensions + double tabWidth = tester.getSize(find.byKey(const ValueKey(0))).width; + double tabLeft = tester.getTopLeft(find.byKey(const ValueKey(0))).dx; + double tabRight = tabLeft + tabWidth; + + expect(tabBarBox, paints..rect( + style: PaintingStyle.fill, + color: color, + rect: new Rect.fromLTRB(tabLeft + padLeft, height, tabRight - padRight, height + weight) + )); + + // Select tab 3 + controller.index = 3; + await tester.pumpAndSettle(); + + tabWidth = tester.getSize(find.byKey(const ValueKey(3))).width; + tabLeft = tester.getTopLeft(find.byKey(const ValueKey(3))).dx; + tabRight = tabLeft + tabWidth; + + expect(tabBarBox, paints..rect( + style: PaintingStyle.fill, + color: color, + rect: new Rect.fromLTRB(tabLeft + padLeft, height, tabRight - padRight, height + weight) + )); + }); + + testWidgets('correct semantics', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + final List tabs = new List.generate(2, (int index) { + return new Tab(text: 'TAB #$index'); + }); + + final TabController controller = new TabController( + vsync: const TestVSync(), + length: tabs.length, + initialIndex: 0, + ); + + await tester.pumpWidget( + new Material( + child: new Semantics( + container: true, + child: new TabBar( + isScrollable: true, + controller: controller, + tabs: tabs, + ), + ), + ), + ); + + final TestSemantics expectedSemantics = new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + rect: TestSemantics.fullScreen, + children: [ + new TestSemantics( + id: 2, + actions: SemanticsAction.tap.index, + flags: SemanticsFlags.isSelected.index, + label: 'TAB #0\nTab 1 of 2', + rect: new Rect.fromLTRB(0.0, 0.0, 108.0, 46.0), + transform: new Matrix4.translationValues(0.0, 276.0, 0.0), + ), + new TestSemantics( + id: 5, + actions: SemanticsAction.tap.index, + label: 'TAB #1\nTab 2 of 2', + rect: new Rect.fromLTRB(0.0, 0.0, 108.0, 46.0), + transform: new Matrix4.translationValues(108.0, 276.0, 0.0), + ), + ]), + ], + ); + + expect(semantics, hasSemantics(expectedSemantics)); + + semantics.dispose(); + }); + + testWidgets('TabBar etc with zero tabs', (WidgetTester tester) async { + final TabController controller = new TabController( + vsync: const TestVSync(), + length: 0, + ); + + await tester.pumpWidget( + new Material( + child: new Column( + children: [ + new TabBar( + controller: controller, + tabs: const [], + ), + new Flexible( + child: new TabBarView( + controller: controller, + children: const [], + ), + ), + ], + ), + ), + ); + + expect(controller.index, 0); + expect(tester.getSize(find.byType(TabBar)), const Size(800.0, 48.0)); + expect(tester.getSize(find.byType(TabBarView)), const Size(800.0, 600.0 - 48.0)); + + // A fling in the TabBar or TabBarView, shouldn't do anything. + + await(tester.fling(find.byType(TabBar), const Offset(-100.0, 0.0), 5000.0)); + await(tester.pumpAndSettle()); + + await(tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 5000.0)); + await(tester.pumpAndSettle()); + + expect(controller.index, 0); + }); + + testWidgets('TabBar etc with one tab', (WidgetTester tester) async { + final TabController controller = new TabController( + vsync: const TestVSync(), + length: 1, + ); + + await tester.pumpWidget( + new Material( + child: new Column( + children: [ + new TabBar( + controller: controller, + tabs: const [const Tab(text: 'TAB')], + ), + new Flexible( + child: new TabBarView( + controller: controller, + children: const [const Text('PAGE')], + ), + ), + ], + ), + ), + ); + + expect(controller.index, 0); + expect(find.text('TAB'), findsOneWidget); + expect(find.text('PAGE'), findsOneWidget); + expect(tester.getSize(find.byType(TabBar)), const Size(800.0, 48.0)); + expect(tester.getSize(find.byType(TabBarView)), const Size(800.0, 600.0 - 48.0)); + + // The one tab spans the app's width + expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, 0); + expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, 800); + + // A fling in the TabBar or TabBarView, shouldn't move the tab. + + await(tester.fling(find.byType(TabBar), const Offset(-100.0, 0.0), 5000.0)); + await(tester.pump(const Duration(milliseconds: 50))); + expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, 0); + expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, 800); + await(tester.pumpAndSettle()); + + await(tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 5000.0)); + await(tester.pump(const Duration(milliseconds: 50))); + expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, 0); + expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, 800); + await(tester.pumpAndSettle()); + + expect(controller.index, 0); + expect(find.text('TAB'), findsOneWidget); + expect(find.text('PAGE'), findsOneWidget); + }); + } diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 0b17aa298ad1e..7591c5cd70d4e 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -46,7 +46,7 @@ void main() { 'First line of text is ' 'Second line goes until ' 'Third line of stuff '; - const String kFourLines = + const String kMoreThanFourLines = kThreeLines + 'Fourth line won\'t display and ends at'; @@ -462,7 +462,7 @@ void main() { ); } - await tester.pumpWidget(builder(3)); + await tester.pumpWidget(builder(null)); RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey)); @@ -470,28 +470,44 @@ void main() { final Size emptyInputSize = inputBox.size; await tester.enterText(find.byType(TextField), 'No wrapping here.'); - await tester.pumpWidget(builder(3)); + await tester.pumpWidget(builder(null)); expect(findInputBox(), equals(inputBox)); expect(inputBox.size, equals(emptyInputSize)); - await tester.enterText(find.byType(TextField), kThreeLines); await tester.pumpWidget(builder(3)); expect(findInputBox(), equals(inputBox)); expect(inputBox.size, greaterThan(emptyInputSize)); final Size threeLineInputSize = inputBox.size; + await tester.enterText(find.byType(TextField), kThreeLines); + await tester.pumpWidget(builder(null)); + expect(findInputBox(), equals(inputBox)); + expect(inputBox.size, greaterThan(emptyInputSize)); + + await tester.enterText(find.byType(TextField), kThreeLines); + await tester.pumpWidget(builder(null)); + expect(findInputBox(), equals(inputBox)); + expect(inputBox.size, threeLineInputSize); + // An extra line won't increase the size because we max at 3. - await tester.enterText(find.byType(TextField), kFourLines); + await tester.enterText(find.byType(TextField), kMoreThanFourLines); await tester.pumpWidget(builder(3)); expect(findInputBox(), equals(inputBox)); expect(inputBox.size, threeLineInputSize); - // But now it will. - await tester.enterText(find.byType(TextField), kFourLines); + // But now it will... but it will max at four + await tester.enterText(find.byType(TextField), kMoreThanFourLines); await tester.pumpWidget(builder(4)); expect(findInputBox(), equals(inputBox)); expect(inputBox.size, greaterThan(threeLineInputSize)); + + final Size fourLineInputSize = inputBox.size; + + // Now it won't max out until the end + await tester.pumpWidget(builder(null)); + expect(findInputBox(), equals(inputBox)); + expect(inputBox.size, greaterThan(fourLineInputSize)); }); testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async { @@ -594,7 +610,7 @@ void main() { await tester.pumpWidget(builder()); await tester.pump(const Duration(seconds: 1)); - await tester.enterText(find.byType(TextField), kFourLines); + await tester.enterText(find.byType(TextField), kMoreThanFourLines); await tester.pumpWidget(builder()); await tester.pump(const Duration(seconds: 1)); @@ -603,8 +619,8 @@ void main() { final RenderBox inputBox = findInputBox(); // Check that the last line of text is not displayed. - final Offset firstPos = textOffsetToPosition(tester, kFourLines.indexOf('First')); - final Offset fourthPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth')); + final Offset firstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First')); + final Offset fourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth')); expect(firstPos.dx, fourthPos.dx); expect(firstPos.dy, lessThan(fourthPos.dy)); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(firstPos)), isTrue); @@ -622,8 +638,8 @@ void main() { await tester.pump(); // Now the first line is scrolled up, and the fourth line is visible. - Offset newFirstPos = textOffsetToPosition(tester, kFourLines.indexOf('First')); - Offset newFourthPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth')); + Offset newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First')); + Offset newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth')); expect(newFirstPos.dy, lessThan(firstPos.dy)); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isFalse); @@ -633,7 +649,7 @@ void main() { // Long press the 'i' in 'Fourth line' to select the word. await tester.pump(const Duration(seconds: 1)); - final Offset untilPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth line')+8); + final Offset untilPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth line')+8); gesture = await tester.startGesture(untilPos, pointer: 7); await tester.pump(const Duration(seconds: 1)); await gesture.up(); @@ -645,7 +661,7 @@ void main() { // Drag the left handle to the first line, just after 'First'. final Offset handlePos = endpoints[0].point + const Offset(-1.0, 1.0); - final Offset newHandlePos = textOffsetToPosition(tester, kFourLines.indexOf('First') + 5); + final Offset newHandlePos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First') + 5); gesture = await tester.startGesture(handlePos, pointer: 7); await tester.pump(const Duration(seconds: 1)); await gesture.moveTo(newHandlePos + const Offset(0.0, -10.0)); @@ -655,8 +671,8 @@ void main() { // The text should have scrolled up with the handle to keep the active // cursor visible, back to its original position. - newFirstPos = textOffsetToPosition(tester, kFourLines.indexOf('First')); - newFourthPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth')); + newFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First')); + newFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth')); expect(newFirstPos.dy, firstPos.dy); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse); diff --git a/packages/flutter/test/material/theme_data_test.dart b/packages/flutter/test/material/theme_data_test.dart index 57ea08665cb00..33b4f307c3994 100644 --- a/packages/flutter/test/material/theme_data_test.dart +++ b/packages/flutter/test/material/theme_data_test.dart @@ -90,7 +90,20 @@ void main() { expect(themeData.accentTextTheme.display4.fontFamily, equals('Ahem')); }); - test('Can estimate brightness', () { + test('Can estimate brightness - directly', () { + expect(ThemeData.estimateBrightnessForColor(Colors.white), equals(Brightness.light)); + expect(ThemeData.estimateBrightnessForColor(Colors.black), equals(Brightness.dark)); + expect(ThemeData.estimateBrightnessForColor(Colors.blue), equals(Brightness.dark)); + expect(ThemeData.estimateBrightnessForColor(Colors.yellow), equals(Brightness.light)); + expect(ThemeData.estimateBrightnessForColor(Colors.deepOrange), equals(Brightness.dark)); + expect(ThemeData.estimateBrightnessForColor(Colors.orange), equals(Brightness.light)); + expect(ThemeData.estimateBrightnessForColor(Colors.lime), equals(Brightness.light)); + expect(ThemeData.estimateBrightnessForColor(Colors.grey), equals(Brightness.light)); + expect(ThemeData.estimateBrightnessForColor(Colors.teal), equals(Brightness.dark)); + expect(ThemeData.estimateBrightnessForColor(Colors.indigo), equals(Brightness.dark)); + }); + + test('Can estimate brightness - indirectly', () { expect(new ThemeData(primaryColor: Colors.white).primaryColorBrightness, equals(Brightness.light)); expect(new ThemeData(primaryColor: Colors.black).primaryColorBrightness, equals(Brightness.dark)); expect(new ThemeData(primaryColor: Colors.blue).primaryColorBrightness, equals(Brightness.dark)); diff --git a/packages/flutter/test/painting/colors_test.dart b/packages/flutter/test/painting/colors_test.dart index a1a4f0ac0df6d..213b8ed7da2a0 100644 --- a/packages/flutter/test/painting/colors_test.dart +++ b/packages/flutter/test/painting/colors_test.dart @@ -38,4 +38,28 @@ void main() { expect(green.toColor(), equals(const Color.fromARGB(0xFF, 0x00, 0xFF, 0x00))); expect(blue.toColor(), equals(const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF))); }); + + test('ColorSwatch test', () { + final int color = 0xFF027223; + final ColorSwatch greens1 = new ColorSwatch( + color, const { + '2259 C': const Color(0xFF027223), + '2273 C': const Color(0xFF257226), + '2426 XGC': const Color(0xFF00932F), + '7732 XGC': const Color(0xFF007940), + }, + ); + final ColorSwatch greens2 = new ColorSwatch( + color, const { + '2259 C': const Color(0xFF027223), + '2273 C': const Color(0xFF257226), + '2426 XGC': const Color(0xFF00932F), + '7732 XGC': const Color(0xFF007940), + }, + ); + expect(greens1, greens2); + expect(greens1.hashCode, greens2.hashCode); + expect(greens1['2259 C'], const Color(0xFF027223)); + expect(greens1.value, 0xFF027223); + }); } diff --git a/packages/flutter/test/rendering/paragraph_test.dart b/packages/flutter/test/rendering/paragraph_test.dart index 327157f9f701c..16ebea1594391 100644 --- a/packages/flutter/test/rendering/paragraph_test.dart +++ b/packages/flutter/test/rendering/paragraph_test.dart @@ -79,12 +79,13 @@ void main() { const TextSpan( text: 'This\n' // 4 characters * 10px font size = 40px width on the first line 'is a wrapping test. It should wrap at manual newlines, and if softWrap is true, also at spaces.', - style: const TextStyle(fontFamily: 'Ahem', fontSize: 10.0)), + style: const TextStyle(fontFamily: 'Ahem', fontSize: 10.0), + ), maxLines: 1, softWrap: true, ); - void relayoutWith({int maxLines, bool softWrap, TextOverflow overflow}) { + void relayoutWith({ int maxLines, bool softWrap, TextOverflow overflow }) { paragraph ..maxLines = maxLines ..softWrap = softWrap @@ -147,5 +148,34 @@ void main() { relayoutWith(maxLines: 100, softWrap: true, overflow: TextOverflow.fade); expect(paragraph.debugHasOverflowShader, isFalse); }); + + + test('maxLines', () { + final RenderParagraph paragraph = new RenderParagraph( + const TextSpan( + text: 'How do you write like you\'re running out of time? Write day and night like you\'re running out of time?', + // 0123456789 0123456789 012 345 0123456 012345 01234 012345678 012345678 0123 012 345 0123456 012345 01234 + // 0 1 2 3 4 5 6 7 8 9 10 11 12 + style: const TextStyle(fontFamily: 'Ahem', fontSize: 10.0), + ), + ); + layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0)); + void layoutAt(int maxLines) { + paragraph.maxLines = maxLines; + pumpFrame(); + } + + layoutAt(null); + expect(paragraph.size.height, 130.0); + + layoutAt(1); + expect(paragraph.size.height, 10.0); + + layoutAt(2); + expect(paragraph.size.height, 20.0); + + layoutAt(3); + expect(paragraph.size.height, 30.0); + }); } diff --git a/packages/flutter/test/rendering/recording_canvas.dart b/packages/flutter/test/rendering/recording_canvas.dart index 481306fe9ca24..dbcef517d711d 100644 --- a/packages/flutter/test/rendering/recording_canvas.dart +++ b/packages/flutter/test/rendering/recording_canvas.dart @@ -38,6 +38,12 @@ class TestRecordingCanvas implements Canvas { invocations.add(new _MethodCall(#save)); } + @override + void saveLayer(Rect bounds, Paint paint) { + _saveCount += 1; + invocations.add(new _MethodCall(#saveLayer, [bounds, paint])); + } + @override void restore() { _saveCount -= 1; @@ -77,8 +83,9 @@ class TestRecordingPaintingContext implements PaintingContext { } class _MethodCall implements Invocation { - _MethodCall(this._name); + _MethodCall(this._name, [ this._arguments = const [] ]); final Symbol _name; + final List _arguments; @override bool get isAccessor => false; @override @@ -92,5 +99,5 @@ class _MethodCall implements Invocation { @override Map get namedArguments => {}; @override - List get positionalArguments => []; + List get positionalArguments => _arguments; } diff --git a/packages/flutter/test/rendering/slivers_block_test.dart b/packages/flutter/test/rendering/slivers_block_test.dart index 9c705f698f91c..7752617f1ecfd 100644 --- a/packages/flutter/test/rendering/slivers_block_test.dart +++ b/packages/flutter/test/rendering/slivers_block_test.dart @@ -26,7 +26,6 @@ class TestRenderSliverBoxChildManager extends RenderSliverBoxChildManager { @override void createChild(int index, { @required RenderBox after }) { - assert(index >= 0); if (index < 0 || index >= children.length) return null; try { @@ -213,4 +212,36 @@ void main() { expect(e.attached, false); }); + test('SliverList - no zero scroll offset correction', () { + RenderSliverList inner; + RenderBox a; + final TestRenderSliverBoxChildManager childManager = new TestRenderSliverBoxChildManager( + children: [ + a = new RenderSizedBox(const Size(100.0, 400.0)), + new RenderSizedBox(const Size(100.0, 400.0)), + new RenderSizedBox(const Size(100.0, 400.0)), + new RenderSizedBox(const Size(100.0, 400.0)), + new RenderSizedBox(const Size(100.0, 400.0)), + ], + ); + final RenderViewport root = new RenderViewport( + axisDirection: AxisDirection.down, + offset: new ViewportOffset.zero(), + children: [ + inner = childManager.createRenderObject(), + ], + ); + layout(root); + + final SliverMultiBoxAdaptorParentData parentData = a.parentData; + parentData.layoutOffset = 0.001; + + root.offset = new ViewportOffset.fixed(900.0); + pumpFrame(); + + root.offset = new ViewportOffset.fixed(0.0); + pumpFrame(); + + expect(inner.geometry.scrollOffsetCorrection, isNull); + }); } diff --git a/packages/flutter/test/services/haptic_feedback_test.dart b/packages/flutter/test/services/haptic_feedback_test.dart index f051fb49e3f44..3f029fdc89bd5 100644 --- a/packages/flutter/test/services/haptic_feedback_test.dart +++ b/packages/flutter/test/services/haptic_feedback_test.dart @@ -15,6 +15,6 @@ void main() { await HapticFeedback.vibrate(); - expect(log, equals([new MethodCall('HapticFeedback.vibrate')])); + expect(log, equals([const MethodCall('HapticFeedback.vibrate')])); }); } diff --git a/packages/flutter/test/services/platform_channel_test.dart b/packages/flutter/test/services/platform_channel_test.dart index 1f17812cbaeaa..bde93d085aeb2 100644 --- a/packages/flutter/test/services/platform_channel_test.dart +++ b/packages/flutter/test/services/platform_channel_test.dart @@ -91,7 +91,7 @@ void main() { }); test('can handle method call with no registered plugin', () async { channel.setMethodCallHandler(null); - final ByteData call = jsonMethod.encodeMethodCall(new MethodCall('sayHello', 'hello')); + final ByteData call = jsonMethod.encodeMethodCall(const MethodCall('sayHello', 'hello')); ByteData envelope; await BinaryMessages.handlePlatformMessage('ch7', call, (ByteData result) { envelope = result; @@ -102,7 +102,7 @@ void main() { channel.setMethodCallHandler((MethodCall call) async { throw new MissingPluginException(); }); - final ByteData call = jsonMethod.encodeMethodCall(new MethodCall('sayHello', 'hello')); + final ByteData call = jsonMethod.encodeMethodCall(const MethodCall('sayHello', 'hello')); ByteData envelope; await BinaryMessages.handlePlatformMessage('ch7', call, (ByteData result) { envelope = result; @@ -111,7 +111,7 @@ void main() { }); test('can handle method call with successful result', () async { channel.setMethodCallHandler((MethodCall call) async => '${call.arguments}, world'); - final ByteData call = jsonMethod.encodeMethodCall(new MethodCall('sayHello', 'hello')); + final ByteData call = jsonMethod.encodeMethodCall(const MethodCall('sayHello', 'hello')); ByteData envelope; await BinaryMessages.handlePlatformMessage('ch7', call, (ByteData result) { envelope = result; @@ -122,7 +122,7 @@ void main() { channel.setMethodCallHandler((MethodCall call) async { throw new PlatformException(code: 'bad', message: 'sayHello failed', details: null); }); - final ByteData call = jsonMethod.encodeMethodCall(new MethodCall('sayHello', 'hello')); + final ByteData call = jsonMethod.encodeMethodCall(const MethodCall('sayHello', 'hello')); ByteData envelope; await BinaryMessages.handlePlatformMessage('ch7', call, (ByteData result) { envelope = result; @@ -141,7 +141,7 @@ void main() { channel.setMethodCallHandler((MethodCall call) async { throw new ArgumentError('bad'); }); - final ByteData call = jsonMethod.encodeMethodCall(new MethodCall('sayHello', 'hello')); + final ByteData call = jsonMethod.encodeMethodCall(const MethodCall('sayHello', 'hello')); ByteData envelope; await BinaryMessages.handlePlatformMessage('ch7', call, (ByteData result) { envelope = result; diff --git a/packages/flutter/test/services/system_navigator_test.dart b/packages/flutter/test/services/system_navigator_test.dart index 828e80b983da4..a693761716406 100644 --- a/packages/flutter/test/services/system_navigator_test.dart +++ b/packages/flutter/test/services/system_navigator_test.dart @@ -15,6 +15,6 @@ void main() { await SystemNavigator.pop(); - expect(log, equals([new MethodCall('SystemNavigator.pop')])); + expect(log, equals([const MethodCall('SystemNavigator.pop')])); }); } diff --git a/packages/flutter/test/services/system_sound_test.dart b/packages/flutter/test/services/system_sound_test.dart index f06683919c9d2..38b4f81a5214c 100644 --- a/packages/flutter/test/services/system_sound_test.dart +++ b/packages/flutter/test/services/system_sound_test.dart @@ -8,13 +8,13 @@ import 'package:test/test.dart'; void main() { test('System sound control test', () async { final List log = []; - + SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async { log.add(methodCall); }); - + await SystemSound.play(SystemSoundType.click); - expect(log, equals([new MethodCall('SystemSound.play', "SystemSoundType.click")])); + expect(log, equals([const MethodCall('SystemSound.play', "SystemSoundType.click")])); }); } diff --git a/packages/flutter/test/widgets/async_test.dart b/packages/flutter/test/widgets/async_test.dart index 684c8b2a76db1..ad5bade69599d 100644 --- a/packages/flutter/test/widgets/async_test.dart +++ b/packages/flutter/test/widgets/async_test.dart @@ -14,7 +14,7 @@ void main() { group('AsyncSnapshot', () { test('requiring data succeeds if data is present', () { expect( - new AsyncSnapshot.withData(ConnectionState.done, 'hello').requireData, + const AsyncSnapshot.withData(ConnectionState.done, 'hello').requireData, 'hello', ); }); diff --git a/packages/flutter/test/widgets/binding_test.dart b/packages/flutter/test/widgets/binding_test.dart index 610733afb1346..8839333224cec 100644 --- a/packages/flutter/test/widgets/binding_test.dart +++ b/packages/flutter/test/widgets/binding_test.dart @@ -26,6 +26,16 @@ class AppLifecycleStateObserver extends WidgetsBindingObserver { } } +class PushRouteObserver extends WidgetsBindingObserver { + String pushedRoute; + + @override + Future didPushRoute(String route) async { + pushedRoute = route; + return true; + } +} + void main() { setUp(() { WidgetsFlutterBinding.ensureInitialized(); @@ -61,4 +71,17 @@ void main() { await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {}); expect(observer.lifecycleState, AppLifecycleState.suspending); }); + + testWidgets('didPushRoute callback', (WidgetTester tester) async { + final PushRouteObserver observer = new PushRouteObserver(); + WidgetsBinding.instance.addObserver(observer); + + final String testRouteName = 'testRouteName'; + final ByteData message = const JSONMethodCodec().encodeMethodCall( + new MethodCall('pushRoute', testRouteName)); + await BinaryMessages.handlePlatformMessage('flutter/navigation', message, (_) {}); + expect(observer.pushedRoute, testRouteName); + + WidgetsBinding.instance.removeObserver(observer); + }); } diff --git a/packages/flutter/test/widgets/box_decoration_test.dart b/packages/flutter/test/widgets/box_decoration_test.dart index 7adf5fa51bdd5..667652b4166f0 100644 --- a/packages/flutter/test/widgets/box_decoration_test.dart +++ b/packages/flutter/test/widgets/box_decoration_test.dart @@ -86,4 +86,82 @@ void main() { expect(find.byKey(key), paints..path(color: green, style: PaintingStyle.fill)); }); + + testWidgets('Can hit test on BoxDecoration', (WidgetTester tester) async { + + List itemsTapped; + + final Key key = const Key('Container with BoxDecoration'); + Widget buildFrame(Border border) { + itemsTapped = []; + return new Center( + child: new GestureDetector( + behavior: HitTestBehavior.deferToChild, + child: new Container( + key: key, + width: 100.0, + height: 50.0, + decoration: new BoxDecoration(border: border), + ), + onTap: () { + itemsTapped.add(1); + }, + ) + ); + } + + await tester.pumpWidget(buildFrame(new Border.all())); + expect(itemsTapped, isEmpty); + + await tester.tap(find.byKey(key)); + expect(itemsTapped, [1]); + + await tester.tapAt(const Offset(350.0, 275.0)); + expect(itemsTapped, [1,1]); + + await tester.tapAt(const Offset(449.0, 324.0)); + expect(itemsTapped, [1,1,1]); + + }); + + testWidgets('Can hit test on BoxDecoration circle', (WidgetTester tester) async { + + List itemsTapped; + + final Key key = const Key('Container with BoxDecoration'); + Widget buildFrame(Border border) { + itemsTapped = []; + return new Center( + child: new GestureDetector( + behavior: HitTestBehavior.deferToChild, + child: new Container( + key: key, + width: 100.0, + height: 50.0, + decoration: new BoxDecoration(border: border, shape: BoxShape.circle), + ), + onTap: () { + itemsTapped.add(1); + }, + ) + ); + } + + await tester.pumpWidget(buildFrame(new Border.all())); + expect(itemsTapped, isEmpty); + + await tester.tapAt(const Offset(0.0, 0.0)); + expect(itemsTapped, isEmpty); + + await tester.tapAt(const Offset(350.0, 275.0)); + expect(itemsTapped, isEmpty); + + await tester.tapAt(const Offset(400.0, 300.0)); + expect(itemsTapped, [1]); + + await tester.tap(find.byKey(key)); + expect(itemsTapped, [1,1]); + + }); + } diff --git a/packages/flutter/test/widgets/draggable_test.dart b/packages/flutter/test/widgets/draggable_test.dart index ace589150142b..4e6acb2666a5a 100644 --- a/packages/flutter/test/widgets/draggable_test.dart +++ b/packages/flutter/test/widgets/draggable_test.dart @@ -518,7 +518,7 @@ void main() { events.clear(); }); - testWidgets('Drag and drop - onDraggableDropped not called if dropped on accepting target', (WidgetTester tester) async { + testWidgets('Drag and drop - onDraggableCanceled not called if dropped on accepting target', (WidgetTester tester) async { final List accepted = []; bool onDraggableCanceledCalled = false; @@ -579,7 +579,7 @@ void main() { expect(onDraggableCanceledCalled, isFalse); }); - testWidgets('Drag and drop - onDraggableDropped called if dropped on non-accepting target', (WidgetTester tester) async { + testWidgets('Drag and drop - onDraggableCanceled called if dropped on non-accepting target', (WidgetTester tester) async { final List accepted = []; bool onDraggableCanceledCalled = false; Velocity onDraggableCanceledVelocity; @@ -649,7 +649,7 @@ void main() { expect(onDraggableCanceledOffset, equals(new Offset(secondLocation.dx, secondLocation.dy))); }); - testWidgets('Drag and drop - onDraggableDropped called if dropped on non-accepting target with correct velocity', (WidgetTester tester) async { + testWidgets('Drag and drop - onDraggableCanceled called if dropped on non-accepting target with correct velocity', (WidgetTester tester) async { final List accepted = []; bool onDraggableCanceledCalled = false; Velocity onDraggableCanceledVelocity; @@ -699,6 +699,131 @@ void main() { expect(onDraggableCanceledOffset, equals(new Offset(flingStart.dx, flingStart.dy) + const Offset(0.0, 100.0))); }); + testWidgets('Drag and drop - onDragCompleted not called if dropped on non-accepting target', (WidgetTester tester) async { + final List accepted = []; + bool onDragCompletedCalled = false; + + await tester.pumpWidget(new MaterialApp( + home: new Column( + children: [ + new Draggable( + data: 1, + child: const Text('Source'), + feedback: const Text('Dragging'), + onDragCompleted: () { + onDragCompletedCalled = true; + } + ), + new DragTarget( + builder: (BuildContext context, List data, List rejects) { + return new Container( + height: 100.0, + child: const Text('Target') + ); + }, + onWillAccept: (int data) => false + ), + ] + ) + )); + + expect(accepted, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + + final Offset firstLocation = tester.getTopLeft(find.text('Source')); + final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); + await tester.pump(); + + expect(accepted, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + + final Offset secondLocation = tester.getCenter(find.text('Target')); + await gesture.moveTo(secondLocation); + await tester.pump(); + + expect(accepted, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + + await gesture.up(); + await tester.pump(); + + expect(accepted, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + }); + + testWidgets('Drag and drop - onDragCompleted called if dropped on accepting target', (WidgetTester tester) async { + final List accepted = []; + bool onDragCompletedCalled = false; + + await tester.pumpWidget(new MaterialApp( + home: new Column( + children: [ + new Draggable( + data: 1, + child: const Text('Source'), + feedback: const Text('Dragging'), + onDragCompleted: () { + onDragCompletedCalled = true; + } + ), + new DragTarget( + builder: (BuildContext context, List data, List rejects) { + return new Container(height: 100.0, child: const Text('Target')); + }, + onAccept: accepted.add + ), + ] + ) + )); + + expect(accepted, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + + final Offset firstLocation = tester.getCenter(find.text('Source')); + final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7); + await tester.pump(); + + expect(accepted, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + + final Offset secondLocation = tester.getCenter(find.text('Target')); + await gesture.moveTo(secondLocation); + await tester.pump(); + + expect(accepted, isEmpty); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsOneWidget); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isFalse); + + await gesture.up(); + await tester.pump(); + + expect(accepted, equals([1])); + expect(find.text('Source'), findsOneWidget); + expect(find.text('Dragging'), findsNothing); + expect(find.text('Target'), findsOneWidget); + expect(onDragCompletedCalled, isTrue); + }); + testWidgets('Drag and drop - allow pass thru of unaccepted data test', (WidgetTester tester) async { final List acceptedInts = []; final List acceptedDoubles = []; diff --git a/packages/flutter/test/widgets/layout_builder_mutations_test.dart b/packages/flutter/test/widgets/layout_builder_mutations_test.dart index e6014f2481911..1eff184584df5 100644 --- a/packages/flutter/test/widgets/layout_builder_mutations_test.dart +++ b/packages/flutter/test/widgets/layout_builder_mutations_test.dart @@ -9,12 +9,11 @@ import 'package:flutter/src/widgets/layout_builder.dart'; import 'package:flutter_test/flutter_test.dart' hide TypeMatcher; class Wrapper extends StatelessWidget { - Wrapper({ + const Wrapper({ Key key, @required this.child - }) : super(key: key) { - assert(child != null); - } + }) : assert(child != null), + super(key: key); final Widget child; diff --git a/packages/flutter/test/widgets/linked_scroll_view_test.dart b/packages/flutter/test/widgets/linked_scroll_view_test.dart index ea044b4b43936..05bd566dff0ae 100644 --- a/packages/flutter/test/widgets/linked_scroll_view_test.dart +++ b/packages/flutter/test/widgets/linked_scroll_view_test.dart @@ -113,14 +113,13 @@ class LinkedScrollPosition extends ScrollPositionWithSingleContext { ScrollContext context, double initialPixels, ScrollPosition oldPosition, - }) : super( - physics: physics, - context: context, - initialPixels: initialPixels, - oldPosition: oldPosition, - ) { - assert(owner != null); - } + }) : assert(owner != null), + super( + physics: physics, + context: context, + initialPixels: initialPixels, + oldPosition: oldPosition, + ); final LinkedScrollController owner; @@ -547,4 +546,4 @@ void main() { await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 60)); }); -} \ No newline at end of file +} diff --git a/packages/flutter/test/widgets/page_storage_test.dart b/packages/flutter/test/widgets/page_storage_test.dart index d52e173cda3bf..fbe0d08881c3d 100644 --- a/packages/flutter/test/widgets/page_storage_test.dart +++ b/packages/flutter/test/widgets/page_storage_test.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; void main() { testWidgets('PageStorage read and write', (WidgetTester tester) async { - final Key builderKey = const Key('builderKey'); + final Key builderKey = const PageStorageKey('builderKey'); StateSetter setState; int storedValue = 0; diff --git a/packages/flutter/test/widgets/page_view_test.dart b/packages/flutter/test/widgets/page_view_test.dart index 29a4d17b7f81b..07f4ea8922d44 100644 --- a/packages/flutter/test/widgets/page_view_test.dart +++ b/packages/flutter/test/widgets/page_view_test.dart @@ -407,6 +407,7 @@ void main() { new PageStorage( bucket: bucket, child: new PageView( + key: const PageStorageKey('PageView'), controller: controller, children: [ const Placeholder(), @@ -431,6 +432,7 @@ void main() { new PageStorage( bucket: bucket, child: new PageView( + key: const PageStorageKey('PageView'), controller: controller, children: [ const Placeholder(), @@ -441,12 +443,14 @@ void main() { ), ); expect(controller.page, 2); + + final PageController controller2 = new PageController(keepPage: false); await tester.pumpWidget( new PageStorage( bucket: bucket, child: new PageView( - key: const Key('Check it again against your list and see consistency!'), - controller: controller, + key: const PageStorageKey('Check it again against your list and see consistency!'), + controller: controller2, children: [ const Placeholder(), const Placeholder(), @@ -455,6 +459,6 @@ void main() { ), ), ); - expect(controller.page, 0); + expect(controller2.page, 0); }); } diff --git a/packages/flutter/test/widgets/remember_scroll_position_test.dart b/packages/flutter/test/widgets/remember_scroll_position_test.dart index 393addb45048a..58f039c6ab675 100644 --- a/packages/flutter/test/widgets/remember_scroll_position_test.dart +++ b/packages/flutter/test/widgets/remember_scroll_position_test.dart @@ -18,6 +18,7 @@ class ThePositiveNumbers extends StatelessWidget { @override Widget build(BuildContext context) { return new ListView.builder( + key: const PageStorageKey('ThePositiveNumbers'), itemExtent: 100.0, controller: _controller, itemBuilder: (BuildContext context, int index) { diff --git a/packages/flutter/test/widgets/scroll_controller_test.dart b/packages/flutter/test/widgets/scroll_controller_test.dart index f935edcd57904..388e347707afc 100644 --- a/packages/flutter/test/widgets/scroll_controller_test.dart +++ b/packages/flutter/test/widgets/scroll_controller_test.dart @@ -259,4 +259,54 @@ void main() { await tester.drag(find.byType(ListView), const Offset(0.0, -130.0)); expect(log, isEmpty); }); + + testWidgets('keepScrollOffset', (WidgetTester tester) async { + final PageStorageBucket bucket = new PageStorageBucket(); + + Widget buildFrame(ScrollController controller) { + return new PageStorage( + bucket: bucket, + child: new KeyedSubtree( + key: const PageStorageKey('ListView'), + child: new ListView( + key: new UniqueKey(), // it's a different ListView every time + controller: controller, + children: new List.generate(50, (int index) { + return new Container(height: 100.0, child: new Text('Item $index')); + }).toList(), + ), + ), + ); + } + + // keepScrollOffset: true (the default). The scroll offset is restored + // when the ListView is recreated with a new ScrollController. + + // The initialScrollOffset is used in this case, because there's no saved + // scroll offset. + ScrollController controller = new ScrollController(initialScrollOffset: 200.0); + await tester.pumpWidget(buildFrame(controller)); + expect(tester.getTopLeft(find.widgetWithText(Container, 'Item 2')), Offset.zero); + + controller.jumpTo(2000.0); + await tester.pump(); + expect(tester.getTopLeft(find.widgetWithText(Container, 'Item 20')), Offset.zero); + + // The initialScrollOffset isn't used in this case, because the scrolloffset + // can be restored. + controller = new ScrollController(initialScrollOffset: 25.0); + await tester.pumpWidget(buildFrame(controller)); + expect(controller.offset, 2000.0); + expect(tester.getTopLeft(find.widgetWithText(Container, 'Item 20')), Offset.zero); + + // keepScrollOffset: false. The scroll offset is -not- restored + // when the ListView is recreated with a new ScrollController and + // the initialScrollOffset is used. + + controller = new ScrollController(keepScrollOffset: false, initialScrollOffset: 100.0); + await tester.pumpWidget(buildFrame(controller)); + expect(controller.offset, 100.0); + expect(tester.getTopLeft(find.widgetWithText(Container, 'Item 1')), Offset.zero); + + }); } diff --git a/packages/flutter/test/widgets/semantics_9_test.dart b/packages/flutter/test/widgets/semantics_9_test.dart new file mode 100644 index 0000000000000..17eccc0beff3f --- /dev/null +++ b/packages/flutter/test/widgets/semantics_9_test.dart @@ -0,0 +1,149 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'semantics_tester.dart'; + +void main() { + group('BlockSemantics', () { + testWidgets('hides semantic nodes of siblings', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await tester.pumpWidget(new Stack( + children: [ + new Semantics( + label: 'layer#1', + child: new Container(), + ), + const BlockSemantics(), + new Semantics( + label: 'layer#2', + child: new Container(), + ), + ], + )); + + expect(semantics, isNot(includesNodeWithLabel('layer#1'))); + + await tester.pumpWidget(new Stack( + children: [ + new Semantics( + label: 'layer#1', + child: new Container(), + ), + ], + )); + + expect(semantics, includesNodeWithLabel('layer#1')); + + semantics.dispose(); + }); + + testWidgets('does not hides semantic nodes of siblings outside the current semantic boundary', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await tester.pumpWidget(new Stack( + children: [ + new Semantics( + label: '#1', + child: new Container(), + ), + new Semantics( + label: '#2', + container: true, + child: new Stack( + children: [ + new Semantics( + label: 'NOT#2.1', + child: new Container(), + ), + new Semantics( + label: '#2.2', + child: new BlockSemantics( + child: new Semantics( + container: true, + label: '#2.2.1', + child: new Container(), + ), + ), + ), + new Semantics( + label: '#2.3', + child: new Container(), + ), + ], + ), + ), + new Semantics( + label: '#3', + child: new Container(), + ), + ], + )); + + expect(semantics, includesNodeWithLabel('#1')); + expect(semantics, includesNodeWithLabel('#2')); + expect(semantics, isNot(includesNodeWithLabel('NOT#2.1'))); + expect(semantics, includesNodeWithLabel('#2.2')); + expect(semantics, includesNodeWithLabel('#2.2.1')); + expect(semantics, includesNodeWithLabel('#2.3')); + expect(semantics, includesNodeWithLabel('#3')); + + semantics.dispose(); + }); + + testWidgets('node is semantic boundary and blocking previously painted nodes', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + final GlobalKey stackKey = new GlobalKey(); + + await tester.pumpWidget(new Stack( + key: stackKey, + children: [ + new Semantics( + label: 'NOT#1', + child: new Container(), + ), + new BoundaryBlockSemantics( + child: new Semantics( + label: '#2.1', + child: new Container(), + ) + ), + new Semantics( + label: '#3', + child: new Container(), + ), + ], + )); + + expect(semantics, isNot(includesNodeWithLabel('NOT#1'))); + expect(semantics, includesNodeWithLabel('#2.1')); + expect(semantics, includesNodeWithLabel('#3')); + + semantics.dispose(); + }); + }); +} + +class BoundaryBlockSemantics extends SingleChildRenderObjectWidget { + const BoundaryBlockSemantics({ Key key, Widget child }) : super(key: key, child: child); + + @override + RenderBoundaryBlockSemantics createRenderObject(BuildContext context) => new RenderBoundaryBlockSemantics(); +} + +class RenderBoundaryBlockSemantics extends RenderProxyBox { + RenderBoundaryBlockSemantics({ RenderBox child }) : super(child); + + @override + bool get isBlockingSemanticsOfPreviouslyPaintedNodes => true; + + @override + bool get isSemanticBoundary => true; +} + diff --git a/packages/flutter/test/widgets/semantics_tester.dart b/packages/flutter/test/widgets/semantics_tester.dart index eca1c757bd2e8..e538cceaaf125 100644 --- a/packages/flutter/test/widgets/semantics_tester.dart +++ b/packages/flutter/test/widgets/semantics_tester.dart @@ -194,6 +194,8 @@ class SemanticsTester { String toString() => 'SemanticsTester'; } +const String _matcherHelp = 'Try dumping the semantics with debugDumpSemanticsTree() from the rendering library to see what the semantics tree looks like.'; + class _HasSemantics extends Matcher { const _HasSemantics(this._semantics) : assert(_semantics != null); @@ -211,30 +213,65 @@ class _HasSemantics extends Matcher { @override Description describeMismatch(dynamic item, Description mismatchDescription, Map matchState, bool verbose) { - const String help = 'Try dumping the semantics with debugDumpSemanticsTree() from the rendering library to see what the semantics tree looks like.'; final TestSemantics testNode = matchState[TestSemantics]; final SemanticsNode node = matchState[SemanticsNode]; if (node == null) - return mismatchDescription.add('could not find node with id ${testNode.id}.\n$help'); + return mismatchDescription.add('could not find node with id ${testNode.id}.\n$_matcherHelp'); if (testNode.id != node.id) - return mismatchDescription.add('expected node id ${testNode.id} but found id ${node.id}.\n$help'); + return mismatchDescription.add('expected node id ${testNode.id} but found id ${node.id}.\n$_matcherHelp'); final SemanticsData data = node.getSemanticsData(); if (testNode.flags != data.flags) - return mismatchDescription.add('expected node id ${testNode.id} to have flags ${testNode.flags} but found flags ${data.flags}.\n$help'); + return mismatchDescription.add('expected node id ${testNode.id} to have flags ${testNode.flags} but found flags ${data.flags}.\n$_matcherHelp'); if (testNode.actions != data.actions) - return mismatchDescription.add('expected node id ${testNode.id} to have actions ${testNode.actions} but found actions ${data.actions}.\n$help'); + return mismatchDescription.add('expected node id ${testNode.id} to have actions ${testNode.actions} but found actions ${data.actions}.\n$_matcherHelp'); if (testNode.label != data.label) - return mismatchDescription.add('expected node id ${testNode.id} to have label "${testNode.label}" but found label "${data.label}".\n$help'); + return mismatchDescription.add('expected node id ${testNode.id} to have label "${testNode.label}" but found label "${data.label}".\n$_matcherHelp'); if (testNode.rect != data.rect) - return mismatchDescription.add('expected node id ${testNode.id} to have rect ${testNode.rect} but found rect ${data.rect}.\n$help'); + return mismatchDescription.add('expected node id ${testNode.id} to have rect ${testNode.rect} but found rect ${data.rect}.\n$_matcherHelp'); if (testNode.transform != data.transform) - return mismatchDescription.add('expected node id ${testNode.id} to have transform ${testNode.transform} but found transform:.\n${data.transform}.\n$help'); + return mismatchDescription.add('expected node id ${testNode.id} to have transform ${testNode.transform} but found transform:.\n${data.transform}.\n$_matcherHelp'); final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount; if (testNode.children.length != childrenCount) - return mismatchDescription.add('expected node id ${testNode.id} to have ${testNode.children.length} children but found $childrenCount.\n$help'); + return mismatchDescription.add('expected node id ${testNode.id} to have ${testNode.children.length} children but found $childrenCount.\n$_matcherHelp'); return mismatchDescription; } } /// Asserts that a [SemanticsTester] has a semantics tree that exactly matches the given semantics. Matcher hasSemantics(TestSemantics semantics) => new _HasSemantics(semantics); + +class _IncludesNodeWithLabel extends Matcher { + const _IncludesNodeWithLabel(this._label) : assert(_label != null); + + final String _label; + + @override + bool matches(covariant SemanticsTester item, Map matchState) { + bool result = false; + SemanticsNodeVisitor visitor; + visitor = (SemanticsNode node) { + if (node.label == _label) { + result = true; + } else { + node.visitChildren(visitor); + } + return !result; + }; + final SemanticsNode root = item.tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode; + visitor(root); + return result; + } + + @override + Description describe(Description description) { + return description.add('includes node with label "$_label"'); + } + + @override + Description describeMismatch(dynamic item, Description mismatchDescription, Map matchState, bool verbose) { + return mismatchDescription.add('could not find node with label "$_label".\n$_matcherHelp'); + } +} + +/// Asserts that a node in the semantics tree of [SemanticsTester] has [label]. +Matcher includesNodeWithLabel(String label) => new _IncludesNodeWithLabel(label); diff --git a/packages/flutter/test/widgets/shader_mask_test.dart b/packages/flutter/test/widgets/shader_mask_test.dart index e79ec9c2b018b..7987139e31e99 100644 --- a/packages/flutter/test/widgets/shader_mask_test.dart +++ b/packages/flutter/test/widgets/shader_mask_test.dart @@ -22,4 +22,28 @@ void main() { final Widget child = new Container(width: 100.0, height: 100.0); await tester.pumpWidget(new ShaderMask(child: child, shaderCallback: createShader)); }); + + testWidgets('Bounds rect includes offset', (WidgetTester tester) async { + Rect shaderBounds; + Shader recordShaderBounds(Rect bounds) { + shaderBounds = bounds; + return createShader(bounds); + } + + final Widget widget = new Align( + alignment: FractionalOffset.center, + child: new SizedBox( + width: 400.0, + height: 400.0, + child: new ShaderMask( + shaderCallback: recordShaderBounds, + child: new Container(width: 100.0, height: 100.0) + ), + ), + ); + await tester.pumpWidget(widget); + + // The shader bounds rectangle should reflect the position of the centered SizedBox. + expect(shaderBounds, equals(new Rect.fromLTWH(200.0, 100.0, 400.0, 400.0))); + }); } diff --git a/packages/flutter/test/widgets/transform_test.dart b/packages/flutter/test/widgets/transform_test.dart index eb8c7cbdd732d..928b209005b8c 100644 --- a/packages/flutter/test/widgets/transform_test.dart +++ b/packages/flutter/test/widgets/transform_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:math' as math; + import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -171,4 +173,26 @@ void main() { final Matrix4 transform = layer.transform; expect(transform.getTranslation(), equals(new Vector3(100.0, 75.0, 0.0))); }); + + testWidgets('Transform.rotate', (WidgetTester tester) async { + await tester.pumpWidget( + new Transform.rotate( + angle: math.PI / 2.0, + child: new Opacity(opacity: 0.5, child: new Container()), + ), + ); + + final List layers = tester.layers + ..retainWhere((Layer layer) => layer is TransformLayer); + expect(layers.length, 2); + // The first transform is from the render view. + final TransformLayer layer = layers[1]; + final Matrix4 transform = layer.transform; + expect(transform.storage, [ + moreOrLessEquals(0.0), 1.0, 0.0, 0.0, + -1.0, moreOrLessEquals(0.0), 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 700.0, -100.0, 0.0, 1.0, + ]); + }); } diff --git a/packages/flutter_driver/lib/src/driver.dart b/packages/flutter_driver/lib/src/driver.dart index 8652cd553acad..edd18518bff5b 100644 --- a/packages/flutter_driver/lib/src/driver.dart +++ b/packages/flutter_driver/lib/src/driver.dart @@ -21,6 +21,7 @@ import 'gesture.dart'; import 'health.dart'; import 'message.dart'; import 'render_tree.dart'; +import 'semantics.dart'; import 'timeline.dart'; /// Timeline stream identifier. @@ -158,8 +159,8 @@ class FlutterDriver { VMIsolate isolate = await vm.isolates.first.loadRunnable(); // TODO(yjbanov): vm_service_client does not support "None" pause event yet. - // It is currently reported as `null`, but we cannot rely on it because - // eventually the event will be reported as a non-`null` object. For now, + // It is currently reported as null, but we cannot rely on it because + // eventually the event will be reported as a non-null object. For now, // list all the events we know about. Later we'll check for "None" event // explicitly. // @@ -383,6 +384,15 @@ class FlutterDriver { return GetTextResult.fromJson(await _sendCommand(new GetText(finder, timeout: timeout))).text; } + /// Turns semantics on or off in the Flutter app under test. + /// + /// Returns `true` when the call actually changed the state from on to off or + /// vice versa. + Future setSemantics(bool enabled, { Duration timeout: _kShortTimeout }) async { + final SetSemanticsResult result = SetSemanticsResult.fromJson(await _sendCommand(new SetSemantics(enabled, timeout: timeout))); + return result.changedState; + } + /// Take a screenshot. The image will be returned as a PNG. Future> screenshot({ Duration timeout }) async { timeout ??= _kLongTimeout; diff --git a/packages/flutter_driver/lib/src/extension.dart b/packages/flutter_driver/lib/src/extension.dart index 1af359d89e615..cb5ff5fcc9f36 100644 --- a/packages/flutter_driver/lib/src/extension.dart +++ b/packages/flutter_driver/lib/src/extension.dart @@ -8,7 +8,7 @@ import 'package:meta/meta.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart' show RendererBinding; +import 'package:flutter/rendering.dart' show RendererBinding, SemanticsHandle; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -20,6 +20,7 @@ import 'gesture.dart'; import 'health.dart'; import 'message.dart'; import 'render_tree.dart'; +import 'semantics.dart'; const String _extensionMethodName = 'driver'; const String _extensionMethod = 'ext.flutter.$_extensionMethodName'; @@ -49,13 +50,14 @@ void enableFlutterDriverExtension() { assert(WidgetsBinding.instance is _DriverBinding); } -/// Handles a command and returns a result. +/// Signature for functions that handle a command and return a result. typedef Future CommandHandlerCallback(Command c); -/// Deserializes JSON map to a command object. +/// Signature for functions that deserialize a JSON map to a command object. typedef Command CommandDeserializerCallback(Map params); -/// Runs the finder and returns the [Element] found, or `null`. +/// Signature for functions that run the given finder and return the [Element] +/// found, if any, or null otherwise. typedef Finder FinderConstructor(SerializableFinder finder); @visibleForTesting @@ -69,6 +71,7 @@ class FlutterDriverExtension { 'tap': _tap, 'get_text': _getText, 'set_frame_sync': _setFrameSync, + 'set_semantics': _setSemantics, 'scroll': _scroll, 'scrollIntoView': _scrollIntoView, 'waitFor': _waitFor, @@ -81,6 +84,7 @@ class FlutterDriverExtension { 'tap': (Map params) => new Tap.deserialize(params), 'get_text': (Map params) => new GetText.deserialize(params), 'set_frame_sync': (Map params) => new SetFrameSync.deserialize(params), + 'set_semantics': (Map params) => new SetSemantics.deserialize(params), 'scroll': (Map params) => new Scroll.deserialize(params), 'scrollIntoView': (Map params) => new ScrollIntoView.deserialize(params), 'waitFor': (Map params) => new WaitFor.deserialize(params), @@ -270,4 +274,27 @@ class FlutterDriverExtension { _frameSync = setFrameSyncCommand.enabled; return new SetFrameSyncResult(); } + + SemanticsHandle _semantics; + bool get _semanticsIsEnabled => RendererBinding.instance.pipelineOwner.semanticsOwner != null; + + Future _setSemantics(Command command) async { + final SetSemantics setSemanticsCommand = command; + final bool semanticsWasEnabled = _semanticsIsEnabled; + if (setSemanticsCommand.enabled && _semantics == null) { + _semantics = RendererBinding.instance.pipelineOwner.ensureSemantics(); + if (!semanticsWasEnabled) { + // wait for the first frame where semantics is enabled. + final Completer completer = new Completer(); + SchedulerBinding.instance.addPostFrameCallback((Duration d) { + completer.complete(); + }); + await completer.future; + } + } else if (!setSemanticsCommand.enabled && _semantics != null) { + _semantics.dispose(); + _semantics = null; + } + return new SetSemanticsResult(semanticsWasEnabled != _semanticsIsEnabled); + } } diff --git a/packages/flutter_driver/lib/src/health.dart b/packages/flutter_driver/lib/src/health.dart index a146570ffea9d..da10a27e89363 100644 --- a/packages/flutter_driver/lib/src/health.dart +++ b/packages/flutter_driver/lib/src/health.dart @@ -16,7 +16,6 @@ class GetHealth extends Command { GetHealth.deserialize(Map json) : super.deserialize(json); } -/// Application health status. enum HealthStatus { /// Application is known to be in a good shape and should be able to respond. ok, @@ -28,7 +27,6 @@ enum HealthStatus { final EnumIndex _healthStatusIndex = new EnumIndex(HealthStatus.values); -/// Application health status. class Health extends Result { /// Creates a [Health] object with the given [status]. Health(this.status) { @@ -40,7 +38,6 @@ class Health extends Result { return new Health(_healthStatusIndex.lookupBySimpleName(json['status'])); } - /// Health status final HealthStatus status; @override diff --git a/packages/flutter_driver/lib/src/message.dart b/packages/flutter_driver/lib/src/message.dart index ebc421a5cdec2..244444410278c 100644 --- a/packages/flutter_driver/lib/src/message.dart +++ b/packages/flutter_driver/lib/src/message.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:meta/meta.dart'; + /// An object sent from the Flutter Driver to a Flutter application to instruct /// the application to perform a task. abstract class Command { @@ -20,6 +22,7 @@ abstract class Command { String get kind; /// Serializes this command to parameter name/value pairs. + @mustCallSuper Map serialize() => { 'command': kind, 'timeout': '${timeout.inMilliseconds}', diff --git a/packages/flutter_driver/lib/src/semantics.dart b/packages/flutter_driver/lib/src/semantics.dart new file mode 100644 index 0000000000000..b9af7c15be6d0 --- /dev/null +++ b/packages/flutter_driver/lib/src/semantics.dart @@ -0,0 +1,43 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'message.dart'; + +/// Enables or disables semantics. +class SetSemantics extends Command { + @override + final String kind = 'set_semantics'; + + /// Whether semantics should be enabled or disabled. + final bool enabled; + + SetSemantics(this.enabled, { Duration timeout }) : super(timeout: timeout); + + /// Deserializes this command from the value generated by [serialize]. + SetSemantics.deserialize(Map params) + : this.enabled = params['enabled'].toLowerCase() == 'true', + super.deserialize(params); + + @override + Map serialize() => super.serialize()..addAll({ + 'enabled': '$enabled', + }); +} + +/// The result of a [SetSemantics] command. +class SetSemanticsResult extends Result { + SetSemanticsResult(this.changedState); + + final bool changedState; + + /// Deserializes this result from JSON. + static SetSemanticsResult fromJson(Map json) { + return new SetSemanticsResult(json['changedState']); + } + + @override + Map toJson() => { + 'changedState': changedState, + }; +} diff --git a/packages/flutter_driver/lib/src/timeline_summary.dart b/packages/flutter_driver/lib/src/timeline_summary.dart index df5c098c11672..f0d90f3f2b65d 100644 --- a/packages/flutter_driver/lib/src/timeline_summary.dart +++ b/packages/flutter_driver/lib/src/timeline_summary.dart @@ -28,14 +28,14 @@ class TimelineSummary { /// Average amount of time spent per frame in the framework building widgets, /// updating layout, painting and compositing. /// - /// Returns `null` if no frames were recorded. + /// Returns null if no frames were recorded. double computeAverageFrameBuildTimeMillis() { return _averageInMillis(_extractFrameThreadDurations()); } /// The longest frame build time in milliseconds. /// - /// Returns `null` if no frames were recorded. + /// Returns null if no frames were recorded. double computeWorstFrameBuildTimeMillis() { return _maxInMillis(_extractFrameThreadDurations()); } @@ -48,14 +48,14 @@ class TimelineSummary { /// Average amount of time spent per frame in the GPU rasterizer. /// - /// Returns `null` if no frames were recorded. + /// Returns null if no frames were recorded. double computeAverageFrameRasterizerTimeMillis() { return _averageInMillis(_extractDuration(_extractGpuRasterizerDrawEvents())); } /// The longest frame rasterization time in milliseconds. /// - /// Returns `null` if no frames were recorded. + /// Returns null if no frames were recorded. double computeWorstFrameRasterizerTimeMillis() { return _maxInMillis(_extractDuration(_extractGpuRasterizerDrawEvents())); } diff --git a/packages/flutter_driver/pubspec.yaml b/packages/flutter_driver/pubspec.yaml index 1a0bc6e4afc2e..3f293665f6024 100644 --- a/packages/flutter_driver/pubspec.yaml +++ b/packages/flutter_driver/pubspec.yaml @@ -1,5 +1,5 @@ name: flutter_driver -version: 0.0.5-dev +version: 0.0.8-dev description: Integration and performance test API for Flutter applications homepage: http://flutter.io author: Flutter Authors @@ -21,6 +21,6 @@ dependencies: sdk: flutter dev_dependencies: - test: 0.12.20 + test: 0.12.21 mockito: ^2.0.2 quiver: ^0.24.0 diff --git a/packages/flutter_test/lib/src/all_elements.dart b/packages/flutter_test/lib/src/all_elements.dart index b0db191d659a7..9cdca78e4669e 100644 --- a/packages/flutter_test/lib/src/all_elements.dart +++ b/packages/flutter_test/lib/src/all_elements.dart @@ -17,7 +17,7 @@ import 'package:flutter/widgets.dart'; /// one, for example the results of calling `where` on this iterable /// are also cached. Iterable collectAllElementsFrom(Element rootElement, { - @required bool skipOffstage + @required bool skipOffstage, }) { return new CachingIterable(new _DepthFirstChildIterator(rootElement, skipOffstage)); } diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index ee8ff34efeaff..b200029c54ce8 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -89,6 +89,10 @@ abstract class TestWidgetsFlutterBinding extends BindingBase // Services binding omitted to avoid dragging in the licenses code. WidgetsBinding { + /// Constructor for [TestWidgetsFlutterBinding]. + /// + /// This constructor overrides the [debugPrint] global hook to point to + /// [debugPrintOverride], which can be overridden by subclasses. TestWidgetsFlutterBinding() { debugPrint = debugPrintOverride; } diff --git a/packages/flutter_test/lib/src/finders.dart b/packages/flutter_test/lib/src/finders.dart index 90a136b656621..1e111c086f1f3 100644 --- a/packages/flutter_test/lib/src/finders.dart +++ b/packages/flutter_test/lib/src/finders.dart @@ -236,6 +236,9 @@ abstract class Finder { /// [Offstage] widgets, as well as children of inactive [Route]s. final bool skipOffstage; + /// Returns all the [Element]s that will be considered by this finder. + /// + /// See [collectAllElementsFrom]. @protected Iterable get allCandidates { return collectAllElementsFrom( diff --git a/packages/flutter_test/pubspec.yaml b/packages/flutter_test/pubspec.yaml index 337555e31d8e9..42d1db7a8306d 100644 --- a/packages/flutter_test/pubspec.yaml +++ b/packages/flutter_test/pubspec.yaml @@ -1,10 +1,10 @@ name: flutter_test -version: 0.0.5-dev +version: 0.0.8-dev dependencies: # The flutter tools depend on very specific internal implementation # details of the 'test' package, which change between versions, so # here we pin it precisely to avoid version skew across our packages. - test: 0.12.20 + test: 0.12.21 # We use FakeAsync and other testing utilities. quiver: ^0.24.0 diff --git a/packages/flutter_tools/bin/fuchsia_tester.dart b/packages/flutter_tools/bin/fuchsia_tester.dart index fb4ef775973e4..d4bd938eece8d 100644 --- a/packages/flutter_tools/bin/fuchsia_tester.dart +++ b/packages/flutter_tools/bin/fuchsia_tester.dart @@ -88,7 +88,6 @@ Future run(List args) async { } loader.installHook( shellPath: shellPath, - debuggerMode: false, ); PackageMap.globalPackagesPath = diff --git a/packages/flutter_tools/flutter_tools.iml b/packages/flutter_tools/flutter_tools.iml index 367b72461cd70..22fe5c309c091 100644 --- a/packages/flutter_tools/flutter_tools.iml +++ b/packages/flutter_tools/flutter_tools.iml @@ -7,6 +7,12 @@ + + + + + + @@ -30,16 +36,10 @@ + - - - - - - - - + diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 9b3c208c3847d..380641270d138 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -83,7 +83,7 @@ Future main(List args) async { new RunCommand(verboseHelp: verboseHelp), new ScreenshotCommand(), new StopCommand(), - new TestCommand(), + new TestCommand(verboseHelp: verboseHelp), new TraceCommand(), new UpdatePackagesCommand(hidden: !verboseHelp), new UpgradeCommand(), diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index fb6fb99c00563..b4dd532fbd59f 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart @@ -397,6 +397,8 @@ class AndroidDevice extends Device { cmd.addAll(['--ez', 'trace-startup', 'true']); if (route != null) cmd.addAll(['--es', 'route', route]); + if (debuggingOptions.enableSoftwareRendering) + cmd.addAll(['--ez', 'enable-software-rendering', 'true']); if (debuggingOptions.debuggingEnabled) { if (debuggingOptions.buildMode == BuildMode.debug) cmd.addAll(['--ez', 'enable-checked-mode', 'true']); @@ -474,7 +476,7 @@ class AndroidDevice extends Device { static final RegExp _timeRegExp = new RegExp(r'^\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}', multiLine: true); - /// Return the most recent timestamp in the Android log or `null` if there is + /// Return the most recent timestamp in the Android log or null if there is /// no available timestamp. The format can be passed to logcat's -T option. String get lastLogcatTimestamp { final String output = runCheckedSync(adbCommandForDevice([ diff --git a/packages/flutter_tools/lib/src/android/android_studio.dart b/packages/flutter_tools/lib/src/android/android_studio.dart index 42fbfb79e8764..7517a23a8a0d6 100644 --- a/packages/flutter_tools/lib/src/android/android_studio.dart +++ b/packages/flutter_tools/lib/src/android/android_studio.dart @@ -291,7 +291,7 @@ class AndroidStudio implements Comparable { if (result.exitCode == 0) { final List versionLines = result.stderr.split('\n'); final String javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0]; - _validationMessages.add('Java version: $javaVersion'); + _validationMessages.add('Java version $javaVersion'); _javaPath = javaPath; } else { _validationMessages.add('Unable to determine bundled Java version.'); diff --git a/packages/flutter_tools/lib/src/android/android_studio_validator.dart b/packages/flutter_tools/lib/src/android/android_studio_validator.dart index d141b29e832a4..5a8da162a7a72 100644 --- a/packages/flutter_tools/lib/src/android/android_studio_validator.dart +++ b/packages/flutter_tools/lib/src/android/android_studio_validator.dart @@ -38,7 +38,9 @@ class AndroidStudioValidator extends DoctorValidator { Future validate() async { final List messages = []; ValidationType type = ValidationType.missing; - final String studioVersionText = 'version ${_studio.version}'; + final String studioVersionText = _studio.version == Version.unknown + ? null + : 'version ${_studio.version}'; messages .add(new ValidationMessage('Android Studio at ${_studio.directory}')); if (_studio.isValid) { diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart index 01ca9f15a4b3a..aedfca7cd06ca 100644 --- a/packages/flutter_tools/lib/src/android/android_workflow.dart +++ b/packages/flutter_tools/lib/src/android/android_workflow.dart @@ -83,12 +83,11 @@ class AndroidWorkflow extends DoctorValidator implements Workflow { messages.add(new ValidationMessage.error('Could not determine java version')); return false; } - messages.add(new ValidationMessage('Java version: $javaVersion')); + messages.add(new ValidationMessage('Java version $javaVersion')); // TODO(johnmccutchan): Validate version. return true; } - @override Future validate() async { final List messages = []; diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart index df951e7eab92c..6f1e39ae87154 100644 --- a/packages/flutter_tools/lib/src/asset.dart +++ b/packages/flutter_tools/lib/src/asset.dart @@ -316,7 +316,7 @@ DevFSContent _createFontManifest(Map manifestDescriptor, /// Given an assetBase location and a pubspec.yaml Flutter manifest, return a /// map of assets to asset variants. /// -/// Returns `null` on missing assets. +/// Returns null on missing assets. Map<_Asset, List<_Asset>> _parseAssets( PackageMap packageMap, Map manifestDescriptor, diff --git a/packages/flutter_tools/lib/src/base/os.dart b/packages/flutter_tools/lib/src/base/os.dart index b4027ad8b3893..1cb56968f03f8 100644 --- a/packages/flutter_tools/lib/src/base/os.dart +++ b/packages/flutter_tools/lib/src/base/os.dart @@ -27,7 +27,7 @@ abstract class OperatingSystemUtils { /// Make the given file executable. This may be a no-op on some platforms. ProcessResult makeExecutable(File file); - /// Return the path (with symlinks resolved) to the given executable, or `null` + /// Return the path (with symlinks resolved) to the given executable, or null /// if `which` was not able to locate the binary. File which(String execName) { final List result = _which(execName); @@ -206,7 +206,7 @@ class _WindowsUtils extends OperatingSystemUtils { /// Find and return the project root directory relative to the specified /// directory or the current working directory if none specified. -/// Return `null` if the project root could not be found +/// Return null if the project root could not be found /// or if the project root is the flutter repository root. String findProjectRoot([String directory]) { const String kProjectRootSentinel = 'pubspec.yaml'; diff --git a/packages/flutter_tools/lib/src/base/version.dart b/packages/flutter_tools/lib/src/base/version.dart index 09008e9f3fd8c..02aa239fa91dc 100644 --- a/packages/flutter_tools/lib/src/base/version.dart +++ b/packages/flutter_tools/lib/src/base/version.dart @@ -45,7 +45,7 @@ class Version implements Comparable { /// Creates a new [Version] by parsing [text]. factory Version.parse(String text) { - final Match match = versionPattern.firstMatch(text); + final Match match = versionPattern.firstMatch(text ?? ''); if (match == null) { return null; } diff --git a/packages/flutter_tools/lib/src/commands/analyze.dart b/packages/flutter_tools/lib/src/commands/analyze.dart index afa01c951e163..f1b3391e2a73a 100644 --- a/packages/flutter_tools/lib/src/commands/analyze.dart +++ b/packages/flutter_tools/lib/src/commands/analyze.dart @@ -57,7 +57,7 @@ class AnalyzeCommand extends FlutterCommand { @override Future runCommand() { if (argResults['watch']) { - return new AnalyzeContinuously(argResults, runner.getRepoAnalysisEntryPoints()).analyze(); + return new AnalyzeContinuously(argResults, runner.getRepoPackages()).analyze(); } else { return new AnalyzeOnce(argResults, runner.getRepoPackages(), workingDirectory: workingDirectory).analyze(); } diff --git a/packages/flutter_tools/lib/src/commands/analyze_base.dart b/packages/flutter_tools/lib/src/commands/analyze_base.dart index ec5191ccde22c..177a344124d4d 100644 --- a/packages/flutter_tools/lib/src/commands/analyze_base.dart +++ b/packages/flutter_tools/lib/src/commands/analyze_base.dart @@ -5,7 +5,9 @@ import 'dart:async'; import 'package:args/args.dart'; +import 'package:yaml/yaml.dart' as yaml; +import '../base/common.dart'; import '../base/file_system.dart'; import '../base/utils.dart'; import '../cache.dart'; @@ -65,3 +67,153 @@ bool inRepo(List fileList) { } return false; } + +class PackageDependency { + // This is a map from dependency targets (lib directories) to a list + // of places that ask for that target (.packages or pubspec.yaml files) + Map> values = >{}; + String canonicalSource; + void addCanonicalCase(String packagePath, String pubSpecYamlPath) { + assert(canonicalSource == null); + add(packagePath, pubSpecYamlPath); + canonicalSource = pubSpecYamlPath; + } + void add(String packagePath, String sourcePath) { + values.putIfAbsent(packagePath, () => []).add(sourcePath); + } + bool get hasConflict => values.length > 1; + bool get hasConflictAffectingFlutterRepo { + assert(fs.path.isAbsolute(Cache.flutterRoot)); + for (List targetSources in values.values) { + for (String source in targetSources) { + assert(fs.path.isAbsolute(source)); + if (fs.path.isWithin(Cache.flutterRoot, source)) + return true; + } + } + return false; + } + void describeConflict(StringBuffer result) { + assert(hasConflict); + final List targets = values.keys.toList(); + targets.sort((String a, String b) => values[b].length.compareTo(values[a].length)); + for (String target in targets) { + final int count = values[target].length; + result.writeln(' $count ${count == 1 ? 'source wants' : 'sources want'} "$target":'); + bool canonical = false; + for (String source in values[target]) { + result.writeln(' $source'); + if (source == canonicalSource) + canonical = true; + } + if (canonical) { + result.writeln(' (This is the actual package definition, so it is considered the canonical "right answer".)'); + } + } + } + String get target => values.keys.single; +} + +class PackageDependencyTracker { + /// Packages whose source is defined in the vended SDK. + static const List _vendedSdkPackages = const ['analyzer', 'front_end', 'kernel']; + + // This is a map from package names to objects that track the paths + // involved (sources and targets). + Map packages = {}; + + PackageDependency getPackageDependency(String packageName) { + return packages.putIfAbsent(packageName, () => new PackageDependency()); + } + + /// Read the .packages file in [directory] and add referenced packages to [dependencies]. + void addDependenciesFromPackagesFileIn(Directory directory) { + final String dotPackagesPath = fs.path.join(directory.path, '.packages'); + final File dotPackages = fs.file(dotPackagesPath); + if (dotPackages.existsSync()) { + // this directory has opinions about what we should be using + dotPackages + .readAsStringSync() + .split('\n') + .where((String line) => !line.startsWith(new RegExp(r'^ *#'))) + .forEach((String line) { + final int colon = line.indexOf(':'); + if (colon > 0) { + final String packageName = line.substring(0, colon); + final String packagePath = fs.path.fromUri(line.substring(colon+1)); + // Ensure that we only add `analyzer` and dependent packages defined in the vended SDK (and referred to with a local + // fs.path. directive). Analyzer package versions reached via transitive dependencies (e.g., via `test`) are ignored + // since they would produce spurious conflicts. + if (!_vendedSdkPackages.contains(packageName) || packagePath.startsWith('..')) + add(packageName, fs.path.normalize(fs.path.absolute(directory.path, packagePath)), dotPackagesPath); + } + }); + } + } + + void addCanonicalCase(String packageName, String packagePath, String pubSpecYamlPath) { + getPackageDependency(packageName).addCanonicalCase(packagePath, pubSpecYamlPath); + } + + void add(String packageName, String packagePath, String dotPackagesPath) { + getPackageDependency(packageName).add(packagePath, dotPackagesPath); + } + + void checkForConflictingDependencies(Iterable pubSpecDirectories, PackageDependencyTracker dependencies) { + for (Directory directory in pubSpecDirectories) { + final String pubSpecYamlPath = fs.path.join(directory.path, 'pubspec.yaml'); + final File pubSpecYamlFile = fs.file(pubSpecYamlPath); + if (pubSpecYamlFile.existsSync()) { + // we are analyzing the actual canonical source for this package; + // make sure we remember that, in case all the packages are actually + // pointing elsewhere somehow. + final yaml.YamlMap pubSpecYaml = yaml.loadYaml(fs.file(pubSpecYamlPath).readAsStringSync()); + final String packageName = pubSpecYaml['name']; + final String packagePath = fs.path.normalize(fs.path.absolute(fs.path.join(directory.path, 'lib'))); + dependencies.addCanonicalCase(packageName, packagePath, pubSpecYamlPath); + } + dependencies.addDependenciesFromPackagesFileIn(directory); + } + + // prepare a union of all the .packages files + if (dependencies.hasConflicts) { + final StringBuffer message = new StringBuffer(); + message.writeln(dependencies.generateConflictReport()); + message.writeln('Make sure you have run "pub upgrade" in all the directories mentioned above.'); + if (dependencies.hasConflictsAffectingFlutterRepo) { + message.writeln( + 'For packages in the flutter repository, try using ' + '"flutter update-packages --upgrade" to do all of them at once.'); + } + message.write( + 'If this does not help, to track down the conflict you can use ' + '"pub deps --style=list" and "pub upgrade --verbosity=solver" in the affected directories.'); + throwToolExit(message.toString()); + } + } + + bool get hasConflicts { + return packages.values.any((PackageDependency dependency) => dependency.hasConflict); + } + + bool get hasConflictsAffectingFlutterRepo { + return packages.values.any((PackageDependency dependency) => dependency.hasConflictAffectingFlutterRepo); + } + + String generateConflictReport() { + assert(hasConflicts); + final StringBuffer result = new StringBuffer(); + for (String package in packages.keys.where((String package) => packages[package].hasConflict)) { + result.writeln('Package "$package" has conflicts:'); + packages[package].describeConflict(result); + } + return result.toString(); + } + + Map asPackageMap() { + final Map result = {}; + for (String package in packages.keys) + result[package] = packages[package].target; + return result; + } +} diff --git a/packages/flutter_tools/lib/src/commands/analyze_continuously.dart b/packages/flutter_tools/lib/src/commands/analyze_continuously.dart index 8a170bf5d5728..dac4347a16d06 100644 --- a/packages/flutter_tools/lib/src/commands/analyze_continuously.dart +++ b/packages/flutter_tools/lib/src/commands/analyze_continuously.dart @@ -20,9 +20,9 @@ import '../globals.dart'; import 'analyze_base.dart'; class AnalyzeContinuously extends AnalyzeBase { - AnalyzeContinuously(ArgResults argResults, this.repoAnalysisEntryPoints) : super(argResults); + AnalyzeContinuously(ArgResults argResults, this.repoPackages) : super(argResults); - final List repoAnalysisEntryPoints; + final List repoPackages; String analysisTarget; bool firstAnalysis = true; @@ -40,7 +40,9 @@ class AnalyzeContinuously extends AnalyzeBase { throwToolExit('The --dartdocs option is currently not supported when using --watch.'); if (argResults['flutter-repo']) { - directories = repoAnalysisEntryPoints.map((Directory dir) => dir.path).toList(); + final PackageDependencyTracker dependencies = new PackageDependencyTracker(); + dependencies.checkForConflictingDependencies(repoPackages, dependencies); + directories = repoPackages.map((Directory dir) => dir.path).toList(); analysisTarget = 'Flutter repository'; printTrace('Analyzing Flutter repository:'); for (String projectPath in directories) diff --git a/packages/flutter_tools/lib/src/commands/analyze_once.dart b/packages/flutter_tools/lib/src/commands/analyze_once.dart index fe514479f1bb6..10342507576f7 100644 --- a/packages/flutter_tools/lib/src/commands/analyze_once.dart +++ b/packages/flutter_tools/lib/src/commands/analyze_once.dart @@ -6,7 +6,6 @@ import 'dart:async'; import 'dart:collection'; import 'package:args/args.dart'; -import 'package:yaml/yaml.dart' as yaml; import '../base/common.dart'; import '../base/file_system.dart'; @@ -133,56 +132,7 @@ class AnalyzeOnce extends AnalyzeBase { // determine what all the various .packages files depend on final PackageDependencyTracker dependencies = new PackageDependencyTracker(); - for (Directory directory in pubSpecDirectories) { - final String pubSpecYamlPath = fs.path.join(directory.path, 'pubspec.yaml'); - final File pubSpecYamlFile = fs.file(pubSpecYamlPath); - if (pubSpecYamlFile.existsSync()) { - // we are analyzing the actual canonical source for this package; - // make sure we remember that, in case all the packages are actually - // pointing elsewhere somehow. - final yaml.YamlMap pubSpecYaml = yaml.loadYaml(fs.file(pubSpecYamlPath).readAsStringSync()); - final String packageName = pubSpecYaml['name']; - final String packagePath = fs.path.normalize(fs.path.absolute(fs.path.join(directory.path, 'lib'))); - dependencies.addCanonicalCase(packageName, packagePath, pubSpecYamlPath); - } - final String dotPackagesPath = fs.path.join(directory.path, '.packages'); - final File dotPackages = fs.file(dotPackagesPath); - if (dotPackages.existsSync()) { - // this directory has opinions about what we should be using - dotPackages - .readAsStringSync() - .split('\n') - .where((String line) => !line.startsWith(new RegExp(r'^ *#'))) - .forEach((String line) { - final int colon = line.indexOf(':'); - if (colon > 0) { - final String packageName = line.substring(0, colon); - final String packagePath = fs.path.fromUri(line.substring(colon+1)); - // Ensure that we only add the `analyzer` package defined in the vended SDK (and referred to with a local fs.path. directive). - // Analyzer package versions reached via transitive dependencies (e.g., via `test`) are ignored since they would produce - // spurious conflicts. - if (packageName != 'analyzer' || packagePath.startsWith('..')) - dependencies.add(packageName, fs.path.normalize(fs.path.absolute(directory.path, packagePath)), dotPackagesPath); - } - }); - } - } - - // prepare a union of all the .packages files - if (dependencies.hasConflicts) { - final StringBuffer message = new StringBuffer(); - message.writeln(dependencies.generateConflictReport()); - message.writeln('Make sure you have run "pub upgrade" in all the directories mentioned above.'); - if (dependencies.hasConflictsAffectingFlutterRepo) { - message.writeln( - 'For packages in the flutter repository, try using ' - '"flutter update-packages --upgrade" to do all of them at once.'); - } - message.write( - 'If this does not help, to track down the conflict you can use ' - '"pub deps --style=list" and "pub upgrade --verbosity=solver" in the affected directories.'); - throwToolExit(message.toString()); - } + dependencies.checkForConflictingDependencies(pubSpecDirectories, dependencies); final Map packages = dependencies.asPackageMap(); Cache.releaseLockEarly(); @@ -332,92 +282,3 @@ class AnalyzeOnce extends AnalyzeBase { return collected; } } - -class PackageDependency { - // This is a map from dependency targets (lib directories) to a list - // of places that ask for that target (.packages or pubspec.yaml files) - Map> values = >{}; - String canonicalSource; - void addCanonicalCase(String packagePath, String pubSpecYamlPath) { - assert(canonicalSource == null); - add(packagePath, pubSpecYamlPath); - canonicalSource = pubSpecYamlPath; - } - void add(String packagePath, String sourcePath) { - values.putIfAbsent(packagePath, () => []).add(sourcePath); - } - bool get hasConflict => values.length > 1; - bool get hasConflictAffectingFlutterRepo { - assert(fs.path.isAbsolute(Cache.flutterRoot)); - for (List targetSources in values.values) { - for (String source in targetSources) { - assert(fs.path.isAbsolute(source)); - if (fs.path.isWithin(Cache.flutterRoot, source)) - return true; - } - } - return false; - } - void describeConflict(StringBuffer result) { - assert(hasConflict); - final List targets = values.keys.toList(); - targets.sort((String a, String b) => values[b].length.compareTo(values[a].length)); - for (String target in targets) { - final int count = values[target].length; - result.writeln(' $count ${count == 1 ? 'source wants' : 'sources want'} "$target":'); - bool canonical = false; - for (String source in values[target]) { - result.writeln(' $source'); - if (source == canonicalSource) - canonical = true; - } - if (canonical) { - result.writeln(' (This is the actual package definition, so it is considered the canonical "right answer".)'); - } - } - } - String get target => values.keys.single; -} - -class PackageDependencyTracker { - // This is a map from package names to objects that track the paths - // involved (sources and targets). - Map packages = {}; - - PackageDependency getPackageDependency(String packageName) { - return packages.putIfAbsent(packageName, () => new PackageDependency()); - } - - void addCanonicalCase(String packageName, String packagePath, String pubSpecYamlPath) { - getPackageDependency(packageName).addCanonicalCase(packagePath, pubSpecYamlPath); - } - - void add(String packageName, String packagePath, String dotPackagesPath) { - getPackageDependency(packageName).add(packagePath, dotPackagesPath); - } - - bool get hasConflicts { - return packages.values.any((PackageDependency dependency) => dependency.hasConflict); - } - - bool get hasConflictsAffectingFlutterRepo { - return packages.values.any((PackageDependency dependency) => dependency.hasConflictAffectingFlutterRepo); - } - - String generateConflictReport() { - assert(hasConflicts); - final StringBuffer result = new StringBuffer(); - for (String package in packages.keys.where((String package) => packages[package].hasConflict)) { - result.writeln('Package "$package" has conflicts:'); - packages[package].describeConflict(result); - } - return result.toString(); - } - - Map asPackageMap() { - final Map result = {}; - for (String package in packages.keys) - result[package] = packages[package].target; - return result; - } -} diff --git a/packages/flutter_tools/lib/src/commands/build_aot.dart b/packages/flutter_tools/lib/src/commands/build_aot.dart index 10f79bb9cc4bc..22e0b2ca23985 100644 --- a/packages/flutter_tools/lib/src/commands/build_aot.dart +++ b/packages/flutter_tools/lib/src/commands/build_aot.dart @@ -82,7 +82,7 @@ String _getPackagePath(PackageMap packageMap, String package) { return fs.path.dirname(packageMap.map[package].toFilePath()); } -/// Build an AOT snapshot. Return `null` (and log to `printError`) if the method +/// Build an AOT snapshot. Return null (and log to `printError`) if the method /// fails. Future buildAotSnapshot( String mainPath, diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart index 93792eca58419..eb72d4ae01ef4 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios.dart @@ -45,7 +45,7 @@ class BuildIOSCommand extends BuildSubCommand { if (getCurrentHostPlatform() != HostPlatform.darwin_x64) throwToolExit('Building for iOS is only supported on the Mac.'); - final IOSApp app = applicationPackages.getPackageForPlatform(TargetPlatform.ios); + final BuildableIOSApp app = applicationPackages.getPackageForPlatform(TargetPlatform.ios); if (app == null) throwToolExit('Application not configured for iOS'); @@ -73,7 +73,7 @@ class BuildIOSCommand extends BuildSubCommand { ); if (!result.success) { - await diagnoseXcodeBuildFailure(result); + await diagnoseXcodeBuildFailure(result, app); throwToolExit('Encountered error while building for $logTarget.'); } diff --git a/packages/flutter_tools/lib/src/commands/config.dart b/packages/flutter_tools/lib/src/commands/config.dart index 11f7e078aaa68..16c06291930df 100644 --- a/packages/flutter_tools/lib/src/commands/config.dart +++ b/packages/flutter_tools/lib/src/commands/config.dart @@ -42,7 +42,7 @@ class ConfigCommand extends FlutterCommand { 'Analytics reporting is currently ${flutterUsage.enabled ? 'enabled' : 'disabled'}.'; } - /// Return `null` to disable tracking of the `config` command. + /// Return null to disable tracking of the `config` command. @override Future get usagePath => null; diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart index 34b39153dd86a..cb60d498f3cf9 100644 --- a/packages/flutter_tools/lib/src/commands/create.dart +++ b/packages/flutter_tools/lib/src/commands/create.dart @@ -275,7 +275,7 @@ To edit platform code in an IDE see https://flutter.io/platform-plugins/#edit-co } String _createAndroidIdentifier(String organization, String name) { - return '$organization.$name'; + return '$organization.$name'.replaceAll('_', ''); } String _createPluginClassName(String name) { @@ -311,7 +311,7 @@ final Set _packageDependencies = new Set.from([ 'yaml' ]); -/// Return `null` if the project name is legal. Return a validation message if +/// Return null if the project name is legal. Return a validation message if /// we should disallow the project name. String _validateProjectName(String projectName) { if (!package_names.isValidPackageName(projectName)) @@ -324,7 +324,7 @@ String _validateProjectName(String projectName) { return null; } -/// Return `null` if the project directory is legal. Return a validation message +/// Return null if the project directory is legal. Return a validation message /// if we should disallow the directory name. String _validateProjectDir(String dirPath, { String flutterRoot }) { if (fs.path.isWithin(flutterRoot, dirPath)) { diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 78d5a7dbb6d78..8c39078f9b596 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -759,7 +759,7 @@ class AppInstance { } dynamic _runInZone(AppDomain domain, dynamic method()) { - _logger ??= new _AppRunLogger(domain, this, logToStdout: logToStdout); + _logger ??= new _AppRunLogger(domain, this, parent: logToStdout ? logger : null); final AppContext appContext = new AppContext(); appContext.setVariable(Logger, _logger); @@ -768,20 +768,25 @@ class AppInstance { } /// A [Logger] which sends log messages to a listening daemon client. +/// +/// This class can either: +/// 1) Send stdout messages and progress events to the client IDE +/// 1) Log messages to stdout and send progress events to the client IDE +/// +/// TODO(devoncarew): To simplify this code a bit, we could choose to specialize +/// this class into two, one for each of the above use cases. class _AppRunLogger extends Logger { - _AppRunLogger(this.domain, this.app, { this.logToStdout: false }); + _AppRunLogger(this.domain, this.app, { this.parent }); AppDomain domain; final AppInstance app; - final bool logToStdout; + final Logger parent; int _nextProgressId = 0; @override void printError(String message, { StackTrace stackTrace, bool emphasis: false }) { - if (logToStdout) { - stderr.writeln(message); - if (stackTrace != null) - stderr.writeln(stackTrace.toString().trimRight()); + if (parent != null) { + parent.printError(message, stackTrace: stackTrace, emphasis: emphasis); } else { if (stackTrace != null) { _sendLogEvent({ @@ -800,18 +805,25 @@ class _AppRunLogger extends Logger { @override void printStatus( - String message, - { bool emphasis: false, bool newline: true, String ansiAlternative, int indent } - ) { - if (logToStdout) { - print(message); + String message, { + bool emphasis: false, bool newline: true, String ansiAlternative, int indent + }) { + if (parent != null) { + parent.printStatus(message, emphasis: emphasis, newline: newline, + ansiAlternative: ansiAlternative, indent: indent); } else { _sendLogEvent({ 'log': message }); } } @override - void printTrace(String message) { } + void printTrace(String message) { + if (parent != null) { + parent.printTrace(message); + } else { + _sendLogEvent({ 'log': message, 'trace': true }); + } + } Status _status; diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index b09fbe6735259..7f84958f3a0fa 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -88,6 +88,13 @@ class RunCommand extends RunCommandBase { defaultsTo: false, negatable: false, help: 'Start in a paused mode and wait for a debugger to connect.'); + argParser.addFlag('enable-software-rendering', + defaultsTo: false, + negatable: false, + help: 'Enable rendering using the Skia software backend. This is useful\n' + 'when testing Flutter on emulators. By default, Flutter will\n' + 'attempt to either use OpenGL or Vulkan and fall back to software\n' + 'when neither is available.'); argParser.addFlag('use-test-fonts', negatable: true, defaultsTo: false, @@ -218,6 +225,7 @@ class RunCommand extends RunCommandBase { getBuildMode(), startPaused: argResults['start-paused'], useTestFonts: argResults['use-test-fonts'], + enableSoftwareRendering: argResults['enable-software-rendering'], observatoryPort: observatoryPort, diagnosticPort: diagnosticPort, ); diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index e3285010a7bf8..6171abcc88b97 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -4,9 +4,6 @@ import 'dart:async'; -import 'package:test/src/executable.dart' as test; // ignore: implementation_imports - -import '../artifacts.dart'; import '../base/common.dart'; import '../base/file_system.dart'; import '../base/io.dart'; @@ -14,16 +11,16 @@ import '../base/logger.dart'; import '../base/os.dart'; import '../base/platform.dart'; import '../base/process_manager.dart'; -import '../base/terminal.dart'; import '../cache.dart'; -import '../dart/package_map.dart'; import '../globals.dart'; import '../runner/flutter_command.dart'; import '../test/coverage_collector.dart'; -import '../test/flutter_platform.dart' as loader; +import '../test/event_printer.dart'; +import '../test/runner.dart'; +import '../test/watcher.dart'; class TestCommand extends FlutterCommand { - TestCommand() { + TestCommand({ bool verboseHelp: false }) { usesPubOption(); argParser.addFlag('start-paused', defaultsTo: false, @@ -53,6 +50,11 @@ class TestCommand extends FlutterCommand { defaultsTo: 'coverage/lcov.info', help: 'Where to store coverage information (if coverage is enabled).' ); + argParser.addFlag('machine', + hide: !verboseHelp, + negatable: false, + help: 'Handle machine structured JSON command input\n' + 'and provide output and progress in machine friendly format.'); commandValidator = () { if (!fs.isFileSync('pubspec.yaml')) { throwToolExit( @@ -70,37 +72,12 @@ class TestCommand extends FlutterCommand { @override String get description => 'Run Flutter unit tests for the current project.'; - Iterable _findTests(Directory directory) { - return directory.listSync(recursive: true, followLinks: false) - .where((FileSystemEntity entity) => entity.path.endsWith('_test.dart') && - fs.isFileSync(entity.path)) - .map((FileSystemEntity entity) => fs.path.absolute(entity.path)); - } - Directory get _currentPackageTestDir { // We don't scan the entire package, only the test/ subdirectory, so that // files with names like like "hit_test.dart" don't get run. return fs.directory('test'); } - Future _runTests(List testArgs, Directory testDirectory) async { - final Directory currentDirectory = fs.currentDirectory; - try { - if (testDirectory != null) { - printTrace('switching to directory $testDirectory to run tests'); - PackageMap.globalPackagesPath = fs.path.normalize(fs.path.absolute(PackageMap.globalPackagesPath)); - fs.currentDirectory = testDirectory; - } - printTrace('running test package with arguments: $testArgs'); - await test.main(testArgs); - // test.main() sets dart:io's exitCode global. - printTrace('test package returned with exit code $exitCode'); - return exitCode; - } finally { - fs.currentDirectory = currentDirectory; - } - } - Future _collectCoverageData(CoverageCollector collector, { bool mergeCoverageData: false }) async { final Status status = logger.startProgress('Collecting coverage information...'); final String coverageData = await collector.finalizeCoverage( @@ -161,7 +138,7 @@ class TestCommand extends FlutterCommand { } @override - Future runCommand() async { + Future runCommand() async { if (platform.isWindows) { throwToolExit( 'The test command is currently not supported on Windows: ' @@ -169,57 +146,57 @@ class TestCommand extends FlutterCommand { ); } - final List testArgs = []; - commandValidator(); - if (!terminal.supportsColor) - testArgs.addAll(['--no-color', '-rexpanded']); + Iterable files = argResults.rest.map((String testPath) => fs.path.absolute(testPath)).toList(); - CoverageCollector collector; - if (argResults['coverage'] || argResults['merge-coverage']) { - collector = new CoverageCollector(); - testArgs.add('--concurrency=1'); + final bool startPaused = argResults['start-paused']; + if (startPaused && files.length != 1) { + throwToolExit( + 'When using --start-paused, you must specify a single test file to run.', + exitCode: 1); } - testArgs.add('--'); - - Directory testDir; - Iterable files = argResults.rest.map((String testPath) => fs.path.absolute(testPath)).toList(); - if (argResults['start-paused']) { - if (files.length != 1) - throwToolExit('When using --start-paused, you must specify a single test file to run.', exitCode: 1); - } else if (files.isEmpty) { - testDir = _currentPackageTestDir; - if (!testDir.existsSync()) - throwToolExit('Test directory "${testDir.path}" not found.'); - files = _findTests(testDir); + Directory workDir; + if (files.isEmpty) { + workDir = _currentPackageTestDir; + if (!workDir.existsSync()) + throwToolExit('Test directory "${workDir.path}" not found.'); + files = _findTests(workDir); if (files.isEmpty) { throwToolExit( - 'Test directory "${testDir.path}" does not appear to contain any test files.\n' - 'Test files must be in that directory and end with the pattern "_test.dart".' + 'Test directory "${workDir.path}" does not appear to contain any test files.\n' + 'Test files must be in that directory and end with the pattern "_test.dart".' ); } } - testArgs.addAll(files); - - final InternetAddressType serverType = argResults['ipv6'] - ? InternetAddressType.IP_V6 - : InternetAddressType.IP_V4; - - final String shellPath = artifacts.getArtifactPath(Artifact.flutterTester); - if (!fs.isFileSync(shellPath)) - throwToolExit('Cannot find Flutter shell at $shellPath'); - loader.installHook( - shellPath: shellPath, - collector: collector, - debuggerMode: argResults['start-paused'], - serverType: serverType, - ); + + CoverageCollector collector; + if (argResults['coverage'] || argResults['merge-coverage']) { + collector = new CoverageCollector(); + } + + final bool wantEvents = argResults['machine']; + if (collector != null && wantEvents) { + throwToolExit( + "The test command doesn't support --machine and coverage together"); + } + + TestWatcher watcher; + if (collector != null) { + watcher = collector; + } else if (wantEvents) { + watcher = new EventPrinter(); + } Cache.releaseLockEarly(); - final int result = await _runTests(testArgs, testDir); + final int result = await runTests(files, + workDir: workDir, + watcher: watcher, + enableObservatory: collector != null || startPaused, + startPaused: startPaused, + ipv6: argResults['ipv6']); if (collector != null) { if (!await _collectCoverageData(collector, mergeCoverageData: argResults['merge-coverage'])) @@ -228,5 +205,13 @@ class TestCommand extends FlutterCommand { if (result != 0) throwToolExit(null); + return const FlutterCommandResult(ExitStatus.success); } } + +Iterable _findTests(Directory directory) { + return directory.listSync(recursive: true, followLinks: false) + .where((FileSystemEntity entity) => entity.path.endsWith('_test.dart') && + fs.isFileSync(entity.path)) + .map((FileSystemEntity entity) => fs.path.absolute(entity.path)); +} diff --git a/packages/flutter_tools/lib/src/dart/analysis.dart b/packages/flutter_tools/lib/src/dart/analysis.dart index cd20c4546c302..a41dfe4d5e6b7 100644 --- a/packages/flutter_tools/lib/src/dart/analysis.dart +++ b/packages/flutter_tools/lib/src/dart/analysis.dart @@ -136,7 +136,7 @@ class AnalysisDriver { bool _isFiltered(AnalysisError error) { final ErrorProcessor processor = ErrorProcessor.getProcessor(context.analysisOptions, error); - // Filtered errors are processed to a severity of `null`. + // Filtered errors are processed to a severity of null. return processor != null && processor.severity == null; } diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart index 8efcc8c40b14a..2657e9254201e 100644 --- a/packages/flutter_tools/lib/src/devfs.dart +++ b/packages/flutter_tools/lib/src/devfs.dart @@ -622,11 +622,15 @@ class DevFS { final String packagePath = fs.path.fromUri(packageUri); final Directory packageDirectory = fs.directory(packageUri); Uri directoryUriOnDevice = fs.path.toUri(fs.path.join('packages', packageName) + fs.path.separator); - bool packageExists; + bool packageExists = packageDirectory.existsSync(); + + if (!packageExists) { + // If the package directory doesn't exist at all, we ignore it. + continue; + } if (fs.path.isWithin(rootDirectory.path, packagePath)) { // We already scanned everything under the root directory. - packageExists = packageDirectory.existsSync(); directoryUriOnDevice = fs.path.toUri( fs.path.relative(packagePath, from: rootDirectory.path) + fs.path.separator ); diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 68988066a4ad8..de3f873363797 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -299,6 +299,7 @@ abstract class Device { class DebuggingOptions { DebuggingOptions.enabled(this.buildMode, { this.startPaused: false, + this.enableSoftwareRendering: false, this.useTestFonts: false, this.observatoryPort, this.diagnosticPort @@ -308,6 +309,7 @@ class DebuggingOptions { debuggingEnabled = false, useTestFonts = false, startPaused = false, + enableSoftwareRendering = false, observatoryPort = null, diagnosticPort = null; @@ -315,6 +317,7 @@ class DebuggingOptions { final BuildMode buildMode; final bool startPaused; + final bool enableSoftwareRendering; final bool useTestFonts; final int observatoryPort; final int diagnosticPort; diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart index 2805ee67aca91..d01e42f8b4288 100644 --- a/packages/flutter_tools/lib/src/doctor.dart +++ b/packages/flutter_tools/lib/src/doctor.dart @@ -16,6 +16,7 @@ import 'base/file_system.dart'; import 'base/os.dart'; import 'base/platform.dart'; import 'base/process_manager.dart'; +import 'base/version.dart'; import 'cache.dart'; import 'device.dart'; import 'globals.dart'; @@ -209,6 +210,10 @@ class _FlutterValidator extends DoctorValidator { final FlutterVersion version = FlutterVersion.instance; messages.add(new ValidationMessage('Flutter at ${Cache.flutterRoot}')); + if (Cache.flutterRoot.contains(' ')) + messages.add(new ValidationMessage.error( + 'Flutter SDK install paths with spaces are not yet supported. (https://github.com/flutter/flutter/issues/6577)\n' + 'Please move the SDK to a path that does not include spaces.')); messages.add(new ValidationMessage( 'Framework revision ${version.frameworkRevisionShort} ' '(${version.frameworkAge}), ${version.frameworkDate}' @@ -261,6 +266,10 @@ abstract class IntelliJValidator extends DoctorValidator { 'WebStorm': 'WebStorm', }; + static final Version kMinIdeaVersion = new Version(2017, 1, 0); + static final Version kMinWebStormVersion = new Version(2017, 1, 0); + static final Version kMinFlutterPluginVersion = new Version(14, 0, 0); + static Iterable get installedValidators { if (platform.isLinux || platform.isWindows) return IntelliJValidatorOnLinuxAndWindows.installed; @@ -273,46 +282,72 @@ abstract class IntelliJValidator extends DoctorValidator { Future validate() async { final List messages = []; - int installCount = 0; + _validatePackage(messages, 'flutter-intellij.jar', 'Flutter', + minVersion: kMinFlutterPluginVersion); - if (isWebStorm) { - // Dart is bundled with WebStorm. - installCount++; - } else { - if (_validateHasPackage(messages, 'Dart', 'Dart')) - installCount++; + // Dart is bundled with WebStorm. + if (!isWebStorm) { + _validatePackage(messages, 'Dart', 'Dart'); } - if (_validateHasPackage(messages, 'flutter-intellij.jar', 'Flutter')) - installCount++; - - if (installCount < 2) { + if (_hasIssues(messages)) { messages.add(new ValidationMessage( - 'For information about managing plugins, see\n' - 'https://www.jetbrains.com/help/idea/managing-plugins.html' + 'For information about managing plugins, see\n' + 'https://www.jetbrains.com/help/idea/managing-plugins.html' )); } + _validateIntelliJVersion(messages, isWebStorm ? kMinWebStormVersion : kMinIdeaVersion); + return new ValidationResult( - installCount == 2 ? ValidationType.installed : ValidationType.partial, - messages, - statusInfo: 'version $version' + _hasIssues(messages) ? ValidationType.partial : ValidationType.installed, + messages, + statusInfo: 'version $version' ); } + bool _hasIssues(List messages) { + return messages.any((ValidationMessage message) => message.isError); + } + bool get isWebStorm => title == 'WebStorm'; - bool _validateHasPackage(List messages, String packageName, String title) { + void _validateIntelliJVersion(List messages, Version minVersion) { + // Ignore unknown versions. + if (minVersion == Version.unknown) + return; + + final Version installedVersion = new Version.parse(version); + if (installedVersion == null) + return; + + if (installedVersion < minVersion) { + messages.add(new ValidationMessage.error( + 'This install is older than the minimum recommended version of $minVersion.' + )); + } + } + + void _validatePackage(List messages, String packageName, String title, { + Version minVersion + }) { if (!hasPackage(packageName)) { - messages.add(new ValidationMessage( + messages.add(new ValidationMessage.error( '$title plugin not installed; this adds $title specific functionality.' )); - return false; + return; + } + final String versionText = _readPackageVersion(packageName); + final Version version = new Version.parse(versionText); + if (version != null && minVersion != null && version < minVersion) { + messages.add(new ValidationMessage.error( + '$title plugin version $versionText - the recommended minimum version is $minVersion' + )); + } else { + messages.add(new ValidationMessage( + '$title plugin ${version != null ? "version $version" : "installed"}' + )); } - final String version = _readPackageVersion(packageName); - messages.add(new ValidationMessage('$title plugin ' - '${version != null ? "version $version" : "installed"}')); - return true; } String _readPackageVersion(String packageName) { diff --git a/packages/flutter_tools/lib/src/ios/code_signing.dart b/packages/flutter_tools/lib/src/ios/code_signing.dart index 5a917dba522e1..169332dc04628 100644 --- a/packages/flutter_tools/lib/src/ios/code_signing.dart +++ b/packages/flutter_tools/lib/src/ios/code_signing.dart @@ -14,34 +14,60 @@ import '../base/process.dart'; import '../base/terminal.dart'; import '../globals.dart'; +/// User message when no development certificates are found in the keychain. +/// +/// The user likely never did any iOS development. const String noCertificatesInstruction = ''' ═══════════════════════════════════════════════════════════════════════════════════ No valid code signing certificates were found -Please ensure that you have a valid Development Team with valid iOS Development Certificates -associated with your Apple ID by: - 1- Opening the Xcode application - 2- Go to Xcode->Preferences->Accounts - 3- Make sure that you're signed in with your Apple ID via the '+' button on the bottom left - 4- Make sure that you have development certificates available by signing up to Apple - Developer Program and/or downloading available profiles as needed. +You can connect to your Apple Developer account by signing in with your Apple ID in Xcode +and create an iOS Development Certificate as well as a Provisioning Profile for your project by: +$fixWithDevelopmentTeamInstruction + 5- Trust your newly created Development Certificate on your iOS device + via Settings > General > Device Management > [your new certificate] > Trust + For more information, please visit: https://developer.apple.com/library/content/documentation/IDEs/Conceptual/AppDistributionGuide/MaintainingCertificates/MaintainingCertificates.html Or run on an iOS simulator without code signing ═══════════════════════════════════════════════════════════════════════════════════'''; +/// User message when there are no provisioning profile for the current app bundle identifier. +/// +/// The user did iOS development but never on this project and/or device. +const String noProvisioningProfileInstruction = ''' +═══════════════════════════════════════════════════════════════════════════════════ +No Provisioning Profile was found for your project's Bundle Identifier or your device. +You can create a new Provisioning Profile for your project in Xcode for your +team by: +$fixWithDevelopmentTeamInstruction + +For more information, please visit: + https://flutter.io/setup/#deploy-to-ios-devices + +Or run on an iOS simulator without code signing +═══════════════════════════════════════════════════════════════════════════════════'''; +/// Fallback error message for signing issues. +/// +/// Couldn't auto sign the app but can likely solved by retracing the signing flow in Xcode. const String noDevelopmentTeamInstruction = ''' ═══════════════════════════════════════════════════════════════════════════════════ Building a deployable iOS app requires a selected Development Team with a Provisioning Profile Please ensure that a Development Team is selected by: - 1- Opening the Flutter project's Xcode target with +$fixWithDevelopmentTeamInstruction + +For more information, please visit: + https://flutter.io/setup/#deploy-to-ios-devices + +Or run on an iOS simulator without code signing +═══════════════════════════════════════════════════════════════════════════════════'''; +const String fixWithDevelopmentTeamInstruction = ''' + 1- Open the Flutter project's Xcode target with open ios/Runner.xcworkspace 2- Select the 'Runner' project in the navigator then the 'Runner' target in the project settings - 3- In the 'General' tab, make sure a 'Development Team' is selected\n -For more information, please visit: - https://flutter.io/setup/#deploy-to-ios-devices\n -Or run on an iOS simulator -═══════════════════════════════════════════════════════════════════════════════════'''; + 3- In the 'General' tab, make sure a 'Development Team' is selected. You may need to add + your Apple ID first. + 4- Build or run your project again'''; final RegExp _securityFindIdentityDeveloperIdentityExtractionPattern = new RegExp(r'^\s*\d+\).+"(.+Developer.+)"$'); diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index efceac65547de..396266fb231d4 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -96,20 +96,20 @@ class IOSDevice extends Device { @override bool get supportsStartPaused => false; - static List getAttachedDevices([IOSDevice mockIOS]) { + static List getAttachedDevices() { if (!doctor.iosWorkflow.hasIDeviceId) return []; final List devices = []; - for (String id in _getAttachedDeviceIDs(mockIOS)) { - final String name = IOSDevice._getDeviceInfo(id, 'DeviceName', mockIOS); + for (String id in _getAttachedDeviceIDs()) { + final String name = IOSDevice._getDeviceInfo(id, 'DeviceName'); devices.add(new IOSDevice(id, name: name)); } return devices; } - static Iterable _getAttachedDeviceIDs([IOSDevice mockIOS]) { - final String listerPath = (mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id'); + static Iterable _getAttachedDeviceIDs() { + final String listerPath = _checkForCommand('idevice_id'); try { final String output = runSync([listerPath, '-l']); return output.trim().split('\n').where((String s) => s != null && s.isNotEmpty); @@ -118,10 +118,8 @@ class IOSDevice extends Device { } } - static String _getDeviceInfo(String deviceID, String infoKey, [IOSDevice mockIOS]) { - final String informerPath = (mockIOS != null) - ? mockIOS.informerPath - : _checkForCommand('ideviceinfo'); + static String _getDeviceInfo(String deviceID, String infoKey) { + final String informerPath = _checkForCommand('ideviceinfo'); return runSync([informerPath, '-k', infoKey, '-u', deviceID]).trim(); } @@ -208,7 +206,7 @@ class IOSDevice extends Device { final XcodeBuildResult buildResult = await buildXcodeProject(app: app, mode: mode, target: mainPath, buildForDevice: true); if (!buildResult.success) { printError('Could not build the precompiled application for the device.'); - await diagnoseXcodeBuildFailure(buildResult); + await diagnoseXcodeBuildFailure(buildResult, app); printError(''); return new LaunchResult.failed(); } @@ -242,6 +240,9 @@ class IOSDevice extends Device { // the port picked and scrape that later. } + if (debuggingOptions.enableSoftwareRendering) + launchArguments.add('--enable-software-rendering'); + if (platformArgs['trace-startup'] ?? false) launchArguments.add('--trace-startup'); diff --git a/packages/flutter_tools/lib/src/ios/ios_workflow.dart b/packages/flutter_tools/lib/src/ios/ios_workflow.dart index 4fe9d24d3fade..696048b221c86 100644 --- a/packages/flutter_tools/lib/src/ios/ios_workflow.dart +++ b/packages/flutter_tools/lib/src/ios/ios_workflow.dart @@ -132,7 +132,9 @@ class IOSWorkflow extends DoctorValidator implements Workflow { messages.add(new ValidationMessage.error( 'Xcode installation is incomplete; a full installation is necessary for iOS development.\n' 'Download at: https://developer.apple.com/xcode/download/\n' - 'Once installed, run \'sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer\'.' + 'Or install Xcode via the App Store.\n' + 'Once installed, run:\n' + ' sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer' )); } } @@ -153,10 +155,10 @@ class IOSWorkflow extends DoctorValidator implements Workflow { brewStatus = ValidationType.partial; messages.add(new ValidationMessage.error( 'libimobiledevice and ideviceinstaller are not installed or require updating. To update, run:\n' - ' brew update\n' - ' brew uninstall --ignore-dependencies libimobiledevice\n' - ' brew install --HEAD libimobiledevice\n' - ' brew install ideviceinstaller' + ' brew update\n' + ' brew uninstall --ignore-dependencies libimobiledevice\n' + ' brew install --HEAD libimobiledevice\n' + ' brew install ideviceinstaller' )); } else if (!await hasIDeviceInstaller) { brewStatus = ValidationType.partial; diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 84f6aabfc34d6..16430aa207fab 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -147,7 +147,7 @@ Future buildXcodeProject({ } String developmentTeam; - if (codesign && mode != BuildMode.release && buildForDevice) + if (codesign && buildForDevice) developmentTeam = await getCodeSigningIdentityDevelopmentTeam(app); // Before the build, all service definitions must be updated and the dylibs @@ -246,51 +246,40 @@ Future buildXcodeProject({ } } -Future diagnoseXcodeBuildFailure(XcodeBuildResult result) async { - final File plistFile = fs.file('ios/Runner/Info.plist'); - if (plistFile.existsSync()) { - final String plistContent = plistFile.readAsStringSync(); - if (plistContent.contains('com.yourcompany')) { - printError(''); - printError('It appears that your application still contains the default signing identifier.'); - printError("Try replacing 'com.yourcompany' with your signing id"); - printError('in ${plistFile.absolute.path}'); - return; - } +Future diagnoseXcodeBuildFailure(XcodeBuildResult result, BuildableIOSApp app) async { + if (result.xcodeBuildExecution != null && + result.xcodeBuildExecution.buildForPhysicalDevice && + result.stdout?.contains('BCEROR') == true && + // May need updating if Xcode changes its outputs. + result.stdout?.contains('Xcode couldn\'t find a provisioning profile matching') == true) { + printError(noProvisioningProfileInstruction, emphasis: true); + return; + } + if (result.xcodeBuildExecution != null && + result.xcodeBuildExecution.buildForPhysicalDevice && + // Make sure the user has specified at least the DEVELOPMENT_TEAM (for automatic Xcode 8) + // signing or the PROVISIONING_PROFILE (for manual signing or Xcode 7). + !(app.buildSettings?.containsKey('DEVELOPMENT_TEAM')) == true || app.buildSettings?.containsKey('PROVISIONING_PROFILE') == true) { + printError(noDevelopmentTeamInstruction, emphasis: true); + return; + } + if (app.id?.contains('com.yourcompany') ?? false) { + printError(''); + printError('It appears that your application still contains the default signing identifier.'); + printError("Try replacing 'com.yourcompany' with your signing id in Xcode:"); + printError(' open ios/Runner.xcworkspace'); + return; } if (result.stdout?.contains('Code Sign error') == true) { printError(''); printError('It appears that there was a problem signing your application prior to installation on the device.'); printError(''); - if (plistFile.existsSync()) { - printError('Verify that the CFBundleIdentifier in the Info.plist file is your signing id'); - printError(' ${plistFile.absolute.path}'); - printError(''); - } - printError("Try launching Xcode and selecting 'Product > Build' to fix the problem:"); - printError(" open ios/Runner.xcworkspace"); + printError('Verify that the Bundle Identifier in your project is your signing id in Xcode'); + printError(' open ios/Runner.xcworkspace'); + printError(''); + printError("Also try selecting 'Product > Build' to fix the problem:"); return; } - if (result.xcodeBuildExecution != null) { - assert(result.xcodeBuildExecution.buildForPhysicalDevice != null); - assert(result.xcodeBuildExecution.buildCommands != null); - assert(result.xcodeBuildExecution.appDirectory != null); - if (result.xcodeBuildExecution.buildForPhysicalDevice && - result.xcodeBuildExecution.buildCommands.contains('build')) { - final RunResult checkBuildSettings = await runAsync( - result.xcodeBuildExecution.buildCommands..add('-showBuildSettings'), - workingDirectory: result.xcodeBuildExecution.appDirectory, - allowReentrantFlutter: true - ); - // Make sure the user has specified at least the DEVELOPMENT_TEAM (for automatic Xcode 8) - // signing or the PROVISIONING_PROFILE (for manual signing or Xcode 7). - if (checkBuildSettings.exitCode == 0 && - !checkBuildSettings.stdout?.contains(new RegExp(r'\bDEVELOPMENT_TEAM\b')) == true && - !checkBuildSettings.stdout?.contains(new RegExp(r'\bPROVISIONING_PROFILE\b')) == true) { - printError(noDevelopmentTeamInstruction, emphasis: true); - } - } - } } class XcodeBuildResult { diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index eb6984beba1d4..28b227ed17cb8 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -134,7 +134,7 @@ abstract class FlutterCommand extends Command { applicationPackages ??= new ApplicationPackageStore(); } - /// The path to send to Google Analytics. Return `null` here to disable + /// The path to send to Google Analytics. Return null here to disable /// tracking of the command. Future get usagePath async => name; @@ -218,7 +218,7 @@ abstract class FlutterCommand extends Command { /// Find and return all target [Device]s based upon currently connected /// devices and criteria entered by the user on the command line. /// If no device can be found that meets specified criteria, - /// then print an error message and return `null`. + /// then print an error message and return null. Future> findAllTargetDevices() async { if (!doctor.canLaunchAnything) { printError("Unable to locate a development device; please run 'flutter doctor' " @@ -264,7 +264,7 @@ abstract class FlutterCommand extends Command { /// Find and return the target [Device] based upon currently connected /// devices and criteria entered by the user on the command line. /// If a device cannot be found that meets specified criteria, - /// then print an error message and return `null`. + /// then print an error message and return null. Future findTargetDevice() async { List deviceList = await findAllTargetDevices(); if (deviceList == null) diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart index 605451f73614c..408db71b15e48 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:args/args.dart'; import 'package:args/command_runner.dart'; @@ -59,6 +60,9 @@ class FlutterCommandRunner extends CommandRunner { argParser.addFlag('version', negatable: false, help: 'Reports the version of this tool.'); + argParser.addFlag('machine', + negatable: false, + hide: true); argParser.addFlag('color', negatable: true, hide: !verboseHelp, @@ -255,10 +259,21 @@ class FlutterCommandRunner extends CommandRunner { if (globalResults['version']) { flutterUsage.sendCommand('version'); - printStatus(FlutterVersion.instance.toString()); + String status; + if (globalResults['machine']) { + status = const JsonEncoder.withIndent(' ').convert(FlutterVersion.instance.toJson()); + } else { + status = FlutterVersion.instance.toString(); + } + printStatus(status); return; } + if (globalResults['machine']) { + printError('The --machine flag is only valid with the --version flag.'); + throw new ProcessExit(2); + } + await super.runCommand(globalResults); } @@ -331,7 +346,10 @@ class FlutterCommandRunner extends CommandRunner { /// Get all pub packages in the Flutter repo. List getRepoPackages() { - return _gatherProjectPaths(fs.path.absolute(Cache.flutterRoot)) + final String root = fs.path.absolute(Cache.flutterRoot); + // not bin, and not the root + return ['dev', 'examples', 'packages'] + .expand((String path) => _gatherProjectPaths(fs.path.join(root, path))) .map((String dir) => fs.directory(dir)) .toList(); } @@ -351,22 +369,6 @@ class FlutterCommandRunner extends CommandRunner { .toList(); } - /// Get the entry-points we want to analyze in the Flutter repo. - List getRepoAnalysisEntryPoints() { - final String rootPath = fs.path.absolute(Cache.flutterRoot); - final List result = [ - // not bin, and not the root - fs.directory(fs.path.join(rootPath, 'dev')), - fs.directory(fs.path.join(rootPath, 'examples')), - ]; - // And since analyzer refuses to look at paths that end in "packages/": - result.addAll( - _gatherProjectPaths(fs.path.join(rootPath, 'packages')) - .map((String path) => fs.directory(path)) - ); - return result; - } - void _checkFlutterCopy() { // If the current directory is contained by a flutter repo, check that it's // the same flutter that is currently running. diff --git a/packages/flutter_tools/lib/src/template.dart b/packages/flutter_tools/lib/src/template.dart index 580b3afc7c3f7..215e1f35ca921 100644 --- a/packages/flutter_tools/lib/src/template.dart +++ b/packages/flutter_tools/lib/src/template.dart @@ -84,20 +84,20 @@ class Template { return null; relativeDestinationPath = relativeDestinationPath.replaceAll('$platform-$language.tmpl', platform); } - final String organization = context['organization']; final String projectName = context['projectName']; + final String androidIdentifier = context['androidIdentifier']; final String pluginClass = context['pluginClass']; final String destinationDirPath = destination.absolute.path; final String pathSeparator = fs.path.separator; String finalDestinationPath = fs.path .join(destinationDirPath, relativeDestinationPath) .replaceAll(_kCopyTemplateExtension, '') - .replaceAll(_kTemplateExtension, '') - .replaceAll( - '${pathSeparator}organization$pathSeparator', - '$pathSeparator${organization.replaceAll('.', pathSeparator)}$pathSeparator', - ); - + .replaceAll(_kTemplateExtension, ''); + + if (androidIdentifier != null) { + finalDestinationPath = finalDestinationPath + .replaceAll('androidIdentifier', androidIdentifier.replaceAll('.', pathSeparator)); + } if (projectName != null) finalDestinationPath = finalDestinationPath.replaceAll('projectName', projectName); if (pluginClass != null) diff --git a/packages/flutter_tools/lib/src/test/coverage_collector.dart b/packages/flutter_tools/lib/src/test/coverage_collector.dart index 3b7ded4a5014d..21b80e0befd9d 100644 --- a/packages/flutter_tools/lib/src/test/coverage_collector.dart +++ b/packages/flutter_tools/lib/src/test/coverage_collector.dart @@ -11,10 +11,18 @@ import '../base/io.dart'; import '../dart/package_map.dart'; import '../globals.dart'; +import 'watcher.dart'; + /// A class that's used to collect coverage data during tests. -class CoverageCollector { +class CoverageCollector extends TestWatcher { Map _globalHitmap; + @override + Future onFinishedTests(ProcessEvent event) async { + printTrace('test ${event.childIndex}: collecting coverage'); + await collectCoverage(event.process, event.observatoryUri); + } + void _addHitmap(Map hitmap) { if (_globalHitmap == null) _globalHitmap = hitmap; diff --git a/packages/flutter_tools/lib/src/test/event_printer.dart b/packages/flutter_tools/lib/src/test/event_printer.dart new file mode 100644 index 0000000000000..4be6b77fe1fb5 --- /dev/null +++ b/packages/flutter_tools/lib/src/test/event_printer.dart @@ -0,0 +1,41 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert' show JSON; + +import '../base/io.dart' show stdout; +import 'watcher.dart'; + +/// Prints JSON events when running a test in --machine mode. +class EventPrinter extends TestWatcher { + EventPrinter({StringSink out}) : this._out = out == null ? stdout: out; + + final StringSink _out; + + @override + void onStartedProcess(ProcessEvent event) { + _sendEvent("test.startedProcess", + {"observatoryUri": event.observatoryUri.toString()}); + } + + void _sendEvent(String name, [dynamic params]) { + final Map map = { 'event': name}; + if (params != null) { + map['params'] = params; + } + _send(map); + } + + void _send(Map command) { + final String encoded = JSON.encode(command, toEncodable: _jsonEncodeObject); + _out.writeln('\n[$encoded]'); + } + + dynamic _jsonEncodeObject(dynamic object) { + if (object is Uri) { + return object.toString(); + } + return object; + } +} diff --git a/packages/flutter_tools/lib/src/test/flutter_platform.dart b/packages/flutter_tools/lib/src/test/flutter_platform.dart index 58de703b7ce22..8872f79004942 100644 --- a/packages/flutter_tools/lib/src/test/flutter_platform.dart +++ b/packages/flutter_tools/lib/src/test/flutter_platform.dart @@ -19,6 +19,7 @@ import '../base/process_manager.dart'; import '../dart/package_map.dart'; import '../globals.dart'; import 'coverage_collector.dart'; +import 'watcher.dart'; /// The timeout we give the test process to connect to the test harness /// once the process has entered its main method. @@ -53,17 +54,22 @@ final Map _kHosts = [TestPlatform.vm], () => new _FlutterPlatform( shellPath: shellPath, - collector: collector, - debuggerMode: debuggerMode, + watcher: watcher, + enableObservatory: enableObservatory, + startPaused: startPaused, explicitObservatoryPort: observatoryPort, explicitDiagnosticPort: diagnosticPort, host: _kHosts[serverType], @@ -78,8 +84,9 @@ typedef Future _Finalizer(); class _FlutterPlatform extends PlatformPlugin { _FlutterPlatform({ @required this.shellPath, - this.collector, - this.debuggerMode, + this.watcher, + this.enableObservatory, + this.startPaused, this.explicitObservatoryPort, this.explicitDiagnosticPort, this.host, @@ -88,8 +95,9 @@ class _FlutterPlatform extends PlatformPlugin { } final String shellPath; - final CoverageCollector collector; - final bool debuggerMode; + final TestWatcher watcher; + final bool enableObservatory; + final bool startPaused; final int explicitObservatoryPort; final int explicitDiagnosticPort; final InternetAddress host; @@ -105,7 +113,8 @@ class _FlutterPlatform extends PlatformPlugin { @override StreamChannel loadChannel(String testPath, TestPlatform platform) { - if (explicitObservatoryPort != null || explicitDiagnosticPort != null || debuggerMode) { + // Fail if there will be a port conflict. + if (explicitObservatoryPort != null || explicitDiagnosticPort != null) { if (_testCount > 0) throwToolExit('installHook() was called with an observatory port, a diagnostic port, both, or debugger mode enabled, but then more than one test suite was run.'); } @@ -190,8 +199,8 @@ class _FlutterPlatform extends PlatformPlugin { shellPath, listenerFile.path, packages: PackageMap.globalPackagesPath, - enableObservatory: collector != null || debuggerMode, - startPaused: debuggerMode, + enableObservatory: enableObservatory, + startPaused: startPaused, observatoryPort: explicitObservatoryPort, diagnosticPort: explicitDiagnosticPort, ); @@ -216,19 +225,23 @@ class _FlutterPlatform extends PlatformPlugin { // Pipe stdout and stderr from the subprocess to our printStatus console. // We also keep track of what observatory port the engine used, if any. Uri processObservatoryUri; + _pipeStandardStreamsToConsole( process, reportObservatoryUri: (Uri detectedUri) { assert(processObservatoryUri == null); assert(explicitObservatoryPort == null || explicitObservatoryPort == detectedUri.port); - if (debuggerMode) { + if (startPaused) { printStatus('The test process has been started.'); printStatus('You can now connect to it using observatory. To connect, load the following Web site in your browser:'); printStatus(' $detectedUri'); printStatus('You should first set appropriate breakpoints, then resume the test in the debugger.'); } else { - printTrace('test $ourTestCount: using observatory uri $detectedUri from pid ${process.pid} to collect coverage'); + printTrace('test $ourTestCount: using observatory uri $detectedUri from pid ${process.pid}'); + } + if (watcher != null) { + watcher.onStartedProcess(new ProcessEvent(ourTestCount, process, detectedUri)); } processObservatoryUri = detectedUri; }, @@ -341,9 +354,9 @@ class _FlutterPlatform extends PlatformPlugin { break; } - if (subprocessActive && collector != null) { - printTrace('test $ourTestCount: collecting coverage'); - await collector.collectCoverage(process, processObservatoryUri); + if (subprocessActive && watcher != null) { + await watcher.onFinishedTests( + new ProcessEvent(ourTestCount, process, processObservatoryUri)); } } catch (error, stack) { printTrace('test $ourTestCount: error caught during test; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}'); diff --git a/packages/flutter_tools/lib/src/test/runner.dart b/packages/flutter_tools/lib/src/test/runner.dart new file mode 100644 index 0000000000000..7bc9572693375 --- /dev/null +++ b/packages/flutter_tools/lib/src/test/runner.dart @@ -0,0 +1,82 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +// ignore: implementation_imports +import 'package:test/src/executable.dart' as test; + +import '../artifacts.dart'; +import '../base/common.dart'; +import '../base/file_system.dart'; +import '../base/io.dart'; +import '../base/terminal.dart'; +import '../dart/package_map.dart'; +import '../globals.dart'; +import '../test/flutter_platform.dart' as loader; +import 'watcher.dart'; + +/// Runs tests using package:test and the Flutter engine. +Future runTests( + List testFiles, { + Directory workDir, + bool enableObservatory: false, + bool startPaused: false, + bool ipv6: false, + TestWatcher watcher, + }) async { + // Compute the command-line arguments for package:test. + final List testArgs = []; + if (!terminal.supportsColor) + testArgs.addAll(['--no-color', '-rexpanded']); + + if (enableObservatory) { + // (In particular, for collecting code coverage.) + testArgs.add('--concurrency=1'); + } + + testArgs.add('--'); + testArgs.addAll(testFiles); + + // Configure package:test to use the Flutter engine for child processes. + final String shellPath = artifacts.getArtifactPath(Artifact.flutterTester); + if (!fs.isFileSync(shellPath)) + throwToolExit('Cannot find Flutter shell at $shellPath'); + + final InternetAddressType serverType = + ipv6 ? InternetAddressType.IP_V6 : InternetAddressType.IP_V4; + + loader.installHook( + shellPath: shellPath, + watcher: watcher, + enableObservatory: enableObservatory, + startPaused: startPaused, + serverType: serverType, + ); + + // Set the package path used for child processes. + // TODO(skybrian): why is this global? Move to installHook? + PackageMap.globalPackagesPath = + fs.path.normalize(fs.path.absolute(PackageMap.globalPackagesPath)); + + // Call package:test's main method in the appropriate directory. + final Directory saved = fs.currentDirectory; + try { + if (workDir != null) { + printTrace('switching to directory $workDir to run tests'); + fs.currentDirectory = workDir; + } + + printTrace('running test package with arguments: $testArgs'); + await test.main(testArgs); + + // test.main() sets dart:io's exitCode global. + // TODO(skybrian): restore previous value? + printTrace('test package returned with exit code $exitCode'); + + return exitCode; + } finally { + fs.currentDirectory = saved; + } +} diff --git a/packages/flutter_tools/lib/src/test/watcher.dart b/packages/flutter_tools/lib/src/test/watcher.dart new file mode 100644 index 0000000000000..114f456151d30 --- /dev/null +++ b/packages/flutter_tools/lib/src/test/watcher.dart @@ -0,0 +1,39 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import '../base/io.dart' show Process; + +/// Callbacks for reporting progress while running tests. +class TestWatcher { + + /// Called after a child process starts. + /// + /// If startPaused was true, the caller needs to resume in Observatory to + /// start running the tests. + void onStartedProcess(ProcessEvent event) {} + + /// Called after the tests finish but before the process exits. + /// + /// The child process won't exit until this method completes. + /// Not called if the process died. + Future onFinishedTests(ProcessEvent event) async {} +} + +/// Describes a child process started during testing. +class ProcessEvent { + ProcessEvent(this.childIndex, this.process, this.observatoryUri); + + /// The index assigned when the child process was launched. + /// + /// Indexes are assigned consecutively starting from zero. + /// When debugging, there should only be one child process so this will + /// always be zero. + final int childIndex; + + final Process process; + + /// The observatory Uri or null if not debugging. + final Uri observatoryUri; +} diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart index e02711ce47428..de68f74d76606 100644 --- a/packages/flutter_tools/lib/src/version.dart +++ b/packages/flutter_tools/lib/src/version.dart @@ -82,6 +82,15 @@ class FlutterVersion { return '$flutterText\n$frameworkText\n$engineText\n$toolsText'; } + Map toJson() => { + 'channel': channel, + 'repositoryUrl': repositoryUrl ?? 'unknown source', + 'frameworkRevision': frameworkRevision, + 'frameworkCommitDate': frameworkCommitDate, + 'engineRevision': engineRevision, + 'dartSdkVersion': dartSdkVersion, + }; + /// A date String describing the last framework commit. String get frameworkCommitDate => _latestGitCommitDate(); @@ -227,7 +236,7 @@ class FlutterVersion { /// This method sends a server request if it's been more than /// [kCheckAgeConsideredUpToDate] since the last version check. /// - /// Returns `null` if the cached version is out-of-date or missing, and we are + /// Returns null if the cached version is out-of-date or missing, and we are /// unable to reach the server to get the latest version. Future _getLatestAvailableFlutterVersion() async { Cache.checkLockAcquired(); diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index e960ea2fd94e4..dfb3e9410306e 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -33,7 +33,7 @@ dependencies: # We depend on very specific internal implementation details of the # 'test' package, which change between versions, so here we pin it # precisely. - test: 0.12.20 + test: 0.12.21 # Version from the vended Dart SDK as defined in `dependency_overrides`. analyzer: any diff --git a/packages/flutter_tools/templates/create/android-java.tmpl/app/src/main/java/organization/projectName/MainActivity.java.tmpl b/packages/flutter_tools/templates/create/android-java.tmpl/app/src/main/java/androidIdentifier/MainActivity.java.tmpl similarity index 100% rename from packages/flutter_tools/templates/create/android-java.tmpl/app/src/main/java/organization/projectName/MainActivity.java.tmpl rename to packages/flutter_tools/templates/create/android-java.tmpl/app/src/main/java/androidIdentifier/MainActivity.java.tmpl diff --git a/packages/flutter_tools/templates/create/android-kotlin.tmpl/app/src/main/kotlin/organization/projectName/MainActivity.kt.tmpl b/packages/flutter_tools/templates/create/android-kotlin.tmpl/app/src/main/kotlin/androidIdentifier/MainActivity.kt.tmpl similarity index 100% rename from packages/flutter_tools/templates/create/android-kotlin.tmpl/app/src/main/kotlin/organization/projectName/MainActivity.kt.tmpl rename to packages/flutter_tools/templates/create/android-kotlin.tmpl/app/src/main/kotlin/androidIdentifier/MainActivity.kt.tmpl diff --git a/packages/flutter_tools/templates/create/lib/main.dart.tmpl b/packages/flutter_tools/templates/create/lib/main.dart.tmpl index 3687440761129..e0eef759559a8 100644 --- a/packages/flutter_tools/templates/create/lib/main.dart.tmpl +++ b/packages/flutter_tools/templates/create/lib/main.dart.tmpl @@ -16,6 +16,7 @@ void main() { runApp(new MyApp()); } +{{^withPluginHook}} class MyApp extends StatelessWidget { // This widget is the root of your application. @override @@ -29,9 +30,9 @@ class MyApp extends StatelessWidget { // the application has a blue toolbar. Then, without quitting // the app, try changing the primarySwatch below to Colors.green // and then invoke "hot reload" (press "r" in the console where - // you ran "flutter run", or press Run > Hot Reload App in IntelliJ). - // Notice that the counter didn't reset back to zero -- the application - // is not restarted. + // you ran "flutter run", or press Run > Hot Reload App in + // IntelliJ). Notice that the counter didn't reset back to zero; + // the application is not restarted. primarySwatch: Colors.blue, ), home: new MyHomePage(title: 'Flutter Demo Home Page'), @@ -57,7 +58,6 @@ class MyHomePage extends StatefulWidget { _MyHomePageState createState() => new _MyHomePageState(); } -{{^withPluginHook}} class _MyHomePageState extends State { int _counter = 0; @@ -89,8 +89,34 @@ class _MyHomePageState extends State { title: new Text(widget.title), ), body: new Center( - child: new Text( - 'Button tapped $_counter time${ _counter == 1 ? '' : 's' }.', + // Center is a layout widget. It takes a single child and + // positions it in the middle of the parent. + child: new Column( + // Column is also layout widget. It takes a list of children + // and arranges them vertically. By default, it sizes itself + // to fit its children horizontally, and tries to be as tall + // as its parent. + // + // Invoke "debug paint" (press "p" in the console where you + // ran "flutter run", or select "Toggle Debug Paint" from the + // Flutter tool window in IntelliJ) to see the wireframe for + // each widget. + // + // Column has various properties to control how it sizes + // itself and how it positions its children. Here we use + // mainAxisAlignment to center the children vertically; the + // main axis here is the vertical axis because Columns are + // vertical (the cross axis would be horizontal). + mainAxisAlignment: MainAxisAlignment.center, + children: [ + new Text( + 'You have pushed the button this many times:', + ), + new Text( + '${_counter}', + style: Theme.of(context).textTheme.display1, + ), + ], ), ), floatingActionButton: new FloatingActionButton( @@ -103,7 +129,12 @@ class _MyHomePageState extends State { } {{/withPluginHook}} {{#withPluginHook}} -class _MyHomePageState extends State { +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => new _MyAppState(); +} + +class _MyAppState extends State { String _platformVersion = 'Unknown'; @override @@ -119,7 +150,7 @@ class _MyHomePageState extends State { try { platformVersion = await {{pluginDartClass}}.platformVersion; } on PlatformException { - platformVersion = "Failed to get platform version"; + platformVersion = 'Failed to get platform version.'; } // If the widget was removed from the tree while the asynchronous platform @@ -135,11 +166,15 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { - return new Scaffold( - appBar: new AppBar( - title: new Text('Plugin example app'), + return new MaterialApp( + home: new Scaffold( + appBar: new AppBar( + title: new Text('Plugin example app'), + ), + body: new Center( + child: new Text('Running on: $_platformVersion\n'), + ), ), - body: new Center(child: new Text('Running on: $_platformVersion\n')), ); } } diff --git a/packages/flutter_tools/templates/plugin/.idea/libraries/Dart_SDK.xml.tmpl b/packages/flutter_tools/templates/plugin/.idea/libraries/Dart_SDK.xml.tmpl new file mode 100644 index 0000000000000..4c3c50cc99e9b --- /dev/null +++ b/packages/flutter_tools/templates/plugin/.idea/libraries/Dart_SDK.xml.tmpl @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/flutter_tools/templates/plugin/.idea/libraries/Flutter_for_Android.xml.tmpl b/packages/flutter_tools/templates/plugin/.idea/libraries/Flutter_for_Android.xml.tmpl new file mode 100644 index 0000000000000..2fac6b7982477 --- /dev/null +++ b/packages/flutter_tools/templates/plugin/.idea/libraries/Flutter_for_Android.xml.tmpl @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/flutter_tools/templates/plugin/.idea/modules.xml.tmpl b/packages/flutter_tools/templates/plugin/.idea/modules.xml.tmpl new file mode 100644 index 0000000000000..cd521752880e1 --- /dev/null +++ b/packages/flutter_tools/templates/plugin/.idea/modules.xml.tmpl @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/flutter_tools/templates/plugin/.idea/runConfigurations/main_dart.xml.tmpl b/packages/flutter_tools/templates/plugin/.idea/runConfigurations/main_dart.xml.tmpl new file mode 100644 index 0000000000000..5fd9159d1c5ba --- /dev/null +++ b/packages/flutter_tools/templates/plugin/.idea/runConfigurations/main_dart.xml.tmpl @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/packages/flutter_tools/templates/plugin/.idea/workspace.xml.tmpl b/packages/flutter_tools/templates/plugin/.idea/workspace.xml.tmpl new file mode 100644 index 0000000000000..6310d69ac8721 --- /dev/null +++ b/packages/flutter_tools/templates/plugin/.idea/workspace.xml.tmpl @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/flutter_tools/templates/plugin/CHANGELOG.md.tmpl b/packages/flutter_tools/templates/plugin/CHANGELOG.md.tmpl new file mode 100644 index 0000000000000..ac071598e5d45 --- /dev/null +++ b/packages/flutter_tools/templates/plugin/CHANGELOG.md.tmpl @@ -0,0 +1,3 @@ +## [0.0.1] - TODO: Add release date. + +* TODO: Describe initial release. diff --git a/packages/flutter_tools/templates/plugin/android-java.tmpl/src/main/java/organization/projectName/pluginClass.java.tmpl b/packages/flutter_tools/templates/plugin/android-java.tmpl/src/main/java/androidIdentifier/pluginClass.java.tmpl similarity index 100% rename from packages/flutter_tools/templates/plugin/android-java.tmpl/src/main/java/organization/projectName/pluginClass.java.tmpl rename to packages/flutter_tools/templates/plugin/android-java.tmpl/src/main/java/androidIdentifier/pluginClass.java.tmpl diff --git a/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/src/main/kotlin/organization/projectName/pluginClass.kt.tmpl b/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/src/main/kotlin/androidIdentifier/pluginClass.kt.tmpl similarity index 100% rename from packages/flutter_tools/templates/plugin/android-kotlin.tmpl/src/main/kotlin/organization/projectName/pluginClass.kt.tmpl rename to packages/flutter_tools/templates/plugin/android-kotlin.tmpl/src/main/kotlin/androidIdentifier/pluginClass.kt.tmpl diff --git a/examples/catalog/animated_list.iml b/packages/flutter_tools/templates/plugin/projectName.iml.tmpl similarity index 90% rename from examples/catalog/animated_list.iml rename to packages/flutter_tools/templates/plugin/projectName.iml.tmpl index f2b096d125108..9d5dae19540c2 100644 --- a/examples/catalog/animated_list.iml +++ b/packages/flutter_tools/templates/plugin/projectName.iml.tmpl @@ -9,8 +9,7 @@ - - + \ No newline at end of file diff --git a/packages/flutter_tools/templates/plugin/projectName_android.iml.tmpl b/packages/flutter_tools/templates/plugin/projectName_android.iml.tmpl new file mode 100644 index 0000000000000..8e9d155a82244 --- /dev/null +++ b/packages/flutter_tools/templates/plugin/projectName_android.iml.tmpl @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/flutter_tools/templates/plugin/pubspec.yaml.tmpl b/packages/flutter_tools/templates/plugin/pubspec.yaml.tmpl index d1747d74e6ced..09b77b7b4d5a1 100644 --- a/packages/flutter_tools/templates/plugin/pubspec.yaml.tmpl +++ b/packages/flutter_tools/templates/plugin/pubspec.yaml.tmpl @@ -1,5 +1,8 @@ name: {{projectName}} description: {{description}} +version: 0.0.1 +author: +homepage: flutter: plugin: diff --git a/packages/flutter_tools/test/commands/create_test.dart b/packages/flutter_tools/test/commands/create_test.dart index 744a2b62b7ca4..45558bb6a8948 100644 --- a/packages/flutter_tools/test/commands/create_test.dart +++ b/packages/flutter_tools/test/commands/create_test.dart @@ -40,7 +40,7 @@ void main() { projectDir, [], [ - 'android/app/src/main/java/com/yourcompany/flutter_project/MainActivity.java', + 'android/app/src/main/java/com/yourcompany/flutterproject/MainActivity.java', 'ios/Runner/AppDelegate.h', 'ios/Runner/AppDelegate.m', 'ios/Runner/main.m', @@ -54,13 +54,13 @@ void main() { projectDir, ['--android-language', 'kotlin', '-i', 'swift'], [ - 'android/app/src/main/kotlin/com/yourcompany/flutter_project/MainActivity.kt', + 'android/app/src/main/kotlin/com/yourcompany/flutterproject/MainActivity.kt', 'ios/Runner/AppDelegate.swift', 'ios/Runner/Runner-Bridging-Header.h', 'lib/main.dart', ], [ - 'android/app/src/main/java/com/yourcompany/flutter_project/MainActivity.java', + 'android/app/src/main/java/com/yourcompany/flutterproject/MainActivity.java', 'ios/Runner/AppDelegate.h', 'ios/Runner/AppDelegate.m', 'ios/Runner/main.m', @@ -73,11 +73,11 @@ void main() { projectDir, ['--plugin'], [ - 'android/src/main/java/com/yourcompany/flutter_project/FlutterProjectPlugin.java', + 'android/src/main/java/com/yourcompany/flutterproject/FlutterProjectPlugin.java', 'ios/Classes/FlutterProjectPlugin.h', 'ios/Classes/FlutterProjectPlugin.m', 'lib/flutter_project.dart', - 'example/android/app/src/main/java/com/yourcompany/flutter_project_example/MainActivity.java', + 'example/android/app/src/main/java/com/yourcompany/flutterprojectexample/MainActivity.java', 'example/ios/Runner/AppDelegate.h', 'example/ios/Runner/AppDelegate.m', 'example/ios/Runner/main.m', @@ -91,19 +91,19 @@ void main() { projectDir, ['--plugin', '-a', 'kotlin', '--ios-language', 'swift'], [ - 'android/src/main/kotlin/com/yourcompany/flutter_project/FlutterProjectPlugin.kt', + 'android/src/main/kotlin/com/yourcompany/flutterproject/FlutterProjectPlugin.kt', 'ios/Classes/FlutterProjectPlugin.h', 'ios/Classes/FlutterProjectPlugin.m', 'ios/Classes/SwiftFlutterProjectPlugin.swift', 'lib/flutter_project.dart', - 'example/android/app/src/main/kotlin/com/yourcompany/flutter_project_example/MainActivity.kt', + 'example/android/app/src/main/kotlin/com/yourcompany/flutterprojectexample/MainActivity.kt', 'example/ios/Runner/AppDelegate.swift', 'example/ios/Runner/Runner-Bridging-Header.h', 'example/lib/main.dart', ], [ - 'android/src/main/java/com/yourcompany/flutter_project/FlutterProjectPlugin.java', - 'example/android/app/src/main/java/com/yourcompany/flutter_project_example/MainActivity.java', + 'android/src/main/java/com/yourcompany/flutterproject/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/yourcompany/flutterprojectexample/MainActivity.java', 'example/ios/Runner/AppDelegate.h', 'example/ios/Runner/AppDelegate.m', 'example/ios/Runner/main.m', @@ -116,12 +116,12 @@ void main() { projectDir, ['--plugin', '--org', 'com.bar.foo'], [ - 'android/src/main/java/com/bar/foo/flutter_project/FlutterProjectPlugin.java', - 'example/android/app/src/main/java/com/bar/foo/flutter_project_example/MainActivity.java', + 'android/src/main/java/com/bar/foo/flutterproject/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/bar/foo/flutterprojectexample/MainActivity.java', ], [ - 'android/src/main/java/com/yourcompany/flutter_project/FlutterProjectPlugin.java', - 'example/android/app/src/main/java/com/yourcompany/flutter_project_example/MainActivity.java', + 'android/src/main/java/com/yourcompany/flutterproject/FlutterProjectPlugin.java', + 'example/android/app/src/main/java/com/yourcompany/flutterprojectexample/MainActivity.java', ], ); }); diff --git a/packages/flutter_tools/test/commands/doctor_test.dart b/packages/flutter_tools/test/commands/doctor_test.dart index 369aaeae19cd1..8140c65e15fee 100644 --- a/packages/flutter_tools/test/commands/doctor_test.dart +++ b/packages/flutter_tools/test/commands/doctor_test.dart @@ -12,9 +12,9 @@ void main() { group('doctor', () { testUsingContext('intellij validator', () async { final ValidationResult result = await new IntelliJValidatorTestTarget('Test').validate(); - expect(result.type, ValidationType.installed); + expect(result.type, ValidationType.partial); expect(result.statusInfo, 'version test.test.test'); - expect(result.messages, hasLength(2)); + expect(result.messages, hasLength(3)); ValidationMessage message = result.messages .firstWhere((ValidationMessage m) => m.message.startsWith('Dart ')); @@ -22,7 +22,8 @@ void main() { message = result.messages .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter ')); - expect(message.message, 'Flutter plugin version 0.1.3'); + expect(message.message, contains('Flutter plugin version 0.1.3')); + expect(message.message, contains('recommended minimum version')); }); }); } diff --git a/packages/flutter_tools/test/ios/mac_test.dart b/packages/flutter_tools/test/ios/mac_test.dart new file mode 100644 index 0000000000000..e267b05ad8553 --- /dev/null +++ b/packages/flutter_tools/test/ios/mac_test.dart @@ -0,0 +1,176 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_tools/src/application_package.dart'; +import 'package:flutter_tools/src/ios/mac.dart'; +import 'package:test/test.dart'; + +import '../src/context.dart'; + +void main() { + group('Diagnose Xcode build failure', () { + BuildableIOSApp app; + + setUp(() { + app = new BuildableIOSApp( + projectBundleId: 'test.app', + buildSettings: { + 'For our purposes': 'a non-empty build settings map is valid', + }, + ); + }); + + testUsingContext('No provisioning profile shows message', () async { + final XcodeBuildResult buildResult = new XcodeBuildResult( + success: false, + stdout: ''' +Launching lib/main.dart on iPhone in debug mode... +Signing iOS app for device deployment using developer identity: "iPhone Developer: test@flutter.io (1122334455)" +Running Xcode build... 1.3s +Failed to build iOS app +Error output from Xcode build: +↳ + ** BUILD FAILED ** + + + The following build commands failed: + Check dependencies + (1 failure) +Xcode's output: +↳ + Build settings from command line: + ARCHS = arm64 + BUILD_DIR = /Users/blah/blah + DEVELOPMENT_TEAM = AABBCCDDEE + ONLY_ACTIVE_ARCH = YES + SDKROOT = iphoneos10.3 + + === CLEAN TARGET Runner OF PROJECT Runner WITH CONFIGURATION Release === + + Check dependencies + [BCEROR]No profiles for 'com.yourcompany.test' were found: Xcode couldn't find a provisioning profile matching 'com.yourcompany.test'. + [BCEROR]Code signing is required for product type 'Application' in SDK 'iOS 10.3' + [BCEROR]Code signing is required for product type 'Application' in SDK 'iOS 10.3' + [BCEROR]Code signing is required for product type 'Application' in SDK 'iOS 10.3' + + Create product structure + /bin/mkdir -p /Users/blah/Runner.app + + Clean.Remove clean /Users/blah/Runner.app.dSYM + builtin-rm -rf /Users/blah/Runner.app.dSYM + + Clean.Remove clean /Users/blah/Runner.app + builtin-rm -rf /Users/blah/Runner.app + + Clean.Remove clean /Users/blah/Runner-dfvicjniknvzghgwsthwtgcjhtsk/Build/Intermediates/Runner.build/Release-iphoneos/Runner.build + builtin-rm -rf /Users/blah/Runner-dfvicjniknvzghgwsthwtgcjhtsk/Build/Intermediates/Runner.build/Release-iphoneos/Runner.build + + ** CLEAN SUCCEEDED ** + + === BUILD TARGET Runner OF PROJECT Runner WITH CONFIGURATION Release === + + Check dependencies + No profiles for 'com.yourcompany.test' were found: Xcode couldn't find a provisioning profile matching 'com.yourcompany.test'. + Code signing is required for product type 'Application' in SDK 'iOS 10.3' + Code signing is required for product type 'Application' in SDK 'iOS 10.3' + Code signing is required for product type 'Application' in SDK 'iOS 10.3' + +Could not build the precompiled application for the device. + +Error launching application on iPhone.''', + xcodeBuildExecution: new XcodeBuildExecution( + ['xcrun', 'xcodebuild', 'blah'], + '/blah/blah', + buildForPhysicalDevice: true + ), + ); + + await diagnoseXcodeBuildFailure(buildResult, app); + expect( + testLogger.errorText, + contains('No Provisioning Profile was found for your project\'s Bundle Identifier or your device.'), + ); + }); + + testUsingContext('No development team shows message', () async { + final XcodeBuildResult buildResult = new XcodeBuildResult( + success: false, + stdout: ''' +Running "flutter packages get" in flutter_gallery... 0.6s +Launching lib/main.dart on x in release mode... +Running pod install... 1.2s +Running Xcode build... 1.4s +Failed to build iOS app +Error output from Xcode build: +↳ + ** BUILD FAILED ** + + + The following build commands failed: + Check dependencies + (1 failure) +Xcode's output: +↳ + blah + + === CLEAN TARGET url_launcher OF PROJECT Pods WITH CONFIGURATION Release === + + Check dependencies + + blah + + === CLEAN TARGET Pods-Runner OF PROJECT Pods WITH CONFIGURATION Release === + + Check dependencies + + blah + + === CLEAN TARGET Runner OF PROJECT Runner WITH CONFIGURATION Release === + + Check dependencies + [BCEROR]Signing for "Runner" requires a development team. Select a development team in the project editor. + [BCEROR]Code signing is required for product type 'Application' in SDK 'iOS 10.3' + [BCEROR]Code signing is required for product type 'Application' in SDK 'iOS 10.3' + [BCEROR]Code signing is required for product type 'Application' in SDK 'iOS 10.3' + + blah + + ** CLEAN SUCCEEDED ** + + === BUILD TARGET url_launcher OF PROJECT Pods WITH CONFIGURATION Release === + + Check dependencies + + blah + + === BUILD TARGET Pods-Runner OF PROJECT Pods WITH CONFIGURATION Release === + + Check dependencies + + blah + + === BUILD TARGET Runner OF PROJECT Runner WITH CONFIGURATION Release === + + Check dependencies + Signing for "Runner" requires a development team. Select a development team in the project editor. + Code signing is required for product type 'Application' in SDK 'iOS 10.3' + Code signing is required for product type 'Application' in SDK 'iOS 10.3' + Code signing is required for product type 'Application' in SDK 'iOS 10.3' + +Could not build the precompiled application for the device.''', + xcodeBuildExecution: new XcodeBuildExecution( + ['xcrun', 'xcodebuild', 'blah'], + '/blah/blah', + buildForPhysicalDevice: true + ), + ); + + await diagnoseXcodeBuildFailure(buildResult, app); + expect( + testLogger.errorText, + contains('Building a deployable iOS app requires a selected Development Team with a Provisioning Profile'), + ); + }); + }); +}