Unverified Commit 327e3eff authored by rami-a's avatar rami-a Committed by GitHub

Add Material/Card borderOnForeground flag to allow border to be painted behind...

Add Material/Card borderOnForeground flag to allow border to be painted behind the child widget (#27297)

In certain situations, a developer may require the border of a Material to be painted behind its child. For example a Card widget that has a full width image across the top half. In that scenario, the image should ideally be painted above the border with regards to z-position.

This change exposes a flag on Material widget to achieve this behavior. Additionally, the same flag is exposed on Card widget to allow the Card widget to pass this down to its Material.

I added a couple golden tests to verify this new behavior. Goldens are here:
https://github.com/flutter/goldens/commit/46a3d26acbb1b0d72b6b02c30f03b9dbda7d5bdf
parent 3d2f9849
b530d67675a5aa9c5458b93019ce91e20ad88758 46a3d26acbb1b0d72b6b02c30f03b9dbda7d5bdf
...@@ -66,17 +66,20 @@ import 'theme.dart'; ...@@ -66,17 +66,20 @@ import 'theme.dart';
class Card extends StatelessWidget { class Card extends StatelessWidget {
/// Creates a material design card. /// Creates a material design card.
/// ///
/// The [elevation] must be null or non-negative. /// The [elevation] must be null or non-negative. The [borderOnForeground]
/// must not be null.
const Card({ const Card({
Key key, Key key,
this.color, this.color,
this.elevation, this.elevation,
this.shape, this.shape,
this.borderOnForeground = true,
this.margin, this.margin,
this.clipBehavior, this.clipBehavior,
this.child, this.child,
this.semanticContainer = true, this.semanticContainer = true,
}) : assert(elevation == null || elevation >= 0.0), }) : assert(elevation == null || elevation >= 0.0),
assert(borderOnForeground != null),
super(key: key); super(key: key);
/// The card's background color. /// The card's background color.
...@@ -105,6 +108,12 @@ class Card extends StatelessWidget { ...@@ -105,6 +108,12 @@ class Card extends StatelessWidget {
/// circular corner radius of 4.0. /// circular corner radius of 4.0.
final ShapeBorder shape; final ShapeBorder shape;
/// Whether to paint the [shape] border in front of the [child].
///
/// The default value is true.
/// If false, the border will be painted behind the [child].
final bool borderOnForeground;
/// {@macro flutter.widgets.Clip} /// {@macro flutter.widgets.Clip}
/// If this property is null then [ThemeData.cardTheme.clipBehavior] is used. /// If this property is null then [ThemeData.cardTheme.clipBehavior] is used.
/// If that's null then the behavior will be [Clip.none]. /// If that's null then the behavior will be [Clip.none].
...@@ -155,6 +164,7 @@ class Card extends StatelessWidget { ...@@ -155,6 +164,7 @@ class Card extends StatelessWidget {
shape: shape ?? cardTheme.shape ?? const RoundedRectangleBorder( shape: shape ?? cardTheme.shape ?? const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4.0)), borderRadius: BorderRadius.all(Radius.circular(4.0)),
), ),
borderOnForeground: borderOnForeground,
clipBehavior: clipBehavior ?? cardTheme.clipBehavior ?? _defaultClipBehavior, clipBehavior: clipBehavior ?? cardTheme.clipBehavior ?? _defaultClipBehavior,
child: Semantics( child: Semantics(
explicitChildNodes: !semanticContainer, explicitChildNodes: !semanticContainer,
......
...@@ -155,8 +155,9 @@ abstract class MaterialInkController { ...@@ -155,8 +155,9 @@ abstract class MaterialInkController {
class Material extends StatefulWidget { class Material extends StatefulWidget {
/// Creates a piece of material. /// Creates a piece of material.
/// ///
/// The [type], [elevation], [shadowColor], and [animationDuration] arguments /// The [type], [elevation], [shadowColor], [borderOnForeground] and
/// must not be null. Additionally, [elevation] must be non-negative. /// [animationDuration] arguments must not be null. Additionally, [elevation]
/// must be non-negative.
/// ///
/// If a [shape] is specified, then the [borderRadius] property must be /// If a [shape] is specified, then the [borderRadius] property must be
/// null and the [type] property must not be [MaterialType.circle]. If the /// null and the [type] property must not be [MaterialType.circle]. If the
...@@ -172,6 +173,7 @@ class Material extends StatefulWidget { ...@@ -172,6 +173,7 @@ class Material extends StatefulWidget {
this.textStyle, this.textStyle,
this.borderRadius, this.borderRadius,
this.shape, this.shape,
this.borderOnForeground = true,
this.clipBehavior = Clip.none, this.clipBehavior = Clip.none,
this.animationDuration = kThemeChangeDuration, this.animationDuration = kThemeChangeDuration,
this.child, this.child,
...@@ -182,6 +184,7 @@ class Material extends StatefulWidget { ...@@ -182,6 +184,7 @@ class Material extends StatefulWidget {
assert(animationDuration != null), assert(animationDuration != null),
assert(!(identical(type, MaterialType.circle) && (borderRadius != null || shape != null))), assert(!(identical(type, MaterialType.circle) && (borderRadius != null || shape != null))),
assert(clipBehavior != null), assert(clipBehavior != null),
assert(borderOnForeground != null),
super(key: key); super(key: key);
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
...@@ -234,6 +237,12 @@ class Material extends StatefulWidget { ...@@ -234,6 +237,12 @@ class Material extends StatefulWidget {
/// zero. /// zero.
final ShapeBorder shape; final ShapeBorder shape;
/// Whether to paint the [shape] border in front of the [child].
///
/// The default value is true.
/// If false, the border will be painted behind the [child].
final bool borderOnForeground;
/// {@template flutter.widgets.Clip} /// {@template flutter.widgets.Clip}
/// The content will be clipped (or not) according to this option. /// The content will be clipped (or not) according to this option.
/// ///
...@@ -282,6 +291,7 @@ class Material extends StatefulWidget { ...@@ -282,6 +291,7 @@ class Material extends StatefulWidget {
properties.add(DiagnosticsProperty<Color>('shadowColor', shadowColor, defaultValue: const Color(0xFF000000))); properties.add(DiagnosticsProperty<Color>('shadowColor', shadowColor, defaultValue: const Color(0xFF000000)));
textStyle?.debugFillProperties(properties, prefix: 'textStyle.'); textStyle?.debugFillProperties(properties, prefix: 'textStyle.');
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null)); properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('borderOnForeground', borderOnForeground, defaultValue: true));
properties.add(EnumProperty<BorderRadius>('borderRadius', borderRadius, defaultValue: null)); properties.add(EnumProperty<BorderRadius>('borderRadius', borderRadius, defaultValue: null));
} }
...@@ -370,6 +380,7 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin { ...@@ -370,6 +380,7 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
curve: Curves.fastOutSlowIn, curve: Curves.fastOutSlowIn,
duration: widget.animationDuration, duration: widget.animationDuration,
shape: shape, shape: shape,
borderOnForeground: widget.borderOnForeground,
clipBehavior: widget.clipBehavior, clipBehavior: widget.clipBehavior,
elevation: widget.elevation, elevation: widget.elevation,
color: backgroundColor, color: backgroundColor,
...@@ -617,6 +628,7 @@ class _MaterialInterior extends ImplicitlyAnimatedWidget { ...@@ -617,6 +628,7 @@ class _MaterialInterior extends ImplicitlyAnimatedWidget {
Key key, Key key,
@required this.child, @required this.child,
@required this.shape, @required this.shape,
this.borderOnForeground = true,
this.clipBehavior = Clip.none, this.clipBehavior = Clip.none,
@required this.elevation, @required this.elevation,
@required this.color, @required this.color,
...@@ -642,6 +654,12 @@ class _MaterialInterior extends ImplicitlyAnimatedWidget { ...@@ -642,6 +654,12 @@ class _MaterialInterior extends ImplicitlyAnimatedWidget {
/// determines the physical shape. /// determines the physical shape.
final ShapeBorder shape; final ShapeBorder shape;
/// Whether to paint the border in front of the child.
///
/// The default value is true.
/// If false, the border will be painted behind the child.
final bool borderOnForeground;
/// {@macro flutter.widgets.Clip} /// {@macro flutter.widgets.Clip}
final Clip clipBehavior; final Clip clipBehavior;
...@@ -689,6 +707,7 @@ class _MaterialInteriorState extends AnimatedWidgetBaseState<_MaterialInterior> ...@@ -689,6 +707,7 @@ class _MaterialInteriorState extends AnimatedWidgetBaseState<_MaterialInterior>
child: _ShapeBorderPaint( child: _ShapeBorderPaint(
child: widget.child, child: widget.child,
shape: shape, shape: shape,
borderOnForeground: widget.borderOnForeground,
), ),
clipper: ShapeBorderClipper( clipper: ShapeBorderClipper(
shape: shape, shape: shape,
...@@ -706,16 +725,19 @@ class _ShapeBorderPaint extends StatelessWidget { ...@@ -706,16 +725,19 @@ class _ShapeBorderPaint extends StatelessWidget {
const _ShapeBorderPaint({ const _ShapeBorderPaint({
@required this.child, @required this.child,
@required this.shape, @required this.shape,
this.borderOnForeground = true,
}); });
final Widget child; final Widget child;
final ShapeBorder shape; final ShapeBorder shape;
final bool borderOnForeground;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomPaint( return CustomPaint(
child: child, child: child,
foregroundPainter: _ShapeBorderPainter(shape, Directionality.of(context)), painter: borderOnForeground ? null : _ShapeBorderPainter(shape, Directionality.of(context)),
foregroundPainter: borderOnForeground ? _ShapeBorderPainter(shape, Directionality.of(context)) : null,
); );
} }
} }
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:io' show Platform;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -543,5 +545,86 @@ void main() { ...@@ -543,5 +545,86 @@ void main() {
final RenderBox box = tester.renderObject(find.byKey(materialKey)); final RenderBox box = tester.renderObject(find.byKey(materialKey));
expect(box, isNot(paints..circle())); expect(box, isNot(paints..circle()));
}); });
testWidgets('border is painted above child by default', (WidgetTester tester) async {
final Key painterKey = UniqueKey();
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: RepaintBoundary(
key: painterKey,
child: Card(
child: SizedBox(
width: 200,
height: 300,
child: Material(
clipBehavior: Clip.hardEdge,
elevation: 0,
shape: RoundedRectangleBorder(
side: const BorderSide(color: Colors.grey, width: 6),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: <Widget>[
Container(
color: Colors.green,
height: 150,
)
],
),
),
),
)
)
),
));
await expectLater(
find.byKey(painterKey),
matchesGoldenFile('material.border_paint_above.png'),
skip: !Platform.isLinux,
);
});
testWidgets('border is painted below child when specified', (WidgetTester tester) async {
final Key painterKey = UniqueKey();
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: RepaintBoundary(
key: painterKey,
child: Card(
child: SizedBox(
width: 200,
height: 300,
child: Material(
clipBehavior: Clip.hardEdge,
elevation: 0,
shape: RoundedRectangleBorder(
side: const BorderSide(color: Colors.grey, width: 6),
borderRadius: BorderRadius.circular(8),
),
borderOnForeground: false,
child: Column(
children: <Widget>[
Container(
color: Colors.green,
height: 150,
)
],
),
),
),
)
)
),
));
await expectLater(
find.byKey(painterKey),
matchesGoldenFile('material.border_paint_below.png'),
skip: !Platform.isLinux,
);
});
}); });
} }
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