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:
46a3d26acb
This commit is contained in:
@@ -1 +1 @@
|
||||
b530d67675a5aa9c5458b93019ce91e20ad88758
|
||||
46a3d26acbb1b0d72b6b02c30f03b9dbda7d5bdf
|
||||
|
||||
@@ -66,17 +66,20 @@ import 'theme.dart';
|
||||
class Card extends StatelessWidget {
|
||||
/// 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({
|
||||
Key key,
|
||||
this.color,
|
||||
this.elevation,
|
||||
this.shape,
|
||||
this.borderOnForeground = true,
|
||||
this.margin,
|
||||
this.clipBehavior,
|
||||
this.child,
|
||||
this.semanticContainer = true,
|
||||
}) : assert(elevation == null || elevation >= 0.0),
|
||||
assert(borderOnForeground != null),
|
||||
super(key: key);
|
||||
|
||||
/// The card's background color.
|
||||
@@ -105,6 +108,12 @@ class Card extends StatelessWidget {
|
||||
/// circular corner radius of 4.0.
|
||||
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}
|
||||
/// If this property is null then [ThemeData.cardTheme.clipBehavior] is used.
|
||||
/// If that's null then the behavior will be [Clip.none].
|
||||
@@ -155,6 +164,7 @@ class Card extends StatelessWidget {
|
||||
shape: shape ?? cardTheme.shape ?? const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4.0)),
|
||||
),
|
||||
borderOnForeground: borderOnForeground,
|
||||
clipBehavior: clipBehavior ?? cardTheme.clipBehavior ?? _defaultClipBehavior,
|
||||
child: Semantics(
|
||||
explicitChildNodes: !semanticContainer,
|
||||
|
||||
@@ -155,8 +155,9 @@ abstract class MaterialInkController {
|
||||
class Material extends StatefulWidget {
|
||||
/// Creates a piece of material.
|
||||
///
|
||||
/// The [type], [elevation], [shadowColor], and [animationDuration] arguments
|
||||
/// must not be null. Additionally, [elevation] must be non-negative.
|
||||
/// The [type], [elevation], [shadowColor], [borderOnForeground] and
|
||||
/// [animationDuration] arguments must not be null. Additionally, [elevation]
|
||||
/// must be non-negative.
|
||||
///
|
||||
/// If a [shape] is specified, then the [borderRadius] property must be
|
||||
/// null and the [type] property must not be [MaterialType.circle]. If the
|
||||
@@ -172,6 +173,7 @@ class Material extends StatefulWidget {
|
||||
this.textStyle,
|
||||
this.borderRadius,
|
||||
this.shape,
|
||||
this.borderOnForeground = true,
|
||||
this.clipBehavior = Clip.none,
|
||||
this.animationDuration = kThemeChangeDuration,
|
||||
this.child,
|
||||
@@ -182,6 +184,7 @@ class Material extends StatefulWidget {
|
||||
assert(animationDuration != null),
|
||||
assert(!(identical(type, MaterialType.circle) && (borderRadius != null || shape != null))),
|
||||
assert(clipBehavior != null),
|
||||
assert(borderOnForeground != null),
|
||||
super(key: key);
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
@@ -234,6 +237,12 @@ class Material extends StatefulWidget {
|
||||
/// zero.
|
||||
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}
|
||||
/// The content will be clipped (or not) according to this option.
|
||||
///
|
||||
@@ -282,6 +291,7 @@ class Material extends StatefulWidget {
|
||||
properties.add(DiagnosticsProperty<Color>('shadowColor', shadowColor, defaultValue: const Color(0xFF000000)));
|
||||
textStyle?.debugFillProperties(properties, prefix: 'textStyle.');
|
||||
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<bool>('borderOnForeground', borderOnForeground, defaultValue: true));
|
||||
properties.add(EnumProperty<BorderRadius>('borderRadius', borderRadius, defaultValue: null));
|
||||
}
|
||||
|
||||
@@ -370,6 +380,7 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
|
||||
curve: Curves.fastOutSlowIn,
|
||||
duration: widget.animationDuration,
|
||||
shape: shape,
|
||||
borderOnForeground: widget.borderOnForeground,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
elevation: widget.elevation,
|
||||
color: backgroundColor,
|
||||
@@ -617,6 +628,7 @@ class _MaterialInterior extends ImplicitlyAnimatedWidget {
|
||||
Key key,
|
||||
@required this.child,
|
||||
@required this.shape,
|
||||
this.borderOnForeground = true,
|
||||
this.clipBehavior = Clip.none,
|
||||
@required this.elevation,
|
||||
@required this.color,
|
||||
@@ -642,6 +654,12 @@ class _MaterialInterior extends ImplicitlyAnimatedWidget {
|
||||
/// determines the physical 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}
|
||||
final Clip clipBehavior;
|
||||
|
||||
@@ -689,6 +707,7 @@ class _MaterialInteriorState extends AnimatedWidgetBaseState<_MaterialInterior>
|
||||
child: _ShapeBorderPaint(
|
||||
child: widget.child,
|
||||
shape: shape,
|
||||
borderOnForeground: widget.borderOnForeground,
|
||||
),
|
||||
clipper: ShapeBorderClipper(
|
||||
shape: shape,
|
||||
@@ -706,16 +725,19 @@ class _ShapeBorderPaint extends StatelessWidget {
|
||||
const _ShapeBorderPaint({
|
||||
@required this.child,
|
||||
@required this.shape,
|
||||
this.borderOnForeground = true,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final ShapeBorder shape;
|
||||
final bool borderOnForeground;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
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 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
@@ -543,5 +545,86 @@ void main() {
|
||||
final RenderBox box = tester.renderObject(find.byKey(materialKey));
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user