box_painter.dart 42.6 KB
Newer Older
1 2 3 4 5
// Copyright 2015 The Chromium 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;
6
import 'dart:ui' as ui show Image, Gradient, lerpDouble;
7

8
import 'package:flutter/services.dart';
9
import 'package:meta/meta.dart';
10

11
import 'basic_types.dart';
12
import 'decoration.dart';
13
import 'edge_insets.dart';
14 15
import 'fractional_offset.dart';
import 'image_fit.dart';
16

17
export 'edge_insets.dart' show EdgeInsets;
18

19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
/// The shape to use when rendering a BoxDecoration.
enum BoxShape {
  /// An axis-aligned, 2D rectangle. May have rounded corners (described by a
  /// [BorderRadius]). The edges of the rectangle will match the edges of the box
  /// into which the [BoxDecoration] is painted.
  rectangle,

  /// A circle centered in the middle of the box into which the [BoxDecoration]
  /// is painted. The diameter of the circle is the shortest dimension of the
  /// box, either the width or the height, such that the circle touches the
  /// edges of the box.
  circle,
}

/// An immutable set of radii for each corner of a rectangle.
///
/// Used by [BoxDecoration] when the shape is a [BoxShape.rectangle].
class BorderRadius {
  /// Creates a border radius where all radii are [radius].
  const BorderRadius.all(Radius radius) : this.only(
    topLeft: radius,
    topRight: radius,
    bottomRight: radius,
    bottomLeft: radius
  );

  /// Creates a border radius where all radii are [Radius.circular(radius)].
  BorderRadius.circular(double radius) : this.all(
    new Radius.circular(radius)
  );

  /// Creates a vertically symmetric border radius where the top and bottom
  /// sides of the rectangle have the same radii.
  const BorderRadius.vertical({
    Radius top: Radius.zero,
    Radius bottom: Radius.zero
  }) : this.only(
    topLeft: top,
    topRight: top,
    bottomRight: bottom,
    bottomLeft: bottom
  );

  /// Creates a horizontally symmetrical border radius where the left and right
  /// sides of the rectangle have the same radii.
  const BorderRadius.horizontal({
    Radius left: Radius.zero,
    Radius right: Radius.zero
  }) : this.only(
    topLeft: left,
    topRight: right,
    bottomRight: right,
    bottomLeft: left
  );

  /// Creates a border radius with only the given non-zero values. The other
  /// corners will be right angles.
  const BorderRadius.only({
    this.topLeft: Radius.zero,
    this.topRight: Radius.zero,
    this.bottomRight: Radius.zero,
    this.bottomLeft: Radius.zero
  });

  /// A border radius with all zero radii.
  static const BorderRadius zero = const BorderRadius.all(Radius.zero);

  /// The top-left [Radius].
  final Radius topLeft;
  /// The top-right [Radius].
  final Radius topRight;
  /// The bottom-right [Radius].
  final Radius bottomRight;
  /// The bottom-left [Radius].
  final Radius bottomLeft;

  /// Linearly interpolates between two [BorderRadius] objects.
  ///
  /// If either is null, this function interpolates from [BorderRadius.zero].
  static BorderRadius lerp(BorderRadius a, BorderRadius b, double t) {
    if (a == null && b == null)
      return null;
    return new BorderRadius.only(
      topLeft: Radius.lerp(a.topLeft, b.topLeft, t),
      topRight: Radius.lerp(a.topRight, b.topRight, t),
      bottomRight: Radius.lerp(a.bottomRight, b.bottomRight, t),
      bottomLeft: Radius.lerp(a.bottomLeft, b.bottomLeft, t)
    );
  }

  /// Creates a [RRect] from the current border radius and a [Rect].
  RRect toRRect(Rect rect) {
    return new RRect.fromRectAndCorners(
      rect,
      topLeft: topLeft,
      topRight: topRight,
      bottomRight: bottomRight,
      bottomLeft: bottomLeft
    );
  }

  @override
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! BorderRadius)
      return false;
    final BorderRadius typedOther = other;
    return topLeft == typedOther.topLeft &&
           topRight == typedOther.topRight &&
           bottomRight == typedOther.bottomRight &&
           bottomLeft == typedOther.bottomLeft;
  }

  @override
  int get hashCode => hashValues(topLeft, topRight, bottomRight, bottomLeft);

  @override
  String toString() {
    return 'BorderRadius($topLeft, $topRight, $bottomRight, $bottomLeft)';
  }
}

Hixie's avatar
Hixie committed
142 143 144 145 146 147 148 149 150 151 152
/// The style of line to draw for a [BorderSide] in a [Border].
enum BorderStyle {
  /// Skip the border.
  none,

  /// Draw the border as a solid line.
  solid,

  // if you add more, think about how they will lerp
}

Florian Loitsch's avatar
Florian Loitsch committed
153
/// A side of a border of a box.
154
class BorderSide {
155 156 157
  /// Creates the side of a border.
  ///
  /// By default, the border is 1.0 logical pixels wide and solid black.
158 159
  const BorderSide({
    this.color: const Color(0xFF000000),
Hixie's avatar
Hixie committed
160 161
    this.width: 1.0,
    this.style: BorderStyle.solid
162
  });
163

Florian Loitsch's avatar
Florian Loitsch committed
164
  /// The color of this side of the border.
165
  final Color color;
166

Hixie's avatar
Hixie committed
167 168 169
  /// The width of this side of the border, in logical pixels. A
  /// zero-width border is a hairline border. To omit the border
  /// entirely, set the [style] to [BorderStyle.none].
170 171
  final double width;

Hixie's avatar
Hixie committed
172 173 174 175 176 177 178 179
  /// The style of this side of the border.
  ///
  /// To omit a side, set [style] to [BorderStyle.none]. This skips
  /// painting the border, but the border still has a [width].
  final BorderStyle style;

  /// A hairline black border that is not rendered.
  static const BorderSide none = const BorderSide(width: 0.0, style: BorderStyle.none);
180

181
  /// Creates a copy of this border but with the given fields replaced with the new values.
182 183
  BorderSide copyWith({
    Color color,
Hixie's avatar
Hixie committed
184 185
    double width,
    BorderStyle style
186 187 188
  }) {
    return new BorderSide(
      color: color ?? this.color,
Hixie's avatar
Hixie committed
189 190
      width: width ?? this.width,
      style: style ?? this.style
191 192 193
    );
  }

194
  /// Linearly interpolate between two border sides.
195 196 197
  static BorderSide lerp(BorderSide a, BorderSide b, double t) {
    assert(a != null);
    assert(b != null);
Hixie's avatar
Hixie committed
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
    if (t == 0.0)
      return a;
    if (t == 1.0)
      return b;
    if (a.style == b.style) {
      return new BorderSide(
        color: Color.lerp(a.color, b.color, t),
        width: ui.lerpDouble(a.width, b.width, t),
        style: a.style // == b.style
      );
    }
    Color colorA, colorB;
    switch (a.style) {
      case BorderStyle.solid:
        colorA = a.color;
        break;
      case BorderStyle.none:
        colorA = a.color.withAlpha(0x00);
        break;
    }
    switch (b.style) {
      case BorderStyle.solid:
        colorB = b.color;
        break;
      case BorderStyle.none:
        colorB = b.color.withAlpha(0x00);
        break;
    }
226
    return new BorderSide(
Hixie's avatar
Hixie committed
227 228 229
      color: Color.lerp(colorA, colorB, t),
      width: ui.lerpDouble(a.width, b.width, t),
      style: BorderStyle.solid
230 231 232
    );
  }

233
  @override
234 235 236 237 238 239 240
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! BorderSide)
      return false;
    final BorderSide typedOther = other;
    return color == typedOther.color &&
Hixie's avatar
Hixie committed
241 242
           width == typedOther.width &&
           style == typedOther.style;
243 244
  }

245
  @override
Hixie's avatar
Hixie committed
246
  int get hashCode => hashValues(color, width, style);
247

248
  @override
Hixie's avatar
Hixie committed
249
  String toString() => 'BorderSide($color, $width, $style)';
250 251
}

Florian Loitsch's avatar
Florian Loitsch committed
252
/// A border of a box, comprised of four sides.
253
class Border {
254 255 256
  /// Creates a border.
  ///
  /// All the sides of the border default to [BorderSide.none].
257 258 259 260 261 262 263
  const Border({
    this.top: BorderSide.none,
    this.right: BorderSide.none,
    this.bottom: BorderSide.none,
    this.left: BorderSide.none
  });

Florian Loitsch's avatar
Florian Loitsch committed
264
  /// A uniform border with all sides the same color and width.
265 266
  factory Border.all({
    Color color: const Color(0xFF000000),
Hixie's avatar
Hixie committed
267 268
    double width: 1.0,
    BorderStyle style: BorderStyle.solid
269
  }) {
Hixie's avatar
Hixie committed
270
    final BorderSide side = new BorderSide(color: color, width: width, style: style);
271 272
    return new Border(top: side, right: side, bottom: side, left: side);
  }
273

Florian Loitsch's avatar
Florian Loitsch committed
274
  /// The top side of this border.
275
  final BorderSide top;
276

Florian Loitsch's avatar
Florian Loitsch committed
277
  /// The right side of this border.
278
  final BorderSide right;
279

Florian Loitsch's avatar
Florian Loitsch committed
280
  /// The bottom side of this border.
281
  final BorderSide bottom;
282

Florian Loitsch's avatar
Florian Loitsch committed
283
  /// The left side of this border.
284 285
  final BorderSide left;

286 287
  /// The widths of the sides of this border represented as an EdgeInsets.
  EdgeInsets get dimensions {
288
    return new EdgeInsets.fromLTRB(left.width, top.width, right.width, bottom.width);
289 290
  }

291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
  /// Whether all four sides of the border are identical.
  bool get isUniform {
    assert(top != null);
    assert(right != null);
    assert(bottom != null);
    assert(left != null);

    final Color topColor = top.color;
    if (right.color != topColor ||
        bottom.color != topColor ||
        left.color != topColor)
      return false;

    final double topWidth = top.width;
    if (right.width != topWidth ||
        bottom.width != topWidth ||
        left.width != topWidth)
      return false;

Hixie's avatar
Hixie committed
310 311 312 313 314 315
    final BorderStyle topStyle = top.style;
    if (right.style != topStyle ||
        bottom.style != topStyle ||
        left.style != topStyle)
      return false;

316 317 318 319
    return true;
  }

  /// Creates a new border with the widths of this border multiplied by [t].
320 321 322 323 324 325 326 327 328
  Border scale(double t) {
    return new Border(
      top: top.copyWith(width: t * top.width),
      right: right.copyWith(width: t * right.width),
      bottom: bottom.copyWith(width: t * bottom.width),
      left: left.copyWith(width: t * left.width)
    );
  }

329
  /// Linearly interpolate between two borders.
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
  static Border lerp(Border a, Border b, double t) {
    if (a == null && b == null)
      return null;
    if (a == null)
      return b.scale(t);
    if (b == null)
      return a.scale(1.0 - t);
    return new Border(
      top: BorderSide.lerp(a.top, b.top, t),
      right: BorderSide.lerp(a.right, b.right, t),
      bottom: BorderSide.lerp(a.bottom, b.bottom, t),
      left: BorderSide.lerp(a.left, b.left, t)
    );
  }

345
  /// Paints the border within the given rect on the given canvas.
346 347
  void paint(Canvas canvas, Rect rect, {
    BoxShape shape: BoxShape.rectangle,
348
    BorderRadius borderRadius: null
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
  }) {
    if (isUniform) {
      if (borderRadius != null) {
        _paintBorderWithRadius(canvas, rect, borderRadius);
        return;
      }
      if (shape == BoxShape.circle) {
        _paintBorderWithCircle(canvas, rect);
        return;
      }
    }

    assert(borderRadius == null); // TODO(abarth): Support non-uniform rounded borders.
    assert(shape == BoxShape.rectangle); // TODO(ianh): Support non-uniform borders on circles.

    assert(top != null);
    assert(right != null);
    assert(bottom != null);
    assert(left != null);

Hixie's avatar
Hixie committed
369 370
    Paint paint = new Paint()
      ..strokeWidth = 0.0; // used for hairline borders
371 372
    Path path;

Hixie's avatar
Hixie committed
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
    switch (top.style) {
      case BorderStyle.solid:
        paint.color = top.color;
        path = new Path();
        path.moveTo(rect.left, rect.top);
        path.lineTo(rect.right, rect.top);
        if (top.width == 0.0) {
          paint.style = PaintingStyle.stroke;
        } else {
          paint.style = PaintingStyle.fill;
          path.lineTo(rect.right - right.width, rect.top + top.width);
          path.lineTo(rect.left + left.width, rect.top + top.width);
        }
        canvas.drawPath(path, paint);
        break;
pq's avatar
pq committed
388 389
      case BorderStyle.none:
        break;
Hixie's avatar
Hixie committed
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
    }

    switch (right.style) {
      case BorderStyle.solid:
        paint.color = right.color;
        path = new Path();
        path.moveTo(rect.right, rect.top);
        path.lineTo(rect.right, rect.bottom);
        if (right.width == 0.0) {
          paint.style = PaintingStyle.stroke;
        } else {
          paint.style = PaintingStyle.fill;
          path.lineTo(rect.right - right.width, rect.bottom - bottom.width);
          path.lineTo(rect.right - right.width, rect.top + top.width);
        }
        canvas.drawPath(path, paint);
        break;
pq's avatar
pq committed
407 408
      case BorderStyle.none:
        break;
Hixie's avatar
Hixie committed
409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425
    }

    switch (bottom.style) {
      case BorderStyle.solid:
        paint.color = bottom.color;
        path = new Path();
        path.moveTo(rect.right, rect.bottom);
        path.lineTo(rect.left, rect.bottom);
        if (bottom.width == 0.0) {
          paint.style = PaintingStyle.stroke;
        } else {
          paint.style = PaintingStyle.fill;
          path.lineTo(rect.left + left.width, rect.bottom - bottom.width);
          path.lineTo(rect.right - right.width, rect.bottom - bottom.width);
        }
        canvas.drawPath(path, paint);
        break;
pq's avatar
pq committed
426 427
      case BorderStyle.none:
        break;
Hixie's avatar
Hixie committed
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
    }

    switch (left.style) {
      case BorderStyle.solid:
        paint.color = left.color;
        path = new Path();
        path.moveTo(rect.left, rect.bottom);
        path.lineTo(rect.left, rect.top);
        if (right.width == 0.0) {
          paint.style = PaintingStyle.stroke;
        } else {
          paint.style = PaintingStyle.fill;
          path.lineTo(rect.left + left.width, rect.top + top.width);
          path.lineTo(rect.left + left.width, rect.bottom - bottom.width);
        }
        canvas.drawPath(path, paint);
        break;
pq's avatar
pq committed
445 446
      case BorderStyle.none:
        break;
Hixie's avatar
Hixie committed
447
    }
448 449
  }

450 451
  void _paintBorderWithRadius(Canvas canvas, Rect rect,
                              BorderRadius borderRadius) {
452
    assert(isUniform);
Hixie's avatar
Hixie committed
453 454
    Paint paint = new Paint()
      ..color = top.color;
455
    RRect outer = borderRadius.toRRect(rect);
Hixie's avatar
Hixie committed
456 457 458 459 460 461 462
    double width = top.width;
    if (width == 0.0) {
      paint
        ..style = PaintingStyle.stroke
        ..strokeWidth = 0.0;
      canvas.drawRRect(outer, paint);
    } else {
463
      RRect inner = outer.deflate(width);
Hixie's avatar
Hixie committed
464 465
      canvas.drawDRRect(outer, inner, paint);
    }
466 467 468 469 470 471 472 473 474 475 476 477 478
  }

  void _paintBorderWithCircle(Canvas canvas, Rect rect) {
    assert(isUniform);
    double width = top.width;
    Paint paint = new Paint()
      ..color = top.color
      ..strokeWidth = width
      ..style = PaintingStyle.stroke;
    double radius = (rect.shortestSide - width) / 2.0;
    canvas.drawCircle(rect.center, radius, paint);
  }

479
  @override
480 481 482
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
483
    if (other.runtimeType != runtimeType)
484 485 486 487 488 489 490 491
      return false;
    final Border typedOther = other;
    return top == typedOther.top &&
           right == typedOther.right &&
           bottom == typedOther.bottom &&
           left == typedOther.left;
  }

492
  @override
493
  int get hashCode => hashValues(top, right, bottom, left);
494

495
  @override
496 497 498
  String toString() => 'Border($top, $right, $bottom, $left)';
}

Florian Loitsch's avatar
Florian Loitsch committed
499
/// A shadow cast by a box.
500
///
501 502 503
/// BoxShadow can cast non-rectangular shadows if the box is non-rectangular
/// (e.g., has a border radius or a circular shape).
///
Hans Muller's avatar
Hans Muller committed
504
/// This class is similar to CSS box-shadow.
505
class BoxShadow {
506 507 508 509
  /// Creates a box shadow.
  ///
  /// By default, the shadow is solid black with zero [offset], [blurRadius],
  /// and [spreadRadius].
510
  const BoxShadow({
511 512 513
    this.color: const Color(0xFF000000),
    this.offset: Offset.zero,
    this.blurRadius: 0.0,
Hans Muller's avatar
Hans Muller committed
514
    this.spreadRadius: 0.0
515 516
  });

Florian Loitsch's avatar
Florian Loitsch committed
517
  /// The color of the shadow.
518
  final Color color;
519

Florian Loitsch's avatar
Florian Loitsch committed
520
  /// The displacement of the shadow from the box.
521
  final Offset offset;
522

Florian Loitsch's avatar
Florian Loitsch committed
523
  /// The standard deviation of the Gaussian to convolve with the box's shape.
Hans Muller's avatar
Hans Muller committed
524
  final double blurRadius;
525

526
  /// The amount the box should be inflated prior to applying the blur.
Hans Muller's avatar
Hans Muller committed
527 528
  final double spreadRadius;

Adam Barth's avatar
Adam Barth committed
529 530 531 532
  /// The [blurRadius] in sigmas instead of logical pixels.
  ///
  /// See the sigma argument to [MaskFilter.blur].
  ///
Florian Loitsch's avatar
Florian Loitsch committed
533
  // See SkBlurMask::ConvertRadiusToSigma().
Adam Barth's avatar
Adam Barth committed
534 535
  // <https://github.com/google/skia/blob/bb5b77db51d2e149ee66db284903572a5aac09be/src/effects/SkBlurMask.cpp#L23>
  double get blurSigma => blurRadius * 0.57735 + 0.5;
Hans Muller's avatar
Hans Muller committed
536

Florian Loitsch's avatar
Florian Loitsch committed
537
  /// Returns a new box shadow with its offset, blurRadius, and spreadRadius scaled by the given factor.
538 539 540 541
  BoxShadow scale(double factor) {
    return new BoxShadow(
      color: color,
      offset: offset * factor,
Hans Muller's avatar
Hans Muller committed
542 543
      blurRadius: blurRadius * factor,
      spreadRadius: spreadRadius * factor
544 545 546
    );
  }

Florian Loitsch's avatar
Florian Loitsch committed
547
  /// Linearly interpolate between two box shadows.
548 549 550
  ///
  /// If either box shadow is null, this function linearly interpolates from a
  /// a box shadow that matches the other box shadow in color but has a zero
Hans Muller's avatar
Hans Muller committed
551
  /// offset and a zero blurRadius.
552 553 554 555 556 557 558 559
  static BoxShadow lerp(BoxShadow a, BoxShadow b, double t) {
    if (a == null && b == null)
      return null;
    if (a == null)
      return b.scale(t);
    if (b == null)
      return a.scale(1.0 - t);
    return new BoxShadow(
Adam Barth's avatar
Adam Barth committed
560 561
      color: Color.lerp(a.color, b.color, t),
      offset: Offset.lerp(a.offset, b.offset, t),
Hans Muller's avatar
Hans Muller committed
562 563
      blurRadius: ui.lerpDouble(a.blurRadius, b.blurRadius, t),
      spreadRadius: ui.lerpDouble(a.spreadRadius, b.spreadRadius, t)
564 565
    );
  }
566

Florian Loitsch's avatar
Florian Loitsch committed
567
  /// Linearly interpolate between two lists of box shadows.
568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
  ///
  /// If the lists differ in length, excess items are lerped with null.
  static List<BoxShadow> lerpList(List<BoxShadow> a, List<BoxShadow> b, double t) {
    if (a == null && b == null)
      return null;
    if (a == null)
      a = new List<BoxShadow>();
    if (b == null)
      b = new List<BoxShadow>();
    List<BoxShadow> result = new List<BoxShadow>();
    int commonLength = math.min(a.length, b.length);
    for (int i = 0; i < commonLength; ++i)
      result.add(BoxShadow.lerp(a[i], b[i], t));
    for (int i = commonLength; i < a.length; ++i)
      result.add(a[i].scale(1.0 - t));
    for (int i = commonLength; i < b.length; ++i)
      result.add(b[i].scale(t));
    return result;
  }
587

588
  @override
589 590 591 592 593 594 595 596
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! BoxShadow)
      return false;
    final BoxShadow typedOther = other;
    return color == typedOther.color &&
           offset == typedOther.offset &&
Hans Muller's avatar
Hans Muller committed
597 598
           blurRadius == typedOther.blurRadius &&
           spreadRadius == typedOther.spreadRadius;
599 600
  }

601
  @override
602
  int get hashCode => hashValues(color, offset, blurRadius, spreadRadius);
603

604
  @override
Hans Muller's avatar
Hans Muller committed
605
  String toString() => 'BoxShadow($color, $offset, $blurRadius, $spreadRadius)';
606 607
}

Florian Loitsch's avatar
Florian Loitsch committed
608
/// A 2D gradient.
609
abstract class Gradient {
610 611
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
612
  const Gradient();
613 614

  /// Creates a [Shader] for this gradient to fill the given rect.
615
  Shader createShader(Rect rect);
616 617
}

Florian Loitsch's avatar
Florian Loitsch committed
618
/// A 2D linear gradient.
619
class LinearGradient extends Gradient {
620 621 622 623
  /// Creates a linear graident.
  ///
  /// The [colors] argument must not be null. If [stops] is non-null, it must
  /// have the same length as [colors].
624
  const LinearGradient({
625 626
    this.begin: FractionalOffset.centerLeft,
    this.end: FractionalOffset.centerRight,
627
    this.colors,
628
    this.stops,
629
    this.tileMode: TileMode.clamp
630
  });
631

632 633 634 635 636 637
  /// The offset from coordinate (0.0,0.0) at which stop 0.0 of the
  /// gradient is placed, in a coordinate space that maps the top left
  /// of the paint box at (0.0,0.0) and the bottom right at (1.0,1.0).
  ///
  /// For example, a begin offset of (0.0,0.5) is half way down the
  /// left side of the box.
638
  final FractionalOffset begin;
639

640 641 642 643 644 645
  /// The offset from coordinate (0.0,0.0) at which stop 1.0 of the
  /// gradient is placed, in a coordinate space that maps the top left
  /// of the paint box at (0.0,0.0) and the bottom right at (1.0,1.0).
  ///
  /// For example, an end offset of (1.0,0.5) is half way down the
  /// right side of the box.
646
  final FractionalOffset end;
647

Florian Loitsch's avatar
Florian Loitsch committed
648
  /// The colors the gradient should obtain at each of the stops.
649
  ///
650
  /// If [stops] is non-null, this list must have the same length as [stops].
651
  final List<Color> colors;
652

653 654
  /// A list of values from 0.0 to 1.0 that denote fractions of the vector from
  /// start to end.
655
  ///
656 657
  /// If non-null, this list must have the same length as [colors]. Otherwise
  /// the colors are distributed evenly between [begin] and [end].
658 659
  final List<double> stops;

Florian Loitsch's avatar
Florian Loitsch committed
660
  /// How this gradient should tile the plane.
661
  final TileMode tileMode;
662

663
  @override
664 665
  Shader createShader(Rect rect) {
    return new ui.Gradient.linear(
666
      <Point>[begin.withinRect(rect), end.withinRect(rect)],
667 668
      colors, stops, tileMode
    );
669 670
  }

671
  @override
672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! LinearGradient)
      return false;
    final LinearGradient typedOther = other;
    if (begin != typedOther.begin ||
        end != typedOther.end ||
        tileMode != typedOther.tileMode ||
        colors?.length != typedOther.colors?.length ||
        stops?.length != typedOther.stops?.length)
      return false;
    if (colors != null) {
      assert(typedOther.colors != null);
      assert(colors.length == typedOther.colors.length);
      for (int i = 0; i < colors.length; i += 1) {
        if (colors[i] != typedOther.colors[i])
          return false;
      }
    }
    if (stops != null) {
      assert(typedOther.stops != null);
      assert(stops.length == typedOther.stops.length);
      for (int i = 0; i < stops.length; i += 1) {
        if (stops[i] != typedOther.stops[i])
          return false;
      }
    }
    return true;
  }

703
  @override
704
  int get hashCode => hashValues(begin, end, tileMode, hashList(colors), hashList(stops));
705

706
  @override
707
  String toString() {
708
    return 'LinearGradient($begin, $end, $colors, $stops, $tileMode)';
709
  }
710 711
}

Florian Loitsch's avatar
Florian Loitsch committed
712
/// A 2D radial gradient.
713
class RadialGradient extends Gradient {
714 715 716 717
  /// Creates a radial graident.
  ///
  /// The [colors] argument must not be null. If [stops] is non-null, it must
  /// have the same length as [colors].
718
  const RadialGradient({
719
    this.center: FractionalOffset.center,
720
    this.radius: 0.5,
721
    this.colors,
722
    this.stops,
723
    this.tileMode: TileMode.clamp
724 725
  });

726 727 728 729 730
  /// The center of the gradient, as an offset into the unit square
  /// describing the gradient which will be mapped onto the paint box.
  ///
  /// For example, an offset of (0.5,0.5) will place the radial
  /// gradient in the center of the box.
731
  final FractionalOffset center;
732

733 734 735 736 737 738
  /// The radius of the gradient, as a fraction of the shortest side
  /// of the paint box.
  ///
  /// For example, if a radial gradient is painted on a box that is
  /// 100.0 pixels wide and 200.0 pixels tall, then a radius of 1.0
  /// will place the 1.0 stop at 100.0 pixels from the [center].
739
  final double radius;
740

Florian Loitsch's avatar
Florian Loitsch committed
741
  /// The colors the gradient should obtain at each of the stops.
742
  ///
743
  /// If [stops] is non-null, this list must have the same length as [stops].
744
  final List<Color> colors;
745

Florian Loitsch's avatar
Florian Loitsch committed
746
  /// A list of values from 0.0 to 1.0 that denote concentric rings.
747 748 749 750
  ///
  /// The rings are centered at [center] and have a radius equal to the value of
  /// the stop times [radius].
  ///
751 752 753
  /// If non-null, this list must have the same length as [colors]. Otherwise
  /// the colors are distributed evenly between the [center] and the ring at
  /// [radius].
754 755
  final List<double> stops;

Florian Loitsch's avatar
Florian Loitsch committed
756
  /// How this gradient should tile the plane.
757
  final TileMode tileMode;
758

759
  @override
760 761
  Shader createShader(Rect rect) {
    return new ui.Gradient.radial(
762
      center.withinRect(rect),
763 764 765
      radius * rect.shortestSide,
      colors, stops, tileMode
    );
766 767
  }

768
  @override
769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! RadialGradient)
      return false;
    final RadialGradient typedOther = other;
    if (center != typedOther.center ||
        radius != typedOther.radius ||
        tileMode != typedOther.tileMode ||
        colors?.length != typedOther.colors?.length ||
        stops?.length != typedOther.stops?.length)
      return false;
    if (colors != null) {
      assert(typedOther.colors != null);
      assert(colors.length == typedOther.colors.length);
      for (int i = 0; i < colors.length; i += 1) {
        if (colors[i] != typedOther.colors[i])
          return false;
      }
    }
    if (stops != null) {
      assert(typedOther.stops != null);
      assert(stops.length == typedOther.stops.length);
      for (int i = 0; i < stops.length; i += 1) {
        if (stops[i] != typedOther.stops[i])
          return false;
      }
    }
    return true;
  }

800
  @override
801
  int get hashCode => hashValues(center, radius, tileMode, hashList(colors), hashList(stops));
802

803
  @override
804
  String toString() {
805
    return 'RadialGradient($center, $radius, $colors, $stops, $tileMode)';
806
  }
807 808
}

Florian Loitsch's avatar
Florian Loitsch committed
809
/// How to paint any portions of a box not covered by an image.
810
enum ImageRepeat {
Florian Loitsch's avatar
Florian Loitsch committed
811
  /// Repeat the image in both the x and y directions until the box is filled.
812
  repeat,
813

Florian Loitsch's avatar
Florian Loitsch committed
814
  /// Repeat the image in the x direction until the box is filled horizontally.
815
  repeatX,
816

Florian Loitsch's avatar
Florian Loitsch committed
817
  /// Repeat the image in the y direction until the box is filled vertically.
818 819
  repeatY,

Florian Loitsch's avatar
Florian Loitsch committed
820
  /// Leave uncovered poritions of the box transparent.
821 822 823
  noRepeat
}

Adam Barth's avatar
Adam Barth committed
824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852
Iterable<Rect> _generateImageTileRects(Rect outputRect, Rect fundamentalRect, ImageRepeat repeat) sync* {
  if (repeat == ImageRepeat.noRepeat) {
    yield fundamentalRect;
    return;
  }

  int startX = 0;
  int startY = 0;
  int stopX = 0;
  int stopY = 0;
  double strideX = fundamentalRect.width;
  double strideY = fundamentalRect.height;

  if (repeat == ImageRepeat.repeat || repeat == ImageRepeat.repeatX) {
    startX = ((outputRect.left - fundamentalRect.left) / strideX).floor();
    stopX = ((outputRect.right - fundamentalRect.right) / strideX).ceil();
  }

  if (repeat == ImageRepeat.repeat || repeat == ImageRepeat.repeatY) {
    startY = ((outputRect.top - fundamentalRect.top) / strideY).floor();
    stopY = ((outputRect.bottom - fundamentalRect.bottom) / strideY).ceil();
  }

  for (int i = startX; i <= stopX; ++i) {
    for (int j = startY; j <= stopY; ++j)
      yield fundamentalRect.shift(new Offset(i * strideX, j * strideY));
  }
}

Florian Loitsch's avatar
Florian Loitsch committed
853
/// Paints an image into the given rectangle in the canvas.
854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884
///
///  * `canvas`: The canvas onto which the image will be painted.
///  * `rect`: The region of the canvas into which the image will be painted.
///    The image might not fill the entire rectangle (e.g., depending on the
///    `fit`).
///  * `image`: The image to paint onto the canvas.
///  * `colorFilter`: If non-null, the color filter to apply when painting the
///    image.
///  * `fit`: How the image should be inscribed into `rect`. If null, the
///    default behavior depends on `centerSlice`. If `centerSlice` is also null,
///    the default behavior is [ImageFit.scaleDown]. If `centerSlice` is
///    non-null, the default behavior is [ImageFit.fill]. See [ImageFit] for
///    details.
///  * `repeat`: If the image does not fill `rect`, whether and how the image
///    should be repeated to fill `rect`. By default, the image is not repeated.
///    See [ImageRepeat] for details.
///  * `centerSlice`: The image is drawn in nine portions described by splitting
///    the image by drawing two horizontal lines and two vertical lines, where
///    `centerSlice` describes the rectangle formed by the four points where
///    these four lines intersect each other. (This forms a 3-by-3 grid
///    of regions, the center region being described by `centerSlice`.)
///    The four regions in the corners are drawn, without scaling, in the four
///    corners of the destination rectangle defined by applying `fit`. The
///    remaining five regions are drawn by stretching them to fit such that they
///    exactly cover the destination rectangle while maintaining their relative
///    positions.
///  * `alignment`: How the destination rectangle defined by applying `fit` is
///    aligned within `rect`. For example, if `fit` is [ImageFit.contain] and
///    `alignment` is [FractionalOffset.bottomRight], the image will be as large
///    as possible within `rect` and placed with its bottom right corner at the
///    bottom right corner of `rect`.
885
void paintImage({
886 887 888
  @required Canvas canvas,
  @required Rect rect,
  @required ui.Image image,
Adam Barth's avatar
Adam Barth committed
889
  ColorFilter colorFilter,
890
  ImageFit fit,
Adam Barth's avatar
Adam Barth committed
891
  ImageRepeat repeat: ImageRepeat.noRepeat,
892
  Rect centerSlice,
893
  FractionalOffset alignment
894
}) {
Ian Hickson's avatar
Ian Hickson committed
895 896
  assert(canvas != null);
  assert(image != null);
897 898 899 900 901 902 903 904 905 906 907 908 909
  Size outputSize = rect.size;
  Size inputSize = new Size(image.width.toDouble(), image.height.toDouble());
  Offset sliceBorder;
  if (centerSlice != null) {
    sliceBorder = new Offset(
      centerSlice.left + inputSize.width - centerSlice.right,
      centerSlice.top + inputSize.height - centerSlice.bottom
    );
    outputSize -= sliceBorder;
    inputSize -= sliceBorder;
  }
  fit ??= centerSlice == null ? ImageFit.scaleDown : ImageFit.fill;
  assert(centerSlice == null || (fit != ImageFit.none && fit != ImageFit.cover));
910 911 912
  final FittedSizes fittedSizes = applyImageFit(fit, inputSize, outputSize);
  final Size sourceSize = fittedSizes.source;
  Size destinationSize = fittedSizes.destination;
913 914 915 916 917 918 919
  if (centerSlice != null) {
    outputSize += sliceBorder;
    destinationSize += sliceBorder;
    // We don't have the ability to draw a subset of the image at the same time
    // as we apply a nine-patch stretch.
    assert(sourceSize == inputSize);
  }
920 921 922 923 924
  if (repeat != ImageRepeat.noRepeat && destinationSize == outputSize) {
    // There's no need to repeat the image because we're exactly filling the
    // output rect with the image.
    repeat = ImageRepeat.noRepeat;
  }
925
  Paint paint = new Paint()..isAntiAlias = false;
926
  if (colorFilter != null)
927
    paint.colorFilter = colorFilter;
928 929 930 931 932 933
  if (sourceSize != destinationSize) {
    // Use the "low" quality setting to scale the image, which corresponds to
    // bilinear interpolation, rather than the default "none" which corresponds
    // to nearest-neighbor.
    paint.filterQuality = FilterQuality.low;
  }
934 935
  double dx = (outputSize.width - destinationSize.width) * (alignment?.dx ?? 0.5);
  double dy = (outputSize.height - destinationSize.height) * (alignment?.dy ?? 0.5);
936
  Point destinationPosition = rect.topLeft + new Offset(dx, dy);
937
  Rect destinationRect = destinationPosition & destinationSize;
938 939 940 941
  if (repeat != ImageRepeat.noRepeat) {
    canvas.save();
    canvas.clipRect(rect);
  }
Adam Barth's avatar
Adam Barth committed
942
  if (centerSlice == null) {
943 944 945
    final Rect sourceRect = (alignment ?? FractionalOffset.center).inscribe(
      fittedSizes.source, Point.origin & inputSize
    );
Adam Barth's avatar
Adam Barth committed
946 947 948 949 950 951
    for (Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat))
      canvas.drawImageRect(image, sourceRect, tileRect, paint);
  } else {
    for (Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat))
      canvas.drawImageNine(image, centerSlice, tileRect, paint);
  }
952 953
  if (repeat != ImageRepeat.noRepeat)
    canvas.restore();
954
}
955

956
/// A background image for a box.
957 958 959
///
/// The image is painted using [paintImage], which describes the meanings of the
/// various fields on this class in more detail.
960
class BackgroundImage {
961 962 963
  /// Creates a background image.
  ///
  /// The [image] argument must not be null.
964 965
  const BackgroundImage({
    this.image,
966 967 968
    this.fit,
    this.repeat: ImageRepeat.noRepeat,
    this.centerSlice,
969 970
    this.colorFilter,
    this.alignment
971 972 973 974
  });

  /// The image to be painted into the background.
  final ImageProvider image;
975

976
  /// How the background image should be inscribed into the box.
977 978 979
  ///
  /// The default varies based on the other fields. See the discussion at
  /// [paintImage].
980
  final ImageFit fit;
981

982
  /// How to paint any portions of the box not covered by the background image.
983
  final ImageRepeat repeat;
984

985 986 987 988 989 990 991 992 993 994
  /// The center slice for a nine-patch image.
  ///
  /// The region of the image inside the center slice will be stretched both
  /// horizontally and vertically to fit the image into its destination. The
  /// region of the image above and below the center slice will be stretched
  /// only horizontally and the region of the image to the left and right of
  /// the center slice will be stretched only vertically.
  final Rect centerSlice;

  /// A color filter to apply to the background image before painting it.
Adam Barth's avatar
Adam Barth committed
995
  final ColorFilter colorFilter;
996

997
  /// How to align the image within its bounds.
998 999 1000 1001
  ///
  /// An alignment of (0.0, 0.0) aligns the image to the top-left corner of its
  /// layout bounds.  An alignment of (1.0, 0.5) aligns the image to the middle
  /// of the right edge of its layout bounds.
1002 1003
  final FractionalOffset alignment;

1004
  @override
1005 1006 1007 1008 1009 1010
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! BackgroundImage)
      return false;
    final BackgroundImage typedOther = other;
1011 1012
    return image == typedOther.image &&
           fit == typedOther.fit &&
1013 1014 1015
           repeat == typedOther.repeat &&
           centerSlice == typedOther.centerSlice &&
           colorFilter == typedOther.colorFilter &&
1016
           alignment == typedOther.alignment;
1017 1018
  }

1019
  @override
1020
  int get hashCode => hashValues(image, fit, repeat, centerSlice, colorFilter, alignment);
1021

1022
  @override
1023
  String toString() => 'BackgroundImage($image, $fit, $repeat)';
1024 1025
}

Florian Loitsch's avatar
Florian Loitsch committed
1026
/// An immutable description of how to paint a box.
1027
class BoxDecoration extends Decoration {
1028 1029 1030 1031 1032 1033 1034 1035 1036 1037
  /// Creates a box decoration.
  ///
  /// * If [backgroundColor] is null, this decoration does not paint a background color.
  /// * If [backgroundImage] 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 use more efficient background
  ///   painting commands. The [borderRadius] argument must be 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.
1038
  const BoxDecoration({
1039 1040 1041 1042 1043 1044
    this.backgroundColor,
    this.backgroundImage,
    this.border,
    this.borderRadius,
    this.boxShadow,
    this.gradient,
1045
    this.shape: BoxShape.rectangle
1046 1047
  });

1048
  @override
1049
  bool debugAssertIsValid() {
1050
    assert(shape != BoxShape.circle ||
Florian Loitsch's avatar
Florian Loitsch committed
1051
           borderRadius == null); // Can't have a border radius if you're a circle.
1052
    return super.debugAssertIsValid();
1053 1054
  }

Florian Loitsch's avatar
Florian Loitsch committed
1055
  /// The color to fill in the background of the box.
1056 1057 1058
  ///
  /// The color is filled into the shape of the box (e.g., either a rectangle,
  /// potentially with a border radius, or a circle).
1059
  final Color backgroundColor;
1060

Florian Loitsch's avatar
Florian Loitsch committed
1061
  /// An image to paint above the background color.
1062
  final BackgroundImage backgroundImage;
1063

Florian Loitsch's avatar
Florian Loitsch committed
1064
  /// A border to draw above the background.
1065
  final Border border;
1066

1067
  /// If non-null, the corners of this box are rounded by this [BorderRadius].
1068 1069
  ///
  /// Applies only to boxes with rectangular shapes.
1070
  final BorderRadius borderRadius;
1071

Florian Loitsch's avatar
Florian Loitsch committed
1072
  /// A list of shadows cast by this box behind the background.
1073
  final List<BoxShadow> boxShadow;
1074

Florian Loitsch's avatar
Florian Loitsch committed
1075
  /// A gradient to use when filling the background.
1076
  final Gradient gradient;
1077

Florian Loitsch's avatar
Florian Loitsch committed
1078
  /// The shape to fill the background color into and to cast as a shadow.
1079
  final BoxShape shape;
1080

1081
  /// The inset space occupied by the border.
1082
  @override
1083
  EdgeInsets get padding => border?.dimensions;
1084

Florian Loitsch's avatar
Florian Loitsch committed
1085
  /// Returns a new box decoration that is scaled by the given factor.
1086 1087 1088
  BoxDecoration scale(double factor) {
    // TODO(abarth): Scale ALL the things.
    return new BoxDecoration(
Adam Barth's avatar
Adam Barth committed
1089
      backgroundColor: Color.lerp(null, backgroundColor, factor),
1090
      backgroundImage: backgroundImage,
1091
      border: Border.lerp(null, border, factor),
1092
      borderRadius: BorderRadius.lerp(null, borderRadius, factor),
1093
      boxShadow: BoxShadow.lerpList(null, boxShadow, factor),
1094 1095 1096 1097 1098
      gradient: gradient,
      shape: shape
    );
  }

1099 1100 1101
  @override
  bool get isComplex => boxShadow != null;

1102
  /// Linearly interpolate between two box decorations.
1103 1104
  ///
  /// Interpolates each parameter of the box decoration separately.
1105 1106
  ///
  /// See also [Decoration.lerp].
1107 1108 1109 1110 1111 1112 1113 1114 1115
  static BoxDecoration lerp(BoxDecoration a, BoxDecoration b, double t) {
    if (a == null && b == null)
      return null;
    if (a == null)
      return b.scale(t);
    if (b == null)
      return a.scale(1.0 - t);
    // TODO(abarth): lerp ALL the fields.
    return new BoxDecoration(
Adam Barth's avatar
Adam Barth committed
1116
      backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
1117
      backgroundImage: b.backgroundImage,
1118
      border: Border.lerp(a.border, b.border, t),
1119
      borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t),
1120 1121 1122 1123 1124 1125
      boxShadow: BoxShadow.lerpList(a.boxShadow, b.boxShadow, t),
      gradient: b.gradient,
      shape: b.shape
    );
  }

1126
  @override
1127 1128 1129 1130 1131 1132
  BoxDecoration lerpFrom(Decoration a, double t) {
    if (a is! BoxDecoration)
      return BoxDecoration.lerp(null, this, t);
    return BoxDecoration.lerp(a, this, t);
  }

1133
  @override
1134 1135 1136 1137 1138 1139
  BoxDecoration lerpTo(Decoration b, double t) {
    if (b is! BoxDecoration)
      return BoxDecoration.lerp(this, null, t);
    return BoxDecoration.lerp(this, b, t);
  }

1140
  @override
1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! BoxDecoration)
      return false;
    final BoxDecoration typedOther = other;
    return backgroundColor == typedOther.backgroundColor &&
           backgroundImage == typedOther.backgroundImage &&
           border == typedOther.border &&
           borderRadius == typedOther.borderRadius &&
           boxShadow == typedOther.boxShadow &&
           gradient == typedOther.gradient &&
           shape == typedOther.shape;
  }

1156
  @override
1157
  int get hashCode {
1158 1159 1160 1161 1162 1163 1164 1165 1166
    return hashValues(
      backgroundColor,
      backgroundImage,
      border,
      borderRadius,
      boxShadow,
      gradient,
      shape
    );
1167 1168
  }

1169 1170 1171
  /// Stringifies the BoxDecoration. By default, the output will be on one line.
  /// If the method is passed a non-empty string argument, then the output will
  /// span multiple lines, each prefixed by that argument.
1172
  @override
1173
  String toString([String prefix = '']) {
Hixie's avatar
Hixie committed
1174
    List<String> result = <String>[];
1175 1176 1177 1178 1179 1180 1181 1182 1183
    if (backgroundColor != null)
      result.add('${prefix}backgroundColor: $backgroundColor');
    if (backgroundImage != null)
      result.add('${prefix}backgroundImage: $backgroundImage');
    if (border != null)
      result.add('${prefix}border: $border');
    if (borderRadius != null)
      result.add('${prefix}borderRadius: $borderRadius');
    if (boxShadow != null)
Hixie's avatar
Hixie committed
1184
      result.add('${prefix}boxShadow: ${boxShadow.map((BoxShadow shadow) => shadow.toString())}');
1185 1186
    if (gradient != null)
      result.add('${prefix}gradient: $gradient');
1187
    if (shape != BoxShape.rectangle)
1188
      result.add('${prefix}shape: $shape');
1189 1190
    if (prefix == '')
      return '$runtimeType(${result.join(', ')})';
1191
    if (result.isEmpty)
Hixie's avatar
Hixie committed
1192
      return '$prefix<no decorations specified>';
1193 1194
    return result.join('\n');
  }
1195

1196
  @override
1197 1198 1199 1200 1201 1202
  bool hitTest(Size size, Point position) {
    assert(shape != null);
    assert((Point.origin & size).contains(position));
    switch (shape) {
      case BoxShape.rectangle:
        if (borderRadius != null) {
1203
          RRect bounds = borderRadius.toRRect(Point.origin & size);
1204 1205 1206 1207 1208 1209 1210 1211 1212
          return bounds.contains(position);
        }
        return true;
      case BoxShape.circle:
        // Circles are inscribed into our smallest dimension.
        Point center = size.center(Point.origin);
        double distance = (position - center).distance;
        return distance <= math.min(size.width, size.height) / 2.0;
    }
pq's avatar
pq committed
1213
    assert(shape != null);
pq's avatar
pq committed
1214
    return null;
1215 1216
  }

1217
  @override
1218 1219 1220 1221
  _BoxDecorationPainter createBoxPainter([VoidCallback onChanged]) {
    assert(onChanged != null || backgroundImage == null);
    return new _BoxDecorationPainter(this, onChanged);
  }
1222 1223
}

Florian Loitsch's avatar
Florian Loitsch committed
1224
/// An object that paints a [BoxDecoration] into a canvas.
1225
class _BoxDecorationPainter extends BoxPainter {
1226
  _BoxDecorationPainter(this._decoration, VoidCallback onChange) : super(onChange) {
1227
    assert(_decoration != null);
1228 1229
  }

1230
  final BoxDecoration _decoration;
1231 1232

  Paint _cachedBackgroundPaint;
1233 1234 1235 1236 1237 1238
  Rect _rectForCachedBackgroundPaint;
  Paint _getBackgroundPaint(Rect rect) {
    assert(rect != null);
    if (_cachedBackgroundPaint == null ||
        (_decoration.gradient == null && _rectForCachedBackgroundPaint != null) ||
        (_decoration.gradient != null && _rectForCachedBackgroundPaint != rect)) {
1239 1240 1241 1242 1243
      Paint paint = new Paint();

      if (_decoration.backgroundColor != null)
        paint.color = _decoration.backgroundColor;

1244 1245 1246 1247 1248 1249
      if (_decoration.gradient != null) {
        paint.shader = _decoration.gradient.createShader(rect);
        _rectForCachedBackgroundPaint = rect;
      } else {
        _rectForCachedBackgroundPaint = null;
      }
1250 1251 1252 1253 1254 1255 1256

      _cachedBackgroundPaint = paint;
    }

    return _cachedBackgroundPaint;
  }

1257
  void _paintBox(Canvas canvas, Rect rect, Paint paint) {
Hans Muller's avatar
Hans Muller committed
1258
    switch (_decoration.shape) {
1259
      case BoxShape.circle:
Hans Muller's avatar
Hans Muller committed
1260 1261 1262 1263 1264
        assert(_decoration.borderRadius == null);
        Point center = rect.center;
        double radius = rect.shortestSide / 2.0;
        canvas.drawCircle(center, radius, paint);
        break;
1265
      case BoxShape.rectangle:
Hans Muller's avatar
Hans Muller committed
1266 1267 1268
        if (_decoration.borderRadius == null) {
          canvas.drawRect(rect, paint);
        } else {
1269
          canvas.drawRRect(_decoration.borderRadius.toRRect(rect), paint);
Hans Muller's avatar
Hans Muller committed
1270 1271
        }
        break;
1272 1273 1274
    }
  }

1275
  void _paintShadows(Canvas canvas, Rect rect) {
Hans Muller's avatar
Hans Muller committed
1276 1277 1278 1279 1280
    if (_decoration.boxShadow == null)
      return;
    for (BoxShadow boxShadow in _decoration.boxShadow) {
      final Paint paint = new Paint()
        ..color = boxShadow.color
Adam Barth's avatar
Adam Barth committed
1281
        ..maskFilter = new MaskFilter.blur(BlurStyle.normal, boxShadow.blurSigma);
Hans Muller's avatar
Hans Muller committed
1282 1283 1284 1285 1286
      final Rect bounds = rect.shift(boxShadow.offset).inflate(boxShadow.spreadRadius);
      _paintBox(canvas, bounds, paint);
    }
  }

1287
  void _paintBackgroundColor(Canvas canvas, Rect rect) {
Hans Muller's avatar
Hans Muller committed
1288
    if (_decoration.backgroundColor != null || _decoration.gradient != null)
1289
      _paintBox(canvas, rect, _getBackgroundPaint(rect));
Hans Muller's avatar
Hans Muller committed
1290 1291
  }

1292 1293 1294 1295
  ImageStream _imageStream;
  ImageInfo _image;

  void _paintBackgroundImage(Canvas canvas, Rect rect, ImageConfiguration configuration) {
1296 1297
    final BackgroundImage backgroundImage = _decoration.backgroundImage;
    if (backgroundImage == null)
1298
      return;
1299 1300 1301 1302 1303 1304 1305
    final ImageStream newImageStream = backgroundImage.image.resolve(configuration);
    if (newImageStream.key != _imageStream?.key) {
      _imageStream?.removeListener(_imageListener);
      _imageStream = newImageStream;
      _imageStream.addListener(_imageListener);
    }
    final ui.Image image = _image?.image;
1306 1307 1308 1309 1310 1311 1312
    if (image == null)
      return;
    paintImage(
      canvas: canvas,
      rect: rect,
      image: image,
      colorFilter: backgroundImage.colorFilter,
1313
      alignment: backgroundImage.alignment,
1314
      fit: backgroundImage.fit,
1315 1316
      repeat: backgroundImage.repeat
    );
1317 1318
  }

1319
  void _imageListener(ImageInfo value, bool synchronousCall) {
1320 1321 1322 1323
    if (_image == value)
      return;
    _image = value;
    assert(onChanged != null);
1324 1325
    if (!synchronousCall)
      onChanged();
1326 1327 1328 1329 1330 1331 1332 1333 1334 1335
  }

  @override
  void dispose() {
    _imageStream?.removeListener(_imageListener);
    _imageStream = null;
    _image = null;
    super.dispose();
  }

1336
  /// Paint the box decoration into the given location on the given canvas
1337
  @override
1338 1339 1340 1341
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    assert(configuration != null);
    assert(configuration.size != null);
    final Rect rect = offset & configuration.size;
Hans Muller's avatar
Hans Muller committed
1342
    _paintShadows(canvas, rect);
1343
    _paintBackgroundColor(canvas, rect);
1344
    _paintBackgroundImage(canvas, rect, configuration);
1345 1346 1347 1348 1349 1350
    _decoration.border?.paint(
      canvas,
      rect,
      shape: _decoration.shape,
      borderRadius: _decoration.borderRadius
    );
1351 1352
  }
}