mirror of
https://github.com/flutter/flutter
synced 2024-10-14 04:02:56 +00:00
Paint the shape border in the Material widget (#14383)
This commit is contained in:
parent
3e1ef19fcb
commit
4ae1b5f415
|
@ -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: contents,
|
||||
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: widget.child,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue