// 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 'basic_types.dart'; import 'borders.dart'; import 'edge_insets.dart'; /// A border that fits a circle within the available space. /// /// Typically used with [ShapeDecoration] to draw a circle. /// /// The [dimensions] assume that the border is being used in a square space. /// 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. class CircleBorder extends OutlinedBorder { /// Create a circle border. /// /// The [side] argument must not be 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 { 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 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), 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 CircleBorder) { 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 delta; switch (side.strokeAlign) { case StrokeAlign.inside: delta = side.width; break; case StrokeAlign.center: delta = side.width / 2.0; break; case StrokeAlign.outside: delta = 0; break; } final Rect adjustedRect = _adjustRect(rect).deflate(delta); return Path()..addOval(adjustedRect); } @override Path getOuterPath(Rect rect, { TextDirection? textDirection }) { return Path()..addOval(_adjustRect(rect)); } @override CircleBorder copyWith({ BorderSide? side, double? eccentricity }) { return CircleBorder(side: side ?? this.side, eccentricity: eccentricity ?? this.eccentricity); } @override void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) { switch (side.style) { case BorderStyle.none: break; case BorderStyle.solid: 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()); } } } 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, ); } } @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) { return false; } return other is CircleBorder && other.side == side && other.eccentricity == eccentricity; } @override 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)'; } }