blob: 960d05627385a5c7be28af565d7e5aa6b699f5ee [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 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'basic_types.dart';
import 'border_radius.dart';
import 'box_border.dart';
import 'box_shadow.dart';
import 'colors.dart';
import 'decoration.dart';
import 'decoration_image.dart';
import 'edge_insets.dart';
import 'gradient.dart';
import 'image_provider.dart';
/// An immutable description of how to paint a box.
///
/// The [BoxDecoration] class provides a variety of ways to draw a box.
///
/// The box has a [border], a body, and may cast a [boxShadow].
///
/// The [shape] of the box can be a circle or a rectangle. If it is a rectangle,
/// then the [borderRadius] property controls the roundness of the corners.
///
/// The body of the box is painted in layers. The bottom-most layer is the
/// [color], which fills the box. Above that is the [gradient], which also fills
/// the box. Finally there is the [image], the precise alignment of which is
/// controlled by the [DecorationImage] class.
///
/// The [border] paints over the body; the [boxShadow], naturally, paints below it.
///
/// {@tool snippet}
///
/// The following applies a [BoxDecoration] to a [Container] widget to draw an
/// [image] of an owl with a thick black [border] and rounded corners.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/box_decoration.png)
///
/// ```dart
/// Container(
/// decoration: BoxDecoration(
/// color: const Color(0xff7c94b6),
/// image: const DecorationImage(
/// image: NetworkImage('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg'),
/// fit: BoxFit.cover,
/// ),
/// border: Border.all(
/// width: 8,
/// ),
/// borderRadius: BorderRadius.circular(12),
/// ),
/// )
/// ```
/// {@end-tool}
///
/// {@template flutter.painting.BoxDecoration.clip}
/// The [shape] or the [borderRadius] won't clip the children of the
/// decorated [Container]. If the clip is required, insert a clip widget
/// (e.g., [ClipRect], [ClipRRect], [ClipPath]) as the child of the [Container].
/// Be aware that clipping may be costly in terms of performance.
/// {@endtemplate}
///
/// See also:
///
/// * [DecoratedBox] and [Container], widgets that can be configured with
/// [BoxDecoration] objects.
/// * [CustomPaint], a widget that lets you draw arbitrary graphics.
/// * [Decoration], the base class which lets you define other decorations.
class BoxDecoration extends Decoration {
/// Creates a box decoration.
///
/// * If [color] is null, this decoration does not paint a background color.
/// * If [image] is null, this decoration does not paint a background image.
/// * If [border] is null, this decoration does not paint a border.
/// * If [borderRadius] is null, this decoration uses more efficient background
/// painting commands. The [borderRadius] argument must be null if [shape] is
/// [BoxShape.circle].
/// * If [boxShadow] is null, this decoration does not paint a shadow.
/// * If [gradient] is null, this decoration does not paint gradients.
/// * If [backgroundBlendMode] is null, this decoration paints with [BlendMode.srcOver]
///
/// The [shape] argument must not be null.
const BoxDecoration({
this.color,
this.image,
this.border,
this.borderRadius,
this.boxShadow,
this.gradient,
this.backgroundBlendMode,
this.shape = BoxShape.rectangle,
}) : assert(shape != null),
assert(
backgroundBlendMode == null || color != null || gradient != null,
"backgroundBlendMode applies to BoxDecoration's background color or "
'gradient, but no color or gradient was provided.',
);
/// Creates a copy of this object but with the given fields replaced with the
/// new values.
BoxDecoration copyWith({
Color? color,
DecorationImage? image,
BoxBorder? border,
BorderRadiusGeometry? borderRadius,
List<BoxShadow>? boxShadow,
Gradient? gradient,
BlendMode? backgroundBlendMode,
BoxShape? shape,
}) {
return BoxDecoration(
color: color ?? this.color,
image: image ?? this.image,
border: border ?? this.border,
borderRadius: borderRadius ?? this.borderRadius,
boxShadow: boxShadow ?? this.boxShadow,
gradient: gradient ?? this.gradient,
backgroundBlendMode: backgroundBlendMode ?? this.backgroundBlendMode,
shape: shape ?? this.shape,
);
}
@override
bool debugAssertIsValid() {
assert(shape != BoxShape.circle || borderRadius == null); // Can't have a border radius if you're a circle.
return super.debugAssertIsValid();
}
/// The color to fill in the background of the box.
///
/// The color is filled into the [shape] of the box (e.g., either a rectangle,
/// potentially with a [borderRadius], or a circle).
///
/// This is ignored if [gradient] is non-null.
///
/// The [color] is drawn under the [image].
final Color? color;
/// An image to paint above the background [color] or [gradient].
///
/// If [shape] is [BoxShape.circle] then the image is clipped to the circle's
/// boundary; if [borderRadius] is non-null then the image is clipped to the
/// given radii.
final DecorationImage? image;
/// A border to draw above the background [color], [gradient], or [image].
///
/// Follows the [shape] and [borderRadius].
///
/// Use [Border] objects to describe borders that do not depend on the reading
/// direction.
///
/// Use [BoxBorder] objects to describe borders that should flip their left
/// and right edges based on whether the text is being read left-to-right or
/// right-to-left.
final BoxBorder? border;
/// If non-null, the corners of this box are rounded by this [BorderRadius].
///
/// Applies only to boxes with rectangular shapes; ignored if [shape] is not
/// [BoxShape.rectangle].
///
/// {@macro flutter.painting.BoxDecoration.clip}
final BorderRadiusGeometry? borderRadius;
/// A list of shadows cast by this box behind the box.
///
/// The shadow follows the [shape] of the box.
///
/// See also:
///
/// * [kElevationToShadow], for some predefined shadows used in Material
/// Design.
/// * [PhysicalModel], a widget for showing shadows.
final List<BoxShadow>? boxShadow;
/// A gradient to use when filling the box.
///
/// If this is specified, [color] has no effect.
///
/// The [gradient] is drawn under the [image].
final Gradient? gradient;
/// The blend mode applied to the [color] or [gradient] background of the box.
///
/// If no [backgroundBlendMode] is provided then the default painting blend
/// mode is used.
///
/// If no [color] or [gradient] is provided then the blend mode has no impact.
final BlendMode? backgroundBlendMode;
/// The shape to fill the background [color], [gradient], and [image] into and
/// to cast as the [boxShadow].
///
/// If this is [BoxShape.circle] then [borderRadius] is ignored.
///
/// The [shape] cannot be interpolated; animating between two [BoxDecoration]s
/// with different [shape]s will result in a discontinuity in the rendering.
/// To interpolate between two shapes, consider using [ShapeDecoration] and
/// different [ShapeBorder]s; in particular, [CircleBorder] instead of
/// [BoxShape.circle] and [RoundedRectangleBorder] instead of
/// [BoxShape.rectangle].
///
/// {@macro flutter.painting.BoxDecoration.clip}
final BoxShape shape;
@override
EdgeInsetsGeometry? get padding => border?.dimensions;
@override
Path getClipPath(Rect rect, TextDirection textDirection) {
switch (shape) {
case BoxShape.circle:
final Offset center = rect.center;
final double radius = rect.shortestSide / 2.0;
final Rect square = Rect.fromCircle(center: center, radius: radius);
return Path()..addOval(square);
case BoxShape.rectangle:
if (borderRadius != null) {
return Path()..addRRect(borderRadius!.resolve(textDirection).toRRect(rect));
}
return Path()..addRect(rect);
}
}
/// Returns a new box decoration that is scaled by the given factor.
BoxDecoration scale(double factor) {
return BoxDecoration(
color: Color.lerp(null, color, factor),
image: image, // TODO(ianh): fade the image from transparent
border: BoxBorder.lerp(null, border, factor),
borderRadius: BorderRadiusGeometry.lerp(null, borderRadius, factor),
boxShadow: BoxShadow.lerpList(null, boxShadow, factor),
gradient: gradient?.scale(factor),
shape: shape,
);
}
@override
bool get isComplex => boxShadow != null;
@override
BoxDecoration? lerpFrom(Decoration? a, double t) {
if (a == null) {
return scale(t);
}
if (a is BoxDecoration) {
return BoxDecoration.lerp(a, this, t);
}
return super.lerpFrom(a, t) as BoxDecoration?;
}
@override
BoxDecoration? lerpTo(Decoration? b, double t) {
if (b == null) {
return scale(1.0 - t);
}
if (b is BoxDecoration) {
return BoxDecoration.lerp(this, b, t);
}
return super.lerpTo(b, t) as BoxDecoration?;
}
/// Linearly interpolate between two box decorations.
///
/// Interpolates each parameter of the box decoration separately.
///
/// The [shape] is not interpolated. To interpolate the shape, consider using
/// a [ShapeDecoration] with different border shapes.
///
/// If both values are null, this returns null. Otherwise, it returns a
/// non-null value. If one of the values is null, then the result is obtained
/// by applying [scale] to the other value. If neither value is null and `t ==
/// 0.0`, then `a` is returned unmodified; if `t == 1.0` then `b` is returned
/// unmodified. Otherwise, the values are computed by interpolating the
/// properties appropriately.
///
/// {@macro dart.ui.shadow.lerp}
///
/// See also:
///
/// * [Decoration.lerp], which can interpolate between any two types of
/// [Decoration]s, not just [BoxDecoration]s.
/// * [lerpFrom] and [lerpTo], which are used to implement [Decoration.lerp]
/// and which use [BoxDecoration.lerp] when interpolating two
/// [BoxDecoration]s or a [BoxDecoration] to or from null.
static BoxDecoration? lerp(BoxDecoration? a, BoxDecoration? b, double t) {
assert(t != null);
if (a == null && b == null) {
return null;
}
if (a == null) {
return b!.scale(t);
}
if (b == null) {
return a.scale(1.0 - t);
}
if (t == 0.0) {
return a;
}
if (t == 1.0) {
return b;
}
return BoxDecoration(
color: Color.lerp(a.color, b.color, t),
image: t < 0.5 ? a.image : b.image, // TODO(ianh): cross-fade the image
border: BoxBorder.lerp(a.border, b.border, t),
borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, b.borderRadius, t),
boxShadow: BoxShadow.lerpList(a.boxShadow, b.boxShadow, t),
gradient: Gradient.lerp(a.gradient, b.gradient, t),
shape: t < 0.5 ? a.shape : b.shape,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is BoxDecoration
&& other.color == color
&& other.image == image
&& other.border == border
&& other.borderRadius == borderRadius
&& listEquals<BoxShadow>(other.boxShadow, boxShadow)
&& other.gradient == gradient
&& other.backgroundBlendMode == backgroundBlendMode
&& other.shape == shape;
}
@override
int get hashCode => Object.hash(
color,
image,
border,
borderRadius,
boxShadow == null ? null : Object.hashAll(boxShadow!),
gradient,
backgroundBlendMode,
shape,
);
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.whitespace
..emptyBodyDescription = '<no decorations specified>';
properties.add(ColorProperty('color', color, defaultValue: null));
properties.add(DiagnosticsProperty<DecorationImage>('image', image, defaultValue: null));
properties.add(DiagnosticsProperty<BoxBorder>('border', border, defaultValue: null));
properties.add(DiagnosticsProperty<BorderRadiusGeometry>('borderRadius', borderRadius, defaultValue: null));
properties.add(IterableProperty<BoxShadow>('boxShadow', boxShadow, defaultValue: null, style: DiagnosticsTreeStyle.whitespace));
properties.add(DiagnosticsProperty<Gradient>('gradient', gradient, defaultValue: null));
properties.add(EnumProperty<BoxShape>('shape', shape, defaultValue: BoxShape.rectangle));
}
@override
bool hitTest(Size size, Offset position, { TextDirection? textDirection }) {
assert(shape != null);
assert((Offset.zero & size).contains(position));
switch (shape) {
case BoxShape.rectangle:
if (borderRadius != null) {
final RRect bounds = borderRadius!.resolve(textDirection).toRRect(Offset.zero & size);
return bounds.contains(position);
}
return true;
case BoxShape.circle:
// Circles are inscribed into our smallest dimension.
final Offset center = size.center(Offset.zero);
final double distance = (position - center).distance;
return distance <= math.min(size.width, size.height) / 2.0;
}
}
@override
BoxPainter createBoxPainter([ VoidCallback? onChanged ]) {
assert(onChanged != null || image == null);
return _BoxDecorationPainter(this, onChanged);
}
}
/// An object that paints a [BoxDecoration] into a canvas.
class _BoxDecorationPainter extends BoxPainter {
_BoxDecorationPainter(this._decoration, super.onChanged)
: assert(_decoration != null);
final BoxDecoration _decoration;
Paint? _cachedBackgroundPaint;
Rect? _rectForCachedBackgroundPaint;
Paint _getBackgroundPaint(Rect rect, TextDirection? textDirection) {
assert(rect != null);
assert(_decoration.gradient != null || _rectForCachedBackgroundPaint == null);
if (_cachedBackgroundPaint == null ||
(_decoration.gradient != null && _rectForCachedBackgroundPaint != rect)) {
final Paint paint = Paint();
if (_decoration.backgroundBlendMode != null) {
paint.blendMode = _decoration.backgroundBlendMode!;
}
if (_decoration.color != null) {
paint.color = _decoration.color!;
}
if (_decoration.gradient != null) {
paint.shader = _decoration.gradient!.createShader(rect, textDirection: textDirection);
_rectForCachedBackgroundPaint = rect;
}
_cachedBackgroundPaint = paint;
}
return _cachedBackgroundPaint!;
}
void _paintBox(Canvas canvas, Rect rect, Paint paint, TextDirection? textDirection) {
switch (_decoration.shape) {
case BoxShape.circle:
assert(_decoration.borderRadius == null);
final Offset center = rect.center;
final double radius = rect.shortestSide / 2.0;
canvas.drawCircle(center, radius, paint);
break;
case BoxShape.rectangle:
if (_decoration.borderRadius == null || _decoration.borderRadius == BorderRadius.zero) {
canvas.drawRect(rect, paint);
} else {
canvas.drawRRect(_decoration.borderRadius!.resolve(textDirection).toRRect(rect), paint);
}
break;
}
}
void _paintShadows(Canvas canvas, Rect rect, TextDirection? textDirection) {
if (_decoration.boxShadow == null) {
return;
}
for (final BoxShadow boxShadow in _decoration.boxShadow!) {
final Paint paint = boxShadow.toPaint();
final Rect bounds = rect.shift(boxShadow.offset).inflate(boxShadow.spreadRadius);
_paintBox(canvas, bounds, paint, textDirection);
}
}
void _paintBackgroundColor(Canvas canvas, Rect rect, TextDirection? textDirection) {
if (_decoration.color != null || _decoration.gradient != null) {
_paintBox(canvas, rect, _getBackgroundPaint(rect, textDirection), textDirection);
}
}
DecorationImagePainter? _imagePainter;
void _paintBackgroundImage(Canvas canvas, Rect rect, ImageConfiguration configuration) {
if (_decoration.image == null) {
return;
}
_imagePainter ??= _decoration.image!.createPainter(onChanged!);
Path? clipPath;
switch (_decoration.shape) {
case BoxShape.circle:
assert(_decoration.borderRadius == null);
final Offset center = rect.center;
final double radius = rect.shortestSide / 2.0;
final Rect square = Rect.fromCircle(center: center, radius: radius);
clipPath = Path()..addOval(square);
break;
case BoxShape.rectangle:
if (_decoration.borderRadius != null) {
clipPath = Path()..addRRect(_decoration.borderRadius!.resolve(configuration.textDirection).toRRect(rect));
}
break;
}
_imagePainter!.paint(canvas, rect, clipPath, configuration);
}
@override
void dispose() {
_imagePainter?.dispose();
super.dispose();
}
/// Paint the box decoration into the given location on the given canvas.
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
assert(configuration != null);
assert(configuration.size != null);
final Rect rect = offset & configuration.size!;
final TextDirection? textDirection = configuration.textDirection;
_paintShadows(canvas, rect, textDirection);
_paintBackgroundColor(canvas, rect, textDirection);
_paintBackgroundImage(canvas, rect, configuration);
_decoration.border?.paint(
canvas,
rect,
shape: _decoration.shape,
borderRadius: _decoration.borderRadius?.resolve(textDirection),
textDirection: configuration.textDirection,
);
}
@override
String toString() {
return 'BoxPainter for $_decoration';
}
}