// 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 [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; } }