import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; enum TooltipDirection { up, down, left, right } enum ShowCloseButton { inside, outside, none } enum ClipAreaShape { oval, rectangle } typedef OutSideTapHandler = void Function(); //////////////////////////////////////////////////////////////////////////////////////////////////// /// Super flexible Tooltip class that allows you to show any content /// inside a Tooltip in the overlay of the screen. /// class SuperTooltip { /// Allows to accedd the closebutton for UI Testing static Key closeButtonKey = const Key("CloseButtonKey"); /// Signals if the Tooltip is visible at the moment bool isOpen = false; /// /// The content of the Tooltip final Widget content; /// /// The direcion in which the tooltip should open TooltipDirection popupDirection; /// /// optional handler that gets called when the Tooltip is closed final OutSideTapHandler? onClose; /// /// [minWidth], [minHeight], [maxWidth], [maxHeight] optional size constraints. /// If a constraint is not set the size will ajust to the content double? minWidth, minHeight, maxWidth, maxHeight; /// /// The minium padding from the Tooltip to the screen limits final double minimumOutSidePadding; /// /// If [snapsFarAwayVertically== true] the bigger free space above or below the target will be /// covered completely by the ToolTip. All other dimension or position constraints get overwritten final bool snapsFarAwayVertically; /// /// If [snapsFarAwayHorizontally== true] the bigger free space left or right of the target will be /// covered completely by the ToolTip. All other dimension or position constraints get overwritten final bool snapsFarAwayHorizontally; /// [top], [right], [bottom], [left] position the Tooltip absolute relative to the whole screen double? top, right, bottom, left; /// /// A Tooltip can have none, an inside or an outside close icon final ShowCloseButton showCloseButton; /// /// [hasShadow] defines if the tooltip should have a shadow final bool hasShadow; /// /// The shadow color. final Color shadowColor; /// /// The shadow blur radius. final double shadowBlurRadius; /// /// The shadow spread radius. final double shadowSpreadRadius; /// /// the stroke width of the border final double borderWidth; /// /// The corder radii of the border final double borderRadius; /// /// The color of the border final Color borderColor; /// /// The color of the close icon final Color closeButtonColor; /// /// The size of the close button final double closeButtonSize; /// /// The icon for the close button final IconData closeButtonIcon; /// /// The length of the Arrow final double arrowLength; /// /// The width of the arrow at its base final double arrowBaseWidth; /// /// The distance of the tip of the arrow's tip to the center of the target final double arrowTipDistance; /// /// The backgroundcolor of the Tooltip final Color backgroundColor; /// The color of the rest of the overlay surrounding the Tooltip. /// typically a translucent color. final Color outsideBackgroundColor; /// /// By default touching the surrounding of the Tooltip closes the tooltip. /// you can define a rectangle area where the background is completely transparent /// and the widgets below react to touch final Rect? touchThrougArea; /// /// The shape of the [touchThrougArea]. final ClipAreaShape touchThroughAreaShape; /// /// If [touchThroughAreaShape] is [ClipAreaShape.rectangle] you can define a border radius final double touchThroughAreaCornerRadius; /// /// Let's you pass a key to the Tooltips cotainer for UI Testing final Key? tooltipContainerKey; /// /// Allow the tooltip to be dismissed tapping outside final bool dismissOnTapOutside; /// /// Enable background overlay final bool containsBackgroundOverlay; final bool custom; Offset? _targetCenter; OverlayEntry? _backGroundOverlay; OverlayEntry? _ballonOverlay; SuperTooltip({ this.tooltipContainerKey, required this.content, // The contents of the tooltip. required this.popupDirection, this.onClose, this.minWidth, this.minHeight, this.maxWidth, this.maxHeight, this.top, this.right, this.bottom, this.left, this.minimumOutSidePadding = 20.0, this.showCloseButton = ShowCloseButton.none, this.snapsFarAwayVertically = false, this.snapsFarAwayHorizontally = false, this.hasShadow = true, this.shadowColor = Colors.black54, this.shadowBlurRadius = 10.0, this.shadowSpreadRadius = 5.0, this.borderWidth = 2.0, this.borderRadius = 10.0, this.borderColor = Colors.black, this.closeButtonIcon = Icons.close, this.closeButtonColor = Colors.black, this.closeButtonSize = 30.0, this.arrowLength = 20.0, this.arrowBaseWidth = 20.0, this.arrowTipDistance = 2.0, this.backgroundColor = Colors.white, this.outsideBackgroundColor = const Color.fromARGB(50, 255, 255, 255), this.touchThroughAreaShape = ClipAreaShape.oval, this.touchThroughAreaCornerRadius = 5.0, this.touchThrougArea, this.dismissOnTapOutside = true, this.containsBackgroundOverlay = true, this.custom = false, }) : assert((maxWidth ?? double.infinity) >= (minWidth ?? 0.0)), assert((maxHeight ?? double.infinity) >= (minHeight ?? 0.0)); /// /// Removes the Tooltip from the overlay void close() { if (onClose != null) { onClose!(); } _ballonOverlay?.remove(); _backGroundOverlay?.remove(); isOpen = false; } void rebuild() { _ballonOverlay!.remove(); _backGroundOverlay?.remove(); isOpen = false; } /// /// Displays the tooltip /// The center of [targetContext] is used as target of the arrow void show(BuildContext targetContext) { final renderBox = targetContext.findRenderObject() as RenderBox; final overlay = Overlay.of(targetContext)!.context.findRenderObject() as RenderBox?; _targetCenter = renderBox.localToGlobal(renderBox.size.center(Offset.zero), ancestor: overlay); // Create the background below the popup including the clipArea. if (containsBackgroundOverlay) { _backGroundOverlay = OverlayEntry( builder: (context) => _AnimationWrapper( builder: (context, opacity) => AnimatedOpacity( opacity: opacity, duration: const Duration(milliseconds: 600), child: GestureDetector( onTap: () { if (dismissOnTapOutside) { close(); } }, child: Container( decoration: ShapeDecoration( shape: _ShapeOverlay( touchThrougArea, touchThroughAreaShape, touchThroughAreaCornerRadius, outsideBackgroundColor))), ), ), )); } /// Handling snap far away feature. if (snapsFarAwayVertically) { maxHeight = null; left = 0.0; right = 0.0; if (_targetCenter!.dy > overlay!.size.center(Offset.zero).dy) { popupDirection = TooltipDirection.up; top = 0.0; } else { popupDirection = TooltipDirection.down; bottom = 0.0; } } // Only one of of them is possible, and vertical has higher priority. else if (snapsFarAwayHorizontally) { maxWidth = null; top = 0.0; bottom = 0.0; if (_targetCenter!.dx < overlay!.size.center(Offset.zero).dx) { popupDirection = TooltipDirection.right; right = 0.0; } else { popupDirection = TooltipDirection.left; left = 0.0; } } _ballonOverlay = OverlayEntry( builder: (context) => _AnimationWrapper( builder: (context, opacity) => AnimatedOpacity( duration: Duration( milliseconds: 300, ), opacity: opacity, child: Center( child: CustomSingleChildLayout( delegate: _PopupBallonLayoutDelegate( popupDirection: popupDirection, targetCenter: _targetCenter, minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight, outSidePadding: minimumOutSidePadding, top: top, bottom: bottom, left: left, right: right, ), child: Stack( fit: StackFit.passthrough, children: [_buildPopUp(), _buildCloseButton()], ))), ), )); var overlays = []; if (containsBackgroundOverlay) { overlays.add(_backGroundOverlay!); } overlays.add(_ballonOverlay!); Overlay.of(targetContext)!.insertAll(overlays); isOpen = true; } void showBox(BuildContext targetContext) { if (targetContext == null || targetContext.findRenderObject() == null) return; final renderBox = targetContext.findRenderObject() as RenderBox; var size = renderBox.size; print("Size $size"); if (containsBackgroundOverlay) { _backGroundOverlay = OverlayEntry( builder: (context) => _AnimationWrapper( builder: (context, opacity) => AnimatedOpacity( opacity: opacity, duration: const Duration(milliseconds: 600), child: GestureDetector( onTap: () { if (dismissOnTapOutside) { close(); } }, child: Container( decoration: ShapeDecoration( shape: _ShapeOverlay( touchThrougArea, touchThroughAreaShape, touchThroughAreaCornerRadius, outsideBackgroundColor))), ), ), )); } _ballonOverlay = OverlayEntry( builder: (context) => _AnimationWrapper( builder: (context, opacity) => Positioned( left: left, //offset.dx, top: top, width: size.width > maxWidth! ? maxWidth : size.width, child: AnimatedOpacity( duration: Duration( milliseconds: 300, ), opacity: opacity, child: Stack( fit: StackFit.passthrough, children: [_buildPopUp(), _buildCloseButton()], ), )), )); var overlays = []; if (containsBackgroundOverlay) { overlays.add(_backGroundOverlay!); } overlays.add(_ballonOverlay!); Overlay.of(targetContext)!.insertAll(overlays); isOpen = true; } Widget _buildPopUp() { return Positioned( child: Container( key: tooltipContainerKey, decoration: ShapeDecoration( color: backgroundColor, shadows: hasShadow ? [BoxShadow(color: shadowColor, blurRadius: shadowBlurRadius, spreadRadius: shadowSpreadRadius)] : null, shape: !custom ? _BubbleShape(popupDirection, _targetCenter, borderRadius, arrowBaseWidth, arrowTipDistance, borderColor, borderWidth, left, top, right, bottom) : RoundedRectangleBorder( side: BorderSide(color: borderColor, width: borderWidth), borderRadius: BorderRadius.all(Radius.circular(borderRadius)))), margin: _getBallonContainerMargin(), child: content, ), ); } Widget _buildCloseButton() { const internalClickAreaPadding = 2.0; // if (showCloseButton == ShowCloseButton.none) { return new SizedBox(); } // --- double right; double top; switch (popupDirection) { // // LEFT: ------------------------------------- case TooltipDirection.left: right = arrowLength + arrowTipDistance + 3.0; if (showCloseButton == ShowCloseButton.inside) { top = 2.0; } else if (showCloseButton == ShowCloseButton.outside) { top = 0.0; } else throw AssertionError(showCloseButton); break; // RIGHT/UP: --------------------------------- case TooltipDirection.right: case TooltipDirection.up: right = 5.0; if (showCloseButton == ShowCloseButton.inside) { top = 2.0; } else if (showCloseButton == ShowCloseButton.outside) { top = 0.0; } else throw AssertionError(showCloseButton); break; // DOWN: ------------------------------------- case TooltipDirection.down: // If this value gets negative the Shadow gets clipped. The problem occurs is arrowlength + arrowTipDistance // is smaller than _outSideCloseButtonPadding which would mean arrowLength would need to be increased if the button is ouside. right = 2.0; if (showCloseButton == ShowCloseButton.inside) { top = arrowLength + arrowTipDistance + 2.0; } else if (showCloseButton == ShowCloseButton.outside) { top = 0.0; } else throw AssertionError(showCloseButton); break; // --------------------------------------------- default: throw AssertionError(popupDirection); } // --- return Positioned( right: right, top: top, child: GestureDetector( onTap: close, child: Padding( padding: const EdgeInsets.all(internalClickAreaPadding), child: Icon( closeButtonIcon, size: closeButtonSize, color: closeButtonColor, ), ), )); } EdgeInsets _getBallonContainerMargin() { var top = (showCloseButton == ShowCloseButton.outside) ? closeButtonSize + 5 : 0.0; switch (popupDirection) { // case TooltipDirection.down: return EdgeInsets.only( top: arrowTipDistance + arrowLength, ); case TooltipDirection.up: return EdgeInsets.only(bottom: arrowTipDistance + arrowLength, top: top); case TooltipDirection.left: return EdgeInsets.only(right: arrowTipDistance + arrowLength, top: top); case TooltipDirection.right: return EdgeInsets.only(left: arrowTipDistance + arrowLength, top: top); default: throw AssertionError(popupDirection); } } } class _CustomBallonLayoutDelegate extends SingleChildLayoutDelegate { final TooltipDirection? _popupDirection; final Offset? _targetCenter; final double? _minWidth; final double? _maxWidth; final double? _minHeight; final double? _maxHeight; final double _top; final double? _bottom; final double _left; final double? _right; final double? _outSidePadding; _CustomBallonLayoutDelegate({ TooltipDirection? popupDirection, Offset? targetCenter, double? minWidth, double? maxWidth, double? minHeight, double? maxHeight, double? outSidePadding, required double top, double? bottom, required double left, double? right, }) : _targetCenter = targetCenter, _popupDirection = popupDirection, _minWidth = minWidth, _maxWidth = maxWidth, _minHeight = minHeight, _maxHeight = maxHeight, _top = top, _bottom = bottom, _left = left, _right = right, _outSidePadding = outSidePadding; @override BoxConstraints getConstraintsForChild(BoxConstraints constraints) { return super.getConstraintsForChild(constraints); } @override Offset getPositionForChild(Size size, Size childSize) { print(" ------ getPositionFroChild: $_top - $_left"); //we place the widget at the cnter, by dividing the width and height by 2 to get the center return Offset(_left, _top); } @override bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) { return false; } } //////////////////////////////////////////////////////////////////////////////////////////////////// class _PopupBallonLayoutDelegate extends SingleChildLayoutDelegate { final TooltipDirection? _popupDirection; final Offset? _targetCenter; final double? _minWidth; final double? _maxWidth; final double? _minHeight; final double? _maxHeight; final double? _top; final double? _bottom; final double? _left; final double? _right; final double? _outSidePadding; _PopupBallonLayoutDelegate({ TooltipDirection? popupDirection, Offset? targetCenter, double? minWidth, double? maxWidth, double? minHeight, double? maxHeight, double? outSidePadding, double? top, double? bottom, double? left, double? right, }) : _targetCenter = targetCenter, _popupDirection = popupDirection, _minWidth = minWidth, _maxWidth = maxWidth, _minHeight = minHeight, _maxHeight = maxHeight, _top = top, _bottom = bottom, _left = left, _right = right, _outSidePadding = outSidePadding; @override Offset getPositionForChild(Size size, Size childSize) { double? calcLeftMostXtoTarget() { double? leftMostXtoTarget; if (_left != null) { leftMostXtoTarget = _left; } else if (_right != null) { leftMostXtoTarget = max( size.topLeft(Offset.zero).dx + _outSidePadding!, size.topRight(Offset.zero).dx - _outSidePadding! - childSize.width - _right!); } else { leftMostXtoTarget = max(_outSidePadding!, min(_targetCenter!.dx - childSize.width / 2, size.topRight(Offset.zero).dx - _outSidePadding! - childSize.width)); } return leftMostXtoTarget; } double? calcTopMostYtoTarget() { double? topmostYtoTarget; if (_top != null) { topmostYtoTarget = _top!; } else if (_bottom != null) { topmostYtoTarget = max(size.topLeft(Offset.zero).dy + _outSidePadding!, size.bottomRight(Offset.zero).dy - _outSidePadding! - childSize.height - _bottom!); } else { topmostYtoTarget = max(_outSidePadding!, min(_targetCenter!.dy - childSize.height / 2, size.bottomRight(Offset.zero).dy - _outSidePadding! - childSize.height)); } return topmostYtoTarget; } switch (_popupDirection) { // case TooltipDirection.down: return new Offset(calcLeftMostXtoTarget()!, _targetCenter!.dy); case TooltipDirection.up: var top = _top ?? _targetCenter!.dy - childSize.height; return new Offset(calcLeftMostXtoTarget()!, top); case TooltipDirection.left: var left = _left ?? _targetCenter!.dx - childSize.width; return new Offset(left, calcTopMostYtoTarget()!); case TooltipDirection.right: return new Offset( _targetCenter!.dx, calcTopMostYtoTarget()!, ); default: throw AssertionError(_popupDirection); } } @override BoxConstraints getConstraintsForChild(BoxConstraints constraints) { // print("ParentConstraints: $constraints"); var calcMinWidth = _minWidth ?? 0.0; var calcMaxWidth = _maxWidth ?? double.infinity; var calcMinHeight = _minHeight ?? 0.0; var calcMaxHeight = _maxHeight ?? double.infinity; void calcMinMaxWidth() { if (_left != null && _right != null) { calcMaxWidth = constraints.maxWidth - (_left! + _right!); } else if ((_left != null && _right == null) || (_left == null && _right != null)) { // make sure that the sum of left, right + maxwidth isn't bigger than the screen width. var sideDelta = (_left ?? 0.0) + (_right ?? 0.0) + _outSidePadding!; if (calcMaxWidth > constraints.maxWidth - sideDelta) { calcMaxWidth = constraints.maxWidth - sideDelta; } } else { if (calcMaxWidth > constraints.maxWidth - 2 * _outSidePadding!) { calcMaxWidth = constraints.maxWidth - 2 * _outSidePadding!; } } } void calcMinMaxHeight() { if (_top != null && _bottom != null) { calcMaxHeight = constraints.maxHeight - (_top! + _bottom!); } else if ((_top != null && _bottom == null) || (_top == null && _bottom != null)) { // make sure that the sum of top, bottom + maxHeight isn't bigger than the screen Height. var sideDelta = (_top ?? 0.0) + (_bottom ?? 0.0) + _outSidePadding!; if (calcMaxHeight > constraints.maxHeight - sideDelta) { calcMaxHeight = constraints.maxHeight - sideDelta; } } else { if (calcMaxHeight > constraints.maxHeight - 2 * _outSidePadding!) { calcMaxHeight = constraints.maxHeight - 2 * _outSidePadding!; } } } switch (_popupDirection) { // case TooltipDirection.down: calcMinMaxWidth(); if (_bottom != null) { calcMinHeight = calcMaxHeight = constraints.maxHeight - _bottom! - _targetCenter!.dy; } else { calcMaxHeight = min((_maxHeight ?? constraints.maxHeight), constraints.maxHeight - _targetCenter!.dy) - _outSidePadding!; } break; case TooltipDirection.up: calcMinMaxWidth(); if (_top != null) { calcMinHeight = calcMaxHeight = _targetCenter!.dy - _top!; } else { calcMaxHeight = min((_maxHeight ?? constraints.maxHeight), _targetCenter!.dy) - _outSidePadding!; } break; case TooltipDirection.right: calcMinMaxHeight(); if (_right != null) { calcMinWidth = calcMaxWidth = constraints.maxWidth - _right! - _targetCenter!.dx; } else { calcMaxWidth = min((_maxWidth ?? constraints.maxWidth), constraints.maxWidth - _targetCenter!.dx) - _outSidePadding!; } break; case TooltipDirection.left: calcMinMaxHeight(); if (_left != null) { calcMinWidth = calcMaxWidth = _targetCenter!.dx - _left!; } else { calcMaxWidth = min((_maxWidth ?? constraints.maxWidth), _targetCenter!.dx) - _outSidePadding!; } break; default: throw AssertionError(_popupDirection); } var childConstraints = new BoxConstraints( minWidth: calcMinWidth > calcMaxWidth ? calcMaxWidth : calcMinWidth, maxWidth: calcMaxWidth, minHeight: calcMinHeight > calcMaxHeight ? calcMaxHeight : calcMinHeight, maxHeight: calcMaxHeight); // print("Child constraints: $childConstraints"); return childConstraints; } @override bool shouldRelayout(SingleChildLayoutDelegate oldDelegate) { return false; } } class _PopupBallonLayoutDelegateBox extends SingleChildLayoutDelegate { final double? _minWidth; final double? _maxWidth; final double? _minHeight; final double? _maxHeight; final double? _top; final double? _bottom; final double? _left; final double? _right; final double? _outSidePadding; _PopupBallonLayoutDelegateBox({ double? minWidth, double? maxWidth, double? minHeight, double? maxHeight, double? outSidePadding, double? top, double? bottom, double? left, double? right, }) : _minWidth = minWidth, _maxWidth = maxWidth, _minHeight = minHeight, _maxHeight = maxHeight, _top = top, _bottom = bottom, _left = left, _right = right, _outSidePadding = outSidePadding; @override Offset getPositionForChild(Size size, Size childSize) { double? calcLeftMostXtoTarget() { double? leftMostXtoTarget; if (_left != null) { leftMostXtoTarget = _left; } else if (_right != null) { leftMostXtoTarget = max( size.topLeft(Offset.zero).dx + _outSidePadding!, size.topRight(Offset.zero).dx - _outSidePadding! - childSize.width - _right!); } return leftMostXtoTarget; } double? calcTopMostYtoTarget() { double? topmostYtoTarget; if (_top != null) { topmostYtoTarget = _top!; } else if (_bottom != null) { topmostYtoTarget = max(size.topLeft(Offset.zero).dy + _outSidePadding!, size.bottomRight(Offset.zero).dy - _outSidePadding! - childSize.height - _bottom!); } return topmostYtoTarget; } return new Offset(calcLeftMostXtoTarget()!, _top!); } @override BoxConstraints getConstraintsForChild(BoxConstraints constraints) { // print("ParentConstraints: $constraints"); var calcMinWidth = _minWidth ?? 0.0; var calcMaxWidth = _maxWidth ?? double.infinity; var calcMinHeight = _minHeight ?? 0.0; var calcMaxHeight = _maxHeight ?? double.infinity; void calcMinMaxWidth() { if (_left != null && _right != null) { calcMaxWidth = constraints.maxWidth - (_left! + _right!); } else if ((_left != null && _right == null) || (_left == null && _right != null)) { // make sure that the sum of left, right + maxwidth isn't bigger than the screen width. var sideDelta = (_left ?? 0.0) + (_right ?? 0.0) + _outSidePadding!; if (calcMaxWidth > constraints.maxWidth - sideDelta) { calcMaxWidth = constraints.maxWidth - sideDelta; } } else { if (calcMaxWidth > constraints.maxWidth - 2 * _outSidePadding!) { calcMaxWidth = constraints.maxWidth - 2 * _outSidePadding!; } } } void calcMinMaxHeight() { if (_top != null && _bottom != null) { calcMaxHeight = constraints.maxHeight - (_top! + _bottom!); } else if ((_top != null && _bottom == null) || (_top == null && _bottom != null)) { // make sure that the sum of top, bottom + maxHeight isn't bigger than the screen Height. var sideDelta = (_top ?? 0.0) + (_bottom ?? 0.0) + _outSidePadding!; if (calcMaxHeight > constraints.maxHeight - sideDelta) { calcMaxHeight = constraints.maxHeight - sideDelta; } } else { if (calcMaxHeight > constraints.maxHeight - 2 * _outSidePadding!) { calcMaxHeight = constraints.maxHeight - 2 * _outSidePadding!; } } } var childConstraints = new BoxConstraints( minWidth: calcMinWidth > calcMaxWidth ? calcMaxWidth : calcMinWidth, maxWidth: calcMaxWidth, minHeight: calcMinHeight > calcMaxHeight ? calcMaxHeight : calcMinHeight, maxHeight: calcMaxHeight); // print("Child constraints: $childConstraints"); return childConstraints; } @override bool shouldRelayout(SingleChildLayoutDelegate oldDelegate) { return false; } } //////////////////////////////////////////////////////////////////////////////////////////////////// class _BubbleShape extends ShapeBorder { final Offset? targetCenter; final double arrowBaseWidth; final double arrowTipDistance; final double borderRadius; final Color borderColor; final double borderWidth; final double? left, top, right, bottom; final TooltipDirection popupDirection; _BubbleShape(this.popupDirection, this.targetCenter, this.borderRadius, this.arrowBaseWidth, this.arrowTipDistance, this.borderColor, this.borderWidth, this.left, this.top, this.right, this.bottom); @override EdgeInsetsGeometry get dimensions => new EdgeInsets.all(10.0); @override Path getInnerPath(Rect rect, {TextDirection? textDirection}) { return new Path() ..fillType = PathFillType.evenOdd ..addPath(getOuterPath(rect), Offset.zero); } @override Path getOuterPath(Rect rect, {TextDirection? textDirection}) { // late double topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius; Path _getLeftTopPath(Rect rect) { return new Path() ..moveTo(rect.left, rect.bottom - bottomLeftRadius) ..lineTo(rect.left, rect.top + topLeftRadius) ..arcToPoint(Offset(rect.left + topLeftRadius, rect.top), radius: new Radius.circular(topLeftRadius)) ..lineTo(rect.right - topRightRadius, rect.top) ..arcToPoint(Offset(rect.right, rect.top + topRightRadius), radius: new Radius.circular(topRightRadius), clockwise: true); } Path _getBottomRightPath(Rect rect) { return new Path() ..moveTo(rect.left + bottomLeftRadius, rect.bottom) ..lineTo(rect.right - bottomRightRadius, rect.bottom) ..arcToPoint(Offset(rect.right, rect.bottom - bottomRightRadius), radius: new Radius.circular(bottomRightRadius), clockwise: false) ..lineTo(rect.right, rect.top + topRightRadius) ..arcToPoint(Offset(rect.right - topRightRadius, rect.top), radius: new Radius.circular(topRightRadius), clockwise: false); } topLeftRadius = (left == 0 || top == 0) ? 0.0 : borderRadius; topRightRadius = (right == 0 || top == 0) ? 0.0 : borderRadius; bottomLeftRadius = (left == 0 || bottom == 0) ? 0.0 : borderRadius; bottomRightRadius = (right == 0 || bottom == 0) ? 0.0 : borderRadius; switch (popupDirection) { // case TooltipDirection.down: return _getBottomRightPath(rect) ..lineTo(min(max(targetCenter!.dx + arrowBaseWidth / 2, rect.left + borderRadius + arrowBaseWidth), rect.right - topRightRadius), rect.top) ..lineTo(targetCenter!.dx, targetCenter!.dy + arrowTipDistance) // up to arrow tip \ ..lineTo(max(min(targetCenter!.dx - arrowBaseWidth / 2, rect.right - topLeftRadius - arrowBaseWidth), rect.left + topLeftRadius), rect.top) // down / ..lineTo(rect.left + topLeftRadius, rect.top) ..arcToPoint(Offset(rect.left, rect.top + topLeftRadius), radius: new Radius.circular(topLeftRadius), clockwise: false) ..lineTo(rect.left, rect.bottom - bottomLeftRadius) ..arcToPoint(Offset(rect.left + bottomLeftRadius, rect.bottom), radius: new Radius.circular(bottomLeftRadius), clockwise: false); case TooltipDirection.up: return _getLeftTopPath(rect) ..lineTo(rect.right, rect.bottom - bottomRightRadius) ..arcToPoint(Offset(rect.right - bottomRightRadius, rect.bottom), radius: new Radius.circular(bottomRightRadius), clockwise: true) ..lineTo( min(max(targetCenter!.dx + arrowBaseWidth / 2, rect.left + bottomLeftRadius + arrowBaseWidth), rect.right - bottomRightRadius), rect.bottom) // up to arrow tip \ ..lineTo(targetCenter!.dx, targetCenter!.dy - arrowTipDistance) // down / ..lineTo( max(min(targetCenter!.dx - arrowBaseWidth / 2, rect.right - bottomRightRadius - arrowBaseWidth), rect.left + bottomLeftRadius), rect.bottom) ..lineTo(rect.left + bottomLeftRadius, rect.bottom) ..arcToPoint(Offset(rect.left, rect.bottom - bottomLeftRadius), radius: new Radius.circular(bottomLeftRadius), clockwise: true) ..lineTo(rect.left, rect.top + topLeftRadius) ..arcToPoint(Offset(rect.left + topLeftRadius, rect.top), radius: new Radius.circular(topLeftRadius), clockwise: true); case TooltipDirection.left: return _getLeftTopPath(rect) ..lineTo(rect.right, max(min(targetCenter!.dy - arrowBaseWidth / 2, rect.bottom - bottomRightRadius - arrowBaseWidth), rect.top + topRightRadius)) ..lineTo(targetCenter!.dx - arrowTipDistance, targetCenter!.dy) // right to arrow tip \ // left / ..lineTo(rect.right, min(targetCenter!.dy + arrowBaseWidth / 2, rect.bottom - bottomRightRadius)) ..lineTo(rect.right, rect.bottom - borderRadius) ..arcToPoint(Offset(rect.right - bottomRightRadius, rect.bottom), radius: new Radius.circular(bottomRightRadius), clockwise: true) ..lineTo(rect.left + bottomLeftRadius, rect.bottom) ..arcToPoint(Offset(rect.left, rect.bottom - bottomLeftRadius), radius: new Radius.circular(bottomLeftRadius), clockwise: true); case TooltipDirection.right: return _getBottomRightPath(rect) ..lineTo(rect.left + topLeftRadius, rect.top) ..arcToPoint(Offset(rect.left, rect.top + topLeftRadius), radius: new Radius.circular(topLeftRadius), clockwise: false) ..lineTo(rect.left, max(min(targetCenter!.dy - arrowBaseWidth / 2, rect.bottom - bottomLeftRadius - arrowBaseWidth), rect.top + topLeftRadius)) //left to arrow tip / ..lineTo(targetCenter!.dx + arrowTipDistance, targetCenter!.dy) // right \ ..lineTo(rect.left, min(targetCenter!.dy + arrowBaseWidth / 2, rect.bottom - bottomLeftRadius)) ..lineTo(rect.left, rect.bottom - bottomLeftRadius) ..arcToPoint(Offset(rect.left + bottomLeftRadius, rect.bottom), radius: new Radius.circular(bottomLeftRadius), clockwise: false); default: throw AssertionError(popupDirection); } } @override void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) { var paint = new Paint() ..color = borderColor ..style = PaintingStyle.stroke ..strokeWidth = borderWidth; canvas.drawPath(getOuterPath(rect), paint); paint = new Paint() ..color = Colors.white ..style = PaintingStyle.stroke ..strokeWidth = borderWidth; if (right == 0.0) { if (top == 0.0 && bottom == 0.0) { canvas.drawPath( new Path() ..moveTo(rect.right, rect.top) ..lineTo(rect.right, rect.bottom), paint); } else { canvas.drawPath( new Path() ..moveTo(rect.right, rect.top + borderWidth / 2) ..lineTo(rect.right, rect.bottom - borderWidth / 2), paint); } } if (left == 0.0) { if (top == 0.0 && bottom == 0.0) { canvas.drawPath( new Path() ..moveTo(rect.left, rect.top) ..lineTo(rect.left, rect.bottom), paint); } else { canvas.drawPath( new Path() ..moveTo(rect.left, rect.top + borderWidth / 2) ..lineTo(rect.left, rect.bottom - borderWidth / 2), paint); } } if (top == 0.0) { if (left == 0.0 && right == 0.0) { canvas.drawPath( new Path() ..moveTo(rect.right, rect.top) ..lineTo(rect.left, rect.top), paint); } else { canvas.drawPath( new Path() ..moveTo(rect.right - borderWidth / 2, rect.top) ..lineTo(rect.left + borderWidth / 2, rect.top), paint); } } if (bottom == 0.0) { if (left == 0.0 && right == 0.0) { canvas.drawPath( new Path() ..moveTo(rect.right, rect.bottom) ..lineTo(rect.left, rect.bottom), paint); } else { canvas.drawPath( new Path() ..moveTo(rect.right - borderWidth / 2, rect.bottom) ..lineTo(rect.left + borderWidth / 2, rect.bottom), paint); } } } @override ShapeBorder scale(double t) { return new _BubbleShape( popupDirection, targetCenter, borderRadius, arrowBaseWidth, arrowTipDistance, borderColor, borderWidth, left, top, right, bottom); } } //////////////////////////////////////////////////////////////////////////////////////////////////// class _ShapeOverlay extends ShapeBorder { final Rect? clipRect; final Color outsideBackgroundColor; final ClipAreaShape clipAreaShape; final double clipAreaCornerRadius; _ShapeOverlay(this.clipRect, this.clipAreaShape, this.clipAreaCornerRadius, this.outsideBackgroundColor); @override EdgeInsetsGeometry get dimensions => new EdgeInsets.all(10.0); @override Path getInnerPath(Rect rect, {TextDirection? textDirection}) { return new Path()..addOval(clipRect!); } @override Path getOuterPath(Rect rect, {TextDirection? textDirection}) { var outer = new Path()..addRect(rect); if (clipRect == null) { return outer; } Path exclusion; if (clipAreaShape == ClipAreaShape.oval) { exclusion = new Path()..addOval(clipRect!); } else { exclusion = new Path() ..moveTo(clipRect!.left + clipAreaCornerRadius, clipRect!.top) ..lineTo(clipRect!.right - clipAreaCornerRadius, clipRect!.top) ..arcToPoint(Offset(clipRect!.right, clipRect!.top + clipAreaCornerRadius), radius: new Radius.circular(clipAreaCornerRadius)) ..lineTo(clipRect!.right, clipRect!.bottom - clipAreaCornerRadius) ..arcToPoint(Offset(clipRect!.right - clipAreaCornerRadius, clipRect!.bottom), radius: new Radius.circular(clipAreaCornerRadius)) ..lineTo(clipRect!.left + clipAreaCornerRadius, clipRect!.bottom) ..arcToPoint(Offset(clipRect!.left, clipRect!.bottom - clipAreaCornerRadius), radius: new Radius.circular(clipAreaCornerRadius)) ..lineTo(clipRect!.left, clipRect!.top + clipAreaCornerRadius) ..arcToPoint(Offset(clipRect!.left + clipAreaCornerRadius, clipRect!.top), radius: new Radius.circular(clipAreaCornerRadius)) ..close(); } return Path.combine(ui.PathOperation.difference, outer, exclusion); } @override void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) { canvas.drawPath(getOuterPath(rect), new Paint()..color = outsideBackgroundColor); } @override ShapeBorder scale(double t) { return new _ShapeOverlay(clipRect, clipAreaShape, clipAreaCornerRadius, outsideBackgroundColor); } } typedef FadeBuilder = Widget Function(BuildContext, double); //////////////////////////////////////////////////////////////////////////////////////////////////// class _AnimationWrapper extends StatefulWidget { final FadeBuilder? builder; _AnimationWrapper({this.builder}); @override _AnimationWrapperState createState() => new _AnimationWrapperState(); } //////////////////////////////////////////////////////////////////////////////////////////////////// class _AnimationWrapperState extends State<_AnimationWrapper> { double opacity = 0.0; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { opacity = 1.0; }); } }); } @override Widget build(BuildContext context) { return widget.builder!(context, opacity); } }