From 67879cf777b401cd127f530c97ceab108d2b9efe Mon Sep 17 00:00:00 2001 From: Sahil-Simform Date: Fri, 7 Mar 2025 19:00:41 +0530 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=E2=9C=A8=20Added=20Multi=20Showcas?= =?UTF-8?q?e=20functionality=20and=20improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 + README.md | 5 + lib/showcaseview.dart | 2 +- lib/src/enum.dart | 2 +- lib/src/get_position.dart | 26 +- lib/src/layout_overlays.dart | 102 +---- lib/src/models/linked_showcase_data.dart | 17 + lib/src/shape_clipper.dart | 41 ++- lib/src/{ => showcase}/showcase.dart | 348 ++++++++---------- lib/src/showcase/showcase_controller.dart | 47 +++ lib/src/showcase_widget.dart | 238 +++++++++--- lib/src/tooltip/animated_tooltip_layout.dart | 6 +- .../tooltip/render_animation_delegate.dart | 1 + lib/src/tooltip/render_position_delegate.dart | 100 +++-- lib/src/tooltip/tooltip.dart | 1 + lib/src/tooltip/tooltip_widget.dart | 24 +- 16 files changed, 573 insertions(+), 392 deletions(-) create mode 100644 lib/src/models/linked_showcase_data.dart rename lib/src/{ => showcase}/showcase.dart (81%) create mode 100644 lib/src/showcase/showcase_controller.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d0b8da4..8c885a53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ Improved Tooltip widget - Feature [#54](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/54) - Added Feasibility to position tooltip left and right to the target widget. +- Feature [#113](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/113) - Added + multiple showcase feature +- Improvement [#514](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/pull/514) - + Improved showcase widget and showcase with widget, Removed inherited widget, keys and setStates, + Added controller to manage showcase ## [4.0.1] - Fixed [#493](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/493) - ShowCase.withWidget not showing issue diff --git a/README.md b/README.md index c6fd669c..da828eea 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,11 @@ WidgetsBinding.instance.addPostFrameCallback((_) => ); ``` +## MultiShowcaseView +To show multiple showcase at the same time provide same key to showcase. +Note: auto scroll to showcase will not work in case of the multi-showcase and we will use property +of first initialized showcase for common things like barrier tap and colors. + ## Functions of `ShowCaseWidget.of(context)`: | Function Name | Description | diff --git a/lib/showcaseview.dart b/lib/showcaseview.dart index 3d597dd7..2c94bb01 100644 --- a/lib/showcaseview.dart +++ b/lib/showcaseview.dart @@ -26,7 +26,7 @@ export 'src/enum.dart'; export 'src/models/action_button_icon.dart'; export 'src/models/tooltip_action_button.dart'; export 'src/models/tooltip_action_config.dart'; -export 'src/showcase.dart'; +export 'src/showcase/showcase.dart'; export 'src/showcase_widget.dart'; export 'src/tooltip_action_button_widget.dart'; export 'src/widget/floating_action_widget.dart'; diff --git a/lib/src/enum.dart b/lib/src/enum.dart index 84db9b62..1db136c1 100644 --- a/lib/src/enum.dart +++ b/lib/src/enum.dart @@ -94,7 +94,7 @@ enum TooltipDefaultActionType { void onTap(ShowCaseWidgetState showCaseState) { switch (this) { case TooltipDefaultActionType.next: - showCaseState.next(); + showCaseState.next(true); break; case TooltipDefaultActionType.previous: showCaseState.previous(); diff --git a/lib/src/get_position.dart b/lib/src/get_position.dart index 91602527..2616a19e 100644 --- a/lib/src/get_position.dart +++ b/lib/src/get_position.dart @@ -26,7 +26,7 @@ import 'package:flutter/material.dart'; class GetPosition { GetPosition({ - required this.key, + required this.context, required this.screenWidth, required this.screenHeight, this.padding = EdgeInsets.zero, @@ -35,7 +35,7 @@ class GetPosition { getRenderBox(); } - final GlobalKey key; + final BuildContext context; final EdgeInsets padding; final double screenWidth; final double screenHeight; @@ -43,11 +43,15 @@ class GetPosition { late final RenderBox? _box; late final Offset? _boxOffset; + late final Offset? overlayOffset; RenderBox? get box => _box; void getRenderBox() { - var renderBox = key.currentContext?.findRenderObject() as RenderBox?; + var renderBox = context.findRenderObject() as RenderBox?; + + overlayOffset = + (rootRenderObject?.parent as RenderBox?)?.localToGlobal(Offset.zero); if (renderBox == null) return; @@ -72,7 +76,10 @@ class GetPosition { final topLeft = _box!.size.topLeft(_boxOffset!); final bottomRight = _box!.size.bottomRight(_boxOffset!); final leftDx = topLeft.dx - padding.left; - final leftDy = topLeft.dy - padding.top; + var leftDy = topLeft.dy - padding.top; + if (leftDy < 0) { + leftDy = 0; + } final rect = Rect.fromLTRB( leftDx.clamp(0, leftDx), leftDy.clamp(0, leftDy), @@ -123,4 +130,15 @@ class GetPosition { double getWidth() => getRight() - getLeft(); double getCenter() => (getLeft() + getRight()) * 0.5; + + Offset topLeft() => + _box?.size.topLeft( + _box!.localToGlobal( + Offset.zero, + ancestor: rootRenderObject, + ), + ) ?? + Offset.zero; + + Offset getOffSet() => _box?.size.center(topLeft()) ?? Offset.zero; } diff --git a/lib/src/layout_overlays.dart b/lib/src/layout_overlays.dart index e0e57a47..e83ebad8 100644 --- a/lib/src/layout_overlays.dart +++ b/lib/src/layout_overlays.dart @@ -24,93 +24,7 @@ import 'package:flutter/material.dart'; import 'showcase_widget.dart'; -typedef OverlayBuilderCallback = Widget Function( - BuildContext context, - Rect anchorBounds, - Offset anchor, -); - -/// Displays an overlay Widget anchored directly above the center of this -/// [AnchoredOverlay]. -/// -/// The overlay Widget is created by invoking the provided [overlayBuilder]. -/// -/// The [anchor] position is provided to the [overlayBuilder], but the builder -/// does not have to respect it. In other words, the [overlayBuilder] can -/// interpret the meaning of "anchor" however it wants - the overlay will not -/// be forced to be centered about the [anchor]. -/// -/// The overlay built by this [AnchoredOverlay] can be conditionally shown -/// and hidden by settings the [showOverlay] property to true or false. -/// -/// The [overlayBuilder] is invoked every time this Widget is rebuilt. -/// -class AnchoredOverlay extends StatelessWidget { - final bool showOverlay; - final OverlayBuilderCallback? overlayBuilder; - final Widget? child; - final RenderObject? rootRenderObject; - - const AnchoredOverlay({ - super.key, - this.showOverlay = false, - this.overlayBuilder, - this.child, - this.rootRenderObject, - }); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - return OverlayBuilder( - showOverlay: showOverlay, - 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?; - - /// Handle null RenderBox safely. - final topLeft = box?.size.topLeft( - box.localToGlobal( - Offset.zero, - ancestor: rootRenderObject, - ), - ) ?? - Offset.zero; - final bottomRight = box?.size.bottomRight( - box.localToGlobal( - Offset.zero, - ancestor: rootRenderObject, - ), - ) ?? - Offset.zero; - - /// Provide a default anchorBounds if box is null. - final anchorBounds = (topLeft.dx.isNaN || - topLeft.dy.isNaN || - bottomRight.dx.isNaN || - bottomRight.dy.isNaN) - ? const Rect.fromLTRB(0.0, 0.0, 0.0, 0.0) - : Rect.fromLTRB( - topLeft.dx, - topLeft.dy, - bottomRight.dx, - bottomRight.dy, - ); - - /// Calculate the anchor center or default to Offset.zero. - final anchorCenter = box?.size.center(topLeft) ?? Offset.zero; - - /// Pass the anchor details to the overlay builder. - return overlayBuilder!(overlayContext, anchorBounds, anchorCenter); - }, - child: child, - ); - }, - ); - } -} +typedef OverlayUpdateCallback = void Function(VoidCallback updateOverlay); /// Displays an overlay Widget as constructed by the given [overlayBuilder]. /// @@ -128,9 +42,11 @@ class OverlayBuilder extends StatefulWidget { final bool showOverlay; final WidgetBuilder? overlayBuilder; final Widget? child; + final OverlayUpdateCallback updateOverlay; const OverlayBuilder({ super.key, + required this.updateOverlay, this.showOverlay = false, this.overlayBuilder, this.child, @@ -150,12 +66,20 @@ class _OverlayBuilderState extends State { if (widget.showOverlay) { WidgetsBinding.instance.addPostFrameCallback((_) => showOverlay()); } + widget.updateOverlay.call(updateOverlay); + } + + void updateOverlay() { + buildOverlay(); + WidgetsBinding.instance.addPostFrameCallback((_) => syncWidgetAndOverlay()); } @override void didUpdateWidget(OverlayBuilder oldWidget) { super.didUpdateWidget(oldWidget); - WidgetsBinding.instance.addPostFrameCallback((_) => syncWidgetAndOverlay()); + if (oldWidget.showOverlay != widget.showOverlay && widget.showOverlay) { + WidgetsBinding.instance.addPostFrameCallback((_) => showOverlay()); + } } @override @@ -221,8 +145,6 @@ class _OverlayBuilderState extends State { @override Widget build(BuildContext context) { - buildOverlay(); - return widget.child!; } } diff --git a/lib/src/models/linked_showcase_data.dart b/lib/src/models/linked_showcase_data.dart new file mode 100644 index 00000000..a84d88fc --- /dev/null +++ b/lib/src/models/linked_showcase_data.dart @@ -0,0 +1,17 @@ +import 'package:flutter/widgets.dart'; + +/// This model is used to move linked showcase overlay data to parent +/// showcase to crop linked showcase rect +class LinkedShowcaseDataModel { + final Rect rect; + final EdgeInsets overlayPadding; + final BorderRadius? radius; + final bool isCircle; + + const LinkedShowcaseDataModel({ + required this.rect, + required this.radius, + required this.overlayPadding, + required this.isCircle, + }); +} diff --git a/lib/src/shape_clipper.dart b/lib/src/shape_clipper.dart index 2017576d..693feff4 100644 --- a/lib/src/shape_clipper.dart +++ b/lib/src/shape_clipper.dart @@ -24,17 +24,21 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; +import 'models/linked_showcase_data.dart'; + class RRectClipper extends CustomClipper { final bool isCircle; final BorderRadius? radius; final EdgeInsets overlayPadding; final Rect area; + final List linkedObjectData; RRectClipper({ this.isCircle = false, this.radius, this.overlayPadding = EdgeInsets.zero, this.area = Rect.zero, + this.linkedObjectData = const [], }); @override @@ -49,7 +53,7 @@ class RRectClipper extends CustomClipper { area.bottom + overlayPadding.bottom, ); - return Path() + var mainObjectPath = Path() ..fillType = ui.PathFillType.evenOdd ..addRect(Offset.zero & size) ..addRRect( @@ -61,6 +65,38 @@ class RRectClipper extends CustomClipper { bottomRight: (radius?.bottomRight ?? customRadius), ), ); + + for (final widgetRect in linkedObjectData) { + final customRadius = widgetRect.isCircle + ? Radius.circular(widgetRect.rect.height) + : const Radius.circular(3.0); + + final rect = Rect.fromLTRB( + widgetRect.rect.left - widgetRect.overlayPadding.left, + widgetRect.rect.top - widgetRect.overlayPadding.top, + widgetRect.rect.right + widgetRect.overlayPadding.right, + widgetRect.rect.bottom + widgetRect.overlayPadding.bottom, + ); + + /// We have use this approach so that overlapping cutout will merge with + /// each other + mainObjectPath = Path.combine( + PathOperation.difference, + mainObjectPath, + Path() + ..addRRect( + RRect.fromRectAndCorners( + rect, + topLeft: (widgetRect.radius?.topLeft ?? customRadius), + topRight: (widgetRect.radius?.topRight ?? customRadius), + bottomLeft: (widgetRect.radius?.bottomLeft ?? customRadius), + bottomRight: (widgetRect.radius?.bottomRight ?? customRadius), + ), + ), + ); + } + + return mainObjectPath; } @override @@ -68,5 +104,6 @@ class RRectClipper extends CustomClipper { isCircle != oldClipper.isCircle || radius != oldClipper.radius || overlayPadding != oldClipper.overlayPadding || - area != oldClipper.area; + area != oldClipper.area || + linkedObjectData != oldClipper.linkedObjectData; } diff --git a/lib/src/showcase.dart b/lib/src/showcase/showcase.dart similarity index 81% rename from lib/src/showcase.dart rename to lib/src/showcase/showcase.dart index e6a19701..218fe134 100644 --- a/lib/src/showcase.dart +++ b/lib/src/showcase/showcase.dart @@ -21,21 +21,20 @@ */ import 'dart:async'; -import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.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/tooltip.dart'; -import 'tooltip_action_button_widget.dart'; -import 'widget/floating_action_widget.dart'; +import '../enum.dart'; +import '../get_position.dart'; +import '../models/linked_showcase_data.dart'; +import '../models/tooltip_action_button.dart'; +import '../models/tooltip_action_config.dart'; +import '../showcase_widget.dart'; +import '../tooltip/tooltip.dart'; +import '../tooltip_action_button_widget.dart'; +import '../widget/floating_action_widget.dart'; +import 'showcase_controller.dart'; class Showcase extends StatefulWidget { /// A key that is unique across the entire app. @@ -43,8 +42,7 @@ class Showcase extends StatefulWidget { /// This Key will be used to control state of individual showcase and also /// used in [ShowCaseWidgetState.startShowCase] to define position of current /// target widget while showcasing. - @override - final GlobalKey key; + final GlobalKey showcaseKey; /// Target widget that will be showcased or highlighted final Widget child; @@ -368,7 +366,7 @@ class Showcase extends StatefulWidget { /// - `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 GlobalKey key, required this.description, required this.child, this.title, @@ -424,6 +422,7 @@ class Showcase extends StatefulWidget { }) : height = null, width = null, container = null, + showcaseKey = key, assert( overlayOpacity >= 0.0 && overlayOpacity <= 1.0, "overlay opacity must be between 0 and 1.", @@ -494,7 +493,7 @@ class Showcase extends StatefulWidget { /// - `overlayOpacity` must be between 0.0 and 1.0. /// - `onBarrierClick` cannot be used with `disableBarrierInteraction`. const Showcase.withWidget({ - required this.key, + required GlobalKey key, required this.height, required this.width, required this.container, @@ -533,7 +532,7 @@ class Showcase extends StatefulWidget { scaleAnimationDuration = const Duration(milliseconds: 300), scaleAnimationCurve = Curves.decelerate, scaleAnimationAlignment = null, - disableScaleAnimation = null, + disableScaleAnimation = true, title = null, description = null, titleTextAlign = TextAlign.start, @@ -551,6 +550,7 @@ class Showcase extends StatefulWidget { titleTextDirection = null, descriptionTextDirection = null, toolTipMargin = 14, + showcaseKey = key, assert( overlayOpacity >= 0.0 && overlayOpacity <= 1.0, "overlay opacity must be between 0 and 1.", @@ -565,155 +565,129 @@ class Showcase extends StatefulWidget { } class _ShowcaseState extends State { - bool _showShowCase = false; - bool _isScrollRunning = false; - bool _isTooltipDismissed = false; - bool _enableShowcase = true; - Timer? timer; + bool enableShowcase = true; GetPosition? position; - Size? rootWidgetSize; - RenderBox? rootRenderObject; + + late ShowcaseController showcaseController; late final showCaseWidgetState = ShowCaseWidget.of(context); FloatingActionWidget? _globalFloatingActionWidget; - @override - void initState() { - super.initState(); - initRootWidget(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _enableShowcase = showCaseWidgetState.enableShowcase; + /// This variable will be true if some other showcase is linked with + /// this showcase and starts this widget showcase + // bool _isLinkedShowCaseStarted = false; - recalculateRootWidgetSize(); + bool get _isCircle => widget.targetShapeBorder is CircleBorder; - if (_enableShowcase) { - _globalFloatingActionWidget = - showCaseWidgetState.globalFloatingActionWidget?.call(context); - final size = MediaQuery.of(context).size; - position ??= GetPosition( - rootRenderObject: rootRenderObject, - key: widget.key, - padding: widget.targetPadding, - screenWidth: rootWidgetSize?.width ?? size.width, - screenHeight: rootWidgetSize?.height ?? size.height, - ); - showOverlay(); - } - } + BorderRadius? get _targetBorderRadius => widget.targetBorderRadius; - /// show overlay if there is any target widget - void showOverlay() { - final activeStep = ShowCaseWidget.activeTargetWidget(context); - setState(() { - _showShowCase = activeStep == widget.key; - }); + EdgeInsets get _targetPadding => widget.targetPadding; - if (activeStep == widget.key) { - if (widget.enableAutoScroll ?? showCaseWidgetState.enableAutoScroll) { - _scrollIntoView(); - } - - if (showCaseWidgetState.autoPlay) { - timer = Timer( - Duration(seconds: showCaseWidgetState.autoPlayDelay.inSeconds), - _nextIfAny, - ); - } - } else if (timer?.isActive ?? false) { - timer?.cancel(); - timer = null; + @override + void initState() { + super.initState(); + initRootWidget(); + final connectedShowcase = + showCaseWidgetState.showcaseController[widget.showcaseKey]; + showcaseController = ShowcaseController( + showcaseId: connectedShowcase?.length ?? 0, + showcaseKey: widget.showcaseKey, + showcaseConfig: widget, + scrollIntoView: _scrollIntoView, + )..startShowcase = startShowcase; + + if (connectedShowcase != null) { + showCaseWidgetState.showcaseController[widget.showcaseKey] + ?.add(showcaseController); + } else { + showCaseWidgetState.showcaseController[widget.showcaseKey] = [ + showcaseController, + ]; } } - void _scrollIntoView() { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { - final keyContext = widget.key.currentContext; - if (!mounted) return; - setState(() => _isScrollRunning = true); - await Scrollable.ensureVisible( - keyContext!, - duration: showCaseWidgetState.widget.scrollDuration, - alignment: widget.scrollAlignment, - ); - if (!mounted) return; - setState(() => _isScrollRunning = false); - }); - } - @override Widget build(BuildContext context) { - if (_enableShowcase) { - return AnchoredOverlay( - key: showCaseWidgetState.anchoredOverlayKey, - rootRenderObject: rootRenderObject, - overlayBuilder: (context, rectBound, offset) { - final size = rootWidgetSize ?? MediaQuery.of(context).size; - position = GetPosition( - rootRenderObject: rootRenderObject, - key: widget.key, - padding: widget.targetPadding, - screenWidth: size.width, - screenHeight: size.height, - ); - return buildOverlayOnTarget(offset, rectBound.size, rectBound, size); - }, - showOverlay: true, - child: widget.child, - ); - } + recalculateRootWidgetSize(); return widget.child; } @override void dispose() { - timer?.cancel(); - timer = null; + showCaseWidgetState.showcaseController[widget.showcaseKey] + ?.remove(showcaseController); super.dispose(); } void initRootWidget() { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - rootWidgetSize = showCaseWidgetState.rootWidgetSize; - rootRenderObject = showCaseWidgetState.rootRenderObject; + showcaseController.rootWidgetSize = showCaseWidgetState.rootWidgetSize; + showcaseController.rootRenderObject = + showCaseWidgetState.rootRenderObject; }); } + void startShowcase() { + enableShowcase = showCaseWidgetState.enableShowcase; + + recalculateRootWidgetSize(); + + if (enableShowcase) { + _globalFloatingActionWidget = showCaseWidgetState + .globalFloatingActionWidget(widget.showcaseKey) + ?.call(context); + final size = MediaQuery.of(context).size; + position ??= GetPosition( + rootRenderObject: showcaseController.rootRenderObject, + context: context, + padding: widget.targetPadding, + screenWidth: showcaseController.rootWidgetSize?.width ?? size.width, + screenHeight: showcaseController.rootWidgetSize?.height ?? size.height, + ); + } + } + + Future _scrollIntoView() async { + if (!mounted) return; + showcaseController.isScrollRunning = true; + _updateControllerData(context); + startShowcase(); + showCaseWidgetState.updateOverlay?.call(); + await Scrollable.ensureVisible( + context, + duration: showCaseWidgetState.widget.scrollDuration, + alignment: widget.scrollAlignment, + ); + if (!mounted) return; + showcaseController.isScrollRunning = false; + _updateControllerData(context); + startShowcase(); + showCaseWidgetState.updateOverlay?.call(); + } + void recalculateRootWidgetSize() { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - final rootWidget = - context.findRootAncestorStateOfType>(); - rootRenderObject = rootWidget?.context.findRenderObject() as RenderBox?; - rootWidgetSize = rootWidget == null + final rootWidget = context.findRootAncestorStateOfType>(); + showcaseController.rootRenderObject = + rootWidget?.context.findRenderObject() as RenderBox?; + showcaseController.rootWidgetSize = rootWidget == null ? MediaQuery.of(context).size - : rootRenderObject?.size; + : showcaseController.rootRenderObject?.size; + if (!enableShowcase) return; + _updateControllerData(context); + showCaseWidgetState.updateOverlay?.call(); }); } Future _nextIfAny() async { if (showCaseWidgetState.isShowCaseCompleted) return; - - if (timer != null && timer!.isActive) { - if (showCaseWidgetState.enableAutoPlayLock) { - return; - } - timer!.cancel(); - } else if (timer != null && !timer!.isActive) { - timer = null; - } - await _reverseAnimateTooltip(); - if (showCaseWidgetState.isShowCaseCompleted) return; - showCaseWidgetState.completed(widget.key); + showCaseWidgetState.completed(widget.showcaseKey); } Future _getOnTargetTap() async { if (widget.disposeOnTap == true) { - await _reverseAnimateTooltip(); showCaseWidgetState.dismiss(); widget.onTargetClick!(); } else { @@ -723,88 +697,30 @@ class _ShowcaseState extends State { Future _getOnTooltipTap() async { if (widget.disposeOnTap == true) { - await _reverseAnimateTooltip(); showCaseWidgetState.dismiss(); } widget.onToolTipClick?.call(); } - /// Reverse animates the provided tooltip or - /// the custom container widget. - Future _reverseAnimateTooltip() async { - if (!mounted) return; - setState(() => _isTooltipDismissed = true); - await Future.delayed(widget.scaleAnimationDuration); - _isTooltipDismissed = false; - } - - Widget buildOverlayOnTarget( + void buildOverlayOnTarget( Offset offset, Size size, Rect rectBound, Size screenSize, ) { - final mediaQuerySize = MediaQuery.of(context).size; var blur = 0.0; - if (_showShowCase) { - blur = widget.blurValue ?? showCaseWidgetState.blurValue; - } - // Set blur to 0 if application is running on web and - // provided blur is less than 0. + blur = widget.blurValue ?? showCaseWidgetState.blurValue; + blur = kIsWeb && blur < 0 ? 0 : blur; - if (!_showShowCase) return const Offstage(); - - return Stack( - children: [ - GestureDetector( - onTap: () { - if (!showCaseWidgetState.disableBarrierInteraction && - !widget.disableBarrierInteraction) { - _nextIfAny(); - } - widget.onBarrierClick?.call(); - }, - child: ClipPath( - clipper: RRectClipper( - area: _isScrollRunning ? Rect.zero : rectBound, - isCircle: widget.targetShapeBorder is CircleBorder, - radius: _isScrollRunning - ? BorderRadius.zero - : widget.targetBorderRadius, - overlayPadding: - _isScrollRunning ? EdgeInsets.zero : widget.targetPadding, - ), - child: blur != 0 - ? BackdropFilter( - filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur), - child: Container( - width: mediaQuerySize.width, - height: mediaQuerySize.height, - decoration: BoxDecoration( - color: widget.overlayColor - - //TODO: Update when we remove support for older version - //ignore: deprecated_member_use - .withOpacity(widget.overlayOpacity), - ), - ), - ) - : Container( - width: mediaQuerySize.width, - height: mediaQuerySize.height, - decoration: BoxDecoration( - color: widget.overlayColor - //TODO: Update when we remove support for older version - //ignore: deprecated_member_use - .withOpacity(widget.overlayOpacity), - ), - ), - ), - ), - if (_isScrollRunning) Center(child: widget.scrollLoadingWidget), - if (!_isScrollRunning) ...[ + showcaseController + ..position = position! + ..blur = blur + ..getToolTipWidget = [ + if (showcaseController.isScrollRunning) + Center(child: widget.scrollLoadingWidget), + if (!showcaseController.isScrollRunning) ...[ _TargetWidget( offset: rectBound.topLeft, size: size, @@ -817,6 +733,7 @@ class _ShowcaseState extends State { targetPadding: widget.targetPadding, ), ToolTipWidget( + key: ValueKey(showcaseController.hashCode), position: position, title: widget.title, titleTextAlign: widget.titleTextAlign, @@ -837,15 +754,15 @@ class _ShowcaseState extends State { tooltipPadding: widget.tooltipPadding, disableMovingAnimation: widget.disableMovingAnimation ?? showCaseWidgetState.disableMovingAnimation, - disableScaleAnimation: (widget.disableScaleAnimation ?? - showCaseWidgetState.disableScaleAnimation) || - widget.container != null, + disableScaleAnimation: widget.container != null + ? true + : widget.disableScaleAnimation ?? + showCaseWidgetState.disableScaleAnimation, movingAnimationDuration: widget.movingAnimationDuration, tooltipBorderRadius: widget.tooltipBorderRadius, scaleAnimationDuration: widget.scaleAnimationDuration, scaleAnimationCurve: widget.scaleAnimationCurve, scaleAnimationAlignment: widget.scaleAnimationAlignment, - isTooltipDismissed: _isTooltipDismissed, tooltipPosition: widget.tooltipPosition, titlePadding: widget.titlePadding, descriptionPadding: widget.descriptionPadding, @@ -856,11 +773,11 @@ class _ShowcaseState extends State { tooltipActionConfig: _getTooltipActionConfig(), tooltipActions: _getTooltipActions(), targetPadding: widget.targetPadding, + showcaseController: showcaseController, ), if (_getFloatingActionWidget != null) _getFloatingActionWidget!, ], - ], - ); + ]; } Widget? get _getFloatingActionWidget => @@ -876,8 +793,7 @@ class _ShowcaseState extends State { /// 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) && + if (action.hideActionWidgetForShowcase.contains(widget.showcaseKey) && (widget.tooltipActions?.isEmpty ?? true)) { continue; } @@ -906,6 +822,34 @@ class _ShowcaseState extends State { showCaseWidgetState.globalTooltipActionConfig ?? const TooltipActionConfig(); } + + void _updateControllerData(BuildContext context) { + final size = + showcaseController.rootWidgetSize ?? MediaQuery.of(context).size; + position = GetPosition( + rootRenderObject: showcaseController.rootRenderObject, + context: context, + padding: widget.targetPadding, + screenWidth: size.width, + screenHeight: size.height, + ); + showcaseController.position = position!; + showcaseController.linkedShowcaseDataModel = LinkedShowcaseDataModel( + rect: + showcaseController.isScrollRunning ? Rect.zero : position!.getRect(), + radius: _targetBorderRadius, + overlayPadding: + showcaseController.isScrollRunning ? EdgeInsets.zero : _targetPadding, + isCircle: _isCircle, + ); + + buildOverlayOnTarget( + position!.getOffSet(), + position!.getRect().size, + position!.getRect(), + size, + ); + } } class _TargetWidget extends StatelessWidget { @@ -954,8 +898,8 @@ class _TargetWidget extends StatelessWidget { onDoubleTap: onDoubleTap, behavior: HitTestBehavior.translucent, child: Container( - height: size.height, - width: size.width, + height: size.height.abs(), + width: size.width.abs(), margin: targetPadding, decoration: ShapeDecoration( shape: radius != null diff --git a/lib/src/showcase/showcase_controller.dart b/lib/src/showcase/showcase_controller.dart new file mode 100644 index 00000000..064a0be7 --- /dev/null +++ b/lib/src/showcase/showcase_controller.dart @@ -0,0 +1,47 @@ +import 'package:flutter/widgets.dart'; + +import '../get_position.dart'; +import '../models/linked_showcase_data.dart'; +import 'showcase.dart'; + +class ShowcaseController { + ShowcaseController({ + required this.showcaseId, + required this.showcaseKey, + required this.showcaseConfig, + required this.scrollIntoView, + }); + + final int showcaseId; + final GlobalKey showcaseKey; + + late Showcase showcaseConfig; + late GetPosition position; + late LinkedShowcaseDataModel linkedShowcaseDataModel; + late VoidCallback startShowcase; + Future Function()? scrollIntoView; + Future Function()? reverseAnimation; + List getToolTipWidget = []; + bool isScrollRunning = false; + double blur = 0.0; + Size? rootWidgetSize; + RenderBox? rootRenderObject; + + @override + int get hashCode { + final result = showcaseId.hashCode + showcaseKey.hashCode; + + return result; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other is! ShowcaseController) { + return false; + } + return other.showcaseKey == showcaseKey && other.showcaseId == showcaseId; + } +} diff --git a/lib/src/showcase_widget.dart b/lib/src/showcase_widget.dart index 34544c0e..3e2a3623 100644 --- a/lib/src/showcase_widget.dart +++ b/lib/src/showcase_widget.dart @@ -20,9 +20,15 @@ * SOFTWARE. */ +import 'dart:async'; +import 'dart:ui'; + import 'package:flutter/material.dart'; import '../showcaseview.dart'; +import 'layout_overlays.dart'; +import 'shape_clipper.dart'; +import 'showcase/showcase_controller.dart'; typedef FloatingActionBuilderCallback = FloatingActionWidget Function( BuildContext context, @@ -163,12 +169,6 @@ class ShowCaseWidget extends StatefulWidget { this.hideFloatingActionWidgetForShowcase = const [], }); - static GlobalKey? activeTargetWidget(BuildContext context) { - return context - .dependOnInheritedWidgetOfExactType<_InheritedShowCaseView>() - ?.activeWidgetIds; - } - static ShowCaseWidgetState of(BuildContext context) { final state = context.findAncestorStateOfType(); if (state != null) { @@ -187,12 +187,13 @@ class ShowCaseWidgetState extends State { int? activeWidgetId; RenderBox? rootRenderObject; Size? rootWidgetSize; - final anchoredOverlayKey = UniqueKey(); late final TooltipActionConfig? globalTooltipActionConfig; late final List? globalTooltipActions; + Map> showcaseController = {}; + /// These properties are only here so that it can be accessed by /// [Showcase] bool get autoPlay => widget.autoPlay; @@ -216,6 +217,10 @@ class ShowCaseWidgetState extends State { List get hiddenFloatingActionKeys => _hideFloatingWidgetKeys.keys.toList(); + Timer? _timer; + + VoidCallback? updateOverlay; + /// This Stores keys of showcase for which we will hide the /// [globalFloatingActionWidget]. late final _hideFloatingWidgetKeys = { @@ -238,8 +243,10 @@ class ShowCaseWidgetState extends State { /// Return a [widget.globalFloatingActionWidget] if not need to hide this for /// current showcase. - FloatingActionBuilderCallback? get globalFloatingActionWidget => - _hideFloatingWidgetKeys[getCurrentActiveShowcaseKey] ?? false + FloatingActionBuilderCallback? globalFloatingActionWidget( + GlobalKey showcaseKey, + ) => + _hideFloatingWidgetKeys[showcaseKey] ?? false ? null : widget.globalFloatingActionWidget; @@ -251,10 +258,112 @@ class ShowCaseWidgetState extends State { initRootWidget(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + if (!mounted) return; + final rootWidget = context.findRootAncestorStateOfType>(); + rootRenderObject = rootWidget?.context.findRenderObject() as RenderBox?; + rootWidgetSize = rootWidget == null + ? MediaQuery.of(context).size + : rootRenderObject?.size; + } + + @override + void didUpdateWidget(covariant ShowCaseWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (!mounted) return; + final rootWidget = context.findRootAncestorStateOfType>(); + rootRenderObject = rootWidget?.context.findRenderObject() as RenderBox?; + rootWidgetSize = rootWidget == null + ? MediaQuery.of(context).size + : rootRenderObject?.size; + } + + @override + Widget build(BuildContext context) { + return OverlayBuilder( + showOverlay: getCurrentActiveShowcaseKey != null, + updateOverlay: (updateOverlays) { + updateOverlay = updateOverlays; + }, + overlayBuilder: (_) { + final controller = showcaseController[getCurrentActiveShowcaseKey] ?? + []; + if (getCurrentActiveShowcaseKey != null && controller.isNotEmpty) { + final firstController = controller.first; + final firstShowcaseConfig = firstController.showcaseConfig; + return Stack( + children: [ + GestureDetector( + onTap: () { + firstShowcaseConfig.onBarrierClick?.call(); + if (!disableBarrierInteraction && + !firstShowcaseConfig.disableBarrierInteraction) { + next(); + } + }, + child: ClipPath( + clipper: RRectClipper( + area: Rect.zero, + isCircle: false, + radius: BorderRadius.zero, + overlayPadding: EdgeInsets.zero, + linkedObjectData: controller + .map( + (e) => e.linkedShowcaseDataModel, + ) + .toList(), + ), + child: controller.first.blur != 0 + ? BackdropFilter( + filter: ImageFilter.blur( + sigmaX: firstController.blur, + sigmaY: firstController.blur, + ), + child: getBackgroundOverlayContainer( + overlayColor: firstShowcaseConfig.overlayColor, + overlayOpacity: firstShowcaseConfig.overlayOpacity, + ), + ) + : getBackgroundOverlayContainer( + overlayColor: firstShowcaseConfig.overlayColor, + overlayOpacity: firstShowcaseConfig.overlayOpacity, + ), + ), + ), + for (final data in controller) ...data.getToolTipWidget.toList(), + ], + ); + } else { + return const SizedBox.shrink(); + } + }, + child: Builder(builder: widget.builder), + ); + } + + Widget getBackgroundOverlayContainer( + {required Color overlayColor, required double overlayOpacity}) { + return Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: overlayColor + + //TODO: Update when we remove support for older version + //ignore: deprecated_member_use + .withOpacity( + overlayOpacity, + ), + ), + ); + } + void initRootWidget() { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - final rootWidget = context.findAncestorStateOfType>(); + final rootWidget = context.findRootAncestorStateOfType>(); rootRenderObject = rootWidget?.context.findRenderObject() as RenderBox?; rootWidgetSize = rootWidget == null ? MediaQuery.of(context).size @@ -276,16 +385,22 @@ class ShowCaseWidgetState extends State { setState(() { ids = widgetIds; activeWidgetId = 0; + updateOverlay?.call(); _onStart(); }); + WidgetsBinding.instance.addPostFrameCallback( + (timeStamp) { + updateOverlay?.call(); + }, + ); } /// Completes showcase of given key and starts next one /// otherwise will finish the entire showcase view - void completed(GlobalKey? key) { + void completed(GlobalKey? key) async { if (ids != null && ids![activeWidgetId!] == key && mounted) { - setState(() { - _onComplete(); + await _onComplete(); + if (mounted) { activeWidgetId = activeWidgetId! + 1; _onStart(); @@ -293,40 +408,46 @@ class ShowCaseWidgetState extends State { _cleanupAfterSteps(); widget.onFinish?.call(); } - }); + updateOverlay?.call(); + } } } /// Completes current active showcase and starts next one /// otherwise will finish the entire showcase view - void next() { + void next([bool fromAutoPlayOrAction = false]) async { + // If this call is from autoPlay timer or action widget we will override the + // enableAutoPlayLock so user can move forward in showcase + if (!fromAutoPlayOrAction && widget.enableAutoPlayLock) return; + if (ids != null && mounted) { - setState(() { - _onComplete(); + await _onComplete(); + if (mounted) { activeWidgetId = activeWidgetId! + 1; _onStart(); - if (activeWidgetId! >= ids!.length) { _cleanupAfterSteps(); widget.onFinish?.call(); } - }); + updateOverlay?.call(); + } } } /// Completes current active showcase and starts previous one /// otherwise will finish the entire showcase view - void previous() { + void previous() async { if (ids != null && ((activeWidgetId ?? 0) - 1) >= 0 && mounted) { - setState(() { - _onComplete(); + await _onComplete(); + if (mounted) { activeWidgetId = activeWidgetId! - 1; _onStart(); if (activeWidgetId! >= ids!.length) { _cleanupAfterSteps(); widget.onFinish?.call(); } - }); + updateOverlay?.call(); + } } } @@ -339,23 +460,63 @@ class ShowCaseWidgetState extends State { activeWidgetId == null || ids == null || ids!.length < activeWidgetId!; widget.onDismiss?.call(idNotExist ? null : ids?[activeWidgetId!]); - - if (mounted) setState(_cleanupAfterSteps); + if (mounted) _cleanupAfterSteps.call(); + updateOverlay?.call(); } - void _onStart() { + Future _onStart() async { if (activeWidgetId! < ids!.length) { widget.onStart?.call(activeWidgetId, ids![activeWidgetId!]); + final controllers = showcaseController[getCurrentActiveShowcaseKey] ?? + []; + if ((controllers).length == 1 && + (controllers.first.showcaseConfig.enableAutoScroll ?? + widget.enableAutoScroll)) { + await controllers.first.scrollIntoView?.call(); + } else { + for (final controller in controllers) { + controller.startShowcase(); + } + } + } + if (widget.autoPlay) { + if (_timer?.isActive ?? false) { + _timer?.cancel(); + _timer = null; + } + _timer = Timer( + Duration(seconds: widget.autoPlayDelay.inSeconds), + () => next(true), + ); } } - void _onComplete() { + Future _onComplete() async { + var futures = []; + for (final controller in showcaseController[getCurrentActiveShowcaseKey] ?? + []) { + if ((controller.showcaseConfig.disableScaleAnimation ?? + widget.disableScaleAnimation) || + controller.reverseAnimation == null) { + continue; + } + futures.add(controller.reverseAnimation!.call()); + } + await Future.wait(futures); widget.onComplete?.call(activeWidgetId, ids![activeWidgetId!]); + if (widget.autoPlay) { + if (_timer?.isActive ?? false) { + _timer?.cancel(); + _timer = null; + } + } } void _cleanupAfterSteps() { ids = null; activeWidgetId = null; + _timer?.cancel(); + _timer = null; } /// Disables the [globalFloatingActionWidget] for the provided keys. @@ -366,27 +527,4 @@ class ShowCaseWidgetState extends State { ..clear() ..addAll({for (final item in updatedList) item: true}); } - - @override - Widget build(BuildContext context) { - return _InheritedShowCaseView( - activeWidgetIds: ids?.elementAt(activeWidgetId!), - child: Builder( - builder: widget.builder, - ), - ); - } -} - -class _InheritedShowCaseView extends InheritedWidget { - final GlobalKey? activeWidgetIds; - - const _InheritedShowCaseView({ - required this.activeWidgetIds, - required super.child, - }); - - @override - bool updateShouldNotify(_InheritedShowCaseView oldWidget) => - oldWidget.activeWidgetIds != activeWidgetIds; } diff --git a/lib/src/tooltip/animated_tooltip_layout.dart b/lib/src/tooltip/animated_tooltip_layout.dart index 271f3930..01367376 100644 --- a/lib/src/tooltip/animated_tooltip_layout.dart +++ b/lib/src/tooltip/animated_tooltip_layout.dart @@ -20,6 +20,7 @@ class _AnimatedTooltipMultiLayout extends MultiChildRenderObjectWidget { required this.scaleAlignment, required this.screenEdgePadding, required this.targetPadding, + required this.showcaseOffset, }); final AnimationController scaleController; @@ -37,6 +38,7 @@ class _AnimatedTooltipMultiLayout extends MultiChildRenderObjectWidget { final Alignment? scaleAlignment; final double screenEdgePadding; final EdgeInsets targetPadding; + final Offset showcaseOffset; @override RenderObject createRenderObject(BuildContext context) { @@ -56,6 +58,7 @@ class _AnimatedTooltipMultiLayout extends MultiChildRenderObjectWidget { toolTipSlideEndDistance: toolTipSlideEndDistance, screenEdgePadding: screenEdgePadding, targetPadding: targetPadding, + showcaseOffset: showcaseOffset, ); } @@ -78,6 +81,7 @@ class _AnimatedTooltipMultiLayout extends MultiChildRenderObjectWidget { ..screenEdgePadding = screenEdgePadding ..toolTipSlideEndDistance = toolTipSlideEndDistance ..gapBetweenContentAndAction = gapBetweenContentAndAction - ..targetPadding = targetPadding; + ..targetPadding = targetPadding + ..showcaseOffset = showcaseOffset; } } diff --git a/lib/src/tooltip/render_animation_delegate.dart b/lib/src/tooltip/render_animation_delegate.dart index 349d6db3..b8e1d08c 100644 --- a/lib/src/tooltip/render_animation_delegate.dart +++ b/lib/src/tooltip/render_animation_delegate.dart @@ -29,6 +29,7 @@ class _RenderAnimationDelegate extends _RenderPositionDelegate { required super.toolTipSlideEndDistance, required super.screenEdgePadding, required super.targetPadding, + required super.showcaseOffset, }) : _scaleController = scaleController, _moveController = moveController, _scaleAnimation = scaleAnimation, diff --git a/lib/src/tooltip/render_position_delegate.dart b/lib/src/tooltip/render_position_delegate.dart index 2642e938..d81b1903 100644 --- a/lib/src/tooltip/render_position_delegate.dart +++ b/lib/src/tooltip/render_position_delegate.dart @@ -30,6 +30,7 @@ class _RenderPositionDelegate extends RenderBox required this.gapBetweenContentAndAction, required this.screenEdgePadding, required this.targetPadding, + required this.showcaseOffset, }); // Core positioning parameters @@ -44,6 +45,12 @@ class _RenderPositionDelegate extends RenderBox double screenEdgePadding; EdgeInsets targetPadding; + /// This is used when there is some space around showcaseview as this widget + /// implementation works in global coordinate system so because of that we + /// need to manage local position by our self + /// To check this usecase wrap material app in padding widget + Offset showcaseOffset; + /// Calculated tooltip position after layout late TooltipPosition tooltipPosition; @@ -58,7 +65,7 @@ class _RenderPositionDelegate extends RenderBox @override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { // Standard hit testing implementation for children - return defaultHitTestChildren(result, position: position); + return defaultHitTestChildren(result, position: position + showcaseOffset); } @override @@ -198,16 +205,25 @@ class _RenderPositionDelegate extends RenderBox double maxHeight = tooltipHeight; // Horizontal boundary handling - if (xOffset < screenEdgePadding) { + if (xOffset < screenEdgePadding + showcaseOffset.dx) { // Tooltip extends beyond left edge if (tooltipPosition.isLeft) { // When positioned left, we have a few options: - var minWidth = maxWidth - screenEdgePadding + xOffset.abs(); + var minWidth = targetPosition.dx - + showcaseOffset.dx - + screenEdgePadding - + Constants.tooltipOffset - + targetPadding.left; + if (hasArrow) { + minWidth -= Constants.withArrowToolTipPadding; + } else { + minWidth -= Constants.withOutArrowToolTipPadding; + } if (minWidth > Constants.minimumToolTipWidth && minWidth > minimumActionBoxSize.width) { // Option 1: Resize tooltip to fit - maxWidth -= screenEdgePadding + xOffset.abs(); - xOffset = screenEdgePadding; + maxWidth = minWidth; + xOffset = screenEdgePadding + showcaseOffset.dx; needToResize = true; } else if (_fitsInPosition( TooltipPosition.right, @@ -241,7 +257,7 @@ class _RenderPositionDelegate extends RenderBox } else { // Option 5: Last resort - resize and keep at left maxWidth -= screenEdgePadding - xOffset; - xOffset = screenEdgePadding; + xOffset = screenEdgePadding + showcaseOffset.dx; needToResize = true; } } else if (tooltipPosition.isVertical) { @@ -250,14 +266,17 @@ class _RenderPositionDelegate extends RenderBox maxWidth = availableScreenWidth; needToResize = true; } - xOffset = screenEdgePadding; + xOffset = screenEdgePadding + showcaseOffset.dx; } - } else if (xOffset + toolTipBoxSize.width > + } else if (xOffset + toolTipBoxSize.width - showcaseOffset.dx > screenSize.width - screenEdgePadding) { // Tooltip extends beyond right edge if (tooltipPosition.isRight) { // When positioned right, similar options as with left position - var minWidth = screenSize.width - screenEdgePadding - xOffset; + var minWidth = screenSize.width - + screenEdgePadding - + xOffset - + targetPadding.right; if (minWidth > Constants.minimumToolTipWidth && minWidth > minimumActionBoxSize.width) { @@ -299,10 +318,13 @@ class _RenderPositionDelegate extends RenderBox if (maxWidth > availableScreenWidth) { maxWidth = availableScreenWidth; needToResize = true; - xOffset = screenEdgePadding; + xOffset = screenEdgePadding + showcaseOffset.dx; } else { // Align to right edge - xOffset = screenSize.width - screenEdgePadding - toolTipBoxSize.width; + xOffset = screenSize.width - + screenEdgePadding - + toolTipBoxSize.width + + showcaseOffset.dx; } } } @@ -320,9 +342,18 @@ class _RenderPositionDelegate extends RenderBox if (hasSecondBox) { maxHeight += (actionBoxSize.height + gapBetweenContentAndAction); } + var extraVerticalComponentHeight = 0.0; + if (tooltipPosition.isVertical) { + extraVerticalComponentHeight += Constants.tooltipOffset; + if (hasArrow) { + extraVerticalComponentHeight += Constants.withArrowToolTipPadding; + } else { + extraVerticalComponentHeight += Constants.withOutArrowToolTipPadding; + } + } // Vertical boundary handling - if (yOffset < screenEdgePadding) { + if (yOffset < screenEdgePadding + showcaseOffset.dy) { // Tooltip extends beyond top edge if (tooltipPosition.isTop) { // When positioned at top, check options @@ -358,7 +389,7 @@ class _RenderPositionDelegate extends RenderBox } else { // Option 4: Last resort - resize and keep at top maxHeight -= screenEdgePadding - xOffset; - yOffset = screenEdgePadding; + yOffset = screenEdgePadding + showcaseOffset.dy; needToResize = true; } } else if (tooltipPosition.isHorizontal) { @@ -367,9 +398,12 @@ class _RenderPositionDelegate extends RenderBox maxHeight = availableScreenHeight; needToResize = true; } - yOffset = screenEdgePadding; + yOffset = screenEdgePadding + showcaseOffset.dy; } - } else if (yOffset + tooltipHeight > + } else if (yOffset + + maxHeight + + extraVerticalComponentHeight - + showcaseOffset.dy > screenSize.height - screenEdgePadding) { // Tooltip extends beyond bottom edge if (tooltipPosition.isBottom) { @@ -405,18 +439,25 @@ class _RenderPositionDelegate extends RenderBox needToResize = true; } else { // Option 4: Last resort - resize and keep at bottom - maxHeight = screenSize.height - yOffset - screenEdgePadding; + maxHeight += extraVerticalComponentHeight; needToResize = true; + yOffset = screenSize.height - + showcaseOffset.dy - + screenEdgePadding - + maxHeight; } } else { // For left/right positions, ensure height fits and adjust alignment if (maxHeight > availableScreenHeight) { maxHeight = availableScreenHeight; needToResize = true; - yOffset = screenEdgePadding; + yOffset = screenEdgePadding + showcaseOffset.dy; } else { // Align to bottom edge - yOffset = screenSize.height - screenEdgePadding - tooltipHeight; + yOffset = screenSize.height - + screenEdgePadding - + tooltipHeight + + showcaseOffset.dy; } } } @@ -524,14 +565,17 @@ class _RenderPositionDelegate extends RenderBox // Ensure tooltip stays within horizontal screen bounds xOffset = xOffset.clamp( - screenEdgePadding, - screenSize.width - toolTipBoxSize.width - screenEdgePadding, + screenEdgePadding + showcaseOffset.dx, + screenSize.width - + toolTipBoxSize.width - + screenEdgePadding + + showcaseOffset.dx, ); // Ensure tooltip stays within vertical screen bounds yOffset = yOffset.clamp( - screenEdgePadding, - screenSize.height - tooltipHeight - screenEdgePadding, + screenEdgePadding + showcaseOffset.dy, + screenSize.height - tooltipHeight - screenEdgePadding + showcaseOffset.dy, ); switch (tooltipPosition) { @@ -777,7 +821,8 @@ class _RenderPositionDelegate extends RenderBox Constants.tooltipOffset + (hasArrow ? Constants.withArrowToolTipPadding - : Constants.withOutArrowToolTipPadding) <= + : Constants.withOutArrowToolTipPadding) - + showcaseOffset.dy <= screenSize.height - screenEdgePadding; case TooltipPosition.top: @@ -787,7 +832,8 @@ class _RenderPositionDelegate extends RenderBox Constants.tooltipOffset - (hasArrow ? Constants.withArrowToolTipPadding - : Constants.withOutArrowToolTipPadding) >= + : Constants.withOutArrowToolTipPadding) - + showcaseOffset.dy >= screenEdgePadding; case TooltipPosition.left: @@ -797,7 +843,8 @@ class _RenderPositionDelegate extends RenderBox Constants.tooltipOffset - (hasArrow ? Constants.withArrowToolTipPadding - : Constants.withOutArrowToolTipPadding) >= + : Constants.withOutArrowToolTipPadding) - + showcaseOffset.dx >= screenEdgePadding; case TooltipPosition.right: @@ -808,7 +855,8 @@ class _RenderPositionDelegate extends RenderBox Constants.tooltipOffset + (hasArrow ? Constants.withArrowToolTipPadding - : Constants.withOutArrowToolTipPadding) <= + : Constants.withOutArrowToolTipPadding) - + showcaseOffset.dx <= screenSize.width - screenEdgePadding; } } diff --git a/lib/src/tooltip/tooltip.dart b/lib/src/tooltip/tooltip.dart index f63bbcbc..c4b254d1 100644 --- a/lib/src/tooltip/tooltip.dart +++ b/lib/src/tooltip/tooltip.dart @@ -5,6 +5,7 @@ import '../constants.dart'; import '../enum.dart'; import '../get_position.dart'; import '../models/tooltip_action_config.dart'; +import '../showcase/showcase_controller.dart'; import '../widget/action_widget.dart'; import '../widget/default_tooltip_text_widget.dart'; import 'render_object_manager.dart'; diff --git a/lib/src/tooltip/tooltip_widget.dart b/lib/src/tooltip/tooltip_widget.dart index e27c01ec..59b4587a 100644 --- a/lib/src/tooltip/tooltip_widget.dart +++ b/lib/src/tooltip/tooltip_widget.dart @@ -23,7 +23,6 @@ class ToolTipWidget extends StatefulWidget { final Duration scaleAnimationDuration; final Curve scaleAnimationCurve; final Alignment? scaleAnimationAlignment; - final bool isTooltipDismissed; final TooltipPosition? tooltipPosition; final EdgeInsets? titlePadding; final EdgeInsets? descriptionPadding; @@ -34,6 +33,7 @@ class ToolTipWidget extends StatefulWidget { final TooltipActionConfig tooltipActionConfig; final List tooltipActions; final EdgeInsets targetPadding; + final ShowcaseController showcaseController; const ToolTipWidget({ super.key, @@ -61,8 +61,8 @@ class ToolTipWidget extends StatefulWidget { required this.scaleAnimationDuration, required this.scaleAnimationCurve, required this.toolTipMargin, + required this.showcaseController, this.scaleAnimationAlignment, - this.isTooltipDismissed = false, this.tooltipPosition, this.titlePadding, this.descriptionPadding, @@ -118,18 +118,8 @@ class _ToolTipWidgetState extends State if (!widget.disableMovingAnimation) { _movingAnimationController.forward(); } - } - - @override - void didUpdateWidget(covariant ToolTipWidget oldWidget) { - super.didUpdateWidget(oldWidget); - if (!widget.disableScaleAnimation && widget.isTooltipDismissed) { - WidgetsBinding.instance.addPostFrameCallback( - (timeStamp) { - _scaleAnimationController.reverse(); - }, - ); - } + widget.showcaseController.reverseAnimation = + widget.disableScaleAnimation ? null : _scaleAnimationController.reverse; } @override @@ -242,7 +232,8 @@ class _ToolTipWidgetState extends State targetPosition: targetPosition, targetSize: targetSize, position: widget.tooltipPosition, - screenSize: MediaQuery.of(context).size, + screenSize: widget.showcaseController.rootWidgetSize ?? + MediaQuery.of(context).size, hasArrow: widget.showArrow, targetPadding: widget.targetPadding, scaleAlignment: widget.scaleAnimationAlignment, @@ -253,6 +244,9 @@ class _ToolTipWidgetState extends State gapBetweenContentAndAction: widget.tooltipActionConfig.gapBetweenContentAndAction, screenEdgePadding: widget.toolTipMargin, + showcaseOffset: widget.showcaseController.rootRenderObject + ?.localToGlobal(Offset.zero) ?? + Offset.zero, children: [ _TooltipLayoutId( id: TooltipLayoutSlot.tooltipBox, From c55862f45a0fd69f41576824bfb28c0c63f95688 Mon Sep 17 00:00:00 2001 From: Sahil-Simform Date: Tue, 25 Mar 2025 14:28:47 +0530 Subject: [PATCH 2/9] =?UTF-8?q?fix:=20=F0=9F=94=A8Fixed=20PR=20issue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/constants.dart | 14 ++ lib/src/enum.dart | 2 +- lib/src/get_position.dart | 38 ++-- lib/src/layout_overlays.dart | 20 +- lib/src/models/linked_showcase_data.dart | 10 +- lib/src/shape_clipper.dart | 45 ++-- lib/src/showcase/showcase.dart | 41 ++-- lib/src/showcase/showcase_controller.dart | 61 ++++-- lib/src/showcase_widget.dart | 200 ++++++++++-------- lib/src/tooltip/animated_tooltip_layout.dart | 1 + lib/src/tooltip/render_position_delegate.dart | 36 ++-- lib/src/tooltip/tooltip_widget.dart | 74 +++---- 12 files changed, 289 insertions(+), 253 deletions(-) diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 6dda3ebd..7edad1ea 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + class Constants { Constants._(); @@ -25,4 +27,16 @@ class Constants { /// i.e if it is bottom position then centerBottom + [extraAlignmentOffset] /// in bottom static const double extraAlignmentOffset = 5; + + static const defaultTargetRadius = Radius.circular(3.0); + + static const ShapeBorder defaultTargetShapeBorder = RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ); + + static const Widget defaultProgressIndicator = CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ); + + static const Duration defaultAnimationDuration = Duration(milliseconds: 2000); } diff --git a/lib/src/enum.dart b/lib/src/enum.dart index 1db136c1..8dbb5a9a 100644 --- a/lib/src/enum.dart +++ b/lib/src/enum.dart @@ -94,7 +94,7 @@ enum TooltipDefaultActionType { void onTap(ShowCaseWidgetState showCaseState) { switch (this) { case TooltipDefaultActionType.next: - showCaseState.next(true); + showCaseState.next(fromAutoPlayOrAction: true); break; case TooltipDefaultActionType.previous: showCaseState.previous(); diff --git a/lib/src/get_position.dart b/lib/src/get_position.dart index 2616a19e..60a55d73 100644 --- a/lib/src/get_position.dart +++ b/lib/src/get_position.dart @@ -32,7 +32,7 @@ class GetPosition { this.padding = EdgeInsets.zero, this.rootRenderObject, }) { - getRenderBox(); + _getRenderBox(); } final BuildContext context; @@ -41,18 +41,13 @@ class GetPosition { final double screenHeight; final RenderObject? rootRenderObject; - late final RenderBox? _box; - late final Offset? _boxOffset; - late final Offset? overlayOffset; + RenderBox? _box; + Offset? _boxOffset; RenderBox? get box => _box; - void getRenderBox() { - var renderBox = context.findRenderObject() as RenderBox?; - - overlayOffset = - (rootRenderObject?.parent as RenderBox?)?.localToGlobal(Offset.zero); - + void _getRenderBox() { + final renderBox = context.findRenderObject() as RenderBox?; if (renderBox == null) return; _box = renderBox; @@ -77,9 +72,7 @@ class GetPosition { final bottomRight = _box!.size.bottomRight(_boxOffset!); final leftDx = topLeft.dx - padding.left; var leftDy = topLeft.dy - padding.top; - if (leftDy < 0) { - leftDy = 0; - } + if (leftDy < 0) leftDy = 0; final rect = Rect.fromLTRB( leftDx.clamp(0, leftDx), leftDy.clamp(0, leftDy), @@ -131,14 +124,17 @@ class GetPosition { double getCenter() => (getLeft() + getRight()) * 0.5; - Offset topLeft() => - _box?.size.topLeft( - _box!.localToGlobal( - Offset.zero, - ancestor: rootRenderObject, - ), - ) ?? - Offset.zero; + Offset topLeft() { + final box = _box; + if (box == null) return Offset.zero; + + return box.size.topLeft( + box.localToGlobal( + Offset.zero, + ancestor: rootRenderObject, + ), + ); + } Offset getOffSet() => _box?.size.center(topLeft()) ?? Offset.zero; } diff --git a/lib/src/layout_overlays.dart b/lib/src/layout_overlays.dart index e83ebad8..4c3c87b4 100644 --- a/lib/src/layout_overlays.dart +++ b/lib/src/layout_overlays.dart @@ -24,8 +24,6 @@ import 'package:flutter/material.dart'; import 'showcase_widget.dart'; -typedef OverlayUpdateCallback = void Function(VoidCallback updateOverlay); - /// Displays an overlay Widget as constructed by the given [overlayBuilder]. /// /// The overlay built by the [overlayBuilder] can be conditionally shown and @@ -39,19 +37,19 @@ typedef OverlayUpdateCallback = void Function(VoidCallback updateOverlay); /// exist in [OverlayEntry]s which are inaccessible to outside Widgets. But if /// a better approach is found then feel free to use it. class OverlayBuilder extends StatefulWidget { - final bool showOverlay; - final WidgetBuilder? overlayBuilder; - final Widget? child; - final OverlayUpdateCallback updateOverlay; - const OverlayBuilder({ super.key, + required this.child, required this.updateOverlay, this.showOverlay = false, this.overlayBuilder, - this.child, }); + final bool showOverlay; + final WidgetBuilder? overlayBuilder; + final Widget child; + final ValueSetter updateOverlay; + @override State createState() => _OverlayBuilderState(); } @@ -66,10 +64,10 @@ class _OverlayBuilderState extends State { if (widget.showOverlay) { WidgetsBinding.instance.addPostFrameCallback((_) => showOverlay()); } - widget.updateOverlay.call(updateOverlay); + widget.updateOverlay.call(_updateOverlay); } - void updateOverlay() { + void _updateOverlay() { buildOverlay(); WidgetsBinding.instance.addPostFrameCallback((_) => syncWidgetAndOverlay()); } @@ -145,6 +143,6 @@ class _OverlayBuilderState extends State { @override Widget build(BuildContext context) { - return widget.child!; + return widget.child; } } diff --git a/lib/src/models/linked_showcase_data.dart b/lib/src/models/linked_showcase_data.dart index a84d88fc..116865a8 100644 --- a/lib/src/models/linked_showcase_data.dart +++ b/lib/src/models/linked_showcase_data.dart @@ -3,15 +3,15 @@ import 'package:flutter/widgets.dart'; /// This model is used to move linked showcase overlay data to parent /// showcase to crop linked showcase rect class LinkedShowcaseDataModel { - final Rect rect; - final EdgeInsets overlayPadding; - final BorderRadius? radius; - final bool isCircle; - const LinkedShowcaseDataModel({ required this.rect, required this.radius, required this.overlayPadding, required this.isCircle, }); + + final Rect rect; + final EdgeInsets overlayPadding; + final BorderRadius? radius; + final bool isCircle; } diff --git a/lib/src/shape_clipper.dart b/lib/src/shape_clipper.dart index 693feff4..af6c3d0f 100644 --- a/lib/src/shape_clipper.dart +++ b/lib/src/shape_clipper.dart @@ -24,27 +24,28 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; +import 'constants.dart'; import 'models/linked_showcase_data.dart'; class RRectClipper extends CustomClipper { - final bool isCircle; - final BorderRadius? radius; - final EdgeInsets overlayPadding; - final Rect area; - final List linkedObjectData; - - RRectClipper({ + const RRectClipper({ this.isCircle = false, - this.radius, this.overlayPadding = EdgeInsets.zero, this.area = Rect.zero, this.linkedObjectData = const [], + this.radius, }); + final bool isCircle; + final BorderRadius? radius; + final EdgeInsets overlayPadding; + final Rect area; + final List linkedObjectData; + @override ui.Path getClip(ui.Size size) { final customRadius = - isCircle ? Radius.circular(area.height) : const Radius.circular(3.0); + isCircle ? Radius.circular(area.height) : Constants.defaultTargetRadius; final rect = Rect.fromLTRB( area.left - overlayPadding.left, @@ -66,16 +67,18 @@ class RRectClipper extends CustomClipper { ), ); - for (final widgetRect in linkedObjectData) { - final customRadius = widgetRect.isCircle - ? Radius.circular(widgetRect.rect.height) - : const Radius.circular(3.0); + final linkedObjectLength = linkedObjectData.length; + for (var i = 0; i < linkedObjectLength; i++) { + final widgetInfo = linkedObjectData[i]; + final customRadius = widgetInfo.isCircle + ? Radius.circular(widgetInfo.rect.height) + : Constants.defaultTargetRadius; final rect = Rect.fromLTRB( - widgetRect.rect.left - widgetRect.overlayPadding.left, - widgetRect.rect.top - widgetRect.overlayPadding.top, - widgetRect.rect.right + widgetRect.overlayPadding.right, - widgetRect.rect.bottom + widgetRect.overlayPadding.bottom, + widgetInfo.rect.left - widgetInfo.overlayPadding.left, + widgetInfo.rect.top - widgetInfo.overlayPadding.top, + widgetInfo.rect.right + widgetInfo.overlayPadding.right, + widgetInfo.rect.bottom + widgetInfo.overlayPadding.bottom, ); /// We have use this approach so that overlapping cutout will merge with @@ -87,10 +90,10 @@ class RRectClipper extends CustomClipper { ..addRRect( RRect.fromRectAndCorners( rect, - topLeft: (widgetRect.radius?.topLeft ?? customRadius), - topRight: (widgetRect.radius?.topRight ?? customRadius), - bottomLeft: (widgetRect.radius?.bottomLeft ?? customRadius), - bottomRight: (widgetRect.radius?.bottomRight ?? customRadius), + topLeft: (widgetInfo.radius?.topLeft ?? customRadius), + topRight: (widgetInfo.radius?.topRight ?? customRadius), + bottomLeft: (widgetInfo.radius?.bottomLeft ?? customRadius), + bottomRight: (widgetInfo.radius?.bottomRight ?? customRadius), ), ), ); diff --git a/lib/src/showcase/showcase.dart b/lib/src/showcase/showcase.dart index 218fe134..217e3a82 100644 --- a/lib/src/showcase/showcase.dart +++ b/lib/src/showcase/showcase.dart @@ -25,6 +25,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import '../constants.dart'; import '../enum.dart'; import '../get_position.dart'; import '../models/linked_showcase_data.dart'; @@ -66,6 +67,11 @@ class Showcase extends StatefulWidget { final ShapeBorder targetShapeBorder; /// Radius of rectangle box while target widget is being showcased. + /// + /// Default value is: + /// ```dart + /// const Radius.circular(3.0), + /// ``` final BorderRadius? targetBorderRadius; /// TextStyle for default tooltip title @@ -425,7 +431,7 @@ class Showcase extends StatefulWidget { showcaseKey = key, assert( overlayOpacity >= 0.0 && overlayOpacity <= 1.0, - "overlay opacity must be between 0 and 1.", + 'overlay opacity must be between 0 and 1.', ), assert( onTargetClick == null || disposeOnTap != null, @@ -499,20 +505,14 @@ class Showcase extends StatefulWidget { required this.container, required this.child, this.floatingActionWidget, - this.targetShapeBorder = const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(8), - ), - ), + this.targetShapeBorder = Constants.defaultTargetShapeBorder, this.overlayColor = Colors.black45, this.targetBorderRadius, this.overlayOpacity = 0.75, - this.scrollLoadingWidget = const CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.white), - ), + this.scrollLoadingWidget = Constants.defaultProgressIndicator, this.onTargetClick, this.disposeOnTap, - this.movingAnimationDuration = const Duration(milliseconds: 2000), + this.movingAnimationDuration = Constants.defaultAnimationDuration, this.disableMovingAnimation, this.targetPadding = EdgeInsets.zero, this.blurValue, @@ -532,7 +532,7 @@ class Showcase extends StatefulWidget { scaleAnimationDuration = const Duration(milliseconds: 300), scaleAnimationCurve = Curves.decelerate, scaleAnimationAlignment = null, - disableScaleAnimation = true, + disableScaleAnimation = null, title = null, description = null, titleTextAlign = TextAlign.start, @@ -553,7 +553,7 @@ class Showcase extends StatefulWidget { showcaseKey = key, assert( overlayOpacity >= 0.0 && overlayOpacity <= 1.0, - "overlay opacity must be between 0 and 1.", + 'overlay opacity must be between 0 and 1.', ), assert( onBarrierClick == null || disableBarrierInteraction == false, @@ -573,10 +573,6 @@ class _ShowcaseState extends State { late final showCaseWidgetState = ShowCaseWidget.of(context); FloatingActionWidget? _globalFloatingActionWidget; - /// This variable will be true if some other showcase is linked with - /// this showcase and starts this widget showcase - // bool _isLinkedShowCaseStarted = false; - bool get _isCircle => widget.targetShapeBorder is CircleBorder; BorderRadius? get _targetBorderRadius => widget.targetBorderRadius; @@ -588,7 +584,7 @@ class _ShowcaseState extends State { super.initState(); initRootWidget(); final connectedShowcase = - showCaseWidgetState.showcaseController[widget.showcaseKey]; + showCaseWidgetState.showcaseControllers[widget.showcaseKey]; showcaseController = ShowcaseController( showcaseId: connectedShowcase?.length ?? 0, showcaseKey: widget.showcaseKey, @@ -597,10 +593,10 @@ class _ShowcaseState extends State { )..startShowcase = startShowcase; if (connectedShowcase != null) { - showCaseWidgetState.showcaseController[widget.showcaseKey] + showCaseWidgetState.showcaseControllers[widget.showcaseKey] ?.add(showcaseController); } else { - showCaseWidgetState.showcaseController[widget.showcaseKey] = [ + showCaseWidgetState.showcaseControllers[widget.showcaseKey] = [ showcaseController, ]; } @@ -614,7 +610,7 @@ class _ShowcaseState extends State { @override void dispose() { - showCaseWidgetState.showcaseController[widget.showcaseKey] + showCaseWidgetState.showcaseControllers[widget.showcaseKey] ?.remove(showcaseController); super.dispose(); } @@ -789,7 +785,10 @@ class _ShowcaseState extends State { : showCaseWidgetState.globalTooltipActions ?? []; final actionWidgets = []; - for (final action in actionData) { + final actionDataLength = actionData.length; + for (var i = 0; i < actionDataLength; i++) { + final action = actionData[i]; + /// 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 diff --git a/lib/src/showcase/showcase_controller.dart b/lib/src/showcase/showcase_controller.dart index 064a0be7..8dc4c500 100644 --- a/lib/src/showcase/showcase_controller.dart +++ b/lib/src/showcase/showcase_controller.dart @@ -4,44 +4,63 @@ import '../get_position.dart'; import '../models/linked_showcase_data.dart'; import 'showcase.dart'; +/// Controller class for managing showcase functionality class ShowcaseController { + /// Creates a [ShowcaseController] with required parameters ShowcaseController({ required this.showcaseId, required this.showcaseKey, required this.showcaseConfig, - required this.scrollIntoView, + this.scrollIntoView, }); + /// Unique identifier for the showcase final int showcaseId; + + /// Global key associated with the showcase widget final GlobalKey showcaseKey; - late Showcase showcaseConfig; - late GetPosition position; - late LinkedShowcaseDataModel linkedShowcaseDataModel; - late VoidCallback startShowcase; - Future Function()? scrollIntoView; - Future Function()? reverseAnimation; + /// Configuration for the showcase + Showcase? showcaseConfig; + + /// Position getter for the showcase + GetPosition? position; + + /// Data model for linked showcases + LinkedShowcaseDataModel? linkedShowcaseDataModel; + + /// Callback to start the showcase + VoidCallback? startShowcase; + + /// Optional function to scroll the view + final ValueGetter>? scrollIntoView; + + /// Optional function to reverse the animation + ValueGetter>? reverseAnimation; + + /// Size of the root widget + Size? rootWidgetSize; + + /// Render box for the root widget + RenderBox? rootRenderObject; + + /// List of tooltip widgets List getToolTipWidget = []; + + /// Flag to track if scrolling is in progress bool isScrollRunning = false; + + /// Blur effect value double blur = 0.0; - Size? rootWidgetSize; - RenderBox? rootRenderObject; @override - int get hashCode { - final result = showcaseId.hashCode + showcaseKey.hashCode; - - return result; - } + int get hashCode => Object.hash(showcaseId, showcaseKey); @override bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - if (other is! ShowcaseController) { - return false; - } - return other.showcaseKey == showcaseKey && other.showcaseId == showcaseId; + return (identical(this, other)) || + other is ShowcaseController && + other.showcaseKey == showcaseKey && + other.showcaseId == showcaseId; } } diff --git a/lib/src/showcase_widget.dart b/lib/src/showcase_widget.dart index 3e2a3623..46bddfe1 100644 --- a/lib/src/showcase_widget.dart +++ b/lib/src/showcase_widget.dart @@ -27,6 +27,7 @@ import 'package:flutter/material.dart'; import '../showcaseview.dart'; import 'layout_overlays.dart'; +import 'models/linked_showcase_data.dart'; import 'shape_clipper.dart'; import 'showcase/showcase_controller.dart'; @@ -192,7 +193,7 @@ class ShowCaseWidgetState extends State { late final List? globalTooltipActions; - Map> showcaseController = {}; + Map> showcaseControllers = {}; /// These properties are only here so that it can be accessed by /// [Showcase] @@ -262,90 +263,98 @@ class ShowCaseWidgetState extends State { void didChangeDependencies() { super.didChangeDependencies(); - if (!mounted) return; - final rootWidget = context.findRootAncestorStateOfType>(); - rootRenderObject = rootWidget?.context.findRenderObject() as RenderBox?; - rootWidgetSize = rootWidget == null - ? MediaQuery.of(context).size - : rootRenderObject?.size; + _updateRootWidget(); } @override void didUpdateWidget(covariant ShowCaseWidget oldWidget) { super.didUpdateWidget(oldWidget); - if (!mounted) return; - final rootWidget = context.findRootAncestorStateOfType>(); - rootRenderObject = rootWidget?.context.findRenderObject() as RenderBox?; - rootWidgetSize = rootWidget == null - ? MediaQuery.of(context).size - : rootRenderObject?.size; + _updateRootWidget(); } @override Widget build(BuildContext context) { return OverlayBuilder( showOverlay: getCurrentActiveShowcaseKey != null, - updateOverlay: (updateOverlays) { - updateOverlay = updateOverlays; - }, + updateOverlay: (updateOverlays) => updateOverlay = updateOverlays, overlayBuilder: (_) { - final controller = showcaseController[getCurrentActiveShowcaseKey] ?? + final controller = showcaseControllers[getCurrentActiveShowcaseKey] ?? []; - if (getCurrentActiveShowcaseKey != null && controller.isNotEmpty) { - final firstController = controller.first; - final firstShowcaseConfig = firstController.showcaseConfig; - return Stack( - children: [ - GestureDetector( - onTap: () { - firstShowcaseConfig.onBarrierClick?.call(); - if (!disableBarrierInteraction && - !firstShowcaseConfig.disableBarrierInteraction) { - next(); - } - }, - child: ClipPath( - clipper: RRectClipper( - area: Rect.zero, - isCircle: false, - radius: BorderRadius.zero, - overlayPadding: EdgeInsets.zero, - linkedObjectData: controller - .map( - (e) => e.linkedShowcaseDataModel, - ) - .toList(), - ), - child: controller.first.blur != 0 - ? BackdropFilter( - filter: ImageFilter.blur( - sigmaX: firstController.blur, - sigmaY: firstController.blur, - ), - child: getBackgroundOverlayContainer( - overlayColor: firstShowcaseConfig.overlayColor, - overlayOpacity: firstShowcaseConfig.overlayOpacity, - ), - ) - : getBackgroundOverlayContainer( - overlayColor: firstShowcaseConfig.overlayColor, - overlayOpacity: firstShowcaseConfig.overlayOpacity, - ), - ), - ), - for (final data in controller) ...data.getToolTipWidget.toList(), - ], - ); - } else { + + if (getCurrentActiveShowcaseKey == null || controller.isEmpty) { return const SizedBox.shrink(); } + + final firstController = controller.first; + final firstShowcaseConfig = firstController.showcaseConfig!; + final listOfLinkedModel = []; + final controllerLength = controller.length; + for (var i = 0; i < controllerLength; i++) { + final model = controller[i].linkedShowcaseDataModel; + if (model == null) continue; + listOfLinkedModel.add(model); + } + final backgroundContainer = getBackgroundOverlayContainer( + overlayColor: firstShowcaseConfig.overlayColor, + overlayOpacity: firstShowcaseConfig.overlayOpacity, + ); + + final tooltipWidgets = []; + for (var i = 0; i < controllerLength; i++) { + tooltipWidgets.addAll(controller[i].getToolTipWidget); + } + return Stack( + children: [ + GestureDetector( + onTap: () => _barrierOnTap(firstShowcaseConfig), + child: ClipPath( + clipper: RRectClipper( + area: Rect.zero, + isCircle: false, + radius: BorderRadius.zero, + overlayPadding: EdgeInsets.zero, + linkedObjectData: listOfLinkedModel, + ), + child: firstController.blur != 0 + ? BackdropFilter( + filter: ImageFilter.blur( + sigmaX: firstController.blur, + sigmaY: firstController.blur, + ), + child: backgroundContainer, + ) + : backgroundContainer, + ), + ), + ...tooltipWidgets, + ], + ); }, child: Builder(builder: widget.builder), ); } - Widget getBackgroundOverlayContainer( - {required Color overlayColor, required double overlayOpacity}) { + void _updateRootWidget() { + if (!mounted) return; + final rootWidget = context.findRootAncestorStateOfType>(); + rootRenderObject = rootWidget?.context.findRenderObject() as RenderBox?; + rootWidgetSize = rootWidget == null + ? MediaQuery.sizeOf(context) + : rootRenderObject?.size; + } + + void _barrierOnTap(Showcase firstShowcaseConfig) { + firstShowcaseConfig.onBarrierClick?.call(); + if (!disableBarrierInteraction && + !firstShowcaseConfig.disableBarrierInteraction) { + next(); + } + } + + Widget getBackgroundOverlayContainer({ + required Color overlayColor, + required double overlayOpacity, + }) { return Container( alignment: Alignment.center, decoration: BoxDecoration( @@ -353,9 +362,7 @@ class ShowCaseWidgetState extends State { //TODO: Update when we remove support for older version //ignore: deprecated_member_use - .withOpacity( - overlayOpacity, - ), + .withOpacity(overlayOpacity), ), ); } @@ -389,7 +396,7 @@ class ShowCaseWidgetState extends State { _onStart(); }); WidgetsBinding.instance.addPostFrameCallback( - (timeStamp) { + (_) { updateOverlay?.call(); }, ); @@ -397,7 +404,7 @@ class ShowCaseWidgetState extends State { /// Completes showcase of given key and starts next one /// otherwise will finish the entire showcase view - void completed(GlobalKey? key) async { + Future completed(GlobalKey? key) async { if (ids != null && ids![activeWidgetId!] == key && mounted) { await _onComplete(); if (mounted) { @@ -415,7 +422,7 @@ class ShowCaseWidgetState extends State { /// Completes current active showcase and starts next one /// otherwise will finish the entire showcase view - void next([bool fromAutoPlayOrAction = false]) async { + void next({bool fromAutoPlayOrAction = false}) async { // If this call is from autoPlay timer or action widget we will override the // enableAutoPlayLock so user can move forward in showcase if (!fromAutoPlayOrAction && widget.enableAutoPlayLock) return; @@ -460,42 +467,47 @@ class ShowCaseWidgetState extends State { activeWidgetId == null || ids == null || ids!.length < activeWidgetId!; widget.onDismiss?.call(idNotExist ? null : ids?[activeWidgetId!]); - if (mounted) _cleanupAfterSteps.call(); - updateOverlay?.call(); + if (mounted) { + _cleanupAfterSteps.call(); + updateOverlay?.call(); + } } Future _onStart() async { if (activeWidgetId! < ids!.length) { widget.onStart?.call(activeWidgetId, ids![activeWidgetId!]); - final controllers = showcaseController[getCurrentActiveShowcaseKey] ?? + final controllers = showcaseControllers[getCurrentActiveShowcaseKey] ?? []; - if ((controllers).length == 1 && - (controllers.first.showcaseConfig.enableAutoScroll ?? + if (controllers.length == 1 && + (controllers.first.showcaseConfig?.enableAutoScroll ?? widget.enableAutoScroll)) { await controllers.first.scrollIntoView?.call(); } else { - for (final controller in controllers) { - controller.startShowcase(); + final controllerLength = controllers.length; + for (var i = 0; i < controllerLength; i++) { + controllers[i].startShowcase?.call(); } } } if (widget.autoPlay) { - if (_timer?.isActive ?? false) { - _timer?.cancel(); - _timer = null; - } + _stopTimer(); _timer = Timer( Duration(seconds: widget.autoPlayDelay.inSeconds), - () => next(true), + () => next(fromAutoPlayOrAction: true), ); } } Future _onComplete() async { - var futures = []; - for (final controller in showcaseController[getCurrentActiveShowcaseKey] ?? - []) { - if ((controller.showcaseConfig.disableScaleAnimation ?? + final futures = []; + final currentControllers = + (showcaseControllers[getCurrentActiveShowcaseKey] ?? + []); + final controllerLength = currentControllers.length; + + for (var i = 0; i < controllerLength; i++) { + final controller = currentControllers[i]; + if ((controller.showcaseConfig?.disableScaleAnimation ?? widget.disableScaleAnimation) || controller.reverseAnimation == null) { continue; @@ -505,18 +517,20 @@ class ShowCaseWidgetState extends State { await Future.wait(futures); widget.onComplete?.call(activeWidgetId, ids![activeWidgetId!]); if (widget.autoPlay) { - if (_timer?.isActive ?? false) { - _timer?.cancel(); - _timer = null; - } + _stopTimer(); } } + void _stopTimer() { + if (!(_timer?.isActive ?? false)) return; + _timer?.cancel(); + _timer = null; + } + void _cleanupAfterSteps() { ids = null; activeWidgetId = null; - _timer?.cancel(); - _timer = null; + _stopTimer(); } /// Disables the [globalFloatingActionWidget] for the provided keys. diff --git a/lib/src/tooltip/animated_tooltip_layout.dart b/lib/src/tooltip/animated_tooltip_layout.dart index 01367376..d6ac4b36 100644 --- a/lib/src/tooltip/animated_tooltip_layout.dart +++ b/lib/src/tooltip/animated_tooltip_layout.dart @@ -2,6 +2,7 @@ part of 'tooltip.dart'; class _AnimatedTooltipMultiLayout extends MultiChildRenderObjectWidget { const _AnimatedTooltipMultiLayout({ + // If we remove this parameter it will cause error in v3.29.0 so ignore // ignore: unused_element_parameter super.key, required this.scaleController, diff --git a/lib/src/tooltip/render_position_delegate.dart b/lib/src/tooltip/render_position_delegate.dart index d81b1903..15ea6e48 100644 --- a/lib/src/tooltip/render_position_delegate.dart +++ b/lib/src/tooltip/render_position_delegate.dart @@ -214,11 +214,9 @@ class _RenderPositionDelegate extends RenderBox screenEdgePadding - Constants.tooltipOffset - targetPadding.left; - if (hasArrow) { - minWidth -= Constants.withArrowToolTipPadding; - } else { - minWidth -= Constants.withOutArrowToolTipPadding; - } + minWidth -= hasArrow + ? Constants.withArrowToolTipPadding + : Constants.withOutArrowToolTipPadding; if (minWidth > Constants.minimumToolTipWidth && minWidth > minimumActionBoxSize.width) { // Option 1: Resize tooltip to fit @@ -345,11 +343,9 @@ class _RenderPositionDelegate extends RenderBox var extraVerticalComponentHeight = 0.0; if (tooltipPosition.isVertical) { extraVerticalComponentHeight += Constants.tooltipOffset; - if (hasArrow) { - extraVerticalComponentHeight += Constants.withArrowToolTipPadding; - } else { - extraVerticalComponentHeight += Constants.withOutArrowToolTipPadding; - } + extraVerticalComponentHeight += hasArrow + ? Constants.withArrowToolTipPadding + : Constants.withOutArrowToolTipPadding; } // Vertical boundary handling @@ -812,6 +808,10 @@ class _RenderPositionDelegate extends RenderBox Size tooltipSize, double totalHeight, ) { + final arrowPadding = hasArrow + ? Constants.withArrowToolTipPadding + : Constants.withOutArrowToolTipPadding; + switch (pos) { case TooltipPosition.bottom: // Check if tooltip fits below target @@ -819,9 +819,7 @@ class _RenderPositionDelegate extends RenderBox targetSize.height + totalHeight + Constants.tooltipOffset + - (hasArrow - ? Constants.withArrowToolTipPadding - : Constants.withOutArrowToolTipPadding) - + arrowPadding - showcaseOffset.dy <= screenSize.height - screenEdgePadding; @@ -830,9 +828,7 @@ class _RenderPositionDelegate extends RenderBox return targetPosition.dy - totalHeight - Constants.tooltipOffset - - (hasArrow - ? Constants.withArrowToolTipPadding - : Constants.withOutArrowToolTipPadding) - + arrowPadding - showcaseOffset.dy >= screenEdgePadding; @@ -841,9 +837,7 @@ class _RenderPositionDelegate extends RenderBox return targetPosition.dx - tooltipSize.width - Constants.tooltipOffset - - (hasArrow - ? Constants.withArrowToolTipPadding - : Constants.withOutArrowToolTipPadding) - + arrowPadding - showcaseOffset.dx >= screenEdgePadding; @@ -853,9 +847,7 @@ class _RenderPositionDelegate extends RenderBox targetSize.width + tooltipSize.width + Constants.tooltipOffset + - (hasArrow - ? Constants.withArrowToolTipPadding - : Constants.withOutArrowToolTipPadding) - + arrowPadding - showcaseOffset.dx <= screenSize.width - screenEdgePadding; } diff --git a/lib/src/tooltip/tooltip_widget.dart b/lib/src/tooltip/tooltip_widget.dart index 59b4587a..459b81fc 100644 --- a/lib/src/tooltip/tooltip_widget.dart +++ b/lib/src/tooltip/tooltip_widget.dart @@ -1,40 +1,6 @@ part of "tooltip.dart"; class ToolTipWidget extends StatefulWidget { - final GetPosition? position; - final String? title; - final TextAlign? titleTextAlign; - final String? description; - final TextAlign? descriptionTextAlign; - final AlignmentGeometry titleAlignment; - final AlignmentGeometry descriptionAlignment; - final TextStyle? titleTextStyle; - final TextStyle? descTextStyle; - final Widget? container; - final Color? tooltipBackgroundColor; - final Color? textColor; - final bool showArrow; - final VoidCallback? onTooltipTap; - final EdgeInsets? tooltipPadding; - final Duration movingAnimationDuration; - final bool disableMovingAnimation; - final bool disableScaleAnimation; - final BorderRadius? tooltipBorderRadius; - final Duration scaleAnimationDuration; - final Curve scaleAnimationCurve; - final Alignment? scaleAnimationAlignment; - final TooltipPosition? tooltipPosition; - final EdgeInsets? titlePadding; - final EdgeInsets? descriptionPadding; - final TextDirection? titleTextDirection; - final TextDirection? descriptionTextDirection; - final double toolTipSlideEndDistance; - final double toolTipMargin; - final TooltipActionConfig tooltipActionConfig; - final List tooltipActions; - final EdgeInsets targetPadding; - final ShowcaseController showcaseController; - const ToolTipWidget({ super.key, required this.position, @@ -62,16 +28,50 @@ class ToolTipWidget extends StatefulWidget { required this.scaleAnimationCurve, required this.toolTipMargin, required this.showcaseController, + required this.tooltipPadding, + required this.toolTipSlideEndDistance, this.scaleAnimationAlignment, this.tooltipPosition, this.titlePadding, this.descriptionPadding, this.titleTextDirection, this.descriptionTextDirection, - this.toolTipSlideEndDistance = 7, - this.tooltipPadding = const EdgeInsets.symmetric(vertical: 8), }); + final GetPosition? position; + final String? title; + final TextAlign? titleTextAlign; + final String? description; + final TextAlign? descriptionTextAlign; + final AlignmentGeometry titleAlignment; + final AlignmentGeometry descriptionAlignment; + final TextStyle? titleTextStyle; + final TextStyle? descTextStyle; + final Widget? container; + final Color? tooltipBackgroundColor; + final Color? textColor; + final bool showArrow; + final VoidCallback? onTooltipTap; + final EdgeInsets? tooltipPadding; + final Duration movingAnimationDuration; + final bool disableMovingAnimation; + final bool disableScaleAnimation; + final BorderRadius? tooltipBorderRadius; + final Duration scaleAnimationDuration; + final Curve scaleAnimationCurve; + final Alignment? scaleAnimationAlignment; + final TooltipPosition? tooltipPosition; + final EdgeInsets? titlePadding; + final EdgeInsets? descriptionPadding; + final TextDirection? titleTextDirection; + final TextDirection? descriptionTextDirection; + final double toolTipSlideEndDistance; + final double toolTipMargin; + final TooltipActionConfig tooltipActionConfig; + final List tooltipActions; + final EdgeInsets targetPadding; + final ShowcaseController showcaseController; + @override State createState() => _ToolTipWidgetState(); } @@ -233,7 +233,7 @@ class _ToolTipWidgetState extends State targetSize: targetSize, position: widget.tooltipPosition, screenSize: widget.showcaseController.rootWidgetSize ?? - MediaQuery.of(context).size, + MediaQuery.sizeOf(context), hasArrow: widget.showArrow, targetPadding: widget.targetPadding, scaleAlignment: widget.scaleAnimationAlignment, From 228af1d7067033889d1ac6167c6d0aa7dc3a208c Mon Sep 17 00:00:00 2001 From: Sahil-Simform Date: Tue, 25 Mar 2025 20:17:22 +0530 Subject: [PATCH 3/9] =?UTF-8?q?fix:=20=F0=9F=94=A8Fixed=20PR=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 25 +++ lib/src/constants.dart | 5 +- lib/src/enum.dart | 2 +- lib/src/get_position.dart | 7 +- lib/src/layout_overlays.dart | 19 +- lib/src/showcase/showcase.dart | 226 ++++++++++++---------- lib/src/showcase/showcase_controller.dart | 2 +- lib/src/showcase_widget.dart | 151 ++++++++------- 8 files changed, 248 insertions(+), 189 deletions(-) diff --git a/README.md b/README.md index da828eea..a626d9a3 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,31 @@ To show multiple showcase at the same time provide same key to showcase. Note: auto scroll to showcase will not work in case of the multi-showcase and we will use property of first initialized showcase for common things like barrier tap and colors. +```dart +GlobalKey _one = GlobalKey(); +... + +Showcase( + key: _one, + title: 'Showcase one', + description: 'Click here to see menu options', + child: Icon( + Icons.menu, + color: Colors.black45, + ), +), + +Showcase( + key: _one, + title: 'Showcase two', + description: 'Click here to see menu options', + child: Icon( + Icons.menu, + color: Colors.black45, + ), +), +``` + ## Functions of `ShowCaseWidget.of(context)`: | Function Name | Description | diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 7edad1ea..17914a25 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -34,8 +34,9 @@ class Constants { borderRadius: BorderRadius.all(Radius.circular(8)), ); - static const Widget defaultProgressIndicator = CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.white), + static const Widget defaultProgressIndicator = + CircularProgressIndicator.adaptive( + backgroundColor: Colors.white, ); static const Duration defaultAnimationDuration = Duration(milliseconds: 2000); diff --git a/lib/src/enum.dart b/lib/src/enum.dart index 8dbb5a9a..e26a014b 100644 --- a/lib/src/enum.dart +++ b/lib/src/enum.dart @@ -94,7 +94,7 @@ enum TooltipDefaultActionType { void onTap(ShowCaseWidgetState showCaseState) { switch (this) { case TooltipDefaultActionType.next: - showCaseState.next(fromAutoPlayOrAction: true); + showCaseState.next(forceNext: true); break; case TooltipDefaultActionType.previous: showCaseState.previous(); diff --git a/lib/src/get_position.dart b/lib/src/get_position.dart index 60a55d73..89c46fd5 100644 --- a/lib/src/get_position.dart +++ b/lib/src/get_position.dart @@ -26,7 +26,7 @@ import 'package:flutter/material.dart'; class GetPosition { GetPosition({ - required this.context, + required this.renderBox, required this.screenWidth, required this.screenHeight, this.padding = EdgeInsets.zero, @@ -35,7 +35,7 @@ class GetPosition { _getRenderBox(); } - final BuildContext context; + final RenderBox? renderBox; final EdgeInsets padding; final double screenWidth; final double screenHeight; @@ -47,7 +47,6 @@ class GetPosition { RenderBox? get box => _box; void _getRenderBox() { - final renderBox = context.findRenderObject() as RenderBox?; if (renderBox == null) return; _box = renderBox; @@ -136,5 +135,5 @@ class GetPosition { ); } - Offset getOffSet() => _box?.size.center(topLeft()) ?? Offset.zero; + Offset getOffset() => _box?.size.center(topLeft()) ?? Offset.zero; } diff --git a/lib/src/layout_overlays.dart b/lib/src/layout_overlays.dart index 4c3c87b4..c4f6024c 100644 --- a/lib/src/layout_overlays.dart +++ b/lib/src/layout_overlays.dart @@ -41,14 +41,12 @@ class OverlayBuilder extends StatefulWidget { super.key, required this.child, required this.updateOverlay, - this.showOverlay = false, this.overlayBuilder, }); - final bool showOverlay; final WidgetBuilder? overlayBuilder; final Widget child; - final ValueSetter updateOverlay; + final ValueSetter> updateOverlay; @override State createState() => _OverlayBuilderState(); @@ -57,17 +55,20 @@ class OverlayBuilder extends StatefulWidget { class _OverlayBuilderState extends State { OverlayEntry? _overlayEntry; + bool _showOverlay = false; + @override void initState() { super.initState(); - if (widget.showOverlay) { + if (_showOverlay) { WidgetsBinding.instance.addPostFrameCallback((_) => showOverlay()); } widget.updateOverlay.call(_updateOverlay); } - void _updateOverlay() { + void _updateOverlay(bool showOverlay) { + _showOverlay = showOverlay; buildOverlay(); WidgetsBinding.instance.addPostFrameCallback((_) => syncWidgetAndOverlay()); } @@ -75,9 +76,7 @@ class _OverlayBuilderState extends State { @override void didUpdateWidget(OverlayBuilder oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.showOverlay != widget.showOverlay && widget.showOverlay) { - WidgetsBinding.instance.addPostFrameCallback((_) => showOverlay()); - } + WidgetsBinding.instance.addPostFrameCallback((_) => showOverlay()); } @override @@ -129,9 +128,9 @@ class _OverlayBuilderState extends State { } void syncWidgetAndOverlay() { - if (isShowingOverlay() && !widget.showOverlay) { + if (isShowingOverlay() && !_showOverlay) { hideOverlay(); - } else if (!isShowingOverlay() && widget.showOverlay) { + } else if (!isShowingOverlay() && _showOverlay) { showOverlay(); } } diff --git a/lib/src/showcase/showcase.dart b/lib/src/showcase/showcase.dart index 217e3a82..426fa552 100644 --- a/lib/src/showcase/showcase.dart +++ b/lib/src/showcase/showcase.dart @@ -21,6 +21,7 @@ */ import 'dart:async'; +import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -389,9 +390,7 @@ class Showcase extends StatefulWidget { this.descTextStyle, this.tooltipBackgroundColor = Colors.white, this.textColor = Colors.black, - this.scrollLoadingWidget = const CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.white), - ), + this.scrollLoadingWidget = Constants.defaultProgressIndicator, this.showArrow = true, this.onTargetClick, this.disposeOnTap, @@ -583,22 +582,30 @@ class _ShowcaseState extends State { void initState() { super.initState(); initRootWidget(); - final connectedShowcase = - showCaseWidgetState.showcaseControllers[widget.showcaseKey]; showcaseController = ShowcaseController( - showcaseId: connectedShowcase?.length ?? 0, + showcaseId: widget.hashCode, showcaseKey: widget.showcaseKey, showcaseConfig: widget, scrollIntoView: _scrollIntoView, )..startShowcase = startShowcase; - if (connectedShowcase != null) { - showCaseWidgetState.showcaseControllers[widget.showcaseKey] - ?.add(showcaseController); - } else { - showCaseWidgetState.showcaseControllers[widget.showcaseKey] = [ - showcaseController, - ]; + showCaseWidgetState.registerShowcaseController( + controller: showcaseController, + key: widget.showcaseKey, + ); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + showcaseController.showcaseConfig = widget; + } + + @override + void didUpdateWidget(covariant Showcase oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget != widget) { + showcaseController.showcaseConfig = widget; } } @@ -610,17 +617,20 @@ class _ShowcaseState extends State { @override void dispose() { - showCaseWidgetState.showcaseControllers[widget.showcaseKey] - ?.remove(showcaseController); + showCaseWidgetState.removeShowcaseController( + key: widget.showcaseKey, + controller: showcaseController, + ); + super.dispose(); } void initRootWidget() { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - showcaseController.rootWidgetSize = showCaseWidgetState.rootWidgetSize; - showcaseController.rootRenderObject = - showCaseWidgetState.rootRenderObject; + showcaseController + ..rootWidgetSize = showCaseWidgetState.rootWidgetSize + ..rootRenderObject = showCaseWidgetState.rootRenderObject; }); } @@ -636,7 +646,7 @@ class _ShowcaseState extends State { final size = MediaQuery.of(context).size; position ??= GetPosition( rootRenderObject: showcaseController.rootRenderObject, - context: context, + renderBox: context.findRenderObject() as RenderBox?, padding: widget.targetPadding, screenWidth: showcaseController.rootWidgetSize?.width ?? size.width, screenHeight: showcaseController.rootWidgetSize?.height ?? size.height, @@ -649,7 +659,9 @@ class _ShowcaseState extends State { showcaseController.isScrollRunning = true; _updateControllerData(context); startShowcase(); - showCaseWidgetState.updateOverlay?.call(); + showCaseWidgetState.updateOverlay?.call( + showCaseWidgetState.isShowcaseRunning, + ); await Scrollable.ensureVisible( context, duration: showCaseWidgetState.widget.scrollDuration, @@ -659,21 +671,26 @@ class _ShowcaseState extends State { showcaseController.isScrollRunning = false; _updateControllerData(context); startShowcase(); - showCaseWidgetState.updateOverlay?.call(); + showCaseWidgetState.updateOverlay?.call( + showCaseWidgetState.isShowcaseRunning, + ); } void recalculateRootWidgetSize() { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; final rootWidget = context.findRootAncestorStateOfType>(); - showcaseController.rootRenderObject = - rootWidget?.context.findRenderObject() as RenderBox?; - showcaseController.rootWidgetSize = rootWidget == null - ? MediaQuery.of(context).size - : showcaseController.rootRenderObject?.size; + showcaseController + ..rootRenderObject = + rootWidget?.context.findRenderObject() as RenderBox? + ..rootWidgetSize = rootWidget == null + ? MediaQuery.of(context).size + : showcaseController.rootRenderObject?.size; if (!enableShowcase) return; _updateControllerData(context); - showCaseWidgetState.updateOverlay?.call(); + showCaseWidgetState.updateOverlay?.call( + showCaseWidgetState.isShowcaseRunning, + ); }); } @@ -704,76 +721,74 @@ class _ShowcaseState extends State { Rect rectBound, Size screenSize, ) { - var blur = 0.0; - - blur = widget.blurValue ?? showCaseWidgetState.blurValue; - - blur = kIsWeb && blur < 0 ? 0 : blur; + var blur = kIsWeb + ? 0.0 + : max(0.0, widget.blurValue ?? showCaseWidgetState.blurValue); showcaseController ..position = position! ..blur = blur - ..getToolTipWidget = [ - if (showcaseController.isScrollRunning) - Center(child: widget.scrollLoadingWidget), - if (!showcaseController.isScrollRunning) ...[ - _TargetWidget( - offset: rectBound.topLeft, - size: size, - onTap: _getOnTargetTap, - radius: widget.targetBorderRadius, - onDoubleTap: widget.onTargetDoubleTap, - onLongPress: widget.onTargetLongPress, - shapeBorder: widget.targetShapeBorder, - disableDefaultChildGestures: widget.disableDefaultTargetGestures, - targetPadding: widget.targetPadding, - ), - ToolTipWidget( - key: ValueKey(showcaseController.hashCode), - position: position, - title: widget.title, - titleTextAlign: widget.titleTextAlign, - description: widget.description, - descriptionTextAlign: widget.descriptionTextAlign, - titleAlignment: widget.titleAlignment, - descriptionAlignment: widget.descriptionAlignment, - titleTextStyle: widget.titleTextStyle, - descTextStyle: widget.descTextStyle, - container: widget.container, - tooltipBackgroundColor: widget.tooltipBackgroundColor, - textColor: widget.textColor, - showArrow: widget.showArrow, - onTooltipTap: - widget.disposeOnTap == true || widget.onToolTipClick != null - ? _getOnTooltipTap - : null, - tooltipPadding: widget.tooltipPadding, - disableMovingAnimation: widget.disableMovingAnimation ?? - showCaseWidgetState.disableMovingAnimation, - disableScaleAnimation: widget.container != null - ? true - : widget.disableScaleAnimation ?? - showCaseWidgetState.disableScaleAnimation, - movingAnimationDuration: widget.movingAnimationDuration, - tooltipBorderRadius: widget.tooltipBorderRadius, - scaleAnimationDuration: widget.scaleAnimationDuration, - scaleAnimationCurve: widget.scaleAnimationCurve, - scaleAnimationAlignment: widget.scaleAnimationAlignment, - tooltipPosition: widget.tooltipPosition, - titlePadding: widget.titlePadding, - descriptionPadding: widget.descriptionPadding, - titleTextDirection: widget.titleTextDirection, - descriptionTextDirection: widget.descriptionTextDirection, - toolTipSlideEndDistance: widget.toolTipSlideEndDistance, - toolTipMargin: widget.toolTipMargin, - tooltipActionConfig: _getTooltipActionConfig(), - tooltipActions: _getTooltipActions(), - targetPadding: widget.targetPadding, - showcaseController: showcaseController, - ), - if (_getFloatingActionWidget != null) _getFloatingActionWidget!, - ], - ]; + ..getToolTipWidget = showcaseController.isScrollRunning + ? [ + Center(child: widget.scrollLoadingWidget), + ] + : [ + _TargetWidget( + offset: rectBound.topLeft, + size: size, + onTap: _getOnTargetTap, + radius: widget.targetBorderRadius, + onDoubleTap: widget.onTargetDoubleTap, + onLongPress: widget.onTargetLongPress, + shapeBorder: widget.targetShapeBorder, + disableDefaultChildGestures: + widget.disableDefaultTargetGestures, + targetPadding: widget.targetPadding, + ), + ToolTipWidget( + key: ValueKey(showcaseController.showcaseId), + position: position, + title: widget.title, + titleTextAlign: widget.titleTextAlign, + description: widget.description, + descriptionTextAlign: widget.descriptionTextAlign, + titleAlignment: widget.titleAlignment, + descriptionAlignment: widget.descriptionAlignment, + titleTextStyle: widget.titleTextStyle, + descTextStyle: widget.descTextStyle, + container: widget.container, + tooltipBackgroundColor: widget.tooltipBackgroundColor, + textColor: widget.textColor, + showArrow: widget.showArrow, + onTooltipTap: + widget.disposeOnTap == true || widget.onToolTipClick != null + ? _getOnTooltipTap + : null, + tooltipPadding: widget.tooltipPadding, + disableMovingAnimation: widget.disableMovingAnimation ?? + showCaseWidgetState.disableMovingAnimation, + disableScaleAnimation: (widget.disableScaleAnimation ?? + showCaseWidgetState.disableScaleAnimation) || + widget.container != null, + movingAnimationDuration: widget.movingAnimationDuration, + tooltipBorderRadius: widget.tooltipBorderRadius, + scaleAnimationDuration: widget.scaleAnimationDuration, + scaleAnimationCurve: widget.scaleAnimationCurve, + scaleAnimationAlignment: widget.scaleAnimationAlignment, + tooltipPosition: widget.tooltipPosition, + titlePadding: widget.titlePadding, + descriptionPadding: widget.descriptionPadding, + titleTextDirection: widget.titleTextDirection, + descriptionTextDirection: widget.descriptionTextDirection, + toolTipSlideEndDistance: widget.toolTipSlideEndDistance, + toolTipMargin: widget.toolTipMargin, + tooltipActionConfig: _getTooltipActionConfig(), + tooltipActions: _getTooltipActions(), + targetPadding: widget.targetPadding, + showcaseController: showcaseController, + ), + if (_getFloatingActionWidget != null) _getFloatingActionWidget!, + ]; } Widget? get _getFloatingActionWidget => @@ -827,23 +842,26 @@ class _ShowcaseState extends State { showcaseController.rootWidgetSize ?? MediaQuery.of(context).size; position = GetPosition( rootRenderObject: showcaseController.rootRenderObject, - context: context, + renderBox: context.findRenderObject() as RenderBox?, padding: widget.targetPadding, screenWidth: size.width, screenHeight: size.height, ); - showcaseController.position = position!; - showcaseController.linkedShowcaseDataModel = LinkedShowcaseDataModel( - rect: - showcaseController.isScrollRunning ? Rect.zero : position!.getRect(), - radius: _targetBorderRadius, - overlayPadding: - showcaseController.isScrollRunning ? EdgeInsets.zero : _targetPadding, - isCircle: _isCircle, - ); + showcaseController + ..position = position! + ..linkedShowcaseDataModel = LinkedShowcaseDataModel( + rect: showcaseController.isScrollRunning + ? Rect.zero + : position!.getRect(), + radius: _targetBorderRadius, + overlayPadding: showcaseController.isScrollRunning + ? EdgeInsets.zero + : _targetPadding, + isCircle: _isCircle, + ); buildOverlayOnTarget( - position!.getOffSet(), + position!.getOffset(), position!.getRect().size, position!.getRect(), size, diff --git a/lib/src/showcase/showcase_controller.dart b/lib/src/showcase/showcase_controller.dart index 8dc4c500..c13780b9 100644 --- a/lib/src/showcase/showcase_controller.dart +++ b/lib/src/showcase/showcase_controller.dart @@ -21,7 +21,7 @@ class ShowcaseController { final GlobalKey showcaseKey; /// Configuration for the showcase - Showcase? showcaseConfig; + Showcase showcaseConfig; /// Position getter for the showcase GetPosition? position; diff --git a/lib/src/showcase_widget.dart b/lib/src/showcase_widget.dart index 46bddfe1..9726b7d8 100644 --- a/lib/src/showcase_widget.dart +++ b/lib/src/showcase_widget.dart @@ -193,7 +193,7 @@ class ShowCaseWidgetState extends State { late final List? globalTooltipActions; - Map> showcaseControllers = {}; + final Map> _showcaseControllers = {}; /// These properties are only here so that it can be accessed by /// [Showcase] @@ -215,12 +215,14 @@ class ShowCaseWidgetState extends State { bool get isShowCaseCompleted => ids == null && activeWidgetId == null; + Duration get scrollDuration => widget.scrollDuration; + List get hiddenFloatingActionKeys => _hideFloatingWidgetKeys.keys.toList(); Timer? _timer; - VoidCallback? updateOverlay; + ValueSetter? updateOverlay; /// This Stores keys of showcase for which we will hide the /// [globalFloatingActionWidget]. @@ -242,6 +244,8 @@ class ShowCaseWidgetState extends State { } } + bool get isShowcaseRunning => getCurrentActiveShowcaseKey != null; + /// Return a [widget.globalFloatingActionWidget] if not need to hide this for /// current showcase. FloatingActionBuilderCallback? globalFloatingActionWidget( @@ -275,10 +279,9 @@ class ShowCaseWidgetState extends State { @override Widget build(BuildContext context) { return OverlayBuilder( - showOverlay: getCurrentActiveShowcaseKey != null, updateOverlay: (updateOverlays) => updateOverlay = updateOverlays, overlayBuilder: (_) { - final controller = showcaseControllers[getCurrentActiveShowcaseKey] ?? + final controller = _showcaseControllers[getCurrentActiveShowcaseKey] ?? []; if (getCurrentActiveShowcaseKey == null || controller.isEmpty) { @@ -286,23 +289,19 @@ class ShowCaseWidgetState extends State { } final firstController = controller.first; - final firstShowcaseConfig = firstController.showcaseConfig!; - final listOfLinkedModel = []; - final controllerLength = controller.length; - for (var i = 0; i < controllerLength; i++) { - final model = controller[i].linkedShowcaseDataModel; - if (model == null) continue; - listOfLinkedModel.add(model); - } - final backgroundContainer = getBackgroundOverlayContainer( - overlayColor: firstShowcaseConfig.overlayColor, - overlayOpacity: firstShowcaseConfig.overlayOpacity, + final firstShowcaseConfig = firstController.showcaseConfig; + + final backgroundContainer = ColoredBox( + color: firstShowcaseConfig.overlayColor + + //TODO: Update when we remove support for older version + //ignore: deprecated_member_use + .withOpacity(firstShowcaseConfig.overlayOpacity), + child: const Align( + alignment: Alignment.center, + ), ); - final tooltipWidgets = []; - for (var i = 0; i < controllerLength; i++) { - tooltipWidgets.addAll(controller[i].getToolTipWidget); - } return Stack( children: [ GestureDetector( @@ -313,20 +312,20 @@ class ShowCaseWidgetState extends State { isCircle: false, radius: BorderRadius.zero, overlayPadding: EdgeInsets.zero, - linkedObjectData: listOfLinkedModel, + linkedObjectData: _getLinkedShowcaseData(controller), ), - child: firstController.blur != 0 - ? BackdropFilter( + child: firstController.blur == 0 + ? backgroundContainer + : BackdropFilter( filter: ImageFilter.blur( sigmaX: firstController.blur, sigmaY: firstController.blur, ), child: backgroundContainer, - ) - : backgroundContainer, + ), ), ), - ...tooltipWidgets, + ...controller.expand((object) => object.getToolTipWidget).toList(), ], ); }, @@ -334,6 +333,21 @@ class ShowCaseWidgetState extends State { ); } + List _getLinkedShowcaseData( + List controller, + ) { + final listOfLinkedModel = []; + + final controllerLength = controller.length; + + for (var i = 0; i < controllerLength; i++) { + final model = controller[i].linkedShowcaseDataModel; + if (model == null) continue; + listOfLinkedModel.add(model); + } + return listOfLinkedModel; + } + void _updateRootWidget() { if (!mounted) return; final rootWidget = context.findRootAncestorStateOfType>(); @@ -351,22 +365,6 @@ class ShowCaseWidgetState extends State { } } - Widget getBackgroundOverlayContainer({ - required Color overlayColor, - required double overlayOpacity, - }) { - return Container( - alignment: Alignment.center, - decoration: BoxDecoration( - color: overlayColor - - //TODO: Update when we remove support for older version - //ignore: deprecated_member_use - .withOpacity(overlayOpacity), - ), - ); - } - void initRootWidget() { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; @@ -389,17 +387,10 @@ class ShowCaseWidgetState extends State { ); } if (!mounted) return; - setState(() { - ids = widgetIds; - activeWidgetId = 0; - updateOverlay?.call(); - _onStart(); - }); - WidgetsBinding.instance.addPostFrameCallback( - (_) { - updateOverlay?.call(); - }, - ); + ids = widgetIds; + activeWidgetId = 0; + _onStart(); + updateOverlay?.call(isShowcaseRunning); } /// Completes showcase of given key and starts next one @@ -415,17 +406,21 @@ class ShowCaseWidgetState extends State { _cleanupAfterSteps(); widget.onFinish?.call(); } - updateOverlay?.call(); + updateOverlay?.call(isShowcaseRunning); } } } /// Completes current active showcase and starts next one /// otherwise will finish the entire showcase view - void next({bool fromAutoPlayOrAction = false}) async { + /// + /// if [forceNext] is true then it will ignore the [enableAutoPlayLock] and + /// move to next showcase. This is default behaviour for + /// [TooltipDefaultActionType.next] + void next({bool forceNext = false}) async { // If this call is from autoPlay timer or action widget we will override the // enableAutoPlayLock so user can move forward in showcase - if (!fromAutoPlayOrAction && widget.enableAutoPlayLock) return; + if (!forceNext && widget.enableAutoPlayLock) return; if (ids != null && mounted) { await _onComplete(); @@ -436,7 +431,7 @@ class ShowCaseWidgetState extends State { _cleanupAfterSteps(); widget.onFinish?.call(); } - updateOverlay?.call(); + updateOverlay?.call(isShowcaseRunning); } } } @@ -453,7 +448,7 @@ class ShowCaseWidgetState extends State { _cleanupAfterSteps(); widget.onFinish?.call(); } - updateOverlay?.call(); + updateOverlay?.call(isShowcaseRunning); } } } @@ -469,17 +464,17 @@ class ShowCaseWidgetState extends State { widget.onDismiss?.call(idNotExist ? null : ids?[activeWidgetId!]); if (mounted) { _cleanupAfterSteps.call(); - updateOverlay?.call(); + updateOverlay?.call(isShowcaseRunning); } } Future _onStart() async { if (activeWidgetId! < ids!.length) { widget.onStart?.call(activeWidgetId, ids![activeWidgetId!]); - final controllers = showcaseControllers[getCurrentActiveShowcaseKey] ?? + final controllers = _showcaseControllers[getCurrentActiveShowcaseKey] ?? []; if (controllers.length == 1 && - (controllers.first.showcaseConfig?.enableAutoScroll ?? + (controllers.first.showcaseConfig.enableAutoScroll ?? widget.enableAutoScroll)) { await controllers.first.scrollIntoView?.call(); } else { @@ -490,10 +485,10 @@ class ShowCaseWidgetState extends State { } } if (widget.autoPlay) { - _stopTimer(); + _cancelTimer(); _timer = Timer( Duration(seconds: widget.autoPlayDelay.inSeconds), - () => next(fromAutoPlayOrAction: true), + () => next(forceNext: true), ); } } @@ -501,13 +496,13 @@ class ShowCaseWidgetState extends State { Future _onComplete() async { final futures = []; final currentControllers = - (showcaseControllers[getCurrentActiveShowcaseKey] ?? + (_showcaseControllers[getCurrentActiveShowcaseKey] ?? []); final controllerLength = currentControllers.length; for (var i = 0; i < controllerLength; i++) { final controller = currentControllers[i]; - if ((controller.showcaseConfig?.disableScaleAnimation ?? + if ((controller.showcaseConfig.disableScaleAnimation ?? widget.disableScaleAnimation) || controller.reverseAnimation == null) { continue; @@ -517,11 +512,11 @@ class ShowCaseWidgetState extends State { await Future.wait(futures); widget.onComplete?.call(activeWidgetId, ids![activeWidgetId!]); if (widget.autoPlay) { - _stopTimer(); + _cancelTimer(); } } - void _stopTimer() { + void _cancelTimer() { if (!(_timer?.isActive ?? false)) return; _timer?.cancel(); _timer = null; @@ -530,7 +525,7 @@ class ShowCaseWidgetState extends State { void _cleanupAfterSteps() { ids = null; activeWidgetId = null; - _stopTimer(); + _cancelTimer(); } /// Disables the [globalFloatingActionWidget] for the provided keys. @@ -541,4 +536,26 @@ class ShowCaseWidgetState extends State { ..clear() ..addAll({for (final item in updatedList) item: true}); } + + List? getShowcaseController(GlobalKey key) { + return _showcaseControllers[key]; + } + + void registerShowcaseController({ + required GlobalKey key, + required ShowcaseController controller, + }) { + if (_showcaseControllers.containsKey(key)) { + _showcaseControllers[key]!.add(controller); + } else { + _showcaseControllers[key] = [controller]; + } + } + + void removeShowcaseController({ + required GlobalKey key, + required ShowcaseController controller, + }) { + _showcaseControllers[key]?.remove(controller); + } } From b476dc867112671c7032c90b6bc53cf7b9cb5cc8 Mon Sep 17 00:00:00 2001 From: Sahil-Simform Date: Fri, 28 Mar 2025 13:36:09 +0530 Subject: [PATCH 4/9] =?UTF-8?q?fix:=20=F0=9F=94=A8Fixed=20PR=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/constants.dart | 2 +- lib/src/enum.dart | 2 +- lib/src/get_position.dart | 24 ++--- lib/src/showcase/showcase.dart | 130 ++++++++++++------------ lib/src/showcase_widget.dart | 150 +++++++++++++++------------- lib/src/tooltip/tooltip.dart | 1 - lib/src/tooltip/tooltip_widget.dart | 7 +- 7 files changed, 164 insertions(+), 152 deletions(-) diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 17914a25..bcba9101 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -28,7 +28,7 @@ class Constants { /// in bottom static const double extraAlignmentOffset = 5; - static const defaultTargetRadius = Radius.circular(3.0); + static const Radius defaultTargetRadius = Radius.circular(3.0); static const ShapeBorder defaultTargetShapeBorder = RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8)), diff --git a/lib/src/enum.dart b/lib/src/enum.dart index e26a014b..6669e688 100644 --- a/lib/src/enum.dart +++ b/lib/src/enum.dart @@ -94,7 +94,7 @@ enum TooltipDefaultActionType { void onTap(ShowCaseWidgetState showCaseState) { switch (this) { case TooltipDefaultActionType.next: - showCaseState.next(forceNext: true); + showCaseState.next(force: true); break; case TooltipDefaultActionType.previous: showCaseState.previous(); diff --git a/lib/src/get_position.dart b/lib/src/get_position.dart index 89c46fd5..25fd52da 100644 --- a/lib/src/get_position.dart +++ b/lib/src/get_position.dart @@ -41,23 +41,19 @@ class GetPosition { final double screenHeight; final RenderObject? rootRenderObject; - RenderBox? _box; Offset? _boxOffset; - RenderBox? get box => _box; - void _getRenderBox() { if (renderBox == null) return; - _box = renderBox; - _boxOffset = _box?.localToGlobal( + _boxOffset = renderBox?.localToGlobal( Offset.zero, ancestor: rootRenderObject, ); } bool _checkBoxOrOffsetIsNull({bool checkDy = false, bool checkDx = false}) { - return _box == null || + return renderBox == null || _boxOffset == null || (checkDx && (_boxOffset?.dx.isNaN ?? true)) || (checkDy && (_boxOffset?.dy.isNaN ?? true)); @@ -67,8 +63,8 @@ class GetPosition { if (_checkBoxOrOffsetIsNull(checkDy: true, checkDx: true)) { return Rect.zero; } - final topLeft = _box!.size.topLeft(_boxOffset!); - final bottomRight = _box!.size.bottomRight(_boxOffset!); + final topLeft = renderBox!.size.topLeft(_boxOffset!); + final bottomRight = renderBox!.size.bottomRight(_boxOffset!); final leftDx = topLeft.dx - padding.left; var leftDy = topLeft.dy - padding.top; if (leftDy < 0) leftDy = 0; @@ -86,7 +82,7 @@ class GetPosition { if (_checkBoxOrOffsetIsNull(checkDy: true)) { return padding.bottom; } - final bottomRight = _box!.size.bottomRight(_boxOffset!); + final bottomRight = renderBox!.size.bottomRight(_boxOffset!); return bottomRight.dy + padding.bottom; } @@ -95,7 +91,7 @@ class GetPosition { if (_checkBoxOrOffsetIsNull(checkDy: true)) { return -padding.top; } - final topLeft = _box!.size.topLeft(_boxOffset!); + final topLeft = renderBox!.size.topLeft(_boxOffset!); return topLeft.dy - padding.top; } @@ -104,7 +100,7 @@ class GetPosition { if (_checkBoxOrOffsetIsNull(checkDx: true)) { return -padding.left; } - final topLeft = _box!.size.topLeft(_boxOffset!); + final topLeft = renderBox!.size.topLeft(_boxOffset!); return topLeft.dx - padding.left; } @@ -113,7 +109,7 @@ class GetPosition { if (_checkBoxOrOffsetIsNull(checkDx: true)) { return padding.right; } - final bottomRight = _box!.size.bottomRight(_boxOffset!); + final bottomRight = renderBox!.size.bottomRight(_boxOffset!); return bottomRight.dx + padding.right; } @@ -124,7 +120,7 @@ class GetPosition { double getCenter() => (getLeft() + getRight()) * 0.5; Offset topLeft() { - final box = _box; + final box = renderBox; if (box == null) return Offset.zero; return box.size.topLeft( @@ -135,5 +131,5 @@ class GetPosition { ); } - Offset getOffset() => _box?.size.center(topLeft()) ?? Offset.zero; + Offset getOffset() => renderBox?.size.center(topLeft()) ?? Offset.zero; } diff --git a/lib/src/showcase/showcase.dart b/lib/src/showcase/showcase.dart index 426fa552..bfd09cfd 100644 --- a/lib/src/showcase/showcase.dart +++ b/lib/src/showcase/showcase.dart @@ -564,10 +564,11 @@ class Showcase extends StatefulWidget { } class _ShowcaseState extends State { - bool enableShowcase = true; - GetPosition? position; - - late ShowcaseController showcaseController; + ShowcaseController get _controller => + showCaseWidgetState.getControllerForShowcase( + widget.showcaseKey, + _uniqueId, + ); late final showCaseWidgetState = ShowCaseWidget.of(context); FloatingActionWidget? _globalFloatingActionWidget; @@ -578,12 +579,14 @@ class _ShowcaseState extends State { EdgeInsets get _targetPadding => widget.targetPadding; + final int _uniqueId = UniqueKey().hashCode; + @override void initState() { super.initState(); initRootWidget(); - showcaseController = ShowcaseController( - showcaseId: widget.hashCode, + final showcaseController = ShowcaseController( + showcaseId: _uniqueId, showcaseKey: widget.showcaseKey, showcaseConfig: widget, scrollIntoView: _scrollIntoView, @@ -592,21 +595,20 @@ class _ShowcaseState extends State { showCaseWidgetState.registerShowcaseController( controller: showcaseController, key: widget.showcaseKey, + showcaseId: _uniqueId, ); } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - showcaseController.showcaseConfig = widget; - } - @override void didUpdateWidget(covariant Showcase oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget != widget) { - showcaseController.showcaseConfig = widget; - } + if (oldWidget == widget) return; + showCaseWidgetState + .getControllerForShowcase( + widget.showcaseKey, + _uniqueId, + ) + .showcaseConfig = widget; } @override @@ -619,7 +621,7 @@ class _ShowcaseState extends State { void dispose() { showCaseWidgetState.removeShowcaseController( key: widget.showcaseKey, - controller: showcaseController, + uniqueShowcaseKey: _uniqueId, ); super.dispose(); @@ -628,36 +630,37 @@ class _ShowcaseState extends State { void initRootWidget() { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - showcaseController + _controller ..rootWidgetSize = showCaseWidgetState.rootWidgetSize ..rootRenderObject = showCaseWidgetState.rootRenderObject; }); } void startShowcase() { - enableShowcase = showCaseWidgetState.enableShowcase; + if (!showCaseWidgetState.enableShowcase) return; recalculateRootWidgetSize(); - if (enableShowcase) { - _globalFloatingActionWidget = showCaseWidgetState - .globalFloatingActionWidget(widget.showcaseKey) - ?.call(context); - final size = MediaQuery.of(context).size; - position ??= GetPosition( - rootRenderObject: showcaseController.rootRenderObject, - renderBox: context.findRenderObject() as RenderBox?, - padding: widget.targetPadding, - screenWidth: showcaseController.rootWidgetSize?.width ?? size.width, - screenHeight: showcaseController.rootWidgetSize?.height ?? size.height, - ); - } + _globalFloatingActionWidget = showCaseWidgetState + .globalFloatingActionWidget(widget.showcaseKey) + ?.call(context); + final size = _controller.rootWidgetSize ?? MediaQuery.sizeOf(context); + _controller.position ??= GetPosition( + rootRenderObject: _controller.rootRenderObject, + renderBox: context.findRenderObject() as RenderBox?, + padding: widget.targetPadding, + screenWidth: size.width, + screenHeight: size.height, + ); } Future _scrollIntoView() async { if (!mounted) return; - showcaseController.isScrollRunning = true; - _updateControllerData(context); + _controller.isScrollRunning = true; + _updateControllerData( + context.findRenderObject() as RenderBox?, + MediaQuery.sizeOf(context), + ); startShowcase(); showCaseWidgetState.updateOverlay?.call( showCaseWidgetState.isShowcaseRunning, @@ -668,8 +671,11 @@ class _ShowcaseState extends State { alignment: widget.scrollAlignment, ); if (!mounted) return; - showcaseController.isScrollRunning = false; - _updateControllerData(context); + _controller.isScrollRunning = false; + _updateControllerData( + context.findRenderObject() as RenderBox?, + MediaQuery.sizeOf(context), + ); startShowcase(); showCaseWidgetState.updateOverlay?.call( showCaseWidgetState.isShowcaseRunning, @@ -680,14 +686,17 @@ class _ShowcaseState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; final rootWidget = context.findRootAncestorStateOfType>(); - showcaseController + _controller ..rootRenderObject = rootWidget?.context.findRenderObject() as RenderBox? ..rootWidgetSize = rootWidget == null ? MediaQuery.of(context).size - : showcaseController.rootRenderObject?.size; - if (!enableShowcase) return; - _updateControllerData(context); + : _controller.rootRenderObject?.size; + if (!showCaseWidgetState.enableShowcase) return; + _updateControllerData( + context.findRenderObject() as RenderBox?, + MediaQuery.sizeOf(context), + ); showCaseWidgetState.updateOverlay?.call( showCaseWidgetState.isShowcaseRunning, ); @@ -725,10 +734,9 @@ class _ShowcaseState extends State { ? 0.0 : max(0.0, widget.blurValue ?? showCaseWidgetState.blurValue); - showcaseController - ..position = position! + _controller ..blur = blur - ..getToolTipWidget = showcaseController.isScrollRunning + ..getToolTipWidget = _controller.isScrollRunning ? [ Center(child: widget.scrollLoadingWidget), ] @@ -746,8 +754,7 @@ class _ShowcaseState extends State { targetPadding: widget.targetPadding, ), ToolTipWidget( - key: ValueKey(showcaseController.showcaseId), - position: position, + key: ValueKey(_controller.showcaseId), title: widget.title, titleTextAlign: widget.titleTextAlign, description: widget.description, @@ -785,7 +792,7 @@ class _ShowcaseState extends State { tooltipActionConfig: _getTooltipActionConfig(), tooltipActions: _getTooltipActions(), targetPadding: widget.targetPadding, - showcaseController: showcaseController, + showcaseController: _controller, ), if (_getFloatingActionWidget != null) _getFloatingActionWidget!, ]; @@ -837,33 +844,32 @@ class _ShowcaseState extends State { const TooltipActionConfig(); } - void _updateControllerData(BuildContext context) { - final size = - showcaseController.rootWidgetSize ?? MediaQuery.of(context).size; - position = GetPosition( - rootRenderObject: showcaseController.rootRenderObject, - renderBox: context.findRenderObject() as RenderBox?, + void _updateControllerData( + RenderBox? renderBox, + Size screenSize, + ) { + final size = _controller.rootWidgetSize ?? screenSize; + final position = GetPosition( + rootRenderObject: _controller.rootRenderObject, + renderBox: renderBox, padding: widget.targetPadding, screenWidth: size.width, screenHeight: size.height, ); - showcaseController - ..position = position! + _controller + ..position = position ..linkedShowcaseDataModel = LinkedShowcaseDataModel( - rect: showcaseController.isScrollRunning - ? Rect.zero - : position!.getRect(), + rect: _controller.isScrollRunning ? Rect.zero : position.getRect(), radius: _targetBorderRadius, - overlayPadding: showcaseController.isScrollRunning - ? EdgeInsets.zero - : _targetPadding, + overlayPadding: + _controller.isScrollRunning ? EdgeInsets.zero : _targetPadding, isCircle: _isCircle, ); buildOverlayOnTarget( - position!.getOffset(), - position!.getRect().size, - position!.getRect(), + position.getOffset(), + position.getRect().size, + position.getRect(), size, ); } diff --git a/lib/src/showcase_widget.dart b/lib/src/showcase_widget.dart index 9726b7d8..edf3a9fd 100644 --- a/lib/src/showcase_widget.dart +++ b/lib/src/showcase_widget.dart @@ -193,7 +193,7 @@ class ShowCaseWidgetState extends State { late final List? globalTooltipActions; - final Map> _showcaseControllers = {}; + final Map> _showcaseControllers = {}; /// These properties are only here so that it can be accessed by /// [Showcase] @@ -244,6 +244,11 @@ class ShowCaseWidgetState extends State { } } + List get _getCurrentActiveControllers { + return _showcaseControllers[getCurrentActiveShowcaseKey]?.values.toList() ?? + []; + } + bool get isShowcaseRunning => getCurrentActiveShowcaseKey != null; /// Return a [widget.globalFloatingActionWidget] if not need to hide this for @@ -281,8 +286,7 @@ class ShowCaseWidgetState extends State { return OverlayBuilder( updateOverlay: (updateOverlays) => updateOverlay = updateOverlays, overlayBuilder: (_) { - final controller = _showcaseControllers[getCurrentActiveShowcaseKey] ?? - []; + final controller = _getCurrentActiveControllers; if (getCurrentActiveShowcaseKey == null || controller.isEmpty) { return const SizedBox.shrink(); @@ -312,7 +316,7 @@ class ShowCaseWidgetState extends State { isCircle: false, radius: BorderRadius.zero, overlayPadding: EdgeInsets.zero, - linkedObjectData: _getLinkedShowcaseData(controller), + linkedObjectData: _getLinkedShowcasesData(controller), ), child: firstController.blur == 0 ? backgroundContainer @@ -333,19 +337,15 @@ class ShowCaseWidgetState extends State { ); } - List _getLinkedShowcaseData( - List controller, + List _getLinkedShowcasesData( + List controllers, ) { - final listOfLinkedModel = []; - - final controllerLength = controller.length; - - for (var i = 0; i < controllerLength; i++) { - final model = controller[i].linkedShowcaseDataModel; - if (model == null) continue; - listOfLinkedModel.add(model); - } - return listOfLinkedModel; + final controllerLength = controllers.length; + return [ + for (var i = 0; i < controllerLength; i++) + if (controllers[i].linkedShowcaseDataModel != null) + controllers[i].linkedShowcaseDataModel!, + ]; } void _updateRootWidget() { @@ -359,10 +359,11 @@ class ShowCaseWidgetState extends State { void _barrierOnTap(Showcase firstShowcaseConfig) { firstShowcaseConfig.onBarrierClick?.call(); - if (!disableBarrierInteraction && - !firstShowcaseConfig.disableBarrierInteraction) { - next(); + if (disableBarrierInteraction || + firstShowcaseConfig.disableBarrierInteraction) { + return; } + next(); } void initRootWidget() { @@ -398,58 +399,56 @@ class ShowCaseWidgetState extends State { Future completed(GlobalKey? key) async { if (ids != null && ids![activeWidgetId!] == key && mounted) { await _onComplete(); - if (mounted) { - activeWidgetId = activeWidgetId! + 1; - _onStart(); + if (!mounted) return; + activeWidgetId = activeWidgetId! + 1; + _onStart(); - if (activeWidgetId! >= ids!.length) { - _cleanupAfterSteps(); - widget.onFinish?.call(); - } - updateOverlay?.call(isShowcaseRunning); + if (activeWidgetId! >= ids!.length) { + _cleanupAfterSteps(); + widget.onFinish?.call(); } + updateOverlay?.call(isShowcaseRunning); } } /// Completes current active showcase and starts next one /// otherwise will finish the entire showcase view /// - /// if [forceNext] is true then it will ignore the [enableAutoPlayLock] and + /// if [force] is true then it will ignore the [enableAutoPlayLock] and /// move to next showcase. This is default behaviour for /// [TooltipDefaultActionType.next] - void next({bool forceNext = false}) async { + Future next({bool force = false}) async { // If this call is from autoPlay timer or action widget we will override the // enableAutoPlayLock so user can move forward in showcase - if (!forceNext && widget.enableAutoPlayLock) return; + if (!force && widget.enableAutoPlayLock) return; if (ids != null && mounted) { await _onComplete(); - if (mounted) { - activeWidgetId = activeWidgetId! + 1; - _onStart(); - if (activeWidgetId! >= ids!.length) { - _cleanupAfterSteps(); - widget.onFinish?.call(); - } - updateOverlay?.call(isShowcaseRunning); + if (!mounted) return; + activeWidgetId = activeWidgetId! + 1; + _onStart(); + if (activeWidgetId! >= ids!.length) { + _cleanupAfterSteps(); + widget.onFinish?.call(); } + updateOverlay?.call(isShowcaseRunning); } } /// Completes current active showcase and starts previous one /// otherwise will finish the entire showcase view - void previous() async { + Future previous() async { if (ids != null && ((activeWidgetId ?? 0) - 1) >= 0 && mounted) { await _onComplete(); - if (mounted) { - activeWidgetId = activeWidgetId! - 1; - _onStart(); - if (activeWidgetId! >= ids!.length) { - _cleanupAfterSteps(); - widget.onFinish?.call(); - } - updateOverlay?.call(isShowcaseRunning); + if (!mounted) return; + + activeWidgetId = activeWidgetId! - 1; + _onStart(); + if (activeWidgetId! >= ids!.length) { + _cleanupAfterSteps(); + widget.onFinish?.call(); } + updateOverlay?.call(isShowcaseRunning); } } @@ -462,17 +461,16 @@ class ShowCaseWidgetState extends State { activeWidgetId == null || ids == null || ids!.length < activeWidgetId!; widget.onDismiss?.call(idNotExist ? null : ids?[activeWidgetId!]); - if (mounted) { - _cleanupAfterSteps.call(); - updateOverlay?.call(isShowcaseRunning); - } + if (!mounted) return; + + _cleanupAfterSteps.call(); + updateOverlay?.call(isShowcaseRunning); } Future _onStart() async { if (activeWidgetId! < ids!.length) { widget.onStart?.call(activeWidgetId, ids![activeWidgetId!]); - final controllers = _showcaseControllers[getCurrentActiveShowcaseKey] ?? - []; + final controllers = _getCurrentActiveControllers; if (controllers.length == 1 && (controllers.first.showcaseConfig.enableAutoScroll ?? widget.enableAutoScroll)) { @@ -488,16 +486,14 @@ class ShowCaseWidgetState extends State { _cancelTimer(); _timer = Timer( Duration(seconds: widget.autoPlayDelay.inSeconds), - () => next(forceNext: true), + () => next(force: true), ); } } Future _onComplete() async { final futures = []; - final currentControllers = - (_showcaseControllers[getCurrentActiveShowcaseKey] ?? - []); + final currentControllers = _getCurrentActiveControllers; final controllerLength = currentControllers.length; for (var i = 0; i < controllerLength; i++) { @@ -534,28 +530,44 @@ class ShowCaseWidgetState extends State { ) { _hideFloatingWidgetKeys ..clear() - ..addAll({for (final item in updatedList) item: true}); - } - - List? getShowcaseController(GlobalKey key) { - return _showcaseControllers[key]; + ..addAll({ + for (final item in updatedList) item: true, + }); } void registerShowcaseController({ required GlobalKey key, required ShowcaseController controller, + required int showcaseId, }) { - if (_showcaseControllers.containsKey(key)) { - _showcaseControllers[key]!.add(controller); - } else { - _showcaseControllers[key] = [controller]; - } + _showcaseControllers + .putIfAbsent( + key, + () => {}, + ) + .update( + showcaseId, + (value) => controller, + ifAbsent: () => controller, + ); } void removeShowcaseController({ required GlobalKey key, - required ShowcaseController controller, + required int uniqueShowcaseKey, }) { - _showcaseControllers[key]?.remove(controller); + _showcaseControllers[key]?.remove(uniqueShowcaseKey); + } + + ShowcaseController getControllerForShowcase( + GlobalKey key, + int showcaseId, + ) { + assert( + _showcaseControllers.containsKey(key) && + _showcaseControllers[key]!.containsKey(showcaseId), + 'Please register showcase controller first', + ); + return _showcaseControllers[key]![showcaseId]!; } } diff --git a/lib/src/tooltip/tooltip.dart b/lib/src/tooltip/tooltip.dart index c4b254d1..3a02fcd9 100644 --- a/lib/src/tooltip/tooltip.dart +++ b/lib/src/tooltip/tooltip.dart @@ -3,7 +3,6 @@ import 'package:flutter/rendering.dart'; import '../constants.dart'; import '../enum.dart'; -import '../get_position.dart'; import '../models/tooltip_action_config.dart'; import '../showcase/showcase_controller.dart'; import '../widget/action_widget.dart'; diff --git a/lib/src/tooltip/tooltip_widget.dart b/lib/src/tooltip/tooltip_widget.dart index 459b81fc..9ecc79b1 100644 --- a/lib/src/tooltip/tooltip_widget.dart +++ b/lib/src/tooltip/tooltip_widget.dart @@ -3,7 +3,6 @@ part of "tooltip.dart"; class ToolTipWidget extends StatefulWidget { const ToolTipWidget({ super.key, - required this.position, required this.title, required this.description, required this.titleTextStyle, @@ -38,7 +37,6 @@ class ToolTipWidget extends StatefulWidget { this.descriptionTextDirection, }); - final GetPosition? position; final String? title; final TextAlign? titleTextAlign; final String? description; @@ -219,8 +217,9 @@ class _ToolTipWidgetState extends State ); // Calculate the target position and size - final targetPosition = widget.position!.box!.localToGlobal(Offset.zero); - final targetSize = widget.position!.box!.size; + final box = widget.showcaseController.position!.renderBox!; + final targetPosition = box.localToGlobal(Offset.zero); + final targetSize = box.size; return Material( type: MaterialType.transparency, From 1aa080dbcee6725fdca8bb7acbe2764f1a2ed558 Mon Sep 17 00:00:00 2001 From: Sahil-Simform Date: Tue, 1 Apr 2025 19:58:19 +0530 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20=F0=9F=94=A8Fixed=20PR=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/constants.dart | 7 +- lib/src/get_position.dart | 4 +- lib/src/layout_overlays.dart | 11 +- lib/src/models/linked_showcase_data.dart | 18 + lib/src/shape_clipper.dart | 3 +- lib/src/showcase/showcase.dart | 356 ++---------------- lib/src/showcase/showcase_controller.dart | 323 +++++++++++++++- lib/src/showcase/target_widget.dart | 108 ++++++ lib/src/showcase_widget.dart | 53 +-- lib/src/tooltip/tooltip_widget.dart | 19 +- .../showcase_circular_progress_indecator.dart | 28 ++ 11 files changed, 555 insertions(+), 375 deletions(-) create mode 100644 lib/src/showcase/target_widget.dart create mode 100644 lib/src/widget/showcase_circular_progress_indecator.dart diff --git a/lib/src/constants.dart b/lib/src/constants.dart index bcba9101..44e94124 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'widget/showcase_circular_progress_indecator.dart'; + class Constants { Constants._(); @@ -34,10 +36,9 @@ class Constants { borderRadius: BorderRadius.all(Radius.circular(8)), ); + static const double cupertinoActivityIndicatorRadius = 12.0; static const Widget defaultProgressIndicator = - CircularProgressIndicator.adaptive( - backgroundColor: Colors.white, - ); + ShowcaseCircularProgressIndicator(); static const Duration defaultAnimationDuration = Duration(milliseconds: 2000); } diff --git a/lib/src/get_position.dart b/lib/src/get_position.dart index 25fd52da..fe6e256a 100644 --- a/lib/src/get_position.dart +++ b/lib/src/get_position.dart @@ -32,7 +32,7 @@ class GetPosition { this.padding = EdgeInsets.zero, this.rootRenderObject, }) { - _getRenderBox(); + _getRenderBoxOffset(); } final RenderBox? renderBox; @@ -43,7 +43,7 @@ class GetPosition { Offset? _boxOffset; - void _getRenderBox() { + void _getRenderBoxOffset() { if (renderBox == null) return; _boxOffset = renderBox?.localToGlobal( diff --git a/lib/src/layout_overlays.dart b/lib/src/layout_overlays.dart index c4f6024c..65c38e84 100644 --- a/lib/src/layout_overlays.dart +++ b/lib/src/layout_overlays.dart @@ -46,6 +46,13 @@ class OverlayBuilder extends StatefulWidget { final WidgetBuilder? overlayBuilder; final Widget child; + + /// A callback that provides a way to control the overlay visibility from + /// showcase widget + /// Basically we pass a reference to [_updateOverlay] function to parent so we + /// can call this from parent class to update the overlay + /// This callback provides a function that can be called with a boolean + /// parameter to show (true) or hide (false) the overlay. final ValueSetter> updateOverlay; @override @@ -141,7 +148,5 @@ class _OverlayBuilderState extends State { } @override - Widget build(BuildContext context) { - return widget.child; - } + Widget build(BuildContext context) => widget.child; } diff --git a/lib/src/models/linked_showcase_data.dart b/lib/src/models/linked_showcase_data.dart index 116865a8..f25b0f1d 100644 --- a/lib/src/models/linked_showcase_data.dart +++ b/lib/src/models/linked_showcase_data.dart @@ -14,4 +14,22 @@ class LinkedShowcaseDataModel { final EdgeInsets overlayPadding; final BorderRadius? radius; final bool isCircle; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LinkedShowcaseDataModel && + runtimeType == other.runtimeType && + rect == other.rect && + radius == other.radius && + overlayPadding == other.overlayPadding && + isCircle == other.isCircle; + + @override + int get hashCode => Object.hash( + rect, + radius, + overlayPadding, + isCircle, + ); } diff --git a/lib/src/shape_clipper.dart b/lib/src/shape_clipper.dart index af6c3d0f..8bb1b9c8 100644 --- a/lib/src/shape_clipper.dart +++ b/lib/src/shape_clipper.dart @@ -22,6 +22,7 @@ import 'dart:ui' as ui; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'constants.dart'; @@ -108,5 +109,5 @@ class RRectClipper extends CustomClipper { radius != oldClipper.radius || overlayPadding != oldClipper.overlayPadding || area != oldClipper.area || - linkedObjectData != oldClipper.linkedObjectData; + !listEquals(linkedObjectData, oldClipper.linkedObjectData); } diff --git a/lib/src/showcase/showcase.dart b/lib/src/showcase/showcase.dart index bfd09cfd..83feceef 100644 --- a/lib/src/showcase/showcase.dart +++ b/lib/src/showcase/showcase.dart @@ -20,21 +20,14 @@ * SOFTWARE. */ -import 'dart:async'; -import 'dart:math'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../constants.dart'; import '../enum.dart'; import '../get_position.dart'; -import '../models/linked_showcase_data.dart'; import '../models/tooltip_action_button.dart'; import '../models/tooltip_action_config.dart'; import '../showcase_widget.dart'; -import '../tooltip/tooltip.dart'; -import '../tooltip_action_button_widget.dart'; import '../widget/floating_action_widget.dart'; import 'showcase_controller.dart'; @@ -565,61 +558,46 @@ class Showcase extends StatefulWidget { class _ShowcaseState extends State { ShowcaseController get _controller => - showCaseWidgetState.getControllerForShowcase( + _showCaseWidgetState.getControllerForShowcase( widget.showcaseKey, _uniqueId, ); - late final showCaseWidgetState = ShowCaseWidget.of(context); - FloatingActionWidget? _globalFloatingActionWidget; - - bool get _isCircle => widget.targetShapeBorder is CircleBorder; - - BorderRadius? get _targetBorderRadius => widget.targetBorderRadius; - - EdgeInsets get _targetPadding => widget.targetPadding; + late var _showCaseWidgetState = ShowCaseWidget.of(context); final int _uniqueId = UniqueKey().hashCode; @override void initState() { super.initState(); - initRootWidget(); - final showcaseController = ShowcaseController( - showcaseId: _uniqueId, - showcaseKey: widget.showcaseKey, - showcaseConfig: widget, - scrollIntoView: _scrollIntoView, - )..startShowcase = startShowcase; - - showCaseWidgetState.registerShowcaseController( - controller: showcaseController, + ShowcaseController( + id: _uniqueId, key: widget.showcaseKey, - showcaseId: _uniqueId, - ); + config: widget, + showCaseWidgetState: ShowCaseWidget.of(context), + scrollIntoViewCallback: scrollIntoView, + ).startShowcase = startShowcase; } @override void didUpdateWidget(covariant Showcase oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget == widget) return; - showCaseWidgetState - .getControllerForShowcase( - widget.showcaseKey, - _uniqueId, - ) - .showcaseConfig = widget; + _showCaseWidgetState = ShowCaseWidget.of(context); + _controller + ..config = widget + ..showCaseWidgetState = _showCaseWidgetState; } @override Widget build(BuildContext context) { - recalculateRootWidgetSize(); + _controller.recalculateRootWidgetSize(context); return widget.child; } @override void dispose() { - showCaseWidgetState.removeShowcaseController( + _showCaseWidgetState.removeShowcaseController( key: widget.showcaseKey, uniqueShowcaseKey: _uniqueId, ); @@ -627,24 +605,15 @@ class _ShowcaseState extends State { super.dispose(); } - void initRootWidget() { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; - _controller - ..rootWidgetSize = showCaseWidgetState.rootWidgetSize - ..rootRenderObject = showCaseWidgetState.rootRenderObject; - }); - } - void startShowcase() { - if (!showCaseWidgetState.enableShowcase) return; + if (!_showCaseWidgetState.enableShowcase) return; - recalculateRootWidgetSize(); - - _globalFloatingActionWidget = showCaseWidgetState - .globalFloatingActionWidget(widget.showcaseKey) - ?.call(context); - final size = _controller.rootWidgetSize ?? MediaQuery.sizeOf(context); + _controller + ..recalculateRootWidgetSize(context) + ..globalFloatingActionWidget = _showCaseWidgetState + .globalFloatingActionWidget(widget.showcaseKey) + ?.call(context); + final size = _controller.rootWidgetSize ?? MediaQuery.of(context).size; _controller.position ??= GetPosition( rootRenderObject: _controller.rootRenderObject, renderBox: context.findRenderObject() as RenderBox?, @@ -654,282 +623,33 @@ class _ShowcaseState extends State { ); } - Future _scrollIntoView() async { + Future scrollIntoView() async { if (!mounted) return; - _controller.isScrollRunning = true; - _updateControllerData( - context.findRenderObject() as RenderBox?, - MediaQuery.sizeOf(context), - ); + _controller + ..isScrollRunning = true + ..updateControllerData( + context.findRenderObject() as RenderBox?, + MediaQuery.of(context).size, + ); startShowcase(); - showCaseWidgetState.updateOverlay?.call( - showCaseWidgetState.isShowcaseRunning, + _showCaseWidgetState.updateOverlay?.call( + _showCaseWidgetState.isShowcaseRunning, ); await Scrollable.ensureVisible( context, - duration: showCaseWidgetState.widget.scrollDuration, + duration: _showCaseWidgetState.widget.scrollDuration, alignment: widget.scrollAlignment, ); if (!mounted) return; - _controller.isScrollRunning = false; - _updateControllerData( - context.findRenderObject() as RenderBox?, - MediaQuery.sizeOf(context), - ); - startShowcase(); - showCaseWidgetState.updateOverlay?.call( - showCaseWidgetState.isShowcaseRunning, - ); - } - - void recalculateRootWidgetSize() { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; - final rootWidget = context.findRootAncestorStateOfType>(); - _controller - ..rootRenderObject = - rootWidget?.context.findRenderObject() as RenderBox? - ..rootWidgetSize = rootWidget == null - ? MediaQuery.of(context).size - : _controller.rootRenderObject?.size; - if (!showCaseWidgetState.enableShowcase) return; - _updateControllerData( - context.findRenderObject() as RenderBox?, - MediaQuery.sizeOf(context), - ); - showCaseWidgetState.updateOverlay?.call( - showCaseWidgetState.isShowcaseRunning, - ); - }); - } - - Future _nextIfAny() async { - if (showCaseWidgetState.isShowCaseCompleted) return; - showCaseWidgetState.completed(widget.showcaseKey); - } - - Future _getOnTargetTap() async { - if (widget.disposeOnTap == true) { - showCaseWidgetState.dismiss(); - widget.onTargetClick!(); - } else { - (widget.onTargetClick ?? _nextIfAny).call(); - } - } - - Future _getOnTooltipTap() async { - if (widget.disposeOnTap == true) { - showCaseWidgetState.dismiss(); - } - widget.onToolTipClick?.call(); - } - - void buildOverlayOnTarget( - Offset offset, - Size size, - Rect rectBound, - Size screenSize, - ) { - var blur = kIsWeb - ? 0.0 - : max(0.0, widget.blurValue ?? showCaseWidgetState.blurValue); - _controller - ..blur = blur - ..getToolTipWidget = _controller.isScrollRunning - ? [ - Center(child: widget.scrollLoadingWidget), - ] - : [ - _TargetWidget( - offset: rectBound.topLeft, - size: size, - onTap: _getOnTargetTap, - radius: widget.targetBorderRadius, - onDoubleTap: widget.onTargetDoubleTap, - onLongPress: widget.onTargetLongPress, - shapeBorder: widget.targetShapeBorder, - disableDefaultChildGestures: - widget.disableDefaultTargetGestures, - targetPadding: widget.targetPadding, - ), - ToolTipWidget( - key: ValueKey(_controller.showcaseId), - title: widget.title, - titleTextAlign: widget.titleTextAlign, - description: widget.description, - descriptionTextAlign: widget.descriptionTextAlign, - titleAlignment: widget.titleAlignment, - descriptionAlignment: widget.descriptionAlignment, - titleTextStyle: widget.titleTextStyle, - descTextStyle: widget.descTextStyle, - container: widget.container, - tooltipBackgroundColor: widget.tooltipBackgroundColor, - textColor: widget.textColor, - showArrow: widget.showArrow, - onTooltipTap: - widget.disposeOnTap == true || widget.onToolTipClick != null - ? _getOnTooltipTap - : null, - tooltipPadding: widget.tooltipPadding, - disableMovingAnimation: widget.disableMovingAnimation ?? - showCaseWidgetState.disableMovingAnimation, - disableScaleAnimation: (widget.disableScaleAnimation ?? - showCaseWidgetState.disableScaleAnimation) || - widget.container != null, - movingAnimationDuration: widget.movingAnimationDuration, - tooltipBorderRadius: widget.tooltipBorderRadius, - scaleAnimationDuration: widget.scaleAnimationDuration, - scaleAnimationCurve: widget.scaleAnimationCurve, - scaleAnimationAlignment: widget.scaleAnimationAlignment, - tooltipPosition: widget.tooltipPosition, - titlePadding: widget.titlePadding, - descriptionPadding: widget.descriptionPadding, - titleTextDirection: widget.titleTextDirection, - descriptionTextDirection: widget.descriptionTextDirection, - toolTipSlideEndDistance: widget.toolTipSlideEndDistance, - toolTipMargin: widget.toolTipMargin, - tooltipActionConfig: _getTooltipActionConfig(), - tooltipActions: _getTooltipActions(), - targetPadding: widget.targetPadding, - showcaseController: _controller, - ), - if (_getFloatingActionWidget != null) _getFloatingActionWidget!, - ]; - } - - Widget? get _getFloatingActionWidget => - widget.floatingActionWidget ?? _globalFloatingActionWidget; - - List _getTooltipActions() { - final actionData = (widget.tooltipActions?.isNotEmpty ?? false) - ? widget.tooltipActions! - : showCaseWidgetState.globalTooltipActions ?? []; - - final actionWidgets = []; - final actionDataLength = actionData.length; - for (var i = 0; i < actionDataLength; i++) { - final action = actionData[i]; - - /// 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 (action.hideActionWidgetForShowcase.contains(widget.showcaseKey) && - (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() { - return widget.tooltipActionConfig ?? - showCaseWidgetState.globalTooltipActionConfig ?? - const TooltipActionConfig(); - } - - void _updateControllerData( - RenderBox? renderBox, - Size screenSize, - ) { - final size = _controller.rootWidgetSize ?? screenSize; - final position = GetPosition( - rootRenderObject: _controller.rootRenderObject, - renderBox: renderBox, - padding: widget.targetPadding, - screenWidth: size.width, - screenHeight: size.height, - ); - _controller - ..position = position - ..linkedShowcaseDataModel = LinkedShowcaseDataModel( - rect: _controller.isScrollRunning ? Rect.zero : position.getRect(), - radius: _targetBorderRadius, - overlayPadding: - _controller.isScrollRunning ? EdgeInsets.zero : _targetPadding, - isCircle: _isCircle, + ..isScrollRunning = false + ..updateControllerData( + context.findRenderObject() as RenderBox?, + MediaQuery.of(context).size, ); - - buildOverlayOnTarget( - position.getOffset(), - position.getRect().size, - position.getRect(), - size, - ); - } -} - -class _TargetWidget extends StatelessWidget { - final Offset offset; - final Size size; - final VoidCallback? onTap; - final VoidCallback? onDoubleTap; - final VoidCallback? onLongPress; - final ShapeBorder shapeBorder; - final BorderRadius? radius; - final bool disableDefaultChildGestures; - final EdgeInsets targetPadding; - - const _TargetWidget({ - required this.offset, - required this.size, - required this.shapeBorder, - required this.targetPadding, - this.onTap, - this.radius, - this.onDoubleTap, - this.onLongPress, - this.disableDefaultChildGestures = false, - }); - - @override - Widget build(BuildContext context) { - return Positioned( - top: offset.dy - targetPadding.top, - left: offset.dx - targetPadding.left, - child: disableDefaultChildGestures - ? IgnorePointer( - child: targetWidgetContent(), - ) - : MouseRegion( - cursor: SystemMouseCursors.click, - child: targetWidgetContent(), - ), - ); - } - - Widget targetWidgetContent() { - return GestureDetector( - onTap: onTap, - onLongPress: onLongPress, - onDoubleTap: onDoubleTap, - behavior: HitTestBehavior.translucent, - child: Container( - height: size.height.abs(), - width: size.width.abs(), - margin: targetPadding, - decoration: ShapeDecoration( - shape: radius != null - ? RoundedRectangleBorder(borderRadius: radius!) - : shapeBorder, - ), - ), + startShowcase(); + _showCaseWidgetState.updateOverlay?.call( + _showCaseWidgetState.isShowcaseRunning, ); } } diff --git a/lib/src/showcase/showcase_controller.dart b/lib/src/showcase/showcase_controller.dart index c13780b9..9f6f0882 100644 --- a/lib/src/showcase/showcase_controller.dart +++ b/lib/src/showcase/showcase_controller.dart @@ -1,29 +1,59 @@ +import 'dart:math'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import '../get_position.dart'; import '../models/linked_showcase_data.dart'; +import '../models/tooltip_action_config.dart'; +import '../showcase_widget.dart'; +import '../tooltip/tooltip.dart'; +import '../tooltip_action_button_widget.dart'; +import '../widget/floating_action_widget.dart'; import 'showcase.dart'; +import 'target_widget.dart'; /// Controller class for managing showcase functionality +/// +/// This controller handles the lifecycle and presentation of a single showcase element. +/// It manages the position, state, and rendering of showcase elements including +/// tooltips, target highlighting, and floating action widgets. class ShowcaseController { /// Creates a [ShowcaseController] with required parameters + /// + /// * [id] - Unique identifier for this showcase instance + /// * [key] - Global key associated with the showcase widget + /// * [config] - Configuration settings for the showcase + /// * [showCaseWidgetState] - Reference to the parent showcase widget state + /// * [scrollIntoViewCallback] - Optional callback to scroll the target into view ShowcaseController({ - required this.showcaseId, - required this.showcaseKey, - required this.showcaseConfig, - this.scrollIntoView, - }); + required this.id, + required this.key, + required this.config, + required this.showCaseWidgetState, + this.scrollIntoViewCallback, + }) { + showCaseWidgetState.registerShowcaseController( + controller: this, + key: key, + showcaseId: id, + ); + initRootWidget(); + } /// Unique identifier for the showcase - final int showcaseId; + final int id; /// Global key associated with the showcase widget - final GlobalKey showcaseKey; + final GlobalKey key; /// Configuration for the showcase - Showcase showcaseConfig; + Showcase config; - /// Position getter for the showcase + /// Reference to the parent showcase widget state + ShowCaseWidgetState showCaseWidgetState; + + /// Position information for the showcase target GetPosition? position; /// Data model for linked showcases @@ -32,11 +62,11 @@ class ShowcaseController { /// Callback to start the showcase VoidCallback? startShowcase; - /// Optional function to scroll the view - final ValueGetter>? scrollIntoView; + /// Optional function to scroll the target into view + final ValueGetter>? scrollIntoViewCallback; /// Optional function to reverse the animation - ValueGetter>? reverseAnimation; + ValueGetter>? reverseAnimationCallback; /// Size of the root widget Size? rootWidgetSize; @@ -44,23 +74,278 @@ class ShowcaseController { /// Render box for the root widget RenderBox? rootRenderObject; - /// List of tooltip widgets + /// List of tooltip widgets to be displayed List getToolTipWidget = []; /// Flag to track if scrolling is in progress bool isScrollRunning = false; - /// Blur effect value + /// Blur effect value for the overlay background double blur = 0.0; + /// Global floating action widget to be displayed + FloatingActionWidget? globalFloatingActionWidget; + + /// Initializes the root widget size and render object + /// + /// Must be called after the widget is mounted to ensure proper measurements. + /// Uses a post-frame callback to capture accurate widget dimensions. + void initRootWidget() { + WidgetsBinding.instance.addPostFrameCallback((_) { + rootWidgetSize = showCaseWidgetState.rootWidgetSize; + rootRenderObject = showCaseWidgetState.rootRenderObject; + }); + } + + /// Updates root widget size and render object when the context changes + /// + /// Called during build to ensure showcase positioning is correct. + /// Recalculates sizes, updates controller data, and triggers overlay updates. + /// mounted check ensure the context is still valid before proceeding. + /// * [context] The BuildContext of the showcase widget + void recalculateRootWidgetSize(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!context.mounted) return; + final rootWidget = context.findRootAncestorStateOfType>(); + rootRenderObject = rootWidget?.context.findRenderObject() as RenderBox?; + rootWidgetSize = rootWidget == null + ? MediaQuery.of(context).size + : rootRenderObject?.size; + if (!showCaseWidgetState.enableShowcase) return; + updateControllerData( + context.findRenderObject() as RenderBox?, + MediaQuery.of(context).size, + ); + showCaseWidgetState.updateOverlay?.call( + showCaseWidgetState.isShowcaseRunning, + ); + }); + } + + /// Updates the controller's data when the showcase position or size changes + /// + /// Rebuilds the showcase overlay with updated positioning information. + /// Creates positioning data and updates the visual representation. + /// + /// * [renderBox] The RenderBox of the target widget + /// * [screenSize] The current screen size + void updateControllerData( + RenderBox? renderBox, + Size screenSize, + ) { + final size = rootWidgetSize ?? screenSize; + final newPosition = GetPosition( + rootRenderObject: rootRenderObject, + renderBox: renderBox, + padding: config.targetPadding, + screenWidth: size.width, + screenHeight: size.height, + ); + + position = newPosition; + final rect = newPosition.getRect(); + linkedShowcaseDataModel = LinkedShowcaseDataModel( + rect: isScrollRunning ? Rect.zero : rect, + radius: config.targetBorderRadius, + overlayPadding: isScrollRunning ? EdgeInsets.zero : config.targetPadding, + isCircle: config.targetShapeBorder is CircleBorder, + ); + + buildOverlayOnTarget( + offset: newPosition.getOffset(), + size: rect.size, + rectBound: rect, + screenSize: size, + ); + } + + /// Builds the overlay widgets for the target widget + /// + /// Includes target highlight, tooltip, and optional floating action widget. + /// Creates different widget sets based on whether scrolling is in progress. + /// + /// * [offset] The position offset of the target + /// * [size] The size of the target + /// * [rectBound] The target's bounding rectangle + /// * [screenSize] The current screen size + void buildOverlayOnTarget({ + required Offset offset, + required Size size, + required Rect rectBound, + required Size screenSize, + }) { + blur = kIsWeb + ? 0.0 + : max(0.0, config.blurValue ?? showCaseWidgetState.blurValue); + + getToolTipWidget = isScrollRunning + ? [ + Center(child: config.scrollLoadingWidget), + ] + : [ + TargetWidget( + offset: rectBound.topLeft, + size: size, + onTap: _getOnTargetTap, + radius: config.targetBorderRadius, + onDoubleTap: config.onTargetDoubleTap, + onLongPress: config.onTargetLongPress, + shapeBorder: config.targetShapeBorder, + disableDefaultChildGestures: config.disableDefaultTargetGestures, + targetPadding: config.targetPadding, + ), + ToolTipWidget( + key: ValueKey(id), + title: config.title, + titleTextAlign: config.titleTextAlign, + description: config.description, + descriptionTextAlign: config.descriptionTextAlign, + titleAlignment: config.titleAlignment, + descriptionAlignment: config.descriptionAlignment, + titleTextStyle: config.titleTextStyle, + descTextStyle: config.descTextStyle, + container: config.container, + tooltipBackgroundColor: config.tooltipBackgroundColor, + textColor: config.textColor, + showArrow: config.showArrow, + onTooltipTap: + config.disposeOnTap == true || config.onToolTipClick != null + ? _getOnTooltipTap + : null, + tooltipPadding: config.tooltipPadding, + disableMovingAnimation: config.disableMovingAnimation ?? + showCaseWidgetState.disableMovingAnimation, + disableScaleAnimation: (config.disableScaleAnimation ?? + showCaseWidgetState.disableScaleAnimation) || + config.container != null, + movingAnimationDuration: config.movingAnimationDuration, + tooltipBorderRadius: config.tooltipBorderRadius, + scaleAnimationDuration: config.scaleAnimationDuration, + scaleAnimationCurve: config.scaleAnimationCurve, + scaleAnimationAlignment: config.scaleAnimationAlignment, + tooltipPosition: config.tooltipPosition, + titlePadding: config.titlePadding, + descriptionPadding: config.descriptionPadding, + titleTextDirection: config.titleTextDirection, + descriptionTextDirection: config.descriptionTextDirection, + toolTipSlideEndDistance: config.toolTipSlideEndDistance, + toolTipMargin: config.toolTipMargin, + tooltipActionConfig: _getTooltipActionConfig(), + tooltipActions: _getTooltipActions(), + targetPadding: config.targetPadding, + showcaseController: this, + ), + if (_getFloatingActionWidget != null) _getFloatingActionWidget!, + ]; + } + + /// Moves to the next showcase if any are remaining + /// + /// Called when a showcase is completed. + /// Notifies the showcase widget state to advance to the next showcase. + void _nextIfAny() { + if (showCaseWidgetState.isShowCaseCompleted) return; + showCaseWidgetState.completed(config.showcaseKey); + } + + /// Handles target tap behavior based on configuration + /// + /// Either dismisses the showcase or moves to the next step based on configuration. + /// If [disposeOnTap] is true, dismisses the entire showcase, otherwise advances. + void _getOnTargetTap() { + if (config.disposeOnTap == true) { + showCaseWidgetState.dismiss(); + assert( + config.onTargetClick != null, + 'onTargetClick callback should be provided when disposeOnTap is true', + ); + config.onTargetClick!(); + } else { + (config.onTargetClick ?? _nextIfAny).call(); + } + } + + /// Handles tooltip tap behavior based on configuration + /// + /// Dismisses the showcase if configured to do so, and executes any configured callback. + /// If [disposeOnTap] is true, dismisses the entire showcase before executing callback. + void _getOnTooltipTap() { + if (config.disposeOnTap == true) { + showCaseWidgetState.dismiss(); + } + config.onToolTipClick?.call(); + } + + /// Retrieves tooltip action widgets based on configuration + /// + /// Filters actions that should be hidden for the current showcase. + /// Assembles action widgets with appropriate spacing and configuration. + /// + /// @return List of tooltip action widgets + List _getTooltipActions() { + final actionData = (config.tooltipActions?.isNotEmpty ?? false) + ? config.tooltipActions! + : showCaseWidgetState.globalTooltipActions ?? []; + + final actionWidgets = []; + final actionDataLength = actionData.length; + for (var i = 0; i < actionDataLength; i++) { + final action = actionData[i]; + + // Skip actions that should be hidden for this specific showcase key + // when no local actions are defined (using global actions) + if (action.hideActionWidgetForShowcase.contains(config.showcaseKey) && + (config.tooltipActions?.isEmpty ?? true)) { + continue; + } + + actionWidgets.add( + Padding( + padding: EdgeInsetsDirectional.only( + end: action == actionData.last + ? 0 + : _getTooltipActionConfig().actionGap, + ), + 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; + } + + /// Gets the tooltip action configuration + /// + /// Uses local config if available, falls back to global config or default. + /// Provides a consistent approach to configuration priority. + /// + /// @return The tooltip action configuration to use + TooltipActionConfig _getTooltipActionConfig() { + return config.tooltipActionConfig ?? + showCaseWidgetState.globalTooltipActionConfig ?? + const TooltipActionConfig(); + } + + /// Retrieves the floating action widget if available + /// + /// Combines local widget with global floating action widget. + /// Prefers local configuration over global when available. + /// + /// @return The floating action widget or null if none is configured + Widget? get _getFloatingActionWidget => + config.floatingActionWidget ?? globalFloatingActionWidget; + @override - int get hashCode => Object.hash(showcaseId, showcaseKey); + int get hashCode => Object.hash(id, key); @override bool operator ==(Object other) { - return (identical(this, other)) || - other is ShowcaseController && - other.showcaseKey == showcaseKey && - other.showcaseId == showcaseId; + return identical(this, other) || + other is ShowcaseController && other.key == key && other.id == id; } } diff --git a/lib/src/showcase/target_widget.dart b/lib/src/showcase/target_widget.dart new file mode 100644 index 00000000..7bddb12c --- /dev/null +++ b/lib/src/showcase/target_widget.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; + +/// A widget that represents the target of a showcase. +/// +/// This widget creates a transparent overlay that highlights a UI element +/// that needs to be showcased. It defines the position, size, and interaction +/// behavior of the target area. +/// +/// The [TargetWidget] is positioned absolutely within the showcase overlay and +/// can respond to various gestures like tap, double tap, and long press. +class TargetWidget extends StatelessWidget { + /// Creates a target widget for showcasing. + /// + /// * [offset] - The position of the target widget in the overlay + /// * [size] - The size of the target widget + /// * [shapeBorder] - The shape of the target highlight (e.g., circle) + /// * [targetPadding] - Padding applied around the target to increase its + /// highlight area + /// * [onTap] - Callback when the target is tapped + /// * [radius] - Border radius when using a rectangular shape + /// * [onDoubleTap] - Callback when the target is double-tapped + /// * [onLongPress] - Callback when the target is long-pressed + /// * [disableDefaultChildGestures] - Whether to disable gesture detection + /// on the target + const TargetWidget({ + required this.offset, + required this.size, + required this.shapeBorder, + required this.targetPadding, + this.onTap, + this.radius, + this.onDoubleTap, + this.onLongPress, + this.disableDefaultChildGestures = false, + }); + + /// The position of the target widget in the overlay coordinates + final Offset offset; + + /// The size of the target widget to be highlighted + final Size size; + + /// Callback function when the target is tapped + final VoidCallback? onTap; + + /// Callback function when the target is double-tapped + final VoidCallback? onDoubleTap; + + /// Callback function when the target is long-pressed + final VoidCallback? onLongPress; + + /// The shape of the target highlight + /// + /// Common shapes include [CircleBorder] and [RoundedRectangleBorder] + final ShapeBorder shapeBorder; + + /// Border radius when using a rectangular shape + /// + /// This is used only when a rectangular shape is needed with rounded corners + final BorderRadius? radius; + + /// Whether to disable gesture detection on the target area + /// + /// When true, the target area will not respond to gestures and will + /// not show a pointer cursor on hover + final bool disableDefaultChildGestures; + + /// Padding applied around the target to increase its highlight area + /// + /// This creates some space between the actual widget and its highlight border + final EdgeInsets targetPadding; + + @override + Widget build(BuildContext context) { + /// Creates the content of the target widget + /// + /// This includes the gesture detector and the container with the appropriate + /// shape decoration that defines the target's visual appearance. + final targetWidgetContent = GestureDetector( + onTap: onTap, + onLongPress: onLongPress, + onDoubleTap: onDoubleTap, + behavior: HitTestBehavior.translucent, + child: Container( + height: size.height.abs(), + width: size.width.abs(), + margin: targetPadding, + decoration: ShapeDecoration( + shape: radius == null + ? shapeBorder + : RoundedRectangleBorder(borderRadius: radius!), + ), + ), + ); + return Positioned( + top: offset.dy - targetPadding.top, + left: offset.dx - targetPadding.left, + child: disableDefaultChildGestures + ? IgnorePointer( + child: targetWidgetContent, + ) + : MouseRegion( + cursor: SystemMouseCursors.click, + child: targetWidgetContent, + ), + ); + } +} diff --git a/lib/src/showcase_widget.dart b/lib/src/showcase_widget.dart index edf3a9fd..4061dc8b 100644 --- a/lib/src/showcase_widget.dart +++ b/lib/src/showcase_widget.dart @@ -193,6 +193,11 @@ class ShowCaseWidgetState extends State { late final List? globalTooltipActions; + /// A mapping of showcase keys to their associated controllers. + /// - Key: GlobalKey of a showcase (provided by user) + /// - Value: Map of showcase IDs to their controllers, + /// allowing multiple controllers + /// to be associated with a single showcase key (e.g., for linked showcases) final Map> _showcaseControllers = {}; /// These properties are only here so that it can be accessed by @@ -293,7 +298,7 @@ class ShowCaseWidgetState extends State { } final firstController = controller.first; - final firstShowcaseConfig = firstController.showcaseConfig; + final firstShowcaseConfig = firstController.config; final backgroundContainer = ColoredBox( color: firstShowcaseConfig.overlayColor @@ -301,9 +306,7 @@ class ShowCaseWidgetState extends State { //TODO: Update when we remove support for older version //ignore: deprecated_member_use .withOpacity(firstShowcaseConfig.overlayOpacity), - child: const Align( - alignment: Alignment.center, - ), + child: const Align(), ); return Stack( @@ -353,7 +356,7 @@ class ShowCaseWidgetState extends State { final rootWidget = context.findRootAncestorStateOfType>(); rootRenderObject = rootWidget?.context.findRenderObject() as RenderBox?; rootWidgetSize = rootWidget == null - ? MediaQuery.sizeOf(context) + ? MediaQuery.of(context).size : rootRenderObject?.size; } @@ -417,21 +420,27 @@ class ShowCaseWidgetState extends State { /// if [force] is true then it will ignore the [enableAutoPlayLock] and /// move to next showcase. This is default behaviour for /// [TooltipDefaultActionType.next] - Future next({bool force = false}) async { + void next({bool force = false}) { // If this call is from autoPlay timer or action widget we will override the // enableAutoPlayLock so user can move forward in showcase if (!force && widget.enableAutoPlayLock) return; if (ids != null && mounted) { - await _onComplete(); - if (!mounted) return; - activeWidgetId = activeWidgetId! + 1; - _onStart(); - if (activeWidgetId! >= ids!.length) { - _cleanupAfterSteps(); - widget.onFinish?.call(); - } - updateOverlay?.call(isShowcaseRunning); + /// We are using [.then] to maintain older functionality. + /// here [_onComplete] method waits for animation to complete so we need + /// to wait before moving to next showcase + _onComplete().then( + (_) { + if (!mounted) return; + activeWidgetId = activeWidgetId! + 1; + _onStart(); + if (activeWidgetId! >= ids!.length) { + _cleanupAfterSteps(); + widget.onFinish?.call(); + } + updateOverlay?.call(isShowcaseRunning); + }, + ); } } @@ -471,10 +480,11 @@ class ShowCaseWidgetState extends State { if (activeWidgetId! < ids!.length) { widget.onStart?.call(activeWidgetId, ids![activeWidgetId!]); final controllers = _getCurrentActiveControllers; + //TODO: Update to firstOrNull when we remove support for older version if (controllers.length == 1 && - (controllers.first.showcaseConfig.enableAutoScroll ?? + (controllers.first.config.enableAutoScroll ?? widget.enableAutoScroll)) { - await controllers.first.scrollIntoView?.call(); + await controllers.first.scrollIntoViewCallback?.call(); } else { final controllerLength = controllers.length; for (var i = 0; i < controllerLength; i++) { @@ -498,12 +508,12 @@ class ShowCaseWidgetState extends State { for (var i = 0; i < controllerLength; i++) { final controller = currentControllers[i]; - if ((controller.showcaseConfig.disableScaleAnimation ?? + if ((controller.config.disableScaleAnimation ?? widget.disableScaleAnimation) || - controller.reverseAnimation == null) { + controller.reverseAnimationCallback == null) { continue; } - futures.add(controller.reverseAnimation!.call()); + futures.add(controller.reverseAnimationCallback!.call()); } await Future.wait(futures); widget.onComplete?.call(activeWidgetId, ids![activeWidgetId!]); @@ -564,8 +574,7 @@ class ShowCaseWidgetState extends State { int showcaseId, ) { assert( - _showcaseControllers.containsKey(key) && - _showcaseControllers[key]!.containsKey(showcaseId), + _showcaseControllers[key]?[showcaseId] != null, 'Please register showcase controller first', ); return _showcaseControllers[key]![showcaseId]!; diff --git a/lib/src/tooltip/tooltip_widget.dart b/lib/src/tooltip/tooltip_widget.dart index 9ecc79b1..0e4c5068 100644 --- a/lib/src/tooltip/tooltip_widget.dart +++ b/lib/src/tooltip/tooltip_widget.dart @@ -116,12 +116,22 @@ class _ToolTipWidgetState extends State if (!widget.disableMovingAnimation) { _movingAnimationController.forward(); } - widget.showcaseController.reverseAnimation = + widget.showcaseController.reverseAnimationCallback = widget.disableScaleAnimation ? null : _scaleAnimationController.reverse; } @override Widget build(BuildContext context) { + // Calculate the target position and size + final box = widget.showcaseController.position?.renderBox; + // This is a workaround to avoid the error when the widget is not mounted + // but won't happen in general cases + if (box == null) { + return const SizedBox.shrink(); + } + final targetPosition = box.localToGlobal(Offset.zero); + final targetSize = box.size; + final defaultToolTipWidget = widget.container != null ? MouseRegion( cursor: widget.onTooltipTap == null @@ -216,11 +226,6 @@ class _ToolTipWidgetState extends State ), ); - // Calculate the target position and size - final box = widget.showcaseController.position!.renderBox!; - final targetPosition = box.localToGlobal(Offset.zero); - final targetSize = box.size; - return Material( type: MaterialType.transparency, child: _AnimatedTooltipMultiLayout( @@ -232,7 +237,7 @@ class _ToolTipWidgetState extends State targetSize: targetSize, position: widget.tooltipPosition, screenSize: widget.showcaseController.rootWidgetSize ?? - MediaQuery.sizeOf(context), + MediaQuery.of(context).size, hasArrow: widget.showArrow, targetPadding: widget.targetPadding, scaleAlignment: widget.scaleAnimationAlignment, diff --git a/lib/src/widget/showcase_circular_progress_indecator.dart b/lib/src/widget/showcase_circular_progress_indecator.dart new file mode 100644 index 00000000..3bc40e6f --- /dev/null +++ b/lib/src/widget/showcase_circular_progress_indecator.dart @@ -0,0 +1,28 @@ +import 'package:flutter/cupertino.dart' show CupertinoActivityIndicator; +import 'package:flutter/material.dart'; + +import '../constants.dart'; + +class ShowcaseCircularProgressIndicator extends StatelessWidget { + const ShowcaseCircularProgressIndicator({super.key}); + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + switch (theme.platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + return const CupertinoActivityIndicator( + radius: Constants.cupertinoActivityIndicatorRadius, + color: Colors.white, + ); + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + return const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ); + } + } +} From 9a94177011cddacd0e2bd63e86bd0fedc0b7af0d Mon Sep 17 00:00:00 2001 From: Sahil-Simform Date: Thu, 3 Apr 2025 13:40:45 +0530 Subject: [PATCH 6/9] =?UTF-8?q?fix:=20=F0=9F=94=A8Fixed=20PR=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/constants.dart | 2 +- lib/src/showcase/showcase.dart | 4 +- lib/src/showcase/showcase_controller.dart | 53 ++- lib/src/showcase_widget.dart | 339 ++++++++++-------- ...showcase_circular_progress_indicator.dart} | 0 5 files changed, 212 insertions(+), 186 deletions(-) rename lib/src/widget/{showcase_circular_progress_indecator.dart => showcase_circular_progress_indicator.dart} (100%) diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 44e94124..a7e43169 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'widget/showcase_circular_progress_indecator.dart'; +import 'widget/showcase_circular_progress_indicator.dart'; class Constants { Constants._(); diff --git a/lib/src/showcase/showcase.dart b/lib/src/showcase/showcase.dart index 83feceef..c311ba4b 100644 --- a/lib/src/showcase/showcase.dart +++ b/lib/src/showcase/showcase.dart @@ -559,8 +559,8 @@ class Showcase extends StatefulWidget { class _ShowcaseState extends State { ShowcaseController get _controller => _showCaseWidgetState.getControllerForShowcase( - widget.showcaseKey, - _uniqueId, + key: widget.showcaseKey, + showcaseId: _uniqueId, ); late var _showCaseWidgetState = ShowCaseWidget.of(context); diff --git a/lib/src/showcase/showcase_controller.dart b/lib/src/showcase/showcase_controller.dart index 9f6f0882..aaf9300c 100644 --- a/lib/src/showcase/showcase_controller.dart +++ b/lib/src/showcase/showcase_controller.dart @@ -283,40 +283,33 @@ class ShowcaseController { /// /// @return List of tooltip action widgets List _getTooltipActions() { - final actionData = (config.tooltipActions?.isNotEmpty ?? false) + final doesHaveLocalActions = config.tooltipActions?.isNotEmpty ?? false; + final actionData = doesHaveLocalActions ? config.tooltipActions! : showCaseWidgetState.globalTooltipActions ?? []; - - final actionWidgets = []; final actionDataLength = actionData.length; - for (var i = 0; i < actionDataLength; i++) { - final action = actionData[i]; - - // Skip actions that should be hidden for this specific showcase key - // when no local actions are defined (using global actions) - if (action.hideActionWidgetForShowcase.contains(config.showcaseKey) && - (config.tooltipActions?.isEmpty ?? true)) { - continue; - } - - actionWidgets.add( - Padding( - padding: EdgeInsetsDirectional.only( - end: action == actionData.last - ? 0 - : _getTooltipActionConfig().actionGap, - ), - 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 [ + for (var i = 0; i < actionDataLength; i++) + if (doesHaveLocalActions || + !actionData[i] + .hideActionWidgetForShowcase + .contains(config.showcaseKey)) + Padding( + padding: EdgeInsetsDirectional.only( + end: actionData[i] == actionData.last + ? 0 + : _getTooltipActionConfig().actionGap, + ), + child: TooltipActionButtonWidget( + config: actionData[i], + // 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; + ]; } /// Gets the tooltip action configuration diff --git a/lib/src/showcase_widget.dart b/lib/src/showcase_widget.dart index 4061dc8b..0d2765e4 100644 --- a/lib/src/showcase_widget.dart +++ b/lib/src/showcase_widget.dart @@ -170,6 +170,10 @@ class ShowCaseWidget extends StatefulWidget { this.hideFloatingActionWidgetForShowcase = const [], }); + static GlobalKey? activeTargetWidget(BuildContext context) => context + .findAncestorStateOfType() + ?.getCurrentActiveShowcaseKey; + static ShowCaseWidgetState of(BuildContext context) { final state = context.findAncestorStateOfType(); if (state != null) { @@ -260,17 +264,18 @@ class ShowCaseWidgetState extends State { /// current showcase. FloatingActionBuilderCallback? globalFloatingActionWidget( GlobalKey showcaseKey, - ) => - _hideFloatingWidgetKeys[showcaseKey] ?? false - ? null - : widget.globalFloatingActionWidget; + ) { + return _hideFloatingWidgetKeys[showcaseKey] ?? false + ? null + : widget.globalFloatingActionWidget; + } @override void initState() { super.initState(); globalTooltipActions = widget.globalTooltipActions; globalTooltipActionConfig = widget.globalTooltipActionConfig; - initRootWidget(); + _initRootWidget(); } @override @@ -340,78 +345,55 @@ class ShowCaseWidgetState extends State { ); } - List _getLinkedShowcasesData( - List controllers, - ) { - final controllerLength = controllers.length; - return [ - for (var i = 0; i < controllerLength; i++) - if (controllers[i].linkedShowcaseDataModel != null) - controllers[i].linkedShowcaseDataModel!, - ]; - } - - void _updateRootWidget() { - if (!mounted) return; - final rootWidget = context.findRootAncestorStateOfType>(); - rootRenderObject = rootWidget?.context.findRenderObject() as RenderBox?; - rootWidgetSize = rootWidget == null - ? MediaQuery.of(context).size - : rootRenderObject?.size; - } - - void _barrierOnTap(Showcase firstShowcaseConfig) { - firstShowcaseConfig.onBarrierClick?.call(); - if (disableBarrierInteraction || - firstShowcaseConfig.disableBarrierInteraction) { - return; - } - next(); - } - - void initRootWidget() { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; - final rootWidget = context.findRootAncestorStateOfType>(); - rootRenderObject = rootWidget?.context.findRenderObject() as RenderBox?; - rootWidgetSize = rootWidget == null - ? MediaQuery.of(context).size - : rootRenderObject?.size; - }); - } - /// Starts Showcase view from the beginning of specified list of widget ids. /// If this function is used when showcase has been disabled then it will /// throw an exception. - void startShowCase(List widgetIds) { + /// + /// [delay] is optional and it will be used to delay the start of showcase + /// which is useful when animation may take some time to complete. + /// + /// Refer this issue https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/378 + void startShowCase( + List widgetIds, { + Duration delay = Duration.zero, + }) { if (!enableShowcase) { throw Exception( "You are trying to start Showcase while it has been disabled with " "`enableShowcase` parameter to false from ShowCaseWidget", ); } - if (!mounted) return; - ids = widgetIds; - activeWidgetId = 0; - _onStart(); - updateOverlay?.call(isShowcaseRunning); + Future.delayed( + delay, + () { + if (!mounted) return; + ids = widgetIds; + activeWidgetId = 0; + _onStart(); + updateOverlay?.call(isShowcaseRunning); + }, + ); } /// Completes showcase of given key and starts next one /// otherwise will finish the entire showcase view - Future completed(GlobalKey? key) async { - if (ids != null && ids![activeWidgetId!] == key && mounted) { - await _onComplete(); - if (!mounted) return; - activeWidgetId = activeWidgetId! + 1; - _onStart(); - - if (activeWidgetId! >= ids!.length) { - _cleanupAfterSteps(); - widget.onFinish?.call(); - } - updateOverlay?.call(isShowcaseRunning); + void completed(GlobalKey? key) { + if (activeWidgetId == null || ids?[activeWidgetId!] != key || !mounted) { + return; } + _onComplete().then( + (_) { + if (!mounted) return; + activeWidgetId = activeWidgetId! + 1; + _onStart(); + + if (activeWidgetId! >= ids!.length) { + _cleanupAfterSteps(); + widget.onFinish?.call(); + } + updateOverlay?.call(isShowcaseRunning); + }, + ); } /// Completes current active showcase and starts next one @@ -423,42 +405,46 @@ class ShowCaseWidgetState extends State { void next({bool force = false}) { // If this call is from autoPlay timer or action widget we will override the // enableAutoPlayLock so user can move forward in showcase - if (!force && widget.enableAutoPlayLock) return; - - if (ids != null && mounted) { - /// We are using [.then] to maintain older functionality. - /// here [_onComplete] method waits for animation to complete so we need - /// to wait before moving to next showcase - _onComplete().then( - (_) { - if (!mounted) return; - activeWidgetId = activeWidgetId! + 1; - _onStart(); - if (activeWidgetId! >= ids!.length) { - _cleanupAfterSteps(); - widget.onFinish?.call(); - } - updateOverlay?.call(isShowcaseRunning); - }, - ); + if ((!force && widget.enableAutoPlayLock) || ids == null || !mounted) { + return; } + + /// We are using [.then] to maintain older functionality. + /// here [_onComplete] method waits for animation to complete so we need + /// to wait before moving to next showcase + _onComplete().then( + (_) { + if (!mounted) return; + activeWidgetId = activeWidgetId! + 1; + _onStart(); + if (activeWidgetId! >= ids!.length) { + _cleanupAfterSteps(); + widget.onFinish?.call(); + } + updateOverlay?.call(isShowcaseRunning); + }, + ); } /// Completes current active showcase and starts previous one /// otherwise will finish the entire showcase view - Future previous() async { - if (ids != null && ((activeWidgetId ?? 0) - 1) >= 0 && mounted) { - await _onComplete(); - if (!mounted) return; - - activeWidgetId = activeWidgetId! - 1; - _onStart(); - if (activeWidgetId! >= ids!.length) { - _cleanupAfterSteps(); - widget.onFinish?.call(); - } - updateOverlay?.call(isShowcaseRunning); + void previous() { + if (ids == null || ((activeWidgetId ?? 0) - 1) < 0 || !mounted) { + return; } + _onComplete().then( + (_) { + if (!mounted) return; + + activeWidgetId = activeWidgetId! - 1; + _onStart(); + if (activeWidgetId! >= ids!.length) { + _cleanupAfterSteps(); + widget.onFinish?.call(); + } + updateOverlay?.call(isShowcaseRunning); + }, + ); } /// Dismiss entire showcase view @@ -476,6 +462,105 @@ class ShowCaseWidgetState extends State { updateOverlay?.call(isShowcaseRunning); } + /// Disables the [globalFloatingActionWidget] for the provided keys. + void hideFloatingActionWidgetForKeys( + List updatedList, + ) { + _hideFloatingWidgetKeys + ..clear() + ..addAll({ + for (final item in updatedList) item: true, + }); + } + + void registerShowcaseController({ + required GlobalKey key, + required ShowcaseController controller, + required int showcaseId, + }) { + assert( + StackTrace.current.toString().contains('_ShowcaseState'), + 'This method should only be called from Showcase class', + ); + _showcaseControllers + .putIfAbsent( + key, + () => {}, + ) + .update( + showcaseId, + (value) => controller, + ifAbsent: () => controller, + ); + } + + void removeShowcaseController({ + required GlobalKey key, + required int uniqueShowcaseKey, + }) { + assert( + StackTrace.current.toString().contains('_ShowcaseState'), + 'This method should only be called from Showcase class', + ); + _showcaseControllers[key]?.remove(uniqueShowcaseKey); + } + + ShowcaseController getControllerForShowcase({ + required GlobalKey key, + required int showcaseId, + }) { + assert( + StackTrace.current.toString().contains('_ShowcaseState'), + 'This method should only be called from Showcase class', + ); + assert( + _showcaseControllers[key]?[showcaseId] != null, + 'Please register showcase controller first by calling ' + 'registerShowcaseController', + ); + return _showcaseControllers[key]![showcaseId]!; + } + + List _getLinkedShowcasesData( + List controllers, + ) { + final controllerLength = controllers.length; + return [ + for (var i = 0; i < controllerLength; i++) + if (controllers[i].linkedShowcaseDataModel != null) + controllers[i].linkedShowcaseDataModel!, + ]; + } + + void _updateRootWidget() { + if (!mounted) return; + final rootWidget = context.findRootAncestorStateOfType>(); + rootRenderObject = rootWidget?.context.findRenderObject() as RenderBox?; + rootWidgetSize = rootWidget == null + ? MediaQuery.of(context).size + : rootRenderObject?.size; + } + + void _barrierOnTap(Showcase firstShowcaseConfig) { + firstShowcaseConfig.onBarrierClick?.call(); + if (disableBarrierInteraction || + firstShowcaseConfig.disableBarrierInteraction) { + return; + } + next(); + } + + void _initRootWidget() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + final rootWidget = context.findRootAncestorStateOfType>(); + rootRenderObject = rootWidget?.context.findRenderObject() as RenderBox?; + rootWidgetSize = rootWidget == null + ? MediaQuery.of(context).size + : rootRenderObject?.size; + }); + } + Future _onStart() async { if (activeWidgetId! < ids!.length) { widget.onStart?.call(activeWidgetId, ids![activeWidgetId!]); @@ -502,24 +587,18 @@ class ShowCaseWidgetState extends State { } Future _onComplete() async { - final futures = []; final currentControllers = _getCurrentActiveControllers; final controllerLength = currentControllers.length; - for (var i = 0; i < controllerLength; i++) { - final controller = currentControllers[i]; - if ((controller.config.disableScaleAnimation ?? - widget.disableScaleAnimation) || - controller.reverseAnimationCallback == null) { - continue; - } - futures.add(controller.reverseAnimationCallback!.call()); - } - await Future.wait(futures); + await Future.wait([ + for (var i = 0; i < controllerLength; i++) + if (!(currentControllers[i].config.disableScaleAnimation ?? + widget.disableScaleAnimation) && + currentControllers[i].reverseAnimationCallback != null) + currentControllers[i].reverseAnimationCallback!.call(), + ]); widget.onComplete?.call(activeWidgetId, ids![activeWidgetId!]); - if (widget.autoPlay) { - _cancelTimer(); - } + if (widget.autoPlay) _cancelTimer(); } void _cancelTimer() { @@ -533,50 +612,4 @@ class ShowCaseWidgetState extends State { activeWidgetId = null; _cancelTimer(); } - - /// Disables the [globalFloatingActionWidget] for the provided keys. - void hideFloatingActionWidgetForKeys( - List updatedList, - ) { - _hideFloatingWidgetKeys - ..clear() - ..addAll({ - for (final item in updatedList) item: true, - }); - } - - void registerShowcaseController({ - required GlobalKey key, - required ShowcaseController controller, - required int showcaseId, - }) { - _showcaseControllers - .putIfAbsent( - key, - () => {}, - ) - .update( - showcaseId, - (value) => controller, - ifAbsent: () => controller, - ); - } - - void removeShowcaseController({ - required GlobalKey key, - required int uniqueShowcaseKey, - }) { - _showcaseControllers[key]?.remove(uniqueShowcaseKey); - } - - ShowcaseController getControllerForShowcase( - GlobalKey key, - int showcaseId, - ) { - assert( - _showcaseControllers[key]?[showcaseId] != null, - 'Please register showcase controller first', - ); - return _showcaseControllers[key]![showcaseId]!; - } } diff --git a/lib/src/widget/showcase_circular_progress_indecator.dart b/lib/src/widget/showcase_circular_progress_indicator.dart similarity index 100% rename from lib/src/widget/showcase_circular_progress_indecator.dart rename to lib/src/widget/showcase_circular_progress_indicator.dart From 5405eee38ea4419ccdccf0bb47b8b706b3afb98a Mon Sep 17 00:00:00 2001 From: Sahil-Simform Date: Thu, 3 Apr 2025 17:26:34 +0530 Subject: [PATCH 7/9] =?UTF-8?q?chore:=20=F0=9F=92=A5=20Update=20min=20dart?= =?UTF-8?q?=20sdk=20to=202.19.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 ++ lib/src/tooltip/animated_tooltip_layout.dart | 4 +++- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c885a53..7943cc49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ - Improvement [#514](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/pull/514) - Improved showcase widget and showcase with widget, Removed inherited widget, keys and setStates, Added controller to manage showcase +- CHORE [#514](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/pull/514) - + Bumped dart minimum sdk to 2.19.6 ## [4.0.1] - Fixed [#493](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/493) - ShowCase.withWidget not showing issue diff --git a/lib/src/tooltip/animated_tooltip_layout.dart b/lib/src/tooltip/animated_tooltip_layout.dart index d6ac4b36..ca0ba63f 100644 --- a/lib/src/tooltip/animated_tooltip_layout.dart +++ b/lib/src/tooltip/animated_tooltip_layout.dart @@ -1,7 +1,9 @@ part of 'tooltip.dart'; class _AnimatedTooltipMultiLayout extends MultiChildRenderObjectWidget { - const _AnimatedTooltipMultiLayout({ + // TODO: make this const when update to new flutter version + // ignore: prefer_const_constructors_in_immutables + _AnimatedTooltipMultiLayout({ // If we remove this parameter it will cause error in v3.29.0 so ignore // ignore: unused_element_parameter super.key, diff --git a/pubspec.yaml b/pubspec.yaml index 962e5874..8af14d99 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ issue_tracker: https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/is repository: https://github.com/simformsolutions/flutter_showcaseview environment: - sdk: '>=2.18.0 <4.0.0' + sdk: '>=2.19.6 <4.0.0' dependencies: flutter: From 35ec3f79b4001b7c4ab733fc0d11feb1552ab5bf Mon Sep 17 00:00:00 2001 From: Sahil-Simform Date: Fri, 4 Apr 2025 12:07:33 +0530 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20=F0=9F=A9=B9=20Fixed=20the=20page=20?= =?UTF-8?q?transition=20issue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/get_position.dart | 7 +++---- ...out_overlays.dart => overlay_builder.dart} | 0 lib/src/showcase/showcase.dart | 20 +++++++++++++------ lib/src/showcase/showcase_controller.dart | 9 +++++++++ lib/src/showcase_widget.dart | 7 ++++++- 5 files changed, 32 insertions(+), 11 deletions(-) rename lib/src/{layout_overlays.dart => overlay_builder.dart} (100%) diff --git a/lib/src/get_position.dart b/lib/src/get_position.dart index fe6e256a..524c528e 100644 --- a/lib/src/get_position.dart +++ b/lib/src/get_position.dart @@ -66,11 +66,10 @@ class GetPosition { final topLeft = renderBox!.size.topLeft(_boxOffset!); final bottomRight = renderBox!.size.bottomRight(_boxOffset!); final leftDx = topLeft.dx - padding.left; - var leftDy = topLeft.dy - padding.top; - if (leftDy < 0) leftDy = 0; + final leftDy = topLeft.dy - padding.top; final rect = Rect.fromLTRB( - leftDx.clamp(0, leftDx), - leftDy.clamp(0, leftDy), + leftDx.clamp(0, double.maxFinite), + leftDy.clamp(0, double.maxFinite), min(bottomRight.dx + padding.right, screenWidth), min(bottomRight.dy + padding.bottom, screenHeight), ); diff --git a/lib/src/layout_overlays.dart b/lib/src/overlay_builder.dart similarity index 100% rename from lib/src/layout_overlays.dart rename to lib/src/overlay_builder.dart diff --git a/lib/src/showcase/showcase.dart b/lib/src/showcase/showcase.dart index c311ba4b..5c714973 100644 --- a/lib/src/showcase/showcase.dart +++ b/lib/src/showcase/showcase.dart @@ -575,8 +575,9 @@ class _ShowcaseState extends State { key: widget.showcaseKey, config: widget, showCaseWidgetState: ShowCaseWidget.of(context), - scrollIntoViewCallback: scrollIntoView, - ).startShowcase = startShowcase; + scrollIntoViewCallback: _scrollIntoView, + updateControllerValue: _updateOverlayData, + ).startShowcase = _startShowcase; } @override @@ -605,7 +606,7 @@ class _ShowcaseState extends State { super.dispose(); } - void startShowcase() { + void _startShowcase() { if (!_showCaseWidgetState.enableShowcase) return; _controller @@ -623,7 +624,14 @@ class _ShowcaseState extends State { ); } - Future scrollIntoView() async { + void _updateOverlayData() { + _controller.updateControllerData( + context.findRenderObject() as RenderBox?, + MediaQuery.of(context).size, + ); + } + + Future _scrollIntoView() async { if (!mounted) return; _controller ..isScrollRunning = true @@ -631,7 +639,7 @@ class _ShowcaseState extends State { context.findRenderObject() as RenderBox?, MediaQuery.of(context).size, ); - startShowcase(); + _startShowcase(); _showCaseWidgetState.updateOverlay?.call( _showCaseWidgetState.isShowcaseRunning, ); @@ -647,7 +655,7 @@ class _ShowcaseState extends State { context.findRenderObject() as RenderBox?, MediaQuery.of(context).size, ); - startShowcase(); + _startShowcase(); _showCaseWidgetState.updateOverlay?.call( _showCaseWidgetState.isShowcaseRunning, ); diff --git a/lib/src/showcase/showcase_controller.dart b/lib/src/showcase/showcase_controller.dart index aaf9300c..41569070 100644 --- a/lib/src/showcase/showcase_controller.dart +++ b/lib/src/showcase/showcase_controller.dart @@ -31,6 +31,7 @@ class ShowcaseController { required this.key, required this.config, required this.showCaseWidgetState, + required this.updateControllerValue, this.scrollIntoViewCallback, }) { showCaseWidgetState.registerShowcaseController( @@ -68,6 +69,14 @@ class ShowcaseController { /// Optional function to reverse the animation ValueGetter>? reverseAnimationCallback; + /// Function to update the controller value + /// + /// Main use of this is to update the controller data just before overlay is + /// inserted so we can get the correct position. Which is need in + /// page transition case where page transition may take some time to reach + /// to it's original position + VoidCallback updateControllerValue; + /// Size of the root widget Size? rootWidgetSize; diff --git a/lib/src/showcase_widget.dart b/lib/src/showcase_widget.dart index 0d2765e4..7dfc0cb0 100644 --- a/lib/src/showcase_widget.dart +++ b/lib/src/showcase_widget.dart @@ -26,8 +26,8 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import '../showcaseview.dart'; -import 'layout_overlays.dart'; import 'models/linked_showcase_data.dart'; +import 'overlay_builder.dart'; import 'shape_clipper.dart'; import 'showcase/showcase_controller.dart'; @@ -302,6 +302,11 @@ class ShowCaseWidgetState extends State { return const SizedBox.shrink(); } + final controllerLength = controller.length; + for (var i = 0; i < controllerLength; i++) { + controller[i].updateControllerValue.call(); + } + final firstController = controller.first; final firstShowcaseConfig = firstController.config; From ec0d57c94a670fa3d7fd95e7a7b65162bb663067 Mon Sep 17 00:00:00 2001 From: Sahil-Simform Date: Fri, 4 Apr 2025 14:19:17 +0530 Subject: [PATCH 9/9] =?UTF-8?q?fix:=20=F0=9F=A9=B9=20Shifted=20everything?= =?UTF-8?q?=20to=20showcase=20controller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 15 ++- example/lib/main.dart | 8 +- lib/src/showcase/showcase.dart | 72 ++---------- lib/src/showcase/showcase_controller.dart | 132 +++++++++++++++++----- lib/src/showcase_widget.dart | 104 +++++++++-------- 5 files changed, 184 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index a626d9a3..24649445 100644 --- a/README.md +++ b/README.md @@ -110,14 +110,14 @@ Showcase.withWidget( ), ``` -5. Starting the `ShowCase` +5. Starting the `ShowCase`: ```dart someEvent(){ ShowCaseWidget.of(context).startShowCase([_one, _two, _three]); } ``` -If you want to start the `ShowCaseView` as soon as your UI built up then use below code. +If you want to start the `ShowCaseView` as soon as your UI built up then use below code: ```dart WidgetsBinding.instance.addPostFrameCallback((_) => @@ -125,6 +125,15 @@ WidgetsBinding.instance.addPostFrameCallback((_) => ); ``` +If you have some animation or transition in your UI and you want to start the `ShowCaseView` after +that then use below code: + +```dart +WidgetsBinding.instance.addPostFrameCallback((_) => + ShowCaseWidget.of(context).startShowCase([_one, _two, _three], delay: "Animation Duration") +); +``` + ## MultiShowcaseView To show multiple showcase at the same time provide same key to showcase. Note: auto scroll to showcase will not work in case of the multi-showcase and we will use property @@ -287,7 +296,7 @@ So, If you want to make a scroll view that contains less number of children widg If using SingleChildScrollView is not an option, then you can assign a ScrollController to that scrollview and manually scroll to the position where showcase widget gets rendered. You can add that code in onStart method of `ShowCaseWidget`. -Example, +Example: ```dart // This controller will be assigned to respected sctollview. diff --git a/example/lib/main.dart b/example/lib/main.dart index b2adb8e4..d1ce61ea 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -468,8 +468,12 @@ class _MailPageState extends State { ), ).then((_) { setState(() { - ShowCaseWidget.of(context) - .startShowCase([_four, _lastShowcaseWidget]); + ShowCaseWidget.of(context).startShowCase( + [_four, _lastShowcaseWidget], + delay: const Duration( + milliseconds: 200, + ), + ); }); }); }, diff --git a/lib/src/showcase/showcase.dart b/lib/src/showcase/showcase.dart index 5c714973..bac3d198 100644 --- a/lib/src/showcase/showcase.dart +++ b/lib/src/showcase/showcase.dart @@ -24,7 +24,6 @@ import 'package:flutter/material.dart'; import '../constants.dart'; import '../enum.dart'; -import '../get_position.dart'; import '../models/tooltip_action_button.dart'; import '../models/tooltip_action_config.dart'; import '../showcase_widget.dart'; @@ -573,25 +572,30 @@ class _ShowcaseState extends State { ShowcaseController( id: _uniqueId, key: widget.showcaseKey, - config: widget, + showcaseState: this, showCaseWidgetState: ShowCaseWidget.of(context), - scrollIntoViewCallback: _scrollIntoView, - updateControllerValue: _updateOverlayData, - ).startShowcase = _startShowcase; + ); } @override void didUpdateWidget(covariant Showcase oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget == widget) return; + _updateControllerValues(); + } + + _updateControllerValues() { _showCaseWidgetState = ShowCaseWidget.of(context); _controller - ..config = widget + ..showcaseState = this ..showCaseWidgetState = _showCaseWidgetState; } @override Widget build(BuildContext context) { + // This is to support hot reload + _updateControllerValues(); + _controller.recalculateRootWidgetSize(context); return widget.child; } @@ -602,62 +606,6 @@ class _ShowcaseState extends State { key: widget.showcaseKey, uniqueShowcaseKey: _uniqueId, ); - super.dispose(); } - - void _startShowcase() { - if (!_showCaseWidgetState.enableShowcase) return; - - _controller - ..recalculateRootWidgetSize(context) - ..globalFloatingActionWidget = _showCaseWidgetState - .globalFloatingActionWidget(widget.showcaseKey) - ?.call(context); - final size = _controller.rootWidgetSize ?? MediaQuery.of(context).size; - _controller.position ??= GetPosition( - rootRenderObject: _controller.rootRenderObject, - renderBox: context.findRenderObject() as RenderBox?, - padding: widget.targetPadding, - screenWidth: size.width, - screenHeight: size.height, - ); - } - - void _updateOverlayData() { - _controller.updateControllerData( - context.findRenderObject() as RenderBox?, - MediaQuery.of(context).size, - ); - } - - Future _scrollIntoView() async { - if (!mounted) return; - _controller - ..isScrollRunning = true - ..updateControllerData( - context.findRenderObject() as RenderBox?, - MediaQuery.of(context).size, - ); - _startShowcase(); - _showCaseWidgetState.updateOverlay?.call( - _showCaseWidgetState.isShowcaseRunning, - ); - await Scrollable.ensureVisible( - context, - duration: _showCaseWidgetState.widget.scrollDuration, - alignment: widget.scrollAlignment, - ); - if (!mounted) return; - _controller - ..isScrollRunning = false - ..updateControllerData( - context.findRenderObject() as RenderBox?, - MediaQuery.of(context).size, - ); - _startShowcase(); - _showCaseWidgetState.updateOverlay?.call( - _showCaseWidgetState.isShowcaseRunning, - ); - } } diff --git a/lib/src/showcase/showcase_controller.dart b/lib/src/showcase/showcase_controller.dart index 41569070..d62778ce 100644 --- a/lib/src/showcase/showcase_controller.dart +++ b/lib/src/showcase/showcase_controller.dart @@ -23,16 +23,13 @@ class ShowcaseController { /// /// * [id] - Unique identifier for this showcase instance /// * [key] - Global key associated with the showcase widget - /// * [config] - Configuration settings for the showcase + /// * [showcaseState] - Reference to the showcase state /// * [showCaseWidgetState] - Reference to the parent showcase widget state - /// * [scrollIntoViewCallback] - Optional callback to scroll the target into view ShowcaseController({ required this.id, required this.key, - required this.config, + required this.showcaseState, required this.showCaseWidgetState, - required this.updateControllerValue, - this.scrollIntoViewCallback, }) { showCaseWidgetState.registerShowcaseController( controller: this, @@ -49,7 +46,7 @@ class ShowcaseController { final GlobalKey key; /// Configuration for the showcase - Showcase config; + State showcaseState; /// Reference to the parent showcase widget state ShowCaseWidgetState showCaseWidgetState; @@ -60,23 +57,9 @@ class ShowcaseController { /// Data model for linked showcases LinkedShowcaseDataModel? linkedShowcaseDataModel; - /// Callback to start the showcase - VoidCallback? startShowcase; - - /// Optional function to scroll the target into view - final ValueGetter>? scrollIntoViewCallback; - /// Optional function to reverse the animation ValueGetter>? reverseAnimationCallback; - /// Function to update the controller value - /// - /// Main use of this is to update the controller data just before overlay is - /// inserted so we can get the correct position. Which is need in - /// page transition case where page transition may take some time to reach - /// to it's original position - VoidCallback updateControllerValue; - /// Size of the root widget Size? rootWidgetSize; @@ -95,6 +78,24 @@ class ShowcaseController { /// Global floating action widget to be displayed FloatingActionWidget? globalFloatingActionWidget; + /// Returns the Showcase widget configuration + /// + /// Provides access to all properties and settings of the current showcase widget. + /// This is used throughout the controller to access showcase configuration options. + Showcase get config => showcaseState.widget; + + /// Returns the BuildContext for this showcase + /// + /// Used for positioning calculations and widget rendering. + /// This context represents the location of the showcase target in the widget tree. + BuildContext get _context => showcaseState.context; + + /// Checks if the showcase context is still valid + /// + /// Returns true if the context is mounted (valid) and false otherwise. + /// Used to prevent operations on widgets that have been removed from the tree. + bool get _mounted => showcaseState.context.mounted; + /// Initializes the root widget size and render object /// /// Must be called after the widget is mounted to ensure proper measurements. @@ -121,10 +122,7 @@ class ShowcaseController { ? MediaQuery.of(context).size : rootRenderObject?.size; if (!showCaseWidgetState.enableShowcase) return; - updateControllerData( - context.findRenderObject() as RenderBox?, - MediaQuery.of(context).size, - ); + updateControllerData(); showCaseWidgetState.updateOverlay?.call( showCaseWidgetState.isShowcaseRunning, ); @@ -136,12 +134,13 @@ class ShowcaseController { /// Rebuilds the showcase overlay with updated positioning information. /// Creates positioning data and updates the visual representation. /// - /// * [renderBox] The RenderBox of the target widget - /// * [screenSize] The current screen size - void updateControllerData( - RenderBox? renderBox, - Size screenSize, - ) { + /// Another use of this is to update the controller data just before overlay is + /// inserted so we can get the correct position. Which is need in + /// page transition case where page transition may take some time to reach + /// to it's original position + void updateControllerData() { + final renderBox = _context.findRenderObject() as RenderBox?; + final screenSize = MediaQuery.of(_context).size; final size = rootWidgetSize ?? screenSize; final newPosition = GetPosition( rootRenderObject: rootRenderObject, @@ -248,6 +247,79 @@ class ShowcaseController { ]; } + /// Callback to start the showcase + /// + /// Initializes the showcase by calculating positions and preparing visual elements. + /// This method is called when a showcase is about to be displayed to ensure all + /// positioning data is accurate and up-to-date. + /// + /// The method performs these key actions: + /// - Exits early if showcases are disabled in the parent widget + /// - Recalculates the root widget size to ensure accurate positioning + /// - Sets up any global floating action widgets + /// - Initializes position data if not already set + /// + /// This method is typically called internally by the showcase system but + /// can also be called manually to force a recalculation of showcase elements. + void startShowcase() { + if (!showCaseWidgetState.enableShowcase) return; + + recalculateRootWidgetSize(_context); + globalFloatingActionWidget = showCaseWidgetState + .globalFloatingActionWidget(config.showcaseKey) + ?.call(_context); + final size = rootWidgetSize ?? MediaQuery.of(_context).size; + position ??= GetPosition( + rootRenderObject: rootRenderObject, + renderBox: _context.findRenderObject() as RenderBox?, + padding: config.targetPadding, + screenWidth: size.width, + screenHeight: size.height, + ); + } + + /// Used to scroll the target into view + /// + /// Ensures the showcased widget is visible on screen by scrolling to it. + /// This method handles the complete scrolling process including: + /// + /// - Setting visual indicators while scrolling is in progress + /// - Updating the overlay to show loading state + /// - Performing the actual scrolling operation + /// - Refreshing the showcase display after scrolling completes + /// + /// The method shows a loading indicator during scrolling and updates + /// the showcase position after scrolling completes. It manages the + /// `isScrollRunning` state to coordinate UI updates. + /// + /// Note: Multi Showcase will not be scrolled into view + /// + /// Returns a Future that completes when scrolling is finished. If the widget + /// is unmounted during scrolling, the operation will be canceled safely. + Future scrollIntoView() async { + if (!_mounted) return; + + isScrollRunning = true; + updateControllerData(); + startShowcase(); + showCaseWidgetState.updateOverlay?.call( + showCaseWidgetState.isShowcaseRunning, + ); + await Scrollable.ensureVisible( + _context, + duration: showCaseWidgetState.widget.scrollDuration, + alignment: config.scrollAlignment, + ); + if (!_mounted) return; + + isScrollRunning = false; + updateControllerData(); + startShowcase(); + showCaseWidgetState.updateOverlay?.call( + showCaseWidgetState.isShowcaseRunning, + ); + } + /// Moves to the next showcase if any are remaining /// /// Called when a showcase is completed. diff --git a/lib/src/showcase_widget.dart b/lib/src/showcase_widget.dart index 7dfc0cb0..3d930b74 100644 --- a/lib/src/showcase_widget.dart +++ b/lib/src/showcase_widget.dart @@ -197,13 +197,6 @@ class ShowCaseWidgetState extends State { late final List? globalTooltipActions; - /// A mapping of showcase keys to their associated controllers. - /// - Key: GlobalKey of a showcase (provided by user) - /// - Value: Map of showcase IDs to their controllers, - /// allowing multiple controllers - /// to be associated with a single showcase key (e.g., for linked showcases) - final Map> _showcaseControllers = {}; - /// These properties are only here so that it can be accessed by /// [Showcase] bool get autoPlay => widget.autoPlay; @@ -229,15 +222,17 @@ class ShowCaseWidgetState extends State { List get hiddenFloatingActionKeys => _hideFloatingWidgetKeys.keys.toList(); - Timer? _timer; - ValueSetter? updateOverlay; - /// This Stores keys of showcase for which we will hide the - /// [globalFloatingActionWidget]. - late final _hideFloatingWidgetKeys = { - for (final item in widget.hideFloatingActionWidgetForShowcase) item: true, - }; + /// Return a [widget.globalFloatingActionWidget] if not need to hide this for + /// current showcase. + FloatingActionBuilderCallback? globalFloatingActionWidget( + GlobalKey showcaseKey, + ) { + return _hideFloatingWidgetKeys[showcaseKey] ?? false + ? null + : widget.globalFloatingActionWidget; + } /// Returns value of [ShowCaseWidget.blurValue] double get blurValue => widget.blurValue; @@ -253,23 +248,28 @@ class ShowCaseWidgetState extends State { } } + bool get isShowcaseRunning => getCurrentActiveShowcaseKey != null; + + Timer? _timer; + + /// A mapping of showcase keys to their associated controllers. + /// - Key: GlobalKey of a showcase (provided by user) + /// - Value: Map of showcase IDs to their controllers, + /// allowing multiple controllers + /// to be associated with a single showcase key (e.g., for linked showcases) + final Map> _showcaseControllers = {}; + + /// This Stores keys of showcase for which we will hide the + /// [globalFloatingActionWidget]. + late final _hideFloatingWidgetKeys = { + for (final item in widget.hideFloatingActionWidgetForShowcase) item: true, + }; + List get _getCurrentActiveControllers { return _showcaseControllers[getCurrentActiveShowcaseKey]?.values.toList() ?? []; } - bool get isShowcaseRunning => getCurrentActiveShowcaseKey != null; - - /// Return a [widget.globalFloatingActionWidget] if not need to hide this for - /// current showcase. - FloatingActionBuilderCallback? globalFloatingActionWidget( - GlobalKey showcaseKey, - ) { - return _hideFloatingWidgetKeys[showcaseKey] ?? false - ? null - : widget.globalFloatingActionWidget; - } - @override void initState() { super.initState(); @@ -304,7 +304,7 @@ class ShowCaseWidgetState extends State { final controllerLength = controller.length; for (var i = 0; i < controllerLength; i++) { - controller[i].updateControllerValue.call(); + controller[i].updateControllerData(); } final firstController = controller.first; @@ -362,22 +362,25 @@ class ShowCaseWidgetState extends State { List widgetIds, { Duration delay = Duration.zero, }) { + if (!mounted) return; if (!enableShowcase) { throw Exception( "You are trying to start Showcase while it has been disabled with " "`enableShowcase` parameter to false from ShowCaseWidget", ); } - Future.delayed( - delay, - () { - if (!mounted) return; - ids = widgetIds; - activeWidgetId = 0; - _onStart(); - updateOverlay?.call(isShowcaseRunning); - }, - ); + + if (delay.inMilliseconds == 0) { + ids = widgetIds; + activeWidgetId = 0; + _onStart(); + updateOverlay?.call(isShowcaseRunning); + } else { + Future.delayed( + delay, + () => startShowCase(widgetIds), + ); + } } /// Completes showcase of given key and starts next one @@ -434,7 +437,7 @@ class ShowCaseWidgetState extends State { /// Completes current active showcase and starts previous one /// otherwise will finish the entire showcase view void previous() { - if (ids == null || ((activeWidgetId ?? 0) - 1) < 0 || !mounted) { + if (ids == null || ((activeWidgetId ?? 0) - 1).isNegative || !mounted) { return; } _onComplete().then( @@ -485,7 +488,7 @@ class ShowCaseWidgetState extends State { }) { assert( StackTrace.current.toString().contains('_ShowcaseState'), - 'This method should only be called from Showcase class', + 'This method should only be called from `Showcase` class', ); _showcaseControllers .putIfAbsent( @@ -505,7 +508,7 @@ class ShowCaseWidgetState extends State { }) { assert( StackTrace.current.toString().contains('_ShowcaseState'), - 'This method should only be called from Showcase class', + 'This method should only be called from `Showcase` class', ); _showcaseControllers[key]?.remove(uniqueShowcaseKey); } @@ -516,7 +519,7 @@ class ShowCaseWidgetState extends State { }) { assert( StackTrace.current.toString().contains('_ShowcaseState'), - 'This method should only be called from Showcase class', + 'This method should only be called from `Showcase` class', ); assert( _showcaseControllers[key]?[showcaseId] != null, @@ -570,18 +573,19 @@ class ShowCaseWidgetState extends State { if (activeWidgetId! < ids!.length) { widget.onStart?.call(activeWidgetId, ids![activeWidgetId!]); final controllers = _getCurrentActiveControllers; - //TODO: Update to firstOrNull when we remove support for older version - if (controllers.length == 1 && - (controllers.first.config.enableAutoScroll ?? - widget.enableAutoScroll)) { - await controllers.first.scrollIntoViewCallback?.call(); - } else { - final controllerLength = controllers.length; - for (var i = 0; i < controllerLength; i++) { - controllers[i].startShowcase?.call(); + final controllerLength = controllers.length; + for (var i = 0; i < controllerLength; i++) { + final controller = controllers[i]; + final isAutoScroll = + controller.config.enableAutoScroll ?? widget.enableAutoScroll; + if (controllerLength == 1 && isAutoScroll) { + await controller.scrollIntoView(); + } else { + controller.startShowcase(); } } } + if (widget.autoPlay) { _cancelTimer(); _timer = Timer(