| // 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:math' as math; |
| |
| import 'basic_types.dart'; |
| import 'borders.dart'; |
| |
| /// A shape with a notch in its outline. |
| /// |
| /// Typically used as the outline of a 'host' widget to make a notch that |
| /// accommodates a 'guest' widget. e.g the [BottomAppBar] may have a notch to |
| /// accommodate the [FloatingActionButton]. |
| /// |
| /// See also: |
| /// |
| /// * [ShapeBorder], which defines a shaped border without a dynamic notch. |
| /// * [AutomaticNotchedShape], an adapter from [ShapeBorder] to [NotchedShape]. |
| abstract class NotchedShape { |
| /// Abstract const constructor. This constructor enables subclasses to provide |
| /// const constructors so that they can be used in const expressions. |
| const NotchedShape(); |
| |
| /// Creates a [Path] that describes the outline of the shape. |
| /// |
| /// The `host` is the bounding rectangle of the shape. |
| /// |
| /// The `guest` is the bounding rectangle of the shape for which a notch will |
| /// be made. It is null when there is no guest. |
| Path getOuterPath(Rect host, Rect? guest); |
| } |
| |
| /// A rectangle with a smooth circular notch. |
| /// |
| /// See also: |
| /// |
| /// * [CircleBorder], a [ShapeBorder] that describes a circle. |
| class CircularNotchedRectangle extends NotchedShape { |
| /// Creates a [CircularNotchedRectangle]. |
| /// |
| /// The same object can be used to create multiple shapes. |
| const CircularNotchedRectangle(); |
| |
| /// Creates a [Path] that describes a rectangle with a smooth circular notch. |
| /// |
| /// `host` is the bounding box for the returned shape. Conceptually this is |
| /// the rectangle to which the notch will be applied. |
| /// |
| /// `guest` is the bounding box of a circle that the notch accommodates. All |
| /// points in the circle bounded by `guest` will be outside of the returned |
| /// path. |
| /// |
| /// The notch is curve that smoothly connects the host's top edge and |
| /// the guest circle. |
| // TODO(amirh): add an example diagram here. |
| @override |
| Path getOuterPath(Rect host, Rect? guest) { |
| if (guest == null || !host.overlaps(guest)) |
| return Path()..addRect(host); |
| |
| // The guest's shape is a circle bounded by the guest rectangle. |
| // So the guest's radius is half the guest width. |
| final double notchRadius = guest.width / 2.0; |
| |
| // We build a path for the notch from 3 segments: |
| // Segment A - a Bezier curve from the host's top edge to segment B. |
| // Segment B - an arc with radius notchRadius. |
| // Segment C - a Bezier curve from segment B back to the host's top edge. |
| // |
| // A detailed explanation and the derivation of the formulas below is |
| // available at: https://goo.gl/Ufzrqn |
| |
| const double s1 = 15.0; |
| const double s2 = 1.0; |
| |
| final double r = notchRadius; |
| final double a = -1.0 * r - s2; |
| final double b = host.top - guest.center.dy; |
| |
| final double n2 = math.sqrt(b * b * r * r * (a * a + b * b - r * r)); |
| final double p2xA = ((a * r * r) - n2) / (a * a + b * b); |
| final double p2xB = ((a * r * r) + n2) / (a * a + b * b); |
| final double p2yA = math.sqrt(r * r - p2xA * p2xA); |
| final double p2yB = math.sqrt(r * r - p2xB * p2xB); |
| |
| final List<Offset?> p = List<Offset?>.filled(6, null); |
| |
| // p0, p1, and p2 are the control points for segment A. |
| p[0] = Offset(a - s1, b); |
| p[1] = Offset(a, b); |
| final double cmp = b < 0 ? -1.0 : 1.0; |
| p[2] = cmp * p2yA > cmp * p2yB ? Offset(p2xA, p2yA) : Offset(p2xB, p2yB); |
| |
| // p3, p4, and p5 are the control points for segment B, which is a mirror |
| // of segment A around the y axis. |
| p[3] = Offset(-1.0 * p[2]!.dx, p[2]!.dy); |
| p[4] = Offset(-1.0 * p[1]!.dx, p[1]!.dy); |
| p[5] = Offset(-1.0 * p[0]!.dx, p[0]!.dy); |
| |
| // translate all points back to the absolute coordinate system. |
| for (int i = 0; i < p.length; i += 1) |
| p[i] = p[i]! + guest.center; |
| |
| return Path() |
| ..moveTo(host.left, host.top) |
| ..lineTo(p[0]!.dx, p[0]!.dy) |
| ..quadraticBezierTo(p[1]!.dx, p[1]!.dy, p[2]!.dx, p[2]!.dy) |
| ..arcToPoint( |
| p[3]!, |
| radius: Radius.circular(notchRadius), |
| clockwise: false, |
| ) |
| ..quadraticBezierTo(p[4]!.dx, p[4]!.dy, p[5]!.dx, p[5]!.dy) |
| ..lineTo(host.right, host.top) |
| ..lineTo(host.right, host.bottom) |
| ..lineTo(host.left, host.bottom) |
| ..close(); |
| } |
| } |
| |
| /// A [NotchedShape] created from [ShapeBorder]s. |
| /// |
| /// Two shapes can be provided. The [host] is the shape of the widget that |
| /// uses the [NotchedShape] (typically a [BottomAppBar]). The [guest] is |
| /// subtracted from the [host] to create the notch (typically to make room |
| /// for a [FloatingActionButton]). |
| class AutomaticNotchedShape extends NotchedShape { |
| /// Creates a [NotchedShape] that is defined by two [ShapeBorder]s. |
| /// |
| /// The [host] must not be null. |
| /// |
| /// The [guest] may be null, in which case no notch is created even |
| /// if a guest rectangle is provided to [getOuterPath]. |
| const AutomaticNotchedShape(this.host, [ this.guest ]); |
| |
| /// The shape of the widget that uses the [NotchedShape] (typically a |
| /// [BottomAppBar]). |
| /// |
| /// This shape cannot depend on the [TextDirection], as no text direction |
| /// is available to [NotchedShape]s. |
| final ShapeBorder host; |
| |
| /// The shape to subtract from the [host] to make the notch. |
| /// |
| /// This shape cannot depend on the [TextDirection], as no text direction |
| /// is available to [NotchedShape]s. |
| /// |
| /// If this is null, [getOuterPath] ignores the guest rectangle. |
| final ShapeBorder? guest; |
| |
| @override |
| Path getOuterPath(Rect hostRect, Rect? guestRect) { // ignore: avoid_renaming_method_parameters |
| // The parameters of this method are renamed over the baseclass because they |
| // would clash with properties of this object, and the use of all four of |
| // them in the code below is really confusing if they have the same names. |
| final Path hostPath = host.getOuterPath(hostRect); |
| if (guest != null && guestRect != null) { |
| final Path guestPath = guest!.getOuterPath(guestRect); |
| return Path.combine(PathOperation.difference, hostPath, guestPath); |
| } |
| return hostPath; |
| } |
| } |