Skip to content

Instantly share code, notes, and snippets.

@nicholasspencer
Created May 14, 2020 05:32
Show Gist options
  • Save nicholasspencer/59713521cce97f53ebdccae0bb7bacc7 to your computer and use it in GitHub Desktop.
Save nicholasspencer/59713521cce97f53ebdccae0bb7bacc7 to your computer and use it in GitHub Desktop.
Tutorial widget knockout system
class Tutorial extends StatefulWidget {
final Widget child;
final bool show;
const Tutorial({
@required this.child,
this.show = false,
Key key,
}) : super(key: key);
static TutorialState of(BuildContext context) =>
context.findAncestorStateOfType<TutorialState>();
@override
TutorialState createState() => TutorialState();
}
class TutorialState extends State<Tutorial> {
Set<RRect> rects = {};
@override
Widget build(BuildContext context) {
return widget.show != true
? widget.child
: Stack(
children: <Widget>[
widget.child,
Positioned.fill(
child: ClipPath(
clipper: TutorialClipper(
rects: rects,
),
child: Material(
color: Colors.black87,
),
),
),
],
);
}
void addRRect(RRect rect) {
rects.add(rect);
}
void removeRRect(RRect rect) {
rects.remove(rect);
}
}
class TutorialHighlight extends StatefulWidget {
final Widget child;
final EdgeInsets padding;
final Radius radius;
const TutorialHighlight({
Key key,
this.child,
this.padding = EdgeInsets.zero,
this.radius = Radius.zero,
}) : super(key: key);
@override
_TutorialHighlightState createState() => _TutorialHighlightState();
}
class _TutorialHighlightState extends State<TutorialHighlight> {
RRect rect;
@override
void didUpdateWidget(TutorialHighlight oldWidget) {
bubbleRect();
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) => Container(child: widget.child);
void bubbleRect() {
final tutorial = Tutorial.of(context);
final tutorialBox = tutorial?.context?.findRenderObject() as RenderBox;
final box = context?.findRenderObject() as RenderBox;
if ((box?.hasSize == false ?? false) ||
(tutorialBox?.hasSize == false ?? false)) {
return;
}
if (this.rect != null) {
tutorial.removeRRect(this.rect);
this.rect = null;
}
final position = box.localToGlobal(Offset.zero, ancestor: tutorialBox);
final size = box.size;
final rect = RRect.fromLTRBR(
position.dx + (-widget.padding.left),
position.dy + (-widget.padding.top),
position.dx + size.width + widget.padding.right,
position.dy + size.height + widget.padding.bottom,
widget.radius,
);
this.rect = rect;
tutorial.addRRect(rect);
}
}
class TutorialClipper extends CustomClipper<Path> {
final Set<RRect> rects;
const TutorialClipper({
@required this.rects,
});
@override
Path getClip(Size size) {
final path = Path();
for (var rrect in rects) {
path.addRRect(rrect);
}
path.addRect(new Rect.fromLTWH(0.0, 0.0, size.width, size.height));
path.fillType = PathFillType.evenOdd;
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => this != oldClipper;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment