|
@@ -0,0 +1,253 @@
|
|
|
+import 'package:flutter/material.dart';
|
|
|
+import 'package:fis_theme/theme.dart';
|
|
|
+
|
|
|
+/// 可选的放置位置
|
|
|
+enum DisplayPosition {
|
|
|
+ top,
|
|
|
+ right,
|
|
|
+ bottom,
|
|
|
+ left,
|
|
|
+ topRight,
|
|
|
+ bottomRight,
|
|
|
+ bottomLeft,
|
|
|
+ topLeft,
|
|
|
+}
|
|
|
+
|
|
|
+/// 支持自定义的纯文本提示框组件
|
|
|
+class TextTooltip extends StatefulWidget {
|
|
|
+ const TextTooltip({
|
|
|
+ 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: Colors.white,
|
|
|
+ fontSize: 12,
|
|
|
+ );
|
|
|
+
|
|
|
+ static const defaultDecoration = BoxDecoration(
|
|
|
+ color: Colors.black,
|
|
|
+ borderRadius: BorderRadius.all(Radius.circular(4)),
|
|
|
+ );
|
|
|
+
|
|
|
+ @override
|
|
|
+ State<TextTooltip> createState() => TextTooltipState();
|
|
|
+}
|
|
|
+
|
|
|
+class TextTooltipState extends State<TextTooltip> {
|
|
|
+ OverlayEntry? _entry;
|
|
|
+
|
|
|
+ /// 需要靠Bottom定位的情况
|
|
|
+ static const List<DisplayPosition> NEED_BOTTOM = [
|
|
|
+ DisplayPosition.top,
|
|
|
+ DisplayPosition.topRight,
|
|
|
+ DisplayPosition.topLeft,
|
|
|
+ ];
|
|
|
+
|
|
|
+ /// 需要靠Right定位的情况
|
|
|
+ static const List<DisplayPosition> NEED_RIGHT = [
|
|
|
+ DisplayPosition.left,
|
|
|
+ DisplayPosition.topLeft,
|
|
|
+ DisplayPosition.bottomLeft,
|
|
|
+ ];
|
|
|
+
|
|
|
+ /// 将容器扩宽来适应上下显示时的文本居中
|
|
|
+ static const List<DisplayPosition> 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: "NotoSansSC",
|
|
|
+ ),
|
|
|
+ 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(TextTooltip oldWidget) {
|
|
|
+ super.didUpdateWidget(oldWidget);
|
|
|
+ if (widget.disable) {
|
|
|
+ _hideTooltip();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ void dispose() {
|
|
|
+ super.dispose();
|
|
|
+ if (_entry != null) {
|
|
|
+ _entry?.remove();
|
|
|
+ _entry = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|