diff --git a/.analysis_options b/.analysis_options index 38cff14072e3e..dfc6953ab3a6e 100644 --- a/.analysis_options +++ b/.analysis_options @@ -110,7 +110,7 @@ linter: - prefer_adjacent_string_concatenation - prefer_collection_literals # - prefer_conditional_assignment # not yet tested - - prefer_const_constructors + # - prefer_const_constructors # https://github.com/dart-lang/linter/issues/752 # - prefer_constructors_over_static_methods # not yet tested - prefer_contains # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods diff --git a/.analysis_options_repo b/.analysis_options_repo index 52e4ffae0279f..ff8e9d7dd283e 100644 --- a/.analysis_options_repo +++ b/.analysis_options_repo @@ -104,7 +104,7 @@ linter: - prefer_adjacent_string_concatenation - prefer_collection_literals # - prefer_conditional_assignment # not yet tested - - prefer_const_constructors + # - prefer_const_constructors # https://github.com/dart-lang/linter/issues/752 # - prefer_constructors_over_static_methods # not yet tested - prefer_contains # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods diff --git a/bin/internal/dart-sdk.version b/bin/internal/dart-sdk.version index 414f6c322c273..3438ee828701f 100644 --- a/bin/internal/dart-sdk.version +++ b/bin/internal/dart-sdk.version @@ -1 +1 @@ -1.25.0-dev.4.0 +1.25.0-dev.7.0 diff --git a/bin/internal/engine.version b/bin/internal/engine.version index dbab89d673ae0..180da75212d33 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -5fcfb995bbce72b5f1ee807121f51a3c0280c8b4 +3a12bc092d58528dce40e7378b29d0a14c952ec0 diff --git a/bin/internal/update_dart_sdk.ps1 b/bin/internal/update_dart_sdk.ps1 index 43a10566b1be8..1f16cb6ebb685 100644 --- a/bin/internal/update_dart_sdk.ps1 +++ b/bin/internal/update_dart_sdk.ps1 @@ -40,13 +40,8 @@ if (Test-Path $dartSdkPath) { } New-Item $dartSdkPath -force -type directory | Out-Null $dartSdkZip = "$cachePath\dart-sdk.zip" -# TODO(goderbauer): remove (slow and backwards-incompatible) appveyor work around -if (Test-Path Env:\APPVEYOR) { - curl $dartSdkUrl -OutFile $dartSdkZip -} else { - Import-Module BitsTransfer - Start-BitsTransfer -Source $dartSdkUrl -Destination $dartSdkZip -} +Import-Module BitsTransfer +Start-BitsTransfer -Source $dartSdkUrl -Destination $dartSdkZip Write-Host "Unzipping Dart SDK..." If (Get-Command 7z -errorAction SilentlyContinue) { diff --git a/bin/internal/update_dart_sdk.sh b/bin/internal/update_dart_sdk.sh index 80e8f6e4ed701..5c8614ece08f3 100755 --- a/bin/internal/update_dart_sdk.sh +++ b/bin/internal/update_dart_sdk.sh @@ -17,6 +17,7 @@ set -e FLUTTER_ROOT="$(dirname "$(dirname "$(dirname "${BASH_SOURCE[0]}")")")" DART_SDK_PATH="$FLUTTER_ROOT/bin/cache/dart-sdk" +DART_SDK_PATH_OLD="$DART_SDK_PATH.old" DART_SDK_STAMP_PATH="$FLUTTER_ROOT/bin/cache/dart-sdk.stamp" DART_SDK_VERSION=`cat "$FLUTTER_ROOT/bin/internal/dart-sdk.version"` @@ -48,6 +49,13 @@ if [ ! -f "$DART_SDK_STAMP_PATH" ] || [ "$DART_SDK_VERSION" != `cat "$DART_SDK_S DART_SDK_URL="https://storage.googleapis.com/dart-archive/channels/$DART_CHANNEL/raw/$DART_SDK_VERSION/sdk/$DART_ZIP_NAME" + # if the sdk path exists, copy it to a temporary location + if [ -d "$DART_SDK_PATH" ]; then + rm -rf "$DART_SDK_PATH_OLD" + mv "$DART_SDK_PATH" "$DART_SDK_PATH_OLD" + fi + + # install the new sdk rm -rf -- "$DART_SDK_PATH" mkdir -p -- "$DART_SDK_PATH" DART_SDK_ZIP="$FLUTTER_ROOT/bin/cache/dart-sdk.zip" @@ -64,4 +72,9 @@ if [ ! -f "$DART_SDK_STAMP_PATH" ] || [ "$DART_SDK_VERSION" != `cat "$DART_SDK_S } rm -f -- "$DART_SDK_ZIP" echo "$DART_SDK_VERSION" > "$DART_SDK_STAMP_PATH" + + # delete any temporary sdk path + if [ -d "$DART_SDK_PATH_OLD" ]; then + rm -rf "$DART_SDK_PATH_OLD" + fi fi diff --git a/dev/devicelab/test/adb_test.dart b/dev/devicelab/test/adb_test.dart index 0f7a827a4971b..9be387461e7d5 100644 --- a/dev/devicelab/test/adb_test.dart +++ b/dev/devicelab/test/adb_test.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'package:test/test.dart'; -import 'package:collection/collection.dart'; +import 'package:collection/collection.dart' show ListEquality, MapEquality; import 'package:flutter_devicelab/framework/adb.dart'; diff --git a/packages/flutter/lib/cupertino.dart b/packages/flutter/lib/cupertino.dart index 6ec803a7f4f64..5875c4cfa6b66 100644 --- a/packages/flutter/lib/cupertino.dart +++ b/packages/flutter/lib/cupertino.dart @@ -17,5 +17,6 @@ export 'src/cupertino/page.dart'; export 'src/cupertino/scaffold.dart'; export 'src/cupertino/slider.dart'; export 'src/cupertino/switch.dart'; +export 'src/cupertino/text_selection.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 e7ac5fbf174f9..dc9f0305ce947 100644 --- a/packages/flutter/lib/foundation.dart +++ b/packages/flutter/lib/foundation.dart @@ -33,6 +33,7 @@ export 'src/foundation/assertions.dart'; export 'src/foundation/basic_types.dart'; export 'src/foundation/binding.dart'; export 'src/foundation/change_notifier.dart'; +export 'src/foundation/collections.dart'; export 'src/foundation/debug.dart'; export 'src/foundation/licenses.dart'; export 'src/foundation/observer_list.dart'; diff --git a/packages/flutter/lib/gestures.dart b/packages/flutter/lib/gestures.dart index 162b61f409a04..cd3ddcd16a111 100644 --- a/packages/flutter/lib/gestures.dart +++ b/packages/flutter/lib/gestures.dart @@ -11,6 +11,7 @@ export 'src/gestures/arena.dart'; export 'src/gestures/binding.dart'; export 'src/gestures/constants.dart'; export 'src/gestures/converter.dart'; +export 'src/gestures/debug.dart'; export 'src/gestures/drag.dart'; export 'src/gestures/drag_details.dart'; export 'src/gestures/events.dart'; diff --git a/packages/flutter/lib/src/cupertino/button.dart b/packages/flutter/lib/src/cupertino/button.dart index f46bac6673c12..db12d153dfbce 100644 --- a/packages/flutter/lib/src/cupertino/button.dart +++ b/packages/flutter/lib/src/cupertino/button.dart @@ -47,8 +47,9 @@ class CupertinoButton extends StatefulWidget { this.color, this.minSize: 44.0, this.pressedOpacity: 0.1, + this.borderRadius: const BorderRadius.all(const Radius.circular(8.0)), @required this.onPressed, - }) : assert(pressedOpacity >= 0.0 && pressedOpacity <= 1.0); + }) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)); /// The widget below this widget in the tree. /// @@ -83,9 +84,15 @@ class CupertinoButton extends StatefulWidget { /// The opacity that the button will fade to when it is pressed. /// The button will have an opacity of 1.0 when it is not pressed. /// - /// This defaults to 0.1. + /// This defaults to 0.1. If null, opacity will not change on pressed if using + /// your own custom effects is desired. final double pressedOpacity; + /// The radius of the button's corners when it has a background color. + /// + /// Defaults to round corners of 8 logical pixels. + final BorderRadius borderRadius; + /// Whether the button is enabled or disabled. Buttons are disabled by default. To /// enable a button, set its [onPressed] property to a non-null value. bool get enabled => onPressed != null; @@ -112,7 +119,7 @@ class _CupertinoButtonState extends State with SingleTickerProv void _setTween() { _opacityTween = new Tween( begin: 1.0, - end: widget.pressedOpacity, + end: widget.pressedOpacity ?? 1.0, ); } @@ -164,10 +171,12 @@ class _CupertinoButtonState extends State with SingleTickerProv child: new GestureDetector( onTap: widget.onPressed, child: new ConstrainedBox( - constraints: new BoxConstraints( - minWidth: widget.minSize, - minHeight: widget.minSize, - ), + constraints: widget.minSize == null + ? const BoxConstraints() + : new BoxConstraints( + minWidth: widget.minSize, + minHeight: widget.minSize, + ), child: new FadeTransition( opacity: _opacityTween.animate(new CurvedAnimation( parent: _animationController, @@ -175,17 +184,15 @@ class _CupertinoButtonState extends State with SingleTickerProv )), child: new DecoratedBox( decoration: new BoxDecoration( - borderRadius: const BorderRadius.all(const Radius.circular(8.0)), + borderRadius: widget.borderRadius, color: backgroundColor != null && !enabled ? _kDisabledBackground : backgroundColor, ), child: new Padding( - padding: widget.padding != null - ? widget.padding - : backgroundColor != null - ? _kBackgroundButtonPadding - : _kButtonPadding, + padding: widget.padding ?? (backgroundColor != null + ? _kBackgroundButtonPadding + : _kButtonPadding), child: new Center( widthFactor: 1.0, heightFactor: 1.0, diff --git a/packages/flutter/lib/src/cupertino/text_selection.dart b/packages/flutter/lib/src/cupertino/text_selection.dart new file mode 100644 index 0000000000000..a839603a8e16c --- /dev/null +++ b/packages/flutter/lib/src/cupertino/text_selection.dart @@ -0,0 +1,301 @@ +// 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:math' as math; + +import 'package:flutter/widgets.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +import 'button.dart'; + +// Padding around the line at the edge of the text selection that has 0 width and +// the height of the text font. +const double _kHandlesPadding = 18.0; +// Minimal padding from all edges of the selection toolbar to all edges of the +// viewport. +const double _kToolbarScreenPadding = 8.0; +const double _kToolbarHeight = 36.0; + +const Color _kToolbarBackgroundColor = const Color(0xFF2E2E2E); +const Color _kToolbarDividerColor = const Color(0xFFB9B9B9); +const Color _kHandlesColor = const Color(0xFF146DDE); + +// This offset is used to determine the center of the selection during a drag. +// It's slightly below the center of the text so the finger isn't entirely +// covering the text being selected. +const Size _kSelectionOffset = const Size(20.0, 30.0); +const Size _kToolbarTriangleSize = const Size(18.0, 9.0); +const EdgeInsets _kToolbarButtonPadding = const EdgeInsets.symmetric(vertical: 10.0, horizontal: 21.0); +const BorderRadius _kToolbarBorderRadius = const BorderRadius.all(const Radius.circular(7.5)); + +const TextStyle _kToolbarButtonFontStyle = const TextStyle( + fontSize: 14.0, + letterSpacing: -0.11, + fontWeight: FontWeight.w300, +); + +/// Paints a triangle below the toolbar. +class _TextSelectionToolbarNotchPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final Paint paint = new Paint() + ..color = _kToolbarBackgroundColor + ..style = PaintingStyle.fill; + final Path triangle = new Path() + ..lineTo(_kToolbarTriangleSize.width / 2, 0.0) + ..lineTo(0.0, _kToolbarTriangleSize.height) + ..lineTo(-(_kToolbarTriangleSize.width / 2), 0.0) + ..close(); + canvas.drawPath(triangle, paint); + } + + @override + bool shouldRepaint(_TextSelectionToolbarNotchPainter oldPainter) => false; +} + +/// Manages a copy/paste text selection toolbar. +class _TextSelectionToolbar extends StatelessWidget { + const _TextSelectionToolbar({ + Key key, + this.delegate, + this.handleCut, + this.handleCopy, + this.handlePaste, + this.handleSelectAll, + }) : super(key: key); + + final TextSelectionDelegate delegate; + TextEditingValue get value => delegate.textEditingValue; + + final VoidCallback handleCut; + final VoidCallback handleCopy; + final VoidCallback handlePaste; + final VoidCallback handleSelectAll; + + @override + Widget build(BuildContext context) { + final List items = []; + final Widget onePhysicalPixelVerticalDivider = + new SizedBox(width: 1.0 / MediaQuery.of(context).devicePixelRatio); + + if (!value.selection.isCollapsed) { + items.add(_buildToolbarButton('Cut', handleCut)); + items.add(onePhysicalPixelVerticalDivider); + items.add(_buildToolbarButton('Copy', handleCopy)); + } + + // TODO(https://github.com/flutter/flutter/issues/11254): + // This should probably be grayed-out if there is nothing to paste. + if (items.isNotEmpty) + items.add(onePhysicalPixelVerticalDivider); + items.add(_buildToolbarButton('Paste', handlePaste)); + + if (value.text.isNotEmpty && value.selection.isCollapsed) { + items.add(onePhysicalPixelVerticalDivider); + items.add(_buildToolbarButton('Select All', handleSelectAll)); + } + + final Widget triangle = new SizedBox.fromSize( + size: _kToolbarTriangleSize, + child: new CustomPaint( + painter: new _TextSelectionToolbarNotchPainter(), + ) + ); + + return new Column( + mainAxisSize: MainAxisSize.min, + children: [ + new ClipRRect( + borderRadius: _kToolbarBorderRadius, + child: new DecoratedBox( + decoration: const BoxDecoration( + color: _kToolbarDividerColor, + ), + child: new Row(mainAxisSize: MainAxisSize.min, children: items), + ), + ), + // TODO(https://github.com/flutter/flutter/issues/11274): + // Position the triangle based on the layout delegate. + // And avoid letting the triangle line up with any dividers. + triangle, + ], + ); + } + + /// Builds a themed [CupertinoButton] for the toolbar. + CupertinoButton _buildToolbarButton(String text, VoidCallback onPressed) { + return new CupertinoButton( + child: new Text(text, style: _kToolbarButtonFontStyle), + color: _kToolbarBackgroundColor, + minSize: _kToolbarHeight, + padding: _kToolbarButtonPadding, + borderRadius: null, + pressedOpacity: 0.7, + onPressed: onPressed, + ); + } +} + +/// Centers the toolbar around the given position, ensuring that it remains on +/// screen. +class _TextSelectionToolbarLayout extends SingleChildLayoutDelegate { + _TextSelectionToolbarLayout(this.screenSize, this.globalEditableRegion, this.position); + + /// The size of the screen at the time that the toolbar was last laid out. + final Size screenSize; + + /// Size and position of the editing region at the time the toolbar was last + /// laid out, in global coordinates. + final Rect globalEditableRegion; + + /// Anchor position of the toolbar, relative to the top left of the + /// [globalEditableRegion]. + final Offset position; + + @override + BoxConstraints getConstraintsForChild(BoxConstraints constraints) { + return constraints.loosen(); + } + + @override + Offset getPositionForChild(Size size, Size childSize) { + final Offset globalPosition = globalEditableRegion.topLeft + position; + + double x = globalPosition.dx - childSize.width / 2.0; + double y = globalPosition.dy - childSize.height; + + if (x < _kToolbarScreenPadding) + x = _kToolbarScreenPadding; + else if (x + childSize.width > screenSize.width - _kToolbarScreenPadding) + x = screenSize.width - childSize.width - _kToolbarScreenPadding; + + if (y < _kToolbarScreenPadding) + y = _kToolbarScreenPadding; + else if (y + childSize.height > screenSize.height - _kToolbarScreenPadding) + y = screenSize.height - childSize.height - _kToolbarScreenPadding; + + return new Offset(x, y); + } + + @override + bool shouldRelayout(_TextSelectionToolbarLayout oldDelegate) { + return screenSize != oldDelegate.screenSize + || globalEditableRegion != oldDelegate.globalEditableRegion + || position != oldDelegate.position; + } +} + +/// Draws a single text selection handle with a bar and a ball. +/// +/// Draws from a point of origin somewhere inside the size of the painter +/// such that the ball is below the point of origin and the bar is above the +/// point of origin. +class _TextSelectionHandlePainter extends CustomPainter { + _TextSelectionHandlePainter({this.origin}); + + final Offset origin; + + @override + void paint(Canvas canvas, Size size) { + final Paint paint = new Paint() + ..color = _kHandlesColor + ..strokeWidth = 2.0; + // Draw circle below the origin that slightly overlaps the bar. + canvas.drawCircle(origin.translate(0.0, 4.0), 5.5, paint); + // Draw up from origin leaving 10 pixels of margin on top. + canvas.drawLine( + origin, + origin.translate( + 0.0, + -(size.height - 2.0 * _kHandlesPadding), + ), + paint, + ); + } + + @override + bool shouldRepaint(_TextSelectionHandlePainter oldPainter) => origin != oldPainter.origin; +} + +class _CupertinoTextSelectionControls extends TextSelectionControls { + @override + Size handleSize = _kSelectionOffset; // Used for drag selection offset. + + /// Builder for iOS-style copy/paste text selection toolbar. + @override + Widget buildToolbar(BuildContext context, Rect globalEditableRegion, Offset position, TextSelectionDelegate delegate) { + assert(debugCheckHasMediaQuery(context)); + return new ConstrainedBox( + constraints: new BoxConstraints.tight(globalEditableRegion.size), + child: new CustomSingleChildLayout( + delegate: new _TextSelectionToolbarLayout( + MediaQuery.of(context).size, + globalEditableRegion, + position, + ), + child: new _TextSelectionToolbar( + delegate: delegate, + handleCut: () => handleCut(delegate), + handleCopy: () => handleCopy(delegate), + handlePaste: () => handlePaste(delegate), + handleSelectAll: () => handleSelectAll(delegate), + ), + ) + ); + } + + /// Builder for iOS text selection edges. + @override + Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight) { + // We want a size that's a vertical line the height of the text plus a 18.0 + // padding in every direction that will constitute the selection drag area. + final Size desiredSize = new Size( + 2.0 * _kHandlesPadding, + textLineHeight + 2.0 * _kHandlesPadding + ); + + final Widget handle = new SizedBox.fromSize( + size: desiredSize, + child: new CustomPaint( + painter: new _TextSelectionHandlePainter( + // We give the painter a point of origin that's at the bottom baseline + // of the selection cursor position. + // + // We give it in the form of an offset from the top left of the + // SizedBox. + origin: new Offset(_kHandlesPadding, textLineHeight + _kHandlesPadding), + ), + ), + ); + + // [buildHandle]'s widget is positioned at the selection cursor's bottom + // baseline. We transform the handle such that the SizedBox is superimposed + // on top of the text selection endpoints. + switch (type) { + case TextSelectionHandleType.left: // The left handle is upside down on iOS. + return new Transform( + transform: new Matrix4.rotationZ(math.PI) + ..translate(-_kHandlesPadding, -_kHandlesPadding), + child: handle + ); + case TextSelectionHandleType.right: + return new Transform( + transform: new Matrix4.translationValues( + -_kHandlesPadding, + -(textLineHeight + _kHandlesPadding), + 0.0 + ), + child: handle + ); + case TextSelectionHandleType.collapsed: // iOS doesn't draw anything for collapsed selections. + return new Container(); + } + assert(type != null); + return null; + } +} + +/// Text selection controls that follows iOS design conventions. +final TextSelectionControls cupertinoTextSelectionControls = new _CupertinoTextSelectionControls(); diff --git a/packages/flutter/lib/src/foundation/annotations.dart b/packages/flutter/lib/src/foundation/annotations.dart index a1ea49fe83e22..87b9f1161c299 100644 --- a/packages/flutter/lib/src/foundation/annotations.dart +++ b/packages/flutter/lib/src/foundation/annotations.dart @@ -34,6 +34,7 @@ /// * [Summary], which is used to provide a one-line description of a /// class that overrides the inline documentations' own description. class Category { + /// Create an annotation to provide a categorization of a class. const Category(this.sections) : assert(sections != null); /// The strings the correspond to the section and subsection of the @@ -67,6 +68,7 @@ class Category { /// * [Summary], which is used to provide a one-line description of a /// class that overrides the inline documentations' own description. class DocumentationIcon { + /// Create an annotation to provide a URL to an image describing a class. const DocumentationIcon(this.url) : assert(url != null); /// The URL to an image that represents the annotated class. @@ -102,6 +104,7 @@ class DocumentationIcon { /// * [DocumentationIcon], which is used to give the URL to an image that /// represents the class. class Summary { + /// Create an annotation to provide a short description of a class. const Summary(this.text) : assert(text != null); /// The text of the summary of the annotated class. diff --git a/packages/flutter/lib/src/foundation/collections.dart b/packages/flutter/lib/src/foundation/collections.dart new file mode 100644 index 0000000000000..9ab2b2687f29a --- /dev/null +++ b/packages/flutter/lib/src/foundation/collections.dart @@ -0,0 +1,47 @@ +// 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. + +// TODO(ianh): These should be on the Set and List classes themselves. + +/// Compares two sets for deep equality. +/// +/// Returns true if the sets are both null, or if they are both non-null, have +/// the same length, and contain the same members. Returns false otherwise. +/// Order is not compared. +/// +/// See also: +/// +/// * [listEquals], which does something similar for lists. +bool setEquals(Set a, Set b) { + if (a == null) + return b == null; + if (b == null || a.length != b.length) + return false; + for (T value in a) { + if (!b.contains(value)) + return false; + } + return true; +} + +/// Compares two lists for deep equality. +/// +/// Returns true if the lists are both null, or if they are both non-null, have +/// the same length, and contain the same members in the same order. Returns +/// false otherwise. +/// +/// See also: +/// +/// * [setEquals], which does something similar for sets. +bool listEquals(List a, List b) { + if (a == null) + return b == null; + if (b == null || a.length != b.length) + return false; + for (int index = 0; index < a.length; index += 1) { + if (a[index] != b[index]) + return false; + } + return true; +} diff --git a/packages/flutter/lib/src/gestures/binding.dart b/packages/flutter/lib/src/gestures/binding.dart index 09945d7d3adc8..fb72449397b3e 100644 --- a/packages/flutter/lib/src/gestures/binding.dart +++ b/packages/flutter/lib/src/gestures/binding.dart @@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart'; import 'arena.dart'; import 'converter.dart'; +import 'debug.dart'; import 'events.dart'; import 'hit_test.dart'; import 'pointer_router.dart'; @@ -75,6 +76,11 @@ abstract class GestureBinding extends BindingBase with HitTestable, HitTestDispa result = new HitTestResult(); hitTest(result, event.position); _hitTests[event.pointer] = result; + assert(() { + if (debugPrintHitTestResults) + debugPrint('$event: $result'); + return true; + }); } else if (event is PointerUpEvent || event is PointerCancelEvent) { result = _hitTests.remove(event.pointer); } else if (event.down) { diff --git a/packages/flutter/lib/src/gestures/debug.dart b/packages/flutter/lib/src/gestures/debug.dart new file mode 100644 index 0000000000000..fbb5cdf6cadf2 --- /dev/null +++ b/packages/flutter/lib/src/gestures/debug.dart @@ -0,0 +1,32 @@ +// 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'; + +// Any changes to this file should be reflected in the debugAssertAllGesturesVarsUnset() +// function below. + +/// Whether to print the results of each hit test to the console. +/// +/// When this is set, in debug mode, any time a hit test is triggered by the +/// [GestureBinding] the results are dumped to the console. +/// +/// This has no effect in release builds. +bool debugPrintHitTestResults = false; + +/// Returns true if none of the gestures library debug variables have been changed. +/// +/// This function is used by the test framework to ensure that debug variables +/// haven't been inadvertently changed. +/// +/// See [https://docs.flutter.io/flutter/gestures/gestures-library.html] for +/// a complete list. +bool debugAssertAllGesturesVarsUnset(String reason) { + assert(() { + if (debugPrintHitTestResults) + throw new FlutterError(reason); + return true; + }); + return true; +} diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart index 8b035d443ecb2..2a73b71acb1bb 100644 --- a/packages/flutter/lib/src/material/app.dart +++ b/packages/flutter/lib/src/material/app.dart @@ -307,7 +307,7 @@ class _MaterialAppState extends State { else builder = widget.routes[name]; if (builder != null) { - return new MaterialPageRoute( + return new MaterialPageRoute( builder: builder, settings: settings, ); diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index eece44cc5f76f..1e0be66d900f4 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -84,7 +84,9 @@ class _ToolbarContainerLayout extends SingleChildLayoutDelegate { /// If the [leading] widget is omitted, but the [AppBar] is in a [Scaffold] with /// a [Drawer], then a button will be inserted to open the drawer. Otherwise, if /// the nearest [Navigator] has any previous routes, a [BackButton] is inserted -/// instead. +/// instead. This behavior can be turned off by setting the [automaticallyImplyLeading] +/// to false. In that case a null leading widget will result in the middle/title widget +/// stretching to start. /// /// ## Sample code /// @@ -126,10 +128,14 @@ class _ToolbarContainerLayout extends SingleChildLayoutDelegate { class AppBar extends StatefulWidget implements PreferredSizeWidget { /// Creates a material design app bar. /// + /// The arguments [elevation], [primary], [toolbarOpacity], [bottomOpacity] + /// and [automaticallyImplyLeading] must not be null. + /// /// Typically used in the [Scaffold.appBar] property. AppBar({ Key key, this.leading, + this.automaticallyImplyLeading: true, this.title, this.actions, this.flexibleSpace, @@ -143,7 +149,8 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { this.centerTitle, this.toolbarOpacity: 1.0, this.bottomOpacity: 1.0, - }) : assert(elevation != null), + }) : assert(automaticallyImplyLeading != null), + assert(elevation != null), assert(primary != null), assert(toolbarOpacity != null), assert(bottomOpacity != null), @@ -152,13 +159,21 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { /// A widget to display before the [title]. /// - /// If this is null, the [AppBar] will imply an appropriate widget. For - /// example, if the [AppBar] is in a [Scaffold] that also has a [Drawer], the - /// [Scaffold] will fill this widget with an [IconButton] that opens the - /// drawer. If there's no [Drawer] and the parent [Navigator] can go back, the - /// [AppBar] will use a [BackButton] that calls [Navigator.maybePop]. + /// If this is null and [automaticallyImplyLeading] is set to true, the [AppBar] will + /// imply an appropriate widget. For example, if the [AppBar] is in a [Scaffold] + /// that also has a [Drawer], the [Scaffold] will fill this widget with an + /// [IconButton] that opens the drawer. If there's no [Drawer] and the parent + /// [Navigator] can go back, the [AppBar] will use a [BackButton] that calls + /// [Navigator.maybePop]. final Widget leading; + /// Controls whether we should try to imply the leading widget if null. + /// + /// If true and [leading] is null, automatically try to deduce what the leading + /// widget should be. If false and [leading] is null, leading space is given to [title]. + /// If leading widget is not null, this parameter has no effect. + final bool automaticallyImplyLeading; + /// The primary widget displayed in the appbar. /// /// Typically a [Text] widget containing a description of the current contents @@ -332,7 +347,7 @@ class _AppBarState extends State { } Widget leading = widget.leading; - if (leading == null) { + if (leading == null && widget.automaticallyImplyLeading) { if (hasDrawer) { leading = new IconButton( icon: const Icon(Icons.menu), @@ -499,6 +514,7 @@ class _FloatingAppBarState extends State<_FloatingAppBar> { class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { _SliverAppBarDelegate({ @required this.leading, + @required this.automaticallyImplyLeading, @required this.title, @required this.actions, @required this.flexibleSpace, @@ -521,6 +537,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { _bottomHeight = bottom?.preferredSize?.height ?? 0.0; final Widget leading; + final bool automaticallyImplyLeading; final Widget title; final List actions; final Widget flexibleSpace; @@ -562,6 +579,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { toolbarOpacity: toolbarOpacity, child: new AppBar( leading: leading, + automaticallyImplyLeading: automaticallyImplyLeading, title: title, actions: actions, flexibleSpace: flexibleSpace, @@ -583,6 +601,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { @override bool shouldRebuild(covariant _SliverAppBarDelegate oldDelegate) { return leading != oldDelegate.leading + || automaticallyImplyLeading != oldDelegate.automaticallyImplyLeading || title != oldDelegate.title || actions != oldDelegate.actions || flexibleSpace != oldDelegate.flexibleSpace @@ -660,9 +679,13 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { /// * class SliverAppBar extends StatefulWidget { /// Creates a material design app bar that can be placed in a [CustomScrollView]. + /// + /// The arguments [forceElevated], [primary], [floating], [pinned], [snap] + /// and [automaticallyImplyLeading] must not be null. const SliverAppBar({ Key key, this.leading, + this.automaticallyImplyLeading: true, this.title, this.actions, this.flexibleSpace, @@ -679,7 +702,8 @@ class SliverAppBar extends StatefulWidget { this.floating: false, this.pinned: false, this.snap: false, - }) : assert(forceElevated != null), + }) : assert(automaticallyImplyLeading != null), + assert(forceElevated != null), assert(primary != null), assert(floating != null), assert(pinned != null), @@ -690,13 +714,21 @@ class SliverAppBar extends StatefulWidget { /// A widget to display before the [title]. /// - /// If this is null, the [AppBar] will imply an appropriate widget. For - /// example, if the [AppBar] is in a [Scaffold] that also has a [Drawer], the - /// [Scaffold] will fill this widget with an [IconButton] that opens the - /// drawer. If there's no [Drawer] and the parent [Navigator] can go back, the - /// [AppBar] will use an [IconButton] that calls [Navigator.pop]. + /// If this is null and [automaticallyImplyLeading] is set to true, the [AppBar] will + /// imply an appropriate widget. For example, if the [AppBar] is in a [Scaffold] + /// that also has a [Drawer], the [Scaffold] will fill this widget with an + /// [IconButton] that opens the drawer. If there's no [Drawer] and the parent + /// [Navigator] can go back, the [AppBar] will use a [BackButton] that calls + /// [Navigator.maybePop]. final Widget leading; + /// Controls whether we should try to imply the leading widget if null. + /// + /// If true and [leading] is null, automatically try to deduce what the leading + /// widget should be. If false and [leading] is null, leading space is given to [title]. + /// If leading widget is not null, this parameter has no effect. + final bool automaticallyImplyLeading; + /// The primary widget displayed in the appbar. /// /// Typically a [Text] widget containing a description of the current contents @@ -893,6 +925,7 @@ class _SliverAppBarState extends State with TickerProviderStateMix pinned: widget.pinned, delegate: new _SliverAppBarDelegate( leading: widget.leading, + automaticallyImplyLeading: widget.automaticallyImplyLeading, title: widget.title, actions: widget.actions, flexibleSpace: widget.flexibleSpace, diff --git a/packages/flutter/lib/src/material/back_button.dart b/packages/flutter/lib/src/material/back_button.dart index 57977c4f1f38d..e42cbdd0e1fef 100644 --- a/packages/flutter/lib/src/material/back_button.dart +++ b/packages/flutter/lib/src/material/back_button.dart @@ -59,7 +59,8 @@ class BackButtonIcon extends StatelessWidget { /// See also: /// /// * [AppBar], which automatically uses a [BackButton] in its -/// [AppBar.leading] slot when appropriate. +/// [AppBar.leading] slot when the [Scaffold] has no [Drawer] and the +/// current [Route] is not the [Navigator]'s first route. /// * [BackButtonIcon], which is useful if you need to create a back button /// that responds differently to being pressed. /// * [IconButton], which is a more general widget for creating buttons with diff --git a/packages/flutter/lib/src/material/date_picker.dart b/packages/flutter/lib/src/material/date_picker.dart index 233aa3db8ebd1..e88577e439e71 100644 --- a/packages/flutter/lib/src/material/date_picker.dart +++ b/packages/flutter/lib/src/material/date_picker.dart @@ -268,13 +268,19 @@ class DayPicker extends StatelessWidget { } // Do not use this directly - call getDaysInMonth instead. - static const List _kDaysInMonth = const [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + static const List _kDaysInMonth = const [31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + /// Returns the number of days in a month, according to the proleptic + /// Gregorian calendar. + /// + /// This applies the leap year logic introduced by the Gregorian reforms of + /// 1582. It will not give valid results for dates prior to that time. static int getDaysInMonth(int year, int month) { if (month == DateTime.FEBRUARY) { final bool isLeapYear = (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0); if (isLeapYear) return 29; + return 28; } return _kDaysInMonth[month - 1]; } diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index fa991205cb8b6..0e53d084251be 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -19,12 +19,18 @@ class _InputDecoratorChildGlobalKey extends GlobalObjectKey { /// Text and styles used to label an input field. /// +/// The [TextField] and [InputDecorator] classes use [InputDecoration] objects +/// to describe their decoration. (In fact, this class is merely the +/// configuration of an [InputDecorator], which does all the heavy lifting.) +/// /// See also: /// /// * [TextField], which is a text input widget that uses an /// [InputDecoration]. /// * [InputDecorator], which is a widget that draws an [InputDecoration] /// around an arbitrary child widget. +/// * [Decoration] and [DecoratedBox], for drawing arbitrary decorations +/// around other widgets. @immutable class InputDecoration { /// Creates a bundle of text and styles used to label an input field. @@ -307,15 +313,22 @@ class InputDecoration { /// Use [InputDecorator] to create widgets that look and behave like a /// [TextField] but can be used to input information other than text. /// +/// The configuration of this widget is primarily provided in the form of an +/// [InputDecoration] object. +/// /// Requires one of its ancestors to be a [Material] widget. /// /// See also: /// -/// * [TextField], which uses an [InputDecorator] to draw labels and other +/// * [TextField], which uses an [InputDecorator] to draw labels and other /// visual elements around a text entry widget. +/// * [Decoration] and [DecoratedBox], for drawing arbitrary decorations +/// around other widgets. class InputDecorator extends StatelessWidget { /// Creates a widget that displayes labels and other visual elements similar /// to a [TextField]. + /// + /// The [isFocused] and [isEmpty] arguments must not be null. const InputDecorator({ Key key, @required this.decoration, @@ -324,7 +337,9 @@ class InputDecorator extends StatelessWidget { this.isFocused: false, this.isEmpty: false, this.child, - }) : super(key: key); + }) : assert(isFocused != null), + assert(isEmpty != null), + super(key: key); /// The text and styles to use when decorating the child. final InputDecoration decoration; diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 2c749989ffebd..69852b1afa68f 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.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/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -166,8 +167,9 @@ class TextField extends StatefulWidget { /// field. final ValueChanged onSubmitted; - /// Optional input validation and formatting overrides. Formatters are run - /// in the provided order when the text input changes. + /// Optional input validation and formatting overrides. + /// + /// Formatters are run in the provided order when the text input changes. final List inputFormatters; @override @@ -257,7 +259,9 @@ class _TextFieldState extends State { maxLines: widget.maxLines, cursorColor: themeData.textSelectionColor, selectionColor: themeData.textSelectionColor, - selectionControls: materialTextSelectionControls, + selectionControls: themeData.platform == TargetPlatform.iOS + ? cupertinoTextSelectionControls + : materialTextSelectionControls, onChanged: widget.onChanged, onSubmitted: widget.onSubmitted, onSelectionChanged: (TextSelection _, bool longPress) => _onSelectionChanged(context, longPress), diff --git a/packages/flutter/lib/src/material/text_selection.dart b/packages/flutter/lib/src/material/text_selection.dart index 719f466e15875..fd5dd3636ef6e 100644 --- a/packages/flutter/lib/src/material/text_selection.dart +++ b/packages/flutter/lib/src/material/text_selection.dart @@ -2,7 +2,6 @@ // 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:math' as math; import 'package:flutter/widgets.dart'; @@ -13,32 +12,47 @@ import 'flat_button.dart'; import 'material.dart'; import 'theme.dart'; -const double _kHandleSize = 22.0; // pixels -const double _kToolbarScreenPadding = 8.0; // pixels +const double _kHandleSize = 22.0; +// Minimal padding from all edges of the selection toolbar to all edges of the +// viewport. +const double _kToolbarScreenPadding = 8.0; /// Manages a copy/paste text selection toolbar. class _TextSelectionToolbar extends StatelessWidget { - const _TextSelectionToolbar(this.delegate, {Key key}) : super(key: key); + const _TextSelectionToolbar({ + Key key, + this.delegate, + this.handleCut, + this.handleCopy, + this.handlePaste, + this.handleSelectAll, + }) : super(key: key); final TextSelectionDelegate delegate; TextEditingValue get value => delegate.textEditingValue; + final VoidCallback handleCut; + final VoidCallback handleCopy; + final VoidCallback handlePaste; + final VoidCallback handleSelectAll; + @override Widget build(BuildContext context) { final List items = []; if (!value.selection.isCollapsed) { - items.add(new FlatButton(child: const Text('CUT'), onPressed: _handleCut)); - items.add(new FlatButton(child: const Text('COPY'), onPressed: _handleCopy)); + items.add(new FlatButton(child: const Text('CUT'), onPressed: handleCut)); + items.add(new FlatButton(child: const Text('COPY'), onPressed: handleCopy)); } items.add(new FlatButton( child: const Text('PASTE'), - // TODO(mpcomplete): This should probably be grayed-out if there is nothing to paste. - onPressed: _handlePaste + // TODO(https://github.com/flutter/flutter/issues/11254): + // This should probably be grayed-out if there is nothing to paste. + onPressed: handlePaste, )); if (value.text.isNotEmpty) { if (value.selection.isCollapsed) - items.add(new FlatButton(child: const Text('SELECT ALL'), onPressed: _handleSelectAll)); + items.add(new FlatButton(child: const Text('SELECT ALL'), onPressed: handleSelectAll)); } return new Material( @@ -49,43 +63,6 @@ class _TextSelectionToolbar extends StatelessWidget { ) ); } - - void _handleCut() { - Clipboard.setData(new ClipboardData(text: value.selection.textInside(value.text))); - delegate.textEditingValue = new TextEditingValue( - text: value.selection.textBefore(value.text) + value.selection.textAfter(value.text), - selection: new TextSelection.collapsed(offset: value.selection.start) - ); - delegate.hideToolbar(); - } - - void _handleCopy() { - Clipboard.setData(new ClipboardData(text: value.selection.textInside(value.text))); - delegate.textEditingValue = new TextEditingValue( - text: value.text, - selection: new TextSelection.collapsed(offset: value.selection.end) - ); - delegate.hideToolbar(); - } - - Future _handlePaste() async { - final TextEditingValue value = this.value; // Snapshot the input before using `await`. - final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain); - if (data != null) { - delegate.textEditingValue = new TextEditingValue( - text: value.selection.textBefore(value.text) + data.text + value.selection.textAfter(value.text), - selection: new TextSelection.collapsed(offset: value.selection.start + data.text.length) - ); - } - delegate.hideToolbar(); - } - - void _handleSelectAll() { - delegate.textEditingValue = new TextEditingValue( - text: value.text, - selection: new TextSelection(baseOffset: 0, extentOffset: value.text.length) - ); - } } /// Centers the toolbar around the given position, ensuring that it remains on @@ -172,14 +149,20 @@ class _MaterialTextSelectionControls extends TextSelectionControls { globalEditableRegion, position, ), - child: new _TextSelectionToolbar(delegate), + child: new _TextSelectionToolbar( + delegate: delegate, + handleCut: () => handleCut(delegate), + handleCopy: () => handleCopy(delegate), + handlePaste: () => handlePaste(delegate), + handleSelectAll: () => handleSelectAll(delegate), + ), ) ); } /// Builder for material-style text selection handles. @override - Widget buildHandle(BuildContext context, TextSelectionHandleType type) { + Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textHeight) { final Widget handle = new SizedBox( width: _kHandleSize, height: _kHandleSize, diff --git a/packages/flutter/lib/src/painting/basic_types.dart b/packages/flutter/lib/src/painting/basic_types.dart index f4c026b57b057..80d0465646092 100644 --- a/packages/flutter/lib/src/painting/basic_types.dart +++ b/packages/flutter/lib/src/painting/basic_types.dart @@ -63,7 +63,7 @@ export 'dart:ui' show /// For example, [layout] (index 3) implies [paint] (2). enum RenderComparison { /// The two objects are identical (meaning deeply equal, not necessarily - /// [identical]). + /// [dart:core.identical]). identical, /// The two objects are identical for the purpose of layout, but may be different diff --git a/packages/flutter/lib/src/painting/text_span.dart b/packages/flutter/lib/src/painting/text_span.dart index feb610e4a2e7b..0738f3defbc75 100644 --- a/packages/flutter/lib/src/painting/text_span.dart +++ b/packages/flutter/lib/src/painting/text_span.dart @@ -11,19 +11,6 @@ import 'package:flutter/services.dart'; import 'basic_types.dart'; import 'text_style.dart'; -// 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 += 1) { - if (a[i] != b[i]) - return false; - } - return true; -} - /// An immutable span of text. /// /// A [TextSpan] object can be styled using its [style] property. @@ -360,7 +347,7 @@ class TextSpan { return typedOther.text == text && typedOther.style == style && typedOther.recognizer == recognizer - && _deepEquals(typedOther.children, children); + && listEquals(typedOther.children, children); } @override diff --git a/packages/flutter/lib/src/rendering/animated_size.dart b/packages/flutter/lib/src/rendering/animated_size.dart index 28f34a2231cb4..61fe33b68fa3a 100644 --- a/packages/flutter/lib/src/rendering/animated_size.dart +++ b/packages/flutter/lib/src/rendering/animated_size.dart @@ -156,14 +156,18 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { _lastValue = _controller.value; _hasVisualOverflow = false; - if (child == null) { + if (child == null || constraints.isTight) { + _controller.stop(); size = _sizeTween.begin = _sizeTween.end = constraints.smallest; + _state = RenderAnimatedSizeState.start; + child?.layout(constraints); return; } child.layout(constraints, parentUsesSize: true); - switch(_state) { + assert(_state != null); + switch (_state) { case RenderAnimatedSizeState.start: _layoutStart(); break; @@ -176,8 +180,6 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { case RenderAnimatedSizeState.unstable: _layoutUnstable(); break; - default: - throw new StateError('$runtimeType is in an invalid state $_state'); } size = constraints.constrain(_animatedSize); @@ -198,7 +200,7 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { /// We have the initial size to animate from, but we do not have the target /// size to animate to, so we set both ends to child's size. void _layoutStart() { - _sizeTween.begin = _sizeTween.end = child.size; + _sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size); _state = RenderAnimatedSizeState.stable; } @@ -209,12 +211,12 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { /// animation. void _layoutStable() { if (_sizeTween.end != child.size) { - _sizeTween.end = child.size; + _sizeTween.end = debugAdoptSize(child.size); _restartAnimation(); _state = RenderAnimatedSizeState.changed; } else if (_controller.value == _controller.upperBound) { // Animation finished. Reset target sizes. - _sizeTween.begin = _sizeTween.end = child.size; + _sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size); } } @@ -227,7 +229,7 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { void _layoutChanged() { if (_sizeTween.end != child.size) { // Child size changed again. Match the child's size and restart animation. - _sizeTween.begin = _sizeTween.end = child.size; + _sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size); _restartAnimation(); _state = RenderAnimatedSizeState.unstable; } else { @@ -242,7 +244,7 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { void _layoutUnstable() { if (_sizeTween.end != child.size) { // Still unstable. Continue tracking the child. - _sizeTween.begin = _sizeTween.end = child.size; + _sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size); _restartAnimation(); } else { // Child size stabilized. diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index fcdb7acf24e1f..5f80fe9400935 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.dart @@ -15,7 +15,7 @@ import 'object.dart'; // This class should only be used in debug builds. class _DebugSize extends Size { - _DebugSize(Size source, this._owner, this._canBeUsedByParent): super.copy(source); + _DebugSize(Size source, this._owner, this._canBeUsedByParent) : super.copy(source); final RenderBox _owner; final bool _canBeUsedByParent; } @@ -856,7 +856,7 @@ class _IntrinsicDimensionsCacheEntry { /// constraints would be growing to fit the parent. /// /// Sizing purely based on the constraints allows the system to make some -/// significant optimisations. Classes that use this approach should override +/// significant optimizations. Classes that use this approach should override /// [sizedByParent] to return true, and then override [performResize] to set the /// [size] using nothing but the constraints, e.g.: /// @@ -882,7 +882,7 @@ class _IntrinsicDimensionsCacheEntry { /// child, passing it a [BoxConstraints] object describing the constraints /// within which the child can render. Passing tight constraints (see /// [BoxConstraints.isTight]) to the child will allow the rendering library to -/// apply some optimisations, as it knows that if the constraints are tight, the +/// apply some optimizations, as it knows that if the constraints are tight, the /// child's dimensions cannot change even if the layout of the child itself /// changes. /// @@ -892,7 +892,7 @@ class _IntrinsicDimensionsCacheEntry { /// then it must specify the `parentUsesSize` argument to the child's [layout] /// function, setting it to true. /// -/// This flag turns off some optimisations; algorithms that do not rely on the +/// This flag turns off some optimizations; algorithms that do not rely on the /// children's sizes will be more efficient. (In particular, relying on the /// child's [size] means that if the child is marked dirty for layout, the /// parent will probably also be marked dirty for layout, unless the @@ -910,7 +910,7 @@ class _IntrinsicDimensionsCacheEntry { /// subclass, and instead of reading the child's size, the parent would read /// whatever the output of [layout] is for that layout protocol. The /// `parentUsesSize` flag is still used to indicate whether the parent is going -/// to read that output, and optimisations still kick in if the child has tight +/// to read that output, and optimizations still kick in if the child has tight /// constraints (as defined by [Constraints.isTight]). /// /// ### Painting @@ -1484,20 +1484,74 @@ abstract class RenderBox extends RenderObject { ); }); assert(() { - if (value is _DebugSize) { - if (value._owner != this) { - assert(value._owner.parent == this); - assert(value._canBeUsedByParent); - } - } + value = debugAdoptSize(value); return true; }); _size = value; + assert(() { debugAssertDoesMeetConstraints(); return true; }); + } + + /// Claims ownership of the given [Size]. + /// + /// In debug mode, the [RenderBox] class verifies that [Size] objects obtained + /// from other [RenderBox] objects are only used according to the semantics of + /// the [RenderBox] protocol, namely that a [Size] from a [RenderBox] can only + /// be used by its parent, and then only if `parentUsesSize` was set. + /// + /// Sometimes, a [Size] that can validly be used ends up no longer being valid + /// over time. The common example is a [Size] taken from a child that is later + /// removed from the parent. In such cases, this method can be called to first + /// check whether the size can legitimately be used, and if so, to then create + /// a new [Size] that can be used going forward, regardless of what happens to + /// the original owner. + Size debugAdoptSize(Size value) { + Size result = value; assert(() { - _size = new _DebugSize(_size, this, debugCanParentUseSize); + if (value is _DebugSize) { + if (value._owner != this) { + if (value._owner.parent != this) { + throw new FlutterError( + 'The size property was assigned a size inappropriately.\n' + 'The following render object:\n' + ' $this\n' + '...was assigned a size obtained from:\n' + ' ${value._owner}\n' + 'However, this second render object is not, or is no longer, a ' + 'child of the first, and it is therefore a violation of the ' + 'RenderBox layout protocol to use that size in the layout of the ' + 'first render object.\n' + 'If the size was obtained at a time where it was valid to read ' + 'the size (because the second render object above was a child ' + 'of the first at the time), then it should be adopted using ' + 'debugAdoptSize at that time.\n' + 'If the size comes from a grandchild or a render object from an ' + 'entirely different part of the render tree, then there is no ' + 'way to be notified when the size changes and therefore attempts ' + 'to read that size are almost certainly a source of bugs. A different ' + 'approach should be used.' + ); + } + if (!value._canBeUsedByParent) { + throw new FlutterError( + 'A child\'s size was used without setting parentUsesSize.\n' + 'The following render object:\n' + ' $this\n' + '...was assigned a size obtained from its child:\n' + ' ${value._owner}\n' + 'However, when the child was laid out, the parentUsesSize argument ' + 'was not set or set to false. Subsequently this transpired to be ' + 'inaccurate: the size was nonetheless used by the parent.\n' + 'It is important to tell the framework if the size will be used or not ' + 'as several important performance optimizations can be made if the ' + 'size will not be used by the parent.' + ); + } + } + } + result = new _DebugSize(value, this, debugCanParentUseSize); return true; }); - assert(() { debugAssertDoesMeetConstraints(); return true; }); + return result; } @override diff --git a/packages/flutter/lib/src/rendering/debug.dart b/packages/flutter/lib/src/rendering/debug.dart index d0302b7b936e8..42430a83a9a10 100644 --- a/packages/flutter/lib/src/rendering/debug.dart +++ b/packages/flutter/lib/src/rendering/debug.dart @@ -147,8 +147,9 @@ bool debugCheckIntrinsicSizes = false; /// * [debugPrintLayouts], which does something similar for layout but using /// console output. /// -/// * [debugPrintRebuildDirtyWidgets], which does something similar for widgets -/// being rebuilt. +/// * [debugProfileBuildsEnabled], which does something similar for widgets +/// being rebuilt, and [debugPrintRebuildDirtyWidgets], its console +/// equivalent. /// /// * The discussion at [RendererBinding.drawFrame]. bool debugProfilePaintsEnabled = false; diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart index b47b2e86c9836..3398cc5ec4b51 100644 --- a/packages/flutter/lib/src/rendering/layer.dart +++ b/packages/flutter/lib/src/rendering/layer.dart @@ -118,6 +118,7 @@ abstract class Layer extends AbstractNode with TreeDiagnosticsMixin { /// /// Picture layers are always leaves in the layer tree. class PictureLayer extends Layer { + /// Creates a leaf layer for the layer tree. PictureLayer(this.canvasBounds); /// The bounds that were used for the canvas that drew this layer's [picture]. diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 1c36d7d775dad..ec0c4e831902a 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/painting.dart'; -import 'package:collection/collection.dart'; import 'package:vector_math/vector_math_64.dart'; import 'box.dart'; @@ -2736,10 +2735,23 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA _onVerticalDragUpdate = onVerticalDragUpdate, super(child); + /// If non-null, the set of actions to allow. Other actions will be omitted, + /// even if their callback is provided. + /// + /// For example, if [onTap] is non-null but [validActions] does not contain + /// [SemanticsAction.tap], then the semantic description of this node will + /// not claim to support taps. + /// + /// This is normally used to filter the actions made available by + /// [onHorizontalDragUpdate] and [onVerticalDragUpdate]. Normally, these make + /// both the right and left, or up and down, actions available. For example, + /// if [onHorizontalDragUpdate] is set but [validActions] only contains + /// [SemanticsAction.scrollLeft], then the [SemanticsAction.scrollRight] + /// action will be omitted. Set get validActions => _validActions; Set _validActions; set validActions(Set value) { - if (const SetEquality().equals(value, _validActions)) + if (setEquals(value, _validActions)) return; _validActions = value; markNeedsSemanticsUpdate(onlyChanges: true); diff --git a/packages/flutter/lib/src/rendering/sliver_persistent_header.dart b/packages/flutter/lib/src/rendering/sliver_persistent_header.dart index 4a1bae5de67fc..1f5f2b82102e4 100644 --- a/packages/flutter/lib/src/rendering/sliver_persistent_header.dart +++ b/packages/flutter/lib/src/rendering/sliver_persistent_header.dart @@ -30,8 +30,13 @@ import 'viewport_offset.dart'; /// /// * hit testing, painting, and other details of the sliver protocol. /// -/// Subclasses must implement [performLayout], [minExtent], and [maxExtent]. +/// Subclasses must implement [performLayout], [minExtent], and [maxExtent], and +/// typically also will implement [updateChild]. abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObjectWithChildMixin, RenderSliverHelpers { + /// Creates a sliver that changes its size when scrolled to the start of the + /// viewport. + /// + /// This is an abstract class; this constructor only initializes the [child]. RenderSliverPersistentHeader({ RenderBox child }) { this.child = child; } @@ -101,6 +106,15 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje super.markNeedsLayout(); } + /// Lays out the [child]. + /// + /// This is called by [performLayout]. It applies the given `scrollOffset` + /// (which need not match the offset given by the [constraints]) and the + /// `maxExtent` (which need not match the value returned by the [maxExtent] + /// getter). + /// + /// The `overlapsContent` argument is passed to [updateChild]. + @protected void layoutChild(double scrollOffset, double maxExtent, { bool overlapsContent: false }) { assert(maxExtent != null); final double shrinkOffset = math.min(scrollOffset, maxExtent); @@ -211,6 +225,8 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje /// /// This sliver makes no effort to avoid overlapping other content. abstract class RenderSliverScrollingPersistentHeader extends RenderSliverPersistentHeader { + /// Creates a sliver that shrinks when it hits the start of the viewport, then + /// scrolls off. RenderSliverScrollingPersistentHeader({ RenderBox child, }) : super(child: child); @@ -247,6 +263,8 @@ abstract class RenderSliverScrollingPersistentHeader extends RenderSliverPersist /// /// This sliver avoids overlapping other earlier slivers where possible. abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistentHeader { + /// Creates a sliver that shrinks when it hits the start of the viewport, then + /// stays pinned there. RenderSliverPinnedPersistentHeader({ RenderBox child, }) : super(child: child); @@ -304,7 +322,15 @@ class FloatingHeaderSnapConfiguration { /// A sliver with a [RenderBox] child which shrinks and scrolls like a /// [RenderSliverScrollingPersistentHeader], but immediately comes back when the /// user scrolls in the reverse direction. +/// +/// See also: +/// +/// * [RenderSliverFloatingPinnedPersistentHeader], which is similar but sticks +/// to the start of the viewport rather than scrolling off. abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersistentHeader { + /// Creates a sliver that shrinks when it hits the start of the viewport, then + /// scrolls off, and comes back immediately when the user reverses the scroll + /// direction. RenderSliverFloatingPersistentHeader({ RenderBox child, FloatingHeaderSnapConfiguration snapConfiguration, @@ -352,7 +378,9 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste _snapConfiguration = value; } - // Update [geometry] and return the new value for [childMainAxisPosition]. + /// Updates [geometry], and returns the new value for [childMainAxisPosition]. + /// + /// This is used by [performLayout]. @protected double updateGeometry() { final double maxExtent = this.maxExtent; @@ -443,7 +471,18 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste } } +/// A sliver with a [RenderBox] child which shrinks and then remains pinned to +/// the start of the viewport like a [RenderSliverPinnedPersistentHeader], but +/// immediately grows when the user scrolls in the reverse direction. +/// +/// See also: +/// +/// * [RenderSliverFloatingPersistentHeader], which is similar but scrolls off +/// the top rather than sticking to it. abstract class RenderSliverFloatingPinnedPersistentHeader extends RenderSliverFloatingPersistentHeader { + /// Creates a sliver that shrinks when it hits the start of the viewport, then + /// stays pinned there, and grows immediately when the user reverses the + /// scroll direction. RenderSliverFloatingPinnedPersistentHeader({ RenderBox child, FloatingHeaderSnapConfiguration snapConfiguration, diff --git a/packages/flutter/lib/src/scheduler/binding.dart b/packages/flutter/lib/src/scheduler/binding.dart index e4e8d2ddf1848..c8121995b2468 100644 --- a/packages/flutter/lib/src/scheduler/binding.dart +++ b/packages/flutter/lib/src/scheduler/binding.dart @@ -8,7 +8,7 @@ import 'dart:developer'; import 'dart:ui' as ui show window; import 'dart:ui' show VoidCallback; -import 'package:collection/collection.dart'; +import 'package:collection/collection.dart' show PriorityQueue, HeapPriorityQueue; import 'package:flutter/foundation.dart'; import 'debug.dart'; diff --git a/packages/flutter/lib/src/widgets/animated_cross_fade.dart b/packages/flutter/lib/src/widgets/animated_cross_fade.dart index 307ea068dee09..7311a1df47e34 100644 --- a/packages/flutter/lib/src/widgets/animated_cross_fade.dart +++ b/packages/flutter/lib/src/widgets/animated_cross_fade.dart @@ -24,6 +24,42 @@ enum CrossFadeState { showSecond, } +/// Signature for the [AnimatedCrossFade.layoutBuilder] callback. +/// +/// The `topChild` is the child fading in, which is normally drawn on top. The +/// `bottomChild` is the child fading out, normally drawn on the bottom. +/// +/// For good performance, the returned widget tree should contain both the +/// `topChild` and the `bottomChild`; the depth of the tree, and the types of +/// the widgets in the tree, from the returned widget to each of the children +/// should be the same; and where there is a widget with multiple children, the +/// top child and the bottom child should be keyed using the provided +/// `topChildKey` and `bottomChildKey` keys respectively. +/// +/// ## Sample code +/// +/// ```dart +/// Widget defaultLayoutBuilder(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey) { +/// return new Stack( +/// fit: StackFit.loose, +/// children: [ +/// new Positioned( +/// key: bottomChildKey, +/// left: 0.0, +/// top: 0.0, +/// right: 0.0, +/// child: bottomChild, +/// ), +/// new Positioned( +/// key: topChildKey, +/// child: topChild, +/// ) +/// ], +/// ); +/// } +/// ``` +typedef Widget AnimatedCrossFadeBuilder(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey); + /// A widget that cross-fades between two given children and animates itself /// between their sizes. /// @@ -70,6 +106,8 @@ class AnimatedCrossFade extends StatefulWidget { /// The [duration] of the animation is the same for all components (fade in, /// fade out, and size), and you can pass [Interval]s instead of [Curve]s in /// order to have finer control, e.g., creating an overlap between the fades. + /// + /// All the arguments other than [key] must be non-null. const AnimatedCrossFade({ Key key, @required this.firstChild, @@ -79,10 +117,17 @@ class AnimatedCrossFade extends StatefulWidget { this.sizeCurve: Curves.linear, this.alignment: FractionalOffset.topCenter, @required this.crossFadeState, - @required this.duration - }) : assert(firstCurve != null), + @required this.duration, + this.layoutBuilder: defaultLayoutBuilder, + }) : assert(firstChild != null), + assert(secondChild != null), + assert(firstCurve != null), assert(secondCurve != null), assert(sizeCurve != null), + assert(alignment != null), + assert(crossFadeState != null), + assert(duration != null), + assert(layoutBuilder != null), super(key: key); /// The child that is visible when [crossFadeState] is @@ -123,6 +168,49 @@ class AnimatedCrossFade extends StatefulWidget { /// Defaults to [FractionalOffset.topCenter]. final FractionalOffset alignment; + /// A builder that positions the [firstChild] and [secondChild] widgets. + /// + /// The widget returned by this method is wrapped in an [AnimatedSize]. + /// + /// By default, this uses [AnimatedCrossFade.defaultLayoutBuilder], which uses + /// a [Stack] and aligns the `bottomChild` to the top of the stack while + /// providing the `topChild` as the non-positioned child to fill the provided + /// constraints. This works well when the [AnimatedCrossFade] is in a position + /// to change size and when the children are not flexible. However, if the + /// children are less fussy about their sizes (for example a + /// [CircularProgressIndicator] inside a [Center]), or if the + /// [AnimatedCrossFade] is being forced to a particular size, then it can + /// result in the widgets jumping about when the cross-fade state is changed. + final AnimatedCrossFadeBuilder layoutBuilder; + + /// The default layout algorithm used by [AnimatedCrossFade]. + /// + /// The top child is placed in a stack that sizes itself to match the top + /// child. The bottom child is positioned at the top of the same stack, sized + /// to fit its width but without forcing the height. The stack is then + /// clipped. + /// + /// This is the default value for [layoutBuilder]. It implements + /// [AnimatedCrossFadeBuilder]. + static Widget defaultLayoutBuilder(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey) { + return new Stack( + overflow: Overflow.visible, + children: [ + new Positioned( + key: bottomChildKey, + left: 0.0, + top: 0.0, + right: 0.0, + child: bottomChild, + ), + new Positioned( + key: topChildKey, + child: topChild, + ) + ], + ); + } + @override _AnimatedCrossFadeState createState() => new _AnimatedCrossFadeState(); @@ -203,7 +291,8 @@ class _AnimatedCrossFadeState extends State with TickerProvid /// Whether we're in the middle of cross-fading this frame. bool get _isTransitioning => _controller.status == AnimationStatus.forward || _controller.status == AnimationStatus.reverse; - List _buildCrossFadedChildren() { + @override + Widget build(BuildContext context) { const Key kFirstChildKey = const ValueKey(CrossFadeState.showFirst); const Key kSecondChildKey = const ValueKey(CrossFadeState.showSecond); final bool transitioningForwards = _controller.status == AnimationStatus.completed || _controller.status == AnimationStatus.forward; @@ -230,54 +319,35 @@ class _AnimatedCrossFadeState extends State with TickerProvid bottomAnimation = _secondAnimation; } - return [ - new TickerMode( - key: bottomKey, - enabled: _isTransitioning, - child: new Positioned( - // TODO(dragostis): Add a way to crop from top right for - // right-to-left languages. - left: 0.0, - top: 0.0, - right: 0.0, - child: new ExcludeSemantics( - excluding: true, // always exclude the semantics of the widget that's fading out - child: new FadeTransition( - opacity: bottomAnimation, - child: bottomChild, - ), - ), + bottomChild = new TickerMode( + key: bottomKey, + enabled: _isTransitioning, + child: new ExcludeSemantics( + excluding: true, // Always exclude the semantics of the widget that's fading out. + child: new FadeTransition( + opacity: bottomAnimation, + child: bottomChild, ), ), - new TickerMode( - key: topKey, - enabled: true, // top widget always has its animations enabled - child: new Positioned( - child: new ExcludeSemantics( - excluding: false, // always publish semantics for the widget that's fading in - child: new FadeTransition( - opacity: topAnimation, - child: topChild, - ), - ), + ); + topChild = new TickerMode( + key: topKey, + enabled: true, // Top widget always has its animations enabled. + child: new ExcludeSemantics( + excluding: false, // Always publish semantics for the widget that's fading in. + child: new FadeTransition( + opacity: topAnimation, + child: topChild, ), ), - ]; - } - - @override - Widget build(BuildContext context) { + ); return new ClipRect( child: new AnimatedSize( - key: new ValueKey(widget.key), alignment: widget.alignment, duration: widget.duration, curve: widget.sizeCurve, vsync: this, - child: new Stack( - overflow: Overflow.visible, - children: _buildCrossFadedChildren(), - ), + child: widget.layoutBuilder(topChild, topKey, bottomChild, bottomKey), ), ); } diff --git a/packages/flutter/lib/src/widgets/animated_list.dart b/packages/flutter/lib/src/widgets/animated_list.dart index 2b6cb747d99eb..cba11a93d0d65 100644 --- a/packages/flutter/lib/src/widgets/animated_list.dart +++ b/packages/flutter/lib/src/widgets/animated_list.dart @@ -105,6 +105,14 @@ class AnimatedList extends StatefulWidget { /// view is scrolled. /// /// Must be null if [primary] is true. + /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). final ScrollController controller; /// Whether this is the primary scroll view associated with the parent diff --git a/packages/flutter/lib/src/widgets/debug.dart b/packages/flutter/lib/src/widgets/debug.dart index 74113d837fe16..8bfb3cd198bc9 100644 --- a/packages/flutter/lib/src/widgets/debug.dart +++ b/packages/flutter/lib/src/widgets/debug.dart @@ -24,6 +24,10 @@ import 'table.dart'; /// Combined with [debugPrintScheduleBuildForStacks], this lets you watch a /// widget's dirty/clean lifecycle. /// +/// To get similar information but showing it on the timeline available from the +/// Observatory rather than getting it in the console (where it can be +/// overwhelming), consider [debugProfileBuildsEnabled]. +/// /// See also the discussion at [WidgetsBinding.drawFrame]. bool debugPrintRebuildDirtyWidgets = false; @@ -63,6 +67,10 @@ bool debugPrintGlobalKeyedWidgetLifecycle = false; /// /// For details on how to use [Timeline] events in the Dart Observatory to /// optimize your app, see https://fuchsia.googlesource.com/sysui/+/master/docs/performance.md +/// +/// See also [debugProfilePaintsEnabled], which does something similar but for +/// painting, and [debugPrintRebuildDirtyWidgets], which does something similar +/// but reporting the builds to the console. bool debugProfileBuildsEnabled = false; /// Show banners for deprecated widgets. diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 8fa685b4cb3b3..8981c26be192a 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -523,6 +523,46 @@ abstract class Widget { /// having an internal clock-driven state, or depending on some system state, /// consider using [StatefulWidget]. /// +/// ## Performance considerations +/// +/// The [build] method of a stateless widget is typically only called in three +/// situations: the first time the widget is inserted in the tree, when the +/// widget's parent changes its configuration, and when an [InheritedWidget] it +/// depends on changes. +/// +/// If a widget's parent will regularly change the widget's configuration, or if +/// it depends on inherited widgets that frequently change, then it is important +/// to optimize the performance of the [build] method to maintain a fluid +/// rendering performance. +/// +/// There are several techniques one can use to minimize the impact of +/// rebuilding a stateless widget: +/// +/// * Minimize the number of nodes transitively created by the build method and +/// any widgets it creates. For example, instead of an elaborate arrangement +/// of [Row]s, [Column]s, [Padding]s, and [SizedBox]es to position a single +/// child in a particularly fancy manner, consider using just an [Align] or a +/// [CustomSingleChildLayout]. Instead of an intricate layering of multiple +/// [Container]s and with [Decoration]s to draw just the right graphical +/// effect, consider a single [CustomPaint] widget. +/// +/// * Use `const` widgets where possible, and provide a `const` constructor for +/// the widget so that users of the widget can also do so. +/// +/// * Consider refactoring the stateless widget into a stateful widget so that +/// it can use some of the techniques described at [StatefulWidget], such as +/// caching common parts of subtrees and using [GlobalKey]s when changing the +/// tree structure. +/// +/// * If the widget is likely to get rebuilt frequently due to the use of +/// [InheritedWidget]s, consider refactoring the stateless widget into +/// multiple widgets, with the parts of the tree that change being pushed to +/// the leaves. For example instead of building a tree with four widgets, the +/// inner-most widget depending on the [Theme], consider factoring out the +/// part of the build function that builds the inner-most widget into its own +/// widget, so that only the inner-most widget needs to be rebuilt when the +/// theme changes. +/// /// ## Sample code /// /// The following is a skeleton of a stateless widget subclass called `GreenFrog`: @@ -614,6 +654,10 @@ abstract class StatelessWidget extends Widget { /// /// If a widget's [build] method is to depend on anything else, use a /// [StatefulWidget] instead. + /// + /// See also: + /// + /// * The discussion on performance considerations at [StatelessWidget]. @protected Widget build(BuildContext context); } @@ -666,6 +710,66 @@ abstract class StatelessWidget extends Widget { /// eligible for grafting, the widget might be inserted into the new location in /// the same animation frame in which it was removed from the old location. /// +/// ## Performance considerations +/// +/// There are two primary categories of [StatefulWidget]s. +/// +/// The first is one which allocates resources in [State.initState] and disposes +/// of them in [State.dispose], but which does not depend on [InheritedWidget]s +/// or call [State.setState]. Such widgets are commonly used at the root of an +/// application or page, and communicate with subwidgets via [ChangeNotifier]s, +/// [Stream]s, or other such objects. Stateful widgets following such a pattern +/// are relatively cheap (in terms of CPU and GPU cycles), because they are +/// built once then never update. They can, therefore, have somewhat complicated +/// and deep build methods. +/// +/// The second category is widgets that use [State.setState] or depend on +/// [InheritedWidget]s. These will typically rebuild many times during the +/// application's lifetime, and it is therefore important to minimise the impact +/// of rebuilding such a widget. (They may also use [State.initState] or +/// [State.didChangeDependencies] and allocate resources, but the important part +/// is that they rebuild.) +/// +/// There are several techniques one can use to minimize the impact of +/// rebuilding a stateful widget: +/// +/// * Push the state to the leaves. For example, if your page has a ticking +/// clock, rather than putting the state at the top of the page and +/// rebuilding the entire page each time the clock ticks, create a dedicated +/// clock widget that only updates itself. +/// +/// * Minimize the number of nodes transitively created by the build method and +/// any widgets it creates. Ideally, a stateful widget would only create a +/// single widget, and that widget would be a [RenderObjectWidget]. +/// (Obviously this isn't always practical, but the closer a widget gets to +/// this ideal, the more efficient it will be.) +/// +/// * If a subtree does not change, cache the widget that represents that +/// subtree and re-use it each time it can be used. It is massively more +/// efficient for a widget to be re-used than for a new (but +/// identically-configured) widget to be created. Factoring out the stateful +/// part into a widget that takes a child argument is a common way of doing +/// this. +/// +/// * Use `const` widgets where possible. (This is equivalent to caching a +/// widget and re-using it.) +/// +/// * Avoid changing the depth of any created subtrees or changing the type of +/// any widgets in the subtree. For example, rather than returning either the +/// child or the child wrapped in an [IgnorePointer], always wrap the child +/// widget in an [IgnorePointer] and control the [IgnorePointer.ignoring] +/// property. This is because changing the depth of the subtree requires +/// rebuilding, laying out, and painting the entire subtree, whereas just +/// changing the property will require the least possible change to the +/// render tree (in the case of [IgnorePointer], for example, no layout or +/// repaint is necessary at all). +/// +/// * If the depth must be changed for some reason, consider wrapping the +/// common parts of the subtrees in widgets that have a [GlobalKey] that +/// remains consistent for the life of the stateful widget. (The +/// [KeyedSubtree] widget may be useful for this purpose if no other widget +/// can conveniently be assigned the key.) +/// /// ## Sample code /// /// The following is a skeleton of a stateful widget subclass called `YellowBird`: @@ -1238,6 +1342,10 @@ abstract class State { /// rebuilds, but the framework has updated that [State] object's [widget] /// property to refer to the new `MyButton` instance and `${widget.color}` /// prints green, as expected. + /// + /// See also: + /// + /// * The discussion on performance considerations at [StatefulWidget]. @protected Widget build(BuildContext context); diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart index ad3db0e259621..0ea72e1860e7d 100644 --- a/packages/flutter/lib/src/widgets/gesture_detector.dart +++ b/packages/flutter/lib/src/widgets/gesture_detector.dart @@ -535,6 +535,16 @@ class RawGestureDetectorState extends State { } } + /// This method can be called after the build phase, during the layout of the + /// nearest descendant [RenderObjectWidget] of the gesture detector, to filter + /// the list of available semantic actions. + /// + /// This is used by [Scrollable] to configure system accessibility tools so + /// that they know in which direction a particular list can be scrolled. + /// + /// If this is never called, then the actions are not filtered. If the list of + /// actions to filter changes, it must be called again (during the layout of + /// the nearest descendant [RenderObjectWidget] of the gesture detector). void replaceSemanticsActions(Set actions) { assert(() { if (!context.findRenderObject().owner.debugDoingLayout) { diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index 6cc8e4499f6f3..e1f1e6defe347 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -443,10 +443,10 @@ typedef bool RoutePredicate(Route route); /// /// ### Popup routes /// -/// Routes don't have to obscure the entire screen. [PopupRoute]s cover -/// the screen with a barrierColor that can be only partially opaque to -/// allow the current screen to show through. Popup routes are "modal" -/// because they block input to the widgets below. +/// Routes don't have to obscure the entire screen. [PopupRoute]s cover the +/// screen with a [ModalRoute.barrierColor] that can be only partially opaque to +/// allow the current screen to show through. Popup routes are "modal" because +/// they block input to the widgets below. /// /// There are functions which create and show popup routes. For /// example: [showDialog], [showMenu], and [showModalBottomSheet]. These diff --git a/packages/flutter/lib/src/widgets/nested_scroll_view.dart b/packages/flutter/lib/src/widgets/nested_scroll_view.dart index bd6a146dea44d..bebc45004eaab 100644 --- a/packages/flutter/lib/src/widgets/nested_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/nested_scroll_view.dart @@ -33,6 +33,9 @@ import 'ticker_provider.dart'; typedef List NestedScrollViewHeaderSliversBuilder(BuildContext context, bool innerBoxIsScrolled); class NestedScrollView extends StatefulWidget { + /// Creates a nested scroll view. + /// + /// The [reverse], [headerSliverBuilder], and [body] arguments must not be null. const NestedScrollView({ Key key, this.controller, @@ -73,9 +76,18 @@ class NestedScrollView extends StatefulWidget { /// How the scroll view should respond to user input. /// /// For example, determines how the scroll view continues to animate after the - /// user stops dragging the scroll view. + /// user stops dragging the scroll view (providing a custom implementation of + /// [ScrollPhysics.createBallisticSimulation] allows this particular aspect of + /// the physics to be overridden). /// /// Defaults to matching platform conventions. + /// + /// The [ScrollPhysics.applyBoundaryConditions] implementation of the provided + /// object should not allow scrolling outside the scroll extent range + /// described by the [ScrollMetrics.minScrollExtent] and + /// [ScrollMetrics.maxScrollExtent] properties passed to that method. If that + /// invariant is not maintained, the nested scroll view may respond to user + /// scrolling erratically. final ScrollPhysics physics; /// A builder for any widgets that are to precede the inner scroll views (as @@ -135,7 +147,9 @@ class _NestedScrollViewState extends State { return new CustomScrollView( scrollDirection: widget.scrollDirection, reverse: widget.reverse, - physics: new ClampingScrollPhysics(parent: widget.physics), + physics: widget.physics != null + ? widget.physics.applyTo(const ClampingScrollPhysics()) + : const ClampingScrollPhysics(), controller: _coordinator._outerController, slivers: widget._buildSlivers(context, _coordinator._innerController, _coordinator.hasScrolledBody), ); diff --git a/packages/flutter/lib/src/widgets/notification_listener.dart b/packages/flutter/lib/src/widgets/notification_listener.dart index 1a498a58980a6..196e7646f7341 100644 --- a/packages/flutter/lib/src/widgets/notification_listener.dart +++ b/packages/flutter/lib/src/widgets/notification_listener.dart @@ -26,6 +26,8 @@ typedef bool NotificationListenerCallback(T notification /// widgets with the appropriate type parameters that are ancestors of the given /// [BuildContext]. abstract class Notification { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. const Notification(); /// Applied to each ancestor of the [dispatch] target. @@ -107,6 +109,15 @@ class NotificationListener extends StatelessWidget { /// /// The notification's [Notification.visitAncestor] method is called for each /// ancestor, and invokes this callback as appropriate. + /// + /// Notifications vary in terms of when they are dispatched. There are two + /// main possibilities: dispatch between frames, and dispatch during layout. + /// + /// For notifications that dispatch during layout, such as those that inherit + /// from [LayoutChangedNotification], it is too late to call [State.setState] + /// in response to the notification (as layout is currently happening in a + /// descendant, by definition, since notifications bubble up the tree). For + /// widgets that depend on layout, consider a [LayoutBuilder] instead. final NotificationListenerCallback onNotification; bool _dispatch(Notification notification, Element element) { diff --git a/packages/flutter/lib/src/widgets/page_view.dart b/packages/flutter/lib/src/widgets/page_view.dart index 54dba500b886e..64be59cc2b833 100644 --- a/packages/flutter/lib/src/widgets/page_view.dart +++ b/packages/flutter/lib/src/widgets/page_view.dart @@ -34,7 +34,7 @@ import 'viewport.dart'; /// /// See also: /// -/// - [PageView], which is the widget this object controls. +/// * [PageView], which is the widget this object controls. class PageController extends ScrollController { /// Creates a page controller. /// @@ -64,7 +64,7 @@ class PageController extends ScrollController { /// See also: /// /// * [PageStorageKey], which should be used when more than one - //// scrollable appears in the same route, to distinguish the [PageStorage] + /// scrollable appears in the same route, to distinguish the [PageStorage] /// locations used to save scroll offsets. final bool keepPage; @@ -251,6 +251,12 @@ class _PagePosition extends ScrollPositionWithSingleContext { /// Scroll physics used by a [PageView]. /// /// These physics cause the page view to snap to page boundaries. +/// +/// See also: +/// +/// * [ScrollPhysics], the base class which defines the API for scrolling +/// physics. +/// * [PageView.physics], which can override the physics used by a page view. class PageScrollPhysics extends ScrollPhysics { /// Creates physics for a [PageView]. const PageScrollPhysics({ ScrollPhysics parent }) : super(parent: parent); @@ -323,6 +329,8 @@ const PageScrollPhysics _kPagePhysics = const PageScrollPhysics(); /// * [SingleChildScrollView], when you need to make a single child scrollable. /// * [ListView], for a scrollable list of boxes. /// * [GridView], for a scrollable grid of boxes. +/// * [ScollNotification] and [NotificationListener], which can be used to watch +/// the scroll position without using a [ScrollController]. class PageView extends StatefulWidget { /// Creates a scrollable list that works page by page from an explicit [List] /// of widgets. diff --git a/packages/flutter/lib/src/widgets/primary_scroll_controller.dart b/packages/flutter/lib/src/widgets/primary_scroll_controller.dart index 5adac0806643e..cc9c831f1beb9 100644 --- a/packages/flutter/lib/src/widgets/primary_scroll_controller.dart +++ b/packages/flutter/lib/src/widgets/primary_scroll_controller.dart @@ -33,6 +33,11 @@ class PrimaryScrollController extends InheritedWidget { super(key: key, child: child); /// The [ScrollController] associated with the subtree. + /// + /// See also: + /// + /// * [ScrollView.controller], which discusses the purpose of specifying a + /// scroll controller. final ScrollController controller; /// Returns the [ScrollController] most closely associated with the given diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index 80f7cb1ae4a20..bbbd9b75d5fa3 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -722,6 +722,9 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute extends TransitionRoute with LocalHistoryRoute _offstage; bool _offstage = false; set offstage(bool value) { @@ -910,7 +916,7 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute color = new ColorTween( begin: _kTransparent, diff --git a/packages/flutter/lib/src/widgets/scroll_controller.dart b/packages/flutter/lib/src/widgets/scroll_controller.dart index 379ba043400dc..12431eb648d06 100644 --- a/packages/flutter/lib/src/widgets/scroll_controller.dart +++ b/packages/flutter/lib/src/widgets/scroll_controller.dart @@ -40,6 +40,8 @@ import 'scroll_position_with_single_context.dart'; /// [PageView]. /// * [ScrollPosition], which manages the scroll offset for an individual /// scrolling widget. +/// * [ScollNotification] and [NotificationListener], which can be used to watch +/// the scroll position without using a [ScrollController]. class ScrollController extends ChangeNotifier { /// Creates a controller for a scrollable widget. /// @@ -59,8 +61,8 @@ class ScrollController extends ChangeNotifier { /// if [keepScrollOffset] is false or a scroll offset hasn't been saved yet. /// /// Defaults to 0.0. - final double _initialScrollOffset; double get initialScrollOffset => _initialScrollOffset; + 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. @@ -272,7 +274,7 @@ class ScrollController extends ChangeNotifier { // Examples can assume: // TrackingScrollController _trackingScrollController; -/// A [ScrollController] whose `initialScrollOffset` tracks its most recently +/// A [ScrollController] whose [initialScrollOffset] tracks its most recently /// updated [ScrollPosition]. /// /// This class can be used to synchronize the scroll offset of two or more @@ -309,6 +311,8 @@ class ScrollController extends ChangeNotifier { /// In this example the `_trackingController` would have been created by the /// stateful widget that built the widget tree. class TrackingScrollController extends ScrollController { + /// Creates a scroll controller that continually updates its + /// [initialScrollOffset] to match the last scroll notification it received. TrackingScrollController({ double initialScrollOffset: 0.0, bool keepScrollOffset: true, @@ -317,14 +321,20 @@ class TrackingScrollController extends ScrollController { keepScrollOffset: keepScrollOffset, debugLabel: debugLabel); - Map _positionToListener = {}; + final Map _positionToListener = {}; ScrollPosition _lastUpdated; /// The last [ScrollPosition] to change. Returns null if there aren't any - /// attached scroll positions or there hasn't been any scrolling yet. + /// attached scroll positions, or there hasn't been any scrolling yet, or the + /// last [ScrollPosition] to change has since been removed. ScrollPosition get mostRecentlyUpdatedPosition => _lastUpdated; - /// Returns the scroll offset of the [mostRecentlyUpdatedPosition] or 0.0. + /// Returns the scroll offset of the [mostRecentlyUpdatedPosition] or, if that + /// is null, the initial scroll offset provided to the constructor. + /// + /// See also: + /// + /// * [ScrollController.initialScrollOffset], which this overrides. @override double get initialScrollOffset => _lastUpdated?.pixels ?? super.initialScrollOffset; @@ -342,6 +352,8 @@ class TrackingScrollController extends ScrollController { assert(_positionToListener.containsKey(position)); position.removeListener(_positionToListener[position]); _positionToListener.remove(position); + if (_lastUpdated == position) + _lastUpdated = null; } @override @@ -350,7 +362,6 @@ class TrackingScrollController extends ScrollController { assert(_positionToListener.containsKey(position)); position.removeListener(_positionToListener[position]); } - _positionToListener.clear(); super.dispose(); } } diff --git a/packages/flutter/lib/src/widgets/scroll_notification.dart b/packages/flutter/lib/src/widgets/scroll_notification.dart index aa216c9f452d3..96322e7800824 100644 --- a/packages/flutter/lib/src/widgets/scroll_notification.dart +++ b/packages/flutter/lib/src/widgets/scroll_notification.dart @@ -69,6 +69,18 @@ abstract class ViewportNotificationMixin extends Notification { /// [Scrollable] widgets. To focus on notifications from the nearest /// [Scrollable] descendant, check that the [depth] property of the notification /// is zero. +/// +/// When a scroll notification is received by a [NotificationListener], the +/// listener will have already completed build and layout, and it is therefore +/// too late for that widget to call [State.setState]. Any attempt to adjust the +/// build or layout based on a scroll notification would result in a layout that +/// lagged one frame behind, which is a poor user experience. Scroll +/// notifications are therefore primarily useful for paint effects (since paint +/// happens after layout). The [GlowingOverscrollIndicator] and [Scrollbar] +/// widgets are examples of paint effects that use scroll notifications. +/// +/// To drive layout based on the scroll position, consider listening to the +/// [ScrollPosition] directly (or indirectly via a [ScrollController]). abstract class ScrollNotification extends LayoutChangedNotification with ViewportNotificationMixin { /// Initializes fields for subclasses. ScrollNotification({ diff --git a/packages/flutter/lib/src/widgets/scroll_position.dart b/packages/flutter/lib/src/widgets/scroll_position.dart index 103fb0a0e321e..83b13835e1425 100644 --- a/packages/flutter/lib/src/widgets/scroll_position.dart +++ b/packages/flutter/lib/src/widgets/scroll_position.dart @@ -4,7 +4,6 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; @@ -59,6 +58,8 @@ export 'scroll_activity.dart' show ScrollHoldController; /// other scrollable widgets to control a [ScrollPosition]. /// * [ScrollPositionWithSingleContext], which is the most commonly used /// concrete subclass of [ScrollPosition]. +/// * [ScollNotification] and [NotificationListener], which can be used to watch +/// the scroll position without using a [ScrollController]. abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { /// Creates an object that determines which portion of the content is visible /// in a scroll view. @@ -390,7 +391,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { if (pixels < maxScrollExtent) actions.add(forward); - if (const SetEquality().equals(actions, _semanticActions)) + if (setEquals(actions, _semanticActions)) return; _semanticActions = actions; diff --git a/packages/flutter/lib/src/widgets/scroll_view.dart b/packages/flutter/lib/src/widgets/scroll_view.dart index 3f1410066f852..50208bf02dba4 100644 --- a/packages/flutter/lib/src/widgets/scroll_view.dart +++ b/packages/flutter/lib/src/widgets/scroll_view.dart @@ -29,6 +29,9 @@ import 'viewport.dart'; /// [ScrollView] helps orchestrate these pieces by creating the [Scrollable] and /// the viewport and defering to its subclass to create the slivers. /// +/// To control the initial scroll offset of the scroll view, provide a +/// [controller] with its [ScrollController.initialScrollOffset] property set. +/// /// See also: /// /// * [ListView], which is a commonly used [ScrollView] that displays a @@ -39,6 +42,8 @@ import 'viewport.dart'; /// of child widgets. /// * [CustomScrollView], which is a [ScrollView] that creates custom scroll /// effects using slivers. +/// * [ScollNotification] and [NotificationListener], which can be used to watch +/// the scroll position without using a [ScrollController]. abstract class ScrollView extends StatelessWidget { /// Creates a widget that scrolls. /// @@ -84,6 +89,14 @@ abstract class ScrollView extends StatelessWidget { /// view is scrolled. /// /// Must be null if [primary] is true. + /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). final ScrollController controller; /// Whether this is the primary scroll view associated with the parent @@ -233,6 +246,9 @@ abstract class ScrollView extends StatelessWidget { /// list and a grid, use a list of three slivers: [SliverAppBar], [SliverList], /// and [SliverGrid]. /// +/// To control the initial scroll offset of the scroll view, provide a +/// [controller] with its [ScrollController.initialScrollOffset] property set. +/// /// ## Sample code /// /// This sample code shows a scroll view that contains a flexible pinned app @@ -292,6 +308,8 @@ abstract class ScrollView extends StatelessWidget { /// sliver. /// * [SliverAppBar], which is a sliver that displays a header that can expand /// and float as the scroll view scrolls. +/// * [ScollNotification] and [NotificationListener], which can be used to watch +/// the scroll position without using a [ScrollController]. class CustomScrollView extends ScrollView { /// Creates a [ScrollView] that creates custom scroll effects using slivers. /// @@ -406,6 +424,9 @@ abstract class BoxScrollView extends ScrollView { /// a [SliverChildDelegate] can control the algorithm used to estimate the /// size of children that are not actually visible. /// +/// To control the initial scroll offset of the scroll view, provide a +/// [controller] with its [ScrollController.initialScrollOffset] property set. +/// /// ## Sample code /// /// An infinite list of children: @@ -504,6 +525,8 @@ abstract class BoxScrollView extends ScrollView { /// scroll effects using slivers. /// * [ListBody], which arranges its children in a similar manner, but without /// scrolling. +/// * [ScollNotification] and [NotificationListener], which can be used to watch +/// the scroll position without using a [ScrollController]. class ListView extends BoxScrollView { /// Creates a scrollable, linear array of widgets from an explicit [List]. /// @@ -686,6 +709,9 @@ class ListView extends BoxScrollView { /// /// To create a linear array of children, use a [ListView]. /// +/// To control the initial scroll offset of the scroll view, provide a +/// [controller] with its [ScrollController.initialScrollOffset] property set. +/// /// ## Transitioning to [CustomScrollView] /// /// A [GridView] is basically a [CustomScrollView] with a single [SliverGrid] in @@ -785,6 +811,8 @@ class ListView extends BoxScrollView { /// a fixed number of tiles in the cross axis. /// * [SliverGridDelegateWithMaxCrossAxisExtent], which creates a layout with /// tiles that have a maximum cross-axis extent. +/// * [ScollNotification] and [NotificationListener], which can be used to watch +/// the scroll position without using a [ScrollController]. class GridView extends BoxScrollView { /// Creates a scrollable, 2D array of widgets with a custom /// [SliverGridDelegate]. diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index 1284b0e8171a7..51ec6140b2dd4 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -67,6 +67,8 @@ typedef Widget ViewportBuilder(BuildContext context, ViewportOffset position); /// effects using slivers. /// * [SingleChildScrollView], which is a scrollable widget that has a single /// child. +/// * [ScollNotification] and [NotificationListener], which can be used to watch +/// the scroll position without using a [ScrollController]. class Scrollable extends StatefulWidget { /// Creates a widget that scrolls. /// @@ -96,6 +98,14 @@ class Scrollable extends StatefulWidget { /// An object that can be used to control the position to which this widget is /// scrolled. /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). + /// /// See also: /// /// * [ensureVisible], which animates the scroll position to reveal a given 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 9b75768b50f03..e904b8c84d436 100644 --- a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart @@ -83,6 +83,14 @@ class SingleChildScrollView extends StatelessWidget { /// view is scrolled. /// /// Must be null if [primary] is true. + /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). final ScrollController controller; /// Whether this is the primary scroll view associated with the parent diff --git a/packages/flutter/lib/src/widgets/sliver_persistent_header.dart b/packages/flutter/lib/src/widgets/sliver_persistent_header.dart index 6836268398a4f..62f6c59471882 100644 --- a/packages/flutter/lib/src/widgets/sliver_persistent_header.dart +++ b/packages/flutter/lib/src/widgets/sliver_persistent_header.dart @@ -7,28 +7,89 @@ import 'package:flutter/rendering.dart'; import 'framework.dart'; +/// Delegate for configuring a [SliverPersistentHeader]. abstract class SliverPersistentHeaderDelegate { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. const SliverPersistentHeaderDelegate(); + /// The widget to place inside the [SliverPersistentHeader]. + /// + /// The `context` is the [BuildContext] of the sliver. + /// + /// The `shrinkOffset` is a distance from [maxExtent] towards [minExtent] + /// representing the current amount by which the sliver has been shrunk. When + /// the `shrinkOffset` is zero, the contents will be rendered with a dimension + /// of [maxExtent] in the main axis. When `shrinkOffset` equals the difference + /// between [maxExtent] and [minExtent] (a positive number), the contents will + /// be rendered with a dimension of [minExtent] in the main axis. The + /// `shrinkOffset` will always be a positive number in that range. + /// + /// The `overlapsContent` argument is true if subsequent slivers (if any) will + /// be rendered beneath this one, and false if the sliver will not have any + /// contents below it. Typically this is used to decide whether to draw a + /// shadow to simulate the sliver being above the contents below it. Typically + /// this is true when `shrinkOffset` is at its greatest value and false + /// otherwise, but that is not guaranteed. See [NestedScrollView] for an + /// example of a case where `overlapsContent`'s value can be unrelated to + /// `shrinkOffset`. Widget build(BuildContext context, double shrinkOffset, bool overlapsContent); + /// The smallest size to allow the header to reach, when it shrinks at the + /// start of the viewport. + /// + /// This must return a value equal to or less than [maxExtent]. + /// + /// This value should not change over the lifetime of the delegate. It should + /// be based entirely on the constructor arguments passed to the delegate. See + /// [shouldRebuild], which must return true if a new delegate would return a + /// different value. double get minExtent; + /// The size of the header when it is not shrinking at the top of the + /// viewport. + /// + /// This must return a value equal to or greater than [minExtent]. + /// + /// This value should not change over the lifetime of the delegate. It should + /// be based entirely on the constructor arguments passed to the delegate. See + /// [shouldRebuild], which must return true if a new delegate would return a + /// different value. double get maxExtent; - bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate); - /// Specifies how floating headers should animate in and out of view. /// /// If the value of this property is null, then floating headers will /// not animate into place. - @protected + /// + /// This is only used for floating headers (those with + /// [SliverPersistentHeader.floating] set to true). + /// + /// Defaults to null. FloatingHeaderSnapConfiguration get snapConfiguration => null; + + /// Whether this delegate is meaningfully different from the old delegate. + /// + /// If this returns false, then the header might not be rebuilt, even though + /// the instance of the delegate changed. + /// + /// This must return true if `oldDelegate` and this object would return + /// different values for [minExtent], [maxExtent], [snapConfiguration], or + /// would return a meaningfully different widget tree from [build] for the + /// same arguments. + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate); } +/// A sliver whose size varies when the sliver is scrolled to the leading edge +/// of the viewport. +/// +/// This is the layout primitive that [SliverAppBar] uses for its +/// shrinking/growing effect. class SliverPersistentHeader extends StatelessWidget { + /// Creates a sliver that varies its size when it is scrolled to the start of + /// a viewport. + /// + /// The [delegate], [pinned], and [floating] arguments must not be null. const SliverPersistentHeader({ Key key, @required this.delegate, @@ -39,10 +100,32 @@ class SliverPersistentHeader extends StatelessWidget { assert(floating != null), super(key: key); + /// Configuration for the sliver's layout. + /// + /// The delegate provides the following information: + /// + /// * The minimum and maximum dimensions of the sliver. + /// + /// * The builder for generating the widgets of the sliver. + /// + /// * The instructions for snapping the scroll offset, if [floating] is true. final SliverPersistentHeaderDelegate delegate; + /// Whether to stick the header to the start of the viewport once it has + /// reached its minimum size. + /// + /// If this is false, the header will continue scrolling off the screen after + /// it has shrunk to its minimum extent. final bool pinned; + /// Whether the header should immediately grow again if the user reverses + /// scroll direction. + /// + /// If this is false, the header only grows again once the user reaches the + /// part of the viewport that contains the sliver. + /// + /// The [delegate]'s [SliverPersistentHeaderDelegate.snapConfiguration] is + /// ignored unless [floating] is true. final bool floating; @override diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index e5a8846418518..70382b0271f23 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.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:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; @@ -66,9 +68,14 @@ abstract class TextSelectionDelegate { /// An interface for building the selection UI, to be provided by the /// implementor of the toolbar widget. +/// +/// Override text operations such as [handleCut] if needed. abstract class TextSelectionControls { /// Builds a selection handle of the given type. - Widget buildHandle(BuildContext context, TextSelectionHandleType type); + /// + /// The top left corner of this widget is positioned at the bottom of the + /// selection position. + Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight); /// Builds a toolbar near a text selection. /// @@ -77,6 +84,59 @@ abstract class TextSelectionControls { /// Returns the size of the selection handle. Size get handleSize; + + void handleCut(TextSelectionDelegate delegate) { + final TextEditingValue value = delegate.textEditingValue; + Clipboard.setData(new ClipboardData( + text: value.selection.textInside(value.text), + )); + delegate.textEditingValue = new TextEditingValue( + text: value.selection.textBefore(value.text) + + value.selection.textAfter(value.text), + selection: new TextSelection.collapsed( + offset: value.selection.start + ), + ); + delegate.hideToolbar(); + } + + void handleCopy(TextSelectionDelegate delegate) { + final TextEditingValue value = delegate.textEditingValue; + Clipboard.setData(new ClipboardData( + text: value.selection.textInside(value.text), + )); + delegate.textEditingValue = new TextEditingValue( + text: value.text, + selection: new TextSelection.collapsed(offset: value.selection.end), + ); + delegate.hideToolbar(); + } + + Future handlePaste(TextSelectionDelegate delegate) async { + final TextEditingValue value = delegate.textEditingValue; // Snapshot the input before using `await`. + final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain); + if (data != null) { + delegate.textEditingValue = new TextEditingValue( + text: value.selection.textBefore(value.text) + + data.text + + value.selection.textAfter(value.text), + selection: new TextSelection.collapsed( + offset: value.selection.start + data.text.length + ), + ); + } + delegate.hideToolbar(); + } + + void handleSelectAll(TextSelectionDelegate delegate) { + delegate.textEditingValue = new TextEditingValue( + text: delegate.textEditingValue.text, + selection: new TextSelection( + baseOffset: 0, + extentOffset: delegate.textEditingValue.text.length + ), + ); + } } /// An object that manages a pair of text selection handles. @@ -416,7 +476,11 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay new Positioned( left: point.dx, top: point.dy, - child: widget.selectionControls.buildHandle(context, type), + child: widget.selectionControls.buildHandle( + context, + type, + widget.renderObject.size.height / widget.renderObject.maxLines, + ), ), ], ), diff --git a/packages/flutter/test/material/app_bar_test.dart b/packages/flutter/test/material/app_bar_test.dart index 36ac6f744e846..298fa0d818dcb 100644 --- a/packages/flutter/test/material/app_bar_test.dart +++ b/packages/flutter/test/material/app_bar_test.dart @@ -790,6 +790,18 @@ void main() { expect(find.byIcon(Icons.menu), findsOneWidget); }); + testWidgets('AppBar does not draw menu for drawer if automaticallyImplyLeading is false', (WidgetTester tester) async { + await tester.pumpWidget( + new MaterialApp( + home: new Scaffold( + drawer: const Drawer(), + appBar: new AppBar(automaticallyImplyLeading: false), + ), + ), + ); + expect(find.byIcon(Icons.menu), findsNothing); + }); + testWidgets('AppBar handles loose children 0', (WidgetTester tester) async { final GlobalKey key = new GlobalKey(); await tester.pumpWidget( diff --git a/packages/flutter/test/material/app_test.dart b/packages/flutter/test/material/app_test.dart index 470e4973c89ed..34f5c2d094b83 100644 --- a/packages/flutter/test/material/app_test.dart +++ b/packages/flutter/test/material/app_test.dart @@ -106,7 +106,7 @@ void main() { return new Builder( builder: (BuildContext context) { ++buildCounter; - return new Container(); + return const Text('Y'); }, ); }, @@ -129,6 +129,7 @@ void main() { expect(buildCounter, 1); await tester.pump(const Duration(seconds: 1)); expect(buildCounter, 2); + expect(find.text('Y'), findsOneWidget); }); testWidgets('Cannot pop the initial route', (WidgetTester tester) async { @@ -171,7 +172,47 @@ void main() { expect(find.text('route "/b"'), findsNothing); }); - testWidgets('Two-step initial route', (WidgetTester tester) async { + testWidgets('Return value from pop is correct', (WidgetTester tester) async { + Future result; + await tester.pumpWidget( + new MaterialApp( + home: new Builder( + builder: (BuildContext context) { + return new Material( + child: new RaisedButton( + child: const Text('X'), + onPressed: () async { + result = Navigator.of(context).pushNamed('/a'); + } + ), + ); + } + ), + routes: { + '/a': (BuildContext context) { + return new Material( + child: new RaisedButton( + child: const Text('Y'), + onPressed: () { + Navigator.of(context).pop('all done'); + }, + ), + ); + } + }, + ) + ); + await tester.tap(find.text('X')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + expect(find.text('Y'), findsOneWidget); + await tester.tap(find.text('Y')); + await tester.pump(); + + expect(await result, equals('all done')); + }); + + testWidgets('Two-step initial route', (WidgetTester tester) async { final Map routes = { '/': (BuildContext context) => const Text('route "/"'), '/a': (BuildContext context) => const Text('route "/a"'), diff --git a/packages/flutter/test/widgets/animated_cross_fade_test.dart b/packages/flutter/test/widgets/animated_cross_fade_test.dart index 18c2a2afa8c89..14b14f6b963dd 100644 --- a/packages/flutter/test/widgets/animated_cross_fade_test.dart +++ b/packages/flutter/test/widgets/animated_cross_fade_test.dart @@ -21,7 +21,7 @@ void main() { height: 200.0 ), duration: const Duration(milliseconds: 200), - crossFadeState: CrossFadeState.showFirst + crossFadeState: CrossFadeState.showFirst, ) ) ); @@ -43,7 +43,7 @@ void main() { height: 200.0 ), duration: const Duration(milliseconds: 200), - crossFadeState: CrossFadeState.showSecond + crossFadeState: CrossFadeState.showSecond, ) ) ); @@ -69,7 +69,7 @@ void main() { height: 200.0 ), duration: const Duration(milliseconds: 200), - crossFadeState: CrossFadeState.showSecond + crossFadeState: CrossFadeState.showSecond, ) ) ); @@ -183,6 +183,35 @@ void main() { expect(state.ticker.muted, true); expect(findSemantics().excluding, true); }); + + testWidgets('AnimatedCrossFade.layoutBuilder', (WidgetTester tester) async { + await tester.pumpWidget(const AnimatedCrossFade( + firstChild: const Text('AAA'), + secondChild: const Text('BBB'), + crossFadeState: CrossFadeState.showFirst, + duration: const Duration(milliseconds: 50), + )); + expect(find.text('AAA'), findsOneWidget); + expect(find.text('BBB'), findsOneWidget); + await tester.pumpWidget(new AnimatedCrossFade( + firstChild: const Text('AAA'), + secondChild: const Text('BBB'), + crossFadeState: CrossFadeState.showFirst, + duration: const Duration(milliseconds: 50), + layoutBuilder: (Widget a, Key aKey, Widget b, Key bKey) => a, + )); + expect(find.text('AAA'), findsOneWidget); + expect(find.text('BBB'), findsNothing); + await tester.pumpWidget(new AnimatedCrossFade( + firstChild: const Text('AAA'), + secondChild: const Text('BBB'), + crossFadeState: CrossFadeState.showSecond, + duration: const Duration(milliseconds: 50), + layoutBuilder: (Widget a, Key aKey, Widget b, Key bKey) => a, + )); + expect(find.text('BBB'), findsOneWidget); + expect(find.text('AAA'), findsNothing); + }); } class _TickerWatchingWidget extends StatefulWidget { diff --git a/packages/flutter/test/widgets/nested_scroll_view_test.dart b/packages/flutter/test/widgets/nested_scroll_view_test.dart index 3483a9c32bd1a..a46965489b2b4 100644 --- a/packages/flutter/test/widgets/nested_scroll_view_test.dart +++ b/packages/flutter/test/widgets/nested_scroll_view_test.dart @@ -6,7 +6,21 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -Widget buildTest({ ScrollController controller, String title: 'TTTTTTTT' }) { +class _CustomPhysics extends ClampingScrollPhysics { + const _CustomPhysics({ ScrollPhysics parent }) : super(parent: parent); + + @override + _CustomPhysics applyTo(ScrollPhysics ancestor) { + return new _CustomPhysics(parent: buildParent(ancestor)); + } + + @override + Simulation createBallisticSimulation(ScrollMetrics position, double dragVelocity) { + return new ScrollSpringSimulation(spring, 1000.0, 1000.0, 1000.0); + } +} + +Widget buildTest({ ScrollController controller, String title:'TTTTTTTT' }) { return new MediaQuery( data: const MediaQueryData(), child: new Scaffold( @@ -288,4 +302,28 @@ void main() { expect(tester.renderObject(find.byType(AppBar)).size.height, 200.0); }); + testWidgets('NestedScrollViews with custom physics', (WidgetTester tester) async { + await tester.pumpWidget(new MediaQuery( + data: const MediaQueryData(), + child: new NestedScrollView( + physics: const _CustomPhysics(), + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + const SliverAppBar( + floating: true, + title: const Text('AA'), + ), + ]; + }, + body: new Container(), + ))); + expect(find.text('AA'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 500)); + final Offset point1 = tester.getCenter(find.text('AA')); + await tester.dragFrom(point1, const Offset(0.0, 200.0)); + await tester.pump(const Duration(milliseconds: 20)); + final Offset point2 = tester.getCenter(find.text('AA')); + expect(point1.dy, greaterThan(point2.dy)); + }); + } diff --git a/packages/flutter/test/widgets/page_forward_transitions_test.dart b/packages/flutter/test/widgets/page_forward_transitions_test.dart index 31d5d2e4f64d1..a40777912c630 100644 --- a/packages/flutter/test/widgets/page_forward_transitions_test.dart +++ b/packages/flutter/test/widgets/page_forward_transitions_test.dart @@ -27,7 +27,7 @@ class TestTransition extends AnimatedWidget { } class TestRoute extends PageRoute { - TestRoute({ this.child, RouteSettings settings }) : super(settings: settings); + TestRoute({ this.child, RouteSettings settings, this.barrierColor }) : super(settings: settings); final Widget child; @@ -35,7 +35,7 @@ class TestRoute extends PageRoute { Duration get transitionDuration => const Duration(milliseconds: 150); @override - Color get barrierColor => null; + final Color barrierColor; @override bool get maintainState => false; @@ -180,4 +180,31 @@ void main() { expect(state(skipOffstage: false), equals('G')); // route 1 is not around any more }); + + testWidgets('Check onstage/offstage handling of barriers around transitions', (WidgetTester tester) async { + await tester.pumpWidget( + new MaterialApp( + onGenerateRoute: (RouteSettings settings) { + switch (settings.name) { + case '/': return new TestRoute(settings: settings, child: const Text('A')); + case '/1': return new TestRoute(settings: settings, barrierColor: const Color(0xFFFFFF00), child: const Text('B')); + } + } + ) + ); + expect(find.byType(ModalBarrier), findsOneWidget); + + tester.state(find.byType(Navigator)).pushNamed('/1'); + expect(find.byType(ModalBarrier), findsOneWidget); + + await tester.pump(); + expect(find.byType(ModalBarrier), findsNWidgets(2)); + expect(tester.widget(find.byType(ModalBarrier).first).color, isNull); + expect(tester.widget(find.byType(ModalBarrier).last).color, isNull); + + await tester.pump(const Duration(seconds: 1)); + expect(find.byType(ModalBarrier), findsOneWidget); + expect(tester.widget(find.byType(ModalBarrier)).color, const Color(0xFFFFFF00)); + + }); } diff --git a/packages/flutter/test/widgets/page_transitions_test.dart b/packages/flutter/test/widgets/page_transitions_test.dart index 9defaf5ecd214..b47a99cab6779 100644 --- a/packages/flutter/test/widgets/page_transitions_test.dart +++ b/packages/flutter/test/widgets/page_transitions_test.dart @@ -200,7 +200,7 @@ void main() { expect(settingsOffset.dy, 100.0); }); - testWidgets('Check back gesture doesnt start during transitions', (WidgetTester tester) async { + testWidgets('Check back gesture doesn\'t start during transitions', (WidgetTester tester) async { final GlobalKey containerKey1 = new GlobalKey(); final GlobalKey containerKey2 = new GlobalKey(); final Map routes = { diff --git a/packages/flutter_driver/lib/src/driver.dart b/packages/flutter_driver/lib/src/driver.dart index 9b2eec4c86466..291d2e0807151 100644 --- a/packages/flutter_driver/lib/src/driver.dart +++ b/packages/flutter_driver/lib/src/driver.dart @@ -113,11 +113,15 @@ class FlutterDriver { /// Creates a driver that uses a connection provided by the given /// [_serviceClient], [_peer] and [_appIsolate]. @visibleForTesting - FlutterDriver.connectedTo(this._serviceClient, this._peer, this._appIsolate, - { bool printCommunication: false, bool logCommunicationToFile: true }) - : _printCommunication = printCommunication, - _logCommunicationToFile = logCommunicationToFile, - _driverId = _nextDriverId++; + FlutterDriver.connectedTo( + this._serviceClient, + this._peer, + this._appIsolate, { + bool printCommunication: false, + bool logCommunicationToFile: true, + }) : _printCommunication = printCommunication, + _logCommunicationToFile = logCommunicationToFile, + _driverId = _nextDriverId++; static const String _kFlutterExtensionMethod = 'ext.flutter.driver'; static const String _kSetVMTimelineFlagsMethod = '_setVMTimelineFlags'; diff --git a/packages/flutter_driver/lib/src/error.dart b/packages/flutter_driver/lib/src/error.dart index 8cc74e6ed9e0b..98ae71c6bf188 100644 --- a/packages/flutter_driver/lib/src/error.dart +++ b/packages/flutter_driver/lib/src/error.dart @@ -71,7 +71,7 @@ enum LogLevel { critical, } -/// A log entry. +/// A log entry, as emitted on [flutterDriverLog]. class LogRecord { const LogRecord._(this.level, this.loggerName, this.message); diff --git a/packages/flutter_driver/lib/src/extension.dart b/packages/flutter_driver/lib/src/extension.dart index 992c698ab9700..bae8aa3d63b10 100644 --- a/packages/flutter_driver/lib/src/extension.dart +++ b/packages/flutter_driver/lib/src/extension.dart @@ -28,6 +28,10 @@ import 'semantics.dart'; const String _extensionMethodName = 'driver'; const String _extensionMethod = 'ext.flutter.$_extensionMethodName'; +/// Signature for the handler passed to [enableFlutterDriverExtension]. +/// +/// Messages are described in string form and should return a [Future] which +/// eventually completes to a string response. typedef Future DataHandler(String message); class _DriverBinding extends BindingBase with SchedulerBinding, GestureBinding, ServicesBinding, RendererBinding, WidgetsBinding { @@ -72,8 +76,14 @@ typedef Command CommandDeserializerCallback(Map params); /// found, if any, or null otherwise. typedef Finder FinderConstructor(SerializableFinder finder); +/// The class that manages communication between a Flutter Driver test and the +/// application being remote-controlled, on the application side. +/// +/// This is not normally used directly. It is instantiated automatically when +/// calling [enableFlutterDriverExtension]. @visibleForTesting class FlutterDriverExtension { + /// Creates an object to manage a Flutter Driver connection. FlutterDriverExtension(this._requestDataHandler) { _commandHandlers.addAll({ 'get_health': _getHealth, diff --git a/packages/flutter_driver/lib/src/find.dart b/packages/flutter_driver/lib/src/find.dart index 6fd1ceda52e16..a5c831ace0a1d 100644 --- a/packages/flutter_driver/lib/src/find.dart +++ b/packages/flutter_driver/lib/src/find.dart @@ -13,7 +13,7 @@ DriverError _createInvalidKeyValueTypeError(String invalidType) { return new DriverError('Unsupported key value type $invalidType. Flutter Driver only supports ${_supportedKeyValueTypes.join(", ")}'); } -/// A command aimed at an object to be located by [finder]. +/// A Flutter Driver command aimed at an object to be located by [finder]. /// /// Implementations must provide a concrete [kind]. If additional data is /// required beyond the [finder] the implementation may override [serialize] @@ -25,7 +25,7 @@ abstract class CommandWithTarget extends Command { throw new DriverError('$runtimeType target cannot be null'); } - /// Deserializes the command from JSON generated by [serialize]. + /// Deserializes this command from the value generated by [serialize]. CommandWithTarget.deserialize(Map json) : finder = SerializableFinder.deserialize(json), super.deserialize(json); @@ -46,11 +46,8 @@ abstract class CommandWithTarget extends Command { super.serialize()..addAll(finder.serialize()); } -/// Waits until [finder] can locate the target. +/// A Flutter Driver command that waits until [finder] can locate the target. class WaitFor extends CommandWithTarget { - @override - final String kind = 'waitFor'; - /// Creates a command that waits for the widget identified by [finder] to /// appear within the [timeout] amount of time. /// @@ -58,15 +55,26 @@ class WaitFor extends CommandWithTarget { WaitFor(SerializableFinder finder, {Duration timeout}) : super(finder, timeout: timeout); - /// Deserializes the command from JSON generated by [serialize]. + /// Deserializes this command from the value generated by [serialize]. WaitFor.deserialize(Map json) : super.deserialize(json); + + @override + final String kind = 'waitFor'; } -/// Waits until [finder] can no longer locate the target. -class WaitForAbsent extends CommandWithTarget { +/// The result of a [WaitFor] command. +class WaitForResult extends Result { + /// Deserializes the result from JSON. + static WaitForResult fromJson(Map json) { + return new WaitForResult(); + } + @override - final String kind = 'waitForAbsent'; + Map toJson() => {}; +} +/// A Flutter Driver command that waits until [finder] can no longer locate the target. +class WaitForAbsent extends CommandWithTarget { /// Creates a command that waits for the widget identified by [finder] to /// disappear within the [timeout] amount of time. /// @@ -74,31 +82,11 @@ class WaitForAbsent extends CommandWithTarget { WaitForAbsent(SerializableFinder finder, {Duration timeout}) : super(finder, timeout: timeout); - /// Deserializes the command from JSON generated by [serialize]. + /// Deserializes this command from the value generated by [serialize]. WaitForAbsent.deserialize(Map json) : super.deserialize(json); -} -/// Waits until there are no more transient callbacks in the queue. -class WaitUntilNoTransientCallbacks extends Command { @override - final String kind = 'waitUntilNoTransientCallbacks'; - - WaitUntilNoTransientCallbacks({Duration timeout}) : super(timeout: timeout); - - /// Deserializes the command from JSON generated by [serialize]. - WaitUntilNoTransientCallbacks.deserialize(Map json) - : super.deserialize(json); -} - -/// The result of a [WaitFor] command. -class WaitForResult extends Result { - /// Deserializes the result from JSON. - static WaitForResult fromJson(Map json) { - return new WaitForResult(); - } - - @override - Map toJson() => {}; + final String kind = 'waitForAbsent'; } /// The result of a [WaitForAbsent] command. @@ -112,11 +100,34 @@ class WaitForAbsentResult extends Result { Map toJson() => {}; } -/// Describes how to the driver should search for elements. +/// A Flutter Driver command that waits until there are no more transient callbacks in the queue. +class WaitUntilNoTransientCallbacks extends Command { + /// Creates a command that waits for there to be no transient callbacks. + WaitUntilNoTransientCallbacks({ Duration timeout }) : super(timeout: timeout); + + /// Deserializes this command from the value generated by [serialize]. + WaitUntilNoTransientCallbacks.deserialize(Map json) + : super.deserialize(json); + + @override + final String kind = 'waitUntilNoTransientCallbacks'; +} + +/// Base class for Flutter Driver finders, objects that describe how the driver +/// should search for elements. abstract class SerializableFinder { /// Identifies the type of finder to be used by the driver extension. String get finderType; + /// Serializes common fields to JSON. + /// + /// Methods that override [serialize] are expected to call `super.serialize` + /// and add more fields to the returned [Map]. + @mustCallSuper + Map serialize() => { + 'finderType': finderType, + }; + /// Deserializes a finder from JSON generated by [serialize]. static SerializableFinder deserialize(Map json) { final String finderType = json['finderType']; @@ -128,28 +139,19 @@ abstract class SerializableFinder { } throw new DriverError('Unsupported search specification type $finderType'); } - - /// Serializes common fields to JSON. - /// - /// Methods that override [serialize] are expected to call `super.serialize` - /// and add more fields to the returned [Map]. - @mustCallSuper - Map serialize() => { - 'finderType': finderType, - }; } -/// Finds widgets by tooltip text. +/// A Flutter Driver finder that finds widgets by tooltip text. class ByTooltipMessage extends SerializableFinder { - @override - final String finderType = 'ByTooltipMessage'; - /// Creates a tooltip finder given the tooltip's message [text]. ByTooltipMessage(this.text); /// Tooltip message text. final String text; + @override + final String finderType = 'ByTooltipMessage'; + @override Map serialize() => super.serialize()..addAll({ 'text': text, @@ -161,17 +163,17 @@ class ByTooltipMessage extends SerializableFinder { } } -/// Finds widgets by [text] inside a `Text` widget. +/// A Flutter Driver finder that finds widgets by [text] inside a `Text` widget. class ByText extends SerializableFinder { - @override - final String finderType = 'ByText'; - /// Creates a text finder given the text. ByText(this.text); /// The text that appears inside the `Text` widget. final String text; + @override + final String finderType = 'ByText'; + @override Map serialize() => super.serialize()..addAll({ 'text': text, @@ -183,11 +185,8 @@ class ByText extends SerializableFinder { } } -/// Finds widgets by `ValueKey`. +/// A Flutter Driver finder that finds widgets by `ValueKey`. class ByValueKey extends SerializableFinder { - @override - final String finderType = 'ByValueKey'; - /// Creates a finder given the key value. ByValueKey(this.keyValue) : this.keyValueString = '$keyValue', @@ -207,6 +206,9 @@ class ByValueKey extends SerializableFinder { /// May be one of "String", "int". The list of supported types may change. final String keyValueType; + @override + final String finderType = 'ByValueKey'; + @override Map serialize() => super.serialize()..addAll({ 'keyValueString': keyValueString, @@ -228,17 +230,17 @@ class ByValueKey extends SerializableFinder { } } -/// Finds widgets by their [runtimeType]. +/// A Flutter Driver finder that finds widgets by their [runtimeType]. class ByType extends SerializableFinder { - @override - final String finderType = 'ByType'; - /// Creates a finder that given the runtime type in string form. ByType(this.type); /// The widget's [runtimeType], in string form. final String type; + @override + final String finderType = 'ByType'; + @override Map serialize() => super.serialize()..addAll({ 'type': type, @@ -250,16 +252,16 @@ class ByType extends SerializableFinder { } } -/// Command to read the text from a given element. +/// A Flutter Driver command that reads the text from a given element. class GetText extends CommandWithTarget { - @override - final String kind = 'get_text'; - /// [finder] looks for an element that contains a piece of text. GetText(SerializableFinder finder, { Duration timeout }) : super(finder, timeout: timeout); - /// Deserializes the command from JSON generated by [serialize]. + /// Deserializes this command from the value generated by [serialize]. GetText.deserialize(Map json) : super.deserialize(json); + + @override + final String kind = 'get_text'; } /// The result of the [GetText] command. diff --git a/packages/flutter_driver/lib/src/frame_sync.dart b/packages/flutter_driver/lib/src/frame_sync.dart index dea7bc5549fde..29e02c8c8e3ae 100644 --- a/packages/flutter_driver/lib/src/frame_sync.dart +++ b/packages/flutter_driver/lib/src/frame_sync.dart @@ -4,21 +4,22 @@ import 'message.dart'; -/// Enables or disables the FrameSync mechanism. +/// A Flutter Driver command that enables or disables the FrameSync mechanism. class SetFrameSync extends Command { - @override - final String kind = 'set_frame_sync'; - + /// Creates a command to toggle the FrameSync mechanism. SetFrameSync(this.enabled, { Duration timeout }) : super(timeout: timeout); - /// Whether frameSync should be enabled or disabled. - final bool enabled; - /// Deserializes this command from the value generated by [serialize]. SetFrameSync.deserialize(Map params) : this.enabled = params['enabled'].toLowerCase() == 'true', super.deserialize(params); + /// Whether frameSync should be enabled or disabled. + final bool enabled; + + @override + final String kind = 'set_frame_sync'; + @override Map serialize() => super.serialize()..addAll({ 'enabled': '$enabled', diff --git a/packages/flutter_driver/lib/src/gesture.dart b/packages/flutter_driver/lib/src/gesture.dart index cf4883247207d..63312ab91e95b 100644 --- a/packages/flutter_driver/lib/src/gesture.dart +++ b/packages/flutter_driver/lib/src/gesture.dart @@ -5,16 +5,16 @@ import 'find.dart'; import 'message.dart'; -/// Taps on a target widget located by [finder]. +/// A Flutter Driver command that taps on a target widget located by [finder]. class Tap extends CommandWithTarget { - @override - final String kind = 'tap'; - /// Creates a tap command to tap on a widget located by [finder]. - Tap(SerializableFinder finder, {Duration timeout}) : super(finder, timeout: timeout); + Tap(SerializableFinder finder, { Duration timeout }) : super(finder, timeout: timeout); - /// Deserializes this command from JSON generated by [serialize]. + /// Deserializes this command from the value generated by [serialize]. Tap.deserialize(Map json) : super.deserialize(json); + + @override + final String kind = 'tap'; } /// The result of a [Tap] command. @@ -29,11 +29,8 @@ class TapResult extends Result { } -/// Command the driver to perform a scrolling action. +/// A Flutter Driver command that commands the driver to perform a scrolling action. class Scroll extends CommandWithTarget { - @override - final String kind = 'scroll'; - /// Creates a scroll command that will attempt to scroll a scrollable view by /// dragging a widget located by the given [finder]. Scroll( @@ -41,11 +38,11 @@ class Scroll extends CommandWithTarget { this.dx, this.dy, this.duration, - this.frequency, - {Duration timeout} - ) : super(finder, timeout: timeout); + this.frequency, { + Duration timeout, + }) : super(finder, timeout: timeout); - /// Deserializes this command from JSON generated by [serialize]. + /// Deserializes this command from the value generated by [serialize]. Scroll.deserialize(Map json) : this.dx = double.parse(json['dx']), this.dy = double.parse(json['dy']), @@ -65,6 +62,9 @@ class Scroll extends CommandWithTarget { /// The frequency in Hz of the generated move events. final int frequency; + @override + final String kind = 'scroll'; + @override Map serialize() => super.serialize()..addAll({ 'dx': '$dx', @@ -85,23 +85,29 @@ class ScrollResult extends Result { Map toJson() => {}; } -/// Command the driver to ensure that the element represented by [finder] -/// has been scrolled completely into view. +/// A Flutter Driver command that commands the driver to ensure that the element +/// represented by [finder] has been scrolled completely into view. class ScrollIntoView extends CommandWithTarget { - @override - final String kind = 'scrollIntoView'; - /// Creates this command given a [finder] used to locate the widget to be /// scrolled into view. ScrollIntoView(SerializableFinder finder, { this.alignment: 0.0, Duration timeout }) : super(finder, timeout: timeout); - /// Deserializes this command from JSON generated by [serialize]. + /// Deserializes this command from the value generated by [serialize]. ScrollIntoView.deserialize(Map json) : this.alignment = double.parse(json['alignment']), super.deserialize(json); + /// How the widget should be aligned. + /// + /// This value is passed to [Scrollable.ensureVisible] as the value of its + /// argument of the same name. + /// + /// Defaults to 0.0. final double alignment; + @override + final String kind = 'scrollIntoView'; + @override Map serialize() => super.serialize()..addAll({ 'alignment': '$alignment', diff --git a/packages/flutter_driver/lib/src/health.dart b/packages/flutter_driver/lib/src/health.dart index dc29205930bda..d6abfcaf6ba19 100644 --- a/packages/flutter_driver/lib/src/health.dart +++ b/packages/flutter_driver/lib/src/health.dart @@ -5,16 +5,16 @@ import 'enum_util.dart'; import 'message.dart'; -/// Requests an application health check. +/// A Flutter Driver command that requests an application health check. class GetHealth extends Command { - @override - final String kind = 'get_health'; - /// Create a health check command. - GetHealth({Duration timeout}) : super(timeout: timeout); + GetHealth({ Duration timeout }) : super(timeout: timeout); - /// Deserializes the command from JSON generated by [serialize]. + /// Deserializes this command from the value generated by [serialize]. GetHealth.deserialize(Map json) : super.deserialize(json); + + @override + final String kind = 'get_health'; } /// A description of application state. @@ -37,16 +37,16 @@ class Health extends Result { assert(status != null); } - /// Deserializes the result from JSON. - static Health fromJson(Map json) { - return new Health(_healthStatusIndex.lookupBySimpleName(json['status'])); - } - /// The status represented by this object. /// /// If the application responded, this will be [HealthStatus.ok]. final HealthStatus status; + /// Deserializes the result from JSON. + static Health fromJson(Map json) { + return new Health(_healthStatusIndex.lookupBySimpleName(json['status'])); + } + @override Map toJson() => { 'status': _healthStatusIndex.toSimpleName(status), diff --git a/packages/flutter_driver/lib/src/matcher_util.dart b/packages/flutter_driver/lib/src/matcher_util.dart index 697973f5007ff..5632f6741a40b 100644 --- a/packages/flutter_driver/lib/src/matcher_util.dart +++ b/packages/flutter_driver/lib/src/matcher_util.dart @@ -10,8 +10,9 @@ MatchResult match(dynamic value, Matcher matcher) { if (matcher.matches(value, matchState)) { return new MatchResult._matched(); } else { - final Description description = - matcher.describeMismatch(value, new _TextDescription(), matchState, false); + final Description description = matcher.describeMismatch( + value, new _TextDescription(), matchState, false, + ); return new MatchResult._mismatched(description.toString()); } } diff --git a/packages/flutter_driver/lib/src/message.dart b/packages/flutter_driver/lib/src/message.dart index 244444410278c..2b37ae4f3c75f 100644 --- a/packages/flutter_driver/lib/src/message.dart +++ b/packages/flutter_driver/lib/src/message.dart @@ -7,9 +7,12 @@ 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 { - Command({Duration timeout}) + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const Command({ Duration timeout }) : this.timeout = timeout ?? const Duration(seconds: 5); + /// Deserializes this command from the value generated by [serialize]. Command.deserialize(Map json) : timeout = new Duration(milliseconds: int.parse(json['timeout'])); diff --git a/packages/flutter_driver/lib/src/render_tree.dart b/packages/flutter_driver/lib/src/render_tree.dart index f65f09a12c571..8eda71d119946 100644 --- a/packages/flutter_driver/lib/src/render_tree.dart +++ b/packages/flutter_driver/lib/src/render_tree.dart @@ -4,30 +4,31 @@ import 'message.dart'; -/// A request for a string representation of the render tree. +/// A Flutter Driver command that requests a string representation of the render tree. class GetRenderTree extends Command { - @override - final String kind = 'get_render_tree'; - - GetRenderTree({Duration timeout}) : super(timeout: timeout); + /// Create a command to request a string representation of the render tree. + GetRenderTree({ Duration timeout }) : super(timeout: timeout); - /// Deserializes the command from JSON generated by [serialize]. + /// Deserializes this command from the value generated by [serialize]. GetRenderTree.deserialize(Map json) : super.deserialize(json); + + @override + final String kind = 'get_render_tree'; } -/// A string representation of the render tree. +/// A string representation of the render tree, the result of a [GetRenderTree] command. class RenderTree extends Result { /// Creates a [RenderTree] object with the given string representation. RenderTree(this.tree); + /// String representation of the render tree. + final String tree; + /// Deserializes the result from JSON. static RenderTree fromJson(Map json) { return new RenderTree(json['tree']); } - /// String representation of the render tree. - final String tree; - @override Map toJson() => { 'tree': tree diff --git a/packages/flutter_driver/lib/src/request_data.dart b/packages/flutter_driver/lib/src/request_data.dart index 097e06727d1e0..06fbf37c6126e 100644 --- a/packages/flutter_driver/lib/src/request_data.dart +++ b/packages/flutter_driver/lib/src/request_data.dart @@ -4,22 +4,23 @@ import 'message.dart'; -/// Send a string and get a string response. +/// A Flutter Driver command that sends a string to the application and expects a +/// string response. class RequestData extends Command { - @override - final String kind = 'request_data'; - /// Create a command that sends a message. RequestData(this.message, { Duration timeout }) : super(timeout: timeout); - /// The message being sent from the test to the application. - final String message; - /// Deserializes this command from the value generated by [serialize]. RequestData.deserialize(Map params) : this.message = params['message'], super.deserialize(params); + /// The message being sent from the test to the application. + final String message; + + @override + final String kind = 'request_data'; + @override Map serialize() => super.serialize()..addAll({ 'message': message, diff --git a/packages/flutter_driver/lib/src/retry.dart b/packages/flutter_driver/lib/src/retry.dart index ff1346fb1037b..cdd6e6455b375 100644 --- a/packages/flutter_driver/lib/src/retry.dart +++ b/packages/flutter_driver/lib/src/retry.dart @@ -17,8 +17,12 @@ typedef bool Predicate(dynamic value); /// /// When the retry time out, the last seen error and stack trace are returned in /// an error [Future]. -Future retry(Action action, Duration timeout, - Duration pauseBetweenRetries, { Predicate predicate }) async { +Future retry( + Action action, + Duration timeout, + Duration pauseBetweenRetries, { + Predicate predicate, +}) async { assert(action != null); assert(timeout != null); assert(pauseBetweenRetries != null); @@ -29,7 +33,7 @@ Future retry(Action action, Duration timeout, dynamic lastStackTrace; bool success = false; - while(!success && sw.elapsed < timeout) { + while (!success && sw.elapsed < timeout) { try { result = await action(); if (predicate == null || predicate(result)) diff --git a/packages/flutter_driver/lib/src/semantics.dart b/packages/flutter_driver/lib/src/semantics.dart index 83cfe8d2987f0..517ce77863a12 100644 --- a/packages/flutter_driver/lib/src/semantics.dart +++ b/packages/flutter_driver/lib/src/semantics.dart @@ -4,21 +4,22 @@ import 'message.dart'; -/// Enables or disables semantics. +/// A Flutter Driver command that enables or disables semantics. class SetSemantics extends Command { - @override - final String kind = 'set_semantics'; - + /// Creates a command that enables or disables semantics. SetSemantics(this.enabled, { Duration timeout }) : super(timeout: timeout); - /// Whether semantics should be enabled or disabled. - final bool enabled; - /// Deserializes this command from the value generated by [serialize]. SetSemantics.deserialize(Map params) : this.enabled = params['enabled'].toLowerCase() == 'true', super.deserialize(params); + /// Whether semantics should be enabled (true) or disabled (false). + final bool enabled; + + @override + final String kind = 'set_semantics'; + @override Map serialize() => super.serialize()..addAll({ 'enabled': '$enabled', @@ -27,8 +28,11 @@ class SetSemantics extends Command { /// The result of a [SetSemantics] command. class SetSemanticsResult extends Result { + /// Create a result with the given [changedState]. SetSemanticsResult(this.changedState); + /// Whether the [SetSemantics] command actually changed the state that the + /// application was in. final bool changedState; /// Deserializes this result from JSON. diff --git a/packages/flutter_driver/lib/src/timeline_summary.dart b/packages/flutter_driver/lib/src/timeline_summary.dart index f0d90f3f2b65d..099d0c8ba978f 100644 --- a/packages/flutter_driver/lib/src/timeline_summary.dart +++ b/packages/flutter_driver/lib/src/timeline_summary.dart @@ -89,8 +89,11 @@ class TimelineSummary { } /// Writes all of the recorded timeline data to a file. - Future writeTimelineToFile(String traceName, - {String destinationDirectory, bool pretty: false}) async { + Future writeTimelineToFile( + String traceName, { + String destinationDirectory, + bool pretty: false, + }) async { destinationDirectory ??= testOutputsDirectory; await fs.directory(destinationDirectory).create(recursive: true); final File file = fs.file(path.join(destinationDirectory, '$traceName.timeline.json')); @@ -98,8 +101,11 @@ class TimelineSummary { } /// Writes [summaryJson] to a file. - Future writeSummaryToFile(String traceName, - {String destinationDirectory, bool pretty: false}) async { + Future writeSummaryToFile( + String traceName, { + String destinationDirectory, + bool pretty: false, + }) async { destinationDirectory ??= testOutputsDirectory; await fs.directory(destinationDirectory).create(recursive: true); final File file = fs.file(path.join(destinationDirectory, '$traceName.timeline_summary.json')); @@ -174,6 +180,10 @@ class TimelineSummary { /// Timing information about an event that happened in the event loop. class TimedEvent { + /// Creates a timed event given begin and end timestamps in microseconds. + TimedEvent(this.beginTimeMicros, this.endTimeMicros) + : this.duration = new Duration(microseconds: endTimeMicros - beginTimeMicros); + /// The timestamp when the event began. final int beginTimeMicros; @@ -182,8 +192,4 @@ class TimedEvent { /// The duration of the event. final Duration duration; - - /// Creates a timed event given begin and end timestamps in microseconds. - TimedEvent(this.beginTimeMicros, this.endTimeMicros) - : this.duration = new Duration(microseconds: endTimeMicros - beginTimeMicros); } diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index a98cce8337a57..8ffee1cfc894f 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -98,9 +98,20 @@ abstract class TestWidgetsFlutterBinding extends BindingBase debugCheckIntrinsicSizes = checkIntrinsicSizes; } + /// The value to set [debugPrint] to while tests are running. + /// + /// This can be used to redirect console output from the framework, or to + /// change the behavior of [debugPrint]. For example, + /// [AutomatedTestWidgetsFlutterBinding] uses it to make [debugPrint] + /// synchronous, disabling its normal throttling behaviour. @protected DebugPrintCallback get debugPrintOverride => debugPrint; + /// The value to set [debugCheckIntrinsicSizes] to while tests are running. + /// + /// This can be used to enable additional checks. For example, + /// [AutomatedTestWidgetsFlutterBinding] sets this to true, so that all tests + /// always run with aggressive intrinsic sizing tests enabled. @protected bool get checkIntrinsicSizes => false; @@ -463,6 +474,9 @@ abstract class TestWidgetsFlutterBinding extends BindingBase 'The value of a foundation debug variable was changed by the test.', debugPrintOverride: debugPrintOverride, )); + assert(debugAssertAllGesturesVarsUnset( + 'The value of a gestures debug variable was changed by the test.', + )); assert(debugAssertAllRenderVarsUnset( 'The value of a rendering debug variable was changed by the test.', debugCheckIntrinsicSizesOverride: checkIntrinsicSizes, diff --git a/packages/flutter_test/lib/src/test_text_input.dart b/packages/flutter_test/lib/src/test_text_input.dart index fd07fd527d893..ed2791b4a3640 100644 --- a/packages/flutter_test/lib/src/test_text_input.dart +++ b/packages/flutter_test/lib/src/test_text_input.dart @@ -27,6 +27,13 @@ class TestTextInput { } int _client = 0; + + /// The last set of arguments that [TextInputConnection.setEditingState] sent + /// to the embedder. + /// + /// This is a map representation of a [TextEditingValue] object. For example, + /// it will have a `text` entry whose value matches the most recent + /// [TextEditingValue.text] that was sent to the embedder. Map editingState; Future _handleTextInputCall(MethodCall methodCall) async { diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 0689738824aab..d800754aeec53 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -524,9 +524,10 @@ class HotRunner extends ResidentRunner { final List reassembleViews = []; for (FlutterDevice device in flutterDevices) { for (FlutterView view in device.views) { + // Check if the isolate is paused, and if so, don't reassemble. Ignore the + // PostPauseEvent event - the client requesting the pause will resume the app. final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent; - if ((pauseEvent != null) && (pauseEvent.isPauseEvent)) { - // Isolate is paused. Don't reassemble. + if (pauseEvent != null && pauseEvent.isPauseEvent && pauseEvent.kind != ServiceEvent.kPausePostRequest) { continue; } reassembleViews.add(view); diff --git a/packages/flutter_tools/templates/create/pubspec.yaml.tmpl b/packages/flutter_tools/templates/create/pubspec.yaml.tmpl index 90255ce96d73b..b6a5c89b10c0a 100644 --- a/packages/flutter_tools/templates/create/pubspec.yaml.tmpl +++ b/packages/flutter_tools/templates/create/pubspec.yaml.tmpl @@ -4,11 +4,15 @@ description: {{description}} dependencies: flutter: sdk: flutter -{{#withDriverTest}} + dev_dependencies: + flutter_test: + sdk: flutter +{{#withDriverTest}} flutter_driver: sdk: flutter {{/withDriverTest}} + {{#withPluginHook}} {{pluginProjectName}}: path: ../ diff --git a/packages/flutter_tools/templates/create/test/widget_test.dart.tmpl b/packages/flutter_tools/templates/create/test/widget_test.dart.tmpl new file mode 100644 index 0000000000000..2c2b24afa8412 --- /dev/null +++ b/packages/flutter_tools/templates/create/test/widget_test.dart.tmpl @@ -0,0 +1,29 @@ +// This is a basic Flutter widget test. +// To perform an interaction with a widget in your test, use the WidgetTester utility that Flutter +// provides. For example, you can send tap and scroll gestures. You can also use WidgetTester to +// find child widgets in the widget tree, read text, and verify that the values of widget properties +// are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../lib/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(new MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/packages/flutter_tools/test/commands/create_test.dart b/packages/flutter_tools/test/commands/create_test.dart index 45558bb6a8948..f8240a09e7a00 100644 --- a/packages/flutter_tools/test/commands/create_test.dart +++ b/packages/flutter_tools/test/commands/create_test.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:flutter_tools/src/base/file_system.dart'; @@ -45,6 +46,7 @@ void main() { 'ios/Runner/AppDelegate.m', 'ios/Runner/main.m', 'lib/main.dart', + 'test/widget_test.dart' ], ); }); @@ -59,7 +61,7 @@ void main() { 'ios/Runner/Runner-Bridging-Header.h', 'lib/main.dart', ], - [ + unexpectedPaths: [ 'android/app/src/main/java/com/yourcompany/flutterproject/MainActivity.java', 'ios/Runner/AppDelegate.h', 'ios/Runner/AppDelegate.m', @@ -83,6 +85,7 @@ void main() { 'example/ios/Runner/main.m', 'example/lib/main.dart', ], + plugin: true, ); }); @@ -101,13 +104,14 @@ void main() { 'example/ios/Runner/Runner-Bridging-Header.h', 'example/lib/main.dart', ], - [ + unexpectedPaths: [ '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', ], + plugin: true, ); }); @@ -119,10 +123,11 @@ void main() { 'android/src/main/java/com/bar/foo/flutterproject/FlutterProjectPlugin.java', 'example/android/app/src/main/java/com/bar/foo/flutterprojectexample/MainActivity.java', ], - [ + unexpectedPaths: [ 'android/src/main/java/com/yourcompany/flutterproject/FlutterProjectPlugin.java', 'example/android/app/src/main/java/com/yourcompany/flutterprojectexample/MainActivity.java', ], + plugin: true, ); }); @@ -163,6 +168,24 @@ void main() { } } + // TODO(pq): enable when sky_shell is available + if (!io.Platform.isWindows) { + // Verify that the sample widget test runs cleanly. + final List args = [ + fs.path.absolute(fs.path.join('bin', 'flutter_tools.dart')), + 'test', + '--no-color', + fs.path.join(projectDir.path, 'test', 'widget_test.dart'), + ]; + + final ProcessResult result = await Process.run( + fs.path.join(dartSdkPath, 'bin', 'dart'), + args, + workingDirectory: projectDir.path, + ); + expect(result.exitCode, 0); + } + // Generated Xcode settings final String xcodeConfigPath = fs.path.join('ios', 'Flutter', 'Generated.xcconfig'); expectExists(xcodeConfigPath); @@ -232,7 +255,7 @@ void main() { Future _createAndAnalyzeProject( Directory dir, List createArgs, List expectedPaths, - [List unexpectedPaths = const []]) async { + { List unexpectedPaths = const [], bool plugin = false }) async { Cache.flutterRoot = '../..'; final CreateCommand command = new CreateCommand(); final CommandRunner runner = createTestCommandRunner(command); @@ -247,14 +270,29 @@ Future _createAndAnalyzeProject( for (String path in unexpectedPaths) { expect(fs.file(fs.path.join(dir.path, path)).existsSync(), false, reason: '$path exists'); } + + if (plugin) { + _analyze(dir.path, target: fs.path.join(dir.path, 'lib', 'flutter_project.dart')); + _analyze(fs.path.join(dir.path, 'example')); + } else { + _analyze(dir.path); + } +} + +void _analyze(String workingDir, {String target}) { final String flutterToolsPath = fs.path.absolute(fs.path.join( 'bin', 'flutter_tools.dart', )); + + final List args = [flutterToolsPath, 'analyze']; + if (target != null) + args.add(target); + final ProcessResult exec = Process.runSync( '$dartSdkPath/bin/dart', - [flutterToolsPath, 'analyze'], - workingDirectory: dir.path, + args, + workingDirectory: workingDir, ); if (exec.exitCode != 0) { print(exec.stdout); diff --git a/packages/flutter_tools/test/version_test.dart b/packages/flutter_tools/test/version_test.dart index efb08433b6152..418651dcbe9fb 100644 --- a/packages/flutter_tools/test/version_test.dart +++ b/packages/flutter_tools/test/version_test.dart @@ -4,7 +4,7 @@ import 'dart:convert'; -import 'package:collection/collection.dart'; +import 'package:collection/collection.dart' show ListEquality; import 'package:mockito/mockito.dart'; import 'package:process/process.dart'; import 'package:quiver/time.dart';