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].
|
/// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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()));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue