From b3224c7539467b9234342e50c83755137d1b4738 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 13 Jun 2017 10:57:10 -0700 Subject: [PATCH 01/39] Prefix and Suffix support for TextFields --- .../demo/material/text_form_field_demo.dart | 11 +++ examples/hello_world/hello_world.iml | 2 +- packages/flutter/flutter.iml | 2 +- .../lib/src/material/input_decorator.dart | 90 +++++++++++++++++-- packages/flutter_tools/flutter_tools.iml | 2 +- 5 files changed, 95 insertions(+), 12 deletions(-) diff --git a/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart b/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart index d1f3f3100b822..7abfade4cafdb 100644 --- a/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/text_form_field_demo.dart @@ -129,6 +129,7 @@ class TextFormFieldDemoState extends State { icon: const Icon(Icons.phone), hintText: 'Where can we reach you?', labelText: 'Phone Number *', + prefixText: '+1' ), keyboardType: TextInputType.phone, onSaved: (String value) { person.phoneNumber = value; }, @@ -147,6 +148,16 @@ class TextFormFieldDemoState extends State { ), maxLines: 3, ), + new TextFormField( + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: 'Salary', + prefixText: '\$', + suffixText: 'USD', + suffixStyle: const TextStyle(color: Colors.green) + ), + maxLines: 1, + ), new Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/examples/hello_world/hello_world.iml b/examples/hello_world/hello_world.iml index 9d5dae19540c2..30ca7dd23f133 100644 --- a/examples/hello_world/hello_world.iml +++ b/examples/hello_world/hello_world.iml @@ -12,4 +12,4 @@ - \ No newline at end of file + diff --git a/packages/flutter/flutter.iml b/packages/flutter/flutter.iml index c40a803190688..32cd5b3f65c03 100644 --- a/packages/flutter/flutter.iml +++ b/packages/flutter/flutter.iml @@ -28,4 +28,4 @@ - \ No newline at end of file + diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index 7e5ed82e065a9..7d28ebb756ce0 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -37,6 +37,10 @@ class InputDecoration { this.errorStyle, this.isDense: false, this.hideDivider: false, + this.prefixText, + this.prefixStyle, + this.suffixText, + this.suffixStyle }) : isCollapsed = false; /// Creates a decoration that is the same size as the input field. @@ -55,7 +59,11 @@ class InputDecoration { errorStyle = null, isDense = false, isCollapsed = true, - hideDivider = true; + hideDivider = true, + prefixText = null, + prefixStyle = null, + suffixText = null, + suffixStyle = null; /// An icon to show before the input field. /// @@ -108,7 +116,7 @@ class InputDecoration { /// If non-null the divider, that appears below the input field is red. final String errorText; - /// The style to use for the [errorText. + /// The style to use for the [errorText]. /// /// If null, defaults of a value derived from the base [TextStyle] for the /// input field and the current [Theme]. @@ -133,6 +141,28 @@ class InputDecoration { /// Defaults to false. final bool hideDivider; + /// Optional text prefix to place on the line before the input. + /// + /// Uses the [prefixStyle]. Uses [hintStyle] if [prefixStyle] isn't + /// specified. Prefix is not returned as part of the input. + final String prefixText; + + /// The style to use for the [prefixText]. + /// + /// If null, defaults to the [hintStyle]. + final TextStyle prefixStyle; + + /// Optional text suffix to place on the line after the input. + /// + /// Uses the [suffixStyle]. Uses [hintStyle] if [suffixStyle] isn't + /// specified. Suffix is not returned as part of the input. + final String suffixText; + + /// The style to use for the [suffixText]. + /// + /// If null, defaults to the [hintStyle]. + final TextStyle suffixStyle; + /// Creates a copy of this input decoration but with the given fields replaced /// with the new values. /// @@ -147,6 +177,10 @@ class InputDecoration { TextStyle errorStyle, bool isDense, bool hideDivider, + String prefixText, + TextStyle prefixStyle, + String suffixText, + TextStyle suffixStyle }) { return new InputDecoration( icon: icon ?? this.icon, @@ -158,6 +192,10 @@ class InputDecoration { errorStyle: errorStyle ?? this.errorStyle, isDense: isDense ?? this.isDense, hideDivider: hideDivider ?? this.hideDivider, + prefixText: prefixText ?? this.prefixText, + prefixStyle: prefixStyle ?? this.prefixStyle, + suffixText: suffixText ?? this.suffixText, + suffixStyle: suffixStyle ?? this.suffixStyle, ); } @@ -177,7 +215,11 @@ class InputDecoration { && typedOther.errorStyle == errorStyle && typedOther.isDense == isDense && typedOther.isCollapsed == isCollapsed - && typedOther.hideDivider == hideDivider; + && typedOther.hideDivider == hideDivider + && typedOther.prefixText == prefixText + && typedOther.prefixStyle == prefixStyle + && typedOther.suffixText == suffixText + && typedOther.suffixStyle == suffixStyle; } @override @@ -193,6 +235,10 @@ class InputDecoration { isDense, isCollapsed, hideDivider, + prefixText, + prefixStyle, + suffixText, + suffixStyle, ); } @@ -213,6 +259,14 @@ class InputDecoration { description.add('isCollapsed: $isCollapsed'); if (hideDivider) description.add('hideDivider: $hideDivider'); + if (prefixText != null) + description.add('prefixText: $prefixText'); + if (prefixStyle != null) + description.add('prefixStyle: $prefixStyle'); + if (suffixText != null) + description.add('suffixText: $suffixText'); + if (suffixStyle != null) + description.add('suffixStyle: $suffixStyle'); return 'InputDecoration(${description.join(', ')})'; } } @@ -293,7 +347,7 @@ class InputDecorator extends StatelessWidget { return themeData.hintColor; } - Widget _buildContent(Color borderColor, double topPadding, bool isDense) { + Widget _buildContent(Color borderColor, double topPadding, bool isDense, Widget inputChild) { final double bottomPadding = isDense ? 8.0 : 1.0; const double bottomBorder = 2.0; final double bottomHeight = isDense ? 14.0 : 18.0; @@ -305,7 +359,7 @@ class InputDecorator extends StatelessWidget { return new Container( margin: margin + const EdgeInsets.only(bottom: bottomBorder), padding: padding, - child: child, + child: inputChild, ); } @@ -322,7 +376,7 @@ class InputDecorator extends StatelessWidget { ), ), ), - child: child, + child: inputChild, ); } @@ -348,7 +402,7 @@ class InputDecorator extends StatelessWidget { final List stackChildren = []; - // If we're not focused, there's not value, and labelText was provided, + // If we're not focused, there's no value, and labelText was provided, // then the label appears where the hint would. And we will not show // the hintText. final bool hasInlineLabel = !isFocused && labelText != null && isEmpty; @@ -402,11 +456,29 @@ class InputDecorator extends StatelessWidget { ); } + Widget inputChild; + if (!hasInlineLabel && (!isEmpty || (hintText ?? '').isEmpty) && + (decoration?.prefixText != null || decoration?.suffixText != null)) { + List rowContents = []; + if (decoration.prefixText != null) { + rowContents.add(new Text(decoration.prefixText, + style: decoration.prefixStyle ?? hintStyle)); + } + rowContents.add(new Expanded(child: child)); + if (decoration.suffixText != null) { + rowContents.add(new Text(decoration.suffixText, + style: decoration.suffixStyle ?? hintStyle)); + } + inputChild = new Row(children: rowContents); + } else { + inputChild = child; + } + if (isCollapsed) { - stackChildren.add(child); + stackChildren.add(inputChild); } else { final Color borderColor = errorText == null ? activeColor : themeData.errorColor; - stackChildren.add(_buildContent(borderColor, topPadding, isDense)); + stackChildren.add(_buildContent(borderColor, topPadding, isDense, inputChild)); } if (!isDense && errorText != null) { diff --git a/packages/flutter_tools/flutter_tools.iml b/packages/flutter_tools/flutter_tools.iml index 367b72461cd70..14bf54812bf86 100644 --- a/packages/flutter_tools/flutter_tools.iml +++ b/packages/flutter_tools/flutter_tools.iml @@ -48,4 +48,4 @@ - \ No newline at end of file + From eaffcb944502c079e07f0082758f61b9ac63bab5 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 13 Jun 2017 15:02:06 -0700 Subject: [PATCH 02/39] Adding Tests --- .../lib/src/material/input_decorator.dart | 2 +- .../test/material/text_field_test.dart | 264 +++++++++++++++++- packages/flutter_test/flutter_test.iml | 2 +- 3 files changed, 255 insertions(+), 13 deletions(-) diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index 7d28ebb756ce0..30893d9d848d7 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -457,7 +457,7 @@ class InputDecorator extends StatelessWidget { } Widget inputChild; - if (!hasInlineLabel && (!isEmpty || (hintText ?? '').isEmpty) && + if (!hasInlineLabel && (!isEmpty || hintText == null) && (decoration?.prefixText != null || decoration?.suffixText != null)) { List rowContents = []; if (decoration.prefixText != null) { diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index d727d3dc17e05..4babb5e62a708 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -785,6 +785,248 @@ void main() { expect(hintText.style, hintStyle); }); + testWidgets('TextField with specified prefixStyle', (WidgetTester tester) async { + final TextStyle prefixStyle = new TextStyle( + color: Colors.pink[500], + fontSize: 10.0, + ); + + Widget builder() { + return new Center( + child: new Material( + child: new TextField( + decoration: new InputDecoration( + prefixText: 'Prefix:', + prefixStyle: prefixStyle, + ), + ), + ), + ); + } + + await tester.pumpWidget(builder()); + + final Text prefixText = tester.widget(find.text('Prefix:')); + expect(prefixText.style, prefixStyle); + }); + + testWidgets('TextField with specified suffixStyle', (WidgetTester tester) async { + final TextStyle suffixStyle = new TextStyle( + color: Colors.pink[500], + fontSize: 10.0, + ); + + Widget builder() { + return new Center( + child: new Material( + child: new TextField( + decoration: new InputDecoration( + suffixText: '.com', + suffixStyle: suffixStyle, + ), + ), + ), + ); + } + + await tester.pumpWidget(builder()); + + final Text suffixText = tester.widget(find.text('.com')); + expect(suffixText.style, suffixStyle); + }); + + testWidgets('TextField prefix and suffix appear correctly with no hint or label', + (WidgetTester tester) async { + final Key secondKey = new UniqueKey(); + + Widget innerBuilder() { + return new Center( + child: new Material( + child: new Column( + children: [ + const TextField( + decoration: const InputDecoration( + labelText: 'First', + ), + ), + new TextField( + key: secondKey, + decoration: const InputDecoration( + prefixText: 'Prefix', + suffixText: 'Suffix', + ), + ), + ], + ), + ), + ); + } + Widget builder() => overlay(innerBuilder()); + + await tester.pumpWidget(builder()); + + expect(find.text('Prefix'), findsOneWidget); + expect(find.text('Suffix'), findsOneWidget); + + // Focus the Input. The prefix should still display. + await tester.tap(find.byKey(secondKey)); + await tester.pump(); + + expect(find.text('Prefix'), findsOneWidget); + expect(find.text('Suffix'), findsOneWidget); + + // Enter some text, and the prefix should still display. + await tester.enterText(find.byKey(secondKey), "Hi"); + await tester.pump(); + await tester.pump(new Duration(seconds: 1)); + + expect(find.text('Prefix'), findsOneWidget); + expect(find.text('Suffix'), findsOneWidget); + }); + + testWidgets('TextField prefix and suffix appear correctly with hint text', + (WidgetTester tester) async { + final TextStyle hintStyle = new TextStyle( + color: Colors.pink[500], + fontSize: 10.0, + ); + final Key secondKey = new UniqueKey(); + + Widget innerBuilder() { + return new Center( + child: new Material( + child: new Column( + children: [ + const TextField( + decoration: const InputDecoration( + labelText: 'First', + ), + ), + new TextField( + key: secondKey, + decoration: new InputDecoration( + hintText: 'Hint', + hintStyle: hintStyle, + prefixText: 'Prefix', + suffixText: 'Suffix', + ), + ), + ], + ), + ), + ); + } + Widget builder() => overlay(innerBuilder()); + + await tester.pumpWidget(builder()); + + // Neither the prefix or the suffix should initially be visible, only the hint. + expect(find.text('Prefix'), findsNothing); + expect(find.text('Suffix'), findsNothing); + expect(find.text('Hint'), findsOneWidget); + + await tester.tap(find.byKey(secondKey)); + await tester.pump(); + + // Focus the Input. The hint should display, but not the prefix and suffix. + expect(find.text('Prefix'), findsNothing); + expect(find.text('Suffix'), findsNothing); + expect(find.text('Hint'), findsOneWidget); + + // Enter some text, and the hint should disappear and the prefix and suffix + // should appear. + await tester.enterText(find.byKey(secondKey), "Hi"); + await tester.pump(); + await tester.pump(new Duration(seconds: 1)); + + expect(find.text('Prefix'), findsOneWidget); + expect(find.text('Suffix'), findsOneWidget); + + // It's onstage, but animated to zero opacity. + expect(find.text('Hint'), findsOneWidget); + final Element target = tester.element(find.text('Hint')); + Opacity opacity = target.ancestorWidgetOfExactType(Opacity); + expect(opacity, isNotNull); + expect(opacity.opacity, equals(0.0)); + + // Check and make sure that the right styles were applied. + final Text prefixText = tester.widget(find.text('Prefix')); + expect(prefixText.style, hintStyle); + final Text suffixText = tester.widget(find.text('Suffix')); + expect(prefixText.style, hintStyle); + }); + + testWidgets('TextField prefix and suffix appear correctly with label text', + (WidgetTester tester) async { + final TextStyle prefixStyle = new TextStyle( + color: Colors.pink[500], + fontSize: 10.0, + ); + final TextStyle suffixStyle = new TextStyle( + color: Colors.green[500], + fontSize: 12.0, + ); + final Key secondKey = new UniqueKey(); + + Widget innerBuilder() { + return new Center( + child: new Material( + child: new Column( + children: [ + const TextField( + decoration: const InputDecoration( + labelText: 'First', + ), + ), + new TextField( + key: secondKey, + decoration: new InputDecoration( + labelText: 'Label', + prefixText: 'Prefix', + prefixStyle: prefixStyle, + suffixText: 'Suffix', + suffixStyle: suffixStyle, + ), + ), + ], + ), + ), + ); + } + Widget builder() => overlay(innerBuilder()); + + await tester.pumpWidget(builder()); + + // Not focused. The prefix should not display, but the label should. + expect(find.text('Prefix'), findsNothing); + expect(find.text('Suffix'), findsNothing); + expect(find.text('Label'), findsOneWidget); + + await tester.tap(find.byKey(secondKey)); + await tester.pump(); + + // Focus the input. The label should display, and also the prefix. + expect(find.text('Prefix'), findsOneWidget); + expect(find.text('Suffix'), findsOneWidget); + expect(find.text('Label'), findsOneWidget); + + // Enter some text, and the label should stay and the prefix should + // remain. + await tester.enterText(find.byKey(secondKey), "Hi"); + await tester.pump(); + await tester.pump(new Duration(seconds: 1)); + + expect(find.text('Prefix'), findsOneWidget); + expect(find.text('Suffix'), findsOneWidget); + expect(find.text('Label'), findsOneWidget); + + // Check and make sure that the right styles were applied. + final Text prefixText = tester.widget(find.text('Prefix')); + expect(prefixText.style, prefixStyle); + final Text suffixText = tester.widget(find.text('Suffix')); + expect(suffixText.style, suffixStyle); + }); + testWidgets('TextField label text animates', (WidgetTester tester) async { final Key secondKey = new UniqueKey(); @@ -830,7 +1072,7 @@ void main() { newPos = tester.getTopLeft(find.text('Second')); expect(newPos.dy, lessThan(pos.dy)); }); - + testWidgets('No space between Input icon and text', (WidgetTester tester) async { await tester.pumpWidget( const Center( @@ -990,7 +1232,7 @@ void main() { }); testWidgets( - 'Cannot enter new lines onto single line TextField', + 'Cannot enter new lines onto single line TextField', (WidgetTester tester) async { final TextEditingController textController = new TextEditingController(); @@ -1005,13 +1247,13 @@ void main() { ); testWidgets( - 'Injected formatters are chained', + 'Injected formatters are chained', (WidgetTester tester) async { final TextEditingController textController = new TextEditingController(); await tester.pumpWidget(new Material( child: new TextField( - controller: textController, + controller: textController, decoration: null, inputFormatters: [ new BlacklistingTextInputFormatter( @@ -1029,13 +1271,13 @@ void main() { ); testWidgets( - 'Chained formatters are in sequence', + 'Chained formatters are in sequence', (WidgetTester tester) async { final TextEditingController textController = new TextEditingController(); await tester.pumpWidget(new Material( child: new TextField( - controller: textController, + controller: textController, decoration: null, maxLines: 2, inputFormatters: [ @@ -1051,15 +1293,15 @@ void main() { await tester.enterText(find.byType(TextField), 'a1b2c3'); // The first formatter turns it into // 12\n112\n212\n3 - // The second formatter turns it into + // The second formatter turns it into // \n1\n2\n3 - // Multiline is allowed since maxLine != 1. + // Multiline is allowed since maxLine != 1. expect(textController.text, '\n1\n2\n3'); } ); testWidgets( - 'Pasted values are formatted', + 'Pasted values are formatted', (WidgetTester tester) async { final TextEditingController textController = new TextEditingController(); @@ -1067,7 +1309,7 @@ void main() { return overlay(new Center( child: new Material( child: new TextField( - controller: textController, + controller: textController, decoration: null, inputFormatters: [ WhitelistingTextInputFormatter.digitsOnly, @@ -1088,7 +1330,7 @@ void main() { await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2'))); await tester.pumpWidget(builder()); final RenderEditable renderEditable = findRenderEditable(tester); - final List endpoints = + final List endpoints = renderEditable.getEndpointsForSelection(textController.selection); await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); await tester.pumpWidget(builder()); diff --git a/packages/flutter_test/flutter_test.iml b/packages/flutter_test/flutter_test.iml index 9b361c06946c4..55026d78a689a 100644 --- a/packages/flutter_test/flutter_test.iml +++ b/packages/flutter_test/flutter_test.iml @@ -13,4 +13,4 @@ - \ No newline at end of file + From 077234add188cf7d20ef34bb9288be7f4e693268 Mon Sep 17 00:00:00 2001 From: gspencergoog Date: Tue, 13 Jun 2017 15:13:58 -0700 Subject: [PATCH 03/39] Removing spurious newline. From 633c6c7287ae0e0650f418b8bd923fa9651e1326 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 13 Jun 2017 15:41:21 -0700 Subject: [PATCH 04/39] Fixing a small problem with the test --- examples/hello_world/hello_world.iml | 3 ++- packages/flutter/flutter.iml | 4 ++-- .../lib/src/material/input_decorator.dart | 2 +- .../flutter/test/material/text_field_test.dart | 10 +++++----- packages/flutter_tools/flutter_tools.iml | 18 +++++++++--------- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/examples/hello_world/hello_world.iml b/examples/hello_world/hello_world.iml index 30ca7dd23f133..c82504f3177c9 100644 --- a/examples/hello_world/hello_world.iml +++ b/examples/hello_world/hello_world.iml @@ -7,9 +7,10 @@ + - + \ No newline at end of file diff --git a/packages/flutter/flutter.iml b/packages/flutter/flutter.iml index 32cd5b3f65c03..b34e1f260d4fb 100644 --- a/packages/flutter/flutter.iml +++ b/packages/flutter/flutter.iml @@ -1,5 +1,5 @@ - + @@ -28,4 +28,4 @@ - + \ No newline at end of file diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index 30893d9d848d7..662782e81e882 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -459,7 +459,7 @@ class InputDecorator extends StatelessWidget { Widget inputChild; if (!hasInlineLabel && (!isEmpty || hintText == null) && (decoration?.prefixText != null || decoration?.suffixText != null)) { - List rowContents = []; + final List rowContents = []; if (decoration.prefixText != null) { rowContents.add(new Text(decoration.prefixText, style: decoration.prefixStyle ?? hintStyle)); diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 4babb5e62a708..49d4f96f3a48f 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -878,7 +878,7 @@ void main() { // Enter some text, and the prefix should still display. await tester.enterText(find.byKey(secondKey), "Hi"); await tester.pump(); - await tester.pump(new Duration(seconds: 1)); + await tester.pump(const Duration(seconds: 1)); expect(find.text('Prefix'), findsOneWidget); expect(find.text('Suffix'), findsOneWidget); @@ -937,7 +937,7 @@ void main() { // should appear. await tester.enterText(find.byKey(secondKey), "Hi"); await tester.pump(); - await tester.pump(new Duration(seconds: 1)); + await tester.pump(const Duration(seconds: 1)); expect(find.text('Prefix'), findsOneWidget); expect(find.text('Suffix'), findsOneWidget); @@ -945,7 +945,7 @@ void main() { // It's onstage, but animated to zero opacity. expect(find.text('Hint'), findsOneWidget); final Element target = tester.element(find.text('Hint')); - Opacity opacity = target.ancestorWidgetOfExactType(Opacity); + final Opacity opacity = target.ancestorWidgetOfExactType(Opacity); expect(opacity, isNotNull); expect(opacity.opacity, equals(0.0)); @@ -953,7 +953,7 @@ void main() { final Text prefixText = tester.widget(find.text('Prefix')); expect(prefixText.style, hintStyle); final Text suffixText = tester.widget(find.text('Suffix')); - expect(prefixText.style, hintStyle); + expect(suffixText.style, hintStyle); }); testWidgets('TextField prefix and suffix appear correctly with label text', @@ -1014,7 +1014,7 @@ void main() { // remain. await tester.enterText(find.byKey(secondKey), "Hi"); await tester.pump(); - await tester.pump(new Duration(seconds: 1)); + await tester.pump(const Duration(seconds: 1)); expect(find.text('Prefix'), findsOneWidget); expect(find.text('Suffix'), findsOneWidget); diff --git a/packages/flutter_tools/flutter_tools.iml b/packages/flutter_tools/flutter_tools.iml index 14bf54812bf86..22fe5c309c091 100644 --- a/packages/flutter_tools/flutter_tools.iml +++ b/packages/flutter_tools/flutter_tools.iml @@ -7,6 +7,12 @@ + + + + + + @@ -30,16 +36,10 @@ + - - - - - - - - + @@ -48,4 +48,4 @@ - + \ No newline at end of file From 04003b0f749680cf8088eabb60d9981b01f22d3f Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 13 Jun 2017 15:41:21 -0700 Subject: [PATCH 05/39] Code review changes --- examples/hello_world/hello_world.iml | 5 +++-- packages/flutter/flutter.iml | 2 +- .../lib/src/material/input_decorator.dart | 22 +++++++++++-------- .../test/material/text_field_test.dart | 12 +++++----- packages/flutter_test/flutter_test.iml | 2 +- packages/flutter_tools/flutter_tools.iml | 2 +- 6 files changed, 25 insertions(+), 20 deletions(-) diff --git a/examples/hello_world/hello_world.iml b/examples/hello_world/hello_world.iml index 30ca7dd23f133..a1c412f3a1431 100644 --- a/examples/hello_world/hello_world.iml +++ b/examples/hello_world/hello_world.iml @@ -7,9 +7,10 @@ + - + - + \ No newline at end of file diff --git a/packages/flutter/flutter.iml b/packages/flutter/flutter.iml index 32cd5b3f65c03..c40a803190688 100644 --- a/packages/flutter/flutter.iml +++ b/packages/flutter/flutter.iml @@ -28,4 +28,4 @@ - + \ No newline at end of file diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index 30893d9d848d7..668d897a54810 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -40,7 +40,7 @@ class InputDecoration { this.prefixText, this.prefixStyle, this.suffixText, - this.suffixStyle + this.suffixStyle, }) : isCollapsed = false; /// Creates a decoration that is the same size as the input field. @@ -143,7 +143,7 @@ class InputDecoration { /// Optional text prefix to place on the line before the input. /// - /// Uses the [prefixStyle]. Uses [hintStyle] if [prefixStyle] isn't + /// Uses the [prefixStyle]. Uses [hintStyle] if [prefixStyle] isn't /// specified. Prefix is not returned as part of the input. final String prefixText; @@ -154,7 +154,7 @@ class InputDecoration { /// Optional text suffix to place on the line after the input. /// - /// Uses the [suffixStyle]. Uses [hintStyle] if [suffixStyle] isn't + /// Uses the [suffixStyle]. Uses [hintStyle] if [suffixStyle] isn't /// specified. Suffix is not returned as part of the input. final String suffixText; @@ -180,7 +180,7 @@ class InputDecoration { String prefixText, TextStyle prefixStyle, String suffixText, - TextStyle suffixStyle + TextStyle suffixStyle, }) { return new InputDecoration( icon: icon ?? this.icon, @@ -459,15 +459,19 @@ class InputDecorator extends StatelessWidget { Widget inputChild; if (!hasInlineLabel && (!isEmpty || hintText == null) && (decoration?.prefixText != null || decoration?.suffixText != null)) { - List rowContents = []; + final List rowContents = []; if (decoration.prefixText != null) { - rowContents.add(new Text(decoration.prefixText, - style: decoration.prefixStyle ?? hintStyle)); + rowContents.add( + new Text(decoration.prefixText, + style: decoration.prefixStyle ?? hintStyle) + ); } rowContents.add(new Expanded(child: child)); if (decoration.suffixText != null) { - rowContents.add(new Text(decoration.suffixText, - style: decoration.suffixStyle ?? hintStyle)); + rowContents.add( + new Text(decoration.suffixText, + style: decoration.suffixStyle ?? hintStyle) + ); } inputChild = new Row(children: rowContents); } else { diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 4babb5e62a708..0b17aa298ad1e 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -878,7 +878,7 @@ void main() { // Enter some text, and the prefix should still display. await tester.enterText(find.byKey(secondKey), "Hi"); await tester.pump(); - await tester.pump(new Duration(seconds: 1)); + await tester.pump(const Duration(seconds: 1)); expect(find.text('Prefix'), findsOneWidget); expect(find.text('Suffix'), findsOneWidget); @@ -937,7 +937,7 @@ void main() { // should appear. await tester.enterText(find.byKey(secondKey), "Hi"); await tester.pump(); - await tester.pump(new Duration(seconds: 1)); + await tester.pump(const Duration(seconds: 1)); expect(find.text('Prefix'), findsOneWidget); expect(find.text('Suffix'), findsOneWidget); @@ -945,7 +945,7 @@ void main() { // It's onstage, but animated to zero opacity. expect(find.text('Hint'), findsOneWidget); final Element target = tester.element(find.text('Hint')); - Opacity opacity = target.ancestorWidgetOfExactType(Opacity); + final Opacity opacity = target.ancestorWidgetOfExactType(Opacity); expect(opacity, isNotNull); expect(opacity.opacity, equals(0.0)); @@ -953,7 +953,7 @@ void main() { final Text prefixText = tester.widget(find.text('Prefix')); expect(prefixText.style, hintStyle); final Text suffixText = tester.widget(find.text('Suffix')); - expect(prefixText.style, hintStyle); + expect(suffixText.style, hintStyle); }); testWidgets('TextField prefix and suffix appear correctly with label text', @@ -1014,7 +1014,7 @@ void main() { // remain. await tester.enterText(find.byKey(secondKey), "Hi"); await tester.pump(); - await tester.pump(new Duration(seconds: 1)); + await tester.pump(const Duration(seconds: 1)); expect(find.text('Prefix'), findsOneWidget); expect(find.text('Suffix'), findsOneWidget); @@ -1072,7 +1072,7 @@ void main() { newPos = tester.getTopLeft(find.text('Second')); expect(newPos.dy, lessThan(pos.dy)); }); - + testWidgets('No space between Input icon and text', (WidgetTester tester) async { await tester.pumpWidget( const Center( diff --git a/packages/flutter_test/flutter_test.iml b/packages/flutter_test/flutter_test.iml index 55026d78a689a..9b361c06946c4 100644 --- a/packages/flutter_test/flutter_test.iml +++ b/packages/flutter_test/flutter_test.iml @@ -13,4 +13,4 @@ - + \ No newline at end of file diff --git a/packages/flutter_tools/flutter_tools.iml b/packages/flutter_tools/flutter_tools.iml index 14bf54812bf86..367b72461cd70 100644 --- a/packages/flutter_tools/flutter_tools.iml +++ b/packages/flutter_tools/flutter_tools.iml @@ -48,4 +48,4 @@ - + \ No newline at end of file From c1370b1412926240b37c56db292d0ce764d822bd Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 13 Jun 2017 17:10:18 -0700 Subject: [PATCH 06/39] Code Review Changes --- examples/hello_world/hello_world.iml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/hello_world/hello_world.iml b/examples/hello_world/hello_world.iml index a1c412f3a1431..9d5dae19540c2 100644 --- a/examples/hello_world/hello_world.iml +++ b/examples/hello_world/hello_world.iml @@ -7,10 +7,9 @@ - - + \ No newline at end of file From 5b86b03eb50a9992ca015f4610180ef4b62c3c4b Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 13 Jun 2017 18:20:47 -0700 Subject: [PATCH 07/39] Review Changes --- examples/hello_world/hello_world.iml | 1 - packages/flutter/flutter.iml | 2 +- .../lib/src/material/input_decorator.dart | 20 +++++++++++-------- .../test/material/text_field_test.dart | 2 +- packages/flutter_test/flutter_test.iml | 2 +- packages/flutter_tools/flutter_tools.iml | 16 +++++++-------- 6 files changed, 23 insertions(+), 20 deletions(-) diff --git a/examples/hello_world/hello_world.iml b/examples/hello_world/hello_world.iml index c82504f3177c9..9d5dae19540c2 100644 --- a/examples/hello_world/hello_world.iml +++ b/examples/hello_world/hello_world.iml @@ -7,7 +7,6 @@ - diff --git a/packages/flutter/flutter.iml b/packages/flutter/flutter.iml index b34e1f260d4fb..c40a803190688 100644 --- a/packages/flutter/flutter.iml +++ b/packages/flutter/flutter.iml @@ -1,5 +1,5 @@ - + diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index 662782e81e882..668d897a54810 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -40,7 +40,7 @@ class InputDecoration { this.prefixText, this.prefixStyle, this.suffixText, - this.suffixStyle + this.suffixStyle, }) : isCollapsed = false; /// Creates a decoration that is the same size as the input field. @@ -143,7 +143,7 @@ class InputDecoration { /// Optional text prefix to place on the line before the input. /// - /// Uses the [prefixStyle]. Uses [hintStyle] if [prefixStyle] isn't + /// Uses the [prefixStyle]. Uses [hintStyle] if [prefixStyle] isn't /// specified. Prefix is not returned as part of the input. final String prefixText; @@ -154,7 +154,7 @@ class InputDecoration { /// Optional text suffix to place on the line after the input. /// - /// Uses the [suffixStyle]. Uses [hintStyle] if [suffixStyle] isn't + /// Uses the [suffixStyle]. Uses [hintStyle] if [suffixStyle] isn't /// specified. Suffix is not returned as part of the input. final String suffixText; @@ -180,7 +180,7 @@ class InputDecoration { String prefixText, TextStyle prefixStyle, String suffixText, - TextStyle suffixStyle + TextStyle suffixStyle, }) { return new InputDecoration( icon: icon ?? this.icon, @@ -461,13 +461,17 @@ class InputDecorator extends StatelessWidget { (decoration?.prefixText != null || decoration?.suffixText != null)) { final List rowContents = []; if (decoration.prefixText != null) { - rowContents.add(new Text(decoration.prefixText, - style: decoration.prefixStyle ?? hintStyle)); + rowContents.add( + new Text(decoration.prefixText, + style: decoration.prefixStyle ?? hintStyle) + ); } rowContents.add(new Expanded(child: child)); if (decoration.suffixText != null) { - rowContents.add(new Text(decoration.suffixText, - style: decoration.suffixStyle ?? hintStyle)); + rowContents.add( + new Text(decoration.suffixText, + style: decoration.suffixStyle ?? hintStyle) + ); } inputChild = new Row(children: rowContents); } else { diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 49d4f96f3a48f..0b17aa298ad1e 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -1072,7 +1072,7 @@ void main() { newPos = tester.getTopLeft(find.text('Second')); expect(newPos.dy, lessThan(pos.dy)); }); - + testWidgets('No space between Input icon and text', (WidgetTester tester) async { await tester.pumpWidget( const Center( diff --git a/packages/flutter_test/flutter_test.iml b/packages/flutter_test/flutter_test.iml index 55026d78a689a..9b361c06946c4 100644 --- a/packages/flutter_test/flutter_test.iml +++ b/packages/flutter_test/flutter_test.iml @@ -13,4 +13,4 @@ - + \ No newline at end of file diff --git a/packages/flutter_tools/flutter_tools.iml b/packages/flutter_tools/flutter_tools.iml index 22fe5c309c091..367b72461cd70 100644 --- a/packages/flutter_tools/flutter_tools.iml +++ b/packages/flutter_tools/flutter_tools.iml @@ -7,12 +7,6 @@ - - - - - - @@ -36,10 +30,16 @@ - + + + + + + - + + From 95450ba64b94709cd41c805da143bb89ae1c2258 Mon Sep 17 00:00:00 2001 From: gspencergoog Date: Wed, 14 Jun 2017 16:55:46 -0700 Subject: [PATCH 08/39] Export the new StrokeJoin enum --- packages/flutter/lib/src/painting/basic_types.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter/lib/src/painting/basic_types.dart b/packages/flutter/lib/src/painting/basic_types.dart index 206ca005716b2..65247d731a21d 100644 --- a/packages/flutter/lib/src/painting/basic_types.dart +++ b/packages/flutter/lib/src/painting/basic_types.dart @@ -25,6 +25,7 @@ export 'dart:ui' show Shader, Size, StrokeCap, + StrokeJoin, TextAlign, TextBaseline, TextDecoration, From 2ca1574cc75bbf5847c9aaca5ff1d9b43cece978 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Thu, 15 Jun 2017 12:41:37 -0700 Subject: [PATCH 09/39] Added example for line styles, and enabled line join styles. --- examples/layers/lib/main.dart | 2 +- examples/layers/raw/line_styles.dart | 86 ++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 examples/layers/raw/line_styles.dart diff --git a/examples/layers/lib/main.dart b/examples/layers/lib/main.dart index 864e8f1d287ca..4d6a7d91ff612 100644 --- a/examples/layers/lib/main.dart +++ b/examples/layers/lib/main.dart @@ -4,4 +4,4 @@ import 'package:flutter/widgets.dart'; -void main() => runApp(const Center(child: const Text('flutter run -t xxx/yyy.dart'))); +void main() => runApp(const Center(child: const Text('flutter run -t raw/spinning_square.dart'))); diff --git a/examples/layers/raw/line_styles.dart b/examples/layers/raw/line_styles.dart new file mode 100644 index 0000000000000..1748aaa76e63d --- /dev/null +++ b/examples/layers/raw/line_styles.dart @@ -0,0 +1,86 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This example shows how to perform a simple animation using the raw interface +// to the engine. + +import 'dart:math' as math; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +void beginFrame(Duration timeStamp) { + // The timeStamp argument to beginFrame indicates the timing information we + // should use to clock our animations. It's important to use timeStamp rather + // than reading the system time because we want all the parts of the system to + // coordinate the timings of their animations. If each component read the + // system clock independently, the animations that we processed later would be + // slightly ahead of the animations we processed earlier. + + // PAINT + const double kRadius = 100.0; + const double kTwoPi = math.PI * 2.0; + const double kVerticalOffset = 100.0; + + final ui.Rect paintBounds = + ui.Offset.zero & (ui.window.physicalSize / ui.window.devicePixelRatio); + final ui.PictureRecorder recorder = new ui.PictureRecorder(); + final ui.Canvas canvas = new ui.Canvas(recorder, paintBounds); + canvas.translate(paintBounds.width / 2.0, paintBounds.height / 2.0); + + // Here we determine the rotation speed according to the timeStamp given to us + // by the engine. + final double t = (timeStamp.inMicroseconds / + Duration.MICROSECONDS_PER_MILLISECOND / + 3200.0) % 1.0; + + final List points = [ + const ui.Offset(kRadius, kVerticalOffset), + const ui.Offset(0.0, kVerticalOffset), + new ui.Offset(kRadius * math.cos(t * kTwoPi), + kRadius * math.sin(t * kTwoPi) + kVerticalOffset), + ]; + + // Try changing values for the stroke style and see what the results are + // for different line drawing primitives. + final ui.Paint paint = new ui.Paint() + ..color = const ui.Color.fromARGB(255, 0, 255, 0) + ..style = ui.PaintingStyle.stroke + ..strokeCap = ui.StrokeCap.butt // Other choices are round and square. + ..strokeJoin = ui.StrokeJoin.miter // Other choices are round and bevel. + ..strokeMiterLimit = 5.0 // Try smaller and larger values greater than zero. + ..strokeWidth = 20.0; + canvas.drawPoints(ui.PointMode.polygon, points, paint); + + final ui.Path path = new ui.Path() + ..moveTo(points[0].dx, points[0].dy - 2 * kVerticalOffset) + ..lineTo(points[1].dx, points[1].dy - 2 * kVerticalOffset) + ..lineTo(points[2].dx, points[2].dy - 2 * kVerticalOffset) + ..close(); + canvas.drawPath(path, paint); + final ui.Picture picture = recorder.endRecording(); + + // COMPOSITE + + final double devicePixelRatio = ui.window.devicePixelRatio; + final Float64List deviceTransform = new Float64List(16) + ..[0] = devicePixelRatio + ..[5] = devicePixelRatio + ..[10] = 1.0 + ..[15] = 1.0; + final ui.SceneBuilder sceneBuilder = new ui.SceneBuilder() + ..pushTransform(deviceTransform) + ..addPicture(ui.Offset.zero, picture) + ..pop(); + ui.window.render(sceneBuilder.build()); + + // After rendering the current frame of the animation, we ask the engine to + // schedule another frame. The engine will call beginFrame again when its time + // to produce the next frame. + ui.window.scheduleFrame(); +} + +void main() { + ui.window.onBeginFrame = beginFrame; + ui.window.scheduleFrame(); +} From 83c156ce05e53257ab5fdd2fd5865da5132bea4a Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Thu, 15 Jun 2017 12:51:18 -0700 Subject: [PATCH 10/39] Reverting inadvertent change to main.dart. --- examples/layers/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/layers/lib/main.dart b/examples/layers/lib/main.dart index 4d6a7d91ff612..864e8f1d287ca 100644 --- a/examples/layers/lib/main.dart +++ b/examples/layers/lib/main.dart @@ -4,4 +4,4 @@ import 'package:flutter/widgets.dart'; -void main() => runApp(const Center(child: const Text('flutter run -t raw/spinning_square.dart'))); +void main() => runApp(const Center(child: const Text('flutter run -t xxx/yyy.dart'))); From 5a54fa2cdaae3599eeb4ca75437385254e025a57 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Thu, 15 Jun 2017 16:24:17 -0700 Subject: [PATCH 11/39] Updated due to code review of engine code --- examples/layers/raw/line_styles.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/layers/raw/line_styles.dart b/examples/layers/raw/line_styles.dart index 1748aaa76e63d..53ceb1b8670ff 100644 --- a/examples/layers/raw/line_styles.dart +++ b/examples/layers/raw/line_styles.dart @@ -48,7 +48,7 @@ void beginFrame(Duration timeStamp) { ..style = ui.PaintingStyle.stroke ..strokeCap = ui.StrokeCap.butt // Other choices are round and square. ..strokeJoin = ui.StrokeJoin.miter // Other choices are round and bevel. - ..strokeMiterLimit = 5.0 // Try smaller and larger values greater than zero. + ..strokeMiterLimit = 4.0 // Try smaller and larger values zero or above. ..strokeWidth = 20.0; canvas.drawPoints(ui.PointMode.polygon, points, paint); From 1755464666a6840dd70abbdff7d13a3a26e1dc3e Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Fri, 16 Jun 2017 17:49:26 -0700 Subject: [PATCH 12/39] Removed example. --- examples/layers/raw/line_styles.dart | 86 ---------------------------- 1 file changed, 86 deletions(-) delete mode 100644 examples/layers/raw/line_styles.dart diff --git a/examples/layers/raw/line_styles.dart b/examples/layers/raw/line_styles.dart deleted file mode 100644 index 53ceb1b8670ff..0000000000000 --- a/examples/layers/raw/line_styles.dart +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2015 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// This example shows how to perform a simple animation using the raw interface -// to the engine. - -import 'dart:math' as math; -import 'dart:typed_data'; -import 'dart:ui' as ui; - -void beginFrame(Duration timeStamp) { - // The timeStamp argument to beginFrame indicates the timing information we - // should use to clock our animations. It's important to use timeStamp rather - // than reading the system time because we want all the parts of the system to - // coordinate the timings of their animations. If each component read the - // system clock independently, the animations that we processed later would be - // slightly ahead of the animations we processed earlier. - - // PAINT - const double kRadius = 100.0; - const double kTwoPi = math.PI * 2.0; - const double kVerticalOffset = 100.0; - - final ui.Rect paintBounds = - ui.Offset.zero & (ui.window.physicalSize / ui.window.devicePixelRatio); - final ui.PictureRecorder recorder = new ui.PictureRecorder(); - final ui.Canvas canvas = new ui.Canvas(recorder, paintBounds); - canvas.translate(paintBounds.width / 2.0, paintBounds.height / 2.0); - - // Here we determine the rotation speed according to the timeStamp given to us - // by the engine. - final double t = (timeStamp.inMicroseconds / - Duration.MICROSECONDS_PER_MILLISECOND / - 3200.0) % 1.0; - - final List points = [ - const ui.Offset(kRadius, kVerticalOffset), - const ui.Offset(0.0, kVerticalOffset), - new ui.Offset(kRadius * math.cos(t * kTwoPi), - kRadius * math.sin(t * kTwoPi) + kVerticalOffset), - ]; - - // Try changing values for the stroke style and see what the results are - // for different line drawing primitives. - final ui.Paint paint = new ui.Paint() - ..color = const ui.Color.fromARGB(255, 0, 255, 0) - ..style = ui.PaintingStyle.stroke - ..strokeCap = ui.StrokeCap.butt // Other choices are round and square. - ..strokeJoin = ui.StrokeJoin.miter // Other choices are round and bevel. - ..strokeMiterLimit = 4.0 // Try smaller and larger values zero or above. - ..strokeWidth = 20.0; - canvas.drawPoints(ui.PointMode.polygon, points, paint); - - final ui.Path path = new ui.Path() - ..moveTo(points[0].dx, points[0].dy - 2 * kVerticalOffset) - ..lineTo(points[1].dx, points[1].dy - 2 * kVerticalOffset) - ..lineTo(points[2].dx, points[2].dy - 2 * kVerticalOffset) - ..close(); - canvas.drawPath(path, paint); - final ui.Picture picture = recorder.endRecording(); - - // COMPOSITE - - final double devicePixelRatio = ui.window.devicePixelRatio; - final Float64List deviceTransform = new Float64List(16) - ..[0] = devicePixelRatio - ..[5] = devicePixelRatio - ..[10] = 1.0 - ..[15] = 1.0; - final ui.SceneBuilder sceneBuilder = new ui.SceneBuilder() - ..pushTransform(deviceTransform) - ..addPicture(ui.Offset.zero, picture) - ..pop(); - ui.window.render(sceneBuilder.build()); - - // After rendering the current frame of the animation, we ask the engine to - // schedule another frame. The engine will call beginFrame again when its time - // to produce the next frame. - ui.window.scheduleFrame(); -} - -void main() { - ui.window.onBeginFrame = beginFrame; - ui.window.scheduleFrame(); -} From 1afb2d7330491ba76aab292fc7ae1287d86ea8b1 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Thu, 29 Jun 2017 13:18:43 -0700 Subject: [PATCH 13/39] Added arguments to named routes, with test. --- .../flutter/lib/src/widgets/navigator.dart | 42 ++++++++++++------- .../flutter/test/widgets/navigator_test.dart | 42 +++++++++++++++++++ 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index 6cc8e4499f6f3..b6752a300f423 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -204,6 +204,7 @@ class RouteSettings { /// Creates data used to construct routes. const RouteSettings({ this.name, + this.arguments, this.isInitialRoute: false, }); @@ -211,10 +212,12 @@ class RouteSettings { /// replaced with the new values. RouteSettings copyWith({ String name, + Map arguments, bool isInitialRoute, }) { return new RouteSettings( name: name ?? this.name, + arguments: arguments ?? this.arguments, isInitialRoute: isInitialRoute ?? this.isInitialRoute, ); } @@ -224,6 +227,11 @@ class RouteSettings { /// If null, the route is anonymous. final String name; + /// The arguments map that this route was created with. + /// + /// If null, there were no arguments given for this route. + final Map arguments; + /// Whether this route is the very first route being pushed onto this [Navigator]. /// /// The initial route typically skips any entrance transition to speed startup. @@ -558,8 +566,9 @@ class Navigator extends StatefulWidget { /// ```dart /// Navigator.pushNamed(context, '/nyc/1776'); /// ``` - static Future pushNamed(BuildContext context, String routeName) { - return Navigator.of(context).pushNamed(routeName); + static Future pushNamed(BuildContext context, String routeName, + {Map arguments}) { + return Navigator.of(context).pushNamed(routeName, arguments: arguments); } /// Adds the given route to the history of the navigator that most tightly @@ -862,11 +871,13 @@ class NavigatorState extends State with TickerProviderStateMixin { bool _debugLocked = false; // used to prevent re-entrant calls to push, pop, and friends - Route _routeNamed(String name, { bool allowNull: false }) { + Route _routeNamed( + String name, { bool allowNull: false, Map arguments}) { assert(!_debugLocked); assert(name != null); final RouteSettings settings = new RouteSettings( name: name, + arguments: arguments, isInitialRoute: _history.isEmpty, ); Route route = widget.onGenerateRoute(settings); @@ -913,8 +924,8 @@ class NavigatorState extends State with TickerProviderStateMixin { /// ```dart /// Navigator.of(context).pushNamed('/nyc/1776'); /// ``` - Future pushNamed(String name) { - return push(_routeNamed(name)); + Future pushNamed(String name, {Map arguments}) { + return push(_routeNamed(name, arguments: arguments)); } /// Adds the given route to the navigator's history, and transitions to it. @@ -1036,15 +1047,17 @@ class NavigatorState extends State with TickerProviderStateMixin { return newRoute.popped; } - /// Push the route named [name] and dispose the old current route. + /// Push the route named [name] with optional [arguments] and dispose the + /// old current route. /// - /// The route name will be passed to [Navigator.onGenerateRoute]. The returned - /// route will be pushed into the navigator. + /// The new route information will be passed to [Navigator.onGenerateRoute]. + /// The returned route will be pushed into the navigator. /// /// Returns a [Future] that completes to the `result` value passed to [pop] /// when the pushed route is popped off the navigator. - Future pushReplacementNamed(String name, { dynamic result }) { - return pushReplacement(_routeNamed(name), result: result); + Future pushReplacementNamed( + String name, { dynamic result, Map arguments }) { + return pushReplacement(_routeNamed(name, arguments: arguments), result: result); } /// Replaces a route that is not currently visible with a new route. @@ -1135,8 +1148,8 @@ class NavigatorState extends State with TickerProviderStateMixin { return newRoute.popped; } - /// Push the route with the given name and then remove all the previous routes - /// until the `predicate` returns true. + /// Push the route with the given `routeName`, and optional `arguments`, and + /// then remove all the previous routes until the `predicate` returns true. /// /// The predicate may be applied to the same route more than once if /// [Route.willHandlePopInternally] is true. @@ -1146,8 +1159,9 @@ class NavigatorState extends State with TickerProviderStateMixin { /// /// To remove all the routes before the pushed route, use a [RoutePredicate] /// that always returns false. - Future pushNamedAndRemoveUntil(String routeName, RoutePredicate predicate) { - return pushAndRemoveUntil(_routeNamed(routeName), predicate); + Future pushNamedAndRemoveUntil( + String routeName, RoutePredicate predicate, {Map arguments}) { + return pushAndRemoveUntil(_routeNamed(routeName, arguments: arguments), predicate); } /// Tries to pop the current route, first giving the active route the chance diff --git a/packages/flutter/test/widgets/navigator_test.dart b/packages/flutter/test/widgets/navigator_test.dart index 3aa80bbc3e391..2254e815450d3 100644 --- a/packages/flutter/test/widgets/navigator_test.dart +++ b/packages/flutter/test/widgets/navigator_test.dart @@ -566,5 +566,47 @@ void main() { navigator.removeRoute(routes['/A']); // stack becomes /, pageValue will not complete }); + testWidgets('push a route with arguments', (WidgetTester tester) async { + Future pageValue; + final Map argumentsA = {"foo": 1, "bar" : "baz"}; + final Map argumentsB = {"foo": 1, "bar" : "baz"}; + final Map pageBuilders = { + '/': (BuildContext context) => new OnTapPage(id: '/', + onTap: () { pageValue = Navigator.pushNamed(context, '/A', arguments: argumentsA); }), + '/A': (BuildContext context) => new OnTapPage(id: 'A', + onTap: () { Navigator.of(context).pushNamed('/B', arguments: argumentsB); }), + '/B': (BuildContext context) => new OnTapPage(id: 'B', onTap: () { Navigator.pop(context, 'B'); }), + }; + final Map> routes = >{}; + + await tester.pumpWidget(new MaterialApp( + onGenerateRoute: (RouteSettings settings) { + if (settings.name == '/A') { + expect(settings.arguments, equals(argumentsA)); + } else if (settings.name == '/B') { + expect(settings.arguments, equals(argumentsB)); + } else { + expect(settings.arguments, isNull); + } + routes[settings.name] = new PageRouteBuilder( + settings: settings, + pageBuilder: (BuildContext context, Animation _, Animation __) { + return pageBuilders[settings.name](context); + }, + ); + return routes[settings.name]; + } + )); + await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A + await tester.pumpAndSettle(); + + // Navigator.of(context).pushNamed('/B'), stack becomes /, /A, /B + await tester.tap(find.text('A')); + await tester.pumpAndSettle(); + pageValue.then((String value) { assert(false); }); + + final NavigatorState navigator = tester.state(find.byType(Navigator)); + navigator.removeRoute(routes['/B']); // stack becomes /, /A, pageValue will not complete + }); } From 11b44e91666498169f66c451ac41eaaa1fb72344 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Thu, 29 Jun 2017 16:06:23 -0700 Subject: [PATCH 14/39] Fixing some formatting --- packages/flutter/lib/src/widgets/navigator.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index b6752a300f423..d02f720437921 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -871,8 +871,7 @@ class NavigatorState extends State with TickerProviderStateMixin { bool _debugLocked = false; // used to prevent re-entrant calls to push, pop, and friends - Route _routeNamed( - String name, { bool allowNull: false, Map arguments}) { + Route _routeNamed(String name, { bool allowNull: false, Map arguments}) { assert(!_debugLocked); assert(name != null); final RouteSettings settings = new RouteSettings( @@ -1055,8 +1054,8 @@ class NavigatorState extends State with TickerProviderStateMixin { /// /// Returns a [Future] that completes to the `result` value passed to [pop] /// when the pushed route is popped off the navigator. - Future pushReplacementNamed( - String name, { dynamic result, Map arguments }) { + Future pushReplacementNamed(String name, + { dynamic result, Map arguments }) { return pushReplacement(_routeNamed(name, arguments: arguments), result: result); } @@ -1159,8 +1158,8 @@ class NavigatorState extends State with TickerProviderStateMixin { /// /// To remove all the routes before the pushed route, use a [RoutePredicate] /// that always returns false. - Future pushNamedAndRemoveUntil( - String routeName, RoutePredicate predicate, {Map arguments}) { + Future pushNamedAndRemoveUntil(String routeName, RoutePredicate predicate, + {Map arguments}) { return pushAndRemoveUntil(_routeNamed(routeName, arguments: arguments), predicate); } From 5f9e5605a6dc43038c9f035a19e252ca4da7867e Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Tue, 18 Jul 2017 17:24:56 -0700 Subject: [PATCH 15/39] Fix analyzer errors (#11284) --- packages/flutter/test/rendering/proxy_box_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter/test/rendering/proxy_box_test.dart b/packages/flutter/test/rendering/proxy_box_test.dart index ff0958d654ed5..99502d600ceff 100644 --- a/packages/flutter/test/rendering/proxy_box_test.dart +++ b/packages/flutter/test/rendering/proxy_box_test.dart @@ -34,7 +34,7 @@ void main() { test('RenderPhysicalModel compositing on Fuchsia', () { debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia; - final root = new RenderPhysicalModel(color: new Color(0xffff00ff)); + final RenderPhysicalModel root = new RenderPhysicalModel(color: const Color(0xffff00ff)); layout(root, phase: EnginePhase.composite); expect(root.needsCompositing, isFalse); @@ -54,7 +54,7 @@ void main() { test('RenderPhysicalModel compositing on non-Fuchsia', () { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - final root = new RenderPhysicalModel(color: new Color(0xffff00ff)); + final RenderPhysicalModel root = new RenderPhysicalModel(color: const Color(0xffff00ff)); layout(root, phase: EnginePhase.composite); expect(root.needsCompositing, isFalse); From daa7860ef0774da31a213e8fa881caf1882c7855 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Tue, 18 Jul 2017 18:24:38 -0700 Subject: [PATCH 16/39] Add a ScrollController parameter to NestedScrollView (#11242) --- .../lib/src/widgets/nested_scroll_view.dart | 23 ++-- .../lib/src/widgets/scroll_controller.dart | 94 ++++++++++++++- .../test/widgets/nested_scroll_view_test.dart | 111 +++++++++++++++++- 3 files changed, 211 insertions(+), 17 deletions(-) diff --git a/packages/flutter/lib/src/widgets/nested_scroll_view.dart b/packages/flutter/lib/src/widgets/nested_scroll_view.dart index 2b6280ec30bd8..bd6a146dea44d 100644 --- a/packages/flutter/lib/src/widgets/nested_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/nested_scroll_view.dart @@ -32,12 +32,10 @@ import 'ticker_provider.dart'; /// content ostensibly below it. typedef List NestedScrollViewHeaderSliversBuilder(BuildContext context, bool innerBoxIsScrolled); -// TODO(abarth): Make this configurable with a controller. -const double _kInitialScrollOffset = 0.0; - class NestedScrollView extends StatefulWidget { const NestedScrollView({ Key key, + this.controller, this.scrollDirection: Axis.vertical, this.reverse: false, this.physics, @@ -49,7 +47,9 @@ class NestedScrollView extends StatefulWidget { assert(body != null), super(key: key); - // TODO(ianh): we should expose a controller so you can call animateTo, etc. + /// An object that can be used to control the position to which the outer + /// scroll view is scrolled. + final ScrollController controller; /// The axis along which the scroll view scrolls. /// @@ -114,7 +114,7 @@ class _NestedScrollViewState extends State { @override void initState() { super.initState(); - _coordinator = new _NestedScrollCoordinator(context, _kInitialScrollOffset); + _coordinator = new _NestedScrollCoordinator(context, widget.controller); } @override @@ -170,12 +170,14 @@ class _NestedScrollMetrics extends FixedScrollMetrics { typedef ScrollActivity _NestedScrollActivityGetter(_NestedScrollPosition position); class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldController { - _NestedScrollCoordinator(this._context, double initialScrollOffset) { + _NestedScrollCoordinator(this._context, this._parent) { + final double initialScrollOffset = _parent?.initialScrollOffset ?? 0.0; _outerController = new _NestedScrollController(this, initialScrollOffset: initialScrollOffset, debugLabel: 'outer'); - _innerController = new _NestedScrollController(this, initialScrollOffset: initialScrollOffset, debugLabel: 'inner'); + _innerController = new _NestedScrollController(this, initialScrollOffset: 0.0, debugLabel: 'inner'); } final BuildContext _context; + final ScrollController _parent; _NestedScrollController _outerController; _NestedScrollController _innerController; @@ -407,7 +409,7 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont Future animateTo(double to, { @required Duration duration, @required Curve curve, - }) { + }) async { final DrivenScrollActivity outerActivity = _outerPosition.createDrivenScrollActivity( nestOffset(to, _outerPosition), duration, @@ -426,7 +428,7 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont return innerActivity; }, ); - return Future.wait(resultFutures); + await Future.wait(resultFutures); } void jumpTo(double to) { @@ -513,7 +515,7 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont } void updateParent() { - _outerPosition?.setParent(PrimaryScrollController.of(_context)); + _outerPosition?.setParent(_parent ?? PrimaryScrollController.of(_context)); } @mustCallSuper @@ -827,7 +829,6 @@ class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity { done = true; } } else if (velocity < 0.0) { - assert(velocity < 0.0); if (value > metrics.maxRange) return true; if (value < metrics.minRange) { diff --git a/packages/flutter/lib/src/widgets/scroll_controller.dart b/packages/flutter/lib/src/widgets/scroll_controller.dart index b96c5de4e5373..379ba043400dc 100644 --- a/packages/flutter/lib/src/widgets/scroll_controller.dart +++ b/packages/flutter/lib/src/widgets/scroll_controller.dart @@ -45,11 +45,12 @@ class ScrollController extends ChangeNotifier { /// /// The values of `initialScrollOffset` and `keepScrollOffset` must not be null. ScrollController({ - this.initialScrollOffset: 0.0, + double initialScrollOffset: 0.0, this.keepScrollOffset: true, this.debugLabel, }) : assert(initialScrollOffset != null), - assert(keepScrollOffset != null); + assert(keepScrollOffset != null), + _initialScrollOffset = initialScrollOffset; /// The initial value to use for [offset]. /// @@ -58,7 +59,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; + final double _initialScrollOffset; + double get initialScrollOffset => _initialScrollOffset; /// Each time a scroll completes, save the current scroll [offset] with /// [PageStorage] and restore it if this controller's scrollable is recreated. @@ -266,3 +268,89 @@ class ScrollController extends ChangeNotifier { } } } + +// Examples can assume: +// TrackingScrollController _trackingScrollController; + +/// A [ScrollController] whose `initialScrollOffset` tracks its most recently +/// updated [ScrollPosition]. +/// +/// This class can be used to synchronize the scroll offset of two or more +/// lazily created scroll views that share a single [TrackingScrollController]. +/// It tracks the most recently updated scroll position and reports it as its +/// `initialScrollOffset`. +/// +/// ## Sample code +/// +/// In this example each [PageView] page contains a [ListView] and all three +/// [ListView]'s share a [TrackingController]. The scroll offsets of all three +/// list views will track each other, to the extent that's possible given the +/// different list lengths. +/// +/// ```dart +/// new PageView( +/// children: [ +/// new ListView( +/// controller: _trackingScrollController, +/// children: new List.generate(100, (int i) => new Text('page 0 item $i')).toList(), +/// ), +/// new ListView( +/// controller: _trackingScrollController, +/// children: new List.generate(200, (int i) => new Text('page 1 item $i')).toList(), +/// ), +/// new ListView( +/// controller: _trackingScrollController, +/// children: new List.generate(300, (int i) => new Text('page 2 item $i')).toList(), +/// ), +/// ], +/// ) +/// ``` +/// +/// In this example the `_trackingController` would have been created by the +/// stateful widget that built the widget tree. +class TrackingScrollController extends ScrollController { + TrackingScrollController({ + double initialScrollOffset: 0.0, + bool keepScrollOffset: true, + String debugLabel, + }) : super(initialScrollOffset: initialScrollOffset, + keepScrollOffset: keepScrollOffset, + debugLabel: debugLabel); + + 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. + ScrollPosition get mostRecentlyUpdatedPosition => _lastUpdated; + + /// Returns the scroll offset of the [mostRecentlyUpdatedPosition] or 0.0. + @override + double get initialScrollOffset => _lastUpdated?.pixels ?? super.initialScrollOffset; + + @override + void attach(ScrollPosition position) { + super.attach(position); + assert(!_positionToListener.containsKey(position)); + _positionToListener[position] = () { _lastUpdated = position; }; + position.addListener(_positionToListener[position]); + } + + @override + void detach(ScrollPosition position) { + super.detach(position); + assert(_positionToListener.containsKey(position)); + position.removeListener(_positionToListener[position]); + _positionToListener.remove(position); + } + + @override + void dispose() { + for (ScrollPosition position in positions) { + assert(_positionToListener.containsKey(position)); + position.removeListener(_positionToListener[position]); + } + _positionToListener.clear(); + super.dispose(); + } +} diff --git a/packages/flutter/test/widgets/nested_scroll_view_test.dart b/packages/flutter/test/widgets/nested_scroll_view_test.dart index 7d8c63f5ab60c..3483a9c32bd1a 100644 --- a/packages/flutter/test/widgets/nested_scroll_view_test.dart +++ b/packages/flutter/test/widgets/nested_scroll_view_test.dart @@ -6,17 +6,18 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -Widget buildTest() { +Widget buildTest({ ScrollController controller, String title: 'TTTTTTTT' }) { return new MediaQuery( data: const MediaQueryData(), child: new Scaffold( body: new DefaultTabController( length: 4, child: new NestedScrollView( + controller: controller, headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ new SliverAppBar( - title: const Text('TTTTTTTT'), + title: new Text(title), pinned: true, expandedHeight: 200.0, forceElevated: innerBoxIsScrolled, @@ -183,4 +184,108 @@ void main() { expect(find.text('ccc1'), findsOneWidget); expect(tester.renderObject(find.byType(AppBar)).size.height, 200.0); }); -} \ No newline at end of file + + testWidgets('NestedScrollView with a ScrollController', (WidgetTester tester) async { + final ScrollController controller = new ScrollController(initialScrollOffset: 50.0); + + double scrollOffset; + controller.addListener(() { + scrollOffset = controller.offset; + }); + + await tester.pumpWidget(buildTest(controller: controller)); + expect(controller.position.minScrollExtent, 0.0); + expect(controller.position.pixels, 50.0); + expect(controller.position.maxScrollExtent, 200.0); + + // The appbar's expandedHeight - initialScrollOffset = 150. + expect(tester.renderObject(find.byType(AppBar)).size.height, 150.0); + + // Fully expand the appbar by scrolling (no animation) to 0.0. + controller.jumpTo(0.0); + await(tester.pumpAndSettle()); + expect(scrollOffset, 0.0); + expect(tester.renderObject(find.byType(AppBar)).size.height, 200.0); + + // Scroll back to 50.0 animating over 100ms. + controller.animateTo(50.0, duration: const Duration(milliseconds: 100), curve: Curves.linear); + await tester.pump(); + await tester.pump(); + expect(scrollOffset, 0.0); + expect(tester.renderObject(find.byType(AppBar)).size.height, 200.0); + await tester.pump(const Duration(milliseconds: 50)); // 50ms - halfway to scroll offset = 50.0. + expect(scrollOffset, 25.0); + expect(tester.renderObject(find.byType(AppBar)).size.height, 175.0); + await tester.pump(const Duration(milliseconds: 50)); // 100ms - all the way to scroll offset = 50.0. + expect(scrollOffset, 50.0); + expect(tester.renderObject(find.byType(AppBar)).size.height, 150.0); + + // Scroll to the end, (we're not scrolling to the end of the list that contains aaa1, + // just to the end of the outer scrollview). Verify that the first item in each tab + // is still visible. + controller.jumpTo(controller.position.maxScrollExtent); + await tester.pumpAndSettle(); + expect(scrollOffset, 200.0); + expect(find.text('aaa1'), findsOneWidget); + + await tester.tap(find.text('BB')); + await tester.pumpAndSettle(); + expect(find.text('bbb1'), findsOneWidget); + + await tester.tap(find.text('CC')); + await tester.pumpAndSettle(); + expect(find.text('ccc1'), findsOneWidget); + + await tester.tap(find.text('DD')); + await tester.pumpAndSettle(); + expect(find.text('ddd1'), findsOneWidget); + }); + + testWidgets('Three NestedScrollViews with one ScrollController', (WidgetTester tester) async { + final TrackingScrollController controller = new TrackingScrollController(); + expect(controller.mostRecentlyUpdatedPosition, isNull); + expect(controller.initialScrollOffset, 0.0); + + await tester.pumpWidget( + new PageView( + children: [ + buildTest(controller: controller, title: 'Page0'), + buildTest(controller: controller, title: 'Page1'), + buildTest(controller: controller, title: 'Page2'), + ], + ), + ); + + // Initially Page0 is visible and Page0's appbar is fully expanded (height = 200.0). + expect(find.text('Page0'), findsOneWidget); + expect(find.text('Page1'), findsNothing); + expect(find.text('Page2'), findsNothing); + expect(tester.renderObject(find.byType(AppBar)).size.height, 200.0); + + // A scroll collapses Page0's appbar to 150.0. + controller.jumpTo(50.0); + await(tester.pumpAndSettle()); + expect(tester.renderObject(find.byType(AppBar)).size.height, 150.0); + + // Fling to Page1. Page1's appbar height is the same as the appbar for Page0. + await tester.fling(find.text('Page0'), const Offset(-100.0, 0.0), 10000.0); + await(tester.pumpAndSettle()); + expect(find.text('Page0'), findsNothing); + expect(find.text('Page1'), findsOneWidget); + expect(find.text('Page2'), findsNothing); + expect(tester.renderObject(find.byType(AppBar)).size.height, 150.0); + + // Expand Page1's appbar and then fling to Page2. Page2's appbar appears + // fully expanded. + controller.jumpTo(0.0); + await(tester.pumpAndSettle()); + expect(tester.renderObject(find.byType(AppBar)).size.height, 200.0); + await tester.fling(find.text('Page1'), const Offset(-100.0, 0.0), 10000.0); + await(tester.pumpAndSettle()); + expect(find.text('Page0'), findsNothing); + expect(find.text('Page1'), findsNothing); + expect(find.text('Page2'), findsOneWidget); + expect(tester.renderObject(find.byType(AppBar)).size.height, 200.0); + }); + +} From c186d0df1c7ed72b977b156da816f30ffeda1389 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Tue, 18 Jul 2017 18:47:20 -0700 Subject: [PATCH 17/39] pass the value of the android sdk (#11268) * pass the value of the android sdk * swap flag * allow the user to set the android-sdk location --- .../lib/src/android/android_sdk.dart | 5 +- .../flutter_tools/lib/src/base/config.dart | 2 + .../lib/src/commands/config.dart | 13 ++++- .../test/commands/config_test.dart | 56 +++++++++++++++++++ packages/flutter_tools/test/config_test.dart | 36 ++---------- 5 files changed, 80 insertions(+), 32 deletions(-) create mode 100644 packages/flutter_tools/test/commands/config_test.dart diff --git a/packages/flutter_tools/lib/src/android/android_sdk.dart b/packages/flutter_tools/lib/src/android/android_sdk.dart index 4f23574e6b1ce..0ea844b465bc5 100644 --- a/packages/flutter_tools/lib/src/android/android_sdk.dart +++ b/packages/flutter_tools/lib/src/android/android_sdk.dart @@ -67,7 +67,10 @@ class AndroidSdk { static AndroidSdk locateAndroidSdk() { String androidHomeDir; - if (platform.environment.containsKey(kAndroidHome)) { + + if (config.containsKey('android-sdk')) { + androidHomeDir = config.getValue('android-sdk'); + } else if (platform.environment.containsKey(kAndroidHome)) { androidHomeDir = platform.environment[kAndroidHome]; } else if (platform.isLinux) { if (homeDirPath != null) diff --git a/packages/flutter_tools/lib/src/base/config.dart b/packages/flutter_tools/lib/src/base/config.dart index 832280f8a580c..373173f3c5891 100644 --- a/packages/flutter_tools/lib/src/base/config.dart +++ b/packages/flutter_tools/lib/src/base/config.dart @@ -24,6 +24,8 @@ class Config { Iterable get keys => _values.keys; + bool containsKey(String key) => _values.containsKey(key); + dynamic getValue(String key) => _values[key]; void setValue(String key, String value) { diff --git a/packages/flutter_tools/lib/src/commands/config.dart b/packages/flutter_tools/lib/src/commands/config.dart index 25225e1809c50..f05980a37e0f5 100644 --- a/packages/flutter_tools/lib/src/commands/config.dart +++ b/packages/flutter_tools/lib/src/commands/config.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:convert'; +import '../android/android_sdk.dart'; import '../android/android_studio.dart'; import '../globals.dart'; import '../runner/flutter_command.dart'; @@ -19,6 +20,7 @@ class ConfigCommand extends FlutterCommand { negatable: false, help: 'Clear the saved development certificate choice used to sign apps for iOS device deployment.'); argParser.addOption('gradle-dir', help: 'The gradle install directory.'); + argParser.addOption('android-sdk', help: 'The Android SDK directory.'); argParser.addOption('android-studio-dir', help: 'The Android Studio install directory.'); argParser.addFlag('machine', negatable: false, @@ -38,6 +40,9 @@ class ConfigCommand extends FlutterCommand { @override final List aliases = ['configure']; + @override + bool get shouldUpdateCache => false; + @override String get usageFooter { // List all config settings. @@ -69,6 +74,9 @@ class ConfigCommand extends FlutterCommand { if (argResults.wasParsed('gradle-dir')) _updateConfig('gradle-dir', argResults['gradle-dir']); + if (argResults.wasParsed('android-sdk')) + _updateConfig('android-sdk', argResults['android-sdk']); + if (argResults.wasParsed('android-studio-dir')) _updateConfig('android-studio-dir', argResults['android-studio-dir']); @@ -90,8 +98,11 @@ class ConfigCommand extends FlutterCommand { if (results['android-studio-dir'] == null && androidStudio != null) { results['android-studio-dir'] = androidStudio.directory; } + if (results['android-sdk'] == null && androidSdk != null) { + results['android-sdk'] = androidSdk.directory; + } - printStatus(JSON.encode(results)); + printStatus(const JsonEncoder.withIndent(' ').convert(results)); } void _updateConfig(String keyName, String keyValue) { diff --git a/packages/flutter_tools/test/commands/config_test.dart b/packages/flutter_tools/test/commands/config_test.dart new file mode 100644 index 0000000000000..c096e3e0e96c7 --- /dev/null +++ b/packages/flutter_tools/test/commands/config_test.dart @@ -0,0 +1,56 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:flutter_tools/src/android/android_sdk.dart'; +import 'package:flutter_tools/src/android/android_studio.dart'; +import 'package:flutter_tools/src/base/context.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/commands/config.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import '../src/context.dart'; + +void main() { + MockAndroidStudio mockAndroidStudio; + MockAndroidSdk mockAndroidSdk; + + setUp(() { + mockAndroidStudio = new MockAndroidStudio(); + mockAndroidSdk = new MockAndroidSdk(); + }); + + group('config', () { + testUsingContext('machine flag', () async { + final BufferLogger logger = context[Logger]; + final ConfigCommand command = new ConfigCommand(); + await command.handleMachine(); + + expect(logger.statusText, isNotEmpty); + final dynamic json = JSON.decode(logger.statusText); + expect(json, isMap); + + expect(json.containsKey('android-studio-dir'), true); + expect(json['android-studio-dir'], isNotNull); + + expect(json.containsKey('android-sdk'), true); + expect(json['android-sdk'], isNotNull); + }, overrides: { + AndroidStudio: () => mockAndroidStudio, + AndroidSdk: () => mockAndroidSdk, + }); + }); +} + +class MockAndroidStudio extends Mock implements AndroidStudio, Comparable { + @override + String get directory => 'path/to/android/stdio'; +} + +class MockAndroidSdk extends Mock implements AndroidSdk { + @override + String get directory => 'path/to/android/sdk'; +} diff --git a/packages/flutter_tools/test/config_test.dart b/packages/flutter_tools/test/config_test.dart index 88f7a0d97525b..a46d15ee7c941 100644 --- a/packages/flutter_tools/test/config_test.dart +++ b/packages/flutter_tools/test/config_test.dart @@ -2,28 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:convert'; - -import 'package:flutter_tools/src/android/android_studio.dart'; import 'package:flutter_tools/src/base/config.dart'; -import 'package:flutter_tools/src/base/context.dart'; import 'package:flutter_tools/src/base/file_system.dart'; -import 'package:flutter_tools/src/base/logger.dart'; -import 'package:flutter_tools/src/commands/config.dart'; -import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import 'src/context.dart'; - void main() { Config config; - MockAndroidStudio mockAndroidStudio; setUp(() { final Directory tempDirectory = fs.systemTempDirectory.createTempSync('flutter_test'); final File file = fs.file(fs.path.join(tempDirectory.path, '.settings')); config = new Config(file); - mockAndroidStudio = new MockAndroidStudio(); }); group('config', () { @@ -34,6 +23,12 @@ void main() { expect(config.keys, contains('foo')); }); + test('containsKey', () async { + expect(config.containsKey('foo'), false); + config.setValue('foo', 'bar'); + expect(config.containsKey('foo'), true); + }); + test('removeValue', () async { expect(config.getValue('foo'), null); config.setValue('foo', 'bar'); @@ -43,24 +38,5 @@ void main() { expect(config.getValue('foo'), null); expect(config.keys, isNot(contains('foo'))); }); - - testUsingContext('machine flag', () async { - final BufferLogger logger = context[Logger]; - final ConfigCommand command = new ConfigCommand(); - await command.handleMachine(); - - expect(logger.statusText, isNotEmpty); - final dynamic json = JSON.decode(logger.statusText); - expect(json, isMap); - expect(json.containsKey('android-studio-dir'), true); - expect(json['android-studio-dir'], isNotNull); - }, overrides: { - AndroidStudio: () => mockAndroidStudio, - }); }); } - -class MockAndroidStudio extends Mock implements AndroidStudio, Comparable { - @override - String get directory => 'path/to/android/stdio'; -} From e13e7806e37a28cfef766b72a6987593063b8d34 Mon Sep 17 00:00:00 2001 From: Dan Rubel Date: Wed, 19 Jul 2017 07:22:38 -0400 Subject: [PATCH 18/39] Flutter analyze watch improvements (#11143) * flutter analyze --watch auto detect if in flutter repo * move isFlutterLibrary from AnalyzeOnce into AnalyzeBase for use by AnalyzeContinuously * pass --flutter-repo to analysis server when analyzing the flutter repository * enhance flutter analyze --watch to summarize public members lacking documentation --- .../lib/src/commands/analyze_base.dart | 19 +++++ .../src/commands/analyze_continuously.dart | 58 +++++++++++--- .../lib/src/commands/analyze_once.dart | 19 ----- .../commands/analyze_continuously_test.dart | 77 +++++++++++-------- 4 files changed, 111 insertions(+), 62 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/analyze_base.dart b/packages/flutter_tools/lib/src/commands/analyze_base.dart index 177a344124d4d..f2e9ec63ed8fe 100644 --- a/packages/flutter_tools/lib/src/commands/analyze_base.dart +++ b/packages/flutter_tools/lib/src/commands/analyze_base.dart @@ -39,6 +39,25 @@ abstract class AnalyzeBase { } } + List flutterRootComponents; + bool isFlutterLibrary(String filename) { + flutterRootComponents ??= fs.path.normalize(fs.path.absolute(Cache.flutterRoot)).split(fs.path.separator); + final List filenameComponents = fs.path.normalize(fs.path.absolute(filename)).split(fs.path.separator); + if (filenameComponents.length < flutterRootComponents.length + 4) // the 4: 'packages', package_name, 'lib', file_name + return false; + for (int index = 0; index < flutterRootComponents.length; index += 1) { + if (flutterRootComponents[index] != filenameComponents[index]) + return false; + } + if (filenameComponents[flutterRootComponents.length] != 'packages') + return false; + if (filenameComponents[flutterRootComponents.length + 1] == 'flutter_tools') + return false; + if (filenameComponents[flutterRootComponents.length + 2] != 'lib') + return false; + return true; + } + void writeBenchmark(Stopwatch stopwatch, int errorCount, int membersMissingDocumentation) { final String benchmarkOut = 'analysis_benchmark.json'; final Map data = { diff --git a/packages/flutter_tools/lib/src/commands/analyze_continuously.dart b/packages/flutter_tools/lib/src/commands/analyze_continuously.dart index dac4347a16d06..aba659bba4ca5 100644 --- a/packages/flutter_tools/lib/src/commands/analyze_continuously.dart +++ b/packages/flutter_tools/lib/src/commands/analyze_continuously.dart @@ -31,15 +31,20 @@ class AnalyzeContinuously extends AnalyzeBase { Stopwatch analysisTimer; int lastErrorCount = 0; Status analysisStatus; + bool flutterRepo; + bool showDartDocIssuesIndividually; @override Future analyze() async { List directories; - if (argResults['dartdocs']) - throwToolExit('The --dartdocs option is currently not supported when using --watch.'); + flutterRepo = argResults['flutter-repo'] || inRepo(null); + showDartDocIssuesIndividually = argResults['dartdocs']; - if (argResults['flutter-repo']) { + if (showDartDocIssuesIndividually && !flutterRepo) + throwToolExit('The --dartdocs option is only supported when using --flutter-repo.'); + + if (flutterRepo) { final PackageDependencyTracker dependencies = new PackageDependencyTracker(); dependencies.checkForConflictingDependencies(repoPackages, dependencies); directories = repoPackages.map((Directory dir) => dir.path).toList(); @@ -52,7 +57,7 @@ class AnalyzeContinuously extends AnalyzeBase { analysisTarget = fs.currentDirectory.path; } - final AnalysisServer server = new AnalysisServer(dartSdkPath, directories); + final AnalysisServer server = new AnalysisServer(dartSdkPath, directories, flutterRepo: flutterRepo); server.onAnalyzing.listen((bool isAnalyzing) => _handleAnalysisStatus(server, isAnalyzing)); server.onErrors.listen(_handleAnalysisErrors); @@ -82,29 +87,52 @@ class AnalyzeContinuously extends AnalyzeBase { logger.printStatus(terminal.clearScreen(), newline: false); // Remove errors for deleted files, sort, and print errors. - final List errors = []; + final List allErrors = []; for (String path in analysisErrors.keys.toList()) { if (fs.isFileSync(path)) { - errors.addAll(analysisErrors[path]); + allErrors.addAll(analysisErrors[path]); } else { analysisErrors.remove(path); } } - errors.sort(); + // Summarize dartdoc issues rather than displaying each individually + int membersMissingDocumentation = 0; + List detailErrors; + if (flutterRepo && !showDartDocIssuesIndividually) { + detailErrors = allErrors.where((AnalysisError error) { + if (error.code == 'public_member_api_docs') { + // https://github.com/dart-lang/linter/issues/208 + if (isFlutterLibrary(error.file)) + membersMissingDocumentation += 1; + return true; + } + return false; + }).toList(); + } else { + detailErrors = allErrors; + } + + detailErrors.sort(); - for (AnalysisError error in errors) { + for (AnalysisError error in detailErrors) { printStatus(error.toString()); if (error.code != null) printTrace('error code: ${error.code}'); } - dumpErrors(errors.map((AnalysisError error) => error.toLegacyString())); + dumpErrors(detailErrors.map((AnalysisError error) => error.toLegacyString())); + + if (membersMissingDocumentation != 0) { + printStatus(membersMissingDocumentation == 1 + ? '1 public member lacks documentation' + : '$membersMissingDocumentation public members lack documentation'); + } // Print an analysis summary. String errorsMessage; - final int issueCount = errors.length; + final int issueCount = detailErrors.length; final int issueDiff = issueCount - lastErrorCount; lastErrorCount = issueCount; @@ -150,10 +178,11 @@ class AnalyzeContinuously extends AnalyzeBase { } class AnalysisServer { - AnalysisServer(this.sdk, this.directories); + AnalysisServer(this.sdk, this.directories, { this.flutterRepo: false }); final String sdk; final List directories; + final bool flutterRepo; Process _process; final StreamController _analyzingController = new StreamController.broadcast(); @@ -169,6 +198,13 @@ class AnalysisServer { '--sdk', sdk, ]; + // Let the analysis server know that the flutter repository is being analyzed + // so that it can enable the public_member_api_docs lint even though + // the analysis_options file does not have that lint enabled. + // It is not enabled in the analysis_option file + // because doing so causes too much noise in the IDE. + if (flutterRepo) + command.add('--flutter-repo'); printTrace('dart ${command.skip(1).join(' ')}'); _process = await processManager.start(command); diff --git a/packages/flutter_tools/lib/src/commands/analyze_once.dart b/packages/flutter_tools/lib/src/commands/analyze_once.dart index 10342507576f7..e5e2888393a50 100644 --- a/packages/flutter_tools/lib/src/commands/analyze_once.dart +++ b/packages/flutter_tools/lib/src/commands/analyze_once.dart @@ -245,25 +245,6 @@ class AnalyzeOnce extends AnalyzeBase { return dir; } - List flutterRootComponents; - bool isFlutterLibrary(String filename) { - flutterRootComponents ??= fs.path.normalize(fs.path.absolute(Cache.flutterRoot)).split(fs.path.separator); - final List filenameComponents = fs.path.normalize(fs.path.absolute(filename)).split(fs.path.separator); - if (filenameComponents.length < flutterRootComponents.length + 4) // the 4: 'packages', package_name, 'lib', file_name - return false; - for (int index = 0; index < flutterRootComponents.length; index += 1) { - if (flutterRootComponents[index] != filenameComponents[index]) - return false; - } - if (filenameComponents[flutterRootComponents.length] != 'packages') - return false; - if (filenameComponents[flutterRootComponents.length + 1] == 'flutter_tools') - return false; - if (filenameComponents[flutterRootComponents.length + 2] != 'lib') - return false; - return true; - } - List _collectDartFiles(Directory dir, List collected) { // Bail out in case of a .dartignore. if (fs.isFileSync(fs.path.join(dir.path, '.dartignore'))) diff --git a/packages/flutter_tools/test/commands/analyze_continuously_test.dart b/packages/flutter_tools/test/commands/analyze_continuously_test.dart index ad72fe178a367..f88858f6863b4 100644 --- a/packages/flutter_tools/test/commands/analyze_continuously_test.dart +++ b/packages/flutter_tools/test/commands/analyze_continuously_test.dart @@ -23,51 +23,55 @@ void main() { tempDir = fs.systemTempDirectory.createTempSync('analysis_test'); }); + Future analyzeWithServer({ bool brokenCode: false, bool flutterRepo: false, int expectedErrorCount: 0 }) async { + _createSampleProject(tempDir, brokenCode: brokenCode); + + await pubGet(directory: tempDir.path); + + server = new AnalysisServer(dartSdkPath, [tempDir.path], flutterRepo: flutterRepo); + + final Future onDone = server.onAnalyzing.where((bool analyzing) => analyzing == false).first; + final List errors = []; + server.onErrors.listen((FileAnalysisErrors fileErrors) { + errors.addAll(fileErrors.errors); + }); + + await server.start(); + await onDone; + + expect(errors, hasLength(expectedErrorCount)); + return server; + } + tearDown(() { tempDir?.deleteSync(recursive: true); return server?.dispose(); }); group('analyze --watch', () { - testUsingContext('AnalysisServer success', () async { - _createSampleProject(tempDir); - - await pubGet(directory: tempDir.path); - - server = new AnalysisServer(dartSdkPath, [tempDir.path]); - - int errorCount = 0; - final Future onDone = server.onAnalyzing.where((bool analyzing) => analyzing == false).first; - server.onErrors.listen((FileAnalysisErrors errors) => errorCount += errors.errors.length); - - await server.start(); - await onDone; + }); - expect(errorCount, 0); + group('AnalysisServer', () { + testUsingContext('success', () async { + server = await analyzeWithServer(); }, overrides: { OperatingSystemUtils: () => os }); - }); - testUsingContext('AnalysisServer errors', () async { - _createSampleProject(tempDir, brokenCode: true); - - await pubGet(directory: tempDir.path); - - server = new AnalysisServer(dartSdkPath, [tempDir.path]); - - int errorCount = 0; - final Future onDone = server.onAnalyzing.where((bool analyzing) => analyzing == false).first; - server.onErrors.listen((FileAnalysisErrors errors) { - errorCount += errors.errors.length; + testUsingContext('errors', () async { + server = await analyzeWithServer(brokenCode: true, expectedErrorCount: 1); + }, overrides: { + OperatingSystemUtils: () => os }); - await server.start(); - await onDone; - - expect(errorCount, greaterThan(0)); - }, overrides: { - OperatingSystemUtils: () => os + testUsingContext('--flutter-repo', () async { + // When a Dart SDK containing support for the --flutter-repo startup flag + // https://github.com/dart-lang/sdk/commit/def1ee6604c4b3385b567cb9832af0dbbaf32e0d + // is rolled into Flutter, then the expectedErrorCount should be set to 1. + server = await analyzeWithServer(flutterRepo: true, expectedErrorCount: 0); + }, overrides: { + OperatingSystemUtils: () => os + }); }); } @@ -77,6 +81,13 @@ void _createSampleProject(Directory directory, { bool brokenCode: false }) { name: foo_project '''); + final File analysisOptionsFile = fs.file(fs.path.join(directory.path, 'analysis_options.yaml')); + analysisOptionsFile.writeAsStringSync(''' +linter: + rules: + - hash_and_equals +'''); + final File dartFile = fs.file(fs.path.join(directory.path, 'lib', 'main.dart')); dartFile.parent.createSync(); dartFile.writeAsStringSync(''' @@ -84,5 +95,7 @@ void main() { print('hello world'); ${brokenCode ? 'prints("hello world");' : ''} } + +class SomeClassWithoutDartDoc { } '''); } From df5cb390e2a44464d2edf499952dca8ed64a238f Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Wed, 19 Jul 2017 10:55:56 -0700 Subject: [PATCH 19/39] Rev engine to 488584f8b7cf188d4699880d7b55144cd48067bf (#11291) --- 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 5acd362f45fae..d3da9032c2483 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -c757fc74512fe9039a31e194906bf3700b4c1319 +488584f8b7cf188d4699880d7b55144cd48067bf From bc4a3f17034ab51693bc932a9b4eab0f3c23bae5 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Wed, 19 Jul 2017 12:05:30 -0700 Subject: [PATCH 20/39] Work around to fix appveyor build (#11295) * work around for appveyor connectivity issues Unfortuantelly, this slows down our build :( * review feedback --- bin/internal/update_dart_sdk.ps1 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bin/internal/update_dart_sdk.ps1 b/bin/internal/update_dart_sdk.ps1 index 1f16cb6ebb685..43a10566b1be8 100644 --- a/bin/internal/update_dart_sdk.ps1 +++ b/bin/internal/update_dart_sdk.ps1 @@ -40,8 +40,13 @@ if (Test-Path $dartSdkPath) { } New-Item $dartSdkPath -force -type directory | Out-Null $dartSdkZip = "$cachePath\dart-sdk.zip" -Import-Module BitsTransfer -Start-BitsTransfer -Source $dartSdkUrl -Destination $dartSdkZip +# 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 +} Write-Host "Unzipping Dart SDK..." If (Get-Command 7z -errorAction SilentlyContinue) { From 1744e8e0aadd8f4115140a29c407d689338bd297 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Wed, 19 Jul 2017 12:21:36 -0700 Subject: [PATCH 21/39] Expose the currently available semantic scroll actions (#11286) * Expose the currently available semantic scroll actions * review comments * add test * refactor to set --- .../flutter/lib/src/rendering/proxy_box.dart | 33 +++++++++--- .../lib/src/widgets/gesture_detector.dart | 23 +++++++++ .../lib/src/widgets/scroll_context.dart | 3 ++ .../lib/src/widgets/scroll_position.dart | 31 +++++++++++ .../flutter/lib/src/widgets/scrollable.dart | 10 ++++ .../widgets/scrollable_semantics_test.dart | 51 +++++++++++++++++++ 6 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 packages/flutter/test/widgets/scrollable_semantics_test.dart diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 90843e1160bb5..d90f4be195e12 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -8,6 +8,7 @@ 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'; @@ -2731,6 +2732,15 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA _onVerticalDragUpdate = onVerticalDragUpdate, super(child); + Set get validActions => _validActions; + Set _validActions; + set validActions(Set value) { + if (const SetEquality().equals(value, _validActions)) + return; + _validActions = value; + markNeedsSemanticsUpdate(onlyChanges: true); + } + /// Called when the user taps on the render object. GestureTapCallback get onTap => _onTap; GestureTapCallback _onTap; @@ -2802,14 +2812,25 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA SemanticsAnnotator get semanticsAnnotator => isSemanticBoundary ? _annotate : null; void _annotate(SemanticsNode node) { + List actions = []; if (onTap != null) - node.addAction(SemanticsAction.tap); + actions.add(SemanticsAction.tap); if (onLongPress != null) - node.addAction(SemanticsAction.longPress); - if (onHorizontalDragUpdate != null) - node.addHorizontalScrollingActions(); - if (onVerticalDragUpdate != null) - node.addVerticalScrollingActions(); + actions.add(SemanticsAction.longPress); + if (onHorizontalDragUpdate != null) { + actions.add(SemanticsAction.scrollRight); + actions.add(SemanticsAction.scrollLeft); + } + if (onVerticalDragUpdate != null) { + actions.add(SemanticsAction.scrollUp); + actions.add(SemanticsAction.scrollDown); + } + + // If a set of validActions has been provided only expose those. + if (validActions != null) + actions = actions.where((SemanticsAction action) => validActions.contains(action)).toList(); + + actions.forEach(node.addAction); } @override diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart index 5c681f35a2015..ad3db0e259621 100644 --- a/packages/flutter/lib/src/widgets/gesture_detector.dart +++ b/packages/flutter/lib/src/widgets/gesture_detector.dart @@ -535,6 +535,25 @@ class RawGestureDetectorState extends State { } } + void replaceSemanticsActions(Set actions) { + assert(() { + if (!context.findRenderObject().owner.debugDoingLayout) { + throw new FlutterError( + 'Unexpected call to replaceSemanticsActions() method of RawGestureDetectorState.\n' + 'The replaceSemanticsActions() method can only be called during the layout phase.' + ); + } + return true; + }); + if (!widget.excludeFromSemantics) { + final RenderSemanticsGestureHandler semanticsGestureHandler = context.findRenderObject(); + context.visitChildElements((Element element) { + final _GestureSemantics widget = element.widget; + widget._updateSemanticsActions(semanticsGestureHandler, actions); + }); + } + } + @override void dispose() { for (GestureRecognizer recognizer in _recognizers.values) @@ -714,6 +733,10 @@ class _GestureSemantics extends SingleChildRenderObjectWidget { recognizers.containsKey(PanGestureRecognizer) ? _handleVerticalDragUpdate : null; } + void _updateSemanticsActions(RenderSemanticsGestureHandler renderObject, Set actions) { + renderObject.validActions = actions; + } + @override void updateRenderObject(BuildContext context, RenderSemanticsGestureHandler renderObject) { _updateHandlers(renderObject, owner._recognizers); diff --git a/packages/flutter/lib/src/widgets/scroll_context.dart b/packages/flutter/lib/src/widgets/scroll_context.dart index 71fec6d219ddd..ef62f6b7659e8 100644 --- a/packages/flutter/lib/src/widgets/scroll_context.dart +++ b/packages/flutter/lib/src/widgets/scroll_context.dart @@ -56,4 +56,7 @@ abstract class ScrollContext { /// Whether the user can drag the widget, for example to initiate a scroll. void setCanDrag(bool value); + + /// Set the [SemanticsAction]s that should be expose to the semantics tree. + void setSemanticsActions(Set actions); } diff --git a/packages/flutter/lib/src/widgets/scroll_position.dart b/packages/flutter/lib/src/widgets/scroll_position.dart index c96ee6ea2fb93..0519f91131620 100644 --- a/packages/flutter/lib/src/widgets/scroll_position.dart +++ b/packages/flutter/lib/src/widgets/scroll_position.dart @@ -4,6 +4,7 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; @@ -367,6 +368,35 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { return true; } + Set _semanticActions; + + void _updateSemanticActions() { + SemanticsAction forward; + SemanticsAction backward; + switch (axis) { + case Axis.vertical: + forward = SemanticsAction.scrollUp; + backward = SemanticsAction.scrollDown; + break; + case Axis.horizontal: + forward = SemanticsAction.scrollLeft; + backward = SemanticsAction.scrollRight; + break; + } + + final Set actions = new Set(); + if (pixels > minScrollExtent) + actions.add(backward); + if (pixels < maxScrollExtent) + actions.add(forward); + + if (const SetEquality().equals(actions, _semanticActions)) + return; + + _semanticActions = actions; + context.setSemanticsActions(_semanticActions); + } + @override bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { if (_minScrollExtent != minScrollExtent || @@ -378,6 +408,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { applyNewDimensions(); _didChangeViewportDimension = false; } + _updateSemanticActions(); return true; } diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index 45e06e99cf249..1284b0e8171a7 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -304,6 +304,16 @@ class ScrollableState extends State with TickerProviderStateMixin } + // SEMANTICS ACTIONS + + @override + @protected + void setSemanticsActions(Set actions) { + if (_gestureDetectorKey.currentState != null) + _gestureDetectorKey.currentState.replaceSemanticsActions(actions); + } + + // GESTURE RECOGNITION AND POINTER IGNORING final GlobalKey _gestureDetectorKey = new GlobalKey(); diff --git a/packages/flutter/test/widgets/scrollable_semantics_test.dart b/packages/flutter/test/widgets/scrollable_semantics_test.dart new file mode 100644 index 0000000000000..1de20fe3a4e64 --- /dev/null +++ b/packages/flutter/test/widgets/scrollable_semantics_test.dart @@ -0,0 +1,51 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +import 'semantics_tester.dart'; + +void main() { + testWidgets('scrollable exposes the correct semantic actions', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + final List textWidgets = []; + for (int i = 0; i < 80; i++) + textWidgets.add(new Text('$i')); + await tester.pumpWidget(new ListView(children: textWidgets)); + + expect(semantics,includesNodeWith(actions: [SemanticsAction.scrollUp])); + + await flingUp(tester); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollUp, SemanticsAction.scrollDown])); + + await flingDown(tester, repetitions: 2); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollUp])); + + await flingUp(tester, repetitions: 5); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollDown])); + + await flingDown(tester); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollUp, SemanticsAction.scrollDown])); + + }); +} + +Future flingUp(WidgetTester tester, { int repetitions: 1 }) async { + while (repetitions-- > 0) { + await tester.fling(find.byType(ListView), const Offset(0.0, -200.0), 1000.0); + await tester.pump(); + await tester.pump(const Duration(seconds: 5)); + } +} + +Future flingDown(WidgetTester tester, { int repetitions: 1 }) async { + while (repetitions-- > 0) { + await tester.fling(find.byType(ListView), const Offset(0.0, 200.0), 1000.0); + await tester.pump(); + await tester.pump(const Duration(seconds: 5)); + } +} \ No newline at end of file From 77b0c1dab0567d64b261ceec34f919ffa1bf2590 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Wed, 19 Jul 2017 12:57:13 -0700 Subject: [PATCH 22/39] Don't pass "null" to debugPrint. (#11265) debugDumpLayerTree in particular was passing null in profile mode since debugLayer isn't available in profile mode. --- packages/flutter/lib/src/rendering/binding.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index 6faf2d72f52f8..655507669dfc4 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -309,12 +309,12 @@ abstract class RendererBinding extends BindingBase with SchedulerBinding, Servic /// Prints a textual representation of the entire render tree. void debugDumpRenderTree() { - debugPrint(RendererBinding.instance?.renderView?.toStringDeep()); + debugPrint(RendererBinding.instance?.renderView?.toStringDeep() ?? 'Render tree unavailable.'); } /// Prints a textual representation of the entire layer tree. void debugDumpLayerTree() { - debugPrint(RendererBinding.instance?.renderView?.debugLayer?.toStringDeep()); + debugPrint(RendererBinding.instance?.renderView?.debugLayer?.toStringDeep() ?? 'Layer tree unavailable.'); } /// Prints a textual representation of the entire semantics tree. From e1adc525d83430ad11966785eadedbe0fcb0114c Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Wed, 19 Jul 2017 12:57:22 -0700 Subject: [PATCH 23/39] Option to enable the performance overlay from 'flutter run'. (#11288) --- dev/devicelab/bin/tasks/commands_test.dart | 93 +++++++++++++++++++ dev/devicelab/lib/framework/framework.dart | 3 +- dev/devicelab/manifest.yaml | 16 ++-- dev/integration_tests/ui/lib/commands.dart | 43 +++++++++ .../commands_debug_paint_test.dart | 22 +++++ .../ui/test_driver/commands_none_test.dart | 23 +++++ .../commands_performance_overlay_test.dart | 23 +++++ .../ui/test_driver/keyboard_resize_test.dart | 4 + .../ui/test_driver/route_test.dart | 4 + packages/flutter_driver/lib/src/driver.dart | 11 ++- .../flutter_driver/lib/src/extension.dart | 28 ++++++ packages/flutter_driver/lib/src/find.dart | 50 ++++++++++ .../flutter_tools/lib/src/commands/drive.dart | 17 ++++ .../lib/src/resident_runner.dart | 23 ++++- .../lib/src/runner/flutter_command.dart | 5 +- packages/flutter_tools/lib/src/vmservice.dart | 10 +- 16 files changed, 358 insertions(+), 17 deletions(-) create mode 100644 dev/devicelab/bin/tasks/commands_test.dart create mode 100644 dev/integration_tests/ui/lib/commands.dart create mode 100644 dev/integration_tests/ui/test_driver/commands_debug_paint_test.dart create mode 100644 dev/integration_tests/ui/test_driver/commands_none_test.dart create mode 100644 dev/integration_tests/ui/test_driver/commands_performance_overlay_test.dart diff --git a/dev/devicelab/bin/tasks/commands_test.dart b/dev/devicelab/bin/tasks/commands_test.dart new file mode 100644 index 0000000000000..01641289dbeca --- /dev/null +++ b/dev/devicelab/bin/tasks/commands_test.dart @@ -0,0 +1,93 @@ +// Copyright (c) 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'package:flutter_devicelab/framework/adb.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; + +void main() { + task(() async { + final Device device = await devices.workingDevice; + await device.unlock(); + final Directory appDir = dir(path.join(flutterDirectory.path, 'dev/integration_tests/ui')); + await inDirectory(appDir, () async { + final Completer ready = new Completer(); + bool ok; + print('run: starting...'); + final Process run = await startProcess( + path.join(flutterDirectory.path, 'bin', 'flutter'), + ['run', '--verbose', '--observatory-port=8888', '-d', device.deviceId, 'lib/commands.dart'], + ); + run.stdout + .transform(UTF8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + print('run:stdout: $line'); + if (line.contains(new RegExp(r'^\[\s+\] For a more detailed help message, press "h"\. To quit, press "q"\.'))) { + print('run: ready!'); + ready.complete(); + ok ??= true; + } + }); + run.stderr + .transform(UTF8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + stderr.writeln('run:stderr: $line'); + }); + run.exitCode.then((int exitCode) { ok = false; }); + await Future.any(>[ ready.future, run.exitCode ]); + if (!ok) + throw 'Failed to run test app.'; + await drive('none'); + print('test: pressing "p" to enable debugPaintSize...'); + run.stdin.write('p'); + await drive('debug_paint'); + print('test: pressing "p" again...'); + run.stdin.write('p'); + await drive('none'); + print('test: pressing "P" to enable performance overlay...'); + run.stdin.write('P'); + await drive('performance_overlay'); + print('test: pressing "P" again...'); + run.stdin.write('P'); + await drive('none'); + run.stdin.write('q'); + final int result = await run.exitCode; + if (result != 0) + throw 'Received unexpected exit code $result from run process.'; + }); + return new TaskResult.success(null); + }); +} + +Future drive(String name) async { + print('drive: running commands_$name check...'); + final Process drive = await startProcess( + path.join(flutterDirectory.path, 'bin', 'flutter'), + ['drive', '--use-existing-app', 'http://127.0.0.1:8888/', '--keep-app-running', '--driver', 'test_driver/commands_${name}_test.dart'], + ); + drive.stdout + .transform(UTF8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + print('drive:stdout: $line'); + }); + drive.stderr + .transform(UTF8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + stderr.writeln('drive:stderr: $line'); + }); + final int result = await drive.exitCode; + if (result != 0) + throw 'Failed to drive test app (exit code $result).'; + print('drive: finished commands_$name check successfully.'); +} \ No newline at end of file diff --git a/dev/devicelab/lib/framework/framework.dart b/dev/devicelab/lib/framework/framework.dart index f86124eeeec7c..be9d26d0c0ee3 100644 --- a/dev/devicelab/lib/framework/framework.dart +++ b/dev/devicelab/lib/framework/framework.dart @@ -134,7 +134,8 @@ class _TaskRunner { // are catching errors coming from arbitrary (and untrustworthy) task // code. Our goal is to convert the failure into a readable message. // Propagating it further is not useful. - completer.complete(new TaskResult.failure(message)); + if (!completer.isCompleted) + completer.complete(new TaskResult.failure(message)); }); return completer.future; } diff --git a/dev/devicelab/manifest.yaml b/dev/devicelab/manifest.yaml index c93ae2b233edb..2b46fe19ba030 100644 --- a/dev/devicelab/manifest.yaml +++ b/dev/devicelab/manifest.yaml @@ -114,6 +114,13 @@ tasks: stage: devicelab required_agent_capabilities: ["has-android-device"] + commands_test: + description: > + Runs tests of flutter run commands. + stage: devicelab + required_agent_capabilities: ["has-android-device"] + flaky: true + android_sample_catalog_generator: description: > Builds sample catalog markdown pages and Android screenshots @@ -131,7 +138,6 @@ tasks: Verifies that `flutter drive --route` still works. No performance numbers. stage: devicelab required_agent_capabilities: ["linux/android"] - flaky: true flutter_gallery_instrumentation_test: description: > @@ -140,7 +146,6 @@ tasks: test can run on off-the-shelf infrastructures, such as Firebase Test Lab. stage: devicelab required_agent_capabilities: ["linux/android"] - flaky: true # iOS on-device tests @@ -149,14 +154,12 @@ tasks: Checks that platform channels work on iOS. stage: devicelab_ios required_agent_capabilities: ["has-ios-device"] - flaky: true platform_channel_sample_test_ios: description: > Runs a driver test on the Platform Channel sample app on iOS. stage: devicelab_ios required_agent_capabilities: ["has-ios-device"] - flaky: true complex_layout_scroll_perf_ios__timeline_summary: description: > @@ -164,7 +167,6 @@ tasks: iOS. stage: devicelab_ios required_agent_capabilities: ["has-ios-device"] - flaky: true # flutter_gallery_ios__start_up: # description: > @@ -186,14 +188,12 @@ tasks: iOS. stage: devicelab_ios required_agent_capabilities: ["has-ios-device"] - flaky: true basic_material_app_ios__size: description: > Measures the IPA size of a basic material app. stage: devicelab_ios required_agent_capabilities: ["has-ios-device"] - flaky: true # microbenchmarks_ios: # description: > @@ -215,14 +215,12 @@ tasks: Runs end-to-end Flutter tests on iOS. stage: devicelab_ios required_agent_capabilities: ["has-ios-device"] - flaky: true ios_sample_catalog_generator: description: > Builds sample catalog markdown pages and iOS screenshots stage: devicelab_ios required_agent_capabilities: ["has-ios-device"] - flaky: true # Tests running on Windows host diff --git a/dev/integration_tests/ui/lib/commands.dart b/dev/integration_tests/ui/lib/commands.dart new file mode 100644 index 0000000000000..2e066408b71af --- /dev/null +++ b/dev/integration_tests/ui/lib/commands.dart @@ -0,0 +1,43 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_driver/driver_extension.dart'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +String log = ''; + +void main() { + enableFlutterDriverExtension(handler: (String message) async { + log = 'log:'; + await WidgetsBinding.instance.reassembleApplication(); + return log; + }); + runApp(new MaterialApp(home: const Test())); +} + +class Test extends SingleChildRenderObjectWidget { + const Test({ Key key }) : super(key: key); + + @override + RenderTest createRenderObject(BuildContext context) { + return new RenderTest(); + } +} + +class RenderTest extends RenderProxyBox { + RenderTest({ RenderBox child }) : super(child); + + @override + void debugPaintSize(PaintingContext context, Offset offset) { + super.debugPaintSize(context, offset); + log += ' debugPaintSize'; + } + + @override + void paint(PaintingContext context, Offset offset) { + log += ' paint'; + } +} diff --git a/dev/integration_tests/ui/test_driver/commands_debug_paint_test.dart b/dev/integration_tests/ui/test_driver/commands_debug_paint_test.dart new file mode 100644 index 0000000000000..d7046c98b5377 --- /dev/null +++ b/dev/integration_tests/ui/test_driver/commands_debug_paint_test.dart @@ -0,0 +1,22 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:test/test.dart'; + +void main() { + FlutterDriver driver; + + setUpAll(() async { + driver = await FlutterDriver.connect(); + }); + + tearDownAll(() async { + driver?.close(); + }); + + test('check that we are painting in debugPaintSize mode', () async { + expect(await driver.requestData('status'), 'log: paint debugPaintSize'); + }); +} diff --git a/dev/integration_tests/ui/test_driver/commands_none_test.dart b/dev/integration_tests/ui/test_driver/commands_none_test.dart new file mode 100644 index 0000000000000..f14815504aaaf --- /dev/null +++ b/dev/integration_tests/ui/test_driver/commands_none_test.dart @@ -0,0 +1,23 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:test/test.dart'; + +void main() { + FlutterDriver driver; + + setUpAll(() async { + driver = await FlutterDriver.connect(); + }); + + tearDownAll(() async { + driver?.close(); + }); + + test('check that we are in normal mode', () async { + expect(await driver.requestData('status'), 'log: paint'); + await driver.waitForAbsent(find.byType('PerformanceOverlay'), timeout: Duration.ZERO); + }); +} diff --git a/dev/integration_tests/ui/test_driver/commands_performance_overlay_test.dart b/dev/integration_tests/ui/test_driver/commands_performance_overlay_test.dart new file mode 100644 index 0000000000000..91140e1223cf9 --- /dev/null +++ b/dev/integration_tests/ui/test_driver/commands_performance_overlay_test.dart @@ -0,0 +1,23 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:test/test.dart'; + +void main() { + FlutterDriver driver; + + setUpAll(() async { + driver = await FlutterDriver.connect(); + }); + + tearDownAll(() async { + driver?.close(); + }); + + test('check that we are showing the performance overlay', () async { + await driver.requestData('status'); // force a reassemble + await driver.waitFor(find.byType('PerformanceOverlay'), timeout: Duration.ZERO); + }); +} diff --git a/dev/integration_tests/ui/test_driver/keyboard_resize_test.dart b/dev/integration_tests/ui/test_driver/keyboard_resize_test.dart index 3efb7fc930bd1..f10dff665f057 100644 --- a/dev/integration_tests/ui/test_driver/keyboard_resize_test.dart +++ b/dev/integration_tests/ui/test_driver/keyboard_resize_test.dart @@ -1,3 +1,7 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'dart:async'; import 'package:integration_ui/keys.dart' as keys; diff --git a/dev/integration_tests/ui/test_driver/route_test.dart b/dev/integration_tests/ui/test_driver/route_test.dart index fa06384398e41..f7ec81e37f27d 100644 --- a/dev/integration_tests/ui/test_driver/route_test.dart +++ b/dev/integration_tests/ui/test_driver/route_test.dart @@ -1,3 +1,7 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; diff --git a/packages/flutter_driver/lib/src/driver.dart b/packages/flutter_driver/lib/src/driver.dart index c3ed994873d17..9b2eec4c86466 100644 --- a/packages/flutter_driver/lib/src/driver.dart +++ b/packages/flutter_driver/lib/src/driver.dart @@ -348,6 +348,12 @@ class FlutterDriver { return null; } + /// Waits until [finder] can no longer locate the target. + Future waitForAbsent(SerializableFinder finder, {Duration timeout}) async { + await _sendCommand(new WaitForAbsent(finder, timeout: timeout)); + return null; + } + /// Waits until there are no more transient callbacks in the queue. /// /// Use this method when you need to wait for the moment when the application @@ -597,9 +603,12 @@ class CommonFinders { /// Finds [Text] widgets containing string equal to [text]. SerializableFinder text(String text) => new ByText(text); - /// Finds widgets by [key]. + /// Finds widgets by [key]. Only [String] and [int] values can be used. SerializableFinder byValueKey(dynamic key) => new ByValueKey(key); /// Finds widgets with a tooltip with the given [message]. SerializableFinder byTooltip(String message) => new ByTooltipMessage(message); + + /// Finds widgets whose class name matches the given string. + SerializableFinder byType(String type) => new ByType(type); } diff --git a/packages/flutter_driver/lib/src/extension.dart b/packages/flutter_driver/lib/src/extension.dart index 6a617cc57b4df..992c698ab9700 100644 --- a/packages/flutter_driver/lib/src/extension.dart +++ b/packages/flutter_driver/lib/src/extension.dart @@ -86,6 +86,7 @@ class FlutterDriverExtension { 'set_semantics': _setSemantics, 'tap': _tap, 'waitFor': _waitFor, + 'waitForAbsent': _waitForAbsent, 'waitUntilNoTransientCallbacks': _waitUntilNoTransientCallbacks, }); @@ -100,6 +101,7 @@ class FlutterDriverExtension { 'set_semantics': (Map params) => new SetSemantics.deserialize(params), 'tap': (Map params) => new Tap.deserialize(params), 'waitFor': (Map params) => new WaitFor.deserialize(params), + 'waitForAbsent': (Map params) => new WaitForAbsent.deserialize(params), 'waitUntilNoTransientCallbacks': (Map params) => new WaitUntilNoTransientCallbacks.deserialize(params), }); @@ -107,6 +109,7 @@ class FlutterDriverExtension { 'ByText': _createByTextFinder, 'ByTooltipMessage': _createByTooltipMessageFinder, 'ByValueKey': _createByValueKeyFinder, + 'ByType': _createByTypeFinder, }); } @@ -195,6 +198,19 @@ class FlutterDriverExtension { return finder; } + /// Runs `finder` repeatedly until it finds zero [Element]s. + Future _waitForAbsentElement(Finder finder) async { + if (_frameSync) + await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); + + await _waitUntilFrame(() => !finder.precache()); + + if (_frameSync) + await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); + + return finder; + } + Finder _createByTextFinder(ByText arguments) { return find.text(arguments.text); } @@ -219,6 +235,12 @@ class FlutterDriverExtension { } } + Finder _createByTypeFinder(ByType arguments) { + return find.byElementPredicate((Element element) { + return element.widget.runtimeType.toString() == arguments.type; + }, description: 'widget with runtimeType "${arguments.type}"'); + } + Finder _createFinder(SerializableFinder finder) { final FinderConstructor constructor = _finders[finder.finderType]; @@ -242,6 +264,12 @@ class FlutterDriverExtension { return null; } + Future _waitForAbsent(Command command) async { + final WaitForAbsent waitForAbsentCommand = command; + await _waitForAbsentElement(_createFinder(waitForAbsentCommand.finder)); + return new WaitForAbsentResult(); + } + Future _waitUntilNoTransientCallbacks(Command command) async { if (SchedulerBinding.instance.transientCallbackCount != 0) await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); diff --git a/packages/flutter_driver/lib/src/find.dart b/packages/flutter_driver/lib/src/find.dart index a45fed0a5b4cd..6fd1ceda52e16 100644 --- a/packages/flutter_driver/lib/src/find.dart +++ b/packages/flutter_driver/lib/src/find.dart @@ -62,6 +62,22 @@ class WaitFor extends CommandWithTarget { WaitFor.deserialize(Map json) : super.deserialize(json); } +/// Waits until [finder] can no longer locate the target. +class WaitForAbsent extends CommandWithTarget { + @override + final String kind = 'waitForAbsent'; + + /// Creates a command that waits for the widget identified by [finder] to + /// disappear within the [timeout] amount of time. + /// + /// If [timeout] is not specified the command times out after 5 seconds. + WaitForAbsent(SerializableFinder finder, {Duration timeout}) + : super(finder, timeout: timeout); + + /// Deserializes the command from JSON 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 @@ -85,6 +101,17 @@ class WaitForResult extends Result { Map toJson() => {}; } +/// The result of a [WaitForAbsent] command. +class WaitForAbsentResult extends Result { + /// Deserializes the result from JSON. + static WaitForAbsentResult fromJson(Map json) { + return new WaitForAbsentResult(); + } + + @override + Map toJson() => {}; +} + /// Describes how to the driver should search for elements. abstract class SerializableFinder { /// Identifies the type of finder to be used by the driver extension. @@ -94,6 +121,7 @@ abstract class SerializableFinder { static SerializableFinder deserialize(Map json) { final String finderType = json['finderType']; switch(finderType) { + case 'ByType': return ByType.deserialize(json); case 'ByValueKey': return ByValueKey.deserialize(json); case 'ByTooltipMessage': return ByTooltipMessage.deserialize(json); case 'ByText': return ByText.deserialize(json); @@ -200,6 +228,28 @@ class ByValueKey extends SerializableFinder { } } +/// 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 + Map serialize() => super.serialize()..addAll({ + 'type': type, + }); + + /// Deserializes the finder from JSON generated by [serialize]. + static ByType deserialize(Map json) { + return new ByType(json['type']); + } +} + /// Command to read the text from a given element. class GetText extends CommandWithTarget { @override diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index 91f65f7a48059..e9ad4176ff4f4 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -60,6 +60,18 @@ class DriveCommand extends RunCommandBase { valueHelp: 'url' ); + + argParser.addOption( + 'driver', + help: + 'The test file to run on the host (as opposed to the target file to run on\n' + 'the device). By default, this file has the same base name as the target\n' + 'file, but in the "test_driver/" directory instead, and with "_test" inserted\n' + 'just before the extension, so e.g. if the target is "lib/main.dart", the\n' + 'driver will be "test_driver/main_test.dart".', + valueHelp: + 'path' + ); } @override @@ -139,6 +151,11 @@ class DriveCommand extends RunCommandBase { } String _getTestFile() { + if (argResults['driver'] != null) + return argResults['driver']; + + // If the --driver argument wasn't provided, then derive the value from + // the target file. String appFile = fs.path.normalize(targetFile); // This command extends `flutter run` and therefore CWD == package dir diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 91de539be1739..f03822d9f95a1 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -171,6 +171,11 @@ class FlutterDevice { await view.uiIsolate.flutterToggleDebugPaintSizeEnabled(); } + Future debugTogglePerformanceOverlayOverride() async { + for (FlutterView view in views) + await view.uiIsolate.flutterTogglePerformanceOverlayOverride(); + } + Future togglePlatform({ String from }) async { String to; switch (from) { @@ -451,6 +456,12 @@ abstract class ResidentRunner { await device.toggleDebugPaintSizeEnabled(); } + Future _debugTogglePerformanceOverlayOverride() async { + await refreshViews(); + for (FlutterDevice device in flutterDevices) + await device.debugTogglePerformanceOverlayOverride(); + } + Future _screenshot(FlutterDevice device) async { final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...'); final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png'); @@ -606,11 +617,16 @@ abstract class ResidentRunner { await _debugDumpSemanticsTree(); return true; } - } else if (lower == 'p') { + } else if (character == 'p') { if (supportsServiceProtocol && isRunningDebug) { await _debugToggleDebugPaintSizeEnabled(); return true; } + } else if (character == 'P') { + if (supportsServiceProtocol) { + await _debugTogglePerformanceOverlayOverride(); + return true; + } } else if (character == 's') { for (FlutterDevice device in flutterDevices) { if (device.device.supportsScreenshot) @@ -726,11 +742,14 @@ abstract class ResidentRunner { if (supportsServiceProtocol) { printStatus('You can dump the widget hierarchy of the app (debugDumpApp) by pressing "w".'); printStatus('To dump the rendering tree of the app (debugDumpRenderTree), press "t".'); - printStatus('For layers (debugDumpLayerTree), use "L"; accessibility (debugDumpSemantics), "S".'); if (isRunningDebug) { + printStatus('For layers (debugDumpLayerTree), use "L"; accessibility (debugDumpSemantics), "S".'); printStatus('To toggle the display of construction lines (debugPaintSizeEnabled), press "p".'); printStatus('To simulate different operating systems, (defaultTargetPlatform), press "o".'); + } else { + printStatus('To dump the accessibility tree (debugDumpSemantics), press "S".'); } + printStatus('To display the performance overlay (WidgetsApp.showPerformanceOverlay), press "P".'); } if (flutterDevices.any((FlutterDevice d) => d.device.supportsScreenshot)) printStatus('To save a screenshot to flutter.png, press "s".'); diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index ddbdfeba10906..56d2bd92f012e 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -79,7 +79,10 @@ abstract class FlutterCommand extends Command { argParser.addOption('target', abbr: 't', defaultsTo: flx.defaultMainPath, - help: 'Target app path / main entry-point file.'); + help: 'The main entry-point file of the application, as run on the device.\n' + 'If the --target option is omitted, but a file name is provided on\n' + 'the command line, then that is used instead.', + valueHelp: 'path'); _usesTargetOption = true; } diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index 2b5f41bb6e1aa..8aea4b3f2d2e3 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -1069,11 +1069,11 @@ class Isolate extends ServiceObjectOwner { return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTree', timeout: kLongRequestTimeout); } - Future> flutterToggleDebugPaintSizeEnabled() async { - Map state = await invokeFlutterExtensionRpcRaw('ext.flutter.debugPaint'); + Future> _flutterToggle(String name) async { + Map state = await invokeFlutterExtensionRpcRaw('ext.flutter.$name'); if (state != null && state.containsKey('enabled') && state['enabled'] is String) { state = await invokeFlutterExtensionRpcRaw( - 'ext.flutter.debugPaint', + 'ext.flutter.$name', params: { 'enabled': state['enabled'] == 'true' ? 'false' : 'true' }, timeout: const Duration(milliseconds: 150), timeoutFatal: false, @@ -1082,6 +1082,10 @@ class Isolate extends ServiceObjectOwner { return state; } + Future> flutterToggleDebugPaintSizeEnabled() => _flutterToggle('debugPaint'); + + Future> flutterTogglePerformanceOverlayOverride() => _flutterToggle('showPerformanceOverlay'); + Future flutterDebugAllowBanner(bool show) async { await invokeFlutterExtensionRpcRaw( 'ext.flutter.debugAllowBanner', From 63b686709d690bbb054a5837f8a2f83af2f0c35f Mon Sep 17 00:00:00 2001 From: Ryan Macnak Date: Wed, 19 Jul 2017 14:18:30 -0700 Subject: [PATCH 24/39] Roll engine to "Speculatively disable GN argument 'enable_profiling' to test its effect on benchmarks." (#11303) --- 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 d3da9032c2483..cdbbb1cbe1c9f 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -488584f8b7cf188d4699880d7b55144cd48067bf +316fa7e22392f485087fad06ecdce0c67b090969 From 02b65bc9843de43f3d9e5a61ff0db86df17e7b3f Mon Sep 17 00:00:00 2001 From: Yegor Date: Wed, 19 Jul 2017 15:45:31 -0700 Subject: [PATCH 25/39] AnimatedCrossFade: shut off animations & semantics in faded out widgets (#11276) * AnimatedCrossFade: shut off animations & semantics in faded out widgets * address comments --- .../lib/src/widgets/animated_cross_fade.dart | 111 ++++++++++++------ .../widgets/animated_cross_fade_test.dart | 78 ++++++++++++ 2 files changed, 151 insertions(+), 38 deletions(-) diff --git a/packages/flutter/lib/src/widgets/animated_cross_fade.dart b/packages/flutter/lib/src/widgets/animated_cross_fade.dart index b64447099f179..80297665d54f0 100644 --- a/packages/flutter/lib/src/widgets/animated_cross_fade.dart +++ b/packages/flutter/lib/src/widgets/animated_cross_fade.dart @@ -151,15 +151,26 @@ class _AnimatedCrossFadeState extends State with TickerProvid } Animation _initAnimation(Curve curve, bool inverted) { - final CurvedAnimation animation = new CurvedAnimation( + Animation animation = new CurvedAnimation( parent: _controller, curve: curve ); - return inverted ? new Tween( - begin: 1.0, - end: 0.0 - ).animate(animation) : animation; + if (inverted) { + animation = new Tween( + begin: 1.0, + end: 0.0 + ).animate(animation); + } + + animation.addStatusListener((AnimationStatus status) { + setState(() { + // Trigger a rebuild because it depends on _isTransitioning, which + // changes its value together with animation status. + }); + }); + + return animation; } @override @@ -189,49 +200,73 @@ class _AnimatedCrossFadeState extends State with TickerProvid } } - @override - Widget build(BuildContext context) { - List children; + /// Whether we're in the middle of cross-fading this frame. + bool get _isTransitioning => _controller.status == AnimationStatus.forward || _controller.status == AnimationStatus.reverse; - if (_controller.status == AnimationStatus.completed || - _controller.status == AnimationStatus.forward) { - children = [ - new FadeTransition( - opacity: _secondAnimation, - child: widget.secondChild, - ), - new Positioned( + List _buildCrossFadedChildren() { + 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; + + Key topKey; + Widget topChild; + Animation topAnimation; + Key bottomKey; + Widget bottomChild; + Animation bottomAnimation; + if (transitioningForwards) { + topKey = kSecondChildKey; + topChild = widget.secondChild; + topAnimation = _secondAnimation; + bottomKey = kFirstChildKey; + bottomChild = widget.firstChild; + bottomAnimation = _firstAnimation; + } else { + topKey = kFirstChildKey; + topChild = widget.firstChild; + topAnimation = _firstAnimation; + bottomKey = kSecondChildKey; + bottomChild = widget.secondChild; + 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 FadeTransition( - opacity: _firstAnimation, - child: widget.firstChild, + child: new ExcludeSemantics( + excluding: true, // always exclude the semantics of the widget that's fading out + child: new FadeTransition( + opacity: bottomAnimation, + child: bottomChild, + ), ), ), - ]; - } else { - children = [ - new FadeTransition( - opacity: _firstAnimation, - child: widget.firstChild, - ), - 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 FadeTransition( - opacity: _secondAnimation, - child: widget.secondChild, + ), + 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, + ), ), ), - ]; - } + ), + ]; + } + @override + Widget build(BuildContext context) { return new ClipRect( child: new AnimatedSize( key: new ValueKey(widget.key), @@ -241,7 +276,7 @@ class _AnimatedCrossFadeState extends State with TickerProvid vsync: this, child: new Stack( overflow: Overflow.visible, - children: children, + children: _buildCrossFadedChildren(), ), ), ); diff --git a/packages/flutter/test/widgets/animated_cross_fade_test.dart b/packages/flutter/test/widgets/animated_cross_fade_test.dart index d0a9dfc434b62..18c2a2afa8c89 100644 --- a/packages/flutter/test/widgets/animated_cross_fade_test.dart +++ b/packages/flutter/test/widgets/animated_cross_fade_test.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/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -131,4 +132,81 @@ void main() { expect(box2.localToGlobal(Offset.zero), const Offset(275.0, 175.0)); }); + Widget crossFadeWithWatcher({bool towardsSecond: false}) { + return new AnimatedCrossFade( + firstChild: const _TickerWatchingWidget(), + secondChild: new Container(), + crossFadeState: towardsSecond ? CrossFadeState.showSecond : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 50), + ); + } + + testWidgets('AnimatedCrossFade preserves widget state', (WidgetTester tester) async { + await tester.pumpWidget(crossFadeWithWatcher()); + + _TickerWatchingWidgetState findState() => tester.state(find.byType(_TickerWatchingWidget)); + final _TickerWatchingWidgetState state = findState(); + + await tester.pumpWidget(crossFadeWithWatcher(towardsSecond: true)); + for (int i = 0; i < 3; i += 1) { + await tester.pump(const Duration(milliseconds: 25)); + expect(findState(), same(state)); + } + }); + + testWidgets('AnimatedCrossFade switches off TickerMode and semantics on faded out widget', (WidgetTester tester) async { + ExcludeSemantics findSemantics() { + return tester.widget(find.descendant( + of: find.byKey(const ValueKey(CrossFadeState.showFirst)), + matching: find.byType(ExcludeSemantics), + )); + } + + await tester.pumpWidget(crossFadeWithWatcher()); + + final _TickerWatchingWidgetState state = tester.state(find.byType(_TickerWatchingWidget)); + expect(state.ticker.muted, false); + expect(findSemantics().excluding, false); + + await tester.pumpWidget(crossFadeWithWatcher(towardsSecond: true)); + for (int i = 0; i < 2; i += 1) { + await tester.pump(const Duration(milliseconds: 25)); + // Animations are kept alive in the middle of cross-fade + expect(state.ticker.muted, false); + // Semantics are turned off immediately on the widget that's fading out + expect(findSemantics().excluding, true); + } + + // In the final state both animations and semantics should be off on the + // widget that's faded out. + await tester.pump(const Duration(milliseconds: 25)); + expect(state.ticker.muted, true); + expect(findSemantics().excluding, true); + }); +} + +class _TickerWatchingWidget extends StatefulWidget { + const _TickerWatchingWidget(); + + @override + State createState() => new _TickerWatchingWidgetState(); +} + +class _TickerWatchingWidgetState extends State<_TickerWatchingWidget> with SingleTickerProviderStateMixin { + Ticker ticker; + + @override + void initState() { + super.initState(); + ticker = createTicker((_) {})..start(); + } + + @override + Widget build(BuildContext context) => new Container(); + + @override + void dispose() { + ticker.dispose(); + super.dispose(); + } } From 669e13ebd471b18feaf4f00c3bdbfc43143e6619 Mon Sep 17 00:00:00 2001 From: Yegor Date: Wed, 19 Jul 2017 15:53:28 -0700 Subject: [PATCH 26/39] AnimatedSize: state machine, tests, animate only when needed (#11305) --- .../lib/src/rendering/animated_size.dart | 156 +++++-- .../test/widgets/animated_size_test.dart | 381 +++++++++++------- 2 files changed, 354 insertions(+), 183 deletions(-) diff --git a/packages/flutter/lib/src/rendering/animated_size.dart b/packages/flutter/lib/src/rendering/animated_size.dart index 788404dddca07..28f34a2231cb4 100644 --- a/packages/flutter/lib/src/rendering/animated_size.dart +++ b/packages/flutter/lib/src/rendering/animated_size.dart @@ -10,6 +10,41 @@ import 'box.dart'; import 'object.dart'; import 'shifted_box.dart'; +/// A [RenderAnimatedSize] can be in exactly one of these states. +@visibleForTesting +enum RenderAnimatedSizeState { + /// The initial state, when we do not yet know what the starting and target + /// sizes are to animate. + /// + /// Next possible state is [stable]. + start, + + /// At this state the child's size is assumed to be stable and we are either + /// animating, or waiting for the child's size to change. + /// + /// Next possible state is [changed]. + stable, + + /// At this state we know that the child has changed once after being assumed + /// [stable]. + /// + /// Next possible states are: + /// + /// - [stable] - if the child's size stabilized immediately, this is a signal + /// for us to begin animating the size towards the child's new size. + /// - [unstable] - if the child's size continues to change, we assume it is + /// not stable and enter the [unstable] state. + changed, + + /// At this state the child's size is assumed to be unstable. + /// + /// Instead of chasing the child's size in this state we tightly track the + /// child's size until it stabilizes. + /// + /// Next possible state is [stable]. + unstable, +} + /// A render object that animates its size to its child's size over a given /// [duration] and with a given [curve]. If the child's size itself animates /// (i.e. if it changes size two frames in a row, as opposed to abruptly @@ -60,10 +95,16 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { AnimationController _controller; CurvedAnimation _animation; final SizeTween _sizeTween = new SizeTween(); - bool _didChangeTargetSizeLastFrame = false; bool _hasVisualOverflow; double _lastValue; + /// The state this size animation is in. + /// + /// See [RenderAnimatedSizeState] for possible states. + @visibleForTesting + RenderAnimatedSizeState get state => _state; + RenderAnimatedSizeState _state = RenderAnimatedSizeState.start; + /// The duration of the animation. Duration get duration => _controller.duration; set duration(Duration value) { @@ -82,6 +123,12 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { _animation.curve = value; } + /// Whether the size is being currently animated towards the child's size. + /// + /// See [RenderAnimatedSizeState] for situations when we may not be animating + /// the size. + bool get isAnimating => _controller.isAnimating; + /// The [TickerProvider] for the [AnimationController] that runs the animation. TickerProvider get vsync => _vsync; TickerProvider _vsync; @@ -93,16 +140,10 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { _controller.resync(vsync); } - @override - void attach(PipelineOwner owner) { - super.attach(owner); - if (_animatedSize != _sizeTween.end && !_controller.isAnimating) - _controller.forward(); - } - @override void detach() { _controller.stop(); + _state = RenderAnimatedSizeState.start; super.detach(); } @@ -121,29 +162,25 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { } child.layout(constraints, parentUsesSize: true); - if (_sizeTween.end != child.size) { - _sizeTween.begin = _animatedSize ?? child.size; - _sizeTween.end = child.size; - - if (_didChangeTargetSizeLastFrame) { - size = child.size; - _controller.stop(); - } else { - // Don't register first change as a last-frame change. - if (_sizeTween.end != _sizeTween.begin) - _didChangeTargetSizeLastFrame = true; - - _lastValue = 0.0; - _controller.forward(from: 0.0); - - size = constraints.constrain(_animatedSize); - } - } else { - _didChangeTargetSizeLastFrame = false; - size = constraints.constrain(_animatedSize); + switch(_state) { + case RenderAnimatedSizeState.start: + _layoutStart(); + break; + case RenderAnimatedSizeState.stable: + _layoutStable(); + break; + case RenderAnimatedSizeState.changed: + _layoutChanged(); + break; + case RenderAnimatedSizeState.unstable: + _layoutUnstable(); + break; + default: + throw new StateError('$runtimeType is in an invalid state $_state'); } + size = constraints.constrain(_animatedSize); alignChild(); if (size.width < _sizeTween.end.width || @@ -151,6 +188,69 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { _hasVisualOverflow = true; } + void _restartAnimation() { + _lastValue = 0.0; + _controller.forward(from: 0.0); + } + + /// Laying out the child for the first time. + /// + /// 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; + _state = RenderAnimatedSizeState.stable; + } + + /// At this state we're assuming the child size is stable and letting the + /// animation run its course. + /// + /// If during animation the size of the child changes we restart the + /// animation. + void _layoutStable() { + if (_sizeTween.end != child.size) { + _sizeTween.end = child.size; + _restartAnimation(); + _state = RenderAnimatedSizeState.changed; + } else if (_controller.value == _controller.upperBound) { + // Animation finished. Reset target sizes. + _sizeTween.begin = _sizeTween.end = child.size; + } + } + + /// This state indicates that the size of the child changed once after being + /// considered stable. + /// + /// If the child stabilizes immediately, we go back to stable state. If it + /// changes again, we match the child's size, restart animation and go to + /// unstable state. + 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; + _restartAnimation(); + _state = RenderAnimatedSizeState.unstable; + } else { + // Child size stabilized. + _state = RenderAnimatedSizeState.stable; + } + } + + /// The child's size is not stable. + /// + /// Continue tracking the child's size until is stabilizes. + void _layoutUnstable() { + if (_sizeTween.end != child.size) { + // Still unstable. Continue tracking the child. + _sizeTween.begin = _sizeTween.end = child.size; + _restartAnimation(); + } else { + // Child size stabilized. + _controller.stop(); + _state = RenderAnimatedSizeState.stable; + } + } + @override void paint(PaintingContext context, Offset offset) { if (child != null && _hasVisualOverflow) { diff --git a/packages/flutter/test/widgets/animated_size_test.dart b/packages/flutter/test/widgets/animated_size_test.dart index 2c0b8609f67fa..fd669a2a3cdc3 100644 --- a/packages/flutter/test/widgets/animated_size_test.dart +++ b/packages/flutter/test/widgets/animated_size_test.dart @@ -16,197 +16,268 @@ class TestPaintingContext implements PaintingContext { } void main() { - testWidgets('AnimatedSize test', (WidgetTester tester) async { - await tester.pumpWidget( - new Center( - child: new AnimatedSize( - duration: const Duration(milliseconds: 200), - vsync: tester, - child: const SizedBox( - width: 100.0, - height: 100.0, + group('AnimatedSize', () { + testWidgets('animates forwards then backwards with stable-sized children', (WidgetTester tester) async { + await tester.pumpWidget( + new Center( + child: new AnimatedSize( + duration: const Duration(milliseconds: 200), + vsync: tester, + child: const SizedBox( + width: 100.0, + height: 100.0, + ), ), ), - ), - ); - - RenderBox box = tester.renderObject(find.byType(AnimatedSize)); - expect(box.size.width, equals(100.0)); - expect(box.size.height, equals(100.0)); - - await tester.pumpWidget( - new Center( - child: new AnimatedSize( - duration: const Duration(milliseconds: 200), - vsync: tester, - child: const SizedBox( - width: 200.0, - height: 200.0, + ); + + RenderBox box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, equals(100.0)); + expect(box.size.height, equals(100.0)); + + await tester.pumpWidget( + new Center( + child: new AnimatedSize( + duration: const Duration(milliseconds: 200), + vsync: tester, + child: const SizedBox( + width: 200.0, + height: 200.0, + ), ), ), - ), - ); - - await tester.pump(const Duration(milliseconds: 100)); - box = tester.renderObject(find.byType(AnimatedSize)); - expect(box.size.width, equals(150.0)); - expect(box.size.height, equals(150.0)); - - TestPaintingContext context = new TestPaintingContext(); - box.paint(context, Offset.zero); - expect(context.invocations.first.memberName, equals(#pushClipRect)); - - await tester.pump(const Duration(milliseconds: 100)); - box = tester.renderObject(find.byType(AnimatedSize)); - expect(box.size.width, equals(200.0)); - expect(box.size.height, equals(200.0)); - - await tester.pumpWidget( - new Center( - child: new AnimatedSize( - duration: const Duration(milliseconds: 200), - vsync: tester, - child: const SizedBox( + ); + + await tester.pump(const Duration(milliseconds: 100)); + box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, equals(150.0)); + expect(box.size.height, equals(150.0)); + + TestPaintingContext context = new TestPaintingContext(); + box.paint(context, Offset.zero); + expect(context.invocations.first.memberName, equals(#pushClipRect)); + + await tester.pump(const Duration(milliseconds: 100)); + box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, equals(200.0)); + expect(box.size.height, equals(200.0)); + + await tester.pumpWidget( + new Center( + child: new AnimatedSize( + duration: const Duration(milliseconds: 200), + vsync: tester, + child: const SizedBox( + width: 100.0, + height: 100.0, + ), + ), + ), + ); + + await tester.pump(const Duration(milliseconds: 100)); + box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, equals(150.0)); + expect(box.size.height, equals(150.0)); + + context = new TestPaintingContext(); + box.paint(context, Offset.zero); + expect(context.invocations.first.memberName, equals(#paintChild)); + + await tester.pump(const Duration(milliseconds: 100)); + box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, equals(100.0)); + expect(box.size.height, equals(100.0)); + }); + + testWidgets('clamps animated size to constraints', (WidgetTester tester) async { + await tester.pumpWidget( + new Center( + child: new SizedBox ( width: 100.0, height: 100.0, + child: new AnimatedSize( + duration: const Duration(milliseconds: 200), + vsync: tester, + child: const SizedBox( + width: 100.0, + height: 100.0, + ), + ), ), ), - ), - ); - - await tester.pump(const Duration(milliseconds: 100)); - box = tester.renderObject(find.byType(AnimatedSize)); - expect(box.size.width, equals(150.0)); - expect(box.size.height, equals(150.0)); - - context = new TestPaintingContext(); - box.paint(context, Offset.zero); - expect(context.invocations.first.memberName, equals(#paintChild)); - - await tester.pump(const Duration(milliseconds: 100)); - box = tester.renderObject(find.byType(AnimatedSize)); - expect(box.size.width, equals(100.0)); - expect(box.size.height, equals(100.0)); - }); + ); + + RenderBox box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, equals(100.0)); + expect(box.size.height, equals(100.0)); + + // Attempt to animate beyond the outer SizedBox. + await tester.pumpWidget( + new Center( + child: new SizedBox ( + width: 100.0, + height: 100.0, + child: new AnimatedSize( + duration: const Duration(milliseconds: 200), + vsync: tester, + child: const SizedBox( + width: 200.0, + height: 200.0, + ), + ), + ), + ), + ); + + // Verify that animated size is the same as the outer SizedBox. + await tester.pump(const Duration(milliseconds: 100)); + box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, equals(100.0)); + expect(box.size.height, equals(100.0)); + }); - testWidgets('AnimatedSize constrained test', (WidgetTester tester) async { - await tester.pumpWidget( - new Center( - child: new SizedBox ( - width: 100.0, - height: 100.0, + testWidgets('tracks unstable child, then resumes animation when child stabilizes', (WidgetTester tester) async { + Future pumpMillis(int millis) async { + await tester.pump(new Duration(milliseconds: millis)); + } + + void verify({double size, RenderAnimatedSizeState state}) { + assert(size != null || state != null); + final RenderAnimatedSize box = tester.renderObject(find.byType(AnimatedSize)); + if (size != null) { + expect(box.size.width, size); + expect(box.size.height, size); + } + if (state != null) { + expect(box.state, state); + } + } + + await tester.pumpWidget( + new Center( child: new AnimatedSize( duration: const Duration(milliseconds: 200), vsync: tester, - child: const SizedBox( + child: new AnimatedContainer( + duration: const Duration(milliseconds: 100), width: 100.0, height: 100.0, ), ), ), - ), - ); - - RenderBox box = tester.renderObject(find.byType(AnimatedSize)); - expect(box.size.width, equals(100.0)); - expect(box.size.height, equals(100.0)); - - await tester.pumpWidget( - new Center( - child: new SizedBox ( - width: 100.0, - height: 100.0, + ); + + verify(size: 100.0, state: RenderAnimatedSizeState.stable); + + // Animate child size from 100 to 200 slowly (100ms). + await tester.pumpWidget( + new Center( child: new AnimatedSize( duration: const Duration(milliseconds: 200), vsync: tester, - child: const SizedBox( + child: new AnimatedContainer( + duration: const Duration(milliseconds: 100), width: 200.0, height: 200.0, ), ), ), - ), - ); + ); - await tester.pump(const Duration(milliseconds: 100)); - box = tester.renderObject(find.byType(AnimatedSize)); - expect(box.size.width, equals(100.0)); - expect(box.size.height, equals(100.0)); - }); + // Make sure animation proceeds at child's pace, with AnimatedSize + // tightly tracking the child's size. + verify(state: RenderAnimatedSizeState.stable); + await pumpMillis(1); // register change + verify(state: RenderAnimatedSizeState.changed); + await pumpMillis(49); + verify(size: 150.0, state: RenderAnimatedSizeState.unstable); + await pumpMillis(50); + verify(size: 200.0, state: RenderAnimatedSizeState.unstable); - testWidgets('AnimatedSize with AnimatedContainer', (WidgetTester tester) async { - await tester.pumpWidget( - new Center( - child: new AnimatedSize( - duration: const Duration(milliseconds: 200), - vsync: tester, - child: new AnimatedContainer( - duration: const Duration(milliseconds: 100), - width: 100.0, - height: 100.0, + // Stabilize size + await pumpMillis(50); + verify(size: 200.0, state: RenderAnimatedSizeState.stable); + + // Quickly (in 1ms) change size back to 100 + await tester.pumpWidget( + new Center( + child: new AnimatedSize( + duration: const Duration(milliseconds: 200), + vsync: tester, + child: new AnimatedContainer( + duration: const Duration(milliseconds: 1), + width: 100.0, + height: 100.0, + ), ), ), - ), - ); - - RenderBox box = tester.renderObject(find.byType(AnimatedSize)); - expect(box.size.width, equals(100.0)); - expect(box.size.height, equals(100.0)); - - await tester.pumpWidget( - new Center( - child: new AnimatedSize( - duration: const Duration(milliseconds: 200), - vsync: tester, - child: new AnimatedContainer( - duration: const Duration(milliseconds: 100), - width: 200.0, - height: 200.0, + ); + + verify(size: 200.0, state: RenderAnimatedSizeState.stable); + await pumpMillis(1); // register change + verify(state: RenderAnimatedSizeState.changed); + await pumpMillis(100); + verify(size: 150.0, state: RenderAnimatedSizeState.stable); + await pumpMillis(100); + verify(size: 100.0, state: RenderAnimatedSizeState.stable); + }); + + testWidgets('resyncs its animation controller', (WidgetTester tester) async { + await tester.pumpWidget( + const Center( + child: const AnimatedSize( + duration: const Duration(milliseconds: 200), + vsync: const TestVSync(), + child: const SizedBox( + width: 100.0, + height: 100.0, + ), ), ), - ), - ); - - await tester.pump(const Duration(milliseconds: 1)); // register change - await tester.pump(const Duration(milliseconds: 49)); - expect(box.size.width, equals(150.0)); - expect(box.size.height, equals(150.0)); - await tester.pump(const Duration(milliseconds: 50)); - box = tester.renderObject(find.byType(AnimatedSize)); - expect(box.size.width, equals(200.0)); - expect(box.size.height, equals(200.0)); - }); + ); - testWidgets('AnimatedSize resync', (WidgetTester tester) async { - await tester.pumpWidget( - const Center( - child: const AnimatedSize( - duration: const Duration(milliseconds: 200), - vsync: const TestVSync(), - child: const SizedBox( - width: 100.0, - height: 100.0, + await tester.pumpWidget( + new Center( + child: new AnimatedSize( + duration: const Duration(milliseconds: 200), + vsync: tester, + child: const SizedBox( + width: 200.0, + height: 100.0, + ), ), ), - ), - ); - - await tester.pumpWidget( - new Center( - child: new AnimatedSize( - duration: const Duration(milliseconds: 200), - vsync: tester, - child: const SizedBox( - width: 200.0, - height: 100.0, + ); + + await tester.pump(const Duration(milliseconds: 100)); + + final RenderBox box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, equals(150.0)); + }); + + testWidgets('does not run animation unnecessarily', (WidgetTester tester) async { + await tester.pumpWidget( + new Center( + child: new AnimatedSize( + duration: const Duration(milliseconds: 200), + vsync: tester, + child: const SizedBox( + width: 100.0, + height: 100.0, + ), ), ), - ), - ); - - await tester.pump(const Duration(milliseconds: 100)); + ); - final RenderBox box = tester.renderObject(find.byType(AnimatedSize)); - expect(box.size.width, equals(150.0)); + for (int i = 0; i < 20; i++) { + final RenderAnimatedSize box = tester.renderObject(find.byType(AnimatedSize)); + expect(box.size.width, 100.0); + expect(box.size.height, 100.0); + expect(box.state, RenderAnimatedSizeState.stable); + expect(box.isAnimating, false); + await tester.pump(const Duration(milliseconds: 10)); + } + }); }); } From d767ac0be548b4227467196b4a9d96b7e31892eb Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Wed, 19 Jul 2017 16:33:55 -0700 Subject: [PATCH 27/39] Fixed a dartdoc sample code typo (#11306) --- packages/flutter/lib/src/widgets/animated_cross_fade.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/widgets/animated_cross_fade.dart b/packages/flutter/lib/src/widgets/animated_cross_fade.dart index 80297665d54f0..307ea068dee09 100644 --- a/packages/flutter/lib/src/widgets/animated_cross_fade.dart +++ b/packages/flutter/lib/src/widgets/animated_cross_fade.dart @@ -46,7 +46,7 @@ enum CrossFadeState { /// ## Sample code /// /// This code fades between two representations of the Flutter logo. It depends -/// on a boolean field `_on`; when `_on` is true, the first logo is shown, +/// on a boolean field `_first`; when `_first` is true, the first logo is shown, /// otherwise the second logo is shown. When the field changes state, the /// [AnimatedCrossFade] widget cross-fades between the two forms of the logo /// over three seconds. From b5c461a9172fc09277c76daa8a5d480d95744f0a Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Wed, 19 Jul 2017 16:40:24 -0700 Subject: [PATCH 28/39] a11y: implement new SemanticsAction "showOnScreen" (v2) (#11156) * a11y: implement new SemanticsAction "showOnScreen" (v2) This action is triggered when the user swipes (in accessibility mode) to the last visible item of a scrollable list to bring that item fully on screen. This requires engine rolled to flutter/engine#3856. I am in the process of adding tests, but I'd like to get early feedback to see if this approach is OK. * fix null check * review comments * review comments * Add test * fix analyzer warning --- .../flutter/lib/src/rendering/object.dart | 20 +++++++++++-- .../flutter/lib/src/rendering/semantics.dart | 18 +++++++++-- .../flutter/lib/src/rendering/viewport.dart | 18 +++++++++++ .../lib/src/rendering/viewport_offset.dart | 9 ++++++ .../lib/src/widgets/scroll_position.dart | 1 + .../src/widgets/single_child_scroll_view.dart | 19 ++++++++++++ .../widgets/scrollable_semantics_test.dart | 30 +++++++++++++++++++ 7 files changed, 109 insertions(+), 6 deletions(-) diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 67d0d47a643f7..1b08db4845da1 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -739,7 +739,8 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment { assert(parentSemantics == null); renderObjectOwner._semantics ??= new SemanticsNode.root( handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null, - owner: renderObjectOwner.owner.semanticsOwner + owner: renderObjectOwner.owner.semanticsOwner, + showOnScreen: renderObjectOwner.showOnScreen, ); final SemanticsNode node = renderObjectOwner._semantics; assert(MatrixUtils.matrixEquals(node.transform, new Matrix4.identity())); @@ -768,7 +769,8 @@ class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment { @override SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) { renderObjectOwner._semantics ??= new SemanticsNode( - handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null + handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null, + showOnScreen: renderObjectOwner.showOnScreen, ); final SemanticsNode node = renderObjectOwner._semantics; if (geometry != null) { @@ -812,7 +814,8 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment { _haveConcreteNode = currentSemantics == null && annotator != null; if (haveConcreteNode) { renderObjectOwner._semantics ??= new SemanticsNode( - handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null + handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null, + showOnScreen: renderObjectOwner.showOnScreen, ); node = renderObjectOwner._semantics; } else { @@ -2777,6 +2780,17 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { @protected String debugDescribeChildren(String prefix) => ''; + + /// Attempt to make this or a descendant RenderObject visible on screen. + /// + /// If [child] is provided, that [RenderObject] is made visible. If [child] is + /// omitted, this [RenderObject] is made visible. + void showOnScreen([RenderObject child]) { + if (parent is RenderObject) { + final RenderObject renderParent = parent; + renderParent.showOnScreen(child ?? this); + } + } } /// Generic mixin for render objects with one child. diff --git a/packages/flutter/lib/src/rendering/semantics.dart b/packages/flutter/lib/src/rendering/semantics.dart index f2bdd7a9e682c..e0a19c215e1ec 100644 --- a/packages/flutter/lib/src/rendering/semantics.dart +++ b/packages/flutter/lib/src/rendering/semantics.dart @@ -143,8 +143,10 @@ class SemanticsNode extends AbstractNode { /// Each semantic node has a unique identifier that is assigned when the node /// is created. SemanticsNode({ - SemanticsActionHandler handler + SemanticsActionHandler handler, + VoidCallback showOnScreen, }) : id = _generateNewId(), + _showOnScreen = showOnScreen, _actionHandler = handler; /// Creates a semantic node to represent the root of the semantics tree. @@ -152,8 +154,10 @@ class SemanticsNode extends AbstractNode { /// The root node is assigned an identifier of zero. SemanticsNode.root({ SemanticsActionHandler handler, - SemanticsOwner owner + VoidCallback showOnScreen, + SemanticsOwner owner, }) : id = 0, + _showOnScreen = showOnScreen, _actionHandler = handler { attach(owner); } @@ -171,6 +175,7 @@ class SemanticsNode extends AbstractNode { final int id; final SemanticsActionHandler _actionHandler; + final VoidCallback _showOnScreen; // GEOMETRY // These are automatically handled by RenderObject's own logic @@ -734,7 +739,14 @@ class SemanticsOwner extends ChangeNotifier { void performAction(int id, SemanticsAction action) { assert(action != null); final SemanticsActionHandler handler = _getSemanticsActionHandlerForId(id, action); - handler?.performAction(action); + if (handler != null) { + handler.performAction(action); + return; + } + + // Default actions if no [handler] was provided. + if (action == SemanticsAction.showOnScreen && _nodes[id]._showOnScreen != null) + _nodes[id]._showOnScreen(); } SemanticsActionHandler _getSemanticsActionHandlerForPosition(SemanticsNode node, Offset position, SemanticsAction action) { diff --git a/packages/flutter/lib/src/rendering/viewport.dart b/packages/flutter/lib/src/rendering/viewport.dart index 89bc9565d40ef..c8243bae611f0 100644 --- a/packages/flutter/lib/src/rendering/viewport.dart +++ b/packages/flutter/lib/src/rendering/viewport.dart @@ -591,6 +591,24 @@ abstract class RenderViewportBase get childrenInHitTestOrder; + + @override + void showOnScreen([RenderObject child]) { + // Logic duplicated in [_RenderSingleChildViewport.showOnScreen]. + if (child != null) { + // Move viewport the smallest distance to bring [child] on screen. + final double leadingEdgeOffset = getOffsetToReveal(child, 0.0); + final double trailingEdgeOffset = getOffsetToReveal(child, 1.0); + final double currentOffset = offset.pixels; + if ((currentOffset - leadingEdgeOffset).abs() < (currentOffset - trailingEdgeOffset).abs()) { + offset.jumpTo(leadingEdgeOffset); + } else { + offset.jumpTo(trailingEdgeOffset); + } + } + // Make sure the viewport itself is on screen. + super.showOnScreen(); + } } /// A render object that is bigger on the inside. diff --git a/packages/flutter/lib/src/rendering/viewport_offset.dart b/packages/flutter/lib/src/rendering/viewport_offset.dart index 6b1721e2562ba..d5fc99cf84624 100644 --- a/packages/flutter/lib/src/rendering/viewport_offset.dart +++ b/packages/flutter/lib/src/rendering/viewport_offset.dart @@ -152,6 +152,10 @@ abstract class ViewportOffset extends ChangeNotifier { /// being called again, though this should be very rare. void correctBy(double correction); + /// Jumps the scroll position from its current value to the given value, + /// without animation, and without checking if the new value is in range. + void jumpTo(double pixels); + /// The direction in which the user is trying to change [pixels], relative to /// the viewport's [RenderViewport.axisDirection]. /// @@ -208,6 +212,11 @@ class _FixedViewportOffset extends ViewportOffset { _pixels += correction; } + @override + void jumpTo(double pixels) { + // Do nothing, viewport is fixed. + } + @override ScrollDirection get userScrollDirection => ScrollDirection.idle; } diff --git a/packages/flutter/lib/src/widgets/scroll_position.dart b/packages/flutter/lib/src/widgets/scroll_position.dart index 0519f91131620..103fb0a0e321e 100644 --- a/packages/flutter/lib/src/widgets/scroll_position.dart +++ b/packages/flutter/lib/src/widgets/scroll_position.dart @@ -510,6 +510,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { /// If this method changes the scroll position, a sequence of start/update/end /// scroll notifications will be dispatched. No overscroll notifications can /// be generated by this method. + @override void jumpTo(double value); /// Deprecated. Use [jumpTo] or a custom [ScrollPosition] instead. 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 e7af006eb435c..9b75768b50f03 100644 --- a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart @@ -418,4 +418,23 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix return leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment; } + + @override + void showOnScreen([RenderObject child]) { + // Logic duplicated in [RenderViewportBase.showOnScreen]. + if (child != null) { + // Move viewport the smallest distance to bring [child] on screen. + final double leadingEdgeOffset = getOffsetToReveal(child, 0.0); + final double trailingEdgeOffset = getOffsetToReveal(child, 1.0); + final double currentOffset = offset.pixels; + if ((currentOffset - leadingEdgeOffset).abs() < (currentOffset - trailingEdgeOffset).abs()) { + offset.jumpTo(leadingEdgeOffset); + } else { + offset.jumpTo(trailingEdgeOffset); + } + } + + // Make sure the viewport itself is on screen. + super.showOnScreen(); + } } diff --git a/packages/flutter/test/widgets/scrollable_semantics_test.dart b/packages/flutter/test/widgets/scrollable_semantics_test.dart index 1de20fe3a4e64..d59adc53f5697 100644 --- a/packages/flutter/test/widgets/scrollable_semantics_test.dart +++ b/packages/flutter/test/widgets/scrollable_semantics_test.dart @@ -30,7 +30,37 @@ void main() { await flingDown(tester); expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollUp, SemanticsAction.scrollDown])); + }); + + testWidgets('showOnScreen works in scrollable', (WidgetTester tester) async { + new SemanticsTester(tester); // enables semantics tree generation + + const double kItemHeight = 40.0; + + final List containers = []; + for (int i = 0; i < 80; i++) + containers.add(new MergeSemantics(child: new Container( + height: kItemHeight, + child: new Text('container $i'), + ))); + + final ScrollController scrollController = new ScrollController( + initialScrollOffset: kItemHeight / 2, + ); + + await tester.pumpWidget(new ListView( + controller: scrollController, + children: containers + )); + + expect(scrollController.offset, kItemHeight / 2); + + final int firstContainerId = tester.renderObject(find.byWidget(containers.first)).debugSemantics.id; + tester.binding.pipelineOwner.semanticsOwner.performAction(firstContainerId, SemanticsAction.showOnScreen); + await tester.pump(); + await tester.pump(const Duration(seconds: 5)); + expect(scrollController.offset, 0.0); }); } From 05ccad7de07801aec9ab66a5d6e749fce8d1d5ce Mon Sep 17 00:00:00 2001 From: Carlo Bernaschina Date: Wed, 19 Jul 2017 16:49:40 -0700 Subject: [PATCH 29/39] Roll engine to 53c9a702821b154f137541d4a528fbd25f00ad1b (#11310) Required for https://github.com/flutter/flutter/pull/11282 --- 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 cdbbb1cbe1c9f..5e797157b42c6 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -316fa7e22392f485087fad06ecdce0c67b090969 +53c9a702821b154f137541d4a528fbd25f00ad1b From 0b392665bfc514fbd203378e2ac26258e72de2f2 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Wed, 19 Jul 2017 16:51:16 -0700 Subject: [PATCH 30/39] More debug help. (#11308) --- packages/flutter/lib/src/rendering/debug.dart | 39 +++++++++++++++++-- packages/flutter/lib/src/rendering/layer.dart | 2 + .../flutter/lib/src/rendering/object.dart | 4 ++ packages/flutter/lib/src/scheduler/debug.dart | 11 +++++- 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/packages/flutter/lib/src/rendering/debug.dart b/packages/flutter/lib/src/rendering/debug.dart index a4ad85c03f2ec..d0302b7b936e8 100644 --- a/packages/flutter/lib/src/rendering/debug.dart +++ b/packages/flutter/lib/src/rendering/debug.dart @@ -95,9 +95,6 @@ HSVColor debugCurrentRepaintColor = _kDebugCurrentRepaintColor; /// The amount to increment the hue of the current repaint color. double debugRepaintRainbowHueIncrement = _kDebugRepaintRainbowHueIncrement; -/// Log the call stacks that mark render objects as needing paint. -bool debugPrintMarkNeedsPaintStacks = false; - /// Log the call stacks that mark render objects as needing layout. /// /// For sanity, this only logs the stack traces of cases where an object is @@ -106,6 +103,29 @@ bool debugPrintMarkNeedsPaintStacks = false; /// up the tree. bool debugPrintMarkNeedsLayoutStacks = false; +/// Log the call stacks that mark render objects as needing paint. +bool debugPrintMarkNeedsPaintStacks = false; + +/// Log the dirty render objects that are laid out each frame. +/// +/// Combined with [debugPrintBeginFrameBanner], this allows you to distinguish +/// layouts triggered by the initial mounting of a render tree (e.g. in a call +/// to [runApp]) from the regular layouts triggered by the pipeline. +/// +/// Combined with [debugPrintMarkNeedsLayoutStacks], this lets you watch a +/// render object's dirty/clean lifecycle. +/// +/// See also: +/// +/// * [debugProfilePaintsEnabled], which does something similar for +/// painting but using the timeline view. +/// +/// * [debugPrintRebuildDirtyWidgets], which does something similar for widgets +/// being rebuilt. +/// +/// * The discussion at [RendererBinding.drawFrame]. +bool debugPrintLayouts = false; + /// Check the intrinsic sizes of each [RenderBox] during layout. /// /// By default this is turned off since these checks are expensive, but it is @@ -121,6 +141,16 @@ bool debugCheckIntrinsicSizes = false; /// For details on how to use [dart:developer.Timeline] events in the Dart /// Observatory to optimize your app, see: /// +/// +/// See also: +/// +/// * [debugPrintLayouts], which does something similar for layout but using +/// console output. +/// +/// * [debugPrintRebuildDirtyWidgets], which does something similar for widgets +/// being rebuilt. +/// +/// * The discussion at [RendererBinding.drawFrame]. bool debugProfilePaintsEnabled = false; @@ -184,8 +214,9 @@ bool debugAssertAllRenderVarsUnset(String reason, { bool debugCheckIntrinsicSize debugPaintPointersEnabled || debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled || - debugPrintMarkNeedsPaintStacks || debugPrintMarkNeedsLayoutStacks || + debugPrintMarkNeedsPaintStacks || + debugPrintLayouts || debugCheckIntrinsicSizes != debugCheckIntrinsicSizesOverride || debugProfilePaintsEnabled || debugPaintSizeColor != _kDebugPaintSizeColor || diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart index 453fe509e1640..b47b2e86c9836 100644 --- a/packages/flutter/lib/src/rendering/layer.dart +++ b/packages/flutter/lib/src/rendering/layer.dart @@ -754,6 +754,8 @@ class PhysicalModelLayer extends ContainerLayer { void debugFillDescription(List description) { super.debugFillDescription(description); description.add('clipRRect: $clipRRect'); + description.add('elevation: $elevation'); + description.add('color: $color'); } } diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 1b08db4845da1..d720c2d956b92 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -1743,6 +1743,8 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { _debugDoingThisLayout = true; debugPreviousActiveLayout = _debugActiveLayout; _debugActiveLayout = this; + if (debugPrintLayouts) + debugPrint('Laying out (without resize) $this'); return true; }); try { @@ -1849,6 +1851,8 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { assert(!_doingThisLayoutWithCallback); assert(() { _debugMutationsLocked = true; + if (debugPrintLayouts) + debugPrint('Laying out (${sizedByParent ? "with separate resize" : "with resize allowed"}) $this'); return true; }); if (sizedByParent) { diff --git a/packages/flutter/lib/src/scheduler/debug.dart b/packages/flutter/lib/src/scheduler/debug.dart index 3e52836b863eb..4d9c127c1480d 100644 --- a/packages/flutter/lib/src/scheduler/debug.dart +++ b/packages/flutter/lib/src/scheduler/debug.dart @@ -21,7 +21,16 @@ import 'package:flutter/foundation.dart'; /// intra-frame output from inter-frame output, set [debugPrintEndFrameBanner] /// to true as well. /// -/// See [SchedulerBinding.handleBeginFrame]. +/// See also: +/// +/// * [debugProfilePaintsEnabled], which does something similar for +/// painting but using the timeline view. +/// +/// * [debugPrintLayouts], which does something similar for layout but using +/// console output. +/// +/// * The discussions at [WidgetsBinding.drawFrame] and at +/// [SchedulerBinding.handleBeginFrame]. bool debugPrintBeginFrameBanner = false; /// Print a banner at the end of each frame. From 741598d848570b4aea887fdba05d3bf0bcb5bbc0 Mon Sep 17 00:00:00 2001 From: Carlo Bernaschina Date: Wed, 19 Jul 2017 16:54:10 -0700 Subject: [PATCH 31/39] Fix restart/reload benchmark synchronization (#11282) Changes introduced in https://github.com/flutter/engine/commit/8ba522eeae35d8c70ada3c7b8e200ca2274f4f95 has removed from the `_flutter.listViews` the thread synchronization side effect used during benchmarks. The thread synchronization is restored via the new `_flutter.flushUIThreadTasks` RPC. Fixes https://github.com/flutter/flutter/issues/11241 Related https://github.com/flutter/engine/pull/3898 --- packages/flutter_tools/lib/src/run_hot.dart | 6 +++++- packages/flutter_tools/lib/src/vmservice.dart | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 51300a601f694..e908bf00af541 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -153,7 +153,6 @@ class HotRunner extends ResidentRunner { // Measure time to perform a hot restart. printStatus('Benchmarking hot restart'); await restart(fullRestart: true); - await refreshViews(); // TODO(johnmccutchan): Modify script entry point. printStatus('Benchmarking hot reload'); // Measure time to perform a hot reload. @@ -313,6 +312,11 @@ class HotRunner extends ResidentRunner { deviceEntryUri, devicePackagesUri, deviceAssetsDirectoryUri); + if (benchmarkMode) { + for (FlutterDevice device in flutterDevices) + for (FlutterView view in device.views) + await view.flushUIThreadTasks(); + } } } diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index 8aea4b3f2d2e3..9b255ced90102 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -1230,6 +1230,10 @@ class FlutterView extends ServiceObject { bool get hasIsolate => _uiIsolate != null; + Future flushUIThreadTasks() async { + await owner.vm.invokeRpcRaw('_flutter.flushUIThreadTasks'); + } + @override String toString() => id; } From 6dbf2269f013cefa304ca9e526f199dd3c1ca121 Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Wed, 19 Jul 2017 17:32:39 -0700 Subject: [PATCH 32/39] Create one listener that merges the leading and trailing glow controllers and use it in each paint (#11311) If a new listener is created for each paint, then the leading and trailing controllers will accumulate and invoke a list of all those listeners --- packages/flutter/lib/src/widgets/overscroll_indicator.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/widgets/overscroll_indicator.dart b/packages/flutter/lib/src/widgets/overscroll_indicator.dart index 83fee2b526d6c..266cf2123d938 100644 --- a/packages/flutter/lib/src/widgets/overscroll_indicator.dart +++ b/packages/flutter/lib/src/widgets/overscroll_indicator.dart @@ -117,12 +117,14 @@ class GlowingOverscrollIndicator extends StatefulWidget { class _GlowingOverscrollIndicatorState extends State with TickerProviderStateMixin { _GlowController _leadingController; _GlowController _trailingController; + Listenable _leadingAndTrailingListener; @override void initState() { super.initState(); _leadingController = new _GlowController(vsync: this, color: widget.color, axis: widget.axis); _trailingController = new _GlowController(vsync: this, color: widget.color, axis: widget.axis); + _leadingAndTrailingListener = new Listenable.merge([_leadingController, _trailingController]); } @override @@ -210,6 +212,7 @@ class _GlowingOverscrollIndicatorState extends State leadingController: widget.showLeading ? _leadingController : null, trailingController: widget.showTrailing ? _trailingController : null, axisDirection: widget.axisDirection, + repaint: _leadingAndTrailingListener, ), child: new RepaintBoundary( child: widget.child, @@ -444,8 +447,9 @@ class _GlowingOverscrollIndicatorPainter extends CustomPainter { this.leadingController, this.trailingController, this.axisDirection, + Listenable repaint, }) : super( - repaint: new Listenable.merge([leadingController, trailingController]) + repaint: repaint, ); /// The controller for the overscroll glow on the side with negative scroll offsets. From 194bf41ee80dcef862bbb8aec02164e71b8e0e54 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Wed, 19 Jul 2017 17:53:32 -0700 Subject: [PATCH 33/39] Don't relayout a Text if only its color changed. (#11313) --- .../flutter/lib/src/painting/basic_types.dart | 38 +++++++++++++++++++ .../flutter/lib/src/painting/text_span.dart | 33 ++++++++++++++++ .../flutter/lib/src/painting/text_style.dart | 27 +++++++++++++ .../flutter/lib/src/rendering/object.dart | 28 ++++++++++++++ .../flutter/lib/src/rendering/paragraph.dart | 19 +++++++--- .../test/rendering/paragraph_test.dart | 30 +++++++++++++++ 6 files changed, 170 insertions(+), 5 deletions(-) diff --git a/packages/flutter/lib/src/painting/basic_types.dart b/packages/flutter/lib/src/painting/basic_types.dart index 65247d731a21d..f4c026b57b057 100644 --- a/packages/flutter/lib/src/painting/basic_types.dart +++ b/packages/flutter/lib/src/painting/basic_types.dart @@ -52,3 +52,41 @@ export 'dart:ui' show // - window, WindowPadding // These are generally wrapped by other APIs so we always refer to them directly // as ui.* to avoid making them seem like high-level APIs. + +/// The description of the difference between two objects, in the context of how +/// it will affect the rendering. +/// +/// Used by [TextSpan.compareTo] and [TextStyle.compareTo]. +/// +/// The values in this enum are ordered such that they are in increasing order +/// of cost. A value with index N implies all the values with index less than N. +/// For example, [layout] (index 3) implies [paint] (2). +enum RenderComparison { + /// The two objects are identical (meaning deeply equal, not necessarily + /// [identical]). + identical, + + /// The two objects are identical for the purpose of layout, but may be different + /// in other ways. + /// + /// For example, maybe some event handlers changed. + metadata, + + /// The two objects are different but only in ways that affect paint, not layout. + /// + /// For example, only the color is changed. + /// + /// [RenderObject.markNeedsPaint] would be necessary to handle this kind of + /// change in a render object. + paint, + + /// The two objects are different in ways that affect layout (and therefore paint). + /// + /// For example, the size is changed. + /// + /// This is the most drastic level of change possible. + /// + /// [RenderObject.markNeedsLayout] would be necessary to handle this kind of + /// change in a render object. + layout, +} diff --git a/packages/flutter/lib/src/painting/text_span.dart b/packages/flutter/lib/src/painting/text_span.dart index a7f068d7d809e..feb610e4a2e7b 100644 --- a/packages/flutter/lib/src/painting/text_span.dart +++ b/packages/flutter/lib/src/painting/text_span.dart @@ -317,6 +317,39 @@ class TextSpan { return true; } + /// Describe the difference between this text span and another, in terms of + /// how much damage it will make to the rendering. The comparison is deep. + /// + /// See also: + /// + /// * [TextStyle.compareTo], which does the same thing for [TextStyle]s. + RenderComparison compareTo(TextSpan other) { + if (identical(this, other)) + return RenderComparison.identical; + if (other.text != text || + children?.length != other.children?.length || + (style == null) != (other.style == null)) + return RenderComparison.layout; + RenderComparison result = recognizer == other.recognizer ? RenderComparison.identical : RenderComparison.metadata; + if (style != null) { + final RenderComparison candidate = style.compareTo(other.style); + if (candidate.index > result.index) + result = candidate; + if (result == RenderComparison.layout) + return result; + } + if (children != null) { + for (int index = 0; index < children.length; index += 1) { + final RenderComparison candidate = children[index].compareTo(other.children[index]); + if (candidate.index > result.index) + result = candidate; + if (result == RenderComparison.layout) + return result; + } + } + return result; + } + @override bool operator ==(dynamic other) { if (identical(this, other)) diff --git a/packages/flutter/lib/src/painting/text_style.dart b/packages/flutter/lib/src/painting/text_style.dart index c7595b8fc1552..f2605523726ec 100644 --- a/packages/flutter/lib/src/painting/text_style.dart +++ b/packages/flutter/lib/src/painting/text_style.dart @@ -387,6 +387,33 @@ class TextStyle { ); } + /// Describe the difference between this style and another, in terms of how + /// much damage it will make to the rendering. + /// + /// See also: + /// + /// * [TextSpan.compareTo], which does the same thing for entire [TextSpan]s. + RenderComparison compareTo(TextStyle other) { + if (identical(this, other)) + return RenderComparison.identical; + if (inherit != other.inherit || + fontFamily != other.fontFamily || + fontSize != other.fontSize || + fontWeight != other.fontWeight || + fontStyle != other.fontStyle || + letterSpacing != other.letterSpacing || + wordSpacing != other.wordSpacing || + textBaseline != other.textBaseline || + height != other.height) + return RenderComparison.layout; + if (color != other.color || + decoration != other.decoration || + decorationColor != other.decorationColor || + decorationStyle != other.decorationStyle) + return RenderComparison.paint; + return RenderComparison.identical; + } + @override bool operator ==(dynamic other) { if (identical(this, other)) diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index d720c2d956b92..d84ca71e431ff 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -1559,6 +1559,8 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { /// to condition their runtime behavior on whether they are dirty or not, /// since they should only be marked dirty immediately prior to being laid /// out and painted. + /// + /// It is intended to be used by tests and asserts. bool get debugNeedsLayout { bool result; assert(() { @@ -2133,6 +2135,28 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { _needsCompositingBitsUpdate = false; } + /// Whether this render object's paint information is dirty. + /// + /// This is only set in debug mode. In general, render objects should not need + /// to condition their runtime behavior on whether they are dirty or not, + /// since they should only be marked dirty immediately prior to being laid + /// out and painted. + /// + /// It is intended to be used by tests and asserts. + /// + /// It is possible (and indeed, quite common) for [debugNeedsPaint] to be + /// false and [debugNeedsLayout] to be true. The render object will still be + /// repainted in the next frame when this is the case, because the + /// [markNeedsPaint] method is implicitly called by the framework after a + /// render object is laid out, prior to the paint phase. + bool get debugNeedsPaint { + bool result; + assert(() { + result = _needsPaint; + return true; + }); + return result; + } bool _needsPaint = true; /// Mark this render object as having changed its visual appearance. @@ -2145,6 +2169,10 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { /// /// This mechanism batches the painting work so that multiple sequential /// writes are coalesced, removing redundant computation. + /// + /// Once [markNeedsPaint] has been called on a render object, + /// [debugNeedsPaint] returns true for that render object until just after + /// the pipeline owner has called [paint] on the render object. void markNeedsPaint() { assert(owner == null || !owner.debugDoingPaint); if (_needsPaint) diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart index 51ef993009e9d..667929c55d903 100644 --- a/packages/flutter/lib/src/rendering/paragraph.dart +++ b/packages/flutter/lib/src/rendering/paragraph.dart @@ -64,11 +64,20 @@ class RenderParagraph extends RenderBox { TextSpan get text => _textPainter.text; set text(TextSpan value) { assert(value != null); - if (_textPainter.text == value) - return; - _textPainter.text = value; - _overflowShader = null; - markNeedsLayout(); + switch (_textPainter.text.compareTo(value)) { + case RenderComparison.identical: + case RenderComparison.metadata: + return; + case RenderComparison.paint: + _textPainter.text = value; + markNeedsPaint(); + break; + case RenderComparison.layout: + _textPainter.text = value; + _overflowShader = null; + markNeedsLayout(); + break; + } } /// How the text should be aligned horizontally. diff --git a/packages/flutter/test/rendering/paragraph_test.dart b/packages/flutter/test/rendering/paragraph_test.dart index e90d7f28d5553..ed01b1e7d2343 100644 --- a/packages/flutter/test/rendering/paragraph_test.dart +++ b/packages/flutter/test/rendering/paragraph_test.dart @@ -177,6 +177,36 @@ void main() { expect(paragraph.size.height, 30.0); }); + test('changing color does not do layout', () { + final RenderParagraph paragraph = new RenderParagraph( + const TextSpan( + text: 'Hello', + style: const TextStyle(color: const Color(0xFF000000)), + ), + ); + layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0), phase: EnginePhase.paint); + expect(paragraph.debugNeedsLayout, isFalse); + expect(paragraph.debugNeedsPaint, isFalse); + paragraph.text = const TextSpan( + text: 'Hello World', + style: const TextStyle(color: const Color(0xFF000000)), + ); + expect(paragraph.debugNeedsLayout, isTrue); + expect(paragraph.debugNeedsPaint, isFalse); + pumpFrame(phase: EnginePhase.paint); + expect(paragraph.debugNeedsLayout, isFalse); + expect(paragraph.debugNeedsPaint, isFalse); + paragraph.text = const TextSpan( + text: 'Hello World', + style: const TextStyle(color: const Color(0xFFFFFFFF)), + ); + expect(paragraph.debugNeedsLayout, isFalse); + expect(paragraph.debugNeedsPaint, isTrue); + pumpFrame(phase: EnginePhase.paint); + expect(paragraph.debugNeedsLayout, isFalse); + expect(paragraph.debugNeedsPaint, isFalse); + }); + test('toStringDeep', () { final RenderParagraph paragraph = new RenderParagraph( const TextSpan(text: _kText), From e890ec5bb6b09f49fe93b0876a7b3f96b949cc12 Mon Sep 17 00:00:00 2001 From: Carlo Bernaschina Date: Wed, 19 Jul 2017 18:37:54 -0700 Subject: [PATCH 34/39] Fix 'reloadSources' service from Observatory (#11315) Fixes https://github.com/flutter/flutter/issues/11314 --- packages/flutter_tools/lib/src/run_hot.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index e908bf00af541..0689738824aab 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -89,7 +89,7 @@ class HotRunner extends ResidentRunner { { bool force: false, bool pause: false }) async { // TODO(cbernaschina): check that isolateId is the id of the UI isolate. final OperationResult result = await restart(pauseAfterRestart: pause); - if (result != OperationResult.ok) { + if (!result.isOk) { throw new rpc.RpcException( rpc_error_code.INTERNAL_ERROR, 'Unable to reload sources', From 56700930a5b2d3ad501f252679e390301e51719b Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Wed, 19 Jul 2017 21:26:35 -0700 Subject: [PATCH 35/39] Rev engine to 5fcfb995bbce72b5f1ee807121f51a3c0280c8b4 (#11318) --- 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 5e797157b42c6..dbab89d673ae0 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -53c9a702821b154f137541d4a528fbd25f00ad1b +5fcfb995bbce72b5f1ee807121f51a3c0280c8b4 From bb15e346bb5e5b3e53bd4e780ca50f783644ef86 Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 20 Jul 2017 11:15:22 -0700 Subject: [PATCH 36/39] Add slider customizations (#11185) * adds inactiveColor and showThumb to Slider * add customizable color and showThumb tests * remove showThumb, add negative tests --- packages/flutter/lib/src/material/slider.dart | 29 ++++++-- .../flutter/test/material/slider_test.dart | 66 +++++++++++++++++++ 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart index 213884cdd7bff..5cc935716c630 100644 --- a/packages/flutter/lib/src/material/slider.dart +++ b/packages/flutter/lib/src/material/slider.dart @@ -64,6 +64,7 @@ class Slider extends StatefulWidget { this.divisions, this.label, this.activeColor, + this.inactiveColor, this.thumbOpenAtMin: false, }) : assert(value != null), assert(min != null), @@ -138,6 +139,11 @@ class Slider extends StatefulWidget { /// Defaults to accent color of the current [Theme]. final Color activeColor; + /// The color for the unselected portion of the slider. + /// + /// Defaults to the unselected widget color of the current [Theme]. + final Color inactiveColor; + /// Whether the thumb should be an open circle when the slider is at its minimum position. /// /// When this property is false, the thumb does not change when it the slider @@ -178,6 +184,7 @@ class _SliderState extends State with TickerProviderStateMixin { divisions: widget.divisions, label: widget.label, activeColor: widget.activeColor ?? theme.accentColor, + inactiveColor: widget.inactiveColor ?? theme.unselectedWidgetColor, thumbOpenAtMin: widget.thumbOpenAtMin, textTheme: theme.accentTextTheme, onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null, @@ -193,6 +200,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { this.divisions, this.label, this.activeColor, + this.inactiveColor, this.thumbOpenAtMin, this.textTheme, this.onChanged, @@ -203,6 +211,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { final int divisions; final String label; final Color activeColor; + final Color inactiveColor; final bool thumbOpenAtMin; final TextTheme textTheme; final ValueChanged onChanged; @@ -215,6 +224,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { divisions: divisions, label: label, activeColor: activeColor, + inactiveColor: inactiveColor, thumbOpenAtMin: thumbOpenAtMin, textTheme: textTheme, onChanged: onChanged, @@ -229,6 +239,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget { ..divisions = divisions ..label = label ..activeColor = activeColor + ..inactiveColor = inactiveColor ..thumbOpenAtMin = thumbOpenAtMin ..textTheme = textTheme ..onChanged = onChanged; @@ -246,11 +257,9 @@ const double _kMinimumTrackWidth = _kActiveThumbRadius; // biggest of the thumb const double _kPreferredTotalWidth = _kPreferredTrackWidth + 2 * _kReactionRadius; const double _kMinimumTotalWidth = _kMinimumTrackWidth + 2 * _kReactionRadius; -final Color _kInactiveTrackColor = Colors.grey.shade400; final Color _kActiveTrackColor = Colors.grey; final Tween _kReactionRadiusTween = new Tween(begin: _kThumbRadius, end: _kReactionRadius); final Tween _kThumbRadiusTween = new Tween(begin: _kThumbRadius, end: _kActiveThumbRadius); -final ColorTween _kTrackColorTween = new ColorTween(begin: _kInactiveTrackColor, end: _kActiveTrackColor); final ColorTween _kTickColorTween = new ColorTween(begin: Colors.transparent, end: Colors.black54); final Duration _kDiscreteTransitionDuration = const Duration(milliseconds: 500); @@ -276,6 +285,7 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler { int divisions, String label, Color activeColor, + Color inactiveColor, bool thumbOpenAtMin, TextTheme textTheme, this.onChanged, @@ -284,6 +294,7 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler { _value = value, _divisions = divisions, _activeColor = activeColor, + _inactiveColor = inactiveColor, _thumbOpenAtMin = thumbOpenAtMin, _textTheme = textTheme { this.label = label; @@ -363,6 +374,15 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler { markNeedsPaint(); } + Color get inactiveColor => _inactiveColor; + Color _inactiveColor; + set inactiveColor(Color value) { + if (value == _inactiveColor) + return; + _inactiveColor = value; + markNeedsPaint(); + } + bool get thumbOpenAtMin => _thumbOpenAtMin; bool _thumbOpenAtMin; set thumbOpenAtMin(bool value) { @@ -451,7 +471,6 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler { } } - @override double computeMinIntrinsicWidth(double height) { return _kMinimumTotalWidth; @@ -501,8 +520,8 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler { final double trackRight = trackLeft + trackLength; final double trackActive = trackLeft + trackLength * value; - final Paint primaryPaint = new Paint()..color = enabled ? _activeColor : _kInactiveTrackColor; - final Paint trackPaint = new Paint()..color = _kTrackColorTween.evaluate(_reaction); + final Paint primaryPaint = new Paint()..color = enabled ? _activeColor : _inactiveColor; + final Paint trackPaint = new Paint()..color = _inactiveColor; final Offset thumbCenter = new Offset(trackActive, trackCenter); final double thumbRadius = enabled ? _kThumbRadiusTween.evaluate(_reaction) : _kDisabledThumbRadius; diff --git a/packages/flutter/test/material/slider_test.dart b/packages/flutter/test/material/slider_test.dart index 3942e4039553d..05f876bc2080c 100644 --- a/packages/flutter/test/material/slider_test.dart +++ b/packages/flutter/test/material/slider_test.dart @@ -118,6 +118,72 @@ void main() { log.clear(); }); + testWidgets('Slider has a customizable active color', + (WidgetTester tester) async { + final Color customColor = const Color(0xFF4CD964); + final ThemeData theme = new ThemeData(platform: TargetPlatform.android); + Widget buildApp(Color activeColor) { + return new Material( + child: new Center( + child: new Theme( + data: theme, + child: new Slider( + value: 0.5, + activeColor: activeColor, + onChanged: (double newValue) {}, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildApp(null)); + + final RenderBox sliderBox = + tester.firstRenderObject(find.byType(Slider)); + + expect(sliderBox, paints..rect(color: theme.accentColor)..rect(color: theme.unselectedWidgetColor)); + expect(sliderBox, paints..circle(color: theme.accentColor)); + expect(sliderBox, isNot(paints..circle(color: customColor))); + expect(sliderBox, isNot(paints..circle(color: theme.unselectedWidgetColor))); + await tester.pumpWidget(buildApp(customColor)); + expect(sliderBox, paints..rect(color: customColor)..rect(color: theme.unselectedWidgetColor)); + expect(sliderBox, paints..circle(color: customColor)); + expect(sliderBox, isNot(paints..circle(color: theme.accentColor))); + expect(sliderBox, isNot(paints..circle(color: theme.unselectedWidgetColor))); + }); + + testWidgets('Slider has a customizable inactive color', + (WidgetTester tester) async { + final Color customColor = const Color(0xFF4CD964); + final ThemeData theme = new ThemeData(platform: TargetPlatform.android); + Widget buildApp(Color inactiveColor) { + return new Material( + child: new Center( + child: new Theme( + data: theme, + child: new Slider( + value: 0.5, + inactiveColor: inactiveColor, + onChanged: (double newValue) {}, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildApp(null)); + + final RenderBox sliderBox = + tester.firstRenderObject(find.byType(Slider)); + + expect(sliderBox, paints..rect(color: theme.accentColor)..rect(color: theme.unselectedWidgetColor)); + expect(sliderBox, paints..circle(color: theme.accentColor)); + await tester.pumpWidget(buildApp(customColor)); + expect(sliderBox, paints..rect(color: theme.accentColor)..rect(color: customColor)); + expect(sliderBox, paints..circle(color: theme.accentColor)); + }); + testWidgets('Slider can draw an open thumb at min', (WidgetTester tester) async { Widget buildApp(bool thumbOpenAtMin) { From 8e3884820386246553b902b16494f41f9310adf4 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Thu, 20 Jul 2017 14:26:52 -0700 Subject: [PATCH 37/39] Fix sample code and update docs (#11257) * fix sample code * review comments * review comments * document other members for extra bonus points --- packages/flutter/lib/src/widgets/routes.dart | 46 ++++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index fd75f000e0cd5..80f7cb1ae4a20 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -971,11 +971,37 @@ abstract class PopupRoute extends ModalRoute { /// A [Navigator] observer that notifies [RouteAware]s of changes to the /// state of their [Route]. /// +/// [RouteObserver] informs subscribers whenever a route of type `T` is pushed +/// on top of their own route of type `T` or popped from it. This is for example +/// useful to keep track of page transitions, e.i. a `RouteObserver` +/// will inform subscribed [RouteAware]s whenever the user navigates away from +/// the current page route to another page route. +/// +/// If you want to be informed about route changes of any type, you should +/// instantiate a `RouteObserver`. +/// +/// ## Sample code +/// /// To make a [StatefulWidget] aware of its current [Route] state, implement -/// [RouteAware] in its [State] and subscribe it to the [RouteObserver]: +/// [RouteAware] in its [State] and subscribe it to a [RouteObserver]: /// /// ```dart +/// // Register the RouteObserver as a navigation observer. +/// final RouteObserver routeObserver = new RouteObserver(); +/// void main() { +/// runApp(new MaterialApp( +/// home: new Container(), +/// navigatorObservers: [routeObserver], +/// )); +/// } +/// +/// class RouteAwareWidget extends StatefulWidget { +/// State createState() => new RouteAwareWidgetState(); +/// } +/// +/// // Implement RouteAware in a widget's state and subscribe it to the RouteObserver. /// class RouteAwareWidgetState extends State with RouteAware { +/// /// @override /// void didChangeDependencies() { /// super.didChangeDependencies(); @@ -990,19 +1016,28 @@ abstract class PopupRoute extends ModalRoute { /// /// @override /// void didPush() { -/// // Do something +/// // Route was pushed onto navigator and is now topmost route. /// } /// /// @override /// void didPopNext() { -/// // Do something +/// // Covering route was popped off the navigator. /// } /// +/// @override +/// Widget build(BuildContext context) => new Container(); +/// /// } +/// /// ``` class RouteObserver> extends NavigatorObserver { final Map _listeners = {}; + /// Subscribe [routeAware] to be informed about changes to [route]. + /// + /// Going forward, [routeAware] will be informed about qualifying changes + /// to [route], e.g. when [route] is covered by another route or when [route] + /// is popped off the [Navigator] stack. void subscribe(RouteAware routeAware, T route) { assert(routeAware != null); assert(route != null); @@ -1012,6 +1047,9 @@ class RouteObserver> extends NavigatorObserver { } } + /// Unsubscribe [routeAware]. + /// + /// [routeAware] is no longer informed about changes to its route. void unsubscribe(RouteAware routeAware) { assert(routeAware != null); _listeners.remove(routeAware); @@ -1048,4 +1086,4 @@ abstract class RouteAware { /// Called when a new route has been pushed, and the current route is no /// longer visible. void didPushNext() { } -} \ No newline at end of file +} From 48427cb77ad0323dc6e77e6ec857836237f9db12 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Thu, 20 Jul 2017 14:47:36 -0700 Subject: [PATCH 38/39] Revert "Flutter analyze watch improvements (#11143)" (#11328) This reverts commit e13e7806e37a28cfef766b72a6987593063b8d34. Turns out that with this patch, we aren't actually catching all errors. For example, `flutter analyze --flutter-repo --watch` didn't report errors in `dev/devicelab/test/adb_test.dart`. --- .../lib/src/commands/analyze_base.dart | 19 ----- .../src/commands/analyze_continuously.dart | 58 +++----------- .../lib/src/commands/analyze_once.dart | 19 +++++ .../commands/analyze_continuously_test.dart | 77 ++++++++----------- 4 files changed, 62 insertions(+), 111 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/analyze_base.dart b/packages/flutter_tools/lib/src/commands/analyze_base.dart index f2e9ec63ed8fe..177a344124d4d 100644 --- a/packages/flutter_tools/lib/src/commands/analyze_base.dart +++ b/packages/flutter_tools/lib/src/commands/analyze_base.dart @@ -39,25 +39,6 @@ abstract class AnalyzeBase { } } - List flutterRootComponents; - bool isFlutterLibrary(String filename) { - flutterRootComponents ??= fs.path.normalize(fs.path.absolute(Cache.flutterRoot)).split(fs.path.separator); - final List filenameComponents = fs.path.normalize(fs.path.absolute(filename)).split(fs.path.separator); - if (filenameComponents.length < flutterRootComponents.length + 4) // the 4: 'packages', package_name, 'lib', file_name - return false; - for (int index = 0; index < flutterRootComponents.length; index += 1) { - if (flutterRootComponents[index] != filenameComponents[index]) - return false; - } - if (filenameComponents[flutterRootComponents.length] != 'packages') - return false; - if (filenameComponents[flutterRootComponents.length + 1] == 'flutter_tools') - return false; - if (filenameComponents[flutterRootComponents.length + 2] != 'lib') - return false; - return true; - } - void writeBenchmark(Stopwatch stopwatch, int errorCount, int membersMissingDocumentation) { final String benchmarkOut = 'analysis_benchmark.json'; final Map data = { diff --git a/packages/flutter_tools/lib/src/commands/analyze_continuously.dart b/packages/flutter_tools/lib/src/commands/analyze_continuously.dart index aba659bba4ca5..dac4347a16d06 100644 --- a/packages/flutter_tools/lib/src/commands/analyze_continuously.dart +++ b/packages/flutter_tools/lib/src/commands/analyze_continuously.dart @@ -31,20 +31,15 @@ class AnalyzeContinuously extends AnalyzeBase { Stopwatch analysisTimer; int lastErrorCount = 0; Status analysisStatus; - bool flutterRepo; - bool showDartDocIssuesIndividually; @override Future analyze() async { List directories; - flutterRepo = argResults['flutter-repo'] || inRepo(null); - showDartDocIssuesIndividually = argResults['dartdocs']; + if (argResults['dartdocs']) + throwToolExit('The --dartdocs option is currently not supported when using --watch.'); - if (showDartDocIssuesIndividually && !flutterRepo) - throwToolExit('The --dartdocs option is only supported when using --flutter-repo.'); - - if (flutterRepo) { + if (argResults['flutter-repo']) { final PackageDependencyTracker dependencies = new PackageDependencyTracker(); dependencies.checkForConflictingDependencies(repoPackages, dependencies); directories = repoPackages.map((Directory dir) => dir.path).toList(); @@ -57,7 +52,7 @@ class AnalyzeContinuously extends AnalyzeBase { analysisTarget = fs.currentDirectory.path; } - final AnalysisServer server = new AnalysisServer(dartSdkPath, directories, flutterRepo: flutterRepo); + final AnalysisServer server = new AnalysisServer(dartSdkPath, directories); server.onAnalyzing.listen((bool isAnalyzing) => _handleAnalysisStatus(server, isAnalyzing)); server.onErrors.listen(_handleAnalysisErrors); @@ -87,52 +82,29 @@ class AnalyzeContinuously extends AnalyzeBase { logger.printStatus(terminal.clearScreen(), newline: false); // Remove errors for deleted files, sort, and print errors. - final List allErrors = []; + final List errors = []; for (String path in analysisErrors.keys.toList()) { if (fs.isFileSync(path)) { - allErrors.addAll(analysisErrors[path]); + errors.addAll(analysisErrors[path]); } else { analysisErrors.remove(path); } } - // Summarize dartdoc issues rather than displaying each individually - int membersMissingDocumentation = 0; - List detailErrors; - if (flutterRepo && !showDartDocIssuesIndividually) { - detailErrors = allErrors.where((AnalysisError error) { - if (error.code == 'public_member_api_docs') { - // https://github.com/dart-lang/linter/issues/208 - if (isFlutterLibrary(error.file)) - membersMissingDocumentation += 1; - return true; - } - return false; - }).toList(); - } else { - detailErrors = allErrors; - } - - detailErrors.sort(); + errors.sort(); - for (AnalysisError error in detailErrors) { + for (AnalysisError error in errors) { printStatus(error.toString()); if (error.code != null) printTrace('error code: ${error.code}'); } - dumpErrors(detailErrors.map((AnalysisError error) => error.toLegacyString())); - - if (membersMissingDocumentation != 0) { - printStatus(membersMissingDocumentation == 1 - ? '1 public member lacks documentation' - : '$membersMissingDocumentation public members lack documentation'); - } + dumpErrors(errors.map((AnalysisError error) => error.toLegacyString())); // Print an analysis summary. String errorsMessage; - final int issueCount = detailErrors.length; + final int issueCount = errors.length; final int issueDiff = issueCount - lastErrorCount; lastErrorCount = issueCount; @@ -178,11 +150,10 @@ class AnalyzeContinuously extends AnalyzeBase { } class AnalysisServer { - AnalysisServer(this.sdk, this.directories, { this.flutterRepo: false }); + AnalysisServer(this.sdk, this.directories); final String sdk; final List directories; - final bool flutterRepo; Process _process; final StreamController _analyzingController = new StreamController.broadcast(); @@ -198,13 +169,6 @@ class AnalysisServer { '--sdk', sdk, ]; - // Let the analysis server know that the flutter repository is being analyzed - // so that it can enable the public_member_api_docs lint even though - // the analysis_options file does not have that lint enabled. - // It is not enabled in the analysis_option file - // because doing so causes too much noise in the IDE. - if (flutterRepo) - command.add('--flutter-repo'); printTrace('dart ${command.skip(1).join(' ')}'); _process = await processManager.start(command); diff --git a/packages/flutter_tools/lib/src/commands/analyze_once.dart b/packages/flutter_tools/lib/src/commands/analyze_once.dart index e5e2888393a50..10342507576f7 100644 --- a/packages/flutter_tools/lib/src/commands/analyze_once.dart +++ b/packages/flutter_tools/lib/src/commands/analyze_once.dart @@ -245,6 +245,25 @@ class AnalyzeOnce extends AnalyzeBase { return dir; } + List flutterRootComponents; + bool isFlutterLibrary(String filename) { + flutterRootComponents ??= fs.path.normalize(fs.path.absolute(Cache.flutterRoot)).split(fs.path.separator); + final List filenameComponents = fs.path.normalize(fs.path.absolute(filename)).split(fs.path.separator); + if (filenameComponents.length < flutterRootComponents.length + 4) // the 4: 'packages', package_name, 'lib', file_name + return false; + for (int index = 0; index < flutterRootComponents.length; index += 1) { + if (flutterRootComponents[index] != filenameComponents[index]) + return false; + } + if (filenameComponents[flutterRootComponents.length] != 'packages') + return false; + if (filenameComponents[flutterRootComponents.length + 1] == 'flutter_tools') + return false; + if (filenameComponents[flutterRootComponents.length + 2] != 'lib') + return false; + return true; + } + List _collectDartFiles(Directory dir, List collected) { // Bail out in case of a .dartignore. if (fs.isFileSync(fs.path.join(dir.path, '.dartignore'))) diff --git a/packages/flutter_tools/test/commands/analyze_continuously_test.dart b/packages/flutter_tools/test/commands/analyze_continuously_test.dart index f88858f6863b4..ad72fe178a367 100644 --- a/packages/flutter_tools/test/commands/analyze_continuously_test.dart +++ b/packages/flutter_tools/test/commands/analyze_continuously_test.dart @@ -23,55 +23,51 @@ void main() { tempDir = fs.systemTempDirectory.createTempSync('analysis_test'); }); - Future analyzeWithServer({ bool brokenCode: false, bool flutterRepo: false, int expectedErrorCount: 0 }) async { - _createSampleProject(tempDir, brokenCode: brokenCode); - - await pubGet(directory: tempDir.path); - - server = new AnalysisServer(dartSdkPath, [tempDir.path], flutterRepo: flutterRepo); - - final Future onDone = server.onAnalyzing.where((bool analyzing) => analyzing == false).first; - final List errors = []; - server.onErrors.listen((FileAnalysisErrors fileErrors) { - errors.addAll(fileErrors.errors); - }); - - await server.start(); - await onDone; - - expect(errors, hasLength(expectedErrorCount)); - return server; - } - tearDown(() { tempDir?.deleteSync(recursive: true); return server?.dispose(); }); group('analyze --watch', () { - }); + testUsingContext('AnalysisServer success', () async { + _createSampleProject(tempDir); - group('AnalysisServer', () { - testUsingContext('success', () async { - server = await analyzeWithServer(); - }, overrides: { - OperatingSystemUtils: () => os - }); + await pubGet(directory: tempDir.path); + + server = new AnalysisServer(dartSdkPath, [tempDir.path]); + + int errorCount = 0; + final Future onDone = server.onAnalyzing.where((bool analyzing) => analyzing == false).first; + server.onErrors.listen((FileAnalysisErrors errors) => errorCount += errors.errors.length); - testUsingContext('errors', () async { - server = await analyzeWithServer(brokenCode: true, expectedErrorCount: 1); + await server.start(); + await onDone; + + expect(errorCount, 0); }, overrides: { OperatingSystemUtils: () => os }); + }); - testUsingContext('--flutter-repo', () async { - // When a Dart SDK containing support for the --flutter-repo startup flag - // https://github.com/dart-lang/sdk/commit/def1ee6604c4b3385b567cb9832af0dbbaf32e0d - // is rolled into Flutter, then the expectedErrorCount should be set to 1. - server = await analyzeWithServer(flutterRepo: true, expectedErrorCount: 0); - }, overrides: { - OperatingSystemUtils: () => os + testUsingContext('AnalysisServer errors', () async { + _createSampleProject(tempDir, brokenCode: true); + + await pubGet(directory: tempDir.path); + + server = new AnalysisServer(dartSdkPath, [tempDir.path]); + + int errorCount = 0; + final Future onDone = server.onAnalyzing.where((bool analyzing) => analyzing == false).first; + server.onErrors.listen((FileAnalysisErrors errors) { + errorCount += errors.errors.length; }); + + await server.start(); + await onDone; + + expect(errorCount, greaterThan(0)); + }, overrides: { + OperatingSystemUtils: () => os }); } @@ -81,13 +77,6 @@ void _createSampleProject(Directory directory, { bool brokenCode: false }) { name: foo_project '''); - final File analysisOptionsFile = fs.file(fs.path.join(directory.path, 'analysis_options.yaml')); - analysisOptionsFile.writeAsStringSync(''' -linter: - rules: - - hash_and_equals -'''); - final File dartFile = fs.file(fs.path.join(directory.path, 'lib', 'main.dart')); dartFile.parent.createSync(); dartFile.writeAsStringSync(''' @@ -95,7 +84,5 @@ void main() { print('hello world'); ${brokenCode ? 'prints("hello world");' : ''} } - -class SomeClassWithoutDartDoc { } '''); } From 9d901327a499e71efd5a96ab3f52ea8238c5c06a Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Thu, 20 Jul 2017 17:06:57 -0700 Subject: [PATCH 39/39] Do not call saveLayer for physical model layers whose bounds are simple rectangles (#11324) This is similar to an optimization done in PhysicalModelLayer::Paint in the engine --- packages/flutter/lib/src/rendering/proxy_box.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index d90f4be195e12..1c36d7d775dad 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -1364,7 +1364,11 @@ class RenderPhysicalModel extends _RenderCustomClip { ); } canvas.drawRRect(offsetClipRRect, new Paint()..color = color); - canvas.saveLayer(offsetBounds, _defaultPaint); + if (offsetClipRRect.isRect) { + canvas.save(); + } else { + canvas.saveLayer(offsetBounds, _defaultPaint); + } canvas.clipRRect(offsetClipRRect); super.paint(context, offset); canvas.restore();