// 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 sample} /// /// 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( /// color: Colors.black, /// 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) { Path clipPath; switch (shape) { case BoxShape.circle: clipPath = Path()..addOval(rect); break; case BoxShape.rectangle: if (borderRadius != null) clipPath = Path()..addRRect(borderRadius.resolve(textDirection).toRRect(rect)); break; } return clipPath; } /// 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 ==(dynamic other) { if (identical(this, other)) return true; if (runtimeType != other.runtimeType) return false; return other is BoxDecoration && other.color == color && other.image == image && other.border == border && other.borderRadius == borderRadius && other.boxShadow == boxShadow && other.gradient == gradient && other.shape == shape; } @override int get hashCode { return hashValues( color, image, border, borderRadius, boxShadow, gradient, 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; } assert(shape != null); return null; } @override _BoxDecorationPainter 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, VoidCallback onChanged) : assert(_decoration != null), super(onChanged); 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) { 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 (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: clipPath = Path()..addOval(rect); 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 as BorderRadius, textDirection: configuration.textDirection, ); } @override String toString() { return 'BoxPainter for $_decoration'; } }