diff --git a/packages/flutter/lib/painting.dart b/packages/flutter/lib/painting.dart index 468c2ff6656..1296f09b71f 100644 --- a/packages/flutter/lib/painting.dart +++ b/packages/flutter/lib/painting.dart @@ -19,6 +19,7 @@ library painting; export 'src/painting/alignment.dart'; export 'src/painting/basic_types.dart'; +export 'src/painting/beveled_rectangle_border.dart'; export 'src/painting/binding.dart'; export 'src/painting/border_radius.dart'; export 'src/painting/borders.dart'; diff --git a/packages/flutter/lib/src/painting/beveled_rectangle_border.dart b/packages/flutter/lib/src/painting/beveled_rectangle_border.dart new file mode 100644 index 00000000000..0ebef7e48a4 --- /dev/null +++ b/packages/flutter/lib/src/painting/beveled_rectangle_border.dart @@ -0,0 +1,151 @@ +// Copyright 2018 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; + +import 'basic_types.dart'; +import 'border_radius.dart'; +import 'borders.dart'; +import 'edge_insets.dart'; + +/// A rectangular border with flattened or "beveled" corners. +/// +/// The line segments that connect the rectangle's four sides will +/// begin and at locations offset by the corresponding border radius, +/// but not farther than the side's center. If all the border radii +/// exceed the sides' half widths/heights the resulting shape is +/// diamond made by connecting the centers of the sides. +class BeveledRectangleBorder extends ShapeBorder { + /// Creates a border like a [RoundedRectangleBorder] except that the corners + /// are joined by straight lines instead of arcs. + /// + /// The arguments must not be null. + const BeveledRectangleBorder({ + this.side: BorderSide.none, + this.borderRadius: BorderRadius.zero, + }) : assert(side != null), + assert(borderRadius != null); + + /// The style of this border. + final BorderSide side; + + /// The radii for each corner. + /// + /// Each corner [Radius] defines the endpoints of a line segment that + /// spans the corner. The endpoints are located in the same place as + /// they would be for [RoundedRectangleBorder], but they're connected + /// by a straight line instead of an arc. + /// + /// Negative radius values are clamped to 0.0 by [getInnerPath] and + /// [getOuterPath]. + final BorderRadiusGeometry borderRadius; + + @override + EdgeInsetsGeometry get dimensions { + return new EdgeInsets.all(side.width); + } + + @override + ShapeBorder scale(double t) { + return new BeveledRectangleBorder( + side: side.scale(t), + borderRadius: borderRadius * t, + ); + } + + @override + ShapeBorder lerpFrom(ShapeBorder a, double t) { + assert(t != null); + if (a is BeveledRectangleBorder) { + return new BeveledRectangleBorder( + side: BorderSide.lerp(a.side, side, t), + borderRadius: BorderRadius.lerp(a.borderRadius, borderRadius, t), + ); + } + return super.lerpFrom(a, t); + } + + @override + ShapeBorder lerpTo(ShapeBorder b, double t) { + assert(t != null); + if (b is BeveledRectangleBorder) { + return new BeveledRectangleBorder( + side: BorderSide.lerp(side, b.side, t), + borderRadius: BorderRadius.lerp(borderRadius, b.borderRadius, t), + ); + } + return super.lerpTo(b, t); + } + + Path _getPath(RRect rrect) { + final Offset centerLeft = new Offset(rrect.left, rrect.center.dy); + final Offset centerRight = new Offset(rrect.right, rrect.center.dy); + final Offset centerTop = new Offset(rrect.center.dx, rrect.top); + final Offset centerBottom = new Offset(rrect.center.dx, rrect.bottom); + + final double tlRadiusX = math.max(0.0, rrect.tlRadiusX); + final double tlRadiusY = math.max(0.0, rrect.tlRadiusY); + final double trRadiusX = math.max(0.0, rrect.trRadiusX); + final double trRadiusY = math.max(0.0, rrect.trRadiusY); + final double blRadiusX = math.max(0.0, rrect.blRadiusX); + final double blRadiusY = math.max(0.0, rrect.blRadiusY); + final double brRadiusX = math.max(0.0, rrect.brRadiusX); + final double brRadiusY = math.max(0.0, rrect.brRadiusY); + + final List vertices = [ + new Offset(rrect.left, math.min(centerLeft.dy, rrect.top + tlRadiusY)), + new Offset(math.min(centerTop.dx, rrect.left + tlRadiusX), rrect.top), + new Offset(math.max(centerTop.dx, rrect.right -trRadiusX), rrect.top), + new Offset(rrect.right, math.min(centerRight.dy, rrect.top + trRadiusY)), + new Offset(rrect.right, math.max(centerRight.dy, rrect.bottom - brRadiusY)), + new Offset(math.max(centerBottom.dx, rrect.right - brRadiusX), rrect.bottom), + new Offset(math.min(centerBottom.dx, rrect.left + blRadiusX), rrect.bottom), + new Offset(rrect.left, math.max(centerLeft.dy, rrect.bottom - blRadiusY)), + ]; + + return new Path()..addPolygon(vertices, true); + } + + @override + Path getInnerPath(Rect rect, { TextDirection textDirection }) { + return _getPath(borderRadius.resolve(textDirection).toRRect(rect).deflate(side.width)); + } + + @override + Path getOuterPath(Rect rect, { TextDirection textDirection }) { + return _getPath(borderRadius.resolve(textDirection).toRRect(rect)); + } + + @override + void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) { + if (rect.isEmpty) + return; + switch (side.style) { + case BorderStyle.none: + break; + case BorderStyle.solid: + final Path path = getOuterPath(rect, textDirection: textDirection) + ..addPath(getInnerPath(rect, textDirection: textDirection), Offset.zero); + canvas.drawPath(path, side.toPaint()); + break; + } + } + + @override + bool operator ==(dynamic other) { + if (runtimeType != other.runtimeType) + return false; + final BeveledRectangleBorder typedOther = other; + return side == typedOther.side + && borderRadius == typedOther.borderRadius; + } + + @override + int get hashCode => hashValues(side, borderRadius); + + @override + String toString() { + return '$runtimeType($side, $borderRadius)'; + } +} diff --git a/packages/flutter/test/painting/beveled_rectangle_border_test.dart b/packages/flutter/test/painting/beveled_rectangle_border_test.dart new file mode 100644 index 00000000000..c10703c6379 --- /dev/null +++ b/packages/flutter/test/painting/beveled_rectangle_border_test.dart @@ -0,0 +1,60 @@ +// Copyright 2018 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/painting.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../rendering/mock_canvas.dart'; + +void main() { + test('BeveledRectangleBorder scale and lerp', () { + final BeveledRectangleBorder c10 = new BeveledRectangleBorder(side: const BorderSide(width: 10.0), borderRadius: new BorderRadius.circular(100.0)); + final BeveledRectangleBorder c15 = new BeveledRectangleBorder(side: const BorderSide(width: 15.0), borderRadius: new BorderRadius.circular(150.0)); + final BeveledRectangleBorder c20 = new BeveledRectangleBorder(side: const BorderSide(width: 20.0), borderRadius: new BorderRadius.circular(200.0)); + expect(c10.dimensions, const EdgeInsets.all(10.0)); + expect(c10.scale(2.0), c20); + expect(c20.scale(0.5), c10); + expect(ShapeBorder.lerp(c10, c20, 0.0), c10); + expect(ShapeBorder.lerp(c10, c20, 0.5), c15); + expect(ShapeBorder.lerp(c10, c20, 1.0), c20); + }); + + test('BeveledRectangleBorder BorderRadius.zero', () { + final Rect rect1 = new Rect.fromLTRB(10.0, 20.0, 30.0, 40.0); + final Matcher looksLikeRect1 = isPathThat( + includes: const [ const Offset(10.0, 20.0), const Offset(20.0, 30.0) ], + excludes: const [ const Offset(9.0, 19.0), const Offset(31.0, 41.0) ], + ); + + // Default border radius and border side are zero, i.e. just a rectangle. + expect(const BeveledRectangleBorder().getOuterPath(rect1), looksLikeRect1); + expect(const BeveledRectangleBorder().getInnerPath(rect1), looksLikeRect1); + + + // Represents the inner path when borderSide.width = 4, which is just rect1 + // inset by 4 on all sides. + final Matcher looksLikeInnerPath = isPathThat( + includes: const [ const Offset(14.0, 24.0), const Offset(16.0, 26.0) ], + excludes: const [ const Offset(9.0, 23.0), const Offset(27.0, 37.0) ], + ); + + const BorderSide side = const BorderSide(width: 4.0); + expect(const BeveledRectangleBorder(side: side).getOuterPath(rect1), looksLikeRect1); + expect(const BeveledRectangleBorder(side: side).getInnerPath(rect1), looksLikeInnerPath); + }); + + test('BeveledRectangleBorder non-zero BorderRadius', () { + final Rect rect = new Rect.fromLTRB(10.0, 20.0, 30.0, 40.0); + final Matcher looksLikeRect = isPathThat( + includes: const [ const Offset(15.0, 25.0), const Offset(20.0, 30.0) ], + excludes: const [ const Offset(10.0, 20.0), const Offset(30.0, 40.0) ], + ); + const BeveledRectangleBorder border = const BeveledRectangleBorder( + borderRadius: const BorderRadius.all(const Radius.circular(5.0)) + ); + expect(border.getOuterPath(rect), looksLikeRect); + expect(border.getInnerPath(rect), looksLikeRect); + }); + +}