You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
160 lines
4.7 KiB
160 lines
4.7 KiB
4 years ago
|
import 'package:flutter/material.dart';
|
||
|
|
||
|
import 'dart:math' as math;
|
||
|
|
||
|
class UITest extends StatefulWidget {
|
||
|
@override
|
||
|
State<StatefulWidget> createState() {
|
||
|
return _UITest();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class _UITest extends State<UITest> {
|
||
|
@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<Path> {
|
||
|
|
||
|
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<Offset> p = List<Offset>.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();
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|