import 'package:flutter/material.dart'; import 'package:flyinsono/lab/color/lab_colors.dart'; import 'package:fis_theme/theme.dart'; /// 可选的放置位置 enum DisplayPosition { top, right, bottom, left, topRight, bottomRight, bottomLeft, topLeft, } /// 支持自定义的纯文本提示框组件 class LabTextTooltip extends StatefulWidget { const LabTextTooltip({ super.key, required this.message, required this.child, this.height = 26, this.position = DisplayPosition.top, this.margin = const EdgeInsets.all(0), this.padding = const EdgeInsets.symmetric(horizontal: 10), this.offset = const Offset(0, 0), this.decoration = defaultDecoration, this.textStyle = defaultTextStyle, this.disable = false, }) : super(); final String message; final Widget child; final double height; final DisplayPosition position; final EdgeInsetsGeometry margin; final EdgeInsetsGeometry padding; final Offset offset; final Decoration decoration; final TextStyle textStyle; final bool disable; static const defaultTextStyle = TextStyle( color: LabColors.text200, fontSize: 12, ); static const defaultDecoration = BoxDecoration( color: LabColors.hoverTextBgColor, borderRadius: BorderRadius.all(Radius.circular(4)), ); @override State createState() => LabTextTooltipState(); } class LabTextTooltipState extends State { OverlayEntry? _entry; /// 需要靠Bottom定位的情况 static const List NEED_BOTTOM = [ DisplayPosition.top, DisplayPosition.topRight, DisplayPosition.topLeft, ]; /// 需要靠Right定位的情况 static const List NEED_RIGHT = [ DisplayPosition.left, DisplayPosition.topLeft, DisplayPosition.bottomLeft, ]; /// 将容器扩宽来适应上下显示时的文本居中 static const List NEED_ENLARGE_WIDTH = [ DisplayPosition.top, DisplayPosition.bottom, ]; /// 默认情况下的文本大小 static const double DEFAULT_TEXT_SIZE = 12; double get _maxMessageWidth => widget.message.length * widget.textStyle.fontSize!; double get _paddingWidth => widget.padding.horizontal; double get _marginWidth => widget.margin.horizontal; double get _enlargeWidth => _maxMessageWidth + _paddingWidth + _marginWidth; /// 显示 Tooltip void _showTooltip() { _createNewEntry(); } /// 隐藏 Tooltip void _hideTooltip() { _entry?.remove(); _entry = null; } /// 创建新的 OverlayEntry void _createNewEntry() { final OverlayState overlayState = Overlay.of( context, debugRequiredFor: widget, ); final RenderBox target = context.findRenderObject()! as RenderBox; final RenderBox targetConatiner = overlayState.context.findRenderObject() as RenderBox; final Offset targetOffset = target.localToGlobal( target.size.center(Offset.zero), ancestor: overlayState.context.findRenderObject(), ); final Offset overlayOffset = _countOffset( targetConatiner.size, target.size, targetOffset, widget.position); final Offset edgeOffset = _edgeCorrection(targetConatiner.size, target.size, overlayOffset); final Widget overlay = _buildOverlay(edgeOffset, widget.position); _entry = OverlayEntry(builder: (BuildContext context) => overlay); overlayState.insert(_entry!); } /// 创建 Overlay 组件 Widget _buildOverlay(Offset offset, DisplayPosition position) { return Directionality( textDirection: Directionality.of(context), child: Positioned.directional( top: NEED_BOTTOM.contains(position) ? null : offset.dy, start: NEED_RIGHT.contains(position) ? null : offset.dx, bottom: NEED_BOTTOM.contains(position) ? offset.dy : null, end: NEED_RIGHT.contains(position) ? offset.dx : null, textDirection: Directionality.of(context), child: SizedBox( width: NEED_ENLARGE_WIDTH.contains(position) ? _enlargeWidth : null, child: Center( child: UnconstrainedBox( child: Transform( transform: Matrix4.translationValues( widget.offset.dx, widget.offset.dy, 0, ), child: Container( decoration: widget.decoration, height: widget.height, padding: widget.padding, margin: widget.margin, child: Center( child: DefaultTextStyle( style: TextStyle( decoration: TextDecoration.none, fontFamily: FTheme.ins.localeSetting.fontFamily, ), child: Text(widget.message, style: widget.textStyle), ), ), ), ), ), ), ), ), ); } /// 计算位置布局 Offset _countOffset(Size containerSize, Size targetSize, Offset targetOffset, DisplayPosition position) { final double x = targetOffset.dx; final double y = targetOffset.dy; final double w = targetSize.width; final double h = targetSize.height; final double cw = containerSize.width; final double ch = containerSize.height; switch (position) { case DisplayPosition.left: return Offset(cw - x + w / 2, y - widget.height / 2); case DisplayPosition.top: return Offset(x - _enlargeWidth / 2, ch - y + h / 2); case DisplayPosition.right: return Offset(x + w / 2, y - widget.height / 2); case DisplayPosition.bottom: return Offset(x - _enlargeWidth / 2, y + h / 2); case DisplayPosition.topRight: return Offset(x + w / 2, ch - y + h / 2); case DisplayPosition.bottomRight: return Offset(x + w / 2, y + h / 2); case DisplayPosition.bottomLeft: return Offset(cw - x + w / 2, y + h / 2); case DisplayPosition.topLeft: return Offset(cw - x + w / 2, ch - y + h / 2); } } /// 边缘矫正 Offset _edgeCorrection( Size containerSize, Size targetSize, Offset targetOffset) { final double x = targetOffset.dx; final double y = targetOffset.dy; final double w = targetSize.width; final double h = targetSize.height; final double cw = containerSize.width; final double ch = containerSize.height; if (x < 0) { return Offset(0, y); } if (x + w > cw) { return Offset(cw - w, y); } if (y < 0) { return Offset(x, 0); } if (y + h > ch) { return Offset(x, ch - h); } return targetOffset; } @override Widget build(BuildContext context) { if (widget.disable) { return widget.child; } return MouseRegion( onEnter: (event) { _showTooltip(); }, onExit: (event) { _hideTooltip(); }, child: Container( child: widget.child, ), ); } /// 组件入参更新时,如果 disable 为 true,则隐藏 Tooltip @override void didUpdateWidget(LabTextTooltip oldWidget) { super.didUpdateWidget(oldWidget); if (widget.disable) { _hideTooltip(); } } @override void dispose() { super.dispose(); if (_entry != null) { _entry?.remove(); _entry = null; } } }