import 'package:flutter/material.dart';
const int _windowPopupDuration = 200;
const Duration _kWindowDuration = Duration(milliseconds: _windowPopupDuration);
class PopupWindow {
static show(BuildContext target, Widget window,
{double elevation = 20,
int? duration,
PopupWindowAlign? alignment,
Offset offset = Offset.zero,
Function(Object? result)? onResult,
bool barrierDismissible = true,
Color? barrierColor
}) {
final RenderBox? targetRender = target.findRenderObject() as RenderBox?;
final RenderBox? overlay =
Overlay.of(target)?.context.findRenderObject() as RenderBox?;
if (targetRender != null && overlay != null) {
final RelativeRect position = RelativeRect.fromRect(
Rect.fromPoints(
targetRender.localToGlobal(Offset.zero, ancestor: overlay),
targetRender.localToGlobal(targetRender.size.bottomRight(Offset.zero),
ancestor: overlay),
),
Offset.zero & overlay.size,
);
_showWindow(
position,
target,
window,
alignment: alignment,
offset: offset,
duration: duration,
elevation: elevation,
onResult: onResult,
barrierDismissible: barrierDismissible,
barrierColor: barrierColor,
);
}
}
static showBottom(
BuildContext context,
Widget window, {
double? windowHeight,
Color barrierColor = Colors.black54,
bool barrierDismissible = true,
Function(Object? result)? onResult,
}) async {
Object? result = await Navigator.of(context).push(_BottomPopupWindowRoute(
context,
window,
windowHeight,
barrierColor,
barrierDismissible));
if (onResult != null) {
onResult(result);
}
}
static showPopupWindowLeft(
BuildContext context,
WidgetBuilder windowBuilder, {
double? windowWidth,
Color barrierColor = Colors.black54,
bool barrierDismissible = true,
Function(Object? result)? onResult,
}) async {
Object? result = await Navigator.of(context).push(_LeftPopupWindowRoute(
context,
windowBuilder,
windowWidth,
barrierColor,
barrierDismissible));
if (onResult != null) {
onResult(result);
}
}
static _showWindow(RelativeRect position, BuildContext context, Widget window,
{double elevation = 10,
int? duration,
PopupWindowAlign? alignment,
Offset offset = Offset.zero,
bool barrierDismissible = false,
Function(Object? result)? onResult,
Color? barrierColor}) async {
Object? result = await Navigator.of(context).push(_PopupWindowRoute(
position,
window,
elevation,
MaterialLocalizations.of(context).modalBarrierDismissLabel,
duration,
alignment,
offset,
barrierDismissible,
barrierColor));
if (onResult != null) {
onResult(result);
}
}
}
class _PopupWindowRoute<T> extends PopupRoute<T> {
final RelativeRect position;
final PopupWindowAlign? alignment;
final Widget child;
final double elevation;
final int? duration;
final Offset offset;
final bool _barrierDismissible;
final Color? _barrierColor;
@override
final String barrierLabel;
_PopupWindowRoute(
this.position,
this.child,
this.elevation,
this.barrierLabel,
this.duration,
this.alignment,
this.offset,
this._barrierDismissible,
this._barrierColor);
@override
Color? get barrierColor => _barrierColor;
@override
bool get barrierDismissible => _barrierDismissible;
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
final CurveTween opacity =
CurveTween(curve: const Interval(0.0, 1.0 / 3.0));
return CustomSingleChildLayout(
delegate: _PopupMenuLayout(position, alignment, offset),
child: AnimatedBuilder(
animation: animation,
child: child,
builder: (BuildContext context, Widget? child) {
return Opacity(
opacity: opacity.evaluate(animation),
child: Material(
elevation: elevation,
color: Colors.transparent,
child: child,
),
);
}),
);
}
@override
Duration get transitionDuration => duration == null || duration == 0
? _kWindowDuration
: Duration(milliseconds: duration!);
}
class _PopupMenuLayout extends SingleChildLayoutDelegate {
final RelativeRect position;
final PopupWindowAlign? align;
final Offset offset;
_PopupMenuLayout(this.position, this.align, this.offset);
@override
bool shouldRelayout(_PopupMenuLayout oldDelegate) {
return position != oldDelegate.position;
}
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return BoxConstraints.loose(constraints.biggest);
}
@override
Offset getPositionForChild(Size size, Size childSize) {
double x = 0, y = 0;
if (align == null) {
x = position.left;
y = size.height - position.bottom;
} else {
switch (align) {
case PopupWindowAlign.centerRight:
x = size.width - position.right;
y = (position.top + size.height - position.bottom) / 2 -
childSize.height / 2;
break;
case PopupWindowAlign.topCenter:
x = (position.left + size.width - position.right) / 2 -
childSize.width / 2;
y = position.top - childSize.height;
break;
case PopupWindowAlign.centerLeft:
x = position.left - childSize.width;
y = (position.top + size.height - position.bottom) / 2 -
childSize.height / 2;
break;
case PopupWindowAlign.bottomCenter:
x = (position.left + size.width - position.right) / 2 -
childSize.width / 2;
y = size.height - position.bottom;
break;
}
}
x += offset.dx;
y += offset.dy;
if (x + childSize.width > size.width) {
x = size.width - childSize.width;
}
if (y + childSize.height > size.height) {
y = size.height - childSize.height;
}
return Offset(x, y);
}
}
class PopupWindowAlign {
final int type;
const PopupWindowAlign(this.type);
static const PopupWindowAlign centerRight = const PopupWindowAlign(0);
static const PopupWindowAlign topCenter = const PopupWindowAlign(1);
static const PopupWindowAlign centerLeft = const PopupWindowAlign(2);
static const PopupWindowAlign bottomCenter = const PopupWindowAlign(3);
}
class _BottomPopupWindowRoute<T> extends PopupRoute<T> {
final BuildContext context;
final Widget window;
final double? windowHeight;
final Color _barrierColor;
final bool _barrierDismissible;
_BottomPopupWindowRoute(this.context, this.window, this.windowHeight,
this._barrierColor, this._barrierDismissible);
@override
Duration get transitionDuration => const Duration(milliseconds: 200);
@override
bool get barrierDismissible => _barrierDismissible;
@override
Color get barrierColor => _barrierColor;
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
Widget bottomWindow = new MediaQuery.removePadding(
context: context,
removeTop: true,
child: AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) {
return ClipRect(
child: CustomSingleChildLayout(
delegate: _BottomPopupWindowLayout(animation.value,
contentHeight: windowHeight),
child: window,
),
);
},
),
);
return bottomWindow;
}
@override
String get barrierLabel =>
MaterialLocalizations.of(context).modalBarrierDismissLabel;
}
class _LeftPopupWindowRoute<T> extends PopupRoute<T> {
final BuildContext context;
final WidgetBuilder windowBuilder;
final double? windowWidth;
final Color _barrierColor;
final bool _barrierDismissible;
_LeftPopupWindowRoute(this.context, this.windowBuilder, this.windowWidth,
this._barrierColor, this._barrierDismissible);
@override
Duration get transitionDuration => const Duration(milliseconds: 200);
@override
bool get barrierDismissible => _barrierDismissible;
@override
Color get barrierColor => _barrierColor;
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
Widget bottomWindow = new MediaQuery.removePadding(
context: context,
removeTop: true,
child: AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) {
return ClipRect(
child: CustomSingleChildLayout(
delegate: _LeftPopupWindowLayout(animation.value,
contentWidth: windowWidth),
child: windowBuilder(context),
),
);
},
),
);
return bottomWindow;
}
@override
String get barrierLabel =>
MaterialLocalizations.of(context).modalBarrierDismissLabel;
}
abstract class _PopupWindowLayout extends SingleChildLayoutDelegate {
final double progress;
_PopupWindowLayout(this.progress);
@override
bool shouldRelayout(_PopupWindowLayout oldDelegate) {
return progress != oldDelegate.progress;
}
}
class _BottomPopupWindowLayout extends _PopupWindowLayout {
_BottomPopupWindowLayout(double progress, {this.contentHeight})
: super(progress);
final double? contentHeight;
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return new BoxConstraints(
minWidth: constraints.maxWidth,
maxWidth: constraints.maxWidth,
minHeight: 0.0,
maxHeight: contentHeight ?? constraints.maxHeight,
);
}
@override
Offset getPositionForChild(Size size, Size childSize) {
double height = size.height - childSize.height * progress;
return new Offset(0.0, height);
}
}
class _LeftPopupWindowLayout extends _PopupWindowLayout {
_LeftPopupWindowLayout(double progress, {this.contentWidth})
: super(progress);
final double? contentWidth;
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return new BoxConstraints(
minWidth: 0,
maxWidth: contentWidth ?? constraints.maxWidth,
minHeight: constraints.maxHeight,
maxHeight: constraints.maxHeight,
);
}
@override
Offset getPositionForChild(Size size, Size childSize) {
double width = childSize.width * (progress - 1);
return new Offset(width, 0.0);
}
}