Created
June 19, 2024 07:10
-
-
Save CoderNamedHendrick/ba06199fc0738e72306f09fce34744dd to your computer and use it in GitHub Desktop.
Highlight widget overlay
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
mixin OverlayStateMixin<T extends StatefulWidget> on State<T> { | |
@override | |
void dispose() { | |
removeOverlay(); | |
super.dispose(); | |
} | |
@override | |
void didChangeDependencies() { | |
removeOverlay(); | |
super.didChangeDependencies(); | |
} | |
bool get isOverlayShown => _overlayEntry != null; | |
void toggleOverlay({ | |
required GlobalKey onboardItemKey, | |
required Widget overlayInformationChild, | |
}) => | |
isOverlayShown | |
? removeOverlay() | |
: _insertOverlay(onboardItemKey, overlayInformationChild); | |
OverlayEntry? _overlayEntry; | |
void removeOverlay() { | |
_overlayEntry?.remove(); | |
_overlayEntry = null; | |
} | |
Widget _dismissibleOverlay( | |
GlobalKey onboardItemKey, | |
Widget overlayInformationChild, | |
) { | |
final renderBox = | |
onboardItemKey.currentContext?.findRenderObject() as RenderBox; | |
final offset = renderBox.localToGlobal(Offset.zero); | |
final boxDecoration = (onboardItemKey.currentContext!.widget as Container) | |
.decoration as BoxDecoration; | |
final borderRadius = boxDecoration.borderRadius as BorderRadius; | |
return Material( | |
color: Colors.transparent, | |
child: Stack( | |
alignment: AlignmentDirectional.topStart, | |
children: [ | |
Positioned.fill( | |
child: ClipPath( | |
clipper: _WidgetFocusClipper( | |
offset: offset, | |
widgetSize: renderBox.size, | |
widgetBorderRadius: borderRadius.topLeft, | |
), | |
child: ColoredBox( | |
color: Colors.black.withOpacity(0.7), | |
child: GestureDetector( | |
onTap: removeOverlay, | |
), | |
), | |
), | |
), | |
Positioned.fill( | |
top: offset.dy + renderBox.size.height + 8, | |
left: offset.dx, | |
child: OverlayInformation( | |
widgetAtTop: true, | |
child: overlayInformationChild, | |
), | |
), | |
], | |
), | |
); | |
} | |
void _insertOverlay( | |
GlobalKey onboardItemKey, | |
Widget overlayInformationChild, | |
) { | |
_overlayEntry = OverlayEntry( | |
builder: (_) => _dismissibleOverlay( | |
onboardItemKey, | |
overlayInformationChild, | |
), | |
); | |
Overlay.of(context).insert(_overlayEntry!); | |
} | |
} | |
class OverlayInformation extends StatelessWidget { | |
const OverlayInformation({ | |
super.key, | |
required this.child, | |
this.widgetAtTop = true, | |
}); | |
final Widget child; | |
final bool widgetAtTop; | |
@override | |
Widget build(BuildContext context) { | |
return Row( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
mainAxisAlignment: MainAxisAlignment.start, | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Expanded( | |
child: _ChatIcon(isBottom: !widgetAtTop, child: child), | |
), | |
const SizedBox(width: 80), | |
], | |
); | |
} | |
} | |
class _ChatIcon extends StatelessWidget { | |
const _ChatIcon({ | |
required this.child, | |
this.isBottom = false, | |
}); | |
final Widget child; | |
final bool isBottom; | |
@override | |
Widget build(BuildContext context) { | |
return Stack( | |
children: [ | |
Positioned( | |
top: isBottom ? null : 0, | |
bottom: isBottom ? 0 : null, | |
left: 40, | |
child: RotatedBox( | |
quarterTurns: isBottom ? 2 : 0, | |
child: const CustomPaint( | |
painter: _RoundedTriangle(Colors.white), | |
size: Size(48, 50), | |
), | |
), | |
), | |
Column( | |
mainAxisSize: MainAxisSize.min, | |
mainAxisAlignment: MainAxisAlignment.start, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
if (!isBottom) const SizedBox(height: 20), | |
Container( | |
padding: const EdgeInsets.all(18), | |
decoration: const BoxDecoration( | |
color: Colors.white, | |
borderRadius: BorderRadius.all(Radius.circular(8)), | |
), | |
alignment: Alignment.center, | |
child: child, | |
), | |
if (isBottom) const SizedBox(height: 20), | |
], | |
), | |
], | |
); | |
} | |
} | |
class _RoundedTriangle extends CustomPainter { | |
final Color color; | |
const _RoundedTriangle([this.color = Colors.red]); | |
@override | |
void paint(Canvas canvas, Size size) { | |
final paint = Paint() | |
..strokeJoin = StrokeJoin.round | |
..strokeWidth = 5 | |
..strokeCap = StrokeCap.round | |
// ..style = PaintingStyle.stroke | |
..color = color; | |
final path = Path() | |
..moveTo(0, size.height - 20) | |
..quadraticBezierTo( | |
//control points | |
-10, size.height, | |
// endpoint | |
20, size.height, | |
) | |
..lineTo(size.width - 20, size.height) | |
..quadraticBezierTo( | |
// control points | |
size.width + 10, size.height, | |
// endpoints | |
size.width, size.height - 20, | |
) | |
..lineTo(size.width / 2 + 20, 20) | |
..quadraticBezierTo( | |
// control points | |
size.width / 2, -10, | |
// endpoints | |
size.width / 2 - 20, 20, | |
) | |
..close(); | |
canvas.drawPath(path, paint); | |
} | |
@override | |
bool shouldRepaint(covariant CustomPainter oldDelegate) => false; | |
} | |
class _WidgetFocusClipper extends CustomClipper<Path> { | |
final Offset offset; | |
final Size widgetSize; | |
final Radius widgetBorderRadius; | |
const _WidgetFocusClipper({ | |
required this.offset, | |
required this.widgetSize, | |
this.widgetBorderRadius = const Radius.circular(4.0), | |
}); | |
@override | |
Path getClip(Size size) { | |
return Path() | |
..moveTo(0, 0) | |
..lineTo(0, size.height) | |
..lineTo(size.width, size.height) | |
..lineTo(size.width, 0) | |
..close() | |
..moveTo(offset.dx, offset.dy) | |
..addRRect( | |
RRect.fromRectAndRadius( | |
Rect.fromLTWH( | |
offset.dx - 5, | |
offset.dy - 5, | |
widgetSize.width + 10, | |
widgetSize.height + 10, | |
), | |
widgetBorderRadius + const Radius.circular(5), | |
), | |
); | |
} | |
@override | |
bool shouldReclip(covariant CustomClipper<Path> oldClipper) { | |
return false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment