Skip to content

Commit 703bfd8

Browse files
committed
fix: 🩹 Shifted everything to showcase controller
1 parent 35ec3f7 commit 703bfd8

File tree

5 files changed

+184
-147
lines changed

5 files changed

+184
-147
lines changed

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,21 +110,30 @@ Showcase.withWidget(
110110
),
111111
```
112112

113-
5. Starting the `ShowCase`
113+
5. Starting the `ShowCase`:
114114
```dart
115115
someEvent(){
116116
ShowCaseWidget.of(context).startShowCase([_one, _two, _three]);
117117
}
118118
```
119119

120-
If you want to start the `ShowCaseView` as soon as your UI built up then use below code.
120+
If you want to start the `ShowCaseView` as soon as your UI built up then use below code:
121121

122122
```dart
123123
WidgetsBinding.instance.addPostFrameCallback((_) =>
124124
ShowCaseWidget.of(context).startShowCase([_one, _two, _three])
125125
);
126126
```
127127

128+
If you have some animation or transition in your UI and you want to start the `ShowCaseView` after
129+
that then use below code:
130+
131+
```dart
132+
WidgetsBinding.instance.addPostFrameCallback((_) =>
133+
ShowCaseWidget.of(context).startShowCase([_one, _two, _three], delay: "Animation Duration")
134+
);
135+
```
136+
128137
## MultiShowcaseView
129138
To show multiple showcase at the same time provide same key to showcase.
130139
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
287296

288297
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`.
289298

290-
Example,
299+
Example:
291300

292301
```dart
293302
// This controller will be assigned to respected sctollview.

example/lib/main.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -468,8 +468,12 @@ class _MailPageState extends State<MailPage> {
468468
),
469469
).then((_) {
470470
setState(() {
471-
ShowCaseWidget.of(context)
472-
.startShowCase([_four, _lastShowcaseWidget]);
471+
ShowCaseWidget.of(context).startShowCase(
472+
[_four, _lastShowcaseWidget],
473+
delay: const Duration(
474+
milliseconds: 200,
475+
),
476+
);
473477
});
474478
});
475479
},

lib/src/showcase/showcase.dart

Lines changed: 10 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import 'package:flutter/material.dart';
2424

2525
import '../constants.dart';
2626
import '../enum.dart';
27-
import '../get_position.dart';
2827
import '../models/tooltip_action_button.dart';
2928
import '../models/tooltip_action_config.dart';
3029
import '../showcase_widget.dart';
@@ -573,25 +572,30 @@ class _ShowcaseState extends State<Showcase> {
573572
ShowcaseController(
574573
id: _uniqueId,
575574
key: widget.showcaseKey,
576-
config: widget,
575+
showcaseState: this,
577576
showCaseWidgetState: ShowCaseWidget.of(context),
578-
scrollIntoViewCallback: _scrollIntoView,
579-
updateControllerValue: _updateOverlayData,
580-
).startShowcase = _startShowcase;
577+
);
581578
}
582579

583580
@override
584581
void didUpdateWidget(covariant Showcase oldWidget) {
585582
super.didUpdateWidget(oldWidget);
586583
if (oldWidget == widget) return;
584+
_updateControllerValues();
585+
}
586+
587+
_updateControllerValues() {
587588
_showCaseWidgetState = ShowCaseWidget.of(context);
588589
_controller
589-
..config = widget
590+
..showcaseState = this
590591
..showCaseWidgetState = _showCaseWidgetState;
591592
}
592593

593594
@override
594595
Widget build(BuildContext context) {
596+
// This is to support hot reload
597+
_updateControllerValues();
598+
595599
_controller.recalculateRootWidgetSize(context);
596600
return widget.child;
597601
}
@@ -602,62 +606,6 @@ class _ShowcaseState extends State<Showcase> {
602606
key: widget.showcaseKey,
603607
uniqueShowcaseKey: _uniqueId,
604608
);
605-
606609
super.dispose();
607610
}
608-
609-
void _startShowcase() {
610-
if (!_showCaseWidgetState.enableShowcase) return;
611-
612-
_controller
613-
..recalculateRootWidgetSize(context)
614-
..globalFloatingActionWidget = _showCaseWidgetState
615-
.globalFloatingActionWidget(widget.showcaseKey)
616-
?.call(context);
617-
final size = _controller.rootWidgetSize ?? MediaQuery.of(context).size;
618-
_controller.position ??= GetPosition(
619-
rootRenderObject: _controller.rootRenderObject,
620-
renderBox: context.findRenderObject() as RenderBox?,
621-
padding: widget.targetPadding,
622-
screenWidth: size.width,
623-
screenHeight: size.height,
624-
);
625-
}
626-
627-
void _updateOverlayData() {
628-
_controller.updateControllerData(
629-
context.findRenderObject() as RenderBox?,
630-
MediaQuery.of(context).size,
631-
);
632-
}
633-
634-
Future<void> _scrollIntoView() async {
635-
if (!mounted) return;
636-
_controller
637-
..isScrollRunning = true
638-
..updateControllerData(
639-
context.findRenderObject() as RenderBox?,
640-
MediaQuery.of(context).size,
641-
);
642-
_startShowcase();
643-
_showCaseWidgetState.updateOverlay?.call(
644-
_showCaseWidgetState.isShowcaseRunning,
645-
);
646-
await Scrollable.ensureVisible(
647-
context,
648-
duration: _showCaseWidgetState.widget.scrollDuration,
649-
alignment: widget.scrollAlignment,
650-
);
651-
if (!mounted) return;
652-
_controller
653-
..isScrollRunning = false
654-
..updateControllerData(
655-
context.findRenderObject() as RenderBox?,
656-
MediaQuery.of(context).size,
657-
);
658-
_startShowcase();
659-
_showCaseWidgetState.updateOverlay?.call(
660-
_showCaseWidgetState.isShowcaseRunning,
661-
);
662-
}
663611
}

lib/src/showcase/showcase_controller.dart

Lines changed: 102 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,13 @@ class ShowcaseController {
2323
///
2424
/// * [id] - Unique identifier for this showcase instance
2525
/// * [key] - Global key associated with the showcase widget
26-
/// * [config] - Configuration settings for the showcase
26+
/// * [showcaseState] - Reference to the showcase state
2727
/// * [showCaseWidgetState] - Reference to the parent showcase widget state
28-
/// * [scrollIntoViewCallback] - Optional callback to scroll the target into view
2928
ShowcaseController({
3029
required this.id,
3130
required this.key,
32-
required this.config,
31+
required this.showcaseState,
3332
required this.showCaseWidgetState,
34-
required this.updateControllerValue,
35-
this.scrollIntoViewCallback,
3633
}) {
3734
showCaseWidgetState.registerShowcaseController(
3835
controller: this,
@@ -49,7 +46,7 @@ class ShowcaseController {
4946
final GlobalKey key;
5047

5148
/// Configuration for the showcase
52-
Showcase config;
49+
State<Showcase> showcaseState;
5350

5451
/// Reference to the parent showcase widget state
5552
ShowCaseWidgetState showCaseWidgetState;
@@ -60,23 +57,9 @@ class ShowcaseController {
6057
/// Data model for linked showcases
6158
LinkedShowcaseDataModel? linkedShowcaseDataModel;
6259

63-
/// Callback to start the showcase
64-
VoidCallback? startShowcase;
65-
66-
/// Optional function to scroll the target into view
67-
final ValueGetter<Future<void>>? scrollIntoViewCallback;
68-
6960
/// Optional function to reverse the animation
7061
ValueGetter<Future<void>>? reverseAnimationCallback;
7162

72-
/// Function to update the controller value
73-
///
74-
/// Main use of this is to update the controller data just before overlay is
75-
/// inserted so we can get the correct position. Which is need in
76-
/// page transition case where page transition may take some time to reach
77-
/// to it's original position
78-
VoidCallback updateControllerValue;
79-
8063
/// Size of the root widget
8164
Size? rootWidgetSize;
8265

@@ -95,6 +78,24 @@ class ShowcaseController {
9578
/// Global floating action widget to be displayed
9679
FloatingActionWidget? globalFloatingActionWidget;
9780

81+
/// Returns the Showcase widget configuration
82+
///
83+
/// Provides access to all properties and settings of the current showcase widget.
84+
/// This is used throughout the controller to access showcase configuration options.
85+
Showcase get config => showcaseState.widget;
86+
87+
/// Returns the BuildContext for this showcase
88+
///
89+
/// Used for positioning calculations and widget rendering.
90+
/// This context represents the location of the showcase target in the widget tree.
91+
BuildContext get _context => showcaseState.context;
92+
93+
/// Checks if the showcase context is still valid
94+
///
95+
/// Returns true if the context is mounted (valid) and false otherwise.
96+
/// Used to prevent operations on widgets that have been removed from the tree.
97+
bool get _mounted => showcaseState.context.mounted;
98+
9899
/// Initializes the root widget size and render object
99100
///
100101
/// Must be called after the widget is mounted to ensure proper measurements.
@@ -121,10 +122,7 @@ class ShowcaseController {
121122
? MediaQuery.of(context).size
122123
: rootRenderObject?.size;
123124
if (!showCaseWidgetState.enableShowcase) return;
124-
updateControllerData(
125-
context.findRenderObject() as RenderBox?,
126-
MediaQuery.of(context).size,
127-
);
125+
updateControllerData();
128126
showCaseWidgetState.updateOverlay?.call(
129127
showCaseWidgetState.isShowcaseRunning,
130128
);
@@ -136,12 +134,13 @@ class ShowcaseController {
136134
/// Rebuilds the showcase overlay with updated positioning information.
137135
/// Creates positioning data and updates the visual representation.
138136
///
139-
/// * [renderBox] The RenderBox of the target widget
140-
/// * [screenSize] The current screen size
141-
void updateControllerData(
142-
RenderBox? renderBox,
143-
Size screenSize,
144-
) {
137+
/// Another use of this is to update the controller data just before overlay is
138+
/// inserted so we can get the correct position. Which is need in
139+
/// page transition case where page transition may take some time to reach
140+
/// to it's original position
141+
void updateControllerData() {
142+
final renderBox = _context.findRenderObject() as RenderBox?;
143+
final screenSize = MediaQuery.of(_context).size;
145144
final size = rootWidgetSize ?? screenSize;
146145
final newPosition = GetPosition(
147146
rootRenderObject: rootRenderObject,
@@ -248,6 +247,79 @@ class ShowcaseController {
248247
];
249248
}
250249

250+
/// Callback to start the showcase
251+
///
252+
/// Initializes the showcase by calculating positions and preparing visual elements.
253+
/// This method is called when a showcase is about to be displayed to ensure all
254+
/// positioning data is accurate and up-to-date.
255+
///
256+
/// The method performs these key actions:
257+
/// - Exits early if showcases are disabled in the parent widget
258+
/// - Recalculates the root widget size to ensure accurate positioning
259+
/// - Sets up any global floating action widgets
260+
/// - Initializes position data if not already set
261+
///
262+
/// This method is typically called internally by the showcase system but
263+
/// can also be called manually to force a recalculation of showcase elements.
264+
void startShowcase() {
265+
if (!showCaseWidgetState.enableShowcase) return;
266+
267+
recalculateRootWidgetSize(_context);
268+
globalFloatingActionWidget = showCaseWidgetState
269+
.globalFloatingActionWidget(config.showcaseKey)
270+
?.call(_context);
271+
final size = rootWidgetSize ?? MediaQuery.of(_context).size;
272+
position ??= GetPosition(
273+
rootRenderObject: rootRenderObject,
274+
renderBox: _context.findRenderObject() as RenderBox?,
275+
padding: config.targetPadding,
276+
screenWidth: size.width,
277+
screenHeight: size.height,
278+
);
279+
}
280+
281+
/// Used to scroll the target into view
282+
///
283+
/// Ensures the showcased widget is visible on screen by scrolling to it.
284+
/// This method handles the complete scrolling process including:
285+
///
286+
/// - Setting visual indicators while scrolling is in progress
287+
/// - Updating the overlay to show loading state
288+
/// - Performing the actual scrolling operation
289+
/// - Refreshing the showcase display after scrolling completes
290+
///
291+
/// The method shows a loading indicator during scrolling and updates
292+
/// the showcase position after scrolling completes. It manages the
293+
/// `isScrollRunning` state to coordinate UI updates.
294+
///
295+
/// Note: Multi Showcase will not be scrolled into view
296+
///
297+
/// Returns a Future that completes when scrolling is finished. If the widget
298+
/// is unmounted during scrolling, the operation will be canceled safely.
299+
Future<void> scrollIntoView() async {
300+
if (!_mounted) return;
301+
302+
isScrollRunning = true;
303+
updateControllerData();
304+
startShowcase();
305+
showCaseWidgetState.updateOverlay?.call(
306+
showCaseWidgetState.isShowcaseRunning,
307+
);
308+
await Scrollable.ensureVisible(
309+
_context,
310+
duration: showCaseWidgetState.widget.scrollDuration,
311+
alignment: config.scrollAlignment,
312+
);
313+
if (!_mounted) return;
314+
315+
isScrollRunning = false;
316+
updateControllerData();
317+
startShowcase();
318+
showCaseWidgetState.updateOverlay?.call(
319+
showCaseWidgetState.isShowcaseRunning,
320+
);
321+
}
322+
251323
/// Moves to the next showcase if any are remaining
252324
///
253325
/// Called when a showcase is completed.

0 commit comments

Comments
 (0)