diff --git a/packages/flutter/lib/painting.dart b/packages/flutter/lib/painting.dart index 572391a93d..33dc9da89c 100644 --- a/packages/flutter/lib/painting.dart +++ b/packages/flutter/lib/painting.dart @@ -49,6 +49,7 @@ export 'src/painting/image_stream.dart'; export 'src/painting/inline_span.dart'; export 'src/painting/matrix_utils.dart'; export 'src/painting/notched_shapes.dart'; +export 'src/painting/oval_border.dart'; export 'src/painting/paint_utilities.dart'; export 'src/painting/placeholder_span.dart'; export 'src/painting/rounded_rectangle_border.dart'; diff --git a/packages/flutter/lib/src/painting/circle_border.dart b/packages/flutter/lib/src/painting/circle_border.dart index 1702f38a3b..24b225160d 100644 --- a/packages/flutter/lib/src/painting/circle_border.dart +++ b/packages/flutter/lib/src/painting/circle_border.dart @@ -2,7 +2,7 @@ // 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 'dart:ui' as ui show lerpDouble; import 'package:flutter/foundation.dart'; @@ -18,16 +18,32 @@ import 'edge_insets.dart'; /// When applied to a rectangular space, the border paints in the center of the /// rectangle. /// +/// The [eccentricity] parameter describes how much a circle will deform to +/// fit the rectangle it is a border for. A value of zero implies no +/// deformation (a circle touching at least two sides of the rectangle), a +/// value of one implies full deformation (an oval touching all sides of the +/// rectangle). +/// /// See also: /// +/// * [OvalBorder], which draws a Circle touching all the edges of the box. /// * [BorderSide], which is used to describe each side of the box. -/// * [Border], which, when used with [BoxDecoration], can also -/// describe a circle. +/// * [Border], which, when used with [BoxDecoration], can also describe a circle. class CircleBorder extends OutlinedBorder { /// Create a circle border. /// /// The [side] argument must not be null. - const CircleBorder({ super.side }) : assert(side != null); + const CircleBorder({ super.side, this.eccentricity = 0.0 }) + : assert(side != null), + assert(eccentricity != null), + assert(eccentricity >= 0.0, 'The eccentricity argument $eccentricity is not greater than or equal to zero.'), + assert(eccentricity <= 1.0, 'The eccentricity argument $eccentricity is not less than or equal to one.'); + + /// Defines the ratio (0.0-1.0) from which the border will deform + /// to fit a rectangle. + /// When 0.0, it draws a circle touching at least two sides of the rectangle. + /// When 1.0, it draws an oval touching all sides of the rectangle. + final double eccentricity; @override EdgeInsetsGeometry get dimensions { @@ -42,12 +58,15 @@ class CircleBorder extends OutlinedBorder { } @override - ShapeBorder scale(double t) => CircleBorder(side: side.scale(t)); + ShapeBorder scale(double t) => CircleBorder(side: side.scale(t), eccentricity: eccentricity); @override ShapeBorder? lerpFrom(ShapeBorder? a, double t) { if (a is CircleBorder) { - return CircleBorder(side: BorderSide.lerp(a.side, side, t)); + return CircleBorder( + side: BorderSide.lerp(a.side, side, t), + eccentricity: clampDouble(ui.lerpDouble(a.eccentricity, eccentricity, t)!, 0.0, 1.0), + ); } return super.lerpFrom(a, t); } @@ -55,45 +74,40 @@ class CircleBorder extends OutlinedBorder { @override ShapeBorder? lerpTo(ShapeBorder? b, double t) { if (b is CircleBorder) { - return CircleBorder(side: BorderSide.lerp(side, b.side, t)); + return CircleBorder( + side: BorderSide.lerp(side, b.side, t), + eccentricity: clampDouble(ui.lerpDouble(eccentricity, b.eccentricity, t)!, 0.0, 1.0), + ); } return super.lerpTo(b, t); } @override Path getInnerPath(Rect rect, { TextDirection? textDirection }) { - final double radius = rect.shortestSide / 2.0; - final double adjustedRadius; + final double delta; switch (side.strokeAlign) { case StrokeAlign.inside: - adjustedRadius = radius - side.width; + delta = side.width; break; case StrokeAlign.center: - adjustedRadius = radius - side.width / 2.0; + delta = side.width / 2.0; break; case StrokeAlign.outside: - adjustedRadius = radius; + delta = 0; break; } - return Path() - ..addOval(Rect.fromCircle( - center: rect.center, - radius: math.max(0.0, adjustedRadius), - )); + final Rect adjustedRect = _adjustRect(rect).deflate(delta); + return Path()..addOval(adjustedRect); } @override Path getOuterPath(Rect rect, { TextDirection? textDirection }) { - return Path() - ..addOval(Rect.fromCircle( - center: rect.center, - radius: rect.shortestSide / 2.0, - )); + return Path()..addOval(_adjustRect(rect)); } @override - CircleBorder copyWith({ BorderSide? side }) { - return CircleBorder(side: side ?? this.side); + CircleBorder copyWith({ BorderSide? side, double? eccentricity }) { + return CircleBorder(side: side ?? this.side, eccentricity: eccentricity ?? this.eccentricity); } @override @@ -102,19 +116,59 @@ class CircleBorder extends OutlinedBorder { case BorderStyle.none: break; case BorderStyle.solid: - final double radius; - switch (side.strokeAlign) { - case StrokeAlign.inside: - radius = (rect.shortestSide - side.width) / 2.0; - break; - case StrokeAlign.center: - radius = rect.shortestSide / 2.0; - break; - case StrokeAlign.outside: - radius = (rect.shortestSide + side.width) / 2.0; - break; + if (eccentricity != 0.0) { + final Rect borderRect = _adjustRect(rect); + final Rect adjustedRect; + switch (side.strokeAlign) { + case StrokeAlign.inside: + adjustedRect = borderRect.deflate(side.width / 2.0); + break; + case StrokeAlign.center: + adjustedRect = borderRect; + break; + case StrokeAlign.outside: + adjustedRect = borderRect.inflate(side.width / 2.0); + break; + } + canvas.drawOval(adjustedRect, side.toPaint()); + } else { + final double radius; + switch (side.strokeAlign) { + case StrokeAlign.inside: + radius = (rect.shortestSide - side.width) / 2.0; + break; + case StrokeAlign.center: + radius = rect.shortestSide / 2.0; + break; + case StrokeAlign.outside: + radius = (rect.shortestSide + side.width) / 2.0; + break; + } + canvas.drawCircle(rect.center, radius, side.toPaint()); } - canvas.drawCircle(rect.center, radius, side.toPaint()); + } + } + + Rect _adjustRect(Rect rect) { + if (eccentricity == 0.0 || rect.width == rect.height) { + return Rect.fromCircle(center: rect.center, radius: rect.shortestSide / 2.0); + } + if (rect.width < rect.height) { + final double delta = (1.0 - eccentricity) * (rect.height - rect.width) / 2.0; + return Rect.fromLTRB( + rect.left, + rect.top + delta, + rect.right, + rect.bottom - delta, + ); + } else { + final double delta = (1.0 - eccentricity) * (rect.width - rect.height) / 2.0; + return Rect.fromLTRB( + rect.left + delta, + rect.top, + rect.right - delta, + rect.bottom, + ); } } @@ -124,14 +178,18 @@ class CircleBorder extends OutlinedBorder { return false; } return other is CircleBorder - && other.side == side; + && other.side == side + && other.eccentricity == eccentricity; } @override - int get hashCode => side.hashCode; + int get hashCode => Object.hash(side, eccentricity); @override String toString() { + if (eccentricity != 0.0) { + return '${objectRuntimeType(this, 'CircleBorder')}($side, eccentricity: $eccentricity)'; + } return '${objectRuntimeType(this, 'CircleBorder')}($side)'; } } diff --git a/packages/flutter/lib/src/painting/oval_border.dart b/packages/flutter/lib/src/painting/oval_border.dart new file mode 100644 index 0000000000..d5bbf5b49b --- /dev/null +++ b/packages/flutter/lib/src/painting/oval_border.dart @@ -0,0 +1,65 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' as ui show lerpDouble; + +import 'package:flutter/foundation.dart'; + +import 'borders.dart'; +import 'circle_border.dart'; + +/// A border that fits an elliptical shape. +/// +/// Typically used with [ShapeDecoration] to draw an oval. Instead of centering +/// the [Border] to a square, like [CircleBorder], it fills the available space, +/// such that it touches the edges of the box. There is no difference between +/// `CircleBorder(eccentricity = 1.0)` and `OvalBorder()`. [OvalBorder] works as +/// an alias for users to discover this feature. +/// +/// See also: +/// +/// * [CircleBorder], which draws a circle, centering when the box is rectangular. +/// * [Border], which, when used with [BoxDecoration], can also describe an oval. +class OvalBorder extends CircleBorder { + /// Create an oval border. + const OvalBorder({ super.side, super.eccentricity = 1.0 }); + + @override + ShapeBorder scale(double t) => OvalBorder(side: side.scale(t), eccentricity: eccentricity); + + @override + OvalBorder copyWith({ BorderSide? side, double? eccentricity }) { + return OvalBorder(side: side ?? this.side, eccentricity: eccentricity ?? this.eccentricity); + } + + @override + ShapeBorder? lerpFrom(ShapeBorder? a, double t) { + if (a is OvalBorder) { + return OvalBorder( + side: BorderSide.lerp(a.side, side, t), + eccentricity: clampDouble(ui.lerpDouble(a.eccentricity, eccentricity, t)!, 0.0, 1.0), + ); + } + return super.lerpFrom(a, t); + } + + @override + ShapeBorder? lerpTo(ShapeBorder? b, double t) { + if (b is OvalBorder) { + return OvalBorder( + side: BorderSide.lerp(side, b.side, t), + eccentricity: clampDouble(ui.lerpDouble(eccentricity, b.eccentricity, t)!, 0.0, 1.0), + ); + } + return super.lerpTo(b, t); + } + + @override + String toString() { + if (eccentricity != 1.0) { + return '${objectRuntimeType(this, 'OvalBorder')}($side, eccentricity: $eccentricity)'; + } + return '${objectRuntimeType(this, 'OvalBorder')}($side)'; + } +} diff --git a/packages/flutter/lib/src/painting/rounded_rectangle_border.dart b/packages/flutter/lib/src/painting/rounded_rectangle_border.dart index 9428f79065..98d042865d 100644 --- a/packages/flutter/lib/src/painting/rounded_rectangle_border.dart +++ b/packages/flutter/lib/src/painting/rounded_rectangle_border.dart @@ -71,6 +71,7 @@ class RoundedRectangleBorder extends OutlinedBorder { side: BorderSide.lerp(a.side, side, t), borderRadius: borderRadius, circleness: 1.0 - t, + eccentricity: a.eccentricity, ); } return super.lerpFrom(a, t); @@ -90,6 +91,7 @@ class RoundedRectangleBorder extends OutlinedBorder { side: BorderSide.lerp(side, b.side, t), borderRadius: borderRadius, circleness: t, + eccentricity: b.eccentricity, ); } return super.lerpTo(b, t); @@ -187,17 +189,25 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { super.side, this.borderRadius = BorderRadius.zero, required this.circleness, + required this.eccentricity, }) : assert(side != null), assert(borderRadius != null), assert(circleness != null); final BorderRadiusGeometry borderRadius; - final double circleness; + final double eccentricity; @override EdgeInsetsGeometry get dimensions { - return EdgeInsets.all(side.width); + switch (side.strokeAlign) { + case StrokeAlign.inside: + return EdgeInsets.all(side.width); + case StrokeAlign.center: + return EdgeInsets.all(side.width / 2); + case StrokeAlign.outside: + return EdgeInsets.zero; + } } @override @@ -206,6 +216,7 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { side: side.scale(t), borderRadius: borderRadius * t, circleness: t, + eccentricity: eccentricity, ); } @@ -217,6 +228,7 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { side: BorderSide.lerp(a.side, side, t), borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t)!, circleness: circleness * t, + eccentricity: eccentricity, ); } if (a is CircleBorder) { @@ -224,6 +236,7 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { side: BorderSide.lerp(a.side, side, t), borderRadius: borderRadius, circleness: circleness + (1.0 - circleness) * (1.0 - t), + eccentricity: a.eccentricity, ); } if (a is _RoundedRectangleToCircleBorder) { @@ -231,6 +244,7 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { side: BorderSide.lerp(a.side, side, t), borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t)!, circleness: ui.lerpDouble(a.circleness, circleness, t)!, + eccentricity: eccentricity, ); } return super.lerpFrom(a, t); @@ -243,6 +257,7 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { side: BorderSide.lerp(side, b.side, t), borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!, circleness: circleness * (1.0 - t), + eccentricity: eccentricity, ); } if (b is CircleBorder) { @@ -250,6 +265,7 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { side: BorderSide.lerp(side, b.side, t), borderRadius: borderRadius, circleness: circleness + (1.0 - circleness) * t, + eccentricity: b.eccentricity, ); } if (b is _RoundedRectangleToCircleBorder) { @@ -257,6 +273,7 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { side: BorderSide.lerp(side, b.side, t), borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!, circleness: ui.lerpDouble(circleness, b.circleness, t)!, + eccentricity: eccentricity, ); } return super.lerpTo(b, t); @@ -267,7 +284,8 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { return rect; } if (rect.width < rect.height) { - final double delta = circleness * (rect.height - rect.width) / 2.0; + final double partialDelta = (rect.height - rect.width) / 2; + final double delta = circleness * partialDelta * (1.0 - eccentricity); return Rect.fromLTRB( rect.left, rect.top + delta, @@ -275,7 +293,8 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { rect.bottom - delta, ); } else { - final double delta = circleness * (rect.width - rect.height) / 2.0; + final double partialDelta = (rect.width - rect.height) / 2; + final double delta = circleness * partialDelta * (1.0 - eccentricity); return Rect.fromLTRB( rect.left + delta, rect.top, @@ -290,7 +309,22 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { if (circleness == 0.0) { return resolvedRadius; } - return BorderRadius.lerp(resolvedRadius, BorderRadius.circular(rect.shortestSide / 2.0), circleness); + if (eccentricity != 0.0) { + if (rect.width < rect.height) { + return BorderRadius.lerp( + resolvedRadius, + BorderRadius.all(Radius.elliptical(rect.width / 2, (0.5 + eccentricity / 2) * rect.height / 2)), + circleness, + )!; + } else { + return BorderRadius.lerp( + resolvedRadius, + BorderRadius.all(Radius.elliptical((0.5 + eccentricity / 2) * rect.width / 2, rect.height / 2)), + circleness, + )!; + } + } + return BorderRadius.lerp(resolvedRadius, BorderRadius.circular(rect.shortestSide / 2), circleness); } @override @@ -319,11 +353,12 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { } @override - _RoundedRectangleToCircleBorder copyWith({ BorderSide? side, BorderRadiusGeometry? borderRadius, double? circleness }) { + _RoundedRectangleToCircleBorder copyWith({ BorderSide? side, BorderRadiusGeometry? borderRadius, double? circleness, double? eccentricity }) { return _RoundedRectangleToCircleBorder( side: side ?? this.side, borderRadius: borderRadius ?? this.borderRadius, circleness: circleness ?? this.circleness, + eccentricity: eccentricity ?? this.eccentricity, ); } @@ -371,6 +406,9 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { @override String toString() { + if (eccentricity != 0.0) { + return 'RoundedRectangleBorder($side, $borderRadius, ${(circleness * 100).toStringAsFixed(1)}% of the way to being a CircleBorder that is ${(eccentricity * 100).toStringAsFixed(1)}% oval)'; + } return 'RoundedRectangleBorder($side, $borderRadius, ${(circleness * 100).toStringAsFixed(1)}% of the way to being a CircleBorder)'; } } diff --git a/packages/flutter/lib/src/painting/stadium_border.dart b/packages/flutter/lib/src/painting/stadium_border.dart index 1bf5d0cae8..cda9d91e6f 100644 --- a/packages/flutter/lib/src/painting/stadium_border.dart +++ b/packages/flutter/lib/src/painting/stadium_border.dart @@ -55,6 +55,7 @@ class StadiumBorder extends OutlinedBorder { return _StadiumToCircleBorder( side: BorderSide.lerp(a.side, side, t), circleness: 1.0 - t, + eccentricity: a.eccentricity, ); } if (a is RoundedRectangleBorder) { @@ -77,6 +78,7 @@ class StadiumBorder extends OutlinedBorder { return _StadiumToCircleBorder( side: BorderSide.lerp(side, b.side, t), circleness: t, + eccentricity: b.eccentricity, ); } if (b is RoundedRectangleBorder) { @@ -127,7 +129,7 @@ class StadiumBorder extends OutlinedBorder { case BorderStyle.none: break; case BorderStyle.solid: - final Radius radius = Radius.circular(rect.shortestSide / 2.0); + final Radius radius = Radius.circular(rect.shortestSide / 2); final RRect borderRect = RRect.fromRectAndRadius(rect, radius); final RRect adjustedRect; switch (side.strokeAlign) { @@ -138,7 +140,7 @@ class StadiumBorder extends OutlinedBorder { adjustedRect = borderRect; break; case StrokeAlign.outside: - adjustedRect = borderRect.inflate(side.width /2); + adjustedRect = borderRect.inflate(side.width / 2); break; } canvas.drawRRect( @@ -171,14 +173,23 @@ class _StadiumToCircleBorder extends OutlinedBorder { const _StadiumToCircleBorder({ super.side, this.circleness = 0.0, + required this.eccentricity, }) : assert(side != null), assert(circleness != null); final double circleness; + final double eccentricity; @override EdgeInsetsGeometry get dimensions { - return EdgeInsets.all(side.width); + switch (side.strokeAlign) { + case StrokeAlign.inside: + return EdgeInsets.all(side.width); + case StrokeAlign.center: + return EdgeInsets.all(side.width / 2); + case StrokeAlign.outside: + return EdgeInsets.zero; + } } @override @@ -186,6 +197,7 @@ class _StadiumToCircleBorder extends OutlinedBorder { return _StadiumToCircleBorder( side: side.scale(t), circleness: t, + eccentricity: eccentricity, ); } @@ -196,18 +208,21 @@ class _StadiumToCircleBorder extends OutlinedBorder { return _StadiumToCircleBorder( side: BorderSide.lerp(a.side, side, t), circleness: circleness * t, + eccentricity: eccentricity, ); } if (a is CircleBorder) { return _StadiumToCircleBorder( side: BorderSide.lerp(a.side, side, t), circleness: circleness + (1.0 - circleness) * (1.0 - t), + eccentricity: a.eccentricity, ); } if (a is _StadiumToCircleBorder) { return _StadiumToCircleBorder( side: BorderSide.lerp(a.side, side, t), circleness: ui.lerpDouble(a.circleness, circleness, t)!, + eccentricity: ui.lerpDouble(a.eccentricity, eccentricity, t)!, ); } return super.lerpFrom(a, t); @@ -220,18 +235,21 @@ class _StadiumToCircleBorder extends OutlinedBorder { return _StadiumToCircleBorder( side: BorderSide.lerp(side, b.side, t), circleness: circleness * (1.0 - t), + eccentricity: eccentricity, ); } if (b is CircleBorder) { return _StadiumToCircleBorder( side: BorderSide.lerp(side, b.side, t), circleness: circleness + (1.0 - circleness) * t, + eccentricity: b.eccentricity, ); } if (b is _StadiumToCircleBorder) { return _StadiumToCircleBorder( side: BorderSide.lerp(side, b.side, t), circleness: ui.lerpDouble(circleness, b.circleness, t)!, + eccentricity: ui.lerpDouble(eccentricity, b.eccentricity, t)!, ); } return super.lerpTo(b, t); @@ -242,7 +260,8 @@ class _StadiumToCircleBorder extends OutlinedBorder { return rect; } if (rect.width < rect.height) { - final double delta = circleness * (rect.height - rect.width) / 2.0; + final double partialDelta = (rect.height - rect.width) / 2; + final double delta = circleness * partialDelta * (1.0 - eccentricity); return Rect.fromLTRB( rect.left, rect.top + delta, @@ -250,7 +269,8 @@ class _StadiumToCircleBorder extends OutlinedBorder { rect.bottom - delta, ); } else { - final double delta = circleness * (rect.width - rect.height) / 2.0; + final double partialDelta = (rect.width - rect.height) / 2; + final double delta = circleness * partialDelta * (1.0 - eccentricity); return Rect.fromLTRB( rect.left + delta, rect.top, @@ -261,7 +281,23 @@ class _StadiumToCircleBorder extends OutlinedBorder { } BorderRadius _adjustBorderRadius(Rect rect) { - return BorderRadius.circular(rect.shortestSide / 2.0); + final BorderRadius circleRadius = BorderRadius.circular(rect.shortestSide / 2); + if (eccentricity != 0.0) { + if (rect.width < rect.height) { + return BorderRadius.lerp( + circleRadius, + BorderRadius.all(Radius.elliptical(rect.width / 2, (0.5 + eccentricity / 2) * rect.height / 2)), + circleness, + )!; + } else { + return BorderRadius.lerp( + circleRadius, + BorderRadius.all(Radius.elliptical((0.5 + eccentricity / 2) * rect.width / 2, rect.height / 2)), + circleness, + )!; + } + } + return circleRadius; } @override @@ -277,10 +313,11 @@ class _StadiumToCircleBorder extends OutlinedBorder { } @override - _StadiumToCircleBorder copyWith({ BorderSide? side, double? circleness }) { + _StadiumToCircleBorder copyWith({ BorderSide? side, double? circleness, double? eccentricity }) { return _StadiumToCircleBorder( side: side ?? this.side, circleness: circleness ?? this.circleness, + eccentricity: eccentricity ?? this.eccentricity, ); } @@ -327,8 +364,10 @@ class _StadiumToCircleBorder extends OutlinedBorder { @override String toString() { - return 'StadiumBorder($side, ${(circleness * 100).toStringAsFixed(1)}% ' - 'of the way to being a CircleBorder)'; + if (eccentricity != 0.0) { + return 'StadiumBorder($side, ${(circleness * 100).toStringAsFixed(1)}% of the way to being a CircleBorder that is ${(eccentricity * 100).toStringAsFixed(1)}% oval)'; + } + return 'StadiumBorder($side, ${(circleness * 100).toStringAsFixed(1)}% of the way to being a CircleBorder)'; } } diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 3b3bf47250..1eed35b2f2 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -2014,11 +2014,11 @@ class RenderPhysicalModel extends _RenderPhysicalModelBase { RRect get _defaultClip { assert(hasSize); assert(_shape != null); + final Rect rect = Offset.zero & size; switch (_shape) { case BoxShape.rectangle: - return (borderRadius ?? BorderRadius.zero).toRRect(Offset.zero & size); + return (borderRadius ?? BorderRadius.zero).toRRect(rect); case BoxShape.circle: - final Rect rect = Offset.zero & size; return RRect.fromRectXY(rect, rect.width / 2, rect.height / 2); } } diff --git a/packages/flutter/test/painting/circle_border_test.dart b/packages/flutter/test/painting/circle_border_test.dart index 3cf3bad1b0..6bd15000e9 100644 --- a/packages/flutter/test/painting/circle_border_test.dart +++ b/packages/flutter/test/painting/circle_border_test.dart @@ -12,11 +12,29 @@ void main() { test('CircleBorder defaults', () { const CircleBorder border = CircleBorder(); expect(border.side, BorderSide.none); + expect(border.eccentricity, 0.0); + }); + + test('CircleBorder getInnerPath and getOuterPath', () { + const Rect circleRect = Rect.fromLTWH(50, 0, 100, 100); + const Rect rect = Rect.fromLTWH(0, 0, 200, 100); + + expect(const CircleBorder().getInnerPath(rect).getBounds(), circleRect); + expect(const CircleBorder().getOuterPath(rect).getBounds(), circleRect); + + const CircleBorder oval = CircleBorder(eccentricity: 1.0); + expect(oval.getOuterPath(rect).getBounds(), rect); + expect(oval.getInnerPath(rect).getBounds(), rect); + + const CircleBorder o10 = CircleBorder(side: BorderSide(width: 10.0), eccentricity: 1.0); + expect(o10.getOuterPath(rect).getBounds(), Offset.zero & const Size(200, 100)); + expect(o10.getInnerPath(rect).getBounds(), const Offset(10, 10) & const Size(180, 80)); }); test('CircleBorder copyWith, ==, hashCode', () { expect(const CircleBorder(), const CircleBorder().copyWith()); expect(const CircleBorder().hashCode, const CircleBorder().copyWith().hashCode); + expect(const CircleBorder(eccentricity: 0.5).hashCode, const CircleBorder().copyWith(eccentricity: 0.5).hashCode); const BorderSide side = BorderSide(width: 10.0, color: Color(0xff123456)); expect(const CircleBorder().copyWith(side: side), const CircleBorder(side: side)); }); diff --git a/packages/flutter/test/painting/oval_border_test.dart b/packages/flutter/test/painting/oval_border_test.dart new file mode 100644 index 0000000000..4b26dedfd6 --- /dev/null +++ b/packages/flutter/test/painting/oval_border_test.dart @@ -0,0 +1,48 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/painting.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../rendering/mock_canvas.dart'; + +void main() { + test('OvalBorder defaults', () { + const OvalBorder border = OvalBorder(); + expect(border.side, BorderSide.none); + }); + + test('OvalBorder copyWith, ==, hashCode', () { + expect(const OvalBorder(), const OvalBorder().copyWith()); + expect(const OvalBorder().hashCode, const OvalBorder().copyWith().hashCode); + const BorderSide side = BorderSide(width: 10.0, color: Color(0xff123456)); + expect(const OvalBorder().copyWith(side: side), const OvalBorder(side: side)); + }); + + test('OvalBorder', () { + const OvalBorder c10 = OvalBorder(side: BorderSide(width: 10.0)); + const OvalBorder c15 = OvalBorder(side: BorderSide(width: 15.0)); + const OvalBorder c20 = OvalBorder(side: BorderSide(width: 20.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); + expect( + c10.getInnerPath(const Rect.fromLTWH(0, 0, 100, 40)), + isPathThat( + includes: const [ Offset(12, 19), Offset(50, 10), Offset(88, 19), Offset(50, 29) ], + excludes: const [ Offset(17, 26), Offset(15, 15), Offset(74, 10), Offset(76, 28) ], + ), + ); + expect( + c10.getOuterPath(const Rect.fromLTWH(0, 0, 100, 20)), + isPathThat( + includes: const [ Offset(2, 9), Offset(50, 0), Offset(98, 9), Offset(50, 19) ], + excludes: const [ Offset(7, 16), Offset(10, 2), Offset(84, 1), Offset(86, 18) ], + ), + ); + }); +} diff --git a/packages/flutter/test/painting/shape_decoration_test.dart b/packages/flutter/test/painting/shape_decoration_test.dart index b50c86680f..86e870bab2 100644 --- a/packages/flutter/test/painting/shape_decoration_test.dart +++ b/packages/flutter/test/painting/shape_decoration_test.dart @@ -45,13 +45,26 @@ void main() { test('ShapeDecoration.lerp and hit test', () { const Decoration a = ShapeDecoration(shape: CircleBorder()); const Decoration b = ShapeDecoration(shape: RoundedRectangleBorder()); + const Decoration c = ShapeDecoration(shape: OvalBorder()); expect(Decoration.lerp(a, b, 0.0), a); expect(Decoration.lerp(a, b, 1.0), b); + expect(Decoration.lerp(a, c, 0.0), a); + expect(Decoration.lerp(a, c, 1.0), c); + expect(Decoration.lerp(b, c, 0.0), b); + expect(Decoration.lerp(b, c, 1.0), c); const Size size = Size(200.0, 100.0); // at t=0.5, width will be 150 (x=25 to x=175). expect(a.hitTest(size, const Offset(20.0, 50.0)), isFalse); + expect(c.hitTest(size, const Offset(50, 5.0)), isFalse); + expect(c.hitTest(size, const Offset(5, 30.0)), isFalse); expect(Decoration.lerp(a, b, 0.1)!.hitTest(size, const Offset(20.0, 50.0)), isFalse); expect(Decoration.lerp(a, b, 0.5)!.hitTest(size, const Offset(20.0, 50.0)), isFalse); expect(Decoration.lerp(a, b, 0.9)!.hitTest(size, const Offset(20.0, 50.0)), isTrue); + expect(Decoration.lerp(a, c, 0.1)!.hitTest(size, const Offset(30.0, 50.0)), isFalse); + expect(Decoration.lerp(a, c, 0.5)!.hitTest(size, const Offset(30.0, 50.0)), isTrue); + expect(Decoration.lerp(a, c, 0.9)!.hitTest(size, const Offset(30.0, 50.0)), isTrue); + expect(Decoration.lerp(b, c, 0.1)!.hitTest(size, const Offset(45.0, 10.0)), isTrue); + expect(Decoration.lerp(b, c, 0.5)!.hitTest(size, const Offset(30.0, 10.0)), isTrue); + expect(Decoration.lerp(b, c, 0.9)!.hitTest(size, const Offset(10.0, 30.0)), isTrue); expect(b.hitTest(size, const Offset(20.0, 50.0)), isTrue); }); @@ -107,6 +120,16 @@ void main() { ); expect(clipPath, isLookLikeExpectedPath); }); + test('ShapeDecoration.getClipPath for oval', () { + const ShapeDecoration decoration = ShapeDecoration(shape: OvalBorder()); + const Rect rect = Rect.fromLTWH(0.0, 0.0, 100.0, 50.0); + final Path clipPath = decoration.getClipPath(rect, TextDirection.ltr); + final Matcher isLookLikeExpectedPath = isPathThat( + includes: const [ Offset(50.0, 10.0), ], + excludes: const [ Offset(1.0, 1.0), Offset(15.0, 1.0), Offset(99.0, 19.0), ], + ); + expect(clipPath, isLookLikeExpectedPath); + }); } class TestImageProvider extends ImageProvider { diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index 758499f0ec..c50db35456 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart @@ -1525,19 +1525,15 @@ class _RendersOnPhysicalModel extends _MatchRenderObject