showFlyout<T> method
- required WidgetBuilder builder,
- bool barrierDismissible = true,
- bool dismissWithEsc = true,
- bool dismissOnPointerMoveAway = false,
- FlyoutPlacementMode placementMode = FlyoutPlacementMode.auto,
- FlyoutAutoConfiguration? autoModeConfiguration,
- bool forceAvailableSpace = false,
- bool shouldConstrainToRootBounds = true,
- double additionalOffset = 8.0,
- double margin = 8.0,
- Color? barrierColor,
- FlyoutTransitionBuilder? transitionBuilder,
- Duration? transitionDuration,
- Offset? position,
- RouteSettings? settings,
- GestureRecognizer? barrierRecognizer,
Shows a flyout.
builder
builds the flyout with the given context. Usually a FlyoutContent
is used
If barrierDismissible
is true, tapping outside of the flyout will close
it.
barrierColor
is the color of the barrier.
When dismissWithEsc
is true, the flyout can be dismissed by pressing the
ESC key.
If dismissOnPointerMoveAway
is enabled, the flyout is dismissed when the
cursor moves away from either the target or the flyout. It's disabled by
default.
placementMode
describes where the flyout will be placed. Defaults to auto
If placementMode
is auto, autoModeConfiguration
is taken in consideration
to determine the correct placement mode
forceAvailableSpace
determines whether the flyout size should be forced
the available space according to the attached target. It's useful when the
flyout is large but can not be on top of the target. Defaults to false
shouldConstrainToRootBounds
, when true, the flyout is limited to the
bounds of the closest Navigator. If false, the flyout may overflow the
screen on all sides. Defaults to true
additionalOffset
is the offset of the flyout around the attached target
margin
is the margin of the flyout to the root bounds
If there isn't a Navigator in the tree, a navigatorKey
can be used to
display the flyout. If null, Navigator.of is used.
transitionBuilder
builds the transition. By default, a slide-fade transition
is used on vertical directions; and a fade transition in horizontal directions.
The default fade animation can not be disabled.
transitionDuration
configures the duration of the transition animation.
By default, FluentThemeData.fastAnimationDuration is used. Set to Duration.zero
to disable transitions at all
position
lets you position the flyout anywhere on the screen, making it
possible to create context menus. If provided, placementMode
is ignored.
barrierRecognizer
is a gesture recognizer that will be added to the
barrier. It's useful when the flyout is used as a context menu and the
barrier should be dismissed when the user clicks outside of the flyout.
If this is provided, barrierDismissible
is ignored.
Implementation
Future<T?> showFlyout<T>({
required WidgetBuilder builder,
bool barrierDismissible = true,
bool dismissWithEsc = true,
bool dismissOnPointerMoveAway = false,
FlyoutPlacementMode placementMode = FlyoutPlacementMode.auto,
FlyoutAutoConfiguration? autoModeConfiguration,
bool forceAvailableSpace = false,
bool shouldConstrainToRootBounds = true,
double additionalOffset = 8.0,
double margin = 8.0,
Color? barrierColor,
NavigatorState? navigatorKey,
FlyoutTransitionBuilder? transitionBuilder,
Duration? transitionDuration,
Offset? position,
RouteSettings? settings,
GestureRecognizer? barrierRecognizer,
}) async {
_ensureAttached();
assert(_attachState!.mounted);
final context = _attachState!.context;
assert(debugCheckHasFluentTheme(context));
final theme = FluentTheme.of(context);
transitionDuration ??= theme.fastAnimationDuration;
final navigator = navigatorKey ?? Navigator.of(context);
final Offset targetOffset;
final Size targetSize;
final Rect targetRect;
if (position != null) {
targetOffset = position;
targetSize = Size.zero;
targetRect = Rect.zero;
} else {
final navigatorBox = navigator.context.findRenderObject() as RenderBox;
final targetBox = context.findRenderObject() as RenderBox;
targetSize = targetBox.size;
targetOffset = targetBox.localToGlobal(
Offset.zero,
ancestor: navigatorBox,
) +
Offset(0, targetSize.height);
targetRect = targetBox.localToGlobal(
Offset.zero,
ancestor: navigatorBox,
) &
targetSize;
}
_open = true;
notifyListeners();
final flyoutKey = GlobalKey();
final result = await navigator.push<T>(PageRouteBuilder<T>(
opaque: false,
transitionDuration: transitionDuration,
reverseTransitionDuration: transitionDuration,
settings: settings,
fullscreenDialog: true,
pageBuilder: (context, animation, secondary) {
transitionBuilder ??= (context, animation, placementMode, flyout) {
switch (placementMode) {
case FlyoutPlacementMode.bottomCenter:
case FlyoutPlacementMode.bottomLeft:
case FlyoutPlacementMode.bottomRight:
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, -0.05),
end: const Offset(0, 0),
).animate(animation),
child: flyout,
);
case FlyoutPlacementMode.topCenter:
case FlyoutPlacementMode.topLeft:
case FlyoutPlacementMode.topRight:
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.05),
end: const Offset(0, 0),
).animate(animation),
child: flyout,
);
default:
return flyout;
}
};
return MenuInfoProvider(
builder: (context, rootSize, menus, keys) {
assert(menus.length == keys.length);
final barrier = ColoredBox(
color: barrierColor ?? Colors.black.withOpacity(0.3),
);
Widget box = Stack(children: [
if (barrierRecognizer != null)
Positioned.fill(
child: Listener(
behavior: HitTestBehavior.opaque,
onPointerDown: (event) {
barrierRecognizer.addPointer(event);
},
child: barrier,
),
)
else if (barrierDismissible)
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: barrierDismissible ? navigator.pop : null,
child: barrier,
),
),
Positioned.fill(
child: SafeArea(
child: CustomSingleChildLayout(
delegate: _FlyoutPositionDelegate(
targetOffset: targetOffset,
targetSize: position == null ? targetSize : Size.zero,
autoModeConfiguration: autoModeConfiguration,
placementMode: placementMode,
defaultPreferred: position == null
? FlyoutPlacementMode.topCenter
: FlyoutPlacementMode.bottomLeft,
margin: margin,
shouldConstrainToRootBounds: shouldConstrainToRootBounds,
forceAvailableSpace: forceAvailableSpace,
),
child: Flyout(
rootFlyout: flyoutKey,
additionalOffset: additionalOffset,
margin: margin,
transitionDuration: transitionDuration!,
root: navigator,
builder: (context) {
final parentBox =
context.findAncestorRenderObjectOfType<
RenderCustomSingleChildLayoutBox>()!;
final delegate =
parentBox.delegate as _FlyoutPositionDelegate;
final realPlacementMode = delegate.autoPlacementMode ??
delegate.placementMode;
final flyout = Padding(
key: flyoutKey,
padding:
realPlacementMode._getAdditionalOffsetPosition(
position == null ? additionalOffset : 0.0,
),
child: builder(context),
);
return transitionBuilder!(
context,
animation,
realPlacementMode,
flyout,
);
},
),
),
),
),
...menus,
]);
if (dismissOnPointerMoveAway) {
box = MouseRegion(
onHover: (hover) {
if (flyoutKey.currentContext == null) return;
final navigatorBox =
navigator.context.findRenderObject() as RenderBox;
// the flyout box needs to be fetched at each [onHover] because the
// flyout size may change (a MenuFlyout, for example)
final flyoutBox =
flyoutKey.currentContext!.findRenderObject() as RenderBox;
final flyoutRect = flyoutBox.localToGlobal(
Offset.zero,
ancestor: navigatorBox,
) &
flyoutBox.size;
final menusRects = keys.map((key) {
if (key.currentContext == null) return Rect.zero;
final menuBox =
key.currentContext!.findRenderObject() as RenderBox;
return menuBox.localToGlobal(
Offset.zero,
ancestor: navigatorBox,
) &
menuBox.size;
});
if (!flyoutRect.contains(hover.position) &&
!targetRect.contains(hover.position) &&
!menusRects
.any((rect) => rect.contains(hover.position))) {
navigator.pop();
}
},
child: box,
);
}
if (dismissWithEsc) {
box = Actions(
actions: {DismissIntent: _DismissAction(navigator.pop)},
child: FocusScope(
autofocus: true,
child: box,
),
);
}
return FadeTransition(
opacity: CurvedAnimation(
curve: Curves.ease,
parent: animation,
),
child: box,
);
},
);
},
));
_open = false;
notifyListeners();
return result;
}