diff --git a/packages/flutter/lib/src/material/button.dart b/packages/flutter/lib/src/material/button.dart index 7167af2abd..1bd69f8edc 100644 --- a/packages/flutter/lib/src/material/button.dart +++ b/packages/flutter/lib/src/material/button.dart @@ -113,9 +113,11 @@ class ButtonTheme extends InheritedWidget { } } -/// A material design button. +/// The framework for building material design buttons. /// -/// Rather than using this class directly, consider using [FlatButton] or [RaisedButton]. +/// Rather than using this class directly, consider using [FlatButton] or +/// [RaisedButton], which configure this class with appropriate defaults that +/// match the material design specification. /// /// MaterialButtons whose [onPressed] handler is null will be disabled. To have /// an enabled button, make sure to pass a non-null value for onPressed. diff --git a/packages/flutter/lib/src/material/material.dart b/packages/flutter/lib/src/material/material.dart index b7837fa81f..e8c09aa7cb 100644 --- a/packages/flutter/lib/src/material/material.dart +++ b/packages/flutter/lib/src/material/material.dart @@ -117,7 +117,7 @@ class Material extends StatefulWidget { this.color, this.textStyle, this.borderRadius, - this.child + this.child, }) : super(key: key) { assert(type != null); assert(elevation != null); @@ -180,8 +180,10 @@ class Material extends StatefulWidget { description.add('elevation: $elevation'); if (color != null) description.add('color: $color'); - if (textStyle != null) - description.add('textStyle: $textStyle'); + if (textStyle != null) { + for (String entry in '$textStyle'.split('\n')) + description.add('textStyle.$entry'); + } if (borderRadius != null) description.add('borderRadius: $borderRadius'); } diff --git a/packages/flutter/lib/src/widgets/container.dart b/packages/flutter/lib/src/widgets/container.dart index 95ef808c7d..80b757387e 100644 --- a/packages/flutter/lib/src/widgets/container.dart +++ b/packages/flutter/lib/src/widgets/container.dart @@ -55,6 +55,26 @@ class DecoratedBox extends SingleChildRenderObjectWidget { ..configuration = createLocalImageConfiguration(context) ..position = position; } + + @override + void debugFillDescription(List description) { + super.debugFillDescription(description); + String label; + if (position != null) { + switch (position) { + case DecorationPosition.background: + label = 'bg'; + break; + case DecorationPosition.foreground: + label = 'fg'; + break; + } + } else { + description.add('position: NULL'); + label = 'decoration'; + } + description.add(decoration != null ? '$label: $decoration' : 'no decoration'); + } } /// A convenience widget that combines common painting, positioning, and sizing diff --git a/packages/flutter/test/material/buttons_test.dart b/packages/flutter/test/material/buttons_test.dart new file mode 100644 index 0000000000..4fe8fc9985 --- /dev/null +++ b/packages/flutter/test/material/buttons_test.dart @@ -0,0 +1,129 @@ +// 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. + +import 'package:flutter/rendering.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../rendering/mock_canvas.dart'; +import '../widgets/semantics_tester.dart'; + +void main() { + testWidgets('Does FlatButton contribute semantics', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + await tester.pumpWidget( + new Material( + child: new Center( + child: new FlatButton( + onPressed: () { }, + child: new Text('ABC') + ) + ) + ) + ); + + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + children: [ + new TestSemantics( + id: 1, + actions: SemanticsAction.tap.index, + label: 'ABC', + rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 36.0), + transform: new Matrix4.translationValues(356.0, 282.0, 0.0) + ) + ] + ) + )); + + semantics.dispose(); + }); + + testWidgets('Does button highlight + splash colors work if set directly', (WidgetTester tester) async { + final Color directSplashColor = new Color(0xFF000011); + final Color directHighlightColor = new Color(0xFF000011); + + Widget buttonWidget = new Material( + child: new Center( + child: new MaterialButton( + splashColor: directSplashColor, + highlightColor: directHighlightColor, + onPressed: () { /* to make sure the button is enabled */ }, + ), + ), + ); + + await tester.pumpWidget( + new Theme( + data: new ThemeData(), + child: buttonWidget, + ), + ); + + final Point center = tester.getCenter(find.byType(MaterialButton)); + final TestGesture gesture = await tester.startGesture(center); + await tester.pump(); // start gesture + await tester.pump(new Duration(milliseconds: 200)); // wait for splash to be well under way + + expect( + Material.of(tester.element(find.byType(MaterialButton))), + paints + ..circle(color: directSplashColor) + ..rrect(color: directHighlightColor) + ); + + final Color themeSplashColor1 = new Color(0xFF001100); + final Color themeHighlightColor1 = new Color(0xFF001100); + + buttonWidget = new Material( + child: new Center( + child: new MaterialButton( + onPressed: () { /* to make sure the button is enabled */ }, + ), + ), + ); + + await tester.pumpWidget( + new Theme( + data: new ThemeData( + highlightColor: themeHighlightColor1, + splashColor: themeSplashColor1, + ), + child: buttonWidget, + ), + ); + + expect( + Material.of(tester.element(find.byType(MaterialButton))), + paints + ..circle(color: themeSplashColor1) + ..rrect(color: themeHighlightColor1) + ); + + final Color themeSplashColor2 = new Color(0xFF002200); + final Color themeHighlightColor2 = new Color(0xFF002200); + + await tester.pumpWidget( + new Theme( + data: new ThemeData( + highlightColor: themeHighlightColor2, + splashColor: themeSplashColor2, + ), + child: buttonWidget, // same widget, so does not get updated because of us + ), + ); + + expect( + Material.of(tester.element(find.byType(MaterialButton))), + paints + ..circle(color: themeSplashColor2) + ..rrect(color: themeHighlightColor2) + ); + + await gesture.up(); + }); + +} diff --git a/packages/flutter/test/material/ink_paint_test.dart b/packages/flutter/test/material/ink_paint_test.dart index 41b00146cc..115cfae826 100644 --- a/packages/flutter/test/material/ink_paint_test.dart +++ b/packages/flutter/test/material/ink_paint_test.dart @@ -9,8 +9,6 @@ import 'package:flutter_test/flutter_test.dart'; import '../rendering/mock_canvas.dart'; void main() { - Material.debugEnablePhysicalModel = true; - testWidgets('Does the ink widget render a border radius', (WidgetTester tester) async { final Color highlightColor = new Color(0xAAFF0000); final Color splashColor = new Color(0xAA0000FF); @@ -38,7 +36,7 @@ void main() { await tester.pump(); // start gesture await tester.pump(new Duration(milliseconds: 200)); // wait for splash to be well under way - final RenderBox box = tester.renderObject(find.byType(PhysicalModel)).child; + final RenderBox box = Material.of(tester.element(find.byType(InkWell))) as dynamic; expect( box, paints diff --git a/packages/flutter/test/material/material_test.dart b/packages/flutter/test/material/material_test.dart index 92594b7d9e..d05ef139cf 100644 --- a/packages/flutter/test/material/material_test.dart +++ b/packages/flutter/test/material/material_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; class NotifyMaterial extends StatelessWidget { @@ -13,6 +14,25 @@ class NotifyMaterial extends StatelessWidget { } } +Widget buildMaterial(int elevation) { + return new Center( + child: new SizedBox( + height: 100.0, + width: 100.0, + child: new Material( + color: const Color(0xFF00FF00), + elevation: elevation, + ), + ), + ); +} + +List getShadow(WidgetTester tester) { + final RenderDecoratedBox box = tester.renderObject(find.byType(DecoratedBox).first); + final BoxDecoration decoration = box.decoration; + return decoration.boxShadow; +} + class PaintRecorder extends CustomPainter { PaintRecorder(this.log); @@ -95,4 +115,69 @@ void main() { expect(log, isEmpty); }); + + testWidgets('Shadows animate smoothly', (WidgetTester tester) async { + await tester.pumpWidget(buildMaterial(0)); + final List shadowA = getShadow(tester); + + await tester.pumpWidget(buildMaterial(9)); + final List shadowB = getShadow(tester); + + await tester.pump(const Duration(milliseconds: 1)); + final List shadowC = getShadow(tester); + + await tester.pump(kThemeChangeDuration ~/ 2); + final List shadowD = getShadow(tester); + + await tester.pump(kThemeChangeDuration); + final List shadowE = getShadow(tester); + + // This code verifies the following: + // 1. When the elevation is zero, there's no shadow. + // 2. When the elevation isn't zero, there's three shadows. + // 3. When the elevation changes from 0 to 9, one millisecond later, the + // shadows are still more or less indistinguishable from zero. + // 4. Have a kThemeChangeDuration later, they are distinguishable form + // zero. + // 5. ...but still distinguishable from the actual 9 elevation. + // 6. After kThemeChangeDuration, the shadows are exactly the elevation 9 + // shadows. + // The point being to verify that the shadows animate, and do so + // continually, not in discrete increments (e.g. not jumping from elevation + // 0 to 1 to 2 to 3 and so forth). + + // TODO(ianh): Port this test when we turn the physical model back on. + + // 1 + expect(shadowA, isNull); + expect(shadowB, isNull); + // 2 + expect(shadowC, hasLength(3)); + // 3 + expect(shadowC[0].offset.dy, closeTo(0.0, 0.001)); + expect(shadowC[1].offset.dy, closeTo(0.0, 0.001)); + expect(shadowC[2].offset.dy, closeTo(0.0, 0.001)); + expect(shadowC[0].blurRadius, closeTo(0.0, 0.001)); + expect(shadowC[1].blurRadius, closeTo(0.0, 0.001)); + expect(shadowC[2].blurRadius, closeTo(0.0, 0.001)); + expect(shadowC[0].spreadRadius, closeTo(0.0, 0.001)); + expect(shadowC[1].spreadRadius, closeTo(0.0, 0.001)); + expect(shadowC[2].spreadRadius, closeTo(0.0, 0.001)); + // 4 + expect(shadowD[0].offset.dy, isNot(closeTo(0.0, 0.001))); + expect(shadowD[1].offset.dy, isNot(closeTo(0.0, 0.001))); + expect(shadowD[2].offset.dy, isNot(closeTo(0.0, 0.001))); + expect(shadowD[0].blurRadius, isNot(closeTo(0.0, 0.001))); + expect(shadowD[1].blurRadius, isNot(closeTo(0.0, 0.001))); + expect(shadowD[2].blurRadius, isNot(closeTo(0.0, 0.001))); + expect(shadowD[0].spreadRadius, isNot(closeTo(0.0, 0.001))); + expect(shadowD[1].spreadRadius, isNot(closeTo(0.0, 0.001))); + expect(shadowD[2].spreadRadius, isNot(closeTo(0.0, 0.001))); + // 5 + expect(shadowD[0], isNot(shadowE[0])); + expect(shadowD[1], isNot(shadowE[1])); + expect(shadowD[2], isNot(shadowE[2])); + // 6 + expect(shadowE, kElevationToShadow[9]); + }); } diff --git a/packages/flutter/test/rendering/recording_canvas.dart b/packages/flutter/test/rendering/recording_canvas.dart index e4366a351d..481306fe9c 100644 --- a/packages/flutter/test/rendering/recording_canvas.dart +++ b/packages/flutter/test/rendering/recording_canvas.dart @@ -18,8 +18,11 @@ import 'package:flutter/rendering.dart'; /// ``` /// /// In some cases it may be useful to define a subclass that overrides the -/// Canvas methods the test is checking and squirrels away the parameters +/// [Canvas] methods the test is checking and squirrels away the parameters /// that the test requires. +/// +/// For simple tests, consider using the [paints] matcher, which overlays a +/// pattern matching API over [TestRecordingCanvas]. class TestRecordingCanvas implements Canvas { /// All of the method calls on this canvas. final List invocations = []; @@ -70,8 +73,7 @@ class TestRecordingPaintingContext implements PaintingContext { } @override - void noSuchMethod(Invocation invocation) { - } + void noSuchMethod(Invocation invocation) { } } class _MethodCall implements Invocation { diff --git a/packages/flutter/test/widgets/buttons_test.dart b/packages/flutter/test/widgets/buttons_test.dart deleted file mode 100644 index 4126a94b0a..0000000000 --- a/packages/flutter/test/widgets/buttons_test.dart +++ /dev/null @@ -1,142 +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. - -import 'package:flutter/rendering.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'semantics_tester.dart'; - -void main() { - testWidgets('Does FlatButton contribute semantics', (WidgetTester tester) async { - final SemanticsTester semantics = new SemanticsTester(tester); - await tester.pumpWidget( - new Material( - child: new Center( - child: new FlatButton( - onPressed: () { }, - child: new Text('ABC') - ) - ) - ) - ); - - expect(semantics, hasSemantics( - new TestSemantics( - id: 0, - children: [ - new TestSemantics( - id: 1, - actions: SemanticsAction.tap.index, - label: 'ABC', - rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 36.0), - transform: new Matrix4.translationValues(356.0, 282.0, 0.0) - ) - ] - ) - )); - - semantics.dispose(); - }); - - - Widget _buttonWidget({ - Key buttonKey, - Key materialKey, - Color color, Color - highlightColor, - Color splashColor, - double minWidth = 150.0, - double height = 60.0, - bool useTheme = false - }) { - - final Key definedMaterialKey = materialKey ?? new UniqueKey(); - final Key definedButtonKey = buttonKey ?? new UniqueKey(); - - Widget buttonWidget = new Material( - key: definedMaterialKey, - child: new Center( - child: new MaterialButton( - key: definedButtonKey, - color: color, - highlightColor: !useTheme ? highlightColor : null, - splashColor: !useTheme ? splashColor : null, - minWidth: minWidth, - height: height, - onPressed: () { }, - ), - ), - ); - if (useTheme) { - final ThemeData themeData = new ThemeData( - accentColor: color, - highlightColor: highlightColor, - splashColor: splashColor, - ); - buttonWidget = new Theme( - data: themeData, - child: buttonWidget, - ); - } - return buttonWidget; - } - - testWidgets('Does button highlight + splash colors work if set directly', (WidgetTester tester) async { - final Color buttonColor = new Color(0xFFFFFF00); - final Color highlightColor = new Color(0xDD0000FF); - final Color splashColor = new Color(0xAA0000FF); - - final Key materialKey = new UniqueKey(); - final Key buttonKey = new UniqueKey(); - - await tester.pumpWidget( - _buttonWidget( - materialKey: materialKey, - buttonKey: buttonKey, - color: buttonColor, - highlightColor: highlightColor, - splashColor: splashColor, - ), - ); - - final Point center = tester.getCenter(find.byKey(buttonKey)); - final TestGesture gesture = await tester.startGesture(center); - await tester.pump(new Duration(milliseconds: 200)); - - // TODO(ianh) - the object returned by renderObject does not contain splash or highlights (??) - - await gesture.up(); - }); - - testWidgets('Does button highlight color work if set via theme', (WidgetTester tester) async { - final Color buttonColor = new Color(0xFFFFFF00); - final Color highlightColor = new Color(0xDD0000FF); - final Color splashColor = new Color(0xAA0000FF); - - final Key materialKey = new UniqueKey(); - final Key buttonKey = new UniqueKey(); - - await tester.pumpWidget( - _buttonWidget( - useTheme: true, // use a theme wrapper - materialKey: materialKey, - buttonKey: buttonKey, - color: buttonColor, - highlightColor: highlightColor, - splashColor: splashColor, - ), - ); - - final Point center = tester.getCenter(find.byKey(buttonKey)); - final TestGesture gesture = await tester.startGesture(center); - await tester.pump(new Duration(milliseconds: 200)); - - // TODO(ianh) - the object returned by renderObject does not contain splash or highlights (??) - - await gesture.up(); - }); - -}