Last active
October 21, 2024 03:57
-
-
Save passsy/8ea2cab6de53f472e5fe797bdc7f114a to your computer and use it in GitHub Desktop.
A Flutter overlay implementation that allows alligning the overlay freely around the target (button)
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
import 'dart:async'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/rendering.dart'; | |
/// Written by PHNTM GmbH | |
void main() { | |
runApp(const AdvancedOverlaySample()); | |
} | |
class AdvancedOverlaySample extends StatefulWidget { | |
const AdvancedOverlaySample({super.key}); | |
@override | |
State<AdvancedOverlaySample> createState() => _AdvancedOverlaySampleState(); | |
} | |
class _AdvancedOverlaySampleState extends State<AdvancedOverlaySample> { | |
final AdvancedOverlayController controller = AdvancedOverlayController(); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
home: Scaffold( | |
appBar: AppBar( | |
title: const Text('AdvancedOverlay Sample'), | |
), | |
body: Align( | |
alignment: Alignment(0, -0.5), | |
child: SizedBox( | |
width: 200, | |
height: 100, | |
child: AdvancedOverlay( | |
// Adjust where the overlay is anchored here | |
targetAnchor: Alignment.bottomRight, | |
overlayAnchor: Alignment.topRight, | |
// | |
overlayController: controller, | |
overlayBuilder: (BuildContext context) { | |
return SizedBox( | |
width: 300, | |
height: 154, | |
child: Card( | |
child: Align( | |
alignment: Alignment.topLeft, | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
ListTile(title: Text('Option 1'), onTap: () {}), | |
ListTile(title: Text('Option 2'), onTap: () {}), | |
ListTile(title: Text('Option 3'), onTap: () {}), | |
], | |
), | |
), | |
), | |
); | |
}, | |
child: GestureDetector( | |
onTap: () { | |
if (controller.isShowing) { | |
controller.hide(); | |
} else { | |
controller.show(); | |
} | |
}, | |
child: ElevatedButton( | |
style: ElevatedButton.styleFrom( | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(2), | |
), | |
), | |
onPressed: () { | |
if (controller.isShowing) { | |
controller.hide(); | |
} else { | |
controller.show(); | |
} | |
}, | |
child: const Center( | |
child: Text('Show Overlay'), | |
), | |
), | |
), | |
), | |
), | |
), | |
), | |
); | |
} | |
} | |
/// An advanced overlay augmenting the [OverlayPortal] with additional features. | |
/// | |
/// These are some additinonal features: | |
/// [alignOverlayWithButtonSize] sizes the overlay to the button size. This is useful when using in combinations with form fields. | |
/// [alwaysOnScreen] ensures the overlay is always on screen. Turn off to not have the overlay move when it does not fit on the screen. | |
/// [onTapOutside] closes the overlay when tapping nearby. | |
class AdvancedOverlay extends StatefulWidget { | |
/// Creates an advanced overlay. | |
AdvancedOverlay({ | |
super.key, | |
required this.overlayController, | |
required this.overlayBuilder, | |
required this.child, | |
Alignment? overlayAnchor, | |
Alignment? targetAnchor, | |
Offset? animationOffset, | |
Offset? overlayOffset, | |
this.alwaysOnScreen, | |
bool? alignOverlayWithButtonSize, | |
this.onTapOutside, | |
this.overlayWidth, | |
this.onClose, | |
bool? includeChildOnTapOutside, | |
bool? hideOnTapOutside, | |
this.barrierColor = const Color(0x33000000), | |
this.useRootOverlay, | |
}) : overlayAnchor = overlayAnchor ?? Alignment.topLeft, | |
targetAnchor = targetAnchor ?? Alignment.bottomLeft, | |
animationOffset = animationOffset ?? _animationOffset(overlayAnchor ?? Alignment.topLeft), | |
overlayOffset = overlayOffset ?? Offset.zero, | |
hideOnTapOutside = hideOnTapOutside ?? true, | |
includeChildOnTapOutside = includeChildOnTapOutside ?? false, | |
alignOverlayWithButtonSize = alignOverlayWithButtonSize ?? false; | |
static Offset _animationOffset(Alignment alignment) { | |
if (alignment.y < 0) { | |
return const Offset(0, -8); | |
} | |
if (alignment.y > 0) { | |
return const Offset(0, 8); | |
} | |
return Offset.zero; | |
} | |
/// The controller that manages the overlay. | |
final AdvancedOverlayController overlayController; | |
/// The builder for the overlay content. | |
final WidgetBuilder overlayBuilder; | |
/// The anchor alignment of the overlay. | |
final Alignment overlayAnchor; | |
/// The anchor alignment of the target. | |
final Alignment targetAnchor; | |
/// The direction of the open animation in which the overlay should slightly move. | |
/// | |
/// Offset(0,0) means no movement. | |
/// Offset(0.1,0) means the overlay will move 10% to the right. | |
final Offset animationOffset; | |
/// When true, always moves the overlay in the visible area of the screen, even if it means that the overlay is not aligned with the button. | |
/// | |
/// This is useful when content is shown within a scrollable area and the overlay should always be visible | |
final bool? alwaysOnScreen; | |
/// If the overlay should align with the button width | |
final bool alignOverlayWithButtonSize; | |
/// If the overlay should close when tapping nearby. | |
final VoidCallback? onTapOutside; | |
/// The target that triggers the overlay. | |
final Widget child; | |
/// A fixed width of the overlay | |
final double? overlayWidth; | |
/// The offset of the overlay. | |
final Offset overlayOffset; | |
/// A callback that is called when the overlay is closed. | |
final VoidCallback? onClose; | |
/// If the overlay should hide when tapping outside. | |
/// This needs to be false when the child is using a [MouseRegion] to open/close the overlay. | |
final bool hideOnTapOutside; | |
/// If the child should be included in the tap outside test. | |
/// If true, when the child is tapped the overlay will not be closed. | |
final bool includeChildOnTapOutside; | |
/// The color of the barrier behind the overlay. | |
final Color? barrierColor; | |
/// Whether the overlay should be rendered in the root overlay | |
/// | |
/// Defaults to true, using the root [Overlay], not the closest | |
final bool? useRootOverlay; | |
@override | |
State<AdvancedOverlay> createState() => _AdvancedOverlayState(); | |
} | |
class _AdvancedOverlayState extends State<AdvancedOverlay> with SingleTickerProviderStateMixin { | |
final _link = LayerLink(); | |
final GlobalKey _childKey = GlobalKey(); | |
/// The size of the widget at the moment the trigger was clicked. | |
/// | |
/// When [includeChildOnTapOutside] is true, the child will be moved into the overlay. | |
/// The size will be used to place a placeholder at the old position to prevent the layout from jumping. | |
Size? _childSize; | |
late final AnimationController _barrierController; | |
late Animation<Color?> _barrierColorAnimation; | |
@override | |
void initState() { | |
super.initState(); | |
_barrierController = AnimationController(duration: const Duration(milliseconds: 200), vsync: this); | |
_barrierColorAnimation = ColorTween(begin: const Color(0x00000000), end: widget.barrierColor) | |
.animate(CurvedAnimation(parent: _barrierController, curve: Curves.easeInOutCubic)); | |
_populateController(); | |
} | |
@override | |
void didUpdateWidget(covariant AdvancedOverlay oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
oldWidget.overlayController._onClose = null; | |
oldWidget.overlayController._onOpen = null; | |
if (oldWidget.barrierColor != widget.barrierColor) { | |
_barrierColorAnimation = ColorTween(begin: const Color(0x00000000), end: widget.barrierColor) | |
.animate(CurvedAnimation(parent: _barrierController, curve: Curves.easeInOutCubic)); | |
} | |
_populateController(); | |
} | |
void _populateController() { | |
widget.overlayController._onClose = () async { | |
await _barrierController.reverse(); | |
widget.onClose?.call(); | |
}; | |
widget.overlayController._onOpen = () async { | |
_updateChildSize(); | |
_barrierController.forward(); | |
}; | |
} | |
@override | |
void dispose() { | |
widget.overlayController._onClose = null; | |
widget.overlayController._onOpen = null; | |
_barrierController.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
final child = () { | |
final trigger = KeyedSubtree( | |
key: _childKey, | |
child: widget.child, | |
); | |
if (!widget.includeChildOnTapOutside) { | |
return trigger; | |
} | |
return ListenableBuilder( | |
listenable: widget.overlayController, | |
builder: (BuildContext context, Widget? child) { | |
if (widget.overlayController.isShowing) { | |
// Placeholder to prevent layout jumping at the location where the child was before it was moved into the overlay. | |
return SizedBox( | |
width: _childSize!.width, | |
height: _childSize!.height, | |
); | |
} | |
return trigger; | |
}, | |
); | |
}(); | |
final portal = widget.useRootOverlay == false | |
? OverlayPortal( | |
controller: widget.overlayController._portalController, | |
overlayChildBuilder: overlayChildBuilder, | |
child: child, | |
) | |
: OverlayPortal.targetsRootOverlay( | |
controller: widget.overlayController._portalController, | |
overlayChildBuilder: overlayChildBuilder, | |
child: child, | |
); | |
return CompositedTransformTarget( | |
link: _link, | |
child: portal, | |
); | |
} | |
Widget overlayChildBuilder(BuildContext context) { | |
return Stack( | |
children: [ | |
if (widget.hideOnTapOutside) | |
Positioned.fill( | |
key: const ValueKey('barrier'), | |
child: AnimatedModalBarrier( | |
color: _barrierColorAnimation, | |
onDismiss: () async { | |
await _barrierController.reverse(); | |
widget.onTapOutside?.call(); | |
if (widget.hideOnTapOutside) { | |
widget.overlayController.hide(); | |
} | |
}, | |
), | |
), | |
// If the trigger should be included in the tap outside test, we put it in the overlay too to be fully | |
// recognized by Gesture Listeners like GestureDetector | |
if (widget.includeChildOnTapOutside) | |
CompositedTransformFollower( | |
key: const ValueKey('follower'), | |
link: _link, | |
child: SizedBox( | |
width: _childSize!.width, | |
height: _childSize!.height, | |
child: KeyedSubtree( | |
key: _childKey, | |
child: widget.child, | |
), | |
), | |
), | |
AdvancedOverlayFollower( | |
key: const ValueKey('widget.overlayBuilder'), | |
link: _link, | |
followerAnchor: widget.overlayAnchor, | |
targetAnchor: widget.targetAnchor, | |
alwaysOnScreen: widget.alwaysOnScreen, | |
targetKey: _childKey, | |
alignOverlayWithButtonSize: widget.alignOverlayWithButtonSize, | |
overlayWidth: widget.overlayWidth, | |
offset: widget.overlayOffset, | |
onClose: widget.onClose, | |
useRootOverlay: widget.useRootOverlay, | |
child: _OverlayRevealAnimation( | |
controller: _barrierController, | |
animationOffset: widget.animationOffset, | |
show: widget.overlayController.isShowing, | |
child: widget.overlayBuilder(context), | |
), | |
), | |
], | |
); | |
} | |
void _updateChildSize() { | |
final RenderBox? renderBox = _childKey.currentContext?.findRenderObject() as RenderBox?; | |
if (renderBox == null) { | |
return; | |
} | |
if (!renderBox.hasSize) { | |
return; | |
} | |
if (_childSize == renderBox.size) { | |
return; | |
} | |
setState(() { | |
_childSize = renderBox.size; | |
}); | |
} | |
} | |
/// A widget that follows a [LayerLink] and is always on screen. | |
class AdvancedOverlayFollower extends SingleChildRenderObjectWidget { | |
/// Creates a [AdvancedOverlayFollower]. | |
const AdvancedOverlayFollower({ | |
super.key, | |
required this.link, | |
required this.targetKey, | |
required this.targetAnchor, | |
required this.followerAnchor, | |
this.offset = Offset.zero, | |
this.alwaysOnScreen, | |
this.alignOverlayWithButtonSize, | |
this.overlayWidth, | |
this.onClose, | |
super.child, | |
this.useRootOverlay, | |
}); | |
/// The link to the target. | |
final LayerLink link; | |
/// The offset of the overlay. | |
final Offset offset; | |
/// The anchor alignment of the target. | |
final Alignment targetAnchor; | |
/// The anchor alignment of the overlay. | |
final Alignment followerAnchor; | |
/// If the overlay should always be on screen. | |
final bool? alwaysOnScreen; | |
/// If the overlay should align with the button size. | |
final bool? alignOverlayWithButtonSize; | |
/// A fixed width of the overlay | |
final double? overlayWidth; | |
/// A callback that is called when the overlay is closed. | |
final VoidCallback? onClose; | |
/// Whether the overlay should be rendered in the root overlay | |
/// | |
/// Defaults to true, using the root [Overlay], not the closest | |
final bool? useRootOverlay; | |
/// The globalKey of the target, allowing to read the size of the render object | |
final GlobalKey targetKey; | |
Rect _getOverlayRect(BuildContext context, {required bool rootOverlay}) { | |
final OverlayState overlay = Overlay.of(context, rootOverlay: rootOverlay); | |
final box = (overlay.context as Element).renderObject! as RenderBox; | |
final position = box.localToGlobal(Offset.zero); | |
final rect = Rect.fromLTWH(position.dx, position.dy, box.size.width, box.size.height); | |
return rect; | |
} | |
@override | |
RenderAdvancedOverlayFollower createRenderObject(BuildContext context) { | |
return RenderAdvancedOverlayFollower( | |
link: link, | |
offset: offset, | |
leaderAnchor: targetAnchor, | |
followerAnchor: followerAnchor, | |
alwaysOnScreen: alwaysOnScreen, | |
alignOverlayWithButtonSize: alignOverlayWithButtonSize, | |
overlayWidth: overlayWidth, | |
targetKey: targetKey, | |
overlayRect: _getOverlayRect(context, rootOverlay: useRootOverlay ?? true), | |
); | |
} | |
@override | |
void updateRenderObject(BuildContext context, RenderAdvancedOverlayFollower renderObject) { | |
renderObject | |
..link = link | |
..offset = offset | |
..leaderAnchor = targetAnchor | |
..followerAnchor = followerAnchor | |
..alwaysOnScreen = alwaysOnScreen | |
..alignOverlayWithButtonSize = alignOverlayWithButtonSize | |
..overlayWidth = overlayWidth | |
..targetKey = targetKey | |
..overlayRect = _getOverlayRect(context, rootOverlay: useRootOverlay ?? true); | |
} | |
} | |
/// A render object that follows a [LayerLink] and is always on screen. | |
class RenderAdvancedOverlayFollower extends RenderProxyBox { | |
/// Creates a [RenderAdvancedOverlayFollower]. | |
RenderAdvancedOverlayFollower({ | |
required LayerLink link, | |
Offset offset = Offset.zero, | |
required Alignment leaderAnchor, | |
required Alignment followerAnchor, | |
required GlobalKey targetKey, | |
bool? alwaysOnScreen, | |
bool? alignOverlayWithButtonSize, | |
double? overlayWidth, | |
RenderBox? child, | |
required Rect overlayRect, | |
}) : _link = link, | |
_offset = offset, | |
_leaderAnchor = leaderAnchor, | |
_alwaysOnScreen = alwaysOnScreen, | |
_overlayWidth = overlayWidth, | |
_alignOverlayWithButtonSize = alignOverlayWithButtonSize, | |
_followerAnchor = followerAnchor, | |
_targetKey = targetKey, | |
_overlayRect = overlayRect { | |
this.child = child; | |
} | |
/// The offset to apply to the origin of the linked [RenderLeaderLayer] to | |
/// obtain this render object's origin. | |
Offset get offset => _offset; | |
Offset _offset; | |
set offset(Offset value) { | |
if (_offset == value) { | |
return; | |
} | |
_offset = value; | |
markNeedsLayout(); | |
} | |
/// The connection between target an follower | |
LayerLink get link => _link; | |
LayerLink _link; | |
set link(LayerLink value) { | |
if (_link != value) { | |
_link = value; | |
markNeedsLayout(); | |
} | |
} | |
/// The anchor alignment of the target. | |
Alignment get leaderAnchor => _leaderAnchor; | |
Alignment _leaderAnchor; | |
set leaderAnchor(Alignment value) { | |
if (_leaderAnchor != value) { | |
_leaderAnchor = value; | |
markNeedsLayout(); | |
} | |
} | |
/// The anchor alignment of the follower. | |
Alignment get followerAnchor => _followerAnchor; | |
Alignment _followerAnchor; | |
set followerAnchor(Alignment value) { | |
if (_followerAnchor != value) { | |
_followerAnchor = value; | |
markNeedsLayout(); | |
} | |
} | |
/// If the overlay should always be within the Overlay | |
bool? get alwaysOnScreen => _alwaysOnScreen; | |
bool? _alwaysOnScreen; | |
set alwaysOnScreen(bool? value) { | |
if (_alwaysOnScreen != value) { | |
_alwaysOnScreen = value; | |
markNeedsLayout(); | |
} | |
} | |
/// If the overlay should have the same width as the trigger | |
bool? get alignOverlayWithButtonSize => _alignOverlayWithButtonSize; | |
bool? _alignOverlayWithButtonSize; | |
set alignOverlayWithButtonSize(bool? value) { | |
if (_alignOverlayWithButtonSize != value) { | |
_alignOverlayWithButtonSize = value; | |
markNeedsLayout(); | |
} | |
} | |
/// The exact width of the overlay | |
double? get overlayWidth => _overlayWidth; | |
double? _overlayWidth; | |
set overlayWidth(double? value) { | |
if (_overlayWidth != value) { | |
_overlayWidth = value; | |
markNeedsLayout(); | |
} | |
} | |
/// The globalKey of the target, allowing to read the size of the render object | |
GlobalKey get targetKey => _targetKey; | |
GlobalKey _targetKey; | |
set targetKey(GlobalKey value) { | |
if (_targetKey != value) { | |
_targetKey = value; | |
markNeedsLayout(); | |
} | |
} | |
/// The size and position of the [Overlay], hosting the follower | |
Rect get overlayRect => _overlayRect; | |
Rect _overlayRect; | |
set overlayRect(Rect value) { | |
if (_overlayRect != value) { | |
_overlayRect = value; | |
markNeedsLayout(); | |
} | |
} | |
@override | |
bool get alwaysNeedsCompositing => true; | |
@override | |
void setupParentData(covariant RenderObject child) { | |
if (child.parentData is! ParentData) { | |
child.parentData = BoxParentData(); | |
} | |
} | |
@override | |
void performLayout() { | |
if (_link.leader == null) { | |
// We don't know where to put the overlay on the screen. | |
return; | |
} | |
BoxConstraints childConstraints = constraints; | |
/// calculate sizes | |
size = childConstraints.biggest; | |
if (_alignOverlayWithButtonSize == true) { | |
childConstraints = childConstraints.loosen().copyWith( | |
maxWidth: _link.leaderSize?.width ?? constraints.biggest.width, | |
minWidth: _link.leaderSize?.width, | |
); | |
child!.layout(childConstraints, parentUsesSize: true); | |
} else if (_overlayWidth != null) { | |
childConstraints = childConstraints.copyWith(maxWidth: _overlayWidth, minWidth: 0).loosen(); | |
size = childConstraints.biggest; | |
child!.layout(childConstraints, parentUsesSize: true); | |
} else { | |
size = (child?..layout(constraints, parentUsesSize: true))?.size ?? computeSizeForNoChild(constraints); | |
} | |
} | |
@override | |
void paint(PaintingContext context, Offset offset) { | |
final Size? leaderSize = _link.leaderSize; | |
final useLeaderSize = leaderSize == null; | |
final anchorSize = _leaderAnchor.alongSize(leaderSize!); | |
final followerSize = _followerAnchor.alongSize(size); | |
final paintOffset = this.offset; | |
// Follower offset based on it's size and the anchor | |
final Offset effectiveLinkedOffset = useLeaderSize ? paintOffset : anchorSize - followerSize + paintOffset; | |
Offset onScreenOffset = Offset.zero; | |
if (_alwaysOnScreen == true) { | |
final box = _targetKey.currentContext?.findRenderObject() as RenderBox?; | |
final targetPosition = box!.localToGlobal(Offset.zero); | |
final Offset startOfLeader = targetPosition; | |
if (_followerAnchor == Alignment.topLeft || | |
_followerAnchor == Alignment.topCenter || | |
_followerAnchor == Alignment.topRight) { | |
// check if overlay overflows the bottom | |
final followerHeight = size.height; | |
final followerBottomEdge = startOfLeader.dy + followerHeight; | |
final overlayBottomEdge = overlayRect.bottom; | |
final bottomOverflow = followerBottomEdge - overlayBottomEdge; | |
if (bottomOverflow > 0) { | |
onScreenOffset = onScreenOffset.translate(0, -bottomOverflow); | |
} | |
} | |
if (_followerAnchor == Alignment.topLeft || | |
_followerAnchor == Alignment.centerLeft || | |
_followerAnchor == Alignment.bottomLeft) { | |
// check if overlay overflows the right | |
final followerWidth = size.width; | |
final followerRightEdge = startOfLeader.dx + followerWidth; | |
final rightOverflow = followerRightEdge - overlayRect.right; | |
if (rightOverflow > 0) { | |
onScreenOffset = onScreenOffset.translate(-rightOverflow, 0); | |
} | |
} | |
if (_followerAnchor == Alignment.bottomRight || | |
_followerAnchor == Alignment.bottomCenter || | |
_followerAnchor == Alignment.bottomLeft) { | |
// check if overlay overflows the top | |
final followerHeight = size.height; | |
final followerTopEdge = startOfLeader.dy - followerHeight; | |
final topOverflow = followerTopEdge - overlayRect.top; | |
if (topOverflow < 0) { | |
onScreenOffset = onScreenOffset.translate(0, -topOverflow); | |
} | |
} | |
if (_followerAnchor == Alignment.topRight || | |
_followerAnchor == Alignment.centerRight || | |
_followerAnchor == Alignment.bottomRight) { | |
// check if overlay overflows the left | |
final followerWidth = size.width; | |
final followerLeftEdge = startOfLeader.dx - followerWidth; | |
final leftOverflow = followerLeftEdge - overlayRect.left; | |
if (leftOverflow < 0) { | |
onScreenOffset = onScreenOffset.translate(-leftOverflow, 0); | |
} | |
} | |
} | |
layer = FollowerLayer( | |
link: _link, | |
showWhenUnlinked: false, | |
linkedOffset: effectiveLinkedOffset + onScreenOffset, | |
unlinkedOffset: offset, | |
); | |
context.pushLayer( | |
layer!, | |
super.paint, | |
Offset.zero, | |
childPaintBounds: const Rect.fromLTRB( | |
// We don't know where we'll end up, so we have no idea what our cull rect should be. | |
double.negativeInfinity, | |
double.negativeInfinity, | |
double.infinity, | |
double.infinity, | |
), | |
); | |
} | |
/// The layer we created when we were last painted. | |
@override | |
FollowerLayer? get layer => super.layer as FollowerLayer?; | |
/// Return the transform that was used in the last composition phase, if any. | |
/// | |
/// If the [FollowerLayer] has not yet been created, was never composited, or | |
/// was unable to determine the transform (see | |
/// [FollowerLayer.getLastTransform]), this returns the identity matrix (see | |
/// [Matrix4.identity]. | |
Matrix4 getCurrentTransform() { | |
return layer?.getLastTransform() ?? Matrix4.identity(); | |
} | |
@override | |
bool hitTest(BoxHitTestResult result, {required Offset position}) { | |
// Disables the hit testing if this render object is hidden. | |
// if (_link.leader == null && !showWhenUnlinked) { | |
if (_link.leader == null) { | |
return false; | |
} | |
// RenderFollowerLayer objects don't check if they are | |
// themselves hit, because it's confusing to think about | |
// how the untransformed size and the child's transformed | |
// position interact. | |
return hitTestChildren(result, position: position); | |
} | |
@override | |
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { | |
return result.addWithPaintTransform( | |
transform: getCurrentTransform(), | |
position: position, | |
hitTest: (BoxHitTestResult result, Offset position) { | |
return super.hitTestChildren(result, position: position); | |
}, | |
); | |
} | |
@override | |
void applyPaintTransform(RenderBox child, Matrix4 transform) { | |
transform.multiply(getCurrentTransform()); | |
} | |
} | |
class _OverlayRevealAnimation extends StatefulWidget { | |
const _OverlayRevealAnimation({ | |
required this.child, | |
required this.show, | |
required this.controller, | |
required this.animationOffset, | |
}); | |
final Widget child; | |
final bool show; | |
final AnimationController controller; | |
final Offset animationOffset; | |
@override | |
State<_OverlayRevealAnimation> createState() => _OverlayRevealAnimationState(); | |
} | |
class _OverlayRevealAnimationState extends State<_OverlayRevealAnimation> { | |
@override | |
Widget build(BuildContext context) { | |
return MatrixTransition( | |
onTransform: (value) { | |
final offset = widget.animationOffset * (1 - widget.controller.value); | |
return Matrix4.identity()..translate(offset.dx, offset.dy); | |
}, | |
animation: widget.controller, | |
child: FadeTransition( | |
opacity: Tween<double>(begin: 0.1, end: 1).animate( | |
CurvedAnimation(parent: widget.controller, curve: Curves.easeOutCubic, reverseCurve: Curves.easeOutCubic), | |
), | |
child: widget.child, | |
), | |
); | |
} | |
} | |
/// A controller for the [AdvancedOverlay]. Basically a wrapper around the [OverlayPortalController] but with a state. | |
class AdvancedOverlayController extends ChangeNotifier { | |
final _portalController = OverlayPortalController(); | |
FutureOr<void> Function()? _onClose; | |
FutureOr<void> Function()? _onOpen; | |
/// If the overlay is showing. See [OverlayPortalController.isShowing] | |
bool get isShowing => _portalController.isShowing; | |
/// see [OverlayPortalController.show] | |
Future<void> show() async { | |
_portalController.show(); | |
await _onOpen?.call(); | |
notifyListeners(); | |
} | |
/// see [OverlayPortalController.hide] | |
Future<void> hide() async { | |
_portalController.hide(); | |
await _onClose?.call(); | |
notifyListeners(); | |
} | |
/// see [OverlayPortalController.toggle] | |
Future<void> toggle() async { | |
if (isShowing) { | |
await hide(); | |
} else { | |
await show(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment