diff --git a/example/lib/main.dart b/example/lib/main.dart index 950c78c6..a8ae4ab2 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -232,6 +232,20 @@ class _MailPageState extends State { ), child: Image.asset('assets/simform.png'), ), + toolTipAction: DefaultToolTipAction( + color: Colors.white, + showCaseWidgetState: ShowCaseWidget.of(context), + back: const Icon( + Icons.arrow_back, + color: Colors.white, + ), + forward: const Icon( + Icons.arrow_forward, + color: Colors.white, + ), + onBackPress: () => debugPrint('Back Pressed!'), + onForwardPress: () => debugPrint('Forward Pressed!'), + ), ), const SizedBox( width: 12, @@ -311,27 +325,28 @@ class _MailPageState extends State { child: Container( padding: const EdgeInsets.symmetric(vertical: 8), child: Showcase( - key: key, - description: 'Tap to check mail', - tooltipPosition: TooltipPosition.top, - disposeOnTap: true, - onTargetClick: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const Detail(), - ), - ).then((_) { - setState(() { - ShowCaseWidget.of(context).startShowCase([_four, _five]); - }); + key: key, + description: 'Tap to check mail', + tooltipPosition: TooltipPosition.top, + disposeOnTap: true, + onTargetClick: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const Detail(), + ), + ).then((_) { + setState(() { + ShowCaseWidget.of(context).startShowCase([_four, _five]); }); - }, - child: MailTile( - mail: mail, - showCaseKey: _four, - showCaseDetail: showCaseDetail, - )), + }); + }, + child: MailTile( + mail: mail, + showCaseKey: _four, + showCaseDetail: showCaseDetail, + ), + ), ), ); } @@ -446,6 +461,12 @@ class MailTile extends StatelessWidget { ) ], ), + toolTipAction: DefaultToolTipAction( + color: Colors.white, + showCaseWidgetState: ShowCaseWidget.of(context), + onBackPress: () => debugPrint('Back Pressed!'), + onForwardPress: () => debugPrint('Forward Pressed!'), + ), child: const SAvatarExampleChild(), ) else diff --git a/lib/showcaseview.dart b/lib/showcaseview.dart index 1c6a0cc0..223507e1 100644 --- a/lib/showcaseview.dart +++ b/lib/showcaseview.dart @@ -22,6 +22,7 @@ library showcaseview; +export 'src/default_tooltip_action.dart'; export 'src/enum.dart'; export 'src/showcase.dart'; export 'src/showcase_widget.dart'; diff --git a/lib/src/default_tooltip_action.dart b/lib/src/default_tooltip_action.dart new file mode 100644 index 00000000..925fdb5d --- /dev/null +++ b/lib/src/default_tooltip_action.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +import 'showcase_widget.dart'; +import 'tooltip_action_button.dart'; + +/// Default Tooltip action Widget Nav +/// Shows tooltip navigation and index / count elements if the conditions are +/// indicated. +class DefaultToolTipAction extends StatelessWidget { + const DefaultToolTipAction({ + super.key, + this.color = Colors.black, + required this.showCaseWidgetState, + this.padding = const EdgeInsets.only(top: 5), + this.textStyle, + this.iconSize, + this.back, + this.forward, + this.buttonColor, + this.onBackPress, + this.onForwardPress, + }); + + final Color color; + final ShowCaseWidgetState showCaseWidgetState; + final EdgeInsets padding; + final TextStyle? textStyle; + final double? iconSize; + final Widget? back; + final Widget? forward; + final Color? buttonColor; + final VoidCallback? onBackPress; + final VoidCallback? onForwardPress; + + @override + Widget build(BuildContext context) { + var ids = showCaseWidgetState.ids; + var activeWidgetId = showCaseWidgetState.activeWidgetId; + bool isFirstTip = activeWidgetId == 0; + bool isLastTip = activeWidgetId == (ids!.length - 1); + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (ids.isNotEmpty && activeWidgetId != null) ...[ + ToolTipActionButton( + action: isFirstTip + ? null + : () { + showCaseWidgetState.previous(); + onBackPress?.call(); + }, + padding: padding, + widget: back ?? + Icon( + Icons.keyboard_arrow_left, + color: buttonColor ?? color, + ), + opacity: isFirstTip ? 0.3 : 1, + ), + const SizedBox( + width: 4.0, + ), + Padding( + padding: padding, + child: Text( + "${activeWidgetId + 1} / ${ids.length}", + style: textStyle ?? + Theme.of(context).textTheme.bodyMedium?.copyWith( + color: color, + ), + ), + ), + const SizedBox( + width: 4.0, + ), + ToolTipActionButton( + action: isLastTip + ? null + : () { + showCaseWidgetState.next(); + onForwardPress?.call(); + }, + padding: padding, + widget: forward ?? + Icon( + Icons.keyboard_arrow_right, + color: buttonColor ?? color, + ), + opacity: isLastTip ? 0.3 : 1, + ) + ], + ], + ); + } +} diff --git a/lib/src/showcase.dart b/lib/src/showcase.dart index 1779f98b..7f41fe02 100644 --- a/lib/src/showcase.dart +++ b/lib/src/showcase.dart @@ -231,6 +231,11 @@ class Showcase extends StatefulWidget { /// Provides padding around the description. Default padding is zero. final EdgeInsets? descriptionPadding; + /// Provides tooTip action widgets at bottom in tool tip. + /// + /// one can use [DefaultToolTipActionWidget] class to use default action + final Widget? toolTipAction; + /// Provides text direction of tooltip title. final TextDirection? titleTextDirection; @@ -293,6 +298,7 @@ class Showcase extends StatefulWidget { this.tooltipPosition, this.titlePadding, this.descriptionPadding, + this.toolTipAction, this.titleTextDirection, this.descriptionTextDirection, this.onBarrierClick, @@ -339,6 +345,7 @@ class Showcase extends StatefulWidget { this.onBarrierClick, this.disableBarrierInteraction = false, this.toolTipSlideEndDistance = 7, + this.toolTipAction, }) : showArrow = false, onToolTipClick = null, scaleAnimationDuration = const Duration(milliseconds: 300), @@ -629,6 +636,7 @@ class _ShowcaseState extends State { tooltipPosition: widget.tooltipPosition, titlePadding: widget.titlePadding, descriptionPadding: widget.descriptionPadding, + toolTipAction: widget.toolTipAction, titleTextDirection: widget.titleTextDirection, descriptionTextDirection: widget.descriptionTextDirection, toolTipSlideEndDistance: widget.toolTipSlideEndDistance, diff --git a/lib/src/tooltip_action_button.dart b/lib/src/tooltip_action_button.dart new file mode 100644 index 00000000..4991c733 --- /dev/null +++ b/lib/src/tooltip_action_button.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class ToolTipActionButton extends StatelessWidget { + const ToolTipActionButton({ + Key? key, + required this.action, + required this.padding, + required this.widget, + required this.opacity, + }) : super(key: key); + + final VoidCallback? action; + final EdgeInsetsGeometry padding; + final Widget? widget; + final double opacity; + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: action, + child: IgnorePointer( + child: Opacity( + opacity: opacity, + child: Padding( + padding: padding, + child: widget, + ), + ), + ), + ); + } +} diff --git a/lib/src/tooltip_widget.dart b/lib/src/tooltip_widget.dart index dba238a2..65bccd0f 100644 --- a/lib/src/tooltip_widget.dart +++ b/lib/src/tooltip_widget.dart @@ -60,6 +60,7 @@ class ToolTipWidget extends StatefulWidget { final TooltipPosition? tooltipPosition; final EdgeInsets? titlePadding; final EdgeInsets? descriptionPadding; + final Widget? toolTipAction; final TextDirection? titleTextDirection; final TextDirection? descriptionTextDirection; final double toolTipSlideEndDistance; @@ -94,6 +95,7 @@ class ToolTipWidget extends StatefulWidget { this.tooltipPosition, this.titlePadding, this.descriptionPadding, + this.toolTipAction, this.titleTextDirection, this.descriptionTextDirection, this.toolTipSlideEndDistance = 7, @@ -115,15 +117,20 @@ class _ToolTipWidgetState extends State late final Animation _scaleAnimation; double tooltipWidth = 0; + double toolTipHeight = 0; double tooltipScreenEdgePadding = 20; double tooltipTextPadding = 15; + double actionWidgetHeight = 0.0; TooltipPosition findPositionForContent(Offset position) { - var height = 120.0; + var height = toolTipHeight; height = widget.contentHeight ?? height; - final bottomPosition = - position.dy + ((widget.position?.getHeight() ?? 0) / 2); - final topPosition = position.dy - ((widget.position?.getHeight() ?? 0) / 2); + final bottomPosition = position.dy + + ((widget.position?.getHeight() ?? 0) / 2) + + actionWidgetHeight; + final topPosition = position.dy - + ((widget.position?.getHeight() ?? 0) / 2) - + actionWidgetHeight; final hasSpaceInTop = topPosition >= height; // TODO: need to update for flutter version > 3.8.X // ignore: deprecated_member_use @@ -240,14 +247,7 @@ class _ToolTipWidgetState extends State } } - double _getAlignmentY() { - var dy = isArrowUp - ? -1.0 - : (MediaQuery.of(context).size.height / 2) < widget.position!.getTop() - ? -1.0 - : 1.0; - return dy; - } + double _getAlignmentY() => -1; final GlobalKey _customContainerKey = GlobalKey(); final ValueNotifier _customContainerWidth = ValueNotifier(1); @@ -388,102 +388,116 @@ class _ToolTipWidgetState extends State ).animate(_movingAnimation), child: Material( type: MaterialType.transparency, - child: Container( - padding: widget.showArrow - ? EdgeInsets.only( - top: paddingTop - (isArrowUp ? arrowHeight : 0), - bottom: paddingBottom - (isArrowUp ? 0 : arrowHeight), - ) - : null, - child: Stack( - alignment: isArrowUp - ? Alignment.topLeft - : _getLeft() == null - ? Alignment.bottomRight - : Alignment.bottomLeft, - children: [ - if (widget.showArrow) - Positioned( - left: _getArrowLeft(arrowWidth), - right: _getArrowRight(arrowWidth), - child: CustomPaint( - painter: _Arrow( - strokeColor: widget.tooltipBackgroundColor!, - strokeWidth: 10, - paintingStyle: PaintingStyle.fill, - isUpArrow: isArrowUp, - ), - child: const SizedBox( - height: arrowHeight, - width: arrowWidth, + child: MeasureSize( + onSizeChange: onTooltipSizeChanged, + child: Container( + padding: widget.showArrow + ? EdgeInsets.only( + top: paddingTop - (isArrowUp ? arrowHeight : 0), + bottom: + paddingBottom - (isArrowUp ? 0 : arrowHeight), + ) + : null, + child: Stack( + alignment: isArrowUp + ? Alignment.topLeft + : _getLeft() == null + ? Alignment.bottomRight + : Alignment.bottomLeft, + children: [ + if (widget.showArrow) + Positioned( + left: _getArrowLeft(arrowWidth), + right: _getArrowRight(arrowWidth), + child: CustomPaint( + painter: _Arrow( + strokeColor: widget.tooltipBackgroundColor!, + strokeWidth: 10, + paintingStyle: PaintingStyle.fill, + isUpArrow: isArrowUp, + ), + child: const SizedBox( + height: arrowHeight, + width: arrowWidth, + ), ), ), - ), - Padding( - padding: EdgeInsets.only( - top: isArrowUp ? arrowHeight - 1 : 0, - bottom: isArrowUp ? 0 : arrowHeight - 1, - ), - child: ClipRRect( - borderRadius: widget.tooltipBorderRadius ?? - BorderRadius.circular(8.0), - child: GestureDetector( - onTap: widget.onTooltipTap, - child: Container( - width: tooltipWidth, - padding: widget.tooltipPadding, - color: widget.tooltipBackgroundColor, - child: Column( - crossAxisAlignment: widget.title != null - ? CrossAxisAlignment.start - : CrossAxisAlignment.center, - children: [ - if (widget.title != null) - Padding( - padding: widget.titlePadding ?? - EdgeInsets.zero, - child: Text( - widget.title!, - textAlign: widget.titleAlignment, - textDirection: - widget.titleTextDirection, - style: widget.titleTextStyle ?? - Theme.of(context) - .textTheme - .titleLarge! - .merge( - TextStyle( - color: widget.textColor, + Padding( + padding: EdgeInsets.only( + top: isArrowUp ? arrowHeight - 1 : 0, + bottom: isArrowUp ? 0 : arrowHeight - 1, + ), + child: ClipRRect( + borderRadius: widget.tooltipBorderRadius ?? + BorderRadius.circular(8.0), + child: GestureDetector( + onTap: widget.onTooltipTap, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: + MediaQuery.of(context).size.width - 30, + ), + child: Container( + width: tooltipWidth, + padding: widget.tooltipPadding, + color: widget.tooltipBackgroundColor, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: widget.title != null + ? CrossAxisAlignment.start + : CrossAxisAlignment.center, + children: [ + if (widget.title != null) + Padding( + padding: widget.titlePadding ?? + EdgeInsets.zero, + child: Text( + widget.title!, + textAlign: widget.titleAlignment, + textDirection: + widget.titleTextDirection, + style: widget.titleTextStyle ?? + Theme.of(context) + .textTheme + .titleLarge! + .merge( + TextStyle( + color: widget.textColor, + ), + ), + ), + ), + Padding( + padding: widget.descriptionPadding ?? + EdgeInsets.zero, + child: Text( + widget.description!, + textAlign: + widget.descriptionAlignment, + textDirection: + widget.descriptionTextDirection, + style: widget.descTextStyle ?? + Theme.of(context) + .textTheme + .titleSmall! + .merge( + TextStyle( + color: widget.textColor, + ), ), - ), + ), ), - ), - Padding( - padding: widget.descriptionPadding ?? - EdgeInsets.zero, - child: Text( - widget.description!, - textAlign: widget.descriptionAlignment, - textDirection: - widget.descriptionTextDirection, - style: widget.descTextStyle ?? - Theme.of(context) - .textTheme - .titleSmall! - .merge( - TextStyle( - color: widget.textColor, - ), - ), - ), + widget.toolTipAction ?? + const SizedBox.shrink() + ], ), - ], + ), ), ), ), ), - ), - ], + ], + ), ), ), ), @@ -497,30 +511,47 @@ class _ToolTipWidgetState extends State Positioned( left: _getSpace(), top: contentY - (10 * contentOffsetMultiplier), - child: FractionalTranslation( - translation: Offset(0.0, contentFractionalOffset as double), - child: ToolTipSlideTransition( - position: Tween( - begin: Offset.zero, - end: Offset( - 0, - widget.toolTipSlideEndDistance * contentOffsetMultiplier, + child: ScaleTransition( + scale: _scaleAnimation, + alignment: widget.scaleAnimationAlignment ?? + Alignment( + _getAlignmentX(), + _getAlignmentY(), ), - ).animate(_movingAnimation), - child: Material( - color: Colors.transparent, - child: GestureDetector( - onTap: widget.onTooltipTap, - child: Container( - padding: EdgeInsets.only( - top: paddingTop, - bottom: paddingBottom, - ), - color: Colors.transparent, - child: Center( - child: MeasureSize( - onSizeChange: onSizeChange, - child: widget.container, + child: FractionalTranslation( + translation: Offset(0.0, contentFractionalOffset as double), + child: ToolTipSlideTransition( + position: Tween( + begin: Offset.zero, + end: Offset( + 0, + widget.toolTipSlideEndDistance * contentOffsetMultiplier, + ), + ).animate(_movingAnimation), + child: Material( + color: Colors.transparent, + child: GestureDetector( + onTap: widget.onTooltipTap, + child: Container( + padding: EdgeInsets.only( + top: paddingTop, + bottom: paddingBottom, + ), + color: Colors.transparent, + child: Center( + child: MeasureSize( + onSizeChange: onSizeChange, + child: Column( + children: [ + widget.container!, + SizedBox( + width: widget.contentWidth, + child: widget.toolTipAction ?? + const SizedBox.shrink(), + ) + ], + ), + ), ), ), ), @@ -554,14 +585,26 @@ class _ToolTipWidgetState extends State double? _getArrowLeft(double arrowWidth) { final left = _getLeft(); if (left == null) return null; - return (widget.position!.getCenter() - (arrowWidth / 2) - left); + return (widget.position!.getCenter() - (arrowWidth * 0.5) - left); } double? _getArrowRight(double arrowWidth) { if (_getLeft() != null) return null; return (widget.screenSize.width - widget.position!.getCenter()) - (_getRight() ?? 0) - - (arrowWidth / 2); + (arrowWidth * 0.5); + } + + void onTooltipSizeChanged(Size? size) { + if (size == null) return; + setState(() { + if (size.width > widget.screenSize.width - tooltipScreenEdgePadding) { + tooltipWidth = widget.screenSize.width - tooltipScreenEdgePadding; + } else { + tooltipWidth = size.width; + } + toolTipHeight = size.height; + }); } } @@ -584,21 +627,24 @@ class _Arrow extends CustomPainter { @override void paint(Canvas canvas, Size size) { - canvas.drawPath(getTrianglePath(size.width, size.height), _paint); + canvas.drawPath( + getTrianglePath(size.width, size.height), + _paint, + ); } Path getTrianglePath(double x, double y) { if (isUpArrow) { return Path() ..moveTo(0, y) - ..lineTo(x / 2, 0) + ..lineTo(x * 0.5, 0) ..lineTo(x, y) ..lineTo(0, y); } return Path() ..moveTo(0, 0) ..lineTo(x, 0) - ..lineTo(x / 2, y) + ..lineTo(x * 0.5, y) ..lineTo(0, 0); }