Paint the shape border in the Material widget (#14383)

This commit is contained in:
amirh 2018-02-07 15:18:04 -08:00 committed by GitHub
parent 3e1ef19fcb
commit 4ae1b5f415
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 126 additions and 9 deletions

View file

@ -130,6 +130,10 @@ abstract class MaterialInkController {
/// rounded edges. The edge radii is specified by [kMaterialEdges]. /// rounded edges. The edge radii is specified by [kMaterialEdges].
/// - [MaterialType.transparency]: the default material shape is a rectangle. /// - [MaterialType.transparency]: the default material shape is a rectangle.
/// ///
/// ## Border
///
/// If [shape] is not null, then its border will also be painted (if any).
///
/// ## Layout change notifications /// ## Layout change notifications
/// ///
/// If the layout changes (e.g. because there's a list on the material, and it's /// If the layout changes (e.g. because there's a list on the material, and it's
@ -327,7 +331,7 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
final ShapeBorder shape = _getShape(); final ShapeBorder shape = _getShape();
if (widget.type == MaterialType.transparency) if (widget.type == MaterialType.transparency)
return _clipToShape(shape: shape, contents: contents); return _transparentInterior(shape: shape, contents: contents);
return new _MaterialInterior( return new _MaterialInterior(
curve: Curves.fastOutSlowIn, curve: Curves.fastOutSlowIn,
@ -338,12 +342,14 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
shadowColor: widget.shadowColor, shadowColor: widget.shadowColor,
child: contents, child: contents,
); );
} }
static Widget _clipToShape({ShapeBorder shape, Widget contents}) { static Widget _transparentInterior({ShapeBorder shape, Widget contents}) {
return new ClipPath( return new ClipPath(
child: contents, child: new _ShapeBorderPaint(
child: contents,
shape: shape,
),
clipper: new ShapeBorderClipper( clipper: new ShapeBorderClipper(
shape: shape, shape: shape,
), ),
@ -625,10 +631,14 @@ class _MaterialInteriorState extends AnimatedWidgetBaseState<_MaterialInterior>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ShapeBorder shape = _border.evaluate(animation);
return new PhysicalShape( return new PhysicalShape(
child: widget.child, child: new _ShapeBorderPaint(
child: widget.child,
shape: shape,
),
clipper: new ShapeBorderClipper( clipper: new ShapeBorderClipper(
shape: _border.evaluate(animation), shape: shape,
textDirection: Directionality.of(context) textDirection: Directionality.of(context)
), ),
elevation: _elevation.evaluate(animation), elevation: _elevation.evaluate(animation),
@ -637,3 +647,37 @@ class _MaterialInteriorState extends AnimatedWidgetBaseState<_MaterialInterior>
); );
} }
} }
class _ShapeBorderPaint extends StatelessWidget {
const _ShapeBorderPaint({
@required this.child,
@required this.shape,
});
final Widget child;
final ShapeBorder shape;
@override
Widget build(BuildContext context) {
return new CustomPaint(
child: child,
foregroundPainter: new _ShapeBorderPainter(shape, Directionality.of(context)),
);
}
}
class _ShapeBorderPainter extends CustomPainter {
_ShapeBorderPainter(this.border, this.textDirection);
final ShapeBorder border;
final TextDirection textDirection;
@override
void paint(Canvas canvas, Size size) {
border.paint(canvas, Offset.zero & size, textDirection: textDirection);
}
@override
bool shouldRepaint(_ShapeBorderPainter oldDelegate) {
return oldDelegate.border != border;
}
}

View file

@ -7,6 +7,8 @@ import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
class NotifyMaterial extends StatelessWidget { class NotifyMaterial extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -404,4 +406,62 @@ void main() {
)); ));
}); });
}); });
group('Border painting', () {
testWidgets('border is painted on physical layers', (WidgetTester tester) async {
final GlobalKey materialKey = new GlobalKey();
await tester.pumpWidget(
new Material(
key: materialKey,
type: MaterialType.button,
child: const SizedBox(width: 100.0, height: 100.0),
color: const Color(0xFF0000FF),
shape: const CircleBorder(
side: const BorderSide(
width: 2.0,
color: const Color(0xFF0000FF),
)
),
)
);
final RenderBox box = tester.renderObject(find.byKey(materialKey));
expect(box, paints..circle());
});
testWidgets('border is painted for transparent material', (WidgetTester tester) async {
final GlobalKey materialKey = new GlobalKey();
await tester.pumpWidget(
new Material(
key: materialKey,
type: MaterialType.transparency,
child: const SizedBox(width: 100.0, height: 100.0),
shape: const CircleBorder(
side: const BorderSide(
width: 2.0,
color: const Color(0xFF0000FF),
)
),
)
);
final RenderBox box = tester.renderObject(find.byKey(materialKey));
expect(box, paints..circle());
});
testWidgets('border is not painted for when border side is none', (WidgetTester tester) async {
final GlobalKey materialKey = new GlobalKey();
await tester.pumpWidget(
new Material(
key: materialKey,
type: MaterialType.transparency,
child: const SizedBox(width: 100.0, height: 100.0),
shape: const CircleBorder(),
)
);
final RenderBox box = tester.renderObject(find.byKey(materialKey));
expect(box, isNot(paints..circle()));
});
});
} }

View file

@ -42,7 +42,7 @@ import 'recording_canvas.dart';
/// To match something which asserts instead of painting, see [paintsAssertion]. /// To match something which asserts instead of painting, see [paintsAssertion].
PaintPattern get paints => new _TestRecordingCanvasPatternMatcher(); PaintPattern get paints => new _TestRecordingCanvasPatternMatcher();
/// Matches objects or functions that paint an empty display list. /// Matches objects or functions that does not paint anything on the canvas.
Matcher get paintsNothing => new _TestRecordingCanvasPaintsNothingMatcher(); Matcher get paintsNothing => new _TestRecordingCanvasPaintsNothingMatcher();
/// Matches objects or functions that assert when they try to paint. /// Matches objects or functions that assert when they try to paint.
@ -539,14 +539,27 @@ class _TestRecordingCanvasPaintsNothingMatcher extends _TestRecordingCanvasMatch
@override @override
bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description) { bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description) {
if (calls.isEmpty) final Iterable<RecordedInvocation> paintingCalls = _filterCanvasCalls(calls);
if (paintingCalls.isEmpty)
return true; return true;
description.write( description.write(
'painted something, the first call having the following stack:\n' 'painted something, the first call having the following stack:\n'
'${calls.first.stackToString(indent: " ")}\n' '${paintingCalls.first.stackToString(indent: " ")}\n'
); );
return false; return false;
} }
static const List<Symbol> _nonPaintingOperations = const <Symbol> [
#save,
#restore,
];
// Filters out canvas calls that are not painting anything.
static Iterable<RecordedInvocation> _filterCanvasCalls(Iterable<RecordedInvocation> canvasCalls) {
return canvasCalls.where((RecordedInvocation canvasCall) =>
!_nonPaintingOperations.contains(canvasCall.invocation.memberName)
);
}
} }
class _TestRecordingCanvasPaintsAssertionMatcher extends Matcher { class _TestRecordingCanvasPaintsAssertionMatcher extends Matcher {