Commit dfd1ffa7 authored by Sebastiano Poggi's avatar Sebastiano Poggi Committed by Yegor

Surface elevation shadow colour on Material (#12881)

* Surface shadowColor from RenderPhysicalModel to Material

* Fix typo in material_test

* Add nullability info to documentation

* Add support for animating elevation shadow color

* Add shadowColor to Material's debugFillProperties()

* Add missing default value for elevation in Material debugFillProperties()

* Add missing non-null asserts for animate flags in AnimatedPhysicalModel

* Add test for shadow color animating smoothly
parent 91bd9bc4
...@@ -97,8 +97,8 @@ abstract class MaterialInkController { ...@@ -97,8 +97,8 @@ abstract class MaterialInkController {
/// splashes and ink highlights) won't move to account for the new layout. /// splashes and ink highlights) won't move to account for the new layout.
/// ///
/// In general, the features of a [Material] should not change over time (e.g. a /// In general, the features of a [Material] should not change over time (e.g. a
/// [Material] should not change its [color] or [type]). The one exception is /// [Material] should not change its [color], [shadowColor] or [type]). The one
/// the [elevation], changes to which will be animated. /// exception is the [elevation], changes to which will be animated.
/// ///
/// See also: /// See also:
/// ///
...@@ -108,17 +108,19 @@ abstract class MaterialInkController { ...@@ -108,17 +108,19 @@ abstract class MaterialInkController {
class Material extends StatefulWidget { class Material extends StatefulWidget {
/// Creates a piece of material. /// Creates a piece of material.
/// ///
/// The [type] and the [elevation] arguments must not be null. /// The [type], [elevation] and [shadowColor] arguments must not be null.
const Material({ const Material({
Key key, Key key,
this.type: MaterialType.canvas, this.type: MaterialType.canvas,
this.elevation: 0.0, this.elevation: 0.0,
this.color, this.color,
this.shadowColor: const Color(0xFF000000),
this.textStyle, this.textStyle,
this.borderRadius, this.borderRadius,
this.child, this.child,
}) : assert(type != null), }) : assert(type != null),
assert(elevation != null), assert(elevation != null),
assert(shadowColor != null),
assert(!(identical(type, MaterialType.circle) && borderRadius != null)), assert(!(identical(type, MaterialType.circle) && borderRadius != null)),
super(key: key); super(key: key);
...@@ -148,6 +150,11 @@ class Material extends StatefulWidget { ...@@ -148,6 +150,11 @@ class Material extends StatefulWidget {
/// By default, the color is derived from the [type] of material. /// By default, the color is derived from the [type] of material.
final Color color; final Color color;
/// The color to paint the shadow below the material.
///
/// Defaults to fully opaque black.
final Color shadowColor;
/// The typographical style to use for text within this material. /// The typographical style to use for text within this material.
final TextStyle textStyle; final TextStyle textStyle;
...@@ -178,8 +185,9 @@ class Material extends StatefulWidget { ...@@ -178,8 +185,9 @@ class Material extends StatefulWidget {
void debugFillProperties(DiagnosticPropertiesBuilder description) { void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description); super.debugFillProperties(description);
description.add(new EnumProperty<MaterialType>('type', type)); description.add(new EnumProperty<MaterialType>('type', type));
description.add(new DoubleProperty('elevation', elevation)); description.add(new DoubleProperty('elevation', elevation, defaultValue: 0.0));
description.add(new DiagnosticsProperty<Color>('color', color, defaultValue: null)); description.add(new DiagnosticsProperty<Color>('color', color, defaultValue: null));
description.add(new DiagnosticsProperty<Color>('shadowColor', shadowColor, defaultValue: const Color(0xFF000000)));
textStyle?.debugFillProperties(description, prefix: 'textStyle.'); textStyle?.debugFillProperties(description, prefix: 'textStyle.');
description.add(new EnumProperty<BorderRadius>('borderRadius', borderRadius, defaultValue: null)); description.add(new EnumProperty<BorderRadius>('borderRadius', borderRadius, defaultValue: null));
} }
...@@ -238,6 +246,7 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin { ...@@ -238,6 +246,7 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
shape: BoxShape.circle, shape: BoxShape.circle,
elevation: widget.elevation, elevation: widget.elevation,
color: backgroundColor, color: backgroundColor,
shadowColor: widget.shadowColor,
animateColor: false, animateColor: false,
child: contents, child: contents,
); );
...@@ -258,6 +267,7 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin { ...@@ -258,6 +267,7 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
borderRadius: radius ?? BorderRadius.zero, borderRadius: radius ?? BorderRadius.zero,
elevation: widget.elevation, elevation: widget.elevation,
color: backgroundColor, color: backgroundColor,
shadowColor: widget.shadowColor,
animateColor: false, animateColor: false,
child: contents, child: contents,
); );
......
...@@ -1298,20 +1298,23 @@ class RenderPhysicalModel extends _RenderCustomClip<RRect> { ...@@ -1298,20 +1298,23 @@ class RenderPhysicalModel extends _RenderCustomClip<RRect> {
/// ///
/// The [color] is required. /// The [color] is required.
/// ///
/// The [shape], [elevation], and [color] must not be null. /// The [shape], [elevation], [color], and [shadowColor] must not be null.
RenderPhysicalModel({ RenderPhysicalModel({
RenderBox child, RenderBox child,
BoxShape shape: BoxShape.rectangle, BoxShape shape: BoxShape.rectangle,
BorderRadius borderRadius, BorderRadius borderRadius,
double elevation: 0.0, double elevation: 0.0,
@required Color color, @required Color color,
Color shadowColor: const Color(0xFF000000),
}) : assert(shape != null), }) : assert(shape != null),
assert(elevation != null), assert(elevation != null),
assert(color != null), assert(color != null),
assert(shadowColor != null),
_shape = shape, _shape = shape,
_borderRadius = borderRadius, _borderRadius = borderRadius,
_elevation = elevation, _elevation = elevation,
_color = color, _color = color,
_shadowColor = shadowColor,
super(child: child); super(child: child);
/// The shape of the layer. /// The shape of the layer.
...@@ -1357,6 +1360,17 @@ class RenderPhysicalModel extends _RenderCustomClip<RRect> { ...@@ -1357,6 +1360,17 @@ class RenderPhysicalModel extends _RenderCustomClip<RRect> {
markNeedsPaint(); markNeedsPaint();
} }
/// The shadow color.
Color get shadowColor => _shadowColor;
Color _shadowColor;
set shadowColor(Color value) {
assert(value != null);
if (shadowColor == value)
return;
_shadowColor = value;
markNeedsPaint();
}
/// The background color. /// The background color.
Color get color => _color; Color get color => _color;
Color _color; Color _color;
...@@ -1427,7 +1441,7 @@ class RenderPhysicalModel extends _RenderCustomClip<RRect> { ...@@ -1427,7 +1441,7 @@ class RenderPhysicalModel extends _RenderCustomClip<RRect> {
); );
canvas.drawShadow( canvas.drawShadow(
new Path()..addRRect(offsetClipRRect), new Path()..addRRect(offsetClipRRect),
const Color(0xFF000000), shadowColor,
elevation, elevation,
color.alpha != 0xFF, color.alpha != 0xFF,
); );
......
...@@ -648,17 +648,19 @@ class PhysicalModel extends SingleChildRenderObjectWidget { ...@@ -648,17 +648,19 @@ class PhysicalModel extends SingleChildRenderObjectWidget {
/// ///
/// The [color] is required; physical things have a color. /// The [color] is required; physical things have a color.
/// ///
/// The [shape], [elevation], and [color] must not be null. /// The [shape], [elevation], [color], and [shadowColor] must not be null.
const PhysicalModel({ const PhysicalModel({
Key key, Key key,
this.shape: BoxShape.rectangle, this.shape: BoxShape.rectangle,
this.borderRadius, this.borderRadius,
this.elevation: 0.0, this.elevation: 0.0,
@required this.color, @required this.color,
this.shadowColor: const Color(0xFF000000),
Widget child, Widget child,
}) : assert(shape != null), }) : assert(shape != null),
assert(elevation != null), assert(elevation != null),
assert(color != null), assert(color != null),
assert(shadowColor != null),
super(key: key, child: child); super(key: key, child: child);
/// The type of shape. /// The type of shape.
...@@ -678,8 +680,11 @@ class PhysicalModel extends SingleChildRenderObjectWidget { ...@@ -678,8 +680,11 @@ class PhysicalModel extends SingleChildRenderObjectWidget {
/// The background color. /// The background color.
final Color color; final Color color;
/// The shadow color.
final Color shadowColor;
@override @override
RenderPhysicalModel createRenderObject(BuildContext context) => new RenderPhysicalModel(shape: shape, borderRadius: borderRadius, elevation: elevation, color: color); RenderPhysicalModel createRenderObject(BuildContext context) => new RenderPhysicalModel(shape: shape, borderRadius: borderRadius, elevation: elevation, color: color, shadowColor: shadowColor);
@override @override
void updateRenderObject(BuildContext context, RenderPhysicalModel renderObject) { void updateRenderObject(BuildContext context, RenderPhysicalModel renderObject) {
...@@ -687,7 +692,8 @@ class PhysicalModel extends SingleChildRenderObjectWidget { ...@@ -687,7 +692,8 @@ class PhysicalModel extends SingleChildRenderObjectWidget {
..shape = shape ..shape = shape
..borderRadius = borderRadius ..borderRadius = borderRadius
..elevation = elevation ..elevation = elevation
..color = color; ..color = color
..shadowColor = shadowColor;
} }
@override @override
...@@ -697,6 +703,7 @@ class PhysicalModel extends SingleChildRenderObjectWidget { ...@@ -697,6 +703,7 @@ class PhysicalModel extends SingleChildRenderObjectWidget {
description.add(new DiagnosticsProperty<BorderRadius>('borderRadius', borderRadius)); description.add(new DiagnosticsProperty<BorderRadius>('borderRadius', borderRadius));
description.add(new DoubleProperty('elevation', elevation)); description.add(new DoubleProperty('elevation', elevation));
description.add(new DiagnosticsProperty<Color>('color', color)); description.add(new DiagnosticsProperty<Color>('color', color));
description.add(new DiagnosticsProperty<Color>('shadowColor', shadowColor));
} }
} }
......
...@@ -931,10 +931,12 @@ class _AnimatedDefaultTextStyleState extends AnimatedWidgetBaseState<AnimatedDef ...@@ -931,10 +931,12 @@ class _AnimatedDefaultTextStyleState extends AnimatedWidgetBaseState<AnimatedDef
class AnimatedPhysicalModel extends ImplicitlyAnimatedWidget { class AnimatedPhysicalModel extends ImplicitlyAnimatedWidget {
/// Creates a widget that animates the properties of a [PhysicalModel]. /// Creates a widget that animates the properties of a [PhysicalModel].
/// ///
/// The [child], [shape], [borderRadius], [elevation], [color], [curve], and /// The [child], [shape], [borderRadius], [elevation], [color], [shadowColor], [curve], and
/// [duration] arguments must not be null. /// [duration] arguments must not be null.
/// ///
/// Animating [color] is optional and is controlled by the [animateColor] flag. /// Animating [color] is optional and is controlled by the [animateColor] flag.
///
/// Animating [shadowColor] is optional and is controlled by the [animateShadowColor] flag.
const AnimatedPhysicalModel({ const AnimatedPhysicalModel({
Key key, Key key,
@required this.child, @required this.child,
...@@ -943,6 +945,8 @@ class AnimatedPhysicalModel extends ImplicitlyAnimatedWidget { ...@@ -943,6 +945,8 @@ class AnimatedPhysicalModel extends ImplicitlyAnimatedWidget {
@required this.elevation, @required this.elevation,
@required this.color, @required this.color,
this.animateColor: true, this.animateColor: true,
@required this.shadowColor,
this.animateShadowColor: true,
Curve curve: Curves.linear, Curve curve: Curves.linear,
@required Duration duration, @required Duration duration,
}) : assert(child != null), }) : assert(child != null),
...@@ -950,6 +954,9 @@ class AnimatedPhysicalModel extends ImplicitlyAnimatedWidget { ...@@ -950,6 +954,9 @@ class AnimatedPhysicalModel extends ImplicitlyAnimatedWidget {
assert(borderRadius != null), assert(borderRadius != null),
assert(elevation != null), assert(elevation != null),
assert(color != null), assert(color != null),
assert(shadowColor != null),
assert(animateColor != null),
assert(animateShadowColor != null),
super(key: key, curve: curve, duration: duration); super(key: key, curve: curve, duration: duration);
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
...@@ -972,6 +979,12 @@ class AnimatedPhysicalModel extends ImplicitlyAnimatedWidget { ...@@ -972,6 +979,12 @@ class AnimatedPhysicalModel extends ImplicitlyAnimatedWidget {
/// Whether the color should be animated. /// Whether the color should be animated.
final bool animateColor; final bool animateColor;
/// The target shadow color.
final Color shadowColor;
/// Whether the shadow color should be animated.
final bool animateShadowColor;
@override @override
_AnimatedPhysicalModelState createState() => new _AnimatedPhysicalModelState(); _AnimatedPhysicalModelState createState() => new _AnimatedPhysicalModelState();
...@@ -983,6 +996,8 @@ class AnimatedPhysicalModel extends ImplicitlyAnimatedWidget { ...@@ -983,6 +996,8 @@ class AnimatedPhysicalModel extends ImplicitlyAnimatedWidget {
description.add(new DoubleProperty('elevation', elevation)); description.add(new DoubleProperty('elevation', elevation));
description.add(new DiagnosticsProperty<Color>('color', color)); description.add(new DiagnosticsProperty<Color>('color', color));
description.add(new DiagnosticsProperty<bool>('animateColor', animateColor)); description.add(new DiagnosticsProperty<bool>('animateColor', animateColor));
description.add(new DiagnosticsProperty<Color>('shadowColor', shadowColor));
description.add(new DiagnosticsProperty<bool>('animateShadowColor', animateShadowColor));
} }
} }
...@@ -990,12 +1005,14 @@ class _AnimatedPhysicalModelState extends AnimatedWidgetBaseState<AnimatedPhysic ...@@ -990,12 +1005,14 @@ class _AnimatedPhysicalModelState extends AnimatedWidgetBaseState<AnimatedPhysic
BorderRadiusTween _borderRadius; BorderRadiusTween _borderRadius;
Tween<double> _elevation; Tween<double> _elevation;
ColorTween _color; ColorTween _color;
ColorTween _shadowColor;
@override @override
void forEachTween(TweenVisitor<dynamic> visitor) { void forEachTween(TweenVisitor<dynamic> visitor) {
_borderRadius = visitor(_borderRadius, widget.borderRadius, (dynamic value) => new BorderRadiusTween(begin: value)); _borderRadius = visitor(_borderRadius, widget.borderRadius, (dynamic value) => new BorderRadiusTween(begin: value));
_elevation = visitor(_elevation, widget.elevation, (dynamic value) => new Tween<double>(begin: value)); _elevation = visitor(_elevation, widget.elevation, (dynamic value) => new Tween<double>(begin: value));
_color = visitor(_color, widget.color, (dynamic value) => new ColorTween(begin: value)); _color = visitor(_color, widget.color, (dynamic value) => new ColorTween(begin: value));
_shadowColor = visitor(_shadowColor, widget.shadowColor, (dynamic value) => new ColorTween(begin: value));
} }
@override @override
...@@ -1006,6 +1023,9 @@ class _AnimatedPhysicalModelState extends AnimatedWidgetBaseState<AnimatedPhysic ...@@ -1006,6 +1023,9 @@ class _AnimatedPhysicalModelState extends AnimatedWidgetBaseState<AnimatedPhysic
borderRadius: _borderRadius.evaluate(animation), borderRadius: _borderRadius.evaluate(animation),
elevation: _elevation.evaluate(animation), elevation: _elevation.evaluate(animation),
color: widget.animateColor ? _color.evaluate(animation) : widget.color, color: widget.animateColor ? _color.evaluate(animation) : widget.color,
shadowColor: widget.animateShadowColor
? _shadowColor.evaluate(animation)
: widget.shadowColor,
); );
} }
} }
...@@ -14,13 +14,14 @@ class NotifyMaterial extends StatelessWidget { ...@@ -14,13 +14,14 @@ class NotifyMaterial extends StatelessWidget {
} }
} }
Widget buildMaterial(double elevation) { Widget buildMaterial(
{double elevation: 0.0, Color shadowColor: const Color(0xFF00FF00)}) {
return new Center( return new Center(
child: new SizedBox( child: new SizedBox(
height: 100.0, height: 100.0,
width: 100.0, width: 100.0,
child: new Material( child: new Material(
color: const Color(0xFF00FF00), shadowColor: shadowColor,
elevation: elevation, elevation: elevation,
), ),
), ),
...@@ -48,7 +49,7 @@ class PaintRecorder extends CustomPainter { ...@@ -48,7 +49,7 @@ class PaintRecorder extends CustomPainter {
} }
void main() { void main() {
testWidgets('LayoutChangedNotificaion test', (WidgetTester tester) async { testWidgets('LayoutChangedNotification test', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
new Material( new Material(
child: new NotifyMaterial(), child: new NotifyMaterial(),
...@@ -119,11 +120,11 @@ void main() { ...@@ -119,11 +120,11 @@ void main() {
// This code verifies that the PhysicalModel's elevation animates over // This code verifies that the PhysicalModel's elevation animates over
// a kThemeChangeDuration time interval. // a kThemeChangeDuration time interval.
await tester.pumpWidget(buildMaterial(0.0)); await tester.pumpWidget(buildMaterial(elevation: 0.0));
final RenderPhysicalModel modelA = getShadow(tester); final RenderPhysicalModel modelA = getShadow(tester);
expect(modelA.elevation, equals(0.0)); expect(modelA.elevation, equals(0.0));
await tester.pumpWidget(buildMaterial(9.0)); await tester.pumpWidget(buildMaterial(elevation: 9.0));
final RenderPhysicalModel modelB = getShadow(tester); final RenderPhysicalModel modelB = getShadow(tester);
expect(modelB.elevation, equals(0.0)); expect(modelB.elevation, equals(0.0));
...@@ -139,4 +140,35 @@ void main() { ...@@ -139,4 +140,35 @@ void main() {
final RenderPhysicalModel modelE = getShadow(tester); final RenderPhysicalModel modelE = getShadow(tester);
expect(modelE.elevation, equals(9.0)); expect(modelE.elevation, equals(9.0));
}); });
testWidgets('Shadow colors animate smoothly', (WidgetTester tester) async {
// This code verifies that the PhysicalModel's elevation animates over
// a kThemeChangeDuration time interval.
await tester.pumpWidget(buildMaterial(shadowColor: const Color(0xFF00FF00)));
final RenderPhysicalModel modelA = getShadow(tester);
expect(modelA.shadowColor, equals(const Color(0xFF00FF00)));
await tester.pumpWidget(buildMaterial(shadowColor: const Color(0xFFFF0000)));
final RenderPhysicalModel modelB = getShadow(tester);
expect(modelB.shadowColor, equals(const Color(0xFF00FF00)));
await tester.pump(const Duration(milliseconds: 1));
final RenderPhysicalModel modelC = getShadow(tester);
expect(modelC.shadowColor.alpha, equals(0xFF));
expect(modelC.shadowColor.red, closeTo(0x00, 1));
expect(modelC.shadowColor.green, closeTo(0xFF, 1));
expect(modelC.shadowColor.blue, equals(0x00));
await tester.pump(kThemeChangeDuration ~/ 2);
final RenderPhysicalModel modelD = getShadow(tester);
expect(modelD.shadowColor.alpha, equals(0xFF));
expect(modelD.shadowColor.red, isNot(closeTo(0x00, 1)));
expect(modelD.shadowColor.green, isNot(closeTo(0xFF, 1)));
expect(modelD.shadowColor.blue, equals(0x00));
await tester.pump(kThemeChangeDuration);
final RenderPhysicalModel modelE = getShadow(tester);
expect(modelE.shadowColor, equals(const Color(0xFFFF0000)));
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment