diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dcd52bc..45076b20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +- Feature [#616](https://github.com/SimformSolutionsPvtLtd/showcaseview/issues/616) - Add overlay animation duration and curve for showcase barrier. Fixed by @[vasu-nageshri](https://github.com/vasu-nageshri) + ## 5.1.0 - Fixed [#650](https://github.com/SimformSolutionsPvtLtd/showcaseview/issues/650) - Fix null-check crash in ShowcaseService.getController during didUpdateWidget. Fixed by @[vatsaltanna-simformsolutions](https://github.com/vatsaltanna-simformsolutions) @@ -6,7 +10,7 @@ - Fixed [#645](https://github.com/SimformSolutionsPvtLtd/showcaseview/issues/645) - Prevent re-entrant calls to _onComplete during rapid barrier taps. Fixed by @[apizon](https://github.com/apizon) - Fixed [#639](https://github.com/SimformSolutionsPvtLtd/showcaseview/issues/639) - Add null-safety guards for async sequence transitions. Fixed by @[vasu-nageshri](https://github.com/vasu-nageshri) - Fixed [#622](https://github.com/SimformSolutionsPvtLtd/showcaseview/issues/622) - Resolve Semantics issue when using `go_router` and `showSemanticsDebugger` flag. Fixed by @[Sahil-Simform](https://github.com/Sahil-Simform) -- Fix [#620](https://github.com/SimformSolutionsPvtLtd/showcaseview/pull/620)- Set MouseRegion opaque property to false for improved interaction in TargetWidget, TooltipWrapper, and FloatingActionWidget. Fixed by @[RuslanTsitser](https://github.com/RuslanTsitser) +- Fixed [#620](https://github.com/SimformSolutionsPvtLtd/showcaseview/pull/620)- Set MouseRegion opaque property to false for improved interaction in TargetWidget, TooltipWrapper, and FloatingActionWidget. Fixed by @[RuslanTsitser](https://github.com/RuslanTsitser) ## [5.0.2] diff --git a/doc/documentation.md b/doc/documentation.md index 9c6a7e51..603a16fc 100644 --- a/doc/documentation.md +++ b/doc/documentation.md @@ -130,6 +130,10 @@ Reference](https://pub.dev/documentation/showcaseview/latest/showcaseview/). ShowcaseView.register( autoPlayDelay: const Duration(seconds: 3), semanticEnable: true, // Enable accessibility support globally + // Overlay barrier fades in when showcase first appears (default 200ms). + // Set overlayAnimationDuration to Duration.zero to disable. + overlayAnimationDuration: const Duration(milliseconds: 200), + overlayAnimationCurve: Curves.easeInOut, globalFloatingActionWidget: (showcaseContext) => FloatingActionWidget( left: 16, bottom: 16, diff --git a/lib/src/showcase/showcase_view.dart b/lib/src/showcase/showcase_view.dart index 15958164..a3be2b07 100644 --- a/lib/src/showcase/showcase_view.dart +++ b/lib/src/showcase/showcase_view.dart @@ -79,6 +79,8 @@ class ShowcaseView { this.blurValue = 0, this.overlayColor, this.overlayOpacity, + this.overlayAnimationDuration = Constants.defaultOverlayAnimationDuration, + this.overlayAnimationCurve = Constants.defaultOverlayAnimationCurve, this.globalTooltipActionConfig, this.globalTooltipActions, this.globalFloatingActionWidget, @@ -161,6 +163,24 @@ class ShowcaseView { /// Opacity apply on [overlayColor] (which ranges from 0.0 to 1.0) final double? overlayOpacity; + /// Duration of the fade-in animation applied to the overlay barrier + /// (background color, opacity and blur) when the showcase first appears. + /// + /// This animates only the initial appearance of the barrier and not the + /// transition between individual showcase steps. + /// + /// Defaults to 200 milliseconds. Set to [Duration.zero] to show the barrier + /// instantly. + final Duration overlayAnimationDuration; + + /// Curve used for the overlay barrier fade-in animation. + /// + /// Only applies when [overlayAnimationDuration] is greater than + /// [Duration.zero]. + /// + /// Defaults to [Curves.easeInOut]. + final Curve overlayAnimationCurve; + /// Whether to enable semantic properties for accessibility. /// /// When set to true, semantic widgets will be added to improve diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index 490e4811..5f6f0e5e 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -66,4 +66,12 @@ class Constants { static const Duration defaultAutoPlayDelay = Duration(milliseconds: 2000); static const Duration defaultScrollDuration = Duration(milliseconds: 300); + + /// Default duration for the overlay barrier fade-in animation. + /// Set to [Duration.zero] to make the barrier appear instantly. + static const Duration defaultOverlayAnimationDuration = + Duration(milliseconds: 200); + + /// Default curve for the overlay barrier fade-in animation. + static const Curve defaultOverlayAnimationCurve = Curves.easeInOut; } diff --git a/lib/src/utils/overlay_manager.dart b/lib/src/utils/overlay_manager.dart index 76646eb6..8ca4c00f 100644 --- a/lib/src/utils/overlay_manager.dart +++ b/lib/src/utils/overlay_manager.dart @@ -29,6 +29,7 @@ import '../showcase/showcase.dart'; import '../showcase/showcase_controller.dart'; import '../showcase/showcase_service.dart'; import '../showcase/showcase_view.dart'; +import '../widget/animated_overlay_barrier.dart'; import 'extensions.dart'; import 'shape_clipper.dart'; @@ -181,30 +182,57 @@ class OverlayManager { child: const Align(), ); + Widget barrier = GestureDetector( + onTap: firstController.handleBarrierTap, + child: ClipPath( + clipper: ShapeClipper( + linkedObjectData: _getLinkedShowcasesData(controllers), + ), + child: firstController.blur <= 0.2 + ? backgroundContainer + : BackdropFilter( + filter: ImageFilter.blur( + sigmaX: firstController.blur, + sigmaY: firstController.blur, + ), + child: backgroundContainer, + ), + ), + ); + + final overlayAnimationDuration = + firstController.showcaseView.overlayAnimationDuration; + if (overlayAnimationDuration > Duration.zero) { + // The barrier is kept outside the per-step keyed stack and given a stable + // key so its fade-in animation runs only once when the showcase first + // appears, instead of restarting (and flickering) on every step. + barrier = AnimatedOverlayBarrier( + key: const ValueKey('showcase_overlay_barrier'), + duration: overlayAnimationDuration, + curve: firstController.showcaseView.overlayAnimationCurve, + child: barrier, + ); + } + final overlayChild = Stack( - // This key is used to force rebuild the overlay when needed. - // this key enables `_overlayEntry?.markNeedsBuild();` to detect that - // output of the builder has changed. - key: ValueKey(firstController.id), children: [ - GestureDetector( - onTap: firstController.handleBarrierTap, - child: ClipPath( - clipper: ShapeClipper( - linkedObjectData: _getLinkedShowcasesData(controllers), - ), - child: firstController.blur <= 0.2 - ? backgroundContainer - : BackdropFilter( - filter: ImageFilter.blur( - sigmaX: firstController.blur, - sigmaY: firstController.blur, - ), - child: backgroundContainer, - ), + barrier, + // Tooltips are kept in their own stack so the barrier (and its + // optional fade-in) lives outside this keyed subtree. `Positioned.fill` + // is required because the inner stack has only `Positioned` children + // and would otherwise collapse, clipping the tooltips. + Positioned.fill( + child: Stack( + // This key forces the tooltips to rebuild when needed. It enables + // `_overlayEntry?.markNeedsBuild();` to detect that the output of + // the builder has changed and restarts the per-step tooltip + // animations between showcase steps. + key: ValueKey(firstController.id), + children: [ + ...controllers.expand((object) => object.tooltipWidgets), + ], ), ), - ...controllers.expand((object) => object.tooltipWidgets), ], ); diff --git a/lib/src/widget/animated_overlay_barrier.dart b/lib/src/widget/animated_overlay_barrier.dart new file mode 100644 index 00000000..b02add64 --- /dev/null +++ b/lib/src/widget/animated_overlay_barrier.dart @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2021 Simform Solutions + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import 'package:flutter/material.dart'; + +/// Fades the showcase overlay barrier (background color, opacity and blur) in +/// when the showcase first appears. +/// +/// The fade only runs once for the lifetime of this widget's [State]. The +/// overlay rebuilds on every showcase step to reposition the highlight cut-out, +/// so this widget is kept outside the per-step keyed subtree in +/// `OverlayManager` to preserve its animation state across steps and avoid the +/// barrier flickering between showcases. +class AnimatedOverlayBarrier extends StatefulWidget { + const AnimatedOverlayBarrier({ + required this.duration, + required this.curve, + required this.child, + super.key, + }); + + /// Duration of the fade-in animation. + final Duration duration; + + /// Curve of the fade-in animation. + final Curve curve; + + /// The barrier widget (background color/blur) to fade in. + final Widget child; + + @override + State createState() => _AnimatedOverlayBarrierState(); +} + +class _AnimatedOverlayBarrierState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller = AnimationController( + vsync: this, + duration: widget.duration, + ); + + late final Animation _opacity = CurvedAnimation( + parent: _controller, + curve: widget.curve, + ); + + @override + void initState() { + super.initState(); + _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: _opacity, + child: widget.child, + ); + } +} diff --git a/lib/src/widget/floating_action_widget.dart b/lib/src/widget/floating_action_widget.dart index 70e049dd..c2b564a0 100644 --- a/lib/src/widget/floating_action_widget.dart +++ b/lib/src/widget/floating_action_widget.dart @@ -172,9 +172,10 @@ class FloatingActionWidget extends StatelessWidget { width: width, height: height, child: Material( - type: MaterialType.transparency, - color: Colors.transparent, - child: child), + type: MaterialType.transparency, + color: Colors.transparent, + child: child, + ), ); } }