diff --git a/CHANGELOG.md b/CHANGELOG.md index dbf526d5..503dff5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ -## [3.0.1] - +## [4.0.0] +- Feature ✨: Added Action widget for tooltip - Feature [#475](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/475) - Add feasibility to change margin of tooltip with `toolTipMargin`. @@ -9,7 +9,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..89b72965 100644 --- a/README.md +++ b/README.md @@ -136,91 +136,90 @@ 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 | ✅ | ✅ | - -## Properties of `TooltipActionButton.withDefault` and `TooltipActionButton.custom`: - -| Name | Type | Default Behaviour | Description | `TooltipActionButton.withDefault` | `TooltipActionButton.custom` | -|---------------------------|---------------------|--------------------------------------------------|------------------------------------------------|-----------------------------------|------------------------------| -| button | Widget | | Provide custom tooltip action button widget | | ✅ | -| type | TooltipActionButton | | Type of action button (next, skip, previous) | ✅ | | -| backgroundColor | Color? | | Give background color to action button | ✅ | | -| borderRadius | BorderRadius? | BorderRadius.all(Radius.circular(50)) | Give border radius to action button | ✅ | | -| textStyle | TextStyle? | | Give text styles to the name of button | ✅ | | -| padding | EdgeInsets? | EdgeInsets.symmetric(horizontal: 15,vertical: 4) | Give padding to button content | ✅ | | -| leadIcon | ActionButtonIcon? | | Add icon at first before name in action button | ✅ | | -| tailIcon | ActionButtonIcon? | | Add icon at last after name in action button | ✅ | | -| name | String? | | Action button name | ✅ | | -| onTap | VoidCallback? | | Triggers when action button is tapped | ✅ | | -| borderWidth | double? | 0 | Give border width to action button | ✅ | | -| borderColor | Color? | Colors.white | Give color to the border of action button | ✅ | | -| shouldShowForFirstTooltip | bool? | true | Should show button on first tooltip | ✅ | | -| shouldShowForLastTooltip | bool? | true | Should show button on last tooltip | ✅ | | +| 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` and `TooltipActionButton.custom`: + +| Name | Type | Default Behaviour | Description | `TooltipActionButton` | `TooltipActionButton.custom` | +|-----------------------------|---------------------|--------------------------------------------------|------------------------------------------------------------|-----------------------|------------------------------| +| button | Widget | | Provide custom tooltip action button widget | | ✅ | +| type | TooltipActionButton | | Type of action button (next, skip, previous) | ✅ | | +| backgroundColor | Color? | | Give background color to action button | ✅ | | +| borderRadius | BorderRadius? | BorderRadius.all(Radius.circular(50)) | Give border radius to action button | ✅ | | +| textStyle | TextStyle? | | Give text styles to the name of button | ✅ | | +| padding | EdgeInsets? | EdgeInsets.symmetric(horizontal: 15,vertical: 4) | Give padding to button content | ✅ | | +| leadIcon | ActionButtonIcon? | | Add icon at first before name in action button | ✅ | | +| tailIcon | ActionButtonIcon? | | Add icon at last after name in action button | ✅ | | +| name | String? | | Action button name | ✅ | | +| onTap | VoidCallback? | | Triggers when action button is tapped | ✅ | | +| border | Border? | | Give border custom border to the action widget | ✅ | | +| hideActionWidgetForShowcase | List | [] | Hide This action widget for provided list of showcase keys | ✅ | | ## 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 | - -## 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 | +|----------------------------|------------------------|:-------------------------------|---------------------------------------------------| +| alignment | MainAxisAlignment | MainAxisAlignment.spaceBetween | Horizontal Alignment of tooltip action buttons | +| crossAxisAlignment | CrossAxisAlignment | CrossAxisAlignment.start | Vertical Alignment of tooltip action buttons | +| 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` and `ActionButtonIcon.withImageIcon`: + +| Name | Type | Default Behaviour | Description | `ActionButtonIcon` | `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/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..18c726a3 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,500 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 0910; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = S8QB4VV633; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.simform.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 4.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.simform.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_SWIFT3_OBJC_INFERENCE = On; + SWIFT_VERSION = 4.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.simform.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_SWIFT3_OBJC_INFERENCE = On; + SWIFT_VERSION = 4.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 7d3a954b..f877e304 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -7,6 +7,12 @@ import 'package:showcaseview/showcaseview.dart'; void main() => runApp(const MyApp()); +/// Global key for the first showcase widget +final GlobalKey _firstShowcaseWidget = GlobalKey(); + +/// Global key for the last showcase widget +final GlobalKey _lastShowcaseWidget = GlobalKey(); + class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @@ -39,8 +45,29 @@ class MyApp extends StatelessWidget { builder: (context) => const MailPage(), globalTooltipActionConfig: const TooltipActionConfig( position: TooltipActionPosition.inside, - alignment: TooltipActionAlignment.spread, + alignment: MainAxisAlignment.spaceBetween, + actionGap: 20, ), + globalTooltipActions: [ + // Here we don't need previous action for the first showcase widget + // so we hide this action for the first showcase widget + TooltipActionButton( + type: TooltipDefaultActionType.previous, + textStyle: const TextStyle( + color: Colors.white, + ), + hideActionWidgetForShowcase: [_firstShowcaseWidget], + ), + // Here we don't need next action for the last showcase widget so we + // hide this action for the last showcase widget + TooltipActionButton( + type: TooltipDefaultActionType.next, + textStyle: const TextStyle( + color: Colors.white, + ), + hideActionWidgetForShowcase: [_lastShowcaseWidget], + ), + ], ), ), ); @@ -55,11 +82,9 @@ class MailPage extends StatefulWidget { } class _MailPageState extends State { - final GlobalKey _one = GlobalKey(); final GlobalKey _two = GlobalKey(); final GlobalKey _three = GlobalKey(); final GlobalKey _four = GlobalKey(); - final GlobalKey _five = GlobalKey(); List mails = []; final scrollController = ScrollController(); @@ -69,8 +94,8 @@ class _MailPageState extends State { super.initState(); //Start showcase view after current widget frames are drawn. WidgetsBinding.instance.addPostFrameCallback( - (_) => ShowCaseWidget.of(context) - .startShowCase([_one, _two, _three, _four, _five]), + (_) => ShowCaseWidget.of(context).startShowCase( + [_firstShowcaseWidget, _two, _three, _four, _lastShowcaseWidget]), ); mails = [ Mail( @@ -180,21 +205,16 @@ class _MailPageState extends State { child: Row( children: [ Showcase( - key: _one, + key: _firstShowcaseWidget, description: 'Tap to see menu options', onBarrierClick: () => debugPrint('Barrier clicked'), tooltipActionConfig: const TooltipActionConfig( - alignment: TooltipActionAlignment.right, + alignment: MainAxisAlignment.end, position: TooltipActionPosition.outside, gapBetweenContentAndAction: 10, ), - tooltipActions: [ - TooltipActionButton.withDefault( - type: TooltipDefaultActionType.next, - ), - ], child: GestureDetector( onTap: () => debugPrint('menu button clicked'), @@ -237,19 +257,27 @@ class _MailPageState extends State { tooltipBackgroundColor: Theme.of(context).primaryColor, textColor: Colors.white, targetShapeBorder: const CircleBorder(), - tooltipActions: [ - TooltipActionButton.withDefault( + tooltipActionConfig: const TooltipActionConfig( + alignment: MainAxisAlignment.spaceBetween, + gapBetweenContentAndAction: 10, + position: TooltipActionPosition.outside, + ), + tooltipActions: const [ + TooltipActionButton( backgroundColor: Colors.transparent, type: TooltipDefaultActionType.previous, padding: EdgeInsets.zero, + textStyle: TextStyle( + color: Colors.white, + ), ), - TooltipActionButton.withDefault( + TooltipActionButton( type: TooltipDefaultActionType.next, backgroundColor: Colors.white, - textStyle: const TextStyle( + textStyle: TextStyle( color: Colors.pinkAccent, ), - ) + ), ], child: Container( padding: const EdgeInsets.all(5), @@ -302,17 +330,13 @@ class _MailPageState extends State { ), ), floatingActionButton: Showcase( - key: _five, + key: _lastShowcaseWidget, title: 'Compose Mail', description: 'Click here to compose mail', targetShapeBorder: const CircleBorder(), showArrow: false, - tooltipActionConfig: const TooltipActionConfig( - alignment: TooltipActionAlignment.spread, - actionGap: 15, - ), tooltipActions: [ - TooltipActionButton.withDefault( + TooltipActionButton( type: TooltipDefaultActionType.previous, name: 'Back', onTap: () { @@ -323,20 +347,19 @@ class _MailPageState extends State { textStyle: const TextStyle( color: Colors.pink, )), - TooltipActionButton.withDefault( - type: TooltipDefaultActionType.next, + const TooltipActionButton( + type: TooltipDefaultActionType.skip, name: 'Close', - tailIcon: const ActionButtonIcon.withIcon( + textStyle: TextStyle( + color: Colors.white, + ), + tailIcon: ActionButtonIcon( icon: Icon( Icons.close, color: Colors.white, size: 15, ), ), - onTap: () { - // Write your code on button tap - ShowCaseWidget.of(context).next(); - }, ), ], child: FloatingActionButton( @@ -347,8 +370,13 @@ class _MailPageState extends State { * currently rendered so the showcased keys are available in the * render tree. */ scrollController.jumpTo(0); - ShowCaseWidget.of(context) - .startShowCase([_one, _two, _three, _four, _five]); + ShowCaseWidget.of(context).startShowCase([ + _firstShowcaseWidget, + _two, + _three, + _four, + _lastShowcaseWidget + ]); }); }, child: const Icon( @@ -375,7 +403,6 @@ class _MailPageState extends State { child: Showcase( key: key, description: 'Tap to check mail', - tooltipPosition: TooltipPosition.top, disposeOnTap: true, onTargetClick: () { Navigator.push( @@ -385,10 +412,45 @@ class _MailPageState extends State { ), ).then((_) { setState(() { - ShowCaseWidget.of(context).startShowCase([_four, _five]); + ShowCaseWidget.of(context) + .startShowCase([_four, _lastShowcaseWidget]); }); }); }, + tooltipActionConfig: const TooltipActionConfig( + alignment: MainAxisAlignment.spaceBetween, + actionGap: 16, + position: TooltipActionPosition.outside, + gapBetweenContentAndAction: 16, + ), + tooltipActions: [ + TooltipActionButton( + 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, + ), + ), + const TooltipActionButton( + type: TooltipDefaultActionType.skip, + name: 'Close', + textStyle: TextStyle( + color: Colors.white, + ), + tailIcon: ActionButtonIcon( + icon: Icon( + Icons.close, + color: Colors.white, + size: 15, + ), + ), + ), + ], child: MailTile( mail: mail, showCaseKey: _four, @@ -475,6 +537,43 @@ class MailTile extends StatelessWidget { key: showCaseKey!, height: 50, width: 140, + tooltipActionConfig: const TooltipActionConfig( + alignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + actionGap: 16, + ), + tooltipActions: [ + const TooltipActionButton( + backgroundColor: Colors.transparent, + type: TooltipDefaultActionType.previous, + padding: EdgeInsets.zero, + textStyle: TextStyle( + color: Colors.white, + ), + ), + TooltipActionButton.custom( + button: InkWell( + onTap: () => ShowCaseWidget.of(context).next(), + child: Container( + decoration: BoxDecoration( + color: Colors.pink, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.white, + width: 2, + ), + ), + padding: const EdgeInsets.all(8), + child: const Text( + 'Next', + style: TextStyle( + color: Colors.white, + ), + ), + ), + ), + ), + ], targetShapeBorder: const CircleBorder(), targetBorderRadius: const BorderRadius.all( Radius.circular(150), diff --git a/lib/src/enum.dart b/lib/src/enum.dart index 5e3fcae3..1c76b798 100644 --- a/lib/src/enum.dart +++ b/lib/src/enum.dart @@ -20,21 +20,17 @@ * SOFTWARE. */ -import 'package:flutter/cupertino.dart'; +import 'showcase_widget.dart'; enum TooltipPosition { top, bottom } -enum TooltipActionPosition { outside, inside } +enum TooltipActionPosition { + outside, + inside; -enum TooltipActionAlignment { - left(MainAxisAlignment.start), - right(MainAxisAlignment.end), - spread(MainAxisAlignment.spaceBetween), - center(MainAxisAlignment.center); + bool get isInside => this == inside; - const TooltipActionAlignment(this.alignment); - - final MainAxisAlignment alignment; + bool get isOutside => this == outside; } enum TooltipDefaultActionType { @@ -47,4 +43,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/layout_overlays.dart b/lib/src/layout_overlays.dart index 6c14f6fd..d8516b70 100644 --- a/lib/src/layout_overlays.dart +++ b/lib/src/layout_overlays.dart @@ -65,9 +65,7 @@ class AnchoredOverlay extends StatelessWidget { overlayBuilder: (overlayContext) { // To calculate the "anchor" point we grab the render box of // our parent Container and then we find the center of that box. - final box = context.findRenderObject() as RenderBox?; - - if (box == null) return const SizedBox.shrink(); + final box = context.findRenderObject() as RenderBox; final topLeft = box.size.topLeft( box.localToGlobal( diff --git a/lib/src/models/action_button_icon.dart b/lib/src/models/action_button_icon.dart index da067045..0bbe56fd 100644 --- a/lib/src/models/action_button_icon.dart +++ b/lib/src/models/action_button_icon.dart @@ -2,15 +2,15 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class ActionButtonIcon { - const ActionButtonIcon.withIcon({ - required this.icon, + const ActionButtonIcon({ + required Icon this.icon, this.padding, - }) : assert(icon is Icon, 'Icon must be of type Icon'); + }); const ActionButtonIcon.withImageIcon({ - required this.icon, + required ImageIcon this.icon, this.padding, - }) : assert(icon is ImageIcon, 'Icon must be of type ImageIcon'); + }); final Widget icon; final EdgeInsets? padding; diff --git a/lib/src/models/tooltip_action_button.dart b/lib/src/models/tooltip_action_button.dart index 754afa96..5e017d47 100644 --- a/lib/src/models/tooltip_action_button.dart +++ b/lib/src/models/tooltip_action_button.dart @@ -3,27 +3,79 @@ import 'package:flutter/material.dart'; import '../../showcaseview.dart'; class TooltipActionButton { + /// To Provide Background color to the action final Color? backgroundColor; + + /// To Provide borderRadius to the action + /// + /// Defaults to const BorderRadius.all(Radius.circular(50)), final BorderRadius? borderRadius; + + /// To Provide textStyle to the action text final TextStyle? textStyle; + + /// To Provide padding to the action widget + /// + /// Defaults to const EdgeInsets.symmetric(horizontal: 15,vertical: 4,) 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 final ActionButtonIcon? leadIcon; + + /// To Provide a tail icon for the action final ActionButtonIcon? tailIcon; + + /// To Provide a action type final TooltipDefaultActionType? type; + + /// To Provide a text for action + /// + /// If type is provided then it will take type name final String? name; + + /// To Provide a onTap for action + /// + /// If type is provided then it will take type's OnTap final VoidCallback? onTap; - final double? borderWidth; - final Color? borderColor; - final bool shouldShowForFirstTooltip; - final bool shouldShowForLastTooltip; - TooltipActionButton.withDefault({ + /// To Provide a border for action + final Border? border; + + /// Hides action widgets for the showcase. Add key of particular showcase + /// in this list. + /// This only works for the global action widgets + /// Defaults to [] + final List hideActionWidgetForShowcase; + + /// A configuration for a tooltip action button or Provide a custom tooltip action. + /// + /// This class allows you to define predefined actions like "Next," + /// "Previous," and "Close," or specify a custom action widget. + /// + /// **Required arguments:** + /// + /// - `type`: The type of the action button (e.g., `TooltipDefaultActionType.next`). + /// + /// **Optional arguments:** + /// + /// - `backgroundColor`: The background color of the button + /// - `textStyle`: The text style for the button label. + /// - `borderRadius`: The border radius of the button. Defaults to a rounded shape. + /// - `padding`: The padding around the button content. + /// - `leadIcon`: An optional leading icon for the button. + /// - `tailIcon`: An optional trailing icon for the button. + /// - `name`: The text for the button label (ignored if `type` is provided). + /// - `onTap`: A callback function triggered when the button is tapped. + /// - `border`: A border to draw around the button. + /// - `hideActionWidgetForShowcase`: A list of `GlobalKey`s of showcases where this + /// action widget should be hidden. This only works for global action widgets. + const TooltipActionButton({ required this.type, this.backgroundColor, - this.textStyle = const TextStyle( - color: Colors.white, - ), + this.textStyle, this.borderRadius = const BorderRadius.all( Radius.circular(50), ), @@ -35,16 +87,13 @@ class TooltipActionButton { this.tailIcon, this.name, this.onTap, - this.borderColor, - this.borderWidth, - this.shouldShowForFirstTooltip = true, - this.shouldShowForLastTooltip = true, + this.hideActionWidgetForShowcase = const [], + this.border, }) : button = null; - TooltipActionButton.custom({ + const TooltipActionButton.custom({ required this.button, - this.shouldShowForFirstTooltip = true, - this.shouldShowForLastTooltip = true, + this.hideActionWidgetForShowcase = const [], }) : backgroundColor = null, borderRadius = null, textStyle = null, @@ -54,6 +103,5 @@ class TooltipActionButton { type = null, name = null, onTap = null, - borderColor = null, - borderWidth = null; + border = null; } diff --git a/lib/src/models/tooltip_action_config.dart b/lib/src/models/tooltip_action_config.dart index 7c8fcd70..0cda5125 100644 --- a/lib/src/models/tooltip_action_config.dart +++ b/lib/src/models/tooltip_action_config.dart @@ -3,12 +3,17 @@ import 'package:flutter/material.dart'; import '../../showcaseview.dart'; class TooltipActionConfig { + /// Configuration options for tooltip action buttons. + /// + /// This class allows you to configure the overall appearance and layout of + /// action buttons within a tooltip widget. 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. @@ -19,8 +24,8 @@ class TooltipActionConfig { /// Defines the alignment of actions buttons of tooltip action widget /// - /// Default to [TooltipActionAlignment.left] - final TooltipActionAlignment alignment; + /// Default to [MainAxisAlignment.spaceBetween] + final MainAxisAlignment alignment; /// Defines the gap between the actions buttons of tooltip action widget /// @@ -36,4 +41,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..3e9a9c1f 100644 --- a/lib/src/showcase.dart +++ b/lib/src/showcase.dart @@ -25,11 +25,15 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:showcaseview/showcaseview.dart'; +import 'enum.dart'; import 'get_position.dart'; import 'layout_overlays.dart'; +import 'models/tooltip_action_button.dart'; +import 'models/tooltip_action_config.dart'; import 'shape_clipper.dart'; +import 'showcase_widget.dart'; +import 'tooltip_action_button_widget.dart'; import 'tooltip_widget.dart'; class Showcase extends StatefulWidget { @@ -257,7 +261,7 @@ class Showcase extends StatefulWidget { /// Defaults to 14. final double toolTipMargin; - /// Provides tooTip action widgets at bottom in tooltip. + /// Provides toolTip action widgets at bottom in tooltip. /// /// one can use [TooltipActionButton] class to use default action final List? tooltipActions; @@ -268,6 +272,71 @@ class Showcase extends StatefulWidget { /// Default to [const TooltipActionConfig()] final TooltipActionConfig? tooltipActionConfig; + /// Highlights a specific widget on the screen with an informative tooltip. + /// + /// This widget helps you showcase specific parts of your UI by drawing an + /// overlay around it and displaying a description. You can customize the + /// appearance and behavior of the showcase and tooltip for a seamless user + /// experience. + /// + /// **Required arguments:** + /// + /// - `key`: A unique key for this Showcase widget. + /// - `description`: A description of the widget being showcased. + /// - `child`: The widget you want to highlight. + /// + /// **Optional arguments:** + /// + /// **Tooltip:** + /// - `title`: An optional title for the tooltip. + /// - `titleAlignment`: Alignment of the title text within the tooltip (defaults to start). + /// - `descriptionAlignment`: Alignment of the description text within the tooltip (defaults to start). + /// - `titleTextStyle`: Style properties for the title text. + /// - `descTextStyle`: Style properties for the description text. + /// - `tooltipBackgroundColor`: Background color of the tooltip (defaults to white). + /// - `textColor`: Color of the text in the tooltip (defaults to black). + /// - `tooltipPadding`: Padding around the content inside the tooltip. + /// - `onToolTipClick`: A callback function called when the user clicks the tooltip. + /// - `tooltipBorderRadius`: The border radius of the tooltip (defaults to 8dp). + /// + /// **Highlight:** + /// - `targetShapeBorder`: The border to draw around the showcased widget (defaults to a rounded rectangle). + /// - `targetPadding`: Padding around the showcased widget (defaults to none). + /// - `showArrow`: Whether to show an arrow pointing to the showcased widget (defaults to true). + /// + /// **Animations:** + /// - `movingAnimationDuration`: Duration of the animation when moving the tooltip (defaults to 2 seconds). + /// - `disableMovingAnimation`: Disables the animation when moving the tooltip. + /// - `disableScaleAnimation`: Disables the animation when scaling the tooltip. + /// - `scaleAnimationDuration`: Duration of the animation when scaling the tooltip (defaults to 300 milliseconds). + /// - `scaleAnimationCurve`: The curve used for the scaling animation (defaults to ease-in). + /// - `scaleAnimationAlignment`: The alignment point for the scaling animation. + /// + /// **Interactions:** + /// - `onTargetClick`: A callback function called when the user clicks the showcased widget. + /// - `disposeOnTap`: Whether to dispose of the showcase after a tap on the showcased widget (requires `onTargetClick`). + /// - `onTargetLongPress`: A callback function called when the user long-presses the showcased widget. + /// - `onTargetDoubleTap`: A callback function called when the user double-taps the showcased widget. + /// - `disableDefaultTargetGestures`: Disables default gestures on the target widget (panning, zooming). + /// - `onBarrierClick`: A callback function called when the user clicks outside the showcase overlay. + /// - `disableBarrierInteraction`: Disables user interaction with the area outside the showcase overlay. + /// + /// **Advanced:** + /// - `container`: A custom widget to use as the tooltip instead of the default one. + /// - `overlayColor`: Color of the showcase overlay (defaults to black with 75% opacity). + /// - `overlayOpacity`: Opacity of the showcase overlay (0.0 to 1.0). + /// - `scrollLoadingWidget`: A widget to display while content is loading (for infinite scrolling scenarios). + /// - `blurValue`: The amount of background blur applied during the showcase. + /// - `tooltipPosition`: The position of the tooltip relative to the showcased widget. + /// - `toolTipSlideEndDistance`: The distance the tooltip slides in from the edge of the screen (defaults to 7dp). + /// - `toolTipMargin`: The margin around the tooltip (defaults to 14dp). + /// - `tooltipActions`: A list of custom actions (widgets) to display within the tooltip. + /// - `tooltipActionConfig`: Configuration options for custom tooltip actions. + /// + /// **Assertions:** + /// + /// - `overlayOpacity` must be between 0.0 and 1.0. + /// - `onTargetClick` and `disposeOnTap` must be used together (one cannot exist without the other). const Showcase({ required this.key, required this.description, @@ -309,16 +378,16 @@ class Showcase extends StatefulWidget { this.tooltipPosition, this.titlePadding, this.descriptionPadding, - this.tooltipActions, this.titleTextDirection, this.descriptionTextDirection, this.onBarrierClick, this.disableBarrierInteraction = false, this.toolTipSlideEndDistance = 7, this.toolTipMargin = 14, + this.tooltipActions, 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."), @@ -329,6 +398,57 @@ class Showcase extends StatefulWidget { assert(onBarrierClick == null || disableBarrierInteraction == false, "can't use onBarrierClick & disableBarrierInteraction property at same time"); + /// Creates a Showcase widget with a custom tooltip widget. + + /// This constructor allows you to provide a completely custom widget + /// for the tooltip instead of using the default one with title and + /// description. This gives you more flexibility in designing the + /// appearance and behavior of the tooltip. + + /// **Required arguments:** + /// + /// - `key`: A unique key for this Showcase widget. + /// - `height`: The height of the custom tooltip widget. + /// - `width`: The width of the custom tooltip widget. + /// - `container`: The custom widget to use as the tooltip. + /// - `child`: The widget you want to highlight. + + /// **Optional arguments:** + + /// **Highlight:** + /// - `targetShapeBorder`: The border to draw around the showcased widget (defaults to a rounded rectangle). + /// - `targetBorderRadius`: The border radius of the showcased widget. + /// - `overlayColor`: Color of the showcase overlay (defaults to black with 75% opacity). + /// - `overlayOpacity`: Opacity of the showcase overlay (0.0 to 1.0). + /// - `scrollLoadingWidget`: A widget to display while content is loading (for infinite scrolling scenarios). + /// - `onTargetClick`: A callback function called when the user clicks the showcased widget. + /// - `disposeOnTap`: Whether to dispose of the showcase after a tap on the showcased widget (requires `onTargetClick`). + /// - `movingAnimationDuration`: Duration of the animation when moving the tooltip (defaults to 2 seconds). + /// - `disableMovingAnimation`: Disables the animation when moving the tooltip. + /// - `targetPadding`: Padding around the showcased widget (defaults to none). + /// - `blurValue`: The amount of background blur applied during the showcase. + /// - `onTargetLongPress`: A callback function called when the user long-presses the showcased widget. + /// - `onTargetDoubleTap`: A callback function called when the user double-taps the showcased widget. + /// - `disableDefaultTargetGestures`: Disables default gestures on the target widget (panning, zooming). + /// - `tooltipPosition`: The position of the tooltip relative to the showcased widget. + /// - `onBarrierClick`: A callback function called when the user clicks outside the showcase overlay. + /// - `disableBarrierInteraction`: Disables user interaction with the area outside the showcase overlay. + + /// **Advanced:** + /// - `toolTipSlideEndDistance`: The distance the tooltip slides in from the edge of the screen (defaults to 7dp). + /// - `tooltipActions`: A list of custom actions (widgets) to display within the tooltip. + /// - `tooltipActionConfig`: Configuration options for custom tooltip actions. + + /// **Differences from default constructor:** + /// + /// - This constructor doesn't require `title` or `description` arguments. + /// - By default, the tooltip won't have an arrow pointing to the target widget (`showArrow` is set to `false`). + /// - Default click behavior is disabled (`onToolTipClick` is set to `null`). + /// - Default animation settings are slightly different (e.g., `scaleAnimationCurve` is `Curves.decelerate`). + + /// **Assertions:** + /// - `overlayOpacity` must be between 0.0 and 1.0. + /// - `onBarrierClick` cannot be used with `disableBarrierInteraction`. const Showcase.withWidget({ required this.key, required this.height, @@ -655,9 +775,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,20 +783,45 @@ class _ShowcaseState extends State { ); } - List _getTooltipActions() => - (widget.tooltipActions?.isEmpty ?? true) - ? ShowCaseWidget.of(context).globalTooltipActions ?? [] - : widget.tooltipActions ?? []; + List _getTooltipActions() { + final actionData = (widget.tooltipActions?.isNotEmpty ?? false) + ? widget.tooltipActions! + : showCaseWidgetState.globalTooltipActions ?? []; + + final actionWidgets = []; + for (final action in actionData) { + /// This checks that if current widget is being showcased and there is + /// no local action has been provided and global action are needed to hide + /// then it will hide that action for current widget + if (_showShowCase && + action.hideActionWidgetForShowcase.contains(widget.key) && + (widget.tooltipActions?.isEmpty ?? true)) { + continue; + } + actionWidgets.add( + Padding( + padding: EdgeInsetsDirectional.only( + end: action != actionData.last + ? _getTooltipActionConfig().actionGap + : 0, + ), + child: TooltipActionButtonWidget( + config: action, + // We have to pass showcaseState from here because + // [TooltipActionButtonWidget] is not direct child of showcaseWidget + // so it won't be able to get the state by using it's context + showCaseState: showCaseWidgetState, + ), + ), + ); + } + return actionWidgets; + } TooltipActionConfig _getTooltipActionConfig() { - final showCaseState = ShowCaseWidget.of(context); - if (widget.tooltipActionConfig != null) { - return widget.tooltipActionConfig!; - } else if (showCaseState.globalTooltipActionConfig != null) { - return showCaseState.globalTooltipActionConfig!; - } else { - return const TooltipActionConfig(); - } + return widget.tooltipActionConfig ?? + showCaseWidgetState.globalTooltipActionConfig ?? + const TooltipActionConfig(); } } diff --git a/lib/src/showcase_widget.dart b/lib/src/showcase_widget.dart index 00a91ec1..c01d9109 100644 --- a/lib/src/showcase_widget.dart +++ b/lib/src/showcase_widget.dart @@ -83,9 +83,38 @@ 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; + /// A widget that manages multiple Showcase widgets. + /// + /// This widget provides a way to sequentially showcase multiple widgets + /// with customizable options like auto-play, animation, and user interaction. + /// + /// **Required arguments:** + /// + /// - `builder`: A builder function that returns a widget containing the `Showcase` widgets to be showcased. + /// + /// **Optional arguments:** + /// + /// - `onFinish`: A callback function triggered when all showcases are completed. + /// - `onStart`: A callback function triggered at the start of each showcase, providing the index and key of the target widget. + /// - `onComplete`: A callback function triggered at the completion of each showcase, providing the index and key of the target widget. + /// - `autoPlay`: Whether to automatically start showcasing the next widget after a delay (defaults to `false`). + /// - `autoPlayDelay`: The delay between each showcase during auto-play (defaults to 2 seconds). + /// - `enableAutoPlayLock`: Whether to block user interaction while auto-play is enabled (defaults to `false`). + /// - `blurValue`: The amount of background blur applied during the showcase (defaults to 0). + /// - `scrollDuration`: The duration of the scrolling animation when auto-scrolling to a target widget (defaults to 300 milliseconds). + /// - `disableMovingAnimation`: Disables the animation when moving the tooltip for all showcases (defaults to `false`). + /// - `disableScaleAnimation`: Disables the initial scale animation for all tooltips (defaults to `false`). + /// - `enableAutoScroll`: Enables automatic scrolling to bring the target widget into view (defaults to `false`). + /// - `disableBarrierInteraction`: Disables user interaction with the area outside the showcase overlay (defaults to `false`). + /// - `enableShowcase`: Enables or disables the showcase functionality globally (defaults to `true`). + /// - `globalTooltipActions`: A list of custom actions to be added to all tooltips. + /// - `globalTooltipActionConfig`: Configuration options for the global tooltip actions. const ShowCaseWidget({ required this.builder, this.onFinish, @@ -132,6 +161,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..59bfad68 100644 --- a/lib/src/tooltip_action_button_widget.dart +++ b/lib/src/tooltip_action_button_widget.dart @@ -9,7 +9,11 @@ class TooltipActionButtonWidget extends StatelessWidget { required this.showCaseState, }); + /// This will provide the configuration for the action buttons final TooltipActionButton config; + + /// This is used for [TooltipActionButton] to close, next and previous + /// showcase navigation final ShowCaseWidgetState showCaseState; @override @@ -24,15 +28,10 @@ class TooltipActionButtonWidget extends StatelessWidget { decoration: BoxDecoration( color: config.backgroundColor ?? theme.primaryColor, borderRadius: config.borderRadius, - border: Border.all( - color: config.borderColor ?? - config.backgroundColor ?? - theme.primaryColor, - width: config.borderWidth ?? 0, - ), + border: config.border, ), child: Row( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ if (config.leadIcon != null) Padding( @@ -60,19 +59,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..d0d7a7bb 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,26 @@ 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; - void setTooltipActionWidth(size) => tooltipActionSize ??= size; + // To store Tooltip action size + Size? _tooltipActionSize; + + final zeroPadding = EdgeInsets.zero; + // 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 +160,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 && renderBox.hasSize) { + 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 (_tooltipActionSize != null) { + return; + } + } + } final titleStyle = widget.titleTextStyle ?? Theme.of(context) .textTheme @@ -171,26 +201,74 @@ 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 = titleSize?.width ?? 0; + final descriptionLength = descriptionSize?.width ?? 0; + // This is padding we will have around the tooltip text + final textPadding = (widget.tooltipPadding ?? zeroPadding).horizontal + + max((widget.titlePadding ?? zeroPadding).horizontal, + (widget.descriptionPadding ?? zeroPadding).horizontal); + + final maxTextWidth = max(titleLength, descriptionLength) + textPadding; + var maxToolTipWidth = max(toolTipActionSize?.width ?? 0, maxTextWidth); + + final availableSpaceForToolTip = + widget.screenSize.width - (2 * widget.toolTipMargin); + + // if Width is greater than available size which won't happen we will + // adjust it to stay in available size + if (maxToolTipWidth > availableSpaceForToolTip) { + tooltipWidth = availableSpaceForToolTip; } else { - tooltipWidth = maxTextWidth + 0; + // 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) >= maxTextWidth) { + tooltipWidth = toolTipActionSize?.width ?? 0; + } else { + tooltipWidth = maxToolTipWidth; + } } + + // 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; + // 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 + + // 16 for the extra space so it won't stick to any side + + tooltipHeight = (widget.tooltipPadding ?? zeroPadding).vertical + + (titleSize?.height ?? 0) + + (descriptionSize?.height ?? 0) + + arrowHeight + + widget.toolTipSlideEndDistance + + (toolTipActionSize?.height ?? + widget.tooltipActionConfig.gapBetweenContentAndAction) + + (widget.contentHeight ?? 0) + + 16; } double? _getLeft() { @@ -229,10 +307,10 @@ class _ToolTipWidgetState extends State } double _getSpace() { - var space = widget.position!.getCenter() - (widget.contentWidth! * 0.5); - if (space + widget.contentWidth! > widget.screenSize.width) { - space = widget.screenSize.width - widget.contentWidth! - 8; - } else if (space < (widget.contentWidth! * 0.5)) { + var space = widget.position!.getCenter() - (tooltipWidth / 2); + if (space + tooltipWidth > widget.screenSize.width) { + space = widget.screenSize.width - tooltipWidth - 8; + } else if (space < (tooltipWidth / 2)) { space = 16; } return space; @@ -260,13 +338,13 @@ class _ToolTipWidgetState extends State double _getAlignmentY() => -1; 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 +405,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 +437,28 @@ class _ToolTipWidgetState extends State contentOrientation == TooltipPosition.bottom ? 1.0 : -1.0; isArrowUp = contentOffsetMultiplier == 1.0; - final contentY = isArrowUp + final screenSize = MediaQuery.of(context).size; + + 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 >= screenSize.height && isArrowUp) { + contentY = screenSize.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 +468,242 @@ 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) + _getActionWidget(), + Padding( padding: widget.showArrow ? EdgeInsets.only( top: paddingTop - (isArrowUp ? arrowHeight : 0), bottom: paddingBottom - (isArrowUp ? 0 : arrowHeight), ) - : EdgeInsets.symmetric( - vertical: paddingTop, - ), - 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, + : zeroPadding, + 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) _getOffstageActionWidget, + 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 ?? + zeroPadding) + .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, + ), + ), + ), + ), + if (widget.description != null) + Padding( + padding: (widget.descriptionPadding ?? + zeroPadding) + .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, + ), ), - ), - ), - 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, - ), - ), - ] - ], ), ), - ), - ), + if (widget.tooltipActions.isNotEmpty && + widget.tooltipActionConfig.position + .isInside && + _tooltipActionSize != null) + _getActionWidget(insideWidget: true), + ], ), ), - ], - ), - 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)) + _getActionWidget(), + ], ), ), ), ), - ); - } + ), + ); } - 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: 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) _getOffstageActionWidget, + + // 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: SizedBox( + width: tooltipWidth, + child: Column( + children: [ + if (widget.tooltipActions.isNotEmpty && + !isArrowUp) + _getActionWidget(), + MeasureSize( + onSizeChange: onSizeChange, + child: widget.container, ), - 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(), - ) - ], + if (widget.tooltipActions.isNotEmpty && + isArrowUp) + _getActionWidget(), + ], + ), ), ), - ), + ], ), ), ), @@ -716,91 +711,98 @@ 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, - ), + Widget get _getOffstageActionWidget => Offstage( + child: ActionWidget( + key: _actionWidgetKey, + outSidePadding: widget.tooltipActionConfig.position.isInside && + widget.container == null + ? EdgeInsets.only( + left: widget.tooltipPadding?.left ?? 0, + right: widget.tooltipPadding?.right ?? 0, + ) + : zeroPadding, + tooltipActionConfig: widget.tooltipActionConfig, + alignment: widget.tooltipActionConfig.alignment, + width: null, + crossAxisAlignment: widget.tooltipActionConfig.crossAxisAlignment, + isArrowUp: true, + children: widget.tooltipActions, ), ); - } - return actions; + + Widget _getActionWidget({ + bool insideWidget = false, + }) { + return ActionWidget( + tooltipActionConfig: widget.tooltipActionConfig, + outSidePadding: (insideWidget) + ? EdgeInsets.only( + left: widget.tooltipPadding?.left ?? 0, + right: widget.tooltipPadding?.right ?? 0, + ) + : zeroPadding, + alignment: widget.tooltipActionConfig.alignment, + crossAxisAlignment: widget.tooltipActionConfig.crossAxisAlignment, + width: _tooltipActionSize == null ? null : tooltipWidth, + isArrowUp: insideWidget || isArrowUp, + children: widget.tooltipActions, + ); } void onSizeChange(Size? size) { var tempPos = position; - tempPos = Offset(position!.dx, position!.dy + size!.height); - setState(() => position = tempPos); + tempPos = Offset( + position?.dx ?? 0, + position?.dy ?? 0 + (size ?? Size.zero).height, + ); + if (mounted) { + 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 ?? zeroPadding).horizontal - + (widget.tooltipPadding ?? zeroPadding).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 +825,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..03c730a6 --- /dev/null +++ b/lib/src/widget/action_widget.dart @@ -0,0 +1,46 @@ +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) { + final getWidgetPadding = EdgeInsets.only( + top: isArrowUp ? tooltipActionConfig.gapBetweenContentAndAction : 0.0, + bottom: !isArrowUp ? tooltipActionConfig.gapBetweenContentAndAction : 0.0, + ).add(outSidePadding); + + return SizedBox( + width: width, + child: Padding( + padding: getWidgetPadding, + child: Row( + mainAxisSize: width == null ? MainAxisSize.min : MainAxisSize.max, + mainAxisAlignment: + width == null ? MainAxisAlignment.start : alignment, + crossAxisAlignment: crossAxisAlignment, + children: children, + ), + ), + ); + } +}