text_tooltip.dart 7.2 KB

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