Skip to content

Commit 67879cf

Browse files
committed
feat: ✨ Added Multi Showcase functionality and improvements
1 parent df39208 commit 67879cf

16 files changed

+573
-392
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
Improved Tooltip widget
1212
- Feature [#54](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/54) - Added
1313
Feasibility to position tooltip left and right to the target widget.
14+
- Feature [#113](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/113) - Added
15+
multiple showcase feature
16+
- Improvement [#514](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/pull/514) -
17+
Improved showcase widget and showcase with widget, Removed inherited widget, keys and setStates,
18+
Added controller to manage showcase
1419

1520
## [4.0.1]
1621
- Fixed [#493](https://github.com/SimformSolutionsPvtLtd/flutter_showcaseview/issues/493) - ShowCase.withWidget not showing issue

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ WidgetsBinding.instance.addPostFrameCallback((_) =>
125125
);
126126
```
127127

128+
## MultiShowcaseView
129+
To show multiple showcase at the same time provide same key to showcase.
130+
Note: auto scroll to showcase will not work in case of the multi-showcase and we will use property
131+
of first initialized showcase for common things like barrier tap and colors.
132+
128133
## Functions of `ShowCaseWidget.of(context)`:
129134

130135
| Function Name | Description |

lib/showcaseview.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export 'src/enum.dart';
2626
export 'src/models/action_button_icon.dart';
2727
export 'src/models/tooltip_action_button.dart';
2828
export 'src/models/tooltip_action_config.dart';
29-
export 'src/showcase.dart';
29+
export 'src/showcase/showcase.dart';
3030
export 'src/showcase_widget.dart';
3131
export 'src/tooltip_action_button_widget.dart';
3232
export 'src/widget/floating_action_widget.dart';

lib/src/enum.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ enum TooltipDefaultActionType {
9494
void onTap(ShowCaseWidgetState showCaseState) {
9595
switch (this) {
9696
case TooltipDefaultActionType.next:
97-
showCaseState.next();
97+
showCaseState.next(true);
9898
break;
9999
case TooltipDefaultActionType.previous:
100100
showCaseState.previous();

lib/src/get_position.dart

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import 'package:flutter/material.dart';
2626

2727
class GetPosition {
2828
GetPosition({
29-
required this.key,
29+
required this.context,
3030
required this.screenWidth,
3131
required this.screenHeight,
3232
this.padding = EdgeInsets.zero,
@@ -35,19 +35,23 @@ class GetPosition {
3535
getRenderBox();
3636
}
3737

38-
final GlobalKey key;
38+
final BuildContext context;
3939
final EdgeInsets padding;
4040
final double screenWidth;
4141
final double screenHeight;
4242
final RenderObject? rootRenderObject;
4343

4444
late final RenderBox? _box;
4545
late final Offset? _boxOffset;
46+
late final Offset? overlayOffset;
4647

4748
RenderBox? get box => _box;
4849

4950
void getRenderBox() {
50-
var renderBox = key.currentContext?.findRenderObject() as RenderBox?;
51+
var renderBox = context.findRenderObject() as RenderBox?;
52+
53+
overlayOffset =
54+
(rootRenderObject?.parent as RenderBox?)?.localToGlobal(Offset.zero);
5155

5256
if (renderBox == null) return;
5357

@@ -72,7 +76,10 @@ class GetPosition {
7276
final topLeft = _box!.size.topLeft(_boxOffset!);
7377
final bottomRight = _box!.size.bottomRight(_boxOffset!);
7478
final leftDx = topLeft.dx - padding.left;
75-
final leftDy = topLeft.dy - padding.top;
79+
var leftDy = topLeft.dy - padding.top;
80+
if (leftDy < 0) {
81+
leftDy = 0;
82+
}
7683
final rect = Rect.fromLTRB(
7784
leftDx.clamp(0, leftDx),
7885
leftDy.clamp(0, leftDy),
@@ -123,4 +130,15 @@ class GetPosition {
123130
double getWidth() => getRight() - getLeft();
124131

125132
double getCenter() => (getLeft() + getRight()) * 0.5;
133+
134+
Offset topLeft() =>
135+
_box?.size.topLeft(
136+
_box!.localToGlobal(
137+
Offset.zero,
138+
ancestor: rootRenderObject,
139+
),
140+
) ??
141+
Offset.zero;
142+
143+
Offset getOffSet() => _box?.size.center(topLeft()) ?? Offset.zero;
126144
}

lib/src/layout_overlays.dart

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

2525
import 'showcase_widget.dart';
2626

27-
typedef OverlayBuilderCallback = Widget Function(
28-
BuildContext context,
29-
Rect anchorBounds,
30-
Offset anchor,
31-
);
32-
33-
/// Displays an overlay Widget anchored directly above the center of this
34-
/// [AnchoredOverlay].
35-
///
36-
/// The overlay Widget is created by invoking the provided [overlayBuilder].
37-
///
38-
/// The [anchor] position is provided to the [overlayBuilder], but the builder
39-
/// does not have to respect it. In other words, the [overlayBuilder] can
40-
/// interpret the meaning of "anchor" however it wants - the overlay will not
41-
/// be forced to be centered about the [anchor].
42-
///
43-
/// The overlay built by this [AnchoredOverlay] can be conditionally shown
44-
/// and hidden by settings the [showOverlay] property to true or false.
45-
///
46-
/// The [overlayBuilder] is invoked every time this Widget is rebuilt.
47-
///
48-
class AnchoredOverlay extends StatelessWidget {
49-
final bool showOverlay;
50-
final OverlayBuilderCallback? overlayBuilder;
51-
final Widget? child;
52-
final RenderObject? rootRenderObject;
53-
54-
const AnchoredOverlay({
55-
super.key,
56-
this.showOverlay = false,
57-
this.overlayBuilder,
58-
this.child,
59-
this.rootRenderObject,
60-
});
61-
62-
@override
63-
Widget build(BuildContext context) {
64-
return LayoutBuilder(
65-
builder: (context, constraints) {
66-
return OverlayBuilder(
67-
showOverlay: showOverlay,
68-
overlayBuilder: (overlayContext) {
69-
// To calculate the "anchor" point we grab the render box of
70-
// our parent Container and then we find the center of that box.
71-
final box = context.findRenderObject() as RenderBox?;
72-
73-
/// Handle null RenderBox safely.
74-
final topLeft = box?.size.topLeft(
75-
box.localToGlobal(
76-
Offset.zero,
77-
ancestor: rootRenderObject,
78-
),
79-
) ??
80-
Offset.zero;
81-
final bottomRight = box?.size.bottomRight(
82-
box.localToGlobal(
83-
Offset.zero,
84-
ancestor: rootRenderObject,
85-
),
86-
) ??
87-
Offset.zero;
88-
89-
/// Provide a default anchorBounds if box is null.
90-
final anchorBounds = (topLeft.dx.isNaN ||
91-
topLeft.dy.isNaN ||
92-
bottomRight.dx.isNaN ||
93-
bottomRight.dy.isNaN)
94-
? const Rect.fromLTRB(0.0, 0.0, 0.0, 0.0)
95-
: Rect.fromLTRB(
96-
topLeft.dx,
97-
topLeft.dy,
98-
bottomRight.dx,
99-
bottomRight.dy,
100-
);
101-
102-
/// Calculate the anchor center or default to Offset.zero.
103-
final anchorCenter = box?.size.center(topLeft) ?? Offset.zero;
104-
105-
/// Pass the anchor details to the overlay builder.
106-
return overlayBuilder!(overlayContext, anchorBounds, anchorCenter);
107-
},
108-
child: child,
109-
);
110-
},
111-
);
112-
}
113-
}
27+
typedef OverlayUpdateCallback = void Function(VoidCallback updateOverlay);
11428

11529
/// Displays an overlay Widget as constructed by the given [overlayBuilder].
11630
///
@@ -128,9 +42,11 @@ class OverlayBuilder extends StatefulWidget {
12842
final bool showOverlay;
12943
final WidgetBuilder? overlayBuilder;
13044
final Widget? child;
45+
final OverlayUpdateCallback updateOverlay;
13146

13247
const OverlayBuilder({
13348
super.key,
49+
required this.updateOverlay,
13450
this.showOverlay = false,
13551
this.overlayBuilder,
13652
this.child,
@@ -150,12 +66,20 @@ class _OverlayBuilderState extends State<OverlayBuilder> {
15066
if (widget.showOverlay) {
15167
WidgetsBinding.instance.addPostFrameCallback((_) => showOverlay());
15268
}
69+
widget.updateOverlay.call(updateOverlay);
70+
}
71+
72+
void updateOverlay() {
73+
buildOverlay();
74+
WidgetsBinding.instance.addPostFrameCallback((_) => syncWidgetAndOverlay());
15375
}
15476

15577
@override
15678
void didUpdateWidget(OverlayBuilder oldWidget) {
15779
super.didUpdateWidget(oldWidget);
158-
WidgetsBinding.instance.addPostFrameCallback((_) => syncWidgetAndOverlay());
80+
if (oldWidget.showOverlay != widget.showOverlay && widget.showOverlay) {
81+
WidgetsBinding.instance.addPostFrameCallback((_) => showOverlay());
82+
}
15983
}
16084

16185
@override
@@ -221,8 +145,6 @@ class _OverlayBuilderState extends State<OverlayBuilder> {
221145

222146
@override
223147
Widget build(BuildContext context) {
224-
buildOverlay();
225-
226148
return widget.child!;
227149
}
228150
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import 'package:flutter/widgets.dart';
2+
3+
/// This model is used to move linked showcase overlay data to parent
4+
/// showcase to crop linked showcase rect
5+
class LinkedShowcaseDataModel {
6+
final Rect rect;
7+
final EdgeInsets overlayPadding;
8+
final BorderRadius? radius;
9+
final bool isCircle;
10+
11+
const LinkedShowcaseDataModel({
12+
required this.rect,
13+
required this.radius,
14+
required this.overlayPadding,
15+
required this.isCircle,
16+
});
17+
}

lib/src/shape_clipper.dart

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,21 @@ import 'dart:ui' as ui;
2424

2525
import 'package:flutter/material.dart';
2626

27+
import 'models/linked_showcase_data.dart';
28+
2729
class RRectClipper extends CustomClipper<ui.Path> {
2830
final bool isCircle;
2931
final BorderRadius? radius;
3032
final EdgeInsets overlayPadding;
3133
final Rect area;
34+
final List<LinkedShowcaseDataModel> linkedObjectData;
3235

3336
RRectClipper({
3437
this.isCircle = false,
3538
this.radius,
3639
this.overlayPadding = EdgeInsets.zero,
3740
this.area = Rect.zero,
41+
this.linkedObjectData = const <LinkedShowcaseDataModel>[],
3842
});
3943

4044
@override
@@ -49,7 +53,7 @@ class RRectClipper extends CustomClipper<ui.Path> {
4953
area.bottom + overlayPadding.bottom,
5054
);
5155

52-
return Path()
56+
var mainObjectPath = Path()
5357
..fillType = ui.PathFillType.evenOdd
5458
..addRect(Offset.zero & size)
5559
..addRRect(
@@ -61,12 +65,45 @@ class RRectClipper extends CustomClipper<ui.Path> {
6165
bottomRight: (radius?.bottomRight ?? customRadius),
6266
),
6367
);
68+
69+
for (final widgetRect in linkedObjectData) {
70+
final customRadius = widgetRect.isCircle
71+
? Radius.circular(widgetRect.rect.height)
72+
: const Radius.circular(3.0);
73+
74+
final rect = Rect.fromLTRB(
75+
widgetRect.rect.left - widgetRect.overlayPadding.left,
76+
widgetRect.rect.top - widgetRect.overlayPadding.top,
77+
widgetRect.rect.right + widgetRect.overlayPadding.right,
78+
widgetRect.rect.bottom + widgetRect.overlayPadding.bottom,
79+
);
80+
81+
/// We have use this approach so that overlapping cutout will merge with
82+
/// each other
83+
mainObjectPath = Path.combine(
84+
PathOperation.difference,
85+
mainObjectPath,
86+
Path()
87+
..addRRect(
88+
RRect.fromRectAndCorners(
89+
rect,
90+
topLeft: (widgetRect.radius?.topLeft ?? customRadius),
91+
topRight: (widgetRect.radius?.topRight ?? customRadius),
92+
bottomLeft: (widgetRect.radius?.bottomLeft ?? customRadius),
93+
bottomRight: (widgetRect.radius?.bottomRight ?? customRadius),
94+
),
95+
),
96+
);
97+
}
98+
99+
return mainObjectPath;
64100
}
65101

66102
@override
67103
bool shouldReclip(covariant RRectClipper oldClipper) =>
68104
isCircle != oldClipper.isCircle ||
69105
radius != oldClipper.radius ||
70106
overlayPadding != oldClipper.overlayPadding ||
71-
area != oldClipper.area;
107+
area != oldClipper.area ||
108+
linkedObjectData != oldClipper.linkedObjectData;
72109
}

0 commit comments

Comments
 (0)