more_operate_menu.dart 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import 'package:flutter/material.dart';
  2. import 'package:flyinsono/lab/color/lab_colors.dart';
  3. import 'package:flyinsono/lab/components/operate_button/more_operate_button.dart';
  4. import 'package:flyinsono/lab/style/lab_box_decoration.dart';
  5. /// 更多功能扩展菜单
  6. class MoreOperateMenu extends StatefulWidget {
  7. const MoreOperateMenu({
  8. super.key,
  9. required this.buttons,
  10. }) : super();
  11. final List<MoreOperateButton> buttons;
  12. static const defaultTextStyle = TextStyle(
  13. color: LabColors.text200,
  14. fontSize: 12,
  15. );
  16. static const defaultDecoration = BoxDecoration(
  17. color: LabColors.base800,
  18. borderRadius: BorderRadius.all(Radius.circular(4)),
  19. );
  20. @override
  21. State<MoreOperateMenu> createState() => MoreOperateMenuState();
  22. static MoreOperateMenuState? maybeOf(BuildContext context) {
  23. // assert(context);
  24. return context.findAncestorStateOfType<MoreOperateMenuState>();
  25. }
  26. }
  27. class MoreOperateMenuState extends State<MoreOperateMenu> {
  28. OverlayEntry? _entry;
  29. bool _isHoverMore = false;
  30. /// 显示 Overlay
  31. void _showOverlay() {
  32. if (_entry != null) {
  33. return;
  34. }
  35. _createNewEntry();
  36. }
  37. /// 隐藏 Overlay
  38. void _hideOverlay() {
  39. _entry?.remove();
  40. _entry = null;
  41. }
  42. /// 创建新的 OverlayEntry
  43. void _createNewEntry() {
  44. final OverlayState overlayState = Overlay.of(
  45. context,
  46. debugRequiredFor: widget,
  47. );
  48. final RenderBox target = context.findRenderObject()! as RenderBox;
  49. final RenderBox targetConatiner =
  50. overlayState.context.findRenderObject() as RenderBox;
  51. final Offset targetOffset = target.localToGlobal(
  52. target.size.center(Offset.zero),
  53. ancestor: overlayState.context.findRenderObject(),
  54. );
  55. final Offset overlayOffset =
  56. _countOffset(targetConatiner.size, target.size, targetOffset);
  57. final Offset edgeOffset =
  58. _edgeCorrection(targetConatiner.size, target.size, overlayOffset);
  59. final Widget overlay = _buildOverlay(edgeOffset);
  60. _entry = OverlayEntry(builder: (BuildContext context) => overlay);
  61. _listenButtonClicked();
  62. overlayState.insert(_entry!);
  63. }
  64. /// 监听按钮事件来关闭 Overlay
  65. void _listenButtonClicked() {
  66. for (final button in widget.buttons) {
  67. button.clicked.removeListener(_closeMoreMenu);
  68. }
  69. for (final button in widget.buttons) {
  70. button.clicked.addListener(_closeMoreMenu);
  71. }
  72. }
  73. /// 创建 Overlay 组件
  74. Widget _buildOverlay(
  75. Offset offset,
  76. ) {
  77. return Directionality(
  78. textDirection: Directionality.of(context),
  79. child: Positioned.directional(
  80. top: offset.dy,
  81. start: offset.dx,
  82. textDirection: Directionality.of(context),
  83. child: MouseRegion(
  84. onEnter: _handleMouseEnterMenu,
  85. onExit: _handleMouseExitMenu,
  86. child: Container(
  87. padding: EdgeInsets.only(top: 5),
  88. child: Container(
  89. decoration: LabBoxDecoration.base.copyWith(
  90. color: LabColors.base300,
  91. boxShadow: [
  92. BoxShadow(
  93. color: Colors.black.withOpacity(0.1),
  94. blurRadius: 2,
  95. spreadRadius: 1,
  96. ),
  97. ],
  98. ),
  99. padding: EdgeInsets.all(5),
  100. child: SizedBox(
  101. width: 100,
  102. child: Column(
  103. children: widget.buttons,
  104. ),
  105. ),
  106. ),
  107. ),
  108. ),
  109. ),
  110. );
  111. }
  112. /// 计算位置布局
  113. Offset _countOffset(
  114. Size containerSize, Size targetSize, Offset targetOffset) {
  115. final double x = targetOffset.dx;
  116. final double y = targetOffset.dy;
  117. final double w = targetSize.width;
  118. final double h = targetSize.height;
  119. return Offset(x - w / 2, y + h / 2);
  120. }
  121. /// 边缘矫正
  122. Offset _edgeCorrection(
  123. Size containerSize, Size targetSize, Offset targetOffset) {
  124. final double x = targetOffset.dx;
  125. final double y = targetOffset.dy;
  126. final double w = targetSize.width;
  127. final double h = targetSize.height;
  128. final double cw = containerSize.width;
  129. final double ch = containerSize.height;
  130. if (x < 0) {
  131. return Offset(0, y);
  132. }
  133. if (x + w > cw) {
  134. return Offset(cw - w, y);
  135. }
  136. if (y < 0) {
  137. return Offset(x, 0);
  138. }
  139. if (y + h > ch) {
  140. return Offset(x, ch - h);
  141. }
  142. return targetOffset;
  143. }
  144. @override
  145. Widget build(BuildContext context) {
  146. Color _colorMore = _isHoverMore ? LabColors.base400 : Colors.transparent;
  147. return MouseRegion(
  148. cursor: SystemMouseCursors.click,
  149. onEnter: _handleMouseEnterMoreButton,
  150. onExit: _handleMouseExitMoreButton,
  151. child: Container(
  152. decoration: BoxDecoration(
  153. color: _colorMore,
  154. borderRadius: BorderRadius.circular(5),
  155. ),
  156. child: Container(
  157. width: 80,
  158. child: Icon(
  159. Icons.keyboard_arrow_down_rounded,
  160. size: 20,
  161. color: LabColors.base700,
  162. ),
  163. ),
  164. ),
  165. );
  166. }
  167. void _handleMouseEnterMoreButton(PointerEvent details) {
  168. _showOverlay();
  169. setState(() {
  170. _isHoverMore = true;
  171. });
  172. }
  173. void _handleMouseExitMoreButton(PointerEvent details) {
  174. setState(() {
  175. _isHoverMore = false;
  176. });
  177. // 如果下一帧不在菜单内,则隐藏 Overlay
  178. WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
  179. if (!_isHoverMore) {
  180. _hideOverlay();
  181. setState(() {
  182. _isHoverMore = false;
  183. });
  184. }
  185. });
  186. }
  187. void _handleMouseEnterMenu(PointerEvent details) {
  188. setState(() {
  189. _isHoverMore = true;
  190. });
  191. }
  192. void _handleMouseExitMenu(PointerEvent details) {
  193. _hideOverlay();
  194. setState(() {
  195. _isHoverMore = false;
  196. });
  197. }
  198. void _closeMoreMenu() {
  199. _hideOverlay();
  200. setState(() {
  201. _isHoverMore = false;
  202. });
  203. }
  204. /// 组件入参更新时,如果 disable 为 true,则隐藏 Overlay
  205. @override
  206. void didUpdateWidget(MoreOperateMenu oldWidget) {
  207. super.didUpdateWidget(oldWidget);
  208. }
  209. @override
  210. void dispose() {
  211. super.dispose();
  212. if (_entry != null) {
  213. _entry?.remove();
  214. _entry = null;
  215. }
  216. }
  217. }