blob: 7f8d10585e01e724cf333f743ef534b34f7e2a13 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'constants.dart';
import 'elevation_overlay.dart';
import 'theme.dart';
/// Signature for the callback used by ink effects to obtain the rectangle for the effect.
///
/// Used by [InkHighlight] and [InkSplash], for example.
typedef RectCallback = Rect Function();
/// The various kinds of material in material design. Used to
/// configure the default behavior of [Material] widgets.
///
/// See also:
///
/// * [Material], in particular [Material.type].
/// * [kMaterialEdges]
enum MaterialType {
/// Rectangle using default theme canvas color.
canvas,
/// Rounded edges, card theme color.
card,
/// A circle, no color by default (used for floating action buttons).
circle,
/// Rounded edges, no color by default (used for [MaterialButton] buttons).
button,
/// A transparent piece of material that draws ink splashes and highlights.
///
/// While the material metaphor describes child widgets as printed on the
/// material itself and do not hide ink effects, in practice the [Material]
/// widget draws child widgets on top of the ink effects.
/// A [Material] with type transparency can be placed on top of opaque widgets
/// to show ink effects on top of them.
///
/// Prefer using the [Ink] widget for showing ink effects on top of opaque
/// widgets.
transparency
}
/// The border radii used by the various kinds of material in material design.
///
/// See also:
///
/// * [MaterialType]
/// * [Material]
final Map<MaterialType, BorderRadius> kMaterialEdges = <MaterialType, BorderRadius>{
MaterialType.canvas: null,
MaterialType.card: BorderRadius.circular(2.0),
MaterialType.circle: null,
MaterialType.button: BorderRadius.circular(2.0),
MaterialType.transparency: null,
};
/// An interface for creating [InkSplash]s and [InkHighlight]s on a material.
///
/// Typically obtained via [Material.of].
abstract class MaterialInkController {
/// The color of the material.
Color get color;
/// The ticker provider used by the controller.
///
/// Ink features that are added to this controller with [addInkFeature] should
/// use this vsync to drive their animations.
TickerProvider get vsync;
/// Add an [InkFeature], such as an [InkSplash] or an [InkHighlight].
///
/// The ink feature will paint as part of this controller.
void addInkFeature(InkFeature feature);
/// Notifies the controller that one of its ink features needs to repaint.
void markNeedsPaint();
}
/// A piece of material.
///
/// The Material widget is responsible for:
///
/// 1. Clipping: If [clipBehavior] is not [Clip.none], Material clips its widget
/// sub-tree to the shape specified by [shape], [type], and [borderRadius].
/// By default, [clipBehavior] is [Clip.none] for performance considerations.
/// 2. Elevation: Material elevates its widget sub-tree on the Z axis by
/// [elevation] pixels, and draws the appropriate shadow.
/// 3. Ink effects: Material shows ink effects implemented by [InkFeature]s
/// like [InkSplash] and [InkHighlight] below its children.
///
/// ## The Material Metaphor
///
/// Material is the central metaphor in material design. Each piece of material
/// exists at a given elevation, which influences how that piece of material
/// visually relates to other pieces of material and how that material casts
/// shadows.
///
/// Most user interface elements are either conceptually printed on a piece of
/// material or themselves made of material. Material reacts to user input using
/// [InkSplash] and [InkHighlight] effects. To trigger a reaction on the
/// material, use a [MaterialInkController] obtained via [Material.of].
///
/// In general, the features of a [Material] should not change over time (e.g. a
/// [Material] should not change its [color], [shadowColor] or [type]).
/// Changes to [elevation] and [shadowColor] are animated for [animationDuration].
/// Changes to [shape] are animated if [type] is not [MaterialType.transparency]
/// and [ShapeBorder.lerp] between the previous and next [shape] values is
/// supported. Shape changes are also animated for [animationDuration].
///
///
/// ## Shape
///
/// The shape for material is determined by [shape], [type], and [borderRadius].
///
/// - If [shape] is non null, it determines the shape.
/// - If [shape] is null and [borderRadius] is non null, the shape is a
/// rounded rectangle, with corners specified by [borderRadius].
/// - If [shape] and [borderRadius] are null, [type] determines the
/// shape as follows:
/// - [MaterialType.canvas]: the default material shape is a rectangle.
/// - [MaterialType.card]: the default material shape is a rectangle with
/// rounded edges. The edge radii is specified by [kMaterialEdges].
/// - [MaterialType.circle]: the default material shape is a circle.
/// - [MaterialType.button]: the default material shape is a rectangle with
/// 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
/// been scrolled), a [LayoutChangedNotification] must be dispatched at the
/// relevant subtree. This in particular means that transitions (e.g.
/// [SlideTransition]) should not be placed inside [Material] widgets so as to
/// move subtrees that contain [InkResponse]s, [InkWell]s, [Ink]s, or other
/// widgets that use the [InkFeature] mechanism. Otherwise, in-progress ink
/// features (e.g., ink splashes and ink highlights) won't move to account for
/// the new layout.
///
/// See also:
///
/// * [MergeableMaterial], a piece of material that can split and re-merge.
/// * [Card], a wrapper for a [Material] of [type] [MaterialType.card].
/// * <https://material.io/design/>
class Material extends StatefulWidget {
/// Creates a piece of material.
///
/// The [type], [elevation], [shadowColor], [borderOnForeground],
/// [clipBehavior], 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
/// [borderRadius] is specified, then the [type] property must not be
/// [MaterialType.circle]. In both cases, these restrictions are intended to
/// catch likely errors.
const Material({
Key key,
this.type = MaterialType.canvas,
this.elevation = 0.0,
this.color,
this.shadowColor = const Color(0xFF000000),
this.textStyle,
this.borderRadius,
this.shape,
this.borderOnForeground = true,
this.clipBehavior = Clip.none,
this.animationDuration = kThemeChangeDuration,
this.child,
}) : assert(type != null),
assert(elevation != null && elevation >= 0.0),
assert(shadowColor != null),
assert(!(shape != null && borderRadius != null)),
assert(animationDuration != null),
assert(!(identical(type, MaterialType.circle) && (borderRadius != null || shape != null))),
assert(borderOnForeground != null),
assert(clipBehavior != null),
super(key: key);
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.child}
final Widget child;
/// The kind of material to show (e.g., card or canvas). This
/// affects the shape of the widget, the roundness of its corners if
/// the shape is rectangular, and the default color.
final MaterialType type;
/// {@template flutter.material.material.elevation}
/// The z-coordinate at which to place this material relative to its parent.
///
/// This controls the size of the shadow below the material and the opacity
/// of the elevation overlay color if it is applied.
///
/// If this is non-zero, the contents of the material are clipped, because the
/// widget conceptually defines an independent printed piece of material.
///
/// Defaults to 0. Changing this value will cause the shadow and the elevation
/// overlay to animate over [animationDuration].
///
/// The value is non-negative.
///
/// See also:
///
/// * [ThemeData.applyElevationOverlayColor] which controls the whether
/// an overlay color will be applied to indicate elevation.
/// * [color] which may have an elevation overlay applied.
///
/// {@endtemplate}
final double elevation;
/// The color to paint the material.
///
/// Must be opaque. To create a transparent piece of material, use
/// [MaterialType.transparency].
///
/// To support dark themes, if the surrounding
/// [ThemeData.applyElevationOverlayColor] is true then a semi-transparent
/// overlay color will be composited on top this color to indicate
/// the elevation.
///
/// By default, the color is derived from the [type] of material.
final Color color;
/// The color to paint the shadow below the material.
///
/// Defaults to fully opaque black.
final Color shadowColor;
/// The typographical style to use for text within this material.
final TextStyle textStyle;
/// Defines the material's shape as well its shadow.
///
/// If shape is non null, the [borderRadius] is ignored and the material's
/// clip boundary and shadow are defined by the shape.
///
/// A shadow is only displayed if the [elevation] is greater than
/// 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.
///
/// See the enum [Clip] for details of all possible options and their common
/// use cases.
/// {@endtemplate}
///
/// Defaults to [Clip.none], and must not be null.
final Clip clipBehavior;
/// Defines the duration of animated changes for [shape], [elevation],
/// [shadowColor] and the elevation overlay if it is applied.
///
/// The default value is [kThemeChangeDuration].
final Duration animationDuration;
/// If non-null, the corners of this box are rounded by this
/// [BorderRadiusGeometry] value.
///
/// Otherwise, the corners specified for the current [type] of material are
/// used.
///
/// If [shape] is non null then the border radius is ignored.
///
/// Must be null if [type] is [MaterialType.circle].
final BorderRadiusGeometry borderRadius;
/// The ink controller from the closest instance of this class that
/// encloses the given context.
///
/// Typical usage is as follows:
///
/// ```dart
/// MaterialInkController inkController = Material.of(context);
/// ```
static MaterialInkController of(BuildContext context) {
final _RenderInkFeatures result = context.findAncestorRenderObjectOfType<_RenderInkFeatures>();
return result;
}
@override
_MaterialState createState() => _MaterialState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(EnumProperty<MaterialType>('type', type));
properties.add(DoubleProperty('elevation', elevation, defaultValue: 0.0));
properties.add(ColorProperty('color', color, defaultValue: null));
properties.add(ColorProperty('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(DiagnosticsProperty<BorderRadiusGeometry>('borderRadius', borderRadius, defaultValue: null));
}
/// The default radius of an ink splash in logical pixels.
static const double defaultSplashRadius = 35.0;
}
class _MaterialState extends State<Material> with TickerProviderStateMixin {
final GlobalKey _inkFeatureRenderer = GlobalKey(debugLabel: 'ink renderer');
Color _getBackgroundColor(BuildContext context) {
final ThemeData theme = Theme.of(context);
Color color = widget.color;
if (color == null) {
switch (widget.type) {
case MaterialType.canvas:
color = theme.canvasColor;
break;
case MaterialType.card:
color = theme.cardColor;
break;
default:
break;
}
}
return color;
}
@override
Widget build(BuildContext context) {
final Color backgroundColor = _getBackgroundColor(context);
assert(
backgroundColor != null || widget.type == MaterialType.transparency,
'If Material type is not MaterialType.transparency, a color must '
'either be passed in through the `color` property, or be defined '
'in the theme (ex. canvasColor != null if type is set to '
'MaterialType.canvas)'
);
Widget contents = widget.child;
if (contents != null) {
contents = AnimatedDefaultTextStyle(
style: widget.textStyle ?? Theme.of(context).textTheme.bodyText2,
duration: widget.animationDuration,
child: contents,
);
}
contents = NotificationListener<LayoutChangedNotification>(
onNotification: (LayoutChangedNotification notification) {
final _RenderInkFeatures renderer = _inkFeatureRenderer.currentContext.findRenderObject() as _RenderInkFeatures;
renderer._didChangeLayout();
return false;
},
child: _InkFeatures(
key: _inkFeatureRenderer,
color: backgroundColor,
child: contents,
vsync: this,
),
);
// PhysicalModel has a temporary workaround for a performance issue that
// speeds up rectangular non transparent material (the workaround is to
// skip the call to ui.Canvas.saveLayer if the border radius is 0).
// Until the saveLayer performance issue is resolved, we're keeping this
// special case here for canvas material type that is using the default
// shape (rectangle). We could go down this fast path for explicitly
// specified rectangles (e.g shape RoundedRectangleBorder with radius 0, but
// we choose not to as we want the change from the fast-path to the
// slow-path to be noticeable in the construction site of Material.
if (widget.type == MaterialType.canvas && widget.shape == null && widget.borderRadius == null) {
return AnimatedPhysicalModel(
curve: Curves.fastOutSlowIn,
duration: widget.animationDuration,
shape: BoxShape.rectangle,
clipBehavior: widget.clipBehavior,
borderRadius: BorderRadius.zero,
elevation: widget.elevation,
color: ElevationOverlay.applyOverlay(context, backgroundColor, widget.elevation),
shadowColor: widget.shadowColor,
animateColor: false,
child: contents,
);
}
final ShapeBorder shape = _getShape();
if (widget.type == MaterialType.transparency) {
return _transparentInterior(
context: context,
shape: shape,
clipBehavior: widget.clipBehavior,
contents: contents,
);
}
return _MaterialInterior(
curve: Curves.fastOutSlowIn,
duration: widget.animationDuration,
shape: shape,
borderOnForeground: widget.borderOnForeground,
clipBehavior: widget.clipBehavior,
elevation: widget.elevation,
color: backgroundColor,
shadowColor: widget.shadowColor,
child: contents,
);
}
static Widget _transparentInterior({
@required BuildContext context,
@required ShapeBorder shape,
@required Clip clipBehavior,
@required Widget contents,
}) {
final _ShapeBorderPaint child = _ShapeBorderPaint(
child: contents,
shape: shape,
);
if (clipBehavior == Clip.none) {
return child;
}
return ClipPath(
child: child,
clipper: ShapeBorderClipper(
shape: shape,
textDirection: Directionality.of(context),
),
clipBehavior: clipBehavior,
);
}
// Determines the shape for this Material.
//
// If a shape was specified, it will determine the shape.
// If a borderRadius was specified, the shape is a rounded
// rectangle.
// Otherwise, the shape is determined by the widget type as described in the
// Material class documentation.
ShapeBorder _getShape() {
if (widget.shape != null)
return widget.shape;
if (widget.borderRadius != null)
return RoundedRectangleBorder(borderRadius: widget.borderRadius);
switch (widget.type) {
case MaterialType.canvas:
case MaterialType.transparency:
return const RoundedRectangleBorder();
case MaterialType.card:
case MaterialType.button:
return RoundedRectangleBorder(
borderRadius: widget.borderRadius ?? kMaterialEdges[widget.type],
);
case MaterialType.circle:
return const CircleBorder();
}
return const RoundedRectangleBorder();
}
}
class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController {
_RenderInkFeatures({
RenderBox child,
@required this.vsync,
this.color,
}) : assert(vsync != null),
super(child);
// This class should exist in a 1:1 relationship with a MaterialState object,
// since there's no current support for dynamically changing the ticker
// provider.
@override
final TickerProvider vsync;
// This is here to satisfy the MaterialInkController contract.
// The actual painting of this color is done by a Container in the
// MaterialState build method.
@override
Color color;
List<InkFeature> _inkFeatures;
@override
void addInkFeature(InkFeature feature) {
assert(!feature._debugDisposed);
assert(feature._controller == this);
_inkFeatures ??= <InkFeature>[];
assert(!_inkFeatures.contains(feature));
_inkFeatures.add(feature);
markNeedsPaint();
}
void _removeFeature(InkFeature feature) {
assert(_inkFeatures != null);
_inkFeatures.remove(feature);
markNeedsPaint();
}
void _didChangeLayout() {
if (_inkFeatures != null && _inkFeatures.isNotEmpty)
markNeedsPaint();
}
@override
bool hitTestSelf(Offset position) => true;
@override
void paint(PaintingContext context, Offset offset) {
if (_inkFeatures != null && _inkFeatures.isNotEmpty) {
final Canvas canvas = context.canvas;
canvas.save();
canvas.translate(offset.dx, offset.dy);
canvas.clipRect(Offset.zero & size);
for (final InkFeature inkFeature in _inkFeatures)
inkFeature._paint(canvas);
canvas.restore();
}
super.paint(context, offset);
}
}
class _InkFeatures extends SingleChildRenderObjectWidget {
const _InkFeatures({
Key key,
this.color,
@required this.vsync,
Widget child,
}) : super(key: key, child: child);
// This widget must be owned by a MaterialState, which must be provided as the vsync.
// This relationship must be 1:1 and cannot change for the lifetime of the MaterialState.
final Color color;
final TickerProvider vsync;
@override
_RenderInkFeatures createRenderObject(BuildContext context) {
return _RenderInkFeatures(
color: color,
vsync: vsync,
);
}
@override
void updateRenderObject(BuildContext context, _RenderInkFeatures renderObject) {
renderObject.color = color;
assert(vsync == renderObject.vsync);
}
}
/// A visual reaction on a piece of [Material].
///
/// To add an ink feature to a piece of [Material], obtain the
/// [MaterialInkController] via [Material.of] and call
/// [MaterialInkController.addInkFeature].
abstract class InkFeature {
/// Initializes fields for subclasses.
InkFeature({
@required MaterialInkController controller,
@required this.referenceBox,
this.onRemoved,
}) : assert(controller != null),
assert(referenceBox != null),
_controller = controller as _RenderInkFeatures;
/// The [MaterialInkController] associated with this [InkFeature].
///
/// Typically used by subclasses to call
/// [MaterialInkController.markNeedsPaint] when they need to repaint.
MaterialInkController get controller => _controller;
final _RenderInkFeatures _controller;
/// The render box whose visual position defines the frame of reference for this ink feature.
final RenderBox referenceBox;
/// Called when the ink feature is no longer visible on the material.
final VoidCallback onRemoved;
bool _debugDisposed = false;
/// Free up the resources associated with this ink feature.
@mustCallSuper
void dispose() {
assert(!_debugDisposed);
assert(() {
_debugDisposed = true;
return true;
}());
_controller._removeFeature(this);
if (onRemoved != null)
onRemoved();
}
void _paint(Canvas canvas) {
assert(referenceBox.attached);
assert(!_debugDisposed);
// find the chain of renderers from us to the feature's referenceBox
final List<RenderObject> descendants = <RenderObject>[referenceBox];
RenderObject node = referenceBox;
while (node != _controller) {
node = node.parent as RenderObject;
assert(node != null);
descendants.add(node);
}
// determine the transform that gets our coordinate system to be like theirs
final Matrix4 transform = Matrix4.identity();
assert(descendants.length >= 2);
for (int index = descendants.length - 1; index > 0; index -= 1)
descendants[index].applyPaintTransform(descendants[index - 1], transform);
paintFeature(canvas, transform);
}
/// Override this method to paint the ink feature.
///
/// The transform argument gives the coordinate conversion from the coordinate
/// system of the canvas to the coordinate system of the [referenceBox].
@protected
void paintFeature(Canvas canvas, Matrix4 transform);
@override
String toString() => describeIdentity(this);
}
/// An interpolation between two [ShapeBorder]s.
///
/// This class specializes the interpolation of [Tween] to use [ShapeBorder.lerp].
class ShapeBorderTween extends Tween<ShapeBorder> {
/// Creates a [ShapeBorder] tween.
///
/// the [begin] and [end] properties may be null; see [ShapeBorder.lerp] for
/// the null handling semantics.
ShapeBorderTween({ShapeBorder begin, ShapeBorder end}) : super(begin: begin, end: end);
/// Returns the value this tween has at the given animation clock value.
@override
ShapeBorder lerp(double t) {
return ShapeBorder.lerp(begin, end, t);
}
}
/// The interior of non-transparent material.
///
/// Animates [elevation], [shadowColor], and [shape].
class _MaterialInterior extends ImplicitlyAnimatedWidget {
/// Creates a const instance of [_MaterialInterior].
///
/// The [child], [shape], [clipBehavior], [color], and [shadowColor] arguments
/// must not be null. The [elevation] must be specified and greater than or
/// equal to zero.
const _MaterialInterior({
Key key,
@required this.child,
@required this.shape,
this.borderOnForeground = true,
this.clipBehavior = Clip.none,
@required this.elevation,
@required this.color,
@required this.shadowColor,
Curve curve = Curves.linear,
@required Duration duration,
}) : assert(child != null),
assert(shape != null),
assert(clipBehavior != null),
assert(elevation != null && elevation >= 0.0),
assert(color != null),
assert(shadowColor != null),
super(key: key, curve: curve, duration: duration);
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.child}
final Widget child;
/// The border of the widget.
///
/// This border will be painted, and in addition the outer path of the border
/// 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}
///
/// Defaults to [Clip.none], and must not be null.
final Clip clipBehavior;
/// The target z-coordinate at which to place this physical object relative
/// to its parent.
///
/// The value is non-negative.
final double elevation;
/// The target background color.
final Color color;
/// The target shadow color.
final Color shadowColor;
@override
_MaterialInteriorState createState() => _MaterialInteriorState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(DiagnosticsProperty<ShapeBorder>('shape', shape));
description.add(DoubleProperty('elevation', elevation));
description.add(ColorProperty('color', color));
description.add(ColorProperty('shadowColor', shadowColor));
}
}
class _MaterialInteriorState extends AnimatedWidgetBaseState<_MaterialInterior> {
Tween<double> _elevation;
ColorTween _shadowColor;
ShapeBorderTween _border;
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_elevation = visitor(
_elevation,
widget.elevation,
(dynamic value) => Tween<double>(begin: value as double),
) as Tween<double>;
_shadowColor = visitor(
_shadowColor,
widget.shadowColor,
(dynamic value) => ColorTween(begin: value as Color),
) as ColorTween;
_border = visitor(
_border,
widget.shape,
(dynamic value) => ShapeBorderTween(begin: value as ShapeBorder),
) as ShapeBorderTween;
}
@override
Widget build(BuildContext context) {
final ShapeBorder shape = _border.evaluate(animation);
final double elevation = _elevation.evaluate(animation);
return PhysicalShape(
child: _ShapeBorderPaint(
child: widget.child,
shape: shape,
borderOnForeground: widget.borderOnForeground,
),
clipper: ShapeBorderClipper(
shape: shape,
textDirection: Directionality.of(context),
),
clipBehavior: widget.clipBehavior,
elevation: elevation,
color: ElevationOverlay.applyOverlay(context, widget.color, elevation),
shadowColor: _shadowColor.evaluate(animation),
);
}
}
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,
painter: borderOnForeground ? null : _ShapeBorderPainter(shape, Directionality.of(context)),
foregroundPainter: borderOnForeground ? _ShapeBorderPainter(shape, Directionality.of(context)) : null,
);
}
}
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;
}
}