// 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'; /// 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(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 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 }) { return Path()..addOval(_adjustRect(rect).deflate(side.strokeInset)); } @override Path getOuterPath(Rect rect, { TextDirection? textDirection }) { return Path()..addOval(_adjustRect(rect)); } @override void paintInterior(Canvas canvas, Rect rect, Paint paint, { TextDirection? textDirection }) { if (eccentricity == 0.0) { canvas.drawCircle(rect.center, rect.shortestSide / 2.0, paint); } else { canvas.drawOval(_adjustRect(rect), paint); } } @override bool get preferPaintInterior => true; @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) { canvas.drawCircle(rect.center, (rect.shortestSide + side.strokeOffset) / 2, side.toPaint()); } else { final Rect borderRect = _adjustRect(rect); canvas.drawOval(borderRect.inflate(side.strokeOffset / 2), 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)'; } }