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].
/// - [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
///
/// 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();
if (widget.type == MaterialType.transparency)
return _clipToShape(shape: shape, contents: contents);
return _transparentInterior(shape: shape, contents: contents);
return new _MaterialInterior(
curve: Curves.fastOutSlowIn,
@ -338,12 +342,14 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
shadowColor: widget.shadowColor,
child: contents,
);
}
static Widget _clipToShape({ShapeBorder shape, Widget contents}) {
static Widget _transparentInterior({ShapeBorder shape, Widget contents}) {
return new ClipPath(
child: new _ShapeBorderPaint(
child: contents,
shape: shape,
),
clipper: new ShapeBorderClipper(
shape: shape,
),
@ -625,10 +631,14 @@ class _MaterialInteriorState extends AnimatedWidgetBaseState<_MaterialInterior>
@override
Widget build(BuildContext context) {
final ShapeBorder shape = _border.evaluate(animation);
return new PhysicalShape(
child: new _ShapeBorderPaint(
child: widget.child,
shape: shape,
),
clipper: new ShapeBorderClipper(
shape: _border.evaluate(animation),
shape: shape,
textDirection: Directionality.of(context)
),
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_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
class NotifyMaterial extends StatelessWidget {
@override
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].
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();
/// Matches objects or functions that assert when they try to paint.
@ -539,14 +539,27 @@ class _TestRecordingCanvasPaintsNothingMatcher extends _TestRecordingCanvasMatch
@override
bool _evaluatePredicates(Iterable<RecordedInvocation> calls, StringBuffer description) {
if (calls.isEmpty)
final Iterable<RecordedInvocation> paintingCalls = _filterCanvasCalls(calls);
if (paintingCalls.isEmpty)
return true;
description.write(
'painted something, the first call having the following stack:\n'
'${calls.first.stackToString(indent: " ")}\n'
'${paintingCalls.first.stackToString(indent: " ")}\n'
);
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 {