Skip to content

Commit

Permalink
Add Material/Card borderOnForeground flag to allow border to be paint…
Browse files Browse the repository at this point in the history
…ed behind the child widget (flutter#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:
flutter/goldens@46a3d26
  • Loading branch information
rami-a authored Jan 30, 2019
1 parent 3d2f984 commit 327e3ef
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 5 deletions.
2 changes: 1 addition & 1 deletion bin/internal/goldens.version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
b530d67675a5aa9c5458b93019ce91e20ad88758
46a3d26acbb1b0d72b6b02c30f03b9dbda7d5bdf
12 changes: 11 additions & 1 deletion packages/flutter/lib/src/material/card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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].
Expand Down Expand Up @@ -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,
Expand Down
28 changes: 25 additions & 3 deletions packages/flutter/lib/src/material/material.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -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));
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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;

Expand Down Expand Up @@ -689,6 +707,7 @@ class _MaterialInteriorState extends AnimatedWidgetBaseState<_MaterialInterior>
child: _ShapeBorderPaint(
child: widget.child,
shape: shape,
borderOnForeground: widget.borderOnForeground,
),
clipper: ShapeBorderClipper(
shape: shape,
Expand All @@ -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,
);
}
}
Expand Down
83 changes: 83 additions & 0 deletions packages/flutter/test/material/material_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
);
});
});
}

0 comments on commit 327e3ef

Please sign in to comment.