lab_text_tool_tip.dart 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import 'package:flutter/material.dart';
  2. import 'package:flyinsono/lab/color/lab_colors.dart';
  3. import 'package:fis_theme/theme.dart';
  4. /// 可选的放置位置
  5. enum DisplayPosition {
  6. top,
  7. right,
  8. bottom,
  9. left,
  10. topRight,
  11. bottomRight,
  12. bottomLeft,
  13. topLeft,
  14. }
  15. /// 支持自定义的纯文本提示框组件
  16. class LabTextTooltip extends StatefulWidget {
  17. const LabTextTooltip({
  18. super.key,
  19. required this.message,
  20. required this.child,
  21. this.height = 26,
  22. this.position = DisplayPosition.top,
  23. this.margin = const EdgeInsets.all(0),
  24. this.padding = const EdgeInsets.symmetric(horizontal: 10),
  25. this.offset = const Offset(0, 0),
  26. this.decoration = defaultDecoration,
  27. this.textStyle = defaultTextStyle,
  28. this.disable = false,
  29. }) : super();
  30. final String message;
  31. final Widget child;
  32. final double height;
  33. final DisplayPosition position;
  34. final EdgeInsetsGeometry margin;
  35. final EdgeInsetsGeometry padding;
  36. final Offset offset;
  37. final Decoration decoration;
  38. final TextStyle textStyle;
  39. final bool disable;
  40. static const defaultTextStyle = TextStyle(
  41. color: LabColors.text200,
  42. fontSize: 12,
  43. );
  44. static const defaultDecoration = BoxDecoration(
  45. color: LabColors.hoverTextBgColor,
  46. borderRadius: BorderRadius.all(Radius.circular(4)),
  47. );
  48. @override
  49. State<LabTextTooltip> createState() => LabTextTooltipState();
  50. }
  51. class LabTextTooltipState extends State<LabTextTooltip> {
  52. OverlayEntry? _entry;
  53. /// 需要靠Bottom定位的情况
  54. static const List<DisplayPosition> NEED_BOTTOM = [
  55. DisplayPosition.top,
  56. DisplayPosition.topRight,
  57. DisplayPosition.topLeft,
  58. ];
  59. /// 需要靠Right定位的情况
  60. static const List<DisplayPosition> NEED_RIGHT = [
  61. DisplayPosition.left,
  62. DisplayPosition.topLeft,
  63. DisplayPosition.bottomLeft,
  64. ];
  65. /// 将容器扩宽来适应上下显示时的文本居中
  66. static const List<DisplayPosition> NEED_ENLARGE_WIDTH = [
  67. DisplayPosition.top,
  68. DisplayPosition.bottom,
  69. ];
  70. /// 默认情况下的文本大小
  71. static const double DEFAULT_TEXT_SIZE = 12;
  72. double get _maxMessageWidth =>
  73. widget.message.length * widget.textStyle.fontSize!;
  74. double get _paddingWidth => widget.padding.horizontal;
  75. double get _marginWidth => widget.margin.horizontal;
  76. double get _enlargeWidth => _maxMessageWidth + _paddingWidth + _marginWidth;
  77. /// 显示 Tooltip
  78. void _showTooltip() {
  79. _createNewEntry();
  80. }
  81. /// 隐藏 Tooltip
  82. void _hideTooltip() {
  83. _entry?.remove();
  84. _entry = null;
  85. }
  86. /// 创建新的 OverlayEntry
  87. void _createNewEntry() {
  88. final OverlayState overlayState = Overlay.of(
  89. context,
  90. debugRequiredFor: widget,
  91. );
  92. final RenderBox target = context.findRenderObject()! as RenderBox;
  93. final RenderBox targetConatiner =
  94. overlayState.context.findRenderObject() as RenderBox;
  95. final Offset targetOffset = target.localToGlobal(
  96. target.size.center(Offset.zero),
  97. ancestor: overlayState.context.findRenderObject(),
  98. );
  99. final Offset overlayOffset = _countOffset(
  100. targetConatiner.size, target.size, targetOffset, widget.position);
  101. final Offset edgeOffset =
  102. _edgeCorrection(targetConatiner.size, target.size, overlayOffset);
  103. final Widget overlay = _buildOverlay(edgeOffset, widget.position);
  104. _entry = OverlayEntry(builder: (BuildContext context) => overlay);
  105. overlayState.insert(_entry!);
  106. }
  107. /// 创建 Overlay 组件
  108. Widget _buildOverlay(Offset offset, DisplayPosition position) {
  109. return Directionality(
  110. textDirection: Directionality.of(context),
  111. child: Positioned.directional(
  112. top: NEED_BOTTOM.contains(position) ? null : offset.dy,
  113. start: NEED_RIGHT.contains(position) ? null : offset.dx,
  114. bottom: NEED_BOTTOM.contains(position) ? offset.dy : null,
  115. end: NEED_RIGHT.contains(position) ? offset.dx : null,
  116. textDirection: Directionality.of(context),
  117. child: SizedBox(
  118. width: NEED_ENLARGE_WIDTH.contains(position) ? _enlargeWidth : null,
  119. child: Center(
  120. child: UnconstrainedBox(
  121. child: Transform(
  122. transform: Matrix4.translationValues(
  123. widget.offset.dx,
  124. widget.offset.dy,
  125. 0,
  126. ),
  127. child: Container(
  128. decoration: widget.decoration,
  129. height: widget.height,
  130. padding: widget.padding,
  131. margin: widget.margin,
  132. child: Center(
  133. child: DefaultTextStyle(
  134. style: TextStyle(
  135. decoration: TextDecoration.none,
  136. fontFamily: FTheme.ins.localeSetting.fontFamily,
  137. ),
  138. child: Text(widget.message, style: widget.textStyle),
  139. ),
  140. ),
  141. ),
  142. ),
  143. ),
  144. ),
  145. ),
  146. ),
  147. );
  148. }
  149. /// 计算位置布局
  150. Offset _countOffset(Size containerSize, Size targetSize, Offset targetOffset,
  151. DisplayPosition position) {
  152. final double x = targetOffset.dx;
  153. final double y = targetOffset.dy;
  154. final double w = targetSize.width;
  155. final double h = targetSize.height;
  156. final double cw = containerSize.width;
  157. final double ch = containerSize.height;
  158. switch (position) {
  159. case DisplayPosition.left:
  160. return Offset(cw - x + w / 2, y - widget.height / 2);
  161. case DisplayPosition.top:
  162. return Offset(x - _enlargeWidth / 2, ch - y + h / 2);
  163. case DisplayPosition.right:
  164. return Offset(x + w / 2, y - widget.height / 2);
  165. case DisplayPosition.bottom:
  166. return Offset(x - _enlargeWidth / 2, y + h / 2);
  167. case DisplayPosition.topRight:
  168. return Offset(x + w / 2, ch - y + h / 2);
  169. case DisplayPosition.bottomRight:
  170. return Offset(x + w / 2, y + h / 2);
  171. case DisplayPosition.bottomLeft:
  172. return Offset(cw - x + w / 2, y + h / 2);
  173. case DisplayPosition.topLeft:
  174. return Offset(cw - x + w / 2, ch - y + h / 2);
  175. }
  176. }
  177. /// 边缘矫正
  178. Offset _edgeCorrection(
  179. Size containerSize, Size targetSize, Offset targetOffset) {
  180. final double x = targetOffset.dx;
  181. final double y = targetOffset.dy;
  182. final double w = targetSize.width;
  183. final double h = targetSize.height;
  184. final double cw = containerSize.width;
  185. final double ch = containerSize.height;
  186. if (x < 0) {
  187. return Offset(0, y);
  188. }
  189. if (x + w > cw) {
  190. return Offset(cw - w, y);
  191. }
  192. if (y < 0) {
  193. return Offset(x, 0);
  194. }
  195. if (y + h > ch) {
  196. return Offset(x, ch - h);
  197. }
  198. return targetOffset;
  199. }
  200. @override
  201. Widget build(BuildContext context) {
  202. if (widget.disable) {
  203. return widget.child;
  204. }
  205. return MouseRegion(
  206. onEnter: (event) {
  207. _showTooltip();
  208. },
  209. onExit: (event) {
  210. _hideTooltip();
  211. },
  212. child: Container(
  213. child: widget.child,
  214. ),
  215. );
  216. }
  217. /// 组件入参更新时,如果 disable 为 true,则隐藏 Tooltip
  218. @override
  219. void didUpdateWidget(LabTextTooltip oldWidget) {
  220. super.didUpdateWidget(oldWidget);
  221. if (widget.disable) {
  222. _hideTooltip();
  223. }
  224. }
  225. @override
  226. void dispose() {
  227. super.dispose();
  228. if (_entry != null) {
  229. _entry?.remove();
  230. _entry = null;
  231. }
  232. }
  233. }