import 'package:flutter/material.dart'; class ShowCaseWidget extends StatefulWidget { final Builder builder; /// Triggered when all the showcases are completed. final VoidCallback? onFinish; /// Triggered every time on start of each showcase. final Function(int?, GlobalKey)? onStart; /// Triggered every time on completion of each showcase final Function(int?, GlobalKey)? onComplete; /// Whether all showcases will auto sequentially start /// having time interval of [autoPlayDelay] . /// /// Default to `false` final bool autoPlay; /// Visibility time of current showcase when [autoplay] sets to true. /// /// Default to [Duration(seconds: 3)] final Duration autoPlayDelay; /// Whether blocking user interaction while [autoPlay] is enabled. /// /// Default to `false` final bool enableAutoPlayLock; /// Whether disabling bouncing/moving animation for all tooltips /// while showcasing /// /// Default to `false` final bool disableMovingAnimation; /// Whether disabling initial scale animation for all the default tooltips /// when showcase is started and completed /// /// Default to `false` final bool disableScaleAnimation; /// Whether disabling barrier interaction final bool disableBarrierInteraction; /// Provides time duration for auto scrolling when [enableAutoScroll] is true final Duration scrollDuration; /// Default overlay blur used by showcase. if [Showcase.blurValue] /// is not provided. /// /// Default value is 0. final double blurValue; /// While target widget is out viewport then /// whether enabling auto scroll so as to make the target widget visible. final bool enableAutoScroll; /// Enable/disable showcase globally. Enabled by default. final bool enableShowcase; const ShowCaseWidget({ required this.builder, this.onFinish, this.onStart, this.onComplete, this.autoPlay = false, this.autoPlayDelay = const Duration(milliseconds: 2000), this.enableAutoPlayLock = false, this.blurValue = 0, this.scrollDuration = const Duration(milliseconds: 300), this.disableMovingAnimation = false, this.disableScaleAnimation = false, this.enableAutoScroll = false, this.disableBarrierInteraction = false, this.enableShowcase = true, }); static GlobalKey? activeTargetWidget(BuildContext context) { return context .dependOnInheritedWidgetOfExactType<_InheritedShowCaseView>() ?.activeWidgetIds; } static ShowCaseWidgetState of(BuildContext context) { final state = context.findAncestorStateOfType(); if (state != null) { return state; } else { throw Exception('Please provide ShowCaseView context'); } } @override ShowCaseWidgetState createState() => ShowCaseWidgetState(); } class ShowCaseWidgetState extends State { List? ids; int? activeWidgetId; /// These properties are only here so that it can be accessed by /// [Showcase] bool get autoPlay => widget.autoPlay; bool get disableMovingAnimation => widget.disableMovingAnimation; bool get disableScaleAnimation => widget.disableScaleAnimation; Duration get autoPlayDelay => widget.autoPlayDelay; bool get enableAutoPlayLock => widget.enableAutoPlayLock; bool get enableAutoScroll => widget.enableAutoScroll; bool get disableBarrierInteraction => widget.disableBarrierInteraction; bool get enableShowcase => widget.enableShowcase; /// Returns value of [ShowCaseWidget.blurValue] double get blurValue => widget.blurValue; /// 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) { 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; setState(() { ids = widgetIds; activeWidgetId = 0; _onStart(); }); } /// Completes showcase of given key and starts next one /// otherwise will finish the entire showcase view void completed(GlobalKey? key) { if (ids != null && ids![activeWidgetId!] == key && mounted) { setState(() { _onComplete(); activeWidgetId = activeWidgetId! + 1; _onStart(); if (activeWidgetId! >= ids!.length) { _cleanupAfterSteps(); widget.onFinish?.call(); } }); } } /// Completes current active showcase and starts next one /// otherwise will finish the entire showcase view void next() { if (ids != null && mounted) { setState(() { _onComplete(); activeWidgetId = activeWidgetId! + 1; _onStart(); if (activeWidgetId! >= ids!.length) { _cleanupAfterSteps(); widget.onFinish?.call(); } }); } } /// 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) { setState(() { _onComplete(); activeWidgetId = activeWidgetId! - 1; _onStart(); if (activeWidgetId! >= ids!.length) { _cleanupAfterSteps(); widget.onFinish?.call(); } }); } } /// Dismiss entire showcase view void dismiss() { if (mounted) setState(_cleanupAfterSteps); } void _onStart() { if (activeWidgetId! < ids!.length) { widget.onStart?.call(activeWidgetId, ids![activeWidgetId!]); } } void _onComplete() { widget.onComplete?.call(activeWidgetId, ids![activeWidgetId!]); } void _cleanupAfterSteps() { ids = null; activeWidgetId = null; } @override Widget build(BuildContext context) { return _InheritedShowCaseView( activeWidgetIds: ids?.elementAt(activeWidgetId!), child: widget.builder, ); } } class _InheritedShowCaseView extends InheritedWidget { final GlobalKey? activeWidgetIds; const _InheritedShowCaseView({ required this.activeWidgetIds, required Widget child, }) : super(child: child); @override bool updateShouldNotify(_InheritedShowCaseView oldWidget) => oldWidget.activeWidgetIds != activeWidgetIds; }