workouttest_app/lib/library/super_tooltip.dart
2022-10-19 21:55:17 +02:00

1144 lines
38 KiB
Dart

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 = <OverlayEntry>[];
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 = <OverlayEntry>[];
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);
}
}