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
This commit is contained in:
parent
91bd9bc4f8
commit
dfd1ffa7c5
@ -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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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> {
|
|||||||
///
|
///
|
||||||
/// 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> {
|
|||||||
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> {
|
|||||||
);
|
);
|
||||||
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 {
|
|||||||
///
|
///
|
||||||
/// 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 {
|
|||||||
/// 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 {
|
|||||||
..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 {
|
|||||||
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
|
|||||||
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 {
|
|||||||
@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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
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
|
|||||||
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
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
|||||||
// 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() {
|
|||||||
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)));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user