Files
FlutterUnit/lib/components/permanent/burst_menu.dart
2022-03-26 21:25:40 +08:00

299 lines
8.2 KiB
Dart

import 'dart:math';
import 'package:flutter/material.dart';
/// create by 张风捷特烈 on 2020/11/17
/// contact me by email 1981462002@qq.com
/// 说明:
enum BurstType {
circle,
topLeft,
bottomLeft,
topRight,
bottomRight,
halfCircle,
}
typedef BurstMenuItemClick = bool Function(int index);
class BurstMenu extends StatefulWidget {
final List<Widget> menus;
final Widget center;
final double radius;
final double startAngle;
final double swapAngle;
final double hideOpacity;
final Duration duration;
final BurstType burstType;
final Curve curve;
final BurstMenuItemClick? burstMenuItemClick;
const BurstMenu({
Key? key,
required this.menus,
required this.center,
this.radius = 100,
this.swapAngle = 120,
this.startAngle = -60,
this.hideOpacity = 0,
this.curve = Curves.ease,
this.duration = const Duration(milliseconds: 300),
this.burstType = BurstType.circle,
this.burstMenuItemClick,
}) : super(key: key);
const BurstMenu.topLeft({Key? key,
required this.menus,
this.burstMenuItemClick,
this.radius = 100,
required this.center,
this.hideOpacity = 0,
this.curve = Curves.ease,
this.duration = const Duration(milliseconds: 300),
this.burstType = BurstType.topLeft,
this.swapAngle = 90,
this.startAngle = 0,
}) : super(key: key);
const BurstMenu.bottomLeft({Key? key,
required this.menus,
this.burstMenuItemClick,
this.radius = 100,
required this.center,
this.hideOpacity = 0,
this.curve = Curves.ease,
this.duration = const Duration(milliseconds: 300),
this.burstType = BurstType.bottomLeft,
this.swapAngle = 90,
this.startAngle = -90,
}) : super(key: key);
const BurstMenu.topRight({Key? key,
required this.menus,
this.burstMenuItemClick,
this.radius = 100,
required this.center,
this.hideOpacity = 0,
this.curve = Curves.ease,
this.duration = const Duration(milliseconds: 500),
this.burstType = BurstType.topRight,
this.swapAngle = -90,
this.startAngle = 180,
}) : super(key: key);
const BurstMenu.bottomRight({Key? key,
required this.menus,
this.burstMenuItemClick,
this.radius = 100,
required this.center,
this.hideOpacity = 0,
this.curve = Curves.ease,
this.duration = const Duration(milliseconds: 300),
this.burstType = BurstType.bottomRight,
this.swapAngle = 90,
this.startAngle = 180,
}) : super(key: key);
@override
BurstMenuState createState() => BurstMenuState();
}
class BurstMenuState extends State<BurstMenu>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
// 是否已关闭
bool _closed = true;
late Animation<double> curveAnim; // 1.定义曲线动画
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
vsync: this,
);
curveAnim =
CurvedAnimation(parent: _controller, curve: widget.curve); //<--2.创建曲线动画
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
width: widget.radius * 2,
height: widget.burstType == BurstType.halfCircle
? widget.radius
: widget.radius * 2,
alignment: Alignment.center,
// color: Colors.orange.withOpacity(0.5),
child: Flow(
delegate: _CircleFlowDelegate(curveAnim,
startAngle: widget.startAngle,
hideOpacity: widget.hideOpacity,
swapAngle: widget.swapAngle,
burstType: widget.burstType),
children: [
...widget.menus.asMap().keys.map((int index) => GestureDetector(
onTap: () => _handleItemClick(index),
child: widget.menus[index],
)),
GestureDetector(onTap: toggle, child: widget.center)
],
));
}
void _handleItemClick(int index) {
if (widget.burstMenuItemClick == null) {
toggle();
return;
}
bool close = widget.burstMenuItemClick?.call(index) ?? false;
if (close) toggle();
}
@override
void didUpdateWidget(BurstMenu oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.duration != oldWidget.duration) {
_controller.dispose();
_controller = AnimationController(duration: widget.duration, vsync: this);
}
if (widget.curve != oldWidget.curve ||
widget.duration != oldWidget.duration) {
curveAnim = CurvedAnimation(parent: _controller, curve: widget.curve);
}
}
void toggle() {
print("--$_closed------------");
if (_closed) {
_controller.forward();
} else {
_controller.reverse();
}
_closed = !_closed;
}
}
class _CircleFlowDelegate extends FlowDelegate {
// 菜单圆弧的扫描角度
final double swapAngle;
// 菜单圆弧的起始角度
final double startAngle;
final double hideOpacity;
final BurstType burstType;
final Animation<double> animation;
_CircleFlowDelegate(
this.animation, {
this.swapAngle = 120,
this.hideOpacity = 0.3,
this.startAngle = -60,
this.burstType = BurstType.circle,
}) : super(repaint: animation);
//绘制孩子的方法
@override
void paintChildren(FlowPaintingContext context) {
double radius = context.size.shortestSide / 2;
final double halfCenterSize =
context.getChildSize(context.childCount - 1)?.width ?? 0 / 2;
switch (burstType) {
case BurstType.circle:
paintWithOffset(context, Offset.zero);
break;
case BurstType.topLeft:
Offset centerOffset =
Offset(-radius + halfCenterSize, -radius + halfCenterSize);
paintWithOffset(context, centerOffset);
break;
case BurstType.bottomLeft:
Offset centerOffset =
Offset(-radius + halfCenterSize, radius - halfCenterSize);
paintWithOffset(context, centerOffset);
break;
case BurstType.topRight:
Offset centerOffset =
Offset(radius - halfCenterSize, -radius + halfCenterSize);
paintWithOffset(context, centerOffset);
break;
case BurstType.bottomRight:
Offset centerOffset =
Offset(radius - halfCenterSize, radius - halfCenterSize);
paintWithOffset(context, centerOffset);
break;
case BurstType.halfCircle:
Offset centerOffset = Offset(radius, radius - halfCenterSize);
paintWithOffset(context, centerOffset);
break;
}
}
void paintWithOffset(FlowPaintingContext context, Offset centerOffset) {
final double radius = context.size.shortestSide / 2;
final int count = context.childCount - 1;
final double perRad = swapAngle / 180 * pi / (count - 1);
final double rotate = startAngle / 180 * pi;
if (animation.value > hideOpacity) {
for (int i = 0; i < count; i++) {
Size? size = context.getChildSize(i);
if(size == null) continue;
final double cSizeX = context.getChildSize(i)!.width/ 2;
final double cSizeY = context.getChildSize(i)!.height/ 2;
final double beforeRadius = (radius - cSizeX);
final double now = beforeRadius + centerOffset.dy.abs();
final swapRadius = (radius - cSizeX) / beforeRadius * now;
final double offsetX =
animation.value * swapRadius * cos(i * perRad + rotate) +
radius +
centerOffset.dx;
final double offsetY =
animation.value * swapRadius * sin(i * perRad + rotate) +
radius +
centerOffset.dy;
context.paintChild(
i,
transform: Matrix4.translationValues(
offsetX - cSizeX,
offsetY - cSizeY,
0.0,
),
opacity: animation.value,
);
}
}
Size? size = context.getChildSize(context.childCount - 1);
if (size == null) return;
context.paintChild(
context.childCount - 1,
transform: Matrix4.translationValues(
radius - size.width / 2 + centerOffset.dx,
radius - size.height / 2 + centerOffset.dy,
0.0,
),
);
}
@override
bool shouldRepaint(FlowDelegate oldDelegate) => false;
}