From 90841c9617e24f005f58d243259c1161d8835ca2 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Tue, 12 Sep 2017 14:21:27 -0700 Subject: [PATCH] Factor out Border and BorderRadius into their own files. (#12055) This will make it more tractable to convert them for RTL. --- packages/flutter/lib/painting.dart | 2 + packages/flutter/lib/src/painting/border.dart | 493 ++++++++++++++ .../lib/src/painting/border_radius.dart | 157 +++++ .../flutter/lib/src/painting/box_painter.dart | 631 +----------------- 4 files changed, 654 insertions(+), 629 deletions(-) create mode 100644 packages/flutter/lib/src/painting/border.dart create mode 100644 packages/flutter/lib/src/painting/border_radius.dart diff --git a/packages/flutter/lib/painting.dart b/packages/flutter/lib/painting.dart index eba34b15c8..babe109172 100644 --- a/packages/flutter/lib/painting.dart +++ b/packages/flutter/lib/painting.dart @@ -18,6 +18,8 @@ library painting; export 'src/painting/basic_types.dart'; +export 'src/painting/border.dart'; +export 'src/painting/border_radius.dart'; export 'src/painting/box_fit.dart'; export 'src/painting/box_painter.dart'; export 'src/painting/colors.dart'; diff --git a/packages/flutter/lib/src/painting/border.dart b/packages/flutter/lib/src/painting/border.dart new file mode 100644 index 0000000000..5865dc8c32 --- /dev/null +++ b/packages/flutter/lib/src/painting/border.dart @@ -0,0 +1,493 @@ +// 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:ui' as ui show lerpDouble; + +import 'package:flutter/foundation.dart'; + +import 'basic_types.dart'; +import 'border_radius.dart'; +import 'decoration.dart'; +import 'edge_insets.dart'; + +export 'edge_insets.dart' show EdgeInsets; + +/// The shape to use when rendering a [Border] or [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 [Border] or [BoxDecoration] is painted. + rectangle, + + /// A circle centered in the middle of the box into which the [Border] or + /// [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, +} + +/// 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 +} + +/// A side of a border of a box. +/// +/// A [Border] consists of four [BorderSide] objects: [Border.top], +/// [Border.left], [Border.right], and [Border.bottom]. +/// +/// ## Sample code +/// +/// This sample shows how [BorderSide] objects can be used in a [Container], via +/// a [BoxDecoration] and a [Border], to decorate some [Text]. In this example, +/// the text has a thick bar above it that is light blue, and a thick bar below +/// it that is a darker shade of blue. +/// +/// ```dart +/// new Container( +/// padding: new EdgeInsets.all(8.0), +/// decoration: new BoxDecoration( +/// border: new Border( +/// top: new BorderSide(width: 16.0, color: Colors.lightBlue.shade50), +/// bottom: new BorderSide(width: 16.0, color: Colors.lightBlue.shade900), +/// ), +/// ), +/// child: new Text('Flutter in the sky', textAlign: TextAlign.center), +/// ) +/// ``` +/// +/// See also: +/// +/// * [Border], which uses [BorderSide] objects to represent its sides. +/// * [BoxDecoration], which optionally takes a [Border] object. +/// * [TableBorder], which extends [Border] to have two more sides +/// ([TableBorder.horizontalInside] and [TableBorder.verticalInside]), both +/// of which are also [BorderSide] objects. +@immutable +class BorderSide { + /// Creates the side of a border. + /// + /// By default, the border is 1.0 logical pixels wide and solid black. + const BorderSide({ + this.color: const Color(0xFF000000), + this.width: 1.0, + this.style: BorderStyle.solid + }); + + /// The color of this side of the border. + final Color color; + + /// 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]. + final double width; + + /// 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); + + /// Creates a copy of this border but with the given fields replaced with the new values. + BorderSide copyWith({ + Color color, + double width, + BorderStyle style + }) { + return new BorderSide( + color: color ?? this.color, + width: width ?? this.width, + style: style ?? this.style + ); + } + + /// Linearly interpolate between two border sides. + static BorderSide lerp(BorderSide a, BorderSide b, double t) { + assert(a != null); + assert(b != null); + 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; + } + return new BorderSide( + color: Color.lerp(colorA, colorB, t), + width: ui.lerpDouble(a.width, b.width, t), + style: BorderStyle.solid, + ); + } + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (runtimeType != other.runtimeType) + return false; + final BorderSide typedOther = other; + return color == typedOther.color && + width == typedOther.width && + style == typedOther.style; + } + + @override + int get hashCode => hashValues(color, width, style); + + @override + String toString() => 'BorderSide($color, $width, $style)'; +} + +/// A border of a box, comprised of four sides. +/// +/// The sides are represented by [BorderSide] objects. +/// +/// ## Sample code +/// +/// All four borders the same, two-pixel wide solid white: +/// +/// ```dart +/// new Border.all(width: 2.0, color: const Color(0xFFFFFFFF)) +/// ``` +/// +/// The border for a material design divider: +/// +/// ```dart +/// new Border(bottom: new BorderSide(color: Theme.of(context).dividerColor)) +/// ``` +/// +/// A 1990s-era "OK" button: +/// +/// ```dart +/// new Container( +/// decoration: const BoxDecoration( +/// border: const Border( +/// top: const BorderSide(width: 1.0, color: const Color(0xFFFFFFFFFF)), +/// left: const BorderSide(width: 1.0, color: const Color(0xFFFFFFFFFF)), +/// right: const BorderSide(width: 1.0, color: const Color(0xFFFF000000)), +/// bottom: const BorderSide(width: 1.0, color: const Color(0xFFFF000000)), +/// ), +/// ), +/// child: new Container( +/// padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 2.0), +/// decoration: const BoxDecoration( +/// border: const Border( +/// top: const BorderSide(width: 1.0, color: const Color(0xFFFFDFDFDF)), +/// left: const BorderSide(width: 1.0, color: const Color(0xFFFFDFDFDF)), +/// right: const BorderSide(width: 1.0, color: const Color(0xFFFF7F7F7F)), +/// bottom: const BorderSide(width: 1.0, color: const Color(0xFFFF7F7F7F)), +/// ), +/// color: const Color(0xFFBFBFBF), +/// ), +/// child: const Text( +/// 'OK', +/// textAlign: TextAlign.center, +/// style: const TextStyle(color: const Color(0xFF000000)) +/// ), +/// ), +/// ) +/// ``` +/// +/// See also: +/// +/// * [BoxDecoration], which uses this class to describe its edge decoration. +/// * [BorderSide], which is used to describe each side of the box. +/// * [Theme], from the material layer, which can be queried to obtain appropriate colors +/// to use for borders in a material app, as shown in the "divider" sample above. +@immutable +class Border { + /// Creates a border. + /// + /// All the sides of the border default to [BorderSide.none]. + const Border({ + this.top: BorderSide.none, + this.right: BorderSide.none, + this.bottom: BorderSide.none, + this.left: BorderSide.none, + }); + + /// A uniform border with all sides the same color and width. + /// + /// The sides default to black solid borders, one logical pixel wide. + factory Border.all({ + Color color: const Color(0xFF000000), + double width: 1.0, + BorderStyle style: BorderStyle.solid, + }) { + final BorderSide side = new BorderSide(color: color, width: width, style: style); + return new Border(top: side, right: side, bottom: side, left: side); + } + + /// The top side of this border. + final BorderSide top; + + /// The right side of this border. + final BorderSide right; + + /// The bottom side of this border. + final BorderSide bottom; + + /// The left side of this border. + final BorderSide left; + + /// The widths of the sides of this border represented as an [EdgeInsets]. + /// + /// This can be used, for example, with a [Padding] widget to inset a box by + /// the size of these borders. + EdgeInsets get dimensions { + return new EdgeInsets.fromLTRB(left.width, top.width, right.width, bottom.width); + } + + /// Whether all four sides of the border are identical. Uniform borders are + /// typically more efficient to paint. + 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; + + final BorderStyle topStyle = top.style; + if (right.style != topStyle || + bottom.style != topStyle || + left.style != topStyle) + return false; + + return true; + } + + /// Creates a new border with the widths of this border multiplied by `t`. + 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) + ); + } + + /// Linearly interpolate between two borders. + /// + /// If a border is null, it is treated as having four [BorderSide.none] + /// borders. + 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) + ); + } + + /// Paints the border within the given [Rect] on the given [Canvas]. + /// + /// Uniform borders are more efficient to paint than more complex borders. + /// + /// You can provide a [BoxShape] to draw the border on. If the shape in + /// [BoxShape.circle], there is the requirement that the border [isUniform]. + /// + /// If you specify a rectangular box shape (BoxShape.rectangle), then you may + /// specify a [BorderRadius]. If a border radius is specified, there is the + /// requirement that the border [isUniform]. + void paint(Canvas canvas, Rect rect, { + BoxShape shape: BoxShape.rectangle, + BorderRadius borderRadius, + }) { + if (isUniform) { + if (borderRadius != null) { + assert(shape == BoxShape.rectangle, 'A borderRadius can only be given for rectangular boxes.'); + _paintBorderWithRadius(canvas, rect, borderRadius); + return; + } + if (shape == BoxShape.circle) { + assert(borderRadius == null); + _paintBorderWithCircle(canvas, rect); + return; + } + } + + assert(borderRadius == null, 'A borderRadius can only be given for uniform borders.'); // TODO(abarth): Support non-uniform rounded borders. + assert(shape == BoxShape.rectangle, 'A border can only be drawn as a circle if it is uniform.'); // TODO(ianh): Support non-uniform borders on circles. + + assert(top != null); + assert(right != null); + assert(bottom != null); + assert(left != null); + + final Paint paint = new Paint() + ..strokeWidth = 0.0; // used for hairline borders + Path path; + + 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; + case BorderStyle.none: + break; + } + + 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; + case BorderStyle.none: + break; + } + + 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; + case BorderStyle.none: + break; + } + + 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 (left.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; + case BorderStyle.none: + break; + } + } + + void _paintBorderWithRadius(Canvas canvas, Rect rect, + BorderRadius borderRadius) { + assert(isUniform); + final Paint paint = new Paint() + ..color = top.color; + final RRect outer = borderRadius.toRRect(rect); + final double width = top.width; + if (width == 0.0) { + paint + ..style = PaintingStyle.stroke + ..strokeWidth = 0.0; + canvas.drawRRect(outer, paint); + } else { + final RRect inner = outer.deflate(width); + canvas.drawDRRect(outer, inner, paint); + } + } + + void _paintBorderWithCircle(Canvas canvas, Rect rect) { + assert(isUniform); + final double width = top.width; + final Paint paint = new Paint() + ..color = top.color + ..strokeWidth = width + ..style = PaintingStyle.stroke; + final double radius = (rect.shortestSide - width) / 2.0; + canvas.drawCircle(rect.center, radius, paint); + } + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (runtimeType != other.runtimeType) + return false; + final Border typedOther = other; + return top == typedOther.top && + right == typedOther.right && + bottom == typedOther.bottom && + left == typedOther.left; + } + + @override + int get hashCode => hashValues(top, right, bottom, left); + + @override + String toString() => 'Border($top, $right, $bottom, $left)'; +} diff --git a/packages/flutter/lib/src/painting/border_radius.dart b/packages/flutter/lib/src/painting/border_radius.dart new file mode 100644 index 0000000000..b6629a9fc0 --- /dev/null +++ b/packages/flutter/lib/src/painting/border_radius.dart @@ -0,0 +1,157 @@ +// 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 'package:flutter/foundation.dart'; + +import 'basic_types.dart'; + +/// An immutable set of radii for each corner of a rectangle. +/// +/// Used by [BoxDecoration] when the shape is a [BoxShape.rectangle]. +@immutable +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 (runtimeType != other.runtimeType) + 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() { + if (topLeft == topRight && + topRight == bottomRight && + bottomRight == bottomLeft) { + if (topLeft == Radius.zero) + return 'BorderRadius.zero'; + if (topLeft.x == topLeft.y) + return 'BorderRadius.circular(${topLeft.x.toStringAsFixed(1)})'; + return 'BorderRadius.all($topLeft)'; + } + if (topLeft == Radius.zero || + topRight == Radius.zero || + bottomLeft == Radius.zero || + bottomRight == Radius.zero) { + final StringBuffer result = new StringBuffer(); + result.write('BorderRadius.only('); + bool comma = false; + if (topLeft != Radius.zero) { + result.write('topLeft: $topLeft'); + comma = true; + } + if (topRight != Radius.zero) { + if (comma) + result.write(', '); + result.write('topRight: $topRight'); + comma = true; + } + if (bottomLeft != Radius.zero) { + if (comma) + result.write(', '); + result.write('bottomLeft: $bottomLeft'); + comma = true; + } + if (bottomRight != Radius.zero) { + if (comma) + result.write(', '); + result.write('bottomRight: $bottomRight'); + } + result.write(')'); + return result.toString(); + } + return 'BorderRadius($topLeft, $topRight, $bottomRight, $bottomLeft)'; + } +} diff --git a/packages/flutter/lib/src/painting/box_painter.dart b/packages/flutter/lib/src/painting/box_painter.dart index 2a79542af8..7fce46d22f 100644 --- a/packages/flutter/lib/src/painting/box_painter.dart +++ b/packages/flutter/lib/src/painting/box_painter.dart @@ -9,6 +9,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'basic_types.dart'; +import 'border.dart'; +import 'border_radius.dart'; import 'box_fit.dart'; import 'decoration.dart'; import 'edge_insets.dart'; @@ -16,635 +18,6 @@ import 'fractional_offset.dart'; export 'edge_insets.dart' show EdgeInsets; -/// 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]. -@immutable -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 (runtimeType != other.runtimeType) - 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() { - if (topLeft == topRight && - topRight == bottomRight && - bottomRight == bottomLeft) { - if (topLeft == Radius.zero) - return 'BorderRadius.zero'; - if (topLeft.x == topLeft.y) - return 'BorderRadius.circular(${topLeft.x.toStringAsFixed(1)})'; - return 'BorderRadius.all($topLeft)'; - } - if (topLeft == Radius.zero || - topRight == Radius.zero || - bottomLeft == Radius.zero || - bottomRight == Radius.zero) { - final StringBuffer result = new StringBuffer(); - result.write('BorderRadius.only('); - bool comma = false; - if (topLeft != Radius.zero) { - result.write('topLeft: $topLeft'); - comma = true; - } - if (topRight != Radius.zero) { - if (comma) - result.write(', '); - result.write('topRight: $topRight'); - comma = true; - } - if (bottomLeft != Radius.zero) { - if (comma) - result.write(', '); - result.write('bottomLeft: $bottomLeft'); - comma = true; - } - if (bottomRight != Radius.zero) { - if (comma) - result.write(', '); - result.write('bottomRight: $bottomRight'); - } - result.write(')'); - return result.toString(); - } - return 'BorderRadius($topLeft, $topRight, $bottomRight, $bottomLeft)'; - } -} - -/// 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 -} - -/// A side of a border of a box. -/// -/// A [Border] consists of four [BorderSide] objects: [Border.top], -/// [Border.left], [Border.right], and [Border.bottom]. -/// -/// ## Sample code -/// -/// This sample shows how [BorderSide] objects can be used in a [Container], via -/// a [BoxDecoration] and a [Border], to decorate some [Text]. In this example, -/// the text has a thick bar above it that is light blue, and a thick bar below -/// it that is a darker shade of blue. -/// -/// ```dart -/// new Container( -/// padding: new EdgeInsets.all(8.0), -/// decoration: new BoxDecoration( -/// border: new Border( -/// top: new BorderSide(width: 16.0, color: Colors.lightBlue.shade50), -/// bottom: new BorderSide(width: 16.0, color: Colors.lightBlue.shade900), -/// ), -/// ), -/// child: new Text('Flutter in the sky', textAlign: TextAlign.center), -/// ) -/// ``` -/// -/// See also: -/// -/// * [Border], which uses [BorderSide] objects to represent its sides. -/// * [BoxDecoration], which optionally takes a [Border] object. -/// * [TableBorder], which extends [Border] to have two more sides -/// ([TableBorder.horizontalInside] and [TableBorder.verticalInside]), both -/// of which are also [BorderSide] objects. -@immutable -class BorderSide { - /// Creates the side of a border. - /// - /// By default, the border is 1.0 logical pixels wide and solid black. - const BorderSide({ - this.color: const Color(0xFF000000), - this.width: 1.0, - this.style: BorderStyle.solid - }); - - /// The color of this side of the border. - final Color color; - - /// 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]. - final double width; - - /// 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); - - /// Creates a copy of this border but with the given fields replaced with the new values. - BorderSide copyWith({ - Color color, - double width, - BorderStyle style - }) { - return new BorderSide( - color: color ?? this.color, - width: width ?? this.width, - style: style ?? this.style - ); - } - - /// Linearly interpolate between two border sides. - static BorderSide lerp(BorderSide a, BorderSide b, double t) { - assert(a != null); - assert(b != null); - 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; - } - return new BorderSide( - color: Color.lerp(colorA, colorB, t), - width: ui.lerpDouble(a.width, b.width, t), - style: BorderStyle.solid, - ); - } - - @override - bool operator ==(dynamic other) { - if (identical(this, other)) - return true; - if (runtimeType != other.runtimeType) - return false; - final BorderSide typedOther = other; - return color == typedOther.color && - width == typedOther.width && - style == typedOther.style; - } - - @override - int get hashCode => hashValues(color, width, style); - - @override - String toString() => 'BorderSide($color, $width, $style)'; -} - -/// A border of a box, comprised of four sides. -/// -/// The sides are represented by [BorderSide] objects. -/// -/// ## Sample code -/// -/// All four borders the same, two-pixel wide solid white: -/// -/// ```dart -/// new Border.all(width: 2.0, color: const Color(0xFFFFFFFF)) -/// ``` -/// -/// The border for a material design divider: -/// -/// ```dart -/// new Border(bottom: new BorderSide(color: Theme.of(context).dividerColor)) -/// ``` -/// -/// A 1990s-era "OK" button: -/// -/// ```dart -/// new Container( -/// decoration: const BoxDecoration( -/// border: const Border( -/// top: const BorderSide(width: 1.0, color: const Color(0xFFFFFFFFFF)), -/// left: const BorderSide(width: 1.0, color: const Color(0xFFFFFFFFFF)), -/// right: const BorderSide(width: 1.0, color: const Color(0xFFFF000000)), -/// bottom: const BorderSide(width: 1.0, color: const Color(0xFFFF000000)), -/// ), -/// ), -/// child: new Container( -/// padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 2.0), -/// decoration: const BoxDecoration( -/// border: const Border( -/// top: const BorderSide(width: 1.0, color: const Color(0xFFFFDFDFDF)), -/// left: const BorderSide(width: 1.0, color: const Color(0xFFFFDFDFDF)), -/// right: const BorderSide(width: 1.0, color: const Color(0xFFFF7F7F7F)), -/// bottom: const BorderSide(width: 1.0, color: const Color(0xFFFF7F7F7F)), -/// ), -/// color: const Color(0xFFBFBFBF), -/// ), -/// child: const Text( -/// 'OK', -/// textAlign: TextAlign.center, -/// style: const TextStyle(color: const Color(0xFF000000)) -/// ), -/// ), -/// ) -/// ``` -/// -/// See also: -/// -/// * [BoxDecoration], which uses this class to describe its edge decoration. -/// * [BorderSide], which is used to describe each side of the box. -/// * [Theme], from the material layer, which can be queried to obtain appropriate colors -/// to use for borders in a material app, as shown in the "divider" sample above. -@immutable -class Border { - /// Creates a border. - /// - /// All the sides of the border default to [BorderSide.none]. - const Border({ - this.top: BorderSide.none, - this.right: BorderSide.none, - this.bottom: BorderSide.none, - this.left: BorderSide.none, - }); - - /// A uniform border with all sides the same color and width. - /// - /// The sides default to black solid borders, one logical pixel wide. - factory Border.all({ - Color color: const Color(0xFF000000), - double width: 1.0, - BorderStyle style: BorderStyle.solid, - }) { - final BorderSide side = new BorderSide(color: color, width: width, style: style); - return new Border(top: side, right: side, bottom: side, left: side); - } - - /// The top side of this border. - final BorderSide top; - - /// The right side of this border. - final BorderSide right; - - /// The bottom side of this border. - final BorderSide bottom; - - /// The left side of this border. - final BorderSide left; - - /// The widths of the sides of this border represented as an [EdgeInsets]. - /// - /// This can be used, for example, with a [Padding] widget to inset a box by - /// the size of these borders. - EdgeInsets get dimensions { - return new EdgeInsets.fromLTRB(left.width, top.width, right.width, bottom.width); - } - - /// Whether all four sides of the border are identical. Uniform borders are - /// typically more efficient to paint. - 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; - - final BorderStyle topStyle = top.style; - if (right.style != topStyle || - bottom.style != topStyle || - left.style != topStyle) - return false; - - return true; - } - - /// Creates a new border with the widths of this border multiplied by `t`. - 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) - ); - } - - /// Linearly interpolate between two borders. - /// - /// If a border is null, it is treated as having four [BorderSide.none] - /// borders. - 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) - ); - } - - /// Paints the border within the given [Rect] on the given [Canvas]. - /// - /// Uniform borders are more efficient to paint than more complex borders. - /// - /// You can provide a [BoxShape] to draw the border on. If the shape in - /// [BoxShape.circle], there is the requirement that the border [isUniform]. - /// - /// If you specify a rectangular box shape (BoxShape.rectangle), then you may - /// specify a [BorderRadius]. If a border radius is specified, there is the - /// requirement that the border [isUniform]. - void paint(Canvas canvas, Rect rect, { - BoxShape shape: BoxShape.rectangle, - BorderRadius borderRadius, - }) { - if (isUniform) { - if (borderRadius != null) { - assert(shape == BoxShape.rectangle, 'A borderRadius can only be given for rectangular boxes.'); - _paintBorderWithRadius(canvas, rect, borderRadius); - return; - } - if (shape == BoxShape.circle) { - assert(borderRadius == null); - _paintBorderWithCircle(canvas, rect); - return; - } - } - - assert(borderRadius == null, 'A borderRadius can only be given for uniform borders.'); // TODO(abarth): Support non-uniform rounded borders. - assert(shape == BoxShape.rectangle, 'A border can only be drawn as a circle if it is uniform.'); // TODO(ianh): Support non-uniform borders on circles. - - assert(top != null); - assert(right != null); - assert(bottom != null); - assert(left != null); - - final Paint paint = new Paint() - ..strokeWidth = 0.0; // used for hairline borders - Path path; - - 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; - case BorderStyle.none: - break; - } - - 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; - case BorderStyle.none: - break; - } - - 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; - case BorderStyle.none: - break; - } - - 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 (left.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; - case BorderStyle.none: - break; - } - } - - void _paintBorderWithRadius(Canvas canvas, Rect rect, - BorderRadius borderRadius) { - assert(isUniform); - final Paint paint = new Paint() - ..color = top.color; - final RRect outer = borderRadius.toRRect(rect); - final double width = top.width; - if (width == 0.0) { - paint - ..style = PaintingStyle.stroke - ..strokeWidth = 0.0; - canvas.drawRRect(outer, paint); - } else { - final RRect inner = outer.deflate(width); - canvas.drawDRRect(outer, inner, paint); - } - } - - void _paintBorderWithCircle(Canvas canvas, Rect rect) { - assert(isUniform); - final double width = top.width; - final Paint paint = new Paint() - ..color = top.color - ..strokeWidth = width - ..style = PaintingStyle.stroke; - final double radius = (rect.shortestSide - width) / 2.0; - canvas.drawCircle(rect.center, radius, paint); - } - - @override - bool operator ==(dynamic other) { - if (identical(this, other)) - return true; - if (runtimeType != other.runtimeType) - return false; - final Border typedOther = other; - return top == typedOther.top && - right == typedOther.right && - bottom == typedOther.bottom && - left == typedOther.left; - } - - @override - int get hashCode => hashValues(top, right, bottom, left); - - @override - String toString() => 'Border($top, $right, $bottom, $left)'; -} - /// A shadow cast by a box. /// /// BoxShadow can cast non-rectangular shadows if the box is non-rectangular