From 29dfc4da176e04ff758bd7ee672824f4e8974d27 Mon Sep 17 00:00:00 2001 From: Sahil-Simform Date: Thu, 24 Oct 2024 14:15:23 +0530 Subject: [PATCH] =?UTF-8?q?feat:=E2=9C=A8Added=20feature=20to=20add=20acti?= =?UTF-8?q?on=20buttons=20for=20the=20tooltip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 +- README.md | 120 +-- example/lib/main.dart | 78 +- lib/src/enum.dart | 27 +- lib/src/models/tooltip_action_button.dart | 27 + lib/src/models/tooltip_action_config.dart | 10 +- lib/src/showcase.dart | 50 +- lib/src/showcase_widget.dart | 4 + lib/src/tooltip_action_button_widget.dart | 16 +- lib/src/tooltip_widget.dart | 914 ++++++++++++---------- lib/src/widget/action_widget.dart | 45 ++ 11 files changed, 774 insertions(+), 522 deletions(-) create mode 100644 lib/src/widget/action_widget.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index dbf526d5..65e64bc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [4.0.0] + +- Feature ✨: Added Action widget for tooltip + ## [3.0.1] - Feature [#475](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/475) - Add @@ -9,7 +13,6 @@ - Fixed [#449](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/449) - Null check operator used on a null value - [BREAKING] Improvement [#400](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/400) - remove Builder widget - Fixed [#435](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/435) - Extra padding when add targetShapeBorder -- Feature [#466](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/pull/466) - Provide tooltip action buttons ## [2.1.1] - Fixed [#425](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/425) - Unhandled breaking change in v2.1.0 diff --git a/README.md b/README.md index c9bb11b0..da1f1d33 100644 --- a/README.md +++ b/README.md @@ -136,54 +136,54 @@ WidgetsBinding.instance.addPostFrameCallback((_) => ## Properties of `Showcase` and `Showcase.withWidget`: -| Name | Type | Default Behaviour | Description | `Showcase` | `ShowCaseWidget` | -|------------------------------|----------------------------|--------------------------------------------------|----------------------------------------------------------------------------------------------------|------------|-------------------| -| key | GlobalKey | | Unique Global key for each showcase. | ✅ | ✅ | -| child | Widget | | The Target widget that you want to be showcased | ✅ | ✅ | -| title | String? | | Title of default tooltip | ✅ | | -| description | String? | | Description of default tooltip | ✅ | | -| container | Widget? | | Allows to create custom tooltip widget. | | ✅ | -| height | double? | | Height of custom tooltip widget | | ✅ | -| width | double? | | Width of custom tooltip widget | | ✅ | -| titleTextStyle | TextStyle? | | Text Style of title | ✅ | | -| descTextStyle | TextStyle? | | Text Style of description | ✅ | | -| titleAlignment | TextAlign | TextAlign.start | Alignment of title | ✅ | | -| descriptionAlignment | TextAlign | TextAlign.start | Alignment of description | ✅ | | -| targetShapeBorder | ShapeBorder | | If `targetBorderRadius` param is not provided then it applies shape border to target widget | ✅ | ✅ | -| targetBorderRadius | BorderRadius? | | Border radius of target widget | ✅ | ✅ | -| tooltipBorderRadius | BorderRadius? | BorderRadius.circular(8.0) | Border radius of tooltip | ✅ | | -| blurValue | double? | `ShowCaseWidget.blurValue` | Gaussian blur effect on overlay | ✅ | ✅ | -| tooltipPadding | EdgeInsets | EdgeInsets.symmetric(vertical: 8, horizontal: 8) | Padding to tooltip content | ✅ | | -| targetPadding | EdgeInsets | EdgeInsets.zero | Padding to target widget | ✅ | ✅ | -| overlayOpacity | double | 0.75 | Opacity of overlay layer | ✅ | ✅ | -| overlayColor | Color | Colors.black45 | Color of overlay layer | ✅ | ✅ | -| tooltipBackgroundColor | Color | Colors.white | Background Color of default tooltip | ✅ | | -| textColor | Color | Colors.black | Color of tooltip text | ✅ | | -| scrollLoadingWidget | Widget | | Loading widget on overlay until active showcase is visible to viewport when `autoScroll` is enable | ✅ | ✅ | -| movingAnimationDuration | Duration | Duration(milliseconds: 2000) | Duration of time this moving animation should last. | ✅ | ✅ | -| showArrow | bool | true | Shows tooltip with arrow | ✅ | | -| disableDefaultTargetGestures | bool | false | disable default gestures of target widget | ✅ | ✅ | -| disposeOnTap | bool? | false | Dismiss all showcases on target/tooltip tap | ✅ | ✅ | -| disableMovingAnimation | bool? | `ShowCaseWidget.disableMovingAnimation` | Disable bouncing/moving transition | ✅ | ✅ | -| disableScaleAnimation | bool? | `ShowCaseWidget.disableScaleAnimation` | Disable initial scale transition when showcase is being started and completed | ✅ | | -| scaleAnimationDuration | Duration | Duration(milliseconds: 300) | Duration of time scale animation should last. | ✅ | | -| scaleAnimationCurve | Curve | Curves.easeIn | Curve to use in scale animation. | ✅ | | -| scaleAnimationAlignment | Alignment? | | Origin of the coordinate in which the scale takes place, relative to the size of the box. | ✅ | | -| onToolTipClick | VoidCallback? | | Triggers when tooltip is being clicked. | ✅ | | -| onTargetClick | VoidCallback? | | Triggers when target widget is being clicked | ✅ | ✅ | -| onTargetDoubleTap | VoidCallback? | | Triggers when target widget is being double clicked | ✅ | ✅ | -| onTargetLongPress | VoidCallback? | | Triggers when target widget is being long pressed | ✅ | ✅ | -| onBarrierClick | VoidCallback? | | Triggers when barrier is clicked | ✅ | ✅ | -| tooltipPosition | TooltipPosition? | | Defines vertical position of tooltip respective to Target widget | ✅ | ✅ | -| titlePadding | EdgeInsets? | EdgeInsets.zero | Padding to title | ✅ | | -| descriptionPadding | EdgeInsets? | EdgeInsets.zero | Padding to description | ✅ | | -| titleTextDirection | TextDirection? | | Give textDirection to title | ✅ | | -| descriptionTextDirection | TextDirection? | | Give textDirection to description | ✅ | | -| descriptionTextDirection | TextDirection? | | Give textDirection to description | ✅ | | -| disableBarrierInteraction | bool | false | Disables barrier interaction for a particular showCase | ✅ | ✅ | -| toolTipSlideEndDistance | double | 7 | Defines motion range for tooltip slide animation | ✅ | ✅ | -| tooltipActions | List? | [] | Provide a list of tooltip actions | ✅ | ✅ | -| tooltipActionConfig | TooltipActionConfig? | | Give configurations (alignment, position, etc...) to the tooltip actionbar | ✅ | ✅ | +| Name | Type | Default Behaviour | Description | `Showcase` | `ShowCaseWidget` | +|------------------------------|----------------------------|--------------------------------------------------|----------------------------------------------------------------------------------------------------|------------|------------------| +| key | GlobalKey | | Unique Global key for each showcase. | ✅ | ✅ | +| child | Widget | | The Target widget that you want to be showcased | ✅ | ✅ | +| title | String? | | Title of default tooltip | ✅ | | +| description | String? | | Description of default tooltip | ✅ | | +| container | Widget? | | Allows to create custom tooltip widget. | | ✅ | +| height | double? | | Height of custom tooltip widget | | ✅ | +| width | double? | | Width of custom tooltip widget | | ✅ | +| titleTextStyle | TextStyle? | | Text Style of title | ✅ | | +| descTextStyle | TextStyle? | | Text Style of description | ✅ | | +| titleAlignment | TextAlign | TextAlign.start | Alignment of title | ✅ | | +| descriptionAlignment | TextAlign | TextAlign.start | Alignment of description | ✅ | | +| targetShapeBorder | ShapeBorder | | If `targetBorderRadius` param is not provided then it applies shape border to target widget | ✅ | ✅ | +| targetBorderRadius | BorderRadius? | | Border radius of target widget | ✅ | ✅ | +| tooltipBorderRadius | BorderRadius? | BorderRadius.circular(8.0) | Border radius of tooltip | ✅ | | +| blurValue | double? | `ShowCaseWidget.blurValue` | Gaussian blur effect on overlay | ✅ | ✅ | +| tooltipPadding | EdgeInsets | EdgeInsets.symmetric(vertical: 8, horizontal: 8) | Padding to tooltip content | ✅ | | +| targetPadding | EdgeInsets | EdgeInsets.zero | Padding to target widget | ✅ | ✅ | +| overlayOpacity | double | 0.75 | Opacity of overlay layer | ✅ | ✅ | +| overlayColor | Color | Colors.black45 | Color of overlay layer | ✅ | ✅ | +| tooltipBackgroundColor | Color | Colors.white | Background Color of default tooltip | ✅ | | +| textColor | Color | Colors.black | Color of tooltip text | ✅ | | +| scrollLoadingWidget | Widget | | Loading widget on overlay until active showcase is visible to viewport when `autoScroll` is enable | ✅ | ✅ | +| movingAnimationDuration | Duration | Duration(milliseconds: 2000) | Duration of time this moving animation should last. | ✅ | ✅ | +| showArrow | bool | true | Shows tooltip with arrow | ✅ | | +| disableDefaultTargetGestures | bool | false | disable default gestures of target widget | ✅ | ✅ | +| disposeOnTap | bool? | false | Dismiss all showcases on target/tooltip tap | ✅ | ✅ | +| disableMovingAnimation | bool? | `ShowCaseWidget.disableMovingAnimation` | Disable bouncing/moving transition | ✅ | ✅ | +| disableScaleAnimation | bool? | `ShowCaseWidget.disableScaleAnimation` | Disable initial scale transition when showcase is being started and completed | ✅ | | +| scaleAnimationDuration | Duration | Duration(milliseconds: 300) | Duration of time scale animation should last. | ✅ | | +| scaleAnimationCurve | Curve | Curves.easeIn | Curve to use in scale animation. | ✅ | | +| scaleAnimationAlignment | Alignment? | | Origin of the coordinate in which the scale takes place, relative to the size of the box. | ✅ | | +| onToolTipClick | VoidCallback? | | Triggers when tooltip is being clicked. | ✅ | | +| onTargetClick | VoidCallback? | | Triggers when target widget is being clicked | ✅ | ✅ | +| onTargetDoubleTap | VoidCallback? | | Triggers when target widget is being double clicked | ✅ | ✅ | +| onTargetLongPress | VoidCallback? | | Triggers when target widget is being long pressed | ✅ | ✅ | +| onBarrierClick | VoidCallback? | | Triggers when barrier is clicked | ✅ | ✅ | +| tooltipPosition | TooltipPosition? | | Defines vertical position of tooltip respective to Target widget | ✅ | ✅ | +| titlePadding | EdgeInsets? | EdgeInsets.zero | Padding to title | ✅ | | +| descriptionPadding | EdgeInsets? | EdgeInsets.zero | Padding to description | ✅ | | +| titleTextDirection | TextDirection? | | Give textDirection to title | ✅ | | +| descriptionTextDirection | TextDirection? | | Give textDirection to description | ✅ | | +| descriptionTextDirection | TextDirection? | | Give textDirection to description | ✅ | | +| disableBarrierInteraction | bool | false | Disables barrier interaction for a particular showCase | ✅ | ✅ | +| toolTipSlideEndDistance | double | 7 | Defines motion range for tooltip slide animation | ✅ | ✅ | +| tooltipActions | List? | [] | Provide a list of tooltip actions | ✅ | ✅ | +| tooltipActionConfig | TooltipActionConfig? | | Give configurations (alignment, position, etc...) to the tooltip actionbar | ✅ | ✅ | ## Properties of `TooltipActionButton.withDefault` and `TooltipActionButton.custom`: @@ -206,21 +206,21 @@ WidgetsBinding.instance.addPostFrameCallback((_) => ## Properties of `TooltipActionConfig`: -| Name | Type | Default Behaviour | Description | -|----------------------------|-------------------------|:-------------------------------|-------------------------------------------------------------------| -| alignment | TooltipActionAlignment? | TooltipActionAlignment.left | Alignment of tooltip action buttons (left, right, center, spread) | -| actionGap | double? | 5 | Horizontal gap between the tooltip action buttons | -| padding | EdgeInsets? | EdgeInsets.zero | Padding to the tooltip actionbar | -| position | TooltipActionPosition? | TooltipActionPosition.inside | Position of tooltip actionbar (inside, outside) | -| gapBetweenContentAndAction | double? | 10 | Gap between tooltip content and actionbar | +| Name | Type | Default Behaviour | Description | +|----------------------------|-------------------------|:-----------------------------|-------------------------------------------------------------------| +| alignment | TooltipActionAlignment? | TooltipActionAlignment.left | Alignment of tooltip action buttons (left, right, center, spread) | +| actionGap | double? | 5 | Horizontal gap between the tooltip action buttons | +| padding | EdgeInsets? | EdgeInsets.zero | Padding to the tooltip actionbar | +| position | TooltipActionPosition? | TooltipActionPosition.inside | Position of tooltip actionbar (inside, outside) | +| gapBetweenContentAndAction | double? | 10 | Gap between tooltip content and actionbar | ## Properties of `ActionButtonIcon.withIcon` and `ActionButtonIcon.withImageIcon`: -| Name | Type | Default Behaviour | Description | `ActionButtonIcon.withIcon` | `ActionButtonIcon.withImageIcon` | -|---------|-------------|-------------------|--------------------------------------|-----------------------------|----------------------------------| -| icon | Icon | | Provide a icon to the button | ✅ | | -| icon | ImageIcon | | Provide a image icon to the button | | ✅ | -| padding | EdgeInsets? | | Give padding to the icon | ✅ | ✅ | +| Name | Type | Default Behaviour | Description | `ActionButtonIcon.withIcon` | `ActionButtonIcon.withImageIcon` | +|---------|-------------|-------------------|------------------------------------|-----------------------------|----------------------------------| +| icon | Icon | | Provide a icon to the button | ✅ | | +| icon | ImageIcon | | Provide a image icon to the button | | ✅ | +| padding | EdgeInsets? | | Give padding to the icon | ✅ | ✅ | ## How to use diff --git a/example/lib/main.dart b/example/lib/main.dart index 7d3a954b..317af350 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -39,7 +39,7 @@ class MyApp extends StatelessWidget { builder: (context) => const MailPage(), globalTooltipActionConfig: const TooltipActionConfig( position: TooltipActionPosition.inside, - alignment: TooltipActionAlignment.spread, + alignment: MainAxisAlignment.spaceBetween, ), ), ), @@ -186,7 +186,7 @@ class _MailPageState extends State { debugPrint('Barrier clicked'), tooltipActionConfig: const TooltipActionConfig( - alignment: TooltipActionAlignment.right, + alignment: MainAxisAlignment.end, position: TooltipActionPosition.outside, gapBetweenContentAndAction: 10, ), @@ -236,7 +236,20 @@ class _MailPageState extends State { "Tap to see profile which contains user's name, profile picture, mobile number and country", tooltipBackgroundColor: Theme.of(context).primaryColor, textColor: Colors.white, + onTargetClick: () { + print('target cliecked'); + }, + disposeOnTap: false, + onToolTipClick: () { + print('clicked tool tip'); + }, + disableDefaultTargetGestures: true, targetShapeBorder: const CircleBorder(), + tooltipActionConfig: const TooltipActionConfig( + alignment: MainAxisAlignment.spaceBetween, + gapBetweenContentAndAction: 10, + position: TooltipActionPosition.outside, + ), tooltipActions: [ TooltipActionButton.withDefault( backgroundColor: Colors.transparent, @@ -249,7 +262,7 @@ class _MailPageState extends State { textStyle: const TextStyle( color: Colors.pinkAccent, ), - ) + ), ], child: Container( padding: const EdgeInsets.all(5), @@ -308,8 +321,8 @@ class _MailPageState extends State { targetShapeBorder: const CircleBorder(), showArrow: false, tooltipActionConfig: const TooltipActionConfig( - alignment: TooltipActionAlignment.spread, - actionGap: 15, + alignment: MainAxisAlignment.spaceBetween, + actionGap: 12, ), tooltipActions: [ TooltipActionButton.withDefault( @@ -324,7 +337,7 @@ class _MailPageState extends State { color: Colors.pink, )), TooltipActionButton.withDefault( - type: TooltipDefaultActionType.next, + type: TooltipDefaultActionType.skip, name: 'Close', tailIcon: const ActionButtonIcon.withIcon( icon: Icon( @@ -333,10 +346,6 @@ class _MailPageState extends State { size: 15, ), ), - onTap: () { - // Write your code on button tap - ShowCaseWidget.of(context).next(); - }, ), ], child: FloatingActionButton( @@ -375,8 +384,37 @@ class _MailPageState extends State { child: Showcase( key: key, description: 'Tap to check mail', - tooltipPosition: TooltipPosition.top, disposeOnTap: true, + tooltipActionConfig: const TooltipActionConfig( + alignment: MainAxisAlignment.spaceBetween, + actionGap: 15, + position: TooltipActionPosition.outside, + gapBetweenContentAndAction: 16, + ), + tooltipActions: [ + TooltipActionButton.withDefault( + type: TooltipDefaultActionType.previous, + name: 'Back', + onTap: () { + // Write your code on button tap + ShowCaseWidget.of(context).previous(); + }, + backgroundColor: Colors.pink.shade50, + textStyle: const TextStyle( + color: Colors.pink, + )), + TooltipActionButton.withDefault( + type: TooltipDefaultActionType.skip, + name: 'Close', + tailIcon: const ActionButtonIcon.withIcon( + icon: Icon( + Icons.close, + color: Colors.white, + size: 15, + ), + ), + ), + ], onTargetClick: () { Navigator.push( context, @@ -475,6 +513,24 @@ class MailTile extends StatelessWidget { key: showCaseKey!, height: 50, width: 140, + tooltipActionConfig: const TooltipActionConfig( + alignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + ), + tooltipActions: [ + TooltipActionButton.withDefault( + backgroundColor: Colors.transparent, + type: TooltipDefaultActionType.previous, + padding: EdgeInsets.zero, + ), + TooltipActionButton.withDefault( + type: TooltipDefaultActionType.next, + backgroundColor: Colors.white, + textStyle: const TextStyle( + color: Colors.pinkAccent, + ), + ), + ], targetShapeBorder: const CircleBorder(), targetBorderRadius: const BorderRadius.all( Radius.circular(150), diff --git a/lib/src/enum.dart b/lib/src/enum.dart index 5e3fcae3..6a7330e7 100644 --- a/lib/src/enum.dart +++ b/lib/src/enum.dart @@ -22,9 +22,18 @@ import 'package:flutter/cupertino.dart'; +import 'showcase_widget.dart'; + enum TooltipPosition { top, bottom } -enum TooltipActionPosition { outside, inside } +enum TooltipActionPosition { + outside, + inside; + + bool get isInside => this == inside; + + bool get isOutside => this == outside; +} enum TooltipActionAlignment { left(MainAxisAlignment.start), @@ -47,4 +56,20 @@ enum TooltipDefaultActionType { }); final String actionName; + + void onTap(ShowCaseWidgetState showCaseState) { + switch (this) { + case TooltipDefaultActionType.next: + showCaseState.next(); + break; + case TooltipDefaultActionType.previous: + showCaseState.previous(); + break; + case TooltipDefaultActionType.skip: + showCaseState.dismiss(); + break; + default: + throw ArgumentError('Invalid tooltip default action type'); + } + } } diff --git a/lib/src/models/tooltip_action_button.dart b/lib/src/models/tooltip_action_button.dart index 754afa96..b806c474 100644 --- a/lib/src/models/tooltip_action_button.dart +++ b/lib/src/models/tooltip_action_button.dart @@ -3,19 +3,46 @@ import 'package:flutter/material.dart'; import '../../showcaseview.dart'; class TooltipActionButton { + /// To Provide Background color to the action in [TooltipActionButton.withDefault] final Color? backgroundColor; + + /// To Provide borderRadius to the action in [TooltipActionButton.withDefault] final BorderRadius? borderRadius; + + /// To Provide textStyle to the action text in [TooltipActionButton.withDefault] final TextStyle? textStyle; + + /// To Provide padding to the action widget final EdgeInsets? padding; + + /// To Provide a custom widget for the action in [TooltipActionButton.custom] final Widget? button; + + /// To Provide a leading icon for the action in [TooltipActionButton.withDefault] final ActionButtonIcon? leadIcon; + + /// To Provide a tail icon for the action in [TooltipActionButton.withDefault] final ActionButtonIcon? tailIcon; + + /// To Provide a action type in [TooltipActionButton.withDefault] final TooltipDefaultActionType? type; + + /// To Provide a text for action in [TooltipActionButton.withDefault] final String? name; + + /// To Provide a onTap for action in [TooltipActionButton.withDefault] final VoidCallback? onTap; + + /// To Provide a border for action in [TooltipActionButton.withDefault] final double? borderWidth; + + /// To Provide a borderColor for action in [TooltipActionButton.withDefault] final Color? borderColor; + + /// To show or hide action for the first tooltip defaults to [true] final bool shouldShowForFirstTooltip; + + /// To show or hide action for the hide tooltip defaults to [true] final bool shouldShowForLastTooltip; TooltipActionButton.withDefault({ diff --git a/lib/src/models/tooltip_action_config.dart b/lib/src/models/tooltip_action_config.dart index 7c8fcd70..074b4253 100644 --- a/lib/src/models/tooltip_action_config.dart +++ b/lib/src/models/tooltip_action_config.dart @@ -4,11 +4,12 @@ import '../../showcaseview.dart'; class TooltipActionConfig { const TooltipActionConfig({ - this.alignment = TooltipActionAlignment.left, + this.alignment = MainAxisAlignment.spaceBetween, this.actionGap = 5, this.padding = EdgeInsets.zero, this.position = TooltipActionPosition.inside, this.gapBetweenContentAndAction = 10, + this.crossAxisAlignment = CrossAxisAlignment.start, }); /// Defines tooltip action widget position. @@ -20,7 +21,7 @@ class TooltipActionConfig { /// Defines the alignment of actions buttons of tooltip action widget /// /// Default to [TooltipActionAlignment.left] - final TooltipActionAlignment alignment; + final MainAxisAlignment alignment; /// Defines the gap between the actions buttons of tooltip action widget /// @@ -36,4 +37,9 @@ class TooltipActionConfig { /// /// Default to 10.0 final double gapBetweenContentAndAction; + + /// Defines running direction alignment for the Action widgets. + /// + /// Default to [crossAxisAlignment.start] + final CrossAxisAlignment crossAxisAlignment; } diff --git a/lib/src/showcase.dart b/lib/src/showcase.dart index 2200ade6..4c3ce8e9 100644 --- a/lib/src/showcase.dart +++ b/lib/src/showcase.dart @@ -25,8 +25,8 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:showcaseview/showcaseview.dart'; +import '../showcaseview.dart'; import 'get_position.dart'; import 'layout_overlays.dart'; import 'shape_clipper.dart'; @@ -257,6 +257,8 @@ class Showcase extends StatefulWidget { /// Defaults to 14. final double toolTipMargin; + /// Defines tooltip action widget position. + /// It can be inside the tooltip widget or outside. /// Provides tooTip action widgets at bottom in tooltip. /// /// one can use [TooltipActionButton] class to use default action @@ -317,8 +319,8 @@ class Showcase extends StatefulWidget { this.toolTipSlideEndDistance = 7, this.toolTipMargin = 14, this.tooltipActionConfig, - }) : width = null, - height = null, + }) : height = null, + width = null, container = null, assert(overlayOpacity >= 0.0 && overlayOpacity <= 1.0, "overlay opacity must be between 0 and 1."), @@ -655,9 +657,6 @@ class _ShowcaseState extends State { descriptionTextDirection: widget.descriptionTextDirection, toolTipSlideEndDistance: widget.toolTipSlideEndDistance, toolTipMargin: widget.toolTipMargin, - tooltipActionPosition: widget.tooltipActionPosition, - gapBetweenContentAndAction: widget.gapBetweenContentAndAction, - showCaseState: ShowCaseWidget.of(context), tooltipActionConfig: _getTooltipActionConfig(), tooltipActions: _getTooltipActions(), ), @@ -666,10 +665,41 @@ class _ShowcaseState extends State { ); } - List _getTooltipActions() => - (widget.tooltipActions?.isEmpty ?? true) - ? ShowCaseWidget.of(context).globalTooltipActions ?? [] - : widget.tooltipActions ?? []; + List _getTooltipActions() { + final showCaseState = ShowCaseWidget.of(context); + final actionData = (widget.tooltipActions?.isEmpty ?? true) + ? showCaseState.globalTooltipActions ?? [] + : widget.tooltipActions ?? []; + + final actionWidgets = []; + for (var action = 0; action < actionData.length; action++) { + /// This checks that if it is first or last tooltip and + /// [shouldShowForLastTooltip] or [shouldShowForFirstTooltip] is true + /// then we will ignore that action + if (((showCaseState.activeWidgetId == 0 && + actionData[action].shouldShowForFirstTooltip) || + (showCaseState.activeWidgetId == + (showCaseState.ids?.length ?? 0) - 1 && + !actionData[action].shouldShowForLastTooltip)) && + (widget.tooltipActions?.isEmpty ?? true)) { + continue; + } + actionWidgets.add( + Padding( + padding: EdgeInsetsDirectional.only( + end: action < actionData.length - 1 + ? _getTooltipActionConfig().actionGap + : 0, + ), + child: TooltipActionButtonWidget( + config: actionData[action], + showCaseState: ShowCaseWidget.of(context), + ), + ), + ); + } + return actionWidgets; + } TooltipActionConfig _getTooltipActionConfig() { final showCaseState = ShowCaseWidget.of(context); diff --git a/lib/src/showcase_widget.dart b/lib/src/showcase_widget.dart index 00a91ec1..8676cc85 100644 --- a/lib/src/showcase_widget.dart +++ b/lib/src/showcase_widget.dart @@ -83,7 +83,10 @@ class ShowCaseWidget extends StatefulWidget { /// Enable/disable showcase globally. Enabled by default. final bool enableShowcase; + /// Global action to apply on every tooltip widget final List? globalTooltipActions; + + /// Global Config for tooltip action to auto apply for all the toolTip final TooltipActionConfig? globalTooltipActionConfig; const ShowCaseWidget({ @@ -132,6 +135,7 @@ class ShowCaseWidgetState extends State { Key? anchoredOverlayKey; late final TooltipActionConfig? globalTooltipActionConfig; + late final List? globalTooltipActions; /// These properties are only here so that it can be accessed by diff --git a/lib/src/tooltip_action_button_widget.dart b/lib/src/tooltip_action_button_widget.dart index 1a1e4504..64d70f85 100644 --- a/lib/src/tooltip_action_button_widget.dart +++ b/lib/src/tooltip_action_button_widget.dart @@ -32,7 +32,7 @@ class TooltipActionButtonWidget extends StatelessWidget { ), ), child: Row( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ if (config.leadIcon != null) Padding( @@ -60,19 +60,7 @@ class TooltipActionButtonWidget extends StatelessWidget { if (config.onTap != null) { config.onTap?.call(); } else { - switch (config.type) { - case TooltipDefaultActionType.next: - showCaseState.next(); - break; - case TooltipDefaultActionType.previous: - showCaseState.previous(); - break; - case TooltipDefaultActionType.skip: - showCaseState.dismiss(); - break; - default: - throw ArgumentError('Invalid tooltip default action type'); - } + config.type?.onTap(showCaseState); } } } diff --git a/lib/src/tooltip_widget.dart b/lib/src/tooltip_widget.dart index db911031..016133c3 100644 --- a/lib/src/tooltip_widget.dart +++ b/lib/src/tooltip_widget.dart @@ -24,9 +24,11 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import '../showcaseview.dart'; +import 'enum.dart'; import 'get_position.dart'; import 'measure_size.dart'; +import 'models/tooltip_action_config.dart'; +import 'widget/action_widget.dart'; import 'widget/tooltip_slide_transition.dart'; class ToolTipWidget extends StatefulWidget { @@ -58,15 +60,14 @@ class ToolTipWidget extends StatefulWidget { final TooltipPosition? tooltipPosition; final EdgeInsets? titlePadding; final EdgeInsets? descriptionPadding; - List? tooltipActions; final TextDirection? titleTextDirection; final TextDirection? descriptionTextDirection; final double toolTipSlideEndDistance; final double toolTipMargin; - final ShowCaseWidgetState showCaseState; - TooltipActionConfig tooltipActionConfig; + final TooltipActionConfig tooltipActionConfig; + final List tooltipActions; - ToolTipWidget({ + const ToolTipWidget({ super.key, required this.position, required this.offset, @@ -91,18 +92,17 @@ class ToolTipWidget extends StatefulWidget { required this.tooltipBorderRadius, required this.scaleAnimationDuration, required this.scaleAnimationCurve, - required this.showCaseState, - required this.tooltipActionConfig, required this.toolTipMargin, this.scaleAnimationAlignment, this.isTooltipDismissed = false, this.tooltipPosition, this.titlePadding, this.descriptionPadding, - this.tooltipActions, this.titleTextDirection, this.descriptionTextDirection, this.toolTipSlideEndDistance = 7, + required this.tooltipActionConfig, + required this.tooltipActions, }); @override @@ -121,26 +121,25 @@ class _ToolTipWidgetState extends State late final Animation _scaleAnimation; double tooltipWidth = 0; - double tooltipHeight = 0; - double tooltipScreenEdgePadding = 20; - double tooltipTextPadding = 15; - double actionWidgetHeight = 0.0; - Size? tooltipActionSize; - final GlobalKey tooltipActionKey = GlobalKey(); - bool isOffstage = true; + // This is Default height considered at the start of this package + double tooltipHeight = 120; + + final _withArrowToolTipPadding = 22.0; + final _withOutArrowToolTipPadding = 10.0; + + // To store Tooltip action size + Size? _tooltipActionSize; - void setTooltipActionWidth(size) => tooltipActionSize ??= size; + // This is used when [_tooltipActionSize] is already calculated and + // on change of something we are recalculating the size of the widget + bool isSizeRecalculating = false; TooltipPosition findPositionForContent(Offset position) { var height = tooltipHeight; - height = widget.contentHeight ?? height; - final bottomPosition = position.dy + - ((widget.position?.getHeight() ?? 0) * 0.5) + - actionWidgetHeight; - final topPosition = position.dy - - ((widget.position?.getHeight() ?? 0) * 0.5) - - actionWidgetHeight; + final bottomPosition = + position.dy + ((widget.position?.getHeight() ?? 0) / 2); + final topPosition = position.dy - ((widget.position?.getHeight() ?? 0) / 2); final hasSpaceInTop = topPosition >= height; // TODO: need to update for flutter version > 3.8.X // ignore: deprecated_member_use @@ -160,7 +159,37 @@ class _ToolTipWidgetState extends State : TooltipPosition.bottom); } - void _getTooltipWidth() { + /// This will calculate the width and height of the tooltip + void _getTooltipSize() { + Size? toolTipActionSize; + // if tooltip action is there this will calculate the height of that + if (widget.tooltipActions.isNotEmpty) { + final renderBox = + _actionWidgetKey.currentContext?.findRenderObject() as RenderBox?; + + // if first frame is drawn then only we will be able to calculate the + // size of the action widget + if (renderBox != null) { + toolTipActionSize = _tooltipActionSize = renderBox.size; + isSizeRecalculating = false; + } else if (_tooltipActionSize == null || renderBox == null) { + // If first frame is not drawn then we will schedule the rebuild after + // the first frame is drawn + isSizeRecalculating = true; + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (mounted) { + _getTooltipSize(); + setState(() {}); + } + }); + // If size is calculated once then we will wait for first frame + // to draw before calculating anything as that may cause a flicker + // in the tooltip + if (isSizeRecalculating && _tooltipActionSize != null) { + return; + } + } + } final titleStyle = widget.titleTextStyle ?? Theme.of(context) .textTheme @@ -171,26 +200,75 @@ class _ToolTipWidgetState extends State .textTheme .titleSmall! .merge(TextStyle(color: widget.textColor)); - final titleLength = widget.title == null - ? 0 - : _textSize(widget.title!, titleStyle).width + - widget.tooltipPadding!.right + - widget.tooltipPadding!.left + - (widget.titlePadding?.right ?? 0) + - (widget.titlePadding?.left ?? 0); - final descriptionLength = widget.description == null - ? 0 - : (_textSize(widget.description!, descriptionStyle).width + - widget.tooltipPadding!.right + - widget.tooltipPadding!.left + - (widget.descriptionPadding?.right ?? 0) + - (widget.descriptionPadding?.left ?? 0)); - var maxTextWidth = max(titleLength, descriptionLength); - if (maxTextWidth > widget.screenSize.width - tooltipScreenEdgePadding) { - tooltipWidth = widget.screenSize.width - tooltipScreenEdgePadding; + + // This is to calculate the size of the title text + // We have passed padding so we get the perfect width of the Title + final titleSize = _textSize( + widget.title, + titleStyle, + widget.titlePadding, + ); + + // This is to calculate the size of the description text + // We have passed padding so we get the perfect width of the Title + final descriptionSize = _textSize( + widget.description, + descriptionStyle, + widget.descriptionPadding, + ); + final titleLength = widget.title == null ? 0 : titleSize!.width; + final descriptionLength = + widget.description == null ? 0 : descriptionSize!.width; + var maxTextWidth = + max(toolTipActionSize?.width ?? 0, max(titleLength, descriptionLength)); + // if Width is greater than available size which won't happen we will + // adjust it to stay in available size + if (maxTextWidth > widget.screenSize.width - (2 * widget.toolTipMargin)) { + tooltipWidth = widget.screenSize.width - (2 * widget.toolTipMargin); } else { - tooltipWidth = maxTextWidth + 0; + // This is padding we will have around the tooltip text + final textPadding = + (widget.tooltipPadding ?? EdgeInsets.zero).horizontal + + max((widget.titlePadding ?? EdgeInsets.zero).horizontal, + (widget.descriptionPadding ?? EdgeInsets.zero).horizontal); + + // Final tooltip width will be text width + padding around the tool tip + // Here we have not considered the margin around the tooltip as that + // doesn't count in width of the tooltip + if ((toolTipActionSize?.width ?? 0) >= + (max(titleLength, descriptionLength) + textPadding)) { + tooltipWidth = toolTipActionSize?.width ?? 0; + } else { + tooltipWidth = maxTextWidth + textPadding; + } + } + + // If user has provided the width then we will use the maximum of action + // width and user provided width + if (widget.contentWidth != null) { + tooltipWidth = max(toolTipActionSize?.width ?? 0, widget.contentWidth!); } + + final arrowHeight = widget.showArrow + ? _withArrowToolTipPadding + : _withOutArrowToolTipPadding; + // Login to calculate the tooltip height + // Text height + padding above and below of text + arrow height + extra + // space provided between target widget and tooltip widget + + // tooltip slide end distance + toolTip action Size + + // 20 for the extra space so it won't stick to any side + + tooltipHeight = (widget.tooltipPadding ?? EdgeInsets.zero).vertical + + (titleSize?.height ?? 0) + + (descriptionSize?.height ?? 0) + + arrowHeight - + 1 + + 3 + + widget.toolTipSlideEndDistance + + (toolTipActionSize?.height ?? + widget.tooltipActionConfig.gapBetweenContentAndAction) + + (widget.contentHeight ?? 0) + + 20; } double? _getLeft() { @@ -229,10 +307,10 @@ class _ToolTipWidgetState extends State } double _getSpace() { - var space = widget.position!.getCenter() - (widget.contentWidth! * 0.5); + var space = widget.position!.getCenter() - (widget.contentWidth! / 2); if (space + widget.contentWidth! > widget.screenSize.width) { space = widget.screenSize.width - widget.contentWidth! - 8; - } else if (space < (widget.contentWidth! * 0.5)) { + } else if (space < (widget.contentWidth! / 2)) { space = 16; } return space; @@ -257,16 +335,23 @@ class _ToolTipWidgetState extends State } } - double _getAlignmentY() => -1; + double _getAlignmentY() { + var dy = isArrowUp + ? -1.0 + : (MediaQuery.of(context).size.height / 2) < widget.position!.getTop() + ? -1.0 + : 1.0; + return dy; + } final GlobalKey _customContainerKey = GlobalKey(); + final GlobalKey _actionWidgetKey = GlobalKey(); final ValueNotifier _customContainerWidth = ValueNotifier(1); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - _getWidgetSize(); if (widget.container != null && _customContainerKey.currentContext != null && _customContainerKey.currentContext?.size != null) { @@ -327,34 +412,29 @@ class _ToolTipWidgetState extends State @override void didChangeDependencies() { super.didChangeDependencies(); - _getTooltipWidth(); + // If tooltip is dismissing then no need to recalculate the size and widgets + if (!widget.isTooltipDismissed) { + _getTooltipSize(); + } } @override void didUpdateWidget(covariant ToolTipWidget oldWidget) { super.didUpdateWidget(oldWidget); - _getTooltipWidth(); + // If tooltip is dismissing then no need to recalculate the size and widgets + // If widget is same as before then also no need to calculate + if (!widget.isTooltipDismissed && oldWidget.hashCode != hashCode) { + _getTooltipSize(); + } } @override void dispose() { _movingAnimationController.dispose(); _scaleAnimationController.dispose(); - super.dispose(); } - void _getWidgetSize() { - if (tooltipActionSize == null) { - final renderBox = - tooltipActionKey.currentContext?.findRenderObject() as RenderBox?; - if (renderBox != null) { - tooltipActionSize = renderBox.size; - setState(() => isOffstage = false); - } - } - } - @override Widget build(BuildContext context) { // TODO: maybe all this calculation doesn't need to run here. Maybe all or some of it can be moved outside? @@ -364,19 +444,27 @@ class _ToolTipWidgetState extends State contentOrientation == TooltipPosition.bottom ? 1.0 : -1.0; isArrowUp = contentOffsetMultiplier == 1.0; - final contentY = isArrowUp + var contentY = isArrowUp ? widget.position!.getBottom() + (contentOffsetMultiplier * 3) : widget.position!.getTop() + (contentOffsetMultiplier * 3); + // if tooltip is going out of screen in bottom this will ensure it is + // visible above the widget + // Here 20 is added to have some space at the bottom of the tooltip + if (contentY + tooltipHeight >= MediaQuery.of(context).size.height && + isArrowUp) { + contentY = MediaQuery.of(context).size.height - tooltipHeight - 20; + } + final num contentFractionalOffset = contentOffsetMultiplier.clamp(-1.0, 0.0); - var paddingTop = isArrowUp ? 22.0 : 0.0; - var paddingBottom = isArrowUp ? 0.0 : 22.0; + var paddingTop = isArrowUp ? _withArrowToolTipPadding : 0.0; + var paddingBottom = isArrowUp ? 0.0 : _withArrowToolTipPadding; if (!widget.showArrow) { - paddingTop = 10; - paddingBottom = 10; + paddingTop = _withOutArrowToolTipPadding; + paddingBottom = _withOutArrowToolTipPadding; } const arrowWidth = 18.0; @@ -386,329 +474,348 @@ class _ToolTipWidgetState extends State _scaleAnimationController.reverse(); } - final tooltipActionButtonAlignment = - widget.tooltipActionConfig.alignment.alignment; - final tooltipAdaptiveWidth = _getTooltipAdaptiveWidth(); - final tooltipActionsList = _getActionWidgets(); - - Widget tooltipActionWidget = Offstage( - offstage: true, - child: Material( - type: MaterialType.transparency, - child: Padding( - key: tooltipActionKey, - padding: widget.tooltipActionConfig.padding ?? EdgeInsets.zero, - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: tooltipActionButtonAlignment, - children: tooltipActionsList, - ), - ), - ), - ); - if (widget.container == null) { - if (isOffstage) { - return tooltipActionWidget; - } else { - return Positioned( - top: contentY, - left: _getLeft(), - right: _getRight(), - child: ScaleTransition( - scale: _scaleAnimation, - alignment: widget.scaleAnimationAlignment ?? - Alignment( - _getAlignmentX(), - _getAlignmentY(), + return Positioned( + top: contentY, + left: _getLeft(), + right: _getRight(), + child: ScaleTransition( + scale: _scaleAnimation, + alignment: widget.scaleAnimationAlignment ?? + Alignment( + _getAlignmentX(), + _getAlignmentY(), + ), + child: FractionalTranslation( + translation: Offset(0.0, contentFractionalOffset as double), + child: ToolTipSlideTransition( + position: Tween( + begin: Offset.zero, + end: Offset( + 0, + widget.toolTipSlideEndDistance * contentOffsetMultiplier, ), - 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( - type: MaterialType.transparency, - child: MeasureSize( - onSizeChange: onTooltipSizeChanged, - child: Container( + ).animate(_movingAnimation), + child: Material( + type: MaterialType.transparency, + child: Column( + children: [ + if (widget.tooltipActions.isNotEmpty && + widget.tooltipActionConfig.position.isOutside && + !isArrowUp) + ActionWidget( + alignment: widget.tooltipActionConfig.alignment, + tooltipActionConfig: widget.tooltipActionConfig, + width: _tooltipActionSize == null ? null : tooltipWidth, + crossAxisAlignment: + widget.tooltipActionConfig.crossAxisAlignment, + outSidePadding: _tooltipActionSize == null + ? EdgeInsets.only( + left: widget.tooltipPadding?.left ?? 0, + right: widget.tooltipPadding?.right ?? 0, + ) + : EdgeInsets.zero, + isArrowUp: isArrowUp, + children: widget.tooltipActions, + ), + Container( padding: widget.showArrow ? EdgeInsets.only( top: paddingTop - (isArrowUp ? arrowHeight : 0), bottom: paddingBottom - (isArrowUp ? 0 : arrowHeight), ) - : EdgeInsets.symmetric( - vertical: paddingTop, + : null, + child: Stack( + alignment: isArrowUp + ? Alignment.topLeft + : _getLeft() == null + ? Alignment.bottomRight + : Alignment.bottomLeft, + children: [ + // This widget is used for calculation of the action + // widget size and it will be removed once the size + // is calculated + if (isSizeRecalculating) + Offstage( + child: ActionWidget( + key: _actionWidgetKey, + outSidePadding: widget + .tooltipActionConfig.position.isInside + ? EdgeInsets.only( + left: widget.tooltipPadding?.left ?? 0, + right: + widget.tooltipPadding?.right ?? 0, + ) + : EdgeInsets.zero, + tooltipActionConfig: widget.tooltipActionConfig, + alignment: widget.tooltipActionConfig.alignment, + width: null, + crossAxisAlignment: widget + .tooltipActionConfig.crossAxisAlignment, + isArrowUp: true, + children: widget.tooltipActions, + ), ), - child: SizedBox( - width: tooltipAdaptiveWidth, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (!isArrowUp && - widget.tooltipActionConfig.position == - TooltipActionPosition.outside && - tooltipActionsList.isNotEmpty) ...[ - Container( - width: tooltipAdaptiveWidth, - padding: widget.tooltipActionConfig.padding ?? - EdgeInsets.zero, - child: Row( - mainAxisAlignment: - tooltipActionButtonAlignment, - children: tooltipActionsList, + 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, ), ), - SizedBox( - height: widget.tooltipActionConfig - .gapBetweenContentAndAction, - ), - ], - 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: widget.showArrow && isArrowUp - ? arrowHeight - 1 - : 0, - bottom: widget.showArrow && !isArrowUp - ? arrowHeight - 1 - : 0, + ), + 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?.copyWith( + left: 0, + right: 0, ), - child: ClipRRect( - borderRadius: widget.tooltipBorderRadius ?? - BorderRadius.circular(8.0), - child: GestureDetector( - onTap: widget.onTooltipTap, - child: Container( - padding: widget.tooltipPadding, - color: widget.tooltipBackgroundColor, - child: Center( - child: Column( - crossAxisAlignment: - widget.title != null - ? CrossAxisAlignment.start - : CrossAxisAlignment.center, - children: [ - if (widget.title != null) - SizedBox( - width: tooltipAdaptiveWidth, - child: 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, - ), - ), + color: widget.tooltipBackgroundColor, + child: Column( + crossAxisAlignment: widget.title != null + ? CrossAxisAlignment.start + : CrossAxisAlignment.center, + children: [ + if (widget.title != null) + Padding( + padding: (widget.titlePadding ?? + EdgeInsets.zero) + .add( + EdgeInsets.only( + left: + widget.tooltipPadding?.left ?? + 0, + right: widget + .tooltipPadding?.right ?? + 0, + ), + ), + child: Text( + widget.title!, + textAlign: widget.titleAlignment, + textDirection: + widget.titleTextDirection, + style: widget.titleTextStyle ?? + Theme.of(context) + .textTheme + .titleLarge! + .merge( + TextStyle( + color: widget.textColor, + ), ), - ), - ), - SizedBox( - width: tooltipAdaptiveWidth, - child: 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, - ), - ), - ), - ), - ), - if (widget.tooltipActionConfig - .position == - TooltipActionPosition - .inside && - tooltipActionsList - .isNotEmpty) ...[ - SizedBox( - height: widget - .tooltipActionConfig - .gapBetweenContentAndAction, - ), - Padding( - padding: widget - .tooltipActionConfig - .padding ?? - EdgeInsets.zero, - child: Row( - mainAxisSize: - MainAxisSize.max, - mainAxisAlignment: - tooltipActionButtonAlignment, - children: - tooltipActionsList, - ), - ), - ] - ], ), ), + Padding( + padding: (widget.descriptionPadding ?? + EdgeInsets.zero) + .add( + EdgeInsets.only( + left: widget.tooltipPadding?.left ?? + 0, + right: + widget.tooltipPadding?.right ?? + 0, + ), + ), + child: Text( + widget.description!, + textAlign: + widget.descriptionAlignment, + textDirection: + widget.descriptionTextDirection, + style: widget.descTextStyle ?? + Theme.of(context) + .textTheme + .titleSmall! + .merge( + TextStyle( + color: widget.textColor, + ), + ), + ), ), - ), + if (widget.tooltipActions.isNotEmpty && + widget.tooltipActionConfig.position + .isInside && + _tooltipActionSize != null) + ActionWidget( + outSidePadding: EdgeInsets.only( + left: widget.tooltipPadding?.left ?? + 0, + right: + widget.tooltipPadding?.right ?? + 0, + ), + tooltipActionConfig: + widget.tooltipActionConfig, + alignment: widget + .tooltipActionConfig.alignment, + width: _tooltipActionSize == null + ? null + : tooltipWidth, + crossAxisAlignment: widget + .tooltipActionConfig + .crossAxisAlignment, + isArrowUp: true, + children: widget.tooltipActions, + ), + ], ), ), - ], - ), - if (isArrowUp && - widget.tooltipActionConfig.position == - TooltipActionPosition.outside && - tooltipActionsList.isNotEmpty) ...[ - SizedBox( - height: widget.tooltipActionConfig - .gapBetweenContentAndAction, ), - Container( - width: tooltipAdaptiveWidth, - padding: widget.tooltipActionConfig.padding ?? - EdgeInsets.zero, - child: Row( - mainAxisAlignment: - tooltipActionButtonAlignment, - children: tooltipActionsList, - ), - ), - ] - ], - ), + ), + ), + ], ), ), - ), + if (widget.tooltipActions.isNotEmpty && + widget.tooltipActionConfig.position.isOutside && + isArrowUp || + (_tooltipActionSize == null && isArrowUp)) + ActionWidget( + alignment: widget.tooltipActionConfig.alignment, + tooltipActionConfig: widget.tooltipActionConfig, + isArrowUp: isArrowUp, + width: _tooltipActionSize == null ? null : tooltipWidth, + crossAxisAlignment: + widget.tooltipActionConfig.crossAxisAlignment, + outSidePadding: _tooltipActionSize == null + ? EdgeInsets.only( + left: widget.tooltipPadding?.left ?? 0, + right: widget.tooltipPadding?.right ?? 0, + ) + : EdgeInsets.zero, + children: widget.tooltipActions, + ), + ], ), ), ), ), - ); - } + ), + ); } - if (isOffstage) { - return tooltipActionWidget; - } else { - return Stack( - children: [ - Positioned( - left: _getSpace(), - top: contentY - (10 * contentOffsetMultiplier), - child: ScaleTransition( - scale: _scaleAnimation, - alignment: widget.scaleAnimationAlignment ?? - Alignment( - _getAlignmentX(), - _getAlignmentY(), - ), - child: FractionalTranslation( - translation: Offset(0.0, contentFractionalOffset as double), - child: ToolTipSlideTransition( - position: Tween( - begin: Offset.zero, - end: Offset( - 0, - widget.toolTipSlideEndDistance * contentOffsetMultiplier, + return Stack( + children: [ + 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, + ), + ).animate(_movingAnimation), + child: Material( + color: Colors.transparent, + child: GestureDetector( + onTap: widget.onTooltipTap, + child: Container( + padding: EdgeInsets.only( + top: paddingTop, + bottom: paddingBottom, ), - ).animate(_movingAnimation), - child: Material( color: Colors.transparent, - child: GestureDetector( - onTap: widget.onTooltipTap, - child: Container( - padding: EdgeInsets.only( - top: paddingTop, - bottom: paddingBottom, - ), - width: widget.contentWidth, - color: Colors.transparent, - child: Center( - child: MeasureSize( - onSizeChange: onSizeChange, - child: Column( - children: [ - widget.container!, - if (tooltipActionsList.isNotEmpty) - SizedBox( - height: widget.tooltipActionConfig - .gapBetweenContentAndAction, + child: Center( + child: Column( + children: [ + Stack( + children: [ + // This widget is used for calculation of the action + // widget size and it will be removed once the size + // is calculated + // We have kept it in colum because if we put is + // outside in the stack then it will take whole + // screen size and width calculation will fail + if (isSizeRecalculating) + Offstage( + child: ActionWidget( + key: _actionWidgetKey, + tooltipActionConfig: + widget.tooltipActionConfig, + alignment: + widget.tooltipActionConfig.alignment, + crossAxisAlignment: widget + .tooltipActionConfig.crossAxisAlignment, + width: null, + isArrowUp: isArrowUp, + children: widget.tooltipActions, ), - SizedBox( - width: max(widget.contentWidth ?? 0, - tooltipActionSize?.width ?? 0), - child: (tooltipActionsList.isNotEmpty) - ? SizedBox( - width: tooltipAdaptiveWidth, - child: Padding( - padding: widget.tooltipActionConfig - .padding ?? - EdgeInsets.zero, - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: - tooltipActionButtonAlignment, - children: tooltipActionsList, - ), - ), - ) - : const SizedBox.shrink(), - ) - ], - ), - ), - ), + ), + // This offset is used to make animation smoother + // when there is big action widget which make + // the tool tip to change it's position + Offstage( + offstage: _tooltipActionSize == null, + child: Column( + children: [ + if (widget.tooltipActions.isNotEmpty && + !isArrowUp) + ActionWidget( + tooltipActionConfig: + widget.tooltipActionConfig, + alignment: widget + .tooltipActionConfig.alignment, + crossAxisAlignment: widget + .tooltipActionConfig + .crossAxisAlignment, + width: _tooltipActionSize == null + ? null + : tooltipWidth, + isArrowUp: isArrowUp, + children: widget.tooltipActions, + ), + MeasureSize( + onSizeChange: onSizeChange, + child: widget.container, + ), + if (widget.tooltipActions.isNotEmpty && + isArrowUp) + ActionWidget( + alignment: widget + .tooltipActionConfig.alignment, + tooltipActionConfig: + widget.tooltipActionConfig, + width: _tooltipActionSize == null + ? null + : tooltipWidth, + crossAxisAlignment: widget + .tooltipActionConfig + .crossAxisAlignment, + isArrowUp: isArrowUp, + children: widget.tooltipActions, + ), + ], + ), + ), + ], + ) + ], ), ), ), @@ -716,48 +823,9 @@ class _ToolTipWidgetState extends State ), ), ), - ], - ); - } - } - - double? _getTooltipAdaptiveWidth() => tooltipActionSize == null - ? null - : max( - tooltipWidth, - tooltipActionSize!.width + - (widget.tooltipActionConfig.position == - TooltipActionPosition.inside - ? (widget.tooltipPadding?.left ?? 0) + - (widget.tooltipPadding?.right ?? 0) - : 0), - ); - - List _getActionWidgets() { - List actions = []; - for (var i = 0; i < (widget.tooltipActions?.length ?? 0); i++) { - if ((widget.showCaseState.activeWidgetId == 0 && - !widget.tooltipActions![i].shouldShowForFirstTooltip) || - (widget.showCaseState.activeWidgetId == - (widget.showCaseState.ids?.length ?? 0) - 1 && - !widget.tooltipActions![i].shouldShowForLastTooltip)) { - continue; - } - actions.add( - Padding( - padding: EdgeInsets.only( - right: i < widget.tooltipActions!.length - 1 - ? widget.tooltipActionConfig.actionGap - : 0, - ), - child: TooltipActionButtonWidget( - config: widget.tooltipActions![i], - showCaseState: widget.showCaseState, - ), ), - ); - } - return actions; + ], + ); } void onSizeChange(Size? size) { @@ -766,41 +834,44 @@ class _ToolTipWidgetState extends State setState(() => position = tempPos); } - Size _textSize(String text, TextStyle style) { + Size? _textSize(String? text, TextStyle style, EdgeInsets? padding) { + if (text == null) { + return null; + } + + final availableSpaceForText = + (widget.position?.screenWidth ?? MediaQuery.of(context).size.width) - + (padding ?? EdgeInsets.zero).horizontal - + (widget.tooltipPadding ?? EdgeInsets.zero).horizontal - + (2 * widget.toolTipMargin); + final textPainter = TextPainter( text: TextSpan(text: text, style: style), - maxLines: 1, + // TODO: replace this once we support sdk v3.12. // ignore: deprecated_member_use textScaleFactor: MediaQuery.of(context).textScaleFactor, textDirection: TextDirection.ltr, - )..layout(); + textWidthBasis: TextWidthBasis.longestLine, + )..layout( + // This is used to make maintain the text in available space so height + // and width calculation will be accurate + maxWidth: availableSpaceForText, + ); return textPainter.size; } double? _getArrowLeft(double arrowWidth) { final left = _getLeft(); if (left == null) return null; - return (widget.position!.getCenter() - (arrowWidth * 0.5) - left); + return (widget.position!.getCenter() - (arrowWidth / 2) - left); } double? _getArrowRight(double arrowWidth) { if (_getLeft() != null) return null; return (widget.screenSize.width - widget.position!.getCenter()) - (_getRight() ?? 0) - - (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; - }); + (arrowWidth / 2); } } @@ -823,24 +894,21 @@ 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 * 0.5, 0) + ..lineTo(x / 2, 0) ..lineTo(x, y) ..lineTo(0, y); } return Path() ..moveTo(0, 0) ..lineTo(x, 0) - ..lineTo(x * 0.5, y) + ..lineTo(x / 2, y) ..lineTo(0, 0); } diff --git a/lib/src/widget/action_widget.dart b/lib/src/widget/action_widget.dart new file mode 100644 index 00000000..7a4b8e71 --- /dev/null +++ b/lib/src/widget/action_widget.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +import '../../showcaseview.dart'; + +class ActionWidget extends StatelessWidget { + const ActionWidget({ + super.key, + required this.children, + required this.tooltipActionConfig, + required this.alignment, + required this.crossAxisAlignment, + required this.isArrowUp, + this.outSidePadding = EdgeInsets.zero, + this.width, + }); + + final TooltipActionConfig tooltipActionConfig; + final List children; + final double? width; + final MainAxisAlignment alignment; + final CrossAxisAlignment crossAxisAlignment; + final EdgeInsets outSidePadding; + final bool isArrowUp; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: width, + child: Padding( + padding: EdgeInsets.only( + top: isArrowUp ? tooltipActionConfig.gapBetweenContentAndAction : 0.0, + bottom: + !isArrowUp ? tooltipActionConfig.gapBetweenContentAndAction : 0.0, + ).add(outSidePadding), + child: Row( + mainAxisSize: width == null ? MainAxisSize.min : MainAxisSize.max, + mainAxisAlignment: + width == null ? MainAxisAlignment.start : alignment, + crossAxisAlignment: crossAxisAlignment, + children: children, + ), + ), + ); + } +}