中间凸起直接拆解为双三阶贝塞尔曲线。
上代码 - 边界使用贝塞尔裁切
// 核心裁切组件
class MyClipper extends CustomClipper<Path> {
/// 顶部向下填充,用于给普通item布局使用。
final double inset;
/// 中心圆 半径
final double circleRadius;
/// 中心圆旁边的 影响radius
final double effectRadius;
MyClipper({
required this.inset,
required this.circleRadius,
required this.effectRadius,
}) : assert(inset > effectRadius);
@override
Path getClip(Size size) {
var path = Path();
double centerX = size.width / 2.0;
path.moveTo(0, inset);
path.lineTo(centerX - circleRadius - effectRadius, inset);
// 左侧半圆
{
var c1 = Offset(centerX - circleRadius + effectRadius, inset);
var c2 = Offset(centerX - (circleRadius + effectRadius) / 2.0, 0);
var c3 = Offset(centerX, 0);
path.cubicTo(c1.dx, c1.dy, c2.dx, c2.dy, c3.dx, c3.dy);
}
// 右侧半圆。
{
var c1 = Offset(centerX + (circleRadius + effectRadius) / 2.0, 0);
var c2 = Offset(centerX + circleRadius - effectRadius, inset);
var c3 = Offset(centerX + circleRadius + effectRadius, inset);
path.cubicTo(c1.dx, c1.dy, c2.dx, c2.dy, c3.dx, c3.dy);
}
path.lineTo(size.width, inset);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.lineTo(0, inset);
return path;
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
return true;
}
}
/// 底部导航栏封装, 用于处理凸起逻辑。
class BottomBar extends StatefulWidget {
final int currentIndex;
final List<BottomNavigationBarItem> items;
final ValueChanged<int> onTap;
/// 中间按钮 建议自己实现点击 事件。
final Widget? centerChild;
// final double? width;
final double height;
/// 每一个元素的inset, 不包含centerChild
final EdgeInsets insets;
BottomBar(
{super.key,
this.height = 66,
this.insets = const EdgeInsets.only(top: 20),
this.currentIndex = 0,
required this.items,
this.centerChild,
required this.onTap})
: assert(items.isNotEmpty),
assert(centerChild != null || items.length.isOdd);
@override
State<BottomBar> createState() => _BottomBarState();
}
class _BottomBarState extends State<BottomBar> {
int get currentIndex => widget.currentIndex;
@override
void initState() {
super.initState();
}
@override
void didUpdateWidget(covariant BottomBar oldWidget) {
super.didUpdateWidget(oldWidget);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
var current =
List.generate(widget.items.length, (index) => buildTiles(index));
if (widget.centerChild != null) {
current.insert(widget.items.length ~/ 2, widget.centerChild!);
}
return Container(
margin: EdgeInsets.symmetric(horizontal: 8.0),
height: widget.height,
color: Colors.white,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: current,
),
);
}
Widget buildTiles(int index) {
var item = widget.items[index];
bool isSelected = index == currentIndex;
print('选中 - $currentIndex ,当前: $index ,当前是否高亮 - $isSelected');
return Padding(
padding: widget.insets,
child: InkWell(
borderRadius: BorderRadius.circular(24),
onTap: () {
print('点击 $index');
widget.onTap(index);
},
child: SizedBox(
width: 48,
height: 48,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
isSelected ? item.activeIcon : item.icon,
Center(
heightFactor: 1.0,
child: Text(
'${item.label}',
style: const TextStyle(
color: Colors.black, fontSize: 10, height: 1.1),
),
),
],
),
),
),
);
}
}
/// 使用
int currentIndex = 0;
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: ClipPath(
clipper: MyClipper(inset: 20, circleRadius: 38, effectRadius: 10),
child: buildBottomNavigationBar(),
),
);
}
Widget buildBottomNavigationBar() {
return BottomBar(
currentIndex: currentIndex,
centerChild: buildFloatButton(),
onTap: (int index) {
currentIndex = index;
setState(() {});
}
},
items: [
BottomNavigationBarItem(
icon: iconImage('images/tabbar/Earn.png'),
activeIcon: iconImage('images/tabbar/earnSelect.png'),
label: 'Earn'.tr,
),
BottomNavigationBarItem(
icon: iconImage('images/tabbar/referral.png'),
activeIcon: iconImage('images/tabbar/referralSelect.png'),
label: 'Referral'.tr,
),
BottomNavigationBarItem(
icon: iconImage('images/tabbar/Social.png'),
activeIcon: iconImage('images/tabbar/SocialSelect.png'),
label: 'Social'.tr,
),
BottomNavigationBarItem(
icon: iconImage('images/tabbar/Market.png'),
activeIcon: iconImage('images/tabbar/MarketSelect.png'),
label: 'Market'.tr,
),
]
);
}
Widget buildFloatButton() {
return InkWell(
onTap: () {
setState(() {
this._currentindex = 4;
});
},
child: Container(
width: 70,
height: 70,
margin: EdgeInsets.all(4.0),
decoration: BoxDecoration(
color: Color(0xFFFF5253),
shape: BoxShape.circle,
),
child: Material(
type: MaterialType.transparency,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Tesla',
style: TextStyle(
color: Colors.white,
fontFamily: getSegoeUIBlack(),
fontSize: SAdapt.size(11)),
),
Text(
'Airdrop'.tr,
style: TextStyle(
color: Colors.white,
fontFamily: getSegoeUIBlack(),
fontSize: SAdapt.size(6)),
),
],
)),
),
),
);
}