import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'dart:math' as math; class UITest extends StatefulWidget { @override State createState() { return _UITest(); } } class _UITest extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Color(0xFFF7F7F7), elevation: 0, title: Text( "测试", style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold), ), leading: GestureDetector( onTap: () { Navigator.of(context).pop(); }, child: Container( alignment: Alignment.centerRight, margin: EdgeInsets.only(left: 10), padding: EdgeInsets.all(6), child: Icon( Icons.arrow_back_ios, color: Colors.black, size: 24, ), ), ), titleSpacing: 2, leadingWidth: 56, ), body: AspectRatio( aspectRatio: 1, child: PhysicalShape( color: ElevationOverlay.applyOverlay(context, Colors.white, 2), elevation: 2, clipper: BottomAppBarClipper( shape: CircularHorizontalNotchedRectangle(), ), child: Container( margin: EdgeInsets.all(50), color: Colors.blue.withAlpha(123), alignment: Alignment.center, child: Text("主体内容"), ), ), ), endDrawer: Drawer(), bottomNavigationBar: BottomAppBar( color: Colors.deepPurpleAccent, shape: CircularNotchedRectangle(), child: Container( height: 50.0, ), ), extendBody: true, floatingActionButton: FloatingActionButton( onPressed: () { print("点击"); }, child: Icon(Icons.add), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, ); } } class BottomAppBarClipper extends CustomClipper { final NotchedShape shape; const BottomAppBarClipper({this.shape}); @override Path getClip(Size size) { final Rect button = Rect.fromCircle(center: Offset(size.width / 2, size.height / 2), radius: 20); return shape.getOuterPath(Offset.zero & size, button?.inflate(2)); } @override bool shouldReclip(BottomAppBarClipper oldClipper) { return true; } } class CircularHorizontalNotchedRectangle extends NotchedShape { @override Path getOuterPath(Rect host, Rect guest) { if (guest == null || !host.overlaps(guest)) return Path()..addRect(host); // 客人的形状是一个以客人矩形为边界的圆形。 // 所以客人的半径是客人宽度的一半。 final double notchRadius = guest.width / 2.0; // 我们从 3 段为缺口构建路径: // A 段 - 从主机顶部边缘到 B 段的贝塞尔曲线。 // B 段 - 半径为 notchRadius 的圆弧。段 // C - 从段 B 返回到主机顶部边缘的贝塞尔曲线。 // 以下公式的详细解释和推导可在以下网址获得:https:goo.glUfzrqn const double s1 = 15.0; const double s2 = 1.0; final double r = notchRadius; final double a = -1.0 * r - s2; // 半径+s2 final double b = host.top - guest.center.dy; // 圆心到矩形的y轴距离 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 p = List.filled(6, null, growable: false); // p0、p1 和 p2 是线段 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 和 p5 是线段 B 的控制点,它是线段 A 绕 y 轴的镜像。 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); // 将所有点转换回绝对坐标系。 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(); } }