import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:go_router/go_router.dart'; import '../../../../app_core/utils/app_style.dart'; class OverlayAction { final String title; final Function() onTap; OverlayAction({required this.title, required this.onTap}); } class OverlayPortalWidget extends StatefulWidget { final Widget child; final double offsetY; final List<OverlayAction> actions; const OverlayPortalWidget({ super.key, required this.child, this.offsetY = 8.0, required this.actions, }); @override State<OverlayPortalWidget> createState() => _OverlayPortalWidgetState(); } class _OverlayPortalWidgetState extends State<OverlayPortalWidget> with SingleTickerProviderStateMixin { final OverlayPortalController _tooltipController = OverlayPortalController(); late AnimationController _animationController; late Animation<double> _fadeAnimation; final GlobalKey _childKey = GlobalKey(); final GlobalKey _tooltipKey = GlobalKey(); Offset _overlayPosition = Offset.zero; bool _showAbove = false; late VoidCallback _routerListener; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 200), reverseDuration: const Duration(milliseconds: 150), ); _fadeAnimation = CurvedAnimation( parent: _animationController, curve: Curves.easeOut, reverseCurve: Curves.easeIn, ); // Добавляем слушатель маршрута в GoRouter _routerListener = () { if (_tooltipController.isShowing) { _tooltipController.hide(); } }; GoRouter.of(context).routerDelegate.addListener(_routerListener); } @override void dispose() { _tooltipController.hide(); // Гарантированное закрытие тултипа _animationController.dispose(); GoRouter.of(context).routerDelegate.removeListener(_routerListener); // Удаление слушателя super.dispose(); } void _toggleOverlay() { if (_tooltipController.isShowing) { _animationController.reverse().then((_) => _tooltipController.hide()); } else { _calculateOverlayPosition(); _tooltipController.show(); _animationController.forward(); } } /// Вычисляем позицию тултипа, чтобы он не выходил за границы экрана void _calculateOverlayPosition() { final RenderBox renderBox = _childKey.currentContext?.findRenderObject() as RenderBox; final Offset localOffset = renderBox.localToGlobal(Offset.zero); final Size screenSize = MediaQuery.of(context).size; WidgetsBinding.instance.addPostFrameCallback((_) { final RenderBox? tooltipBox = _tooltipKey.currentContext?.findRenderObject() as RenderBox?; final double tooltipHeight = tooltipBox?.size.height ?? 50.h; final double tooltipWidth = tooltipBox?.size.width ?? 200.w; double newX = localOffset.dx; double newY = localOffset.dy + renderBox.size.height + widget.offsetY; // Проверка на выход за правый край экрана if (newX + tooltipWidth > screenSize.width) { newX = screenSize.width - tooltipWidth - 30; } // Проверка на выход за левый край экрана if (newX < 0) { newX = 8; // Минимальный отступ от экрана } // Если тултип выходит за нижний край — показываем сверху bool showAbove = false; if (newY + tooltipHeight > screenSize.height) { newY = localOffset.dy - tooltipHeight - widget.offsetY; showAbove = true; } setState(() { _overlayPosition = Offset(newX, newY); _showAbove = showAbove; }); }); } @override Widget build(BuildContext context) { return GestureDetector( onTap: _toggleOverlay, child: OverlayPortal( controller: _tooltipController, overlayChildBuilder: (context) { return Positioned( left: _overlayPosition.dx, top: _overlayPosition.dy, child: FadeTransition( opacity: _fadeAnimation, child: Material( key: _tooltipKey, color: Colors.transparent, child: _listActions(), ), ), ); }, child: Container( key: _childKey, child: AbsorbPointer(child: widget.child), ), ), ); } Widget _listActions() { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12.r), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 5, spreadRadius: 2, ), ], ), clipBehavior: Clip.antiAlias, child: Column( children: widget.actions.map(_action).toList(), ), ); } Widget _action(OverlayAction action) { return GestureDetector( onTap: () { _tooltipController.hide(); action.onTap.call(); }, child: Container( width: 150.w, color: Colors.white, padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), child: Row( children: [ FittedBox( child: Text( action.title.tr(), maxLines: 1, textAlign: TextAlign.start, style: Theme.of(context).textTheme.titleSmall?.copyWith( color: AppStyle.dark, fontSize: 12.sp, ), ), ), ], ), ), ); } }
Preview:
downloadDownload PNG
downloadDownload JPEG
downloadDownload SVG
Tip: You can change the style, width & colours of the snippet with the inspect tool before clicking Download!
Click to optimize width for Twitter