优化 burst_menu

This commit is contained in:
toly
2021-06-15 16:45:22 +08:00
parent 4d1baafd10
commit 1f78f08bbf
5 changed files with 364 additions and 190 deletions

View File

@@ -1,116 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
/// create by 张风捷特烈 on 2020/11/17
/// contact me by email 1981462002@qq.com
/// 说明:
class BurstFlow extends StatefulWidget {
final List<Widget> children;
final Widget menu;
final double startAngle;
final double swapAngle;
BurstFlow({Key key,@required this.children,
this.startAngle = 30 + 90.0,
this.swapAngle = 120,
@required this.menu}) : super(key: key);
@override
BurstFlowState createState() => BurstFlowState();
}
class BurstFlowState extends State<BurstFlow>
with SingleTickerProviderStateMixin {
AnimationController _controller;
bool _closed = true;
@override
void initState() {
super.initState();
_controller =
AnimationController(duration: Duration(milliseconds: 300), vsync: this)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Flow(
delegate: _CircleFlowDelegate(_controller,
swapAngle: widget.swapAngle, startAngle: widget.startAngle),
children: [
...widget.children,
GestureDetector(onTap: toggle, child: widget.menu)
],
);
}
toggle() {
if (_closed) {
_controller.forward();
} else {
_controller.reverse();
}
_closed = !_closed;
}
}
class _CircleFlowDelegate extends FlowDelegate {
final Animation<double> repaint;
_CircleFlowDelegate(this.repaint,
{this.startAngle = 30 + 90.0, this.swapAngle = 120})
: super(repaint: repaint);
final double startAngle;
final double swapAngle;
@override //绘制孩子的方法
void paintChildren(FlowPaintingContext context) {
double radius = context.size.shortestSide / 2;
if (repaint.value > 0.3) {
var count = context.childCount - 1;
var perRad = swapAngle / 180 * pi / (count - 1);
double rotate = startAngle / 180 * pi;
for (int i = 0; i < count; i++) {
var cSizeX = context.getChildSize(i).width / 2;
var cSizeY = context.getChildSize(i).height / 2;
var offsetX =
repaint.value * (radius - cSizeX) * cos(i * perRad + rotate) +
radius;
var offsetY =
repaint.value * (radius - cSizeY) * sin(i * perRad + rotate) +
radius;
context.paintChild(i,
transform: Matrix4.translationValues(
offsetX - cSizeX, offsetY - cSizeY, 0.0),
opacity: repaint.value);
}
}
context.paintChild(context.childCount - 1,
transform: Matrix4.translationValues(
radius - context.getChildSize(context.childCount - 1).width / 2,
radius - context.getChildSize(context.childCount - 1).height / 2,
0.0));
}
@override
bool shouldRepaint(FlowDelegate oldDelegate) {
return true;
}
}

View File

@@ -0,0 +1,296 @@
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);
BurstMenu.topLeft({
this.menus,
this.burstMenuItemClick,
this.radius,
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,
});
BurstMenu.bottomLeft({
this.menus,
this.burstMenuItemClick,
this.radius,
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,
});
BurstMenu.topRight({
this.menus,
this.burstMenuItemClick,
this.radius,
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,
});
BurstMenu.bottomRight({
this.menus,
this.burstMenuItemClick,
this.radius,
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,
});
@override
BurstMenuState createState() => BurstMenuState();
}
class BurstMenuState extends State<BurstMenu>
with SingleTickerProviderStateMixin {
AnimationController _controller;
// 是否已关闭
bool _closed = true;
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,
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);
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 / 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++) {
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,
);
}
}
context.paintChild(
context.childCount - 1,
transform: Matrix4.translationValues(
radius -
context.getChildSize(context.childCount - 1).width / 2 +
centerOffset.dx,
radius -
context.getChildSize(context.childCount - 1).height / 2 +
centerOffset.dy,
0.0,
),
);
}
@override
bool shouldRepaint(FlowDelegate oldDelegate) => false;
}

View File

@@ -4,10 +4,7 @@ import 'package:flutter_unit/app/res/toly_icon.dart';
import 'package:flutter_unit/app/router/unit_router.dart';
import 'package:flutter_unit/blocs/bloc_exp.dart';
import 'package:flutter_unit/views/components/permanent/feedback_widget.dart';
import '../permanent/burst_flow.dart';
import '../permanent/burst_menu.dart';
import '../permanent/color_wrapper.dart';
/// create by 张风捷特烈 on 2020/10/21
@@ -84,11 +81,8 @@ class OverlayToolWrapperState extends State<OverlayToolWrapper>
final double circleRadius = 80;
final double menuSize = 36;
GlobalKey<BurstFlowState> burstFlowKey = GlobalKey<BurstFlowState>();
_buildFloating() {
Widget _buildFloating() {
Color wrapColor = Colors.blue.withOpacity(0.6);
bool left = offset.dx < 100;
return Container(
@@ -98,96 +92,100 @@ class OverlayToolWrapperState extends State<OverlayToolWrapper>
// color: Colors.orangeAccent,
child: IconTheme(
data: IconTheme.of(context).copyWith(color: Colors.white, size: 18),
child: BurstFlow(
key: burstFlowKey,
startAngle: !left ? 90.0 + 15 : -90 + 15.0,
swapAngle: !left ? 180.0 - 15 * 2 : 180.0 - 15 * 2.0,
menu: GestureDetector(
onPanEnd: _onPanEnd,
onPanDown: _onPanDown,
onPanUpdate: _updatePosition,
child: Opacity(
opacity: 0.9,
child: Container(
width: menuSize,
height: menuSize,
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(menuSize / 2)),
child: Container(
decoration: BoxDecoration(
color: Colors.blue,
image: DecorationImage(
image: AssetImage('assets/images/icon_head.webp')),
borderRadius: BorderRadius.circular(menuSize / 2)),
),
),
),
),
children: _buildMenuItems(wrapColor)),
child: BurstMenu(
startAngle: !left ? 90.0 + 15 : -90 - 15.0 + 180,
swapAngle: !left ? 180.0 - 15 * 2 : -(180.0 - 15 * 2),
center: _buildCenter(),
burstMenuItemClick: _burstMenuItemClick,
menus: _buildMenuItems(wrapColor)),
),
);
}
Widget _buildCenter() => GestureDetector(
onPanEnd: _onPanEnd,
onPanUpdate: _updatePosition,
child: Opacity(
opacity: 0.9,
child: Container(
width: menuSize,
height: menuSize,
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(menuSize / 2)),
child: Container(
decoration: BoxDecoration(
color: Colors.blue,
image: DecorationImage(
image: const AssetImage('assets/images/icon_head.webp')),
borderRadius: BorderRadius.circular(menuSize / 2)),
),
),
),
);
// 构建 菜单 item
List<Widget> _buildMenuItems(Color wrapColor) => [
FeedbackWidget(
onPressed: _doClose,
child: Circled(color: wrapColor, child: Icon(Icons.close))),
FeedbackWidget(
onPressed: _toPoint,
child: Circled(
color: wrapColor, radius: 15, child: Icon(TolyIcon.icon_bug))),
FeedbackWidget(
onPressed: _toGalley,
child: Circled(
color: wrapColor, radius: 15, child: Icon(Icons.palette))),
FeedbackWidget(
onPressed: _toWidget,
child: Circled(color: wrapColor, child: Icon(Icons.widgets))),
FeedbackWidget(
onPressed: _toSetting,
child: Circled(color: wrapColor, child: Icon(Icons.settings))),
Circled(color: wrapColor, child: const Icon(Icons.close)),
Circled(color: wrapColor, radius: 15, child: const Icon(TolyIcon.icon_bug)),
Circled(color: wrapColor, radius: 15, child: const Icon(Icons.palette)),
Circled(color: wrapColor, child: const Icon(Icons.widgets)),
Circled(color: wrapColor, child: const Icon(Icons.settings)),
];
bool _burstMenuItemClick(int index) {
print(index);
switch (index) {
case 0:
_doClose();
return true;
break;
case 1:
_toPoint();
break;
case 2:
_toGalley();
break;
case 3:
_toWidget();
break;
case 4:
_toSetting();
break;
}
return true;
}
// 处理 菜单 item 点击事件
void _toSetting() {
Navigator.of(context).pushNamed(UnitRouter.setting);
burstFlowKey.currentState.toggle();
}
void _toWidget() {
burstFlowKey.currentState.toggle();
}
void _toWidget() {}
void _toGalley() {
Navigator.of(context).pushNamed(UnitRouter.galley);
burstFlowKey.currentState.toggle();
}
void _toPoint() {
BlocProvider.of<PointBloc>(context).add(EventLoadPoint());
Navigator.of(context).pushNamed(UnitRouter.point);
burstFlowKey.currentState.toggle();
}
void _doClose() {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
burstFlowKey.currentState.toggle();
}
double endx;
double endX;
void _onPanEnd(details) {
endx = offset.dx;
endX = offset.dx;
_ctrl.reset();
_ctrl.forward();
// offset = Offset(x, y);
// entry.markNeedsBuild();
}
void _listenAnimate() {
@@ -197,7 +195,7 @@ class OverlayToolWrapperState extends State<OverlayToolWrapper>
// print(offset.dx);
if (offset.dx > MediaQuery.of(context).size.width / 2 - circleRadius) {
double begin = endx;
double begin = endX;
double end = MediaQuery.of(context).size.width -
menuSize / 2 -
circleRadius;
@@ -205,7 +203,7 @@ class OverlayToolWrapperState extends State<OverlayToolWrapper>
px = begin + (end - begin) * t; // x = menuSize / 2 - circleRadius;
} else {
double begin = endx;
double begin = endX;
double end = menuSize / 2 - circleRadius;
double t = _ctrl.value;
px = begin + (end - begin) * t; // x = menuSize / 2 - circleRadius;
@@ -237,15 +235,14 @@ class OverlayToolWrapperState extends State<OverlayToolWrapper>
entry.markNeedsBuild();
}
showFloating() {
void showFloating() {
if (!show) {
Overlay.of(context).insert(entry);
show = true;
}
}
//
hideFloating() {
void hideFloating() {
if (show) {
entry.remove();
show = false;
@@ -256,6 +253,4 @@ class OverlayToolWrapperState extends State<OverlayToolWrapper>
Widget build(BuildContext context) {
return widget.child;
}
void _onPanDown(DragDownDetails details) {}
}

View File

@@ -6,7 +6,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_unit/app/router/unit_router.dart';
import 'package:flutter_unit/app/utils/convert.dart';
import 'package:flutter_unit/blocs/bloc_exp.dart';
import 'package:flutter_unit/model/enums.dart';
import 'package:flutter_unit/model/widget_model.dart';
import 'package:flutter_unit/views/components/permanent/feedback_widget.dart';
import 'package:flutter_unit/views/components/project/default/empty_shower.dart';

View File

@@ -1,7 +1,7 @@
name: flutter_unit
description: A new Flutter application.
version: 1.5.1
version: 1.5.2
author: 张风捷特烈 <1981462002@qq.com>
homepage: https://juejin.cn/user/149189281194766/posts