diff --git a/lib/src/showcase.dart b/lib/src/showcase.dart index ac35344e..9dd69514 100644 --- a/lib/src/showcase.dart +++ b/lib/src/showcase.dart @@ -63,6 +63,9 @@ class Showcase extends StatefulWidget { final VoidCallback? onTargetDoubleTap; final VoidCallback? onTargetLongPress; final BorderRadius? tipBorderRadius; + final bool showForwardBackNav; + final bool showTipCountIndex; + final bool showEndIcon; /// if disableDefaultTargetGestures parameter is true /// onTargetClick, onTargetDoubleTap, onTargetLongPress and @@ -107,6 +110,9 @@ class Showcase extends StatefulWidget { this.onTargetDoubleTap, this.tipBorderRadius, this.disableDefaultTargetGestures = false, + this.showForwardBackNav = false, + this.showTipCountIndex = false, + this.showEndIcon = false, }) : height = null, width = null, container = null, @@ -123,6 +129,9 @@ class Showcase extends StatefulWidget { : (onTargetClick == null ? false : true), "onTargetClick is required if you're using disposeOnTap"); + /// Showcase.withWidget allows a widget to be passed to the tooltip instead of just a description String + /// It is expected that a user would build their own implementation of forward / back [showForwardBackNav], + /// tip count [showTipCountIndex], and end icon [showEndIcon] with this constructor. const Showcase.withWidget({ required this.key, required this.child, @@ -154,6 +163,9 @@ class Showcase extends StatefulWidget { this.disableDefaultTargetGestures = false, }) : showArrow = false, onToolTipClick = null, + showForwardBackNav = false, + showTipCountIndex = false, + showEndIcon = false, assert(overlayOpacity >= 0.0 && overlayOpacity <= 1.0, "overlay opacity must be between 0 and 1."); @@ -335,6 +347,7 @@ class _ShowcaseState extends State { ), if (!_isScrollRunning) ToolTipWidget( + globalKey: widget.key, position: position, offset: offset, screenSize: screenSize, @@ -354,6 +367,9 @@ class _ShowcaseState extends State { showCaseWidgetState.disableAnimation, animationDuration: widget.animationDuration, borderRadius: widget.tipBorderRadius, + showForwardBackNav: widget.showForwardBackNav, + showTipCountIndex: widget.showTipCountIndex, + showEndIcon: widget.showEndIcon, ), ], ) diff --git a/lib/src/tooltip_widget.dart b/lib/src/tooltip_widget.dart index 964cd33d..d5e38435 100644 --- a/lib/src/tooltip_widget.dart +++ b/lib/src/tooltip_widget.dart @@ -24,12 +24,19 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import '../showcaseview.dart'; import 'get_position.dart'; import 'measure_size.dart'; +import 'tooltip_widget_nav.dart'; const _kDefaultPaddingFromParent = 14.0; +const _kEndIconPaddingAll = 2.0; +const _kEndIconSize = 16.0; +const _kEndIconTotalWidth = _kEndIconSize + _kEndIconPaddingAll; +const _kMinRightPaddingIfEndIconEnabled = 2.0; class ToolTipWidget extends StatefulWidget { + final GlobalKey globalKey; final GetPosition? position; final Offset? offset; final Size? screenSize; @@ -48,9 +55,13 @@ class ToolTipWidget extends StatefulWidget { final Duration animationDuration; final bool disableAnimation; final BorderRadius? borderRadius; + final bool showForwardBackNav; + final bool showTipCountIndex; + final bool showEndIcon; const ToolTipWidget({ Key? key, + required this.globalKey, required this.position, required this.offset, required this.screenSize, @@ -69,6 +80,9 @@ class ToolTipWidget extends StatefulWidget { this.contentPadding = const EdgeInsets.symmetric(vertical: 8), required this.disableAnimation, required this.borderRadius, + required this.showForwardBackNav, + required this.showTipCountIndex, + required this.showEndIcon, }) : super(key: key); @override @@ -78,7 +92,6 @@ class ToolTipWidget extends StatefulWidget { class _ToolTipWidgetState extends State with SingleTickerProviderStateMixin { Offset? position; - bool isArrowUp = false; late final AnimationController _parentController; @@ -130,6 +143,9 @@ class _ToolTipWidgetState extends State widget.contentPadding!.right + widget.contentPadding!.left); var maxTextWidth = max(titleLength, descriptionLength); + if (widget.showEndIcon) { + maxTextWidth += _kEndIconTotalWidth; + } if (maxTextWidth > widget.screenSize!.width - tooltipScreenEdgePadding) { tooltipWidth = widget.screenSize!.width - tooltipScreenEdgePadding; } else { @@ -157,8 +173,13 @@ class _ToolTipWidgetState extends State if (widget.position != null) { var rightPosition = widget.position!.getCenter() + (tooltipWidth * 0.5); + // Accommodate end icon offset width if enabled + double minRightPadding = (widget.showEndIcon) + ? _kMinRightPaddingIfEndIconEnabled + : _kDefaultPaddingFromParent; + return (rightPosition + tooltipWidth) > MediaQuery.of(context).size.width - ? _kDefaultPaddingFromParent + ? minRightPadding : null; } return null; @@ -238,6 +259,7 @@ class _ToolTipWidgetState extends State const arrowWidth = 18.0; const arrowHeight = 9.0; + const defaultBorderRadius = 8.0; if (widget.container == null) { return Positioned( @@ -293,60 +315,113 @@ class _ToolTipWidgetState extends State ), ), ), - Padding( + Container( padding: EdgeInsets.only( top: isArrowUp ? arrowHeight - 1 : 0, bottom: isArrowUp ? 0 : arrowHeight - 1, ), - child: ClipRRect( - borderRadius: - widget.borderRadius ?? BorderRadius.circular(8.0), - child: GestureDetector( - onTap: widget.onTooltipTap, - child: Container( - width: tooltipWidth, - padding: widget.contentPadding, - color: widget.tooltipColor, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( - crossAxisAlignment: widget.title != null - ? CrossAxisAlignment.start - : CrossAxisAlignment.center, - children: [ - widget.title != null - ? Text( - widget.title!, - style: widget.titleTextStyle ?? + child: Stack( + children: [ + Padding( + // Conditional container padding to accommodate an offset endIcon + padding: EdgeInsets.only( + right: (widget.showEndIcon) + ? _kEndIconTotalWidth / 2 + : 0), + child: ClipRRect( + borderRadius: widget.borderRadius ?? + BorderRadius.circular(defaultBorderRadius), + child: GestureDetector( + onTap: widget.onTooltipTap, + child: Container( + width: tooltipWidth, + padding: widget.contentPadding, + color: widget.tooltipColor, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: widget.title != null + ? CrossAxisAlignment.start + : CrossAxisAlignment.center, + children: [ + widget.title != null + ? Text( + widget.title!, + style: + widget.titleTextStyle ?? + Theme.of(context) + .textTheme + .headline6! + .merge( + TextStyle( + color: widget + .textColor, + ), + ), + ) + : const SizedBox(), + Text( + widget.description!, + style: widget.descTextStyle ?? Theme.of(context) .textTheme - .headline6! + .subtitle2! .merge( TextStyle( color: widget.textColor, ), ), + ), + TooltipWidgetNav( + globalKey: widget.globalKey, + showForwardBackNav: + widget.showForwardBackNav, + showTipCountIndex: + widget.showTipCountIndex, + textColor: widget.textColor, ) - : const SizedBox(), - Text( - widget.description!, - style: widget.descTextStyle ?? - Theme.of(context) - .textTheme - .subtitle2! - .merge( - TextStyle( - color: widget.textColor, - ), - ), - ), - ], - ) - ], + ], + ) + ], + ), + ), + ), ), ), - ), + if (widget.showEndIcon) + Positioned( + right: 0, + top: (isArrowUp) ? arrowHeight - 1 : 0, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + ShowCaseWidget.of( + widget.globalKey.currentContext!) + .dismiss(); + }, + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.zero, + topRight: Radius.circular(50), + bottomRight: Radius.circular(50), + bottomLeft: Radius.circular(50), + ), + child: Container( + padding: const EdgeInsets.all( + _kEndIconPaddingAll), + color: widget.tooltipColor, + child: Icon( + Icons.close, + size: _kEndIconSize, + color: widget.textColor, + ), + ), + ), + ), + ), + ], ), ), ], diff --git a/lib/src/tooltip_widget_nav.dart b/lib/src/tooltip_widget_nav.dart new file mode 100644 index 00000000..01adfa4d --- /dev/null +++ b/lib/src/tooltip_widget_nav.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; + +import '../showcaseview.dart'; + +/// Tooltip Widget Nav +/// Shows tooltip navigation and index / count elements if the conditions are indicated. +class TooltipWidgetNav extends StatelessWidget { + final GlobalKey globalKey; + final bool showForwardBackNav; + final bool showTipCountIndex; + final Color? textColor; + const TooltipWidgetNav({ + Key? key, + required this.globalKey, + required this.showForwardBackNav, + required this.showTipCountIndex, + required this.textColor, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var ids = ShowCaseWidget.of(globalKey.currentContext!).ids; + var activeWidgetId = + ShowCaseWidget.of(globalKey.currentContext!).activeWidgetId; + bool isFirstTip = activeWidgetId == 0; + + if (showForwardBackNav || showTipCountIndex) { + return Column( + children: [ + // const SizedBox(height: 4.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (showForwardBackNav) + GestureDetector( + behavior: HitTestBehavior.translucent, + + // Disable if activeWidgetId (index) == 0 + onTap: (isFirstTip) + ? null + : () { + ShowCaseWidget.of(globalKey.currentContext!) + .previous(); + }, + child: Padding( + padding: const EdgeInsets.only(right: 8.0, top: 4.0), + child: Icon( + Icons.keyboard_arrow_left, + color: (isFirstTip) + ? textColor?.withOpacity(0.3) ?? Colors.black26 + : textColor, + ), + ), + ), + if (showTipCountIndex && + ids != null && + activeWidgetId != null) ...[ + const SizedBox(width: 4.0), + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + "${activeWidgetId + 1} / ${ids.length}", + style: TextStyle( + color: textColor, + ), + ), + ), + const SizedBox(width: 4.0), + ], + if (showForwardBackNav) + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + ShowCaseWidget.of(globalKey.currentContext!).next(); + }, + child: Padding( + padding: const EdgeInsets.only(left: 8.0, top: 4.0), + child: Icon( + Icons.keyboard_arrow_right, + color: textColor, + ), + ), + ), + ], + ), + ], + ); + } else { + return const SizedBox(); + } + } +}