From 3b35c0c9fce3d3f74a045b07c7e7d30e5f75a024 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Thu, 20 Jul 2017 21:34:27 -0700 Subject: [PATCH 01/14] copy the dart sdk directory when we're upgrading it (#11331) --- bin/internal/update_dart_sdk.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 From aa096b50af4722915ddfa873820c5d9f59689ce1 Mon Sep 17 00:00:00 2001 From: xster Date: Fri, 21 Jul 2017 11:33:17 -0400 Subject: [PATCH 02/14] iOS text selection (#11224) Extract common text selection overlay logic from Material to Widget and create a Cupertino version of the overlays --- packages/flutter/lib/cupertino.dart | 1 + .../flutter/lib/src/cupertino/button.dart | 33 +- .../lib/src/cupertino/text_selection.dart | 301 ++++++++++++++++++ .../flutter/lib/src/material/text_field.dart | 10 +- .../lib/src/material/text_selection.dart | 79 ++--- .../lib/src/widgets/text_selection.dart | 68 +++- 6 files changed, 426 insertions(+), 66 deletions(-) create mode 100644 packages/flutter/lib/src/cupertino/text_selection.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/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/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/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, + ), ), ], ), From e4860ef0eb8affbb72c397819fa3412689bd63d9 Mon Sep 17 00:00:00 2001 From: gspencergoog Date: Fri, 21 Jul 2017 11:12:21 -0700 Subject: [PATCH 03/14] Fix Navigator.pop for named routes. (#11289) * Prefix and Suffix support for TextFields * Adding Tests * Removing spurious newline. * Fixing a small problem with the test * Code review changes * Code Review Changes * Review Changes * Export the new StrokeJoin enum * Added example for line styles, and enabled line join styles. * Reverting inadvertent change to main.dart. * Updated due to code review of engine code * Removed example. * Added arguments to named routes, with test. * Fixing some formatting * Fixing Navigator.pop for named routes. * Fixing comment. * Simplifying test. * Fixing new -> const for Text object. * Tiny text change (also to kick a new Travis build) * Added a more realistic test case. * Reverting unintentional iml changes. * Fixing trailing newline * Removing some changes that snuck in. --- packages/flutter/lib/src/material/app.dart | 2 +- packages/flutter/test/material/app_test.dart | 45 +++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) 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/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"'), From c0e9809653471386439c9ed74a48d1ddc78eefd2 Mon Sep 17 00:00:00 2001 From: Phil Quitslund Date: Fri, 21 Jul 2017 11:43:30 -0700 Subject: [PATCH 04/14] Bump Dart SDK to 1.25.0-dev.7.0. (#11342) --- .analysis_options | 2 +- .analysis_options_repo | 2 +- bin/internal/dart-sdk.version | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 From f0c2d5eddc9b75e38878a8b712cbd29179acf3c1 Mon Sep 17 00:00:00 2001 From: Phil Quitslund Date: Fri, 21 Jul 2017 15:11:59 -0700 Subject: [PATCH 05/14] Flutter create widget test template. (#11304) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Flutter create widget test template. Running `flutter create —with-widget-test` produces a test/widget_test.dart sample widget test. * Generate widget test bits in flutter create. * Path fix. * Added types. * Skip shell testing on windows. * formatting fixes * Update test sample to test the sample app. * Formatting tweak. --- .../templates/create/pubspec.yaml.tmpl | 6 ++- .../create/test/widget_test.dart.tmpl | 29 +++++++++++ .../test/commands/create_test.dart | 50 ++++++++++++++++--- 3 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 packages/flutter_tools/templates/create/test/widget_test.dart.tmpl 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); From 1f08bda3043cf25f893c2ec36b58695caee8115a Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Fri, 21 Jul 2017 15:23:55 -0700 Subject: [PATCH 06/14] AnimatedCrossFade layout customisation (#11343) * Optimise AnimatedSize for the tight case. * Remove `default` from a switch statement over enum (so that analyzer will complain if we add enum values). * Adopt the Size since we use it after the child may have changed (which would throw normally). * AnimatedCrossFade.layoutBuilder --- .../lib/src/rendering/animated_size.dart | 20 +-- packages/flutter/lib/src/rendering/box.dart | 80 +++++++-- .../lib/src/widgets/animated_cross_fade.dart | 152 +++++++++++++----- .../widgets/animated_cross_fade_test.dart | 35 +++- 4 files changed, 221 insertions(+), 66 deletions(-) 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/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/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 { From e0f3001fde05e57f0ee73ab106f9b3510aea38db Mon Sep 17 00:00:00 2001 From: Collin Jackson Date: Fri, 21 Jul 2017 18:42:12 -0400 Subject: [PATCH 07/14] Fix physics with NestedScrollView (#11326) * Fix physics with NestedScrollView * Review feedback --- .../lib/src/widgets/nested_scroll_view.dart | 4 +- .../test/widgets/nested_scroll_view_test.dart | 40 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/widgets/nested_scroll_view.dart b/packages/flutter/lib/src/widgets/nested_scroll_view.dart index bd6a146dea44d..2661db7cce712 100644 --- a/packages/flutter/lib/src/widgets/nested_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/nested_scroll_view.dart @@ -135,7 +135,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/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)); + }); + } From 8f56f6fdd1c6b4586518b927623bc87341452b50 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Fri, 21 Jul 2017 16:39:04 -0700 Subject: [PATCH 08/14] Add documentation and clean up code. (#11330) Mainly, this adds documentation to members that were previously lacking documentation. It also adds a big block of documentation about improving performance of widgets. This also removes some references to package:collection and adds global setEquals and listEquals methods in foundation that we can use. (setEquals in particular should be much faster than the package:collection equivalent, though both should be faster as they avoid allocating new objects.) All remaining references now qualify the import so we know what our remaining dependencies are. Also lots of code reordering in Flutter driver to make the code consistent and apply the style guide more thoroughly. --- dev/devicelab/test/adb_test.dart | 2 +- packages/flutter/lib/foundation.dart | 1 + .../lib/src/foundation/annotations.dart | 3 + .../lib/src/foundation/collections.dart | 47 +++++++ .../flutter/lib/src/material/back_button.dart | 3 +- .../flutter/lib/src/material/date_picker.dart | 8 +- .../lib/src/material/input_decorator.dart | 19 ++- .../flutter/lib/src/painting/basic_types.dart | 2 +- .../flutter/lib/src/painting/text_span.dart | 15 +- packages/flutter/lib/src/rendering/debug.dart | 5 +- packages/flutter/lib/src/rendering/layer.dart | 1 + .../flutter/lib/src/rendering/proxy_box.dart | 16 ++- .../rendering/sliver_persistent_header.dart | 43 +++++- .../flutter/lib/src/scheduler/binding.dart | 2 +- .../lib/src/widgets/animated_list.dart | 8 ++ packages/flutter/lib/src/widgets/debug.dart | 8 ++ .../flutter/lib/src/widgets/framework.dart | 108 +++++++++++++++ .../lib/src/widgets/gesture_detector.dart | 10 ++ .../lib/src/widgets/nested_scroll_view.dart | 14 +- .../src/widgets/notification_listener.dart | 11 ++ .../flutter/lib/src/widgets/page_view.dart | 12 +- .../widgets/primary_scroll_controller.dart | 5 + .../lib/src/widgets/scroll_controller.dart | 23 +++- .../lib/src/widgets/scroll_notification.dart | 12 ++ .../lib/src/widgets/scroll_position.dart | 5 +- .../flutter/lib/src/widgets/scroll_view.dart | 28 ++++ .../flutter/lib/src/widgets/scrollable.dart | 10 ++ .../src/widgets/single_child_scroll_view.dart | 8 ++ .../src/widgets/sliver_persistent_header.dart | 89 +++++++++++- packages/flutter_driver/lib/src/driver.dart | 14 +- packages/flutter_driver/lib/src/error.dart | 2 +- .../flutter_driver/lib/src/extension.dart | 10 ++ packages/flutter_driver/lib/src/find.dart | 128 +++++++++--------- .../flutter_driver/lib/src/frame_sync.dart | 15 +- packages/flutter_driver/lib/src/gesture.dart | 46 ++++--- packages/flutter_driver/lib/src/health.dart | 22 +-- .../flutter_driver/lib/src/matcher_util.dart | 5 +- packages/flutter_driver/lib/src/message.dart | 5 +- .../flutter_driver/lib/src/render_tree.dart | 21 +-- .../flutter_driver/lib/src/request_data.dart | 15 +- packages/flutter_driver/lib/src/retry.dart | 10 +- .../flutter_driver/lib/src/semantics.dart | 18 ++- .../lib/src/timeline_summary.dart | 22 +-- packages/flutter_test/lib/src/binding.dart | 11 ++ .../flutter_test/lib/src/test_text_input.dart | 7 + packages/flutter_tools/test/version_test.dart | 2 +- 46 files changed, 684 insertions(+), 187 deletions(-) create mode 100644 packages/flutter/lib/src/foundation/collections.dart 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/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/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/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/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/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_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/nested_scroll_view.dart b/packages/flutter/lib/src/widgets/nested_scroll_view.dart index 2661db7cce712..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 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/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_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..e96617416b8f5 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; 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/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'; From dd40d0e29cc62ddc9e34cfe45e3ed767481bf087 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Fri, 21 Jul 2017 16:39:11 -0700 Subject: [PATCH 09/14] Modal barrier shouldn't paint when the route is offstage. (#11347) Fixes https://github.com/flutter/flutter/issues/11323 --- .../flutter/lib/src/widgets/navigator.dart | 8 ++--- packages/flutter/lib/src/widgets/routes.dart | 8 ++++- .../page_forward_transitions_test.dart | 31 +++++++++++++++++-- .../test/widgets/page_transitions_test.dart | 2 +- 4 files changed, 41 insertions(+), 8 deletions(-) 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/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/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 = { From a92a62706c4b8588e113c978b29eb7bd13a6e332 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Fri, 21 Jul 2017 16:57:33 -0700 Subject: [PATCH 10/14] ignore PausePostRequest when reloading (#11340) * ignore postpauseevents when reloading * Update run_hot.dart comment changes to kick the appveyor bot --- packages/flutter_tools/lib/src/run_hot.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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); From 94ed7dce41d61a898f9b0eb7e38104045777ffb2 Mon Sep 17 00:00:00 2001 From: Mehmet Fidanboylu Date: Sat, 22 Jul 2017 06:12:42 -0700 Subject: [PATCH 11/14] Support automaticallyImplyLeading param in AppBar (#11264) * Support automaticallyImplyLeading param in AppBar * Review fixes * fix review comments --- .../flutter/lib/src/material/app_bar.dart | 61 ++++++++++++++----- .../flutter/test/material/app_bar_test.dart | 12 ++++ 2 files changed, 59 insertions(+), 14 deletions(-) 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/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( From f0dec6e305d2229fdb34a76d57b099a84cab69c0 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Mon, 24 Jul 2017 10:10:19 -0700 Subject: [PATCH 12/14] Add a debug feature to the gestures library to dump hit test results (#11346) --- packages/flutter/lib/gestures.dart | 1 + .../flutter/lib/src/gestures/binding.dart | 6 ++++ packages/flutter/lib/src/gestures/debug.dart | 32 +++++++++++++++++++ packages/flutter_test/lib/src/binding.dart | 3 ++ 4 files changed, 42 insertions(+) create mode 100644 packages/flutter/lib/src/gestures/debug.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/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_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index e96617416b8f5..8ffee1cfc894f 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -474,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, From e6f71555f6b400b25de2a0ad74e7661b64c57b58 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Mon, 24 Jul 2017 12:54:07 -0700 Subject: [PATCH 13/14] Revert "Work around to fix appveyor build (#11295)" (#11297) This reverts commit bc4a3f17034ab51693bc932a9b4eab0f3c23bae5. --- bin/internal/update_dart_sdk.ps1 | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) 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) { From 5278588d8015b44b312f33addaaa95721e85f24c Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Mon, 24 Jul 2017 14:00:48 -0700 Subject: [PATCH 14/14] roll engine (#11360) --- bin/internal/engine.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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