You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

356 lines
11 KiB

3 years ago
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'bubble.dart';
const Duration _kMenuDuration = Duration(milliseconds: 300);
const Color _kMenuBackgroundColor = Color(0xFF2E2E2E);
const Color _kMenuBackgroundLightColor = Color(0xD1F8F8F8);
const EdgeInsets _kMenuButtonPadding =
EdgeInsets.symmetric(vertical: 12.0, horizontal: 18.0);
const double _kMenuScreenPadding = 8.0;
const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep;
const double _kMenuMinWidth = 2.0 * _kMenuWidthStep;
const double _kMenuWidthStep = 56.0;
const double _kMenuCloseIntervalEnd = 2.0 / 3.0;
const double _kMenuHeight = 36.0;
const double _kMenuButtonMinHeight = 22;
const TextStyle _kToolbarButtonFontStyle = TextStyle(
inherit: false,
fontSize: 14.0,
letterSpacing: -0.11,
fontWeight: FontWeight.w300,
color: CupertinoColors.white,
);
typedef FLBubbleMenuItemBuilder<T> = List<FLBubbleMenuItem<T>>? Function(
BuildContext context);
typedef FLBubbleMenuCancelled = void Function();
typedef FLBubbleMenuItemSelected<T> = void Function(T value);
enum FLBubbleMenuInteraction { tap, longPress }
class FLBubbleMenuWidget<T> extends StatefulWidget {
FLBubbleMenuWidget(
{Key? key,
@required this.itemBuilder,
this.onSelected,
this.onCancelled,
this.interaction = FLBubbleMenuInteraction.longPress,
@required this.child,
this.offset = Offset.zero})
: assert(itemBuilder != null),
assert(child != null),
assert(offset != null),
super(key: key);
final FLBubbleMenuInteraction? interaction;
final FLBubbleMenuItemBuilder<T>? itemBuilder;
final FLBubbleMenuItemSelected<T>? onSelected;
final FLBubbleMenuCancelled? onCancelled;
final Widget? child;
final Offset? offset;
@override
_FLBubbleMenuWidgetState<T> createState() => _FLBubbleMenuWidgetState();
}
class _FLBubbleMenuWidgetState<T> extends State<FLBubbleMenuWidget<T>> {
void showButtonMenu() {
final RenderBox? button = context.findRenderObject() as RenderBox?;
final RenderBox? overlay =
Overlay.of(context)?.context.findRenderObject() as RenderBox?;
final RelativeRect position = RelativeRect.fromRect(
Rect.fromPoints(
button!.localToGlobal(widget.offset!, ancestor: overlay),
button.localToGlobal(button.size.bottomRight(Offset.zero),
ancestor: overlay),
),
Offset.zero & overlay!.size);
showBubbleMenu<T>(
context: context,
position: position,
items: widget.itemBuilder!(context))
.then<void>((T? value) {
if (!mounted) return null;
if (value == null) {
if (widget.onCancelled != null) widget.onCancelled!();
return null;
}
if (widget.onSelected != null) widget.onSelected!(value);
});
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: (widget.interaction == FLBubbleMenuInteraction.tap)
? showButtonMenu
: null,
onLongPress: (widget.interaction == FLBubbleMenuInteraction.longPress)
? showButtonMenu
: null,
child: widget.child);
}
}
Future<T?> showBubbleMenu<T>({
@required BuildContext? context,
@required RelativeRect? position,
@required List<FLBubbleMenuItem<T>>? items,
String? semanticLabel,
}) {
assert(context != null);
assert(position != null);
assert(items != null && items.isNotEmpty);
assert(debugCheckHasMaterialLocalizations(context!));
String label = semanticLabel!;
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
label = semanticLabel;
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
label =
semanticLabel; // ?? MaterialLocalizations.of(context!).popupMenuLabel;
}
return Navigator.push(
context!,
_FLBubblePopupRoute<T>(
position: position!,
items: items!,
semanticLabel: label,
barrierLabel:
MaterialLocalizations.of(context).modalBarrierDismissLabel));
}
class FLBubbleMenuItem<T> {
FLBubbleMenuItem({@required this.text, @required this.value});
final String? text;
final T? value;
}
class _FLBubbleMenu<T> extends StatelessWidget {
_FLBubbleMenu(
{Key? key,
this.route,
this.semanticLabel,
this.from = FLBubbleFrom.bottom,
@required this.items})
: super(key: key);
final _FLBubblePopupRoute? route;
final String? semanticLabel;
final FLBubbleFrom? from;
final List<FLBubbleMenuItem>? items;
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final List<Widget> children = <Widget>[];
for (int i = 0; i < route!.items!.length; i += 1) {
final CurvedAnimation opacity = CurvedAnimation(
parent: route!.animation!, curve: Interval(0.0, 1.0 / 3.0));
FLBubbleMenuItem item = route!.items![i];
Widget itemWidget = _buildMenuButton(context, item, isDarkMode);
children.add(_transitionWrapper(itemWidget, opacity));
if (i != route!.items!.length - 1) {
children.add(_transitionWrapper(_divider(isDarkMode), opacity));
}
}
final CurveTween opacity =
CurveTween(curve: const Interval(0.0, 1.0 / 3.0));
final Color backgroundColor =
isDarkMode ? _kMenuBackgroundLightColor : _kMenuBackgroundColor;
final Widget child = ConstrainedBox(
constraints: const BoxConstraints(
minWidth: _kMenuMinWidth,
maxWidth: _kMenuMaxWidth,
),
child: IntrinsicWidth(
stepWidth: _kMenuWidthStep,
child: Semantics(
scopesRoute: true,
namesRoute: true,
explicitChildNodes: true,
label: semanticLabel,
child: FLBubble(
from: from!,
padding: EdgeInsets.zero,
backgroundColor: backgroundColor,
child: Row(
mainAxisSize: MainAxisSize.min,
children: children,
)),
),
),
);
return AnimatedBuilder(
animation: route!.animation!,
builder: (BuildContext? context, Widget? child) {
return Opacity(
opacity: opacity.evaluate(route!.animation!),
child: Material(
color: Colors.transparent,
child: child,
),
);
},
child: child,
);
}
CupertinoButton _buildMenuButton(
BuildContext context, FLBubbleMenuItem menuItem, bool isDarkMode) {
TextStyle textStyle = isDarkMode
? _kToolbarButtonFontStyle.copyWith(color: Colors.black)
: _kToolbarButtonFontStyle;
return CupertinoButton(
child: Text(menuItem.text!, style: textStyle),
minSize: _kMenuButtonMinHeight,
padding: _kMenuButtonPadding,
borderRadius: null,
pressedOpacity: 0.7,
onPressed: () {
Navigator.pop(context, menuItem.value);
},
);
}
Widget _divider(bool isDarkMode) {
Color dividerColor = isDarkMode ? Colors.blueGrey : Colors.white;
return Container(width: 1 / 2.0, height: _kMenuHeight, color: dividerColor);
}
Widget _transitionWrapper(Widget child, CurvedAnimation opacity) {
return FadeTransition(opacity: opacity, child: child);
}
}
// Positioning of the menu
class _FLBubbleMenuRouteLayoutDelegate extends SingleChildLayoutDelegate {
_FLBubbleMenuRouteLayoutDelegate({this.position, this.from});
final RelativeRect? position;
final FLBubbleFrom? from; // only support top/bottom
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
// return BoxConstraints.loose(constraints.biggest - Size(_kMenuScreenPadding * 2.0, _kMenuScreenPadding * 2.0));
Size size = Size(constraints.biggest.width - _kMenuScreenPadding * 2.0,
constraints.biggest.width - _kMenuScreenPadding * 2.0);
return BoxConstraints.loose(size);
}
@override
Offset getPositionForChild(Size size, Size childSize) {
// vertical position
double y = (from == FLBubbleFrom.bottom)
? position!.top - childSize.height
: size.height - position!.bottom;
// horizontal position
double pW = size.width - position!.right - position!.left;
double x = position!.left + (pW - childSize.width) / 2;
// check horizontal edge
if (x < _kMenuScreenPadding)
x = _kMenuScreenPadding;
else if (x + childSize.width > size.width - _kMenuScreenPadding)
x = size.width - childSize.width - _kMenuScreenPadding;
// vertical
if (y < _kMenuScreenPadding)
y = _kMenuScreenPadding;
else if (y + childSize.height > size.height - _kMenuScreenPadding)
y = size.height - childSize.height - _kMenuScreenPadding;
return Offset(x, y);
}
@override
bool shouldRelayout(_FLBubbleMenuRouteLayoutDelegate oldDelegate) {
return position != oldDelegate.position;
}
}
class _FLBubblePopupRoute<T> extends PopupRoute<T> {
_FLBubblePopupRoute({
this.position,
this.items,
this.barrierLabel,
this.semanticLabel,
});
final RelativeRect? position;
final List<FLBubbleMenuItem<T>>? items;
final String? semanticLabel;
@override
Animation<double> createAnimation() {
return CurvedAnimation(
parent: super.createAnimation(),
curve: Curves.linear,
reverseCurve: const Interval(0.0, _kMenuCloseIntervalEnd));
}
@override
Duration get transitionDuration => _kMenuDuration;
@override
bool get barrierDismissible => true;
@override
Color? get barrierColor => null;
@override
final String? barrierLabel;
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
// triangle direction
FLBubbleFrom from = _determineBubbleFrom(position);
// retrieve menu
Widget menu = _FLBubbleMenu<T>(
route: this,
semanticLabel: semanticLabel,
from: from,
items: items,
);
return MediaQuery.removePadding(
context: context,
removeTop: true,
removeLeft: true,
removeBottom: true,
removeRight: true,
child: Builder(
builder: (BuildContext context) {
return CustomSingleChildLayout(
delegate: _FLBubbleMenuRouteLayoutDelegate(
position: position!, from: from),
child: menu,
);
},
));
}
FLBubbleFrom _determineBubbleFrom(RelativeRect? position) {
return (position!.top >
(35 // estimated value
+
kToolbarHeight +
_kMenuHeight +
_kMenuScreenPadding))
? FLBubbleFrom.bottom
: FLBubbleFrom.top;
}
}