From 9fc29dbbb84121c574c63df720e04fa7614e19a3 Mon Sep 17 00:00:00 2001 From: Hixie Date: Thu, 24 Mar 2016 12:53:17 -0700 Subject: [PATCH] Support hairline borders Previously, border with '0' was ambiguous. Sometimes we treated it as hairline borders, sometimes as "don't show the border", though even in the latter case we did some graphics work sometimes. Now we have an explicit BorderStyle.none flag to not draw the border efficiently. --- dev/manual_tests/drag_and_drop.dart | 2 +- examples/layers/widgets/sectors.dart | 2 +- .../material_gallery/lib/demo/list_demo.dart | 2 +- .../demo/persistent_bottom_sheet_demo.dart | 2 +- examples/stocks/lib/stock_symbol_viewer.dart | 2 +- .../flutter/lib/src/painting/box_painter.dart | 207 +++++++++++++----- 6 files changed, 159 insertions(+), 58 deletions(-) diff --git a/dev/manual_tests/drag_and_drop.dart b/dev/manual_tests/drag_and_drop.dart index f7c5a761bf..a8fd7b7d24 100644 --- a/dev/manual_tests/drag_and_drop.dart +++ b/dev/manual_tests/drag_and_drop.dart @@ -64,7 +64,7 @@ class DotState extends State { height: config.size, decoration: new BoxDecoration( backgroundColor: config.color, - border: new Border.all(color: const Color(0xFF000000), width: taps.toDouble()), + border: new Border.all(width: taps.toDouble()), shape: BoxShape.circle ), child: config.child diff --git a/examples/layers/widgets/sectors.dart b/examples/layers/widgets/sectors.dart index 33ac58437e..7e5fc9f8eb 100644 --- a/examples/layers/widgets/sectors.dart +++ b/examples/layers/widgets/sectors.dart @@ -134,7 +134,7 @@ class SectorAppState extends State { child: new Container( margin: new EdgeInsets.all(8.0), decoration: new BoxDecoration( - border: new Border.all(color: new Color(0xFF000000)) + border: new Border.all() ), padding: new EdgeInsets.all(8.0), child: new WidgetToRenderBoxAdapter( diff --git a/examples/material_gallery/lib/demo/list_demo.dart b/examples/material_gallery/lib/demo/list_demo.dart index daa92ff82d..903226e24b 100644 --- a/examples/material_gallery/lib/demo/list_demo.dart +++ b/examples/material_gallery/lib/demo/list_demo.dart @@ -42,7 +42,7 @@ class ListDemoState extends State { _bottomSheet = scaffoldKey.currentState.showBottomSheet((BuildContext bottomSheetContext) { return new Container( decoration: new BoxDecoration( - border: new Border(top: new BorderSide(color: Colors.black26, width: 1.0)) + border: new Border(top: new BorderSide(color: Colors.black26)) ), child: new Column( mainAxisAlignment: MainAxisAlignment.collapse, diff --git a/examples/material_gallery/lib/demo/persistent_bottom_sheet_demo.dart b/examples/material_gallery/lib/demo/persistent_bottom_sheet_demo.dart index 5709593f8d..0d11a95d3a 100644 --- a/examples/material_gallery/lib/demo/persistent_bottom_sheet_demo.dart +++ b/examples/material_gallery/lib/demo/persistent_bottom_sheet_demo.dart @@ -16,7 +16,7 @@ class PersistentBottomSheetDemo extends StatelessWidget { Scaffold.of(context).showBottomSheet((_) { return new Container( decoration: new BoxDecoration( - border: new Border(top: new BorderSide(color: Colors.black26, width: 1.0)) + border: new Border(top: new BorderSide(color: Colors.black26)) ), child: new Padding( padding: const EdgeInsets.all(32.0), diff --git a/examples/stocks/lib/stock_symbol_viewer.dart b/examples/stocks/lib/stock_symbol_viewer.dart index 5f2ff07bd0..6812fedc12 100644 --- a/examples/stocks/lib/stock_symbol_viewer.dart +++ b/examples/stocks/lib/stock_symbol_viewer.dart @@ -100,7 +100,7 @@ class StockSymbolBottomSheet extends StatelessWidget { return new Container( padding: new EdgeInsets.all(10.0), decoration: new BoxDecoration( - border: new Border(top: new BorderSide(color: Colors.black26, width: 1.0)) + border: new Border(top: new BorderSide(color: Colors.black26)) ), child: new StockSymbolView(stock: stock) ); diff --git a/packages/flutter/lib/src/painting/box_painter.dart b/packages/flutter/lib/src/painting/box_painter.dart index 50cd3981a3..3823188e16 100644 --- a/packages/flutter/lib/src/painting/box_painter.dart +++ b/packages/flutter/lib/src/painting/box_painter.dart @@ -25,30 +25,52 @@ double _getEffectiveBorderRadius(Rect rect, double borderRadius) { return borderRadius > shortestSide ? shortestSide : borderRadius; } +/// 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. class BorderSide { const BorderSide({ this.color: const Color(0xFF000000), - this.width: 1.0 + 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. + /// 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; - /// A black border side of zero width. - static const BorderSide none = const BorderSide(width: 0.0); + /// 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 + double width, + BorderStyle style }) { return new BorderSide( color: color ?? this.color, - width: width ?? this.width + width: width ?? this.width, + style: style ?? this.style ); } @@ -56,9 +78,38 @@ class BorderSide { 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(a.color, b.color, t), - width: ui.lerpDouble(a.width, b.width, t) + color: Color.lerp(colorA, colorB, t), + width: ui.lerpDouble(a.width, b.width, t), + style: BorderStyle.solid ); } @@ -70,14 +121,15 @@ class BorderSide { return false; final BorderSide typedOther = other; return color == typedOther.color && - width == typedOther.width; + width == typedOther.width && + style == typedOther.style; } @override - int get hashCode => hashValues(color, width); + int get hashCode => hashValues(color, width, style); @override - String toString() => 'BorderSide($color, $width)'; + String toString() => 'BorderSide($color, $width, $style)'; } /// A border of a box, comprised of four sides. @@ -92,9 +144,10 @@ class Border { /// A uniform border with all sides the same color and width. factory Border.all({ Color color: const Color(0xFF000000), - double width: 1.0 + double width: 1.0, + BorderStyle style: BorderStyle.solid }) { - final BorderSide side = new BorderSide(color: color, width: width); + final BorderSide side = new BorderSide(color: color, width: width, style: style); return new Border(top: side, right: side, bottom: side, left: side); } @@ -134,6 +187,12 @@ class Border { left.width != topWidth) return false; + final BorderStyle topStyle = top.style; + if (right.style != topStyle || + bottom.style != topStyle || + left.style != topStyle) + return false; + return true; } @@ -186,57 +245,99 @@ class Border { assert(bottom != null); assert(left != null); - Paint paint = new Paint(); + Paint paint = new Paint() + ..strokeWidth = 0.0; // used for hairline borders Path path; - // TODO(ianh): Handle hairline border by drawing a single line instead of a wedge + 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: ; + } - paint.color = top.color; - path = new Path(); - path.moveTo(rect.left, rect.top); - path.lineTo(rect.left + left.width, rect.top + top.width); - path.lineTo(rect.right - right.width, rect.top + top.width); - path.lineTo(rect.right, rect.top); - path.close(); - canvas.drawPath(path, paint); + 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: ; + } - paint.color = right.color; - path = new Path(); - path.moveTo(rect.right, rect.top); - path.lineTo(rect.right - right.width, rect.top + top.width); - path.lineTo(rect.right - right.width, rect.bottom - bottom.width); - path.lineTo(rect.right, rect.bottom); - path.close(); - canvas.drawPath(path, paint); + 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: ; + } - paint.color = bottom.color; - path = new Path(); - path.moveTo(rect.right, rect.bottom); - path.lineTo(rect.right - right.width, rect.bottom - bottom.width); - path.lineTo(rect.left + left.width, rect.bottom - bottom.width); - path.lineTo(rect.left, rect.bottom); - path.close(); - canvas.drawPath(path, paint); - - paint.color = left.color; - path = new Path(); - path.moveTo(rect.left, rect.bottom); - path.lineTo(rect.left + left.width, rect.bottom - bottom.width); - path.lineTo(rect.left + left.width, rect.top + top.width); - path.lineTo(rect.left, rect.top); - path.close(); - canvas.drawPath(path, paint); + 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; + case BorderStyle.none: ; + } } void _paintBorderWithRadius(Canvas canvas, Rect rect, double borderRadius) { assert(isUniform); - Color color = top.color; - double width = top.width; + Paint paint = new Paint() + ..color = top.color; double radius = _getEffectiveBorderRadius(rect, borderRadius); - // TODO(ianh): Handle hairline borders by just drawing an RRect instead RRect outer = new RRect.fromRectXY(rect, radius, radius); - RRect inner = new RRect.fromRectXY(rect.deflate(width), radius - width, radius - width); - canvas.drawDRRect(outer, inner, new Paint()..color = color); + double width = top.width; + if (width == 0.0) { + paint + ..style = PaintingStyle.stroke + ..strokeWidth = 0.0; + canvas.drawRRect(outer, paint); + } else { + RRect inner = new RRect.fromRectXY(rect.deflate(width), radius - width, radius - width); + canvas.drawDRRect(outer, inner, paint); + } } void _paintBorderWithCircle(Canvas canvas, Rect rect) {