mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
[infra_ui][overlay] Impl overlay manager based on stack
This commit is contained in:
parent
e7a6a41437
commit
9508ad84e4
@ -1,4 +1,7 @@
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'home/home_screen.dart';
|
||||
|
||||
void main() {
|
||||
@ -10,9 +13,10 @@ class ExampleApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
return MaterialApp(
|
||||
builder: overlayManagerBuilder(),
|
||||
title: "Flowy Infra Title",
|
||||
home: HomeScreen(),
|
||||
home: const HomeScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../home/demo_item.dart';
|
||||
@ -42,7 +43,14 @@ class OverlayScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
FlowyOverlay.of(context).insert(
|
||||
const FlutterLogo(
|
||||
size: 200,
|
||||
),
|
||||
'overlay_flutter_logo',
|
||||
);
|
||||
},
|
||||
child: const Text('Show Overlay'),
|
||||
),
|
||||
],
|
||||
|
@ -3,3 +3,6 @@ export 'basis.dart';
|
||||
|
||||
// Keyboard
|
||||
export 'src/keyboard/keyboard_visibility_detector.dart';
|
||||
|
||||
// Overlay
|
||||
export 'src/flowy_overlay/flowy_overlay.dart';
|
||||
|
@ -3,3 +3,6 @@ export 'basis.dart';
|
||||
|
||||
// Keyboard
|
||||
export 'src/keyboard/keyboard_visibility_detector.dart';
|
||||
|
||||
// Overlay
|
||||
export 'src/flowy_overlay/flowy_overlay.dart';
|
||||
|
@ -0,0 +1,157 @@
|
||||
import 'package:dartz/dartz.dart' show Tuple2;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
/// Specifies how overlay are anchored to the SourceWidget
|
||||
enum AnchorDirection {
|
||||
// Corner aligned with a corner of the SourceWidget
|
||||
topLeft,
|
||||
topRight,
|
||||
bottomLeft,
|
||||
bottomRight,
|
||||
|
||||
// Edge aligned with a edge of the SourceWidget
|
||||
topWithLeftAligned,
|
||||
topWithCenterAligned,
|
||||
topWithRightAligned,
|
||||
rightWithTopAligned,
|
||||
rightWithCenterAligned,
|
||||
rightWithBottomAligned,
|
||||
bottomWithLeftAligned,
|
||||
bottomWithCenterAligned,
|
||||
bottomWithRightAligned,
|
||||
leftWithTopAligned,
|
||||
leftWithCenterAligned,
|
||||
leftWithBottomAligned,
|
||||
|
||||
// Custom position
|
||||
custom,
|
||||
}
|
||||
|
||||
/// The behavior of overlay when user tapping system back button
|
||||
enum OnBackBehavior {
|
||||
/// Won't handle the back action
|
||||
none,
|
||||
|
||||
/// Animate to get the user's attention
|
||||
alert,
|
||||
|
||||
/// Intercept the back action and abort directly
|
||||
abort,
|
||||
|
||||
/// Intercept the back action and dismiss overlay
|
||||
dismiss,
|
||||
}
|
||||
|
||||
final GlobalKey<FlowyOverlayState> _key = GlobalKey<FlowyOverlayState>();
|
||||
|
||||
/// Invoke this method in app generation process
|
||||
TransitionBuilder overlayManagerBuilder() {
|
||||
return (context, child) {
|
||||
assert(child != null, 'Child can\'t be null.');
|
||||
return FlowyOverlay(key: _key, child: child!);
|
||||
};
|
||||
}
|
||||
|
||||
class FlowyOverlay extends StatefulWidget {
|
||||
const FlowyOverlay({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.barrierColor = Colors.transparent,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget child;
|
||||
|
||||
final Color? barrierColor;
|
||||
|
||||
static FlowyOverlayState of(
|
||||
BuildContext context, {
|
||||
bool rootOverlay = false,
|
||||
}) {
|
||||
FlowyOverlayState? overlayManager;
|
||||
if (rootOverlay) {
|
||||
overlayManager = context.findRootAncestorStateOfType<FlowyOverlayState>() ?? overlayManager;
|
||||
} else {
|
||||
overlayManager = overlayManager ?? context.findAncestorStateOfType<FlowyOverlayState>();
|
||||
}
|
||||
|
||||
assert(() {
|
||||
if (overlayManager == null) {
|
||||
throw FlutterError(
|
||||
'Can\'t find overlay manager in current context, please check if already wrapped by overlay manager.',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
return overlayManager!;
|
||||
}
|
||||
|
||||
static FlowyOverlayState? maybeOf(
|
||||
BuildContext context, {
|
||||
bool rootOverlay = false,
|
||||
}) {
|
||||
FlowyOverlayState? overlayManager;
|
||||
if (rootOverlay) {
|
||||
overlayManager = context.findRootAncestorStateOfType<FlowyOverlayState>() ?? overlayManager;
|
||||
} else {
|
||||
overlayManager = overlayManager ?? context.findAncestorStateOfType<FlowyOverlayState>();
|
||||
}
|
||||
|
||||
return overlayManager;
|
||||
}
|
||||
|
||||
@override
|
||||
FlowyOverlayState createState() => FlowyOverlayState();
|
||||
}
|
||||
|
||||
class FlowyOverlayState extends State<FlowyOverlay> {
|
||||
List<Tuple2<Widget, String>> _overlayList = [];
|
||||
|
||||
void insert(Widget widget, String identifier) {
|
||||
setState(() {
|
||||
_overlayList.add(Tuple2(widget, identifier));
|
||||
});
|
||||
}
|
||||
|
||||
void remove(String identifier) {
|
||||
setState(() {
|
||||
_overlayList.removeWhere((ele) => ele.value2 == identifier);
|
||||
});
|
||||
}
|
||||
|
||||
void removeAll() {
|
||||
setState(() {
|
||||
_overlayList = [];
|
||||
});
|
||||
}
|
||||
|
||||
void _markDirty() {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final overlays = _overlayList.map((ele) => ele.value1);
|
||||
final children = <Widget>[
|
||||
widget.child,
|
||||
if (overlays.isNotEmpty)
|
||||
Container(
|
||||
color: widget.barrierColor,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: _handleTapOnBackground,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return Stack(
|
||||
children: children..addAll(overlays),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleTapOnBackground() {
|
||||
removeAll();
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
// import 'dart:math' as math;
|
||||
// import 'dart:ui';
|
||||
|
||||
// import 'package:flutter/material.dart';
|
||||
|
||||
// import 'flowy_overlay.dart';
|
||||
|
||||
// class OverlayLayoutDelegate extends SingleChildLayoutDelegate {
|
||||
// OverlayLayoutDelegate({
|
||||
// required this.route,
|
||||
// required this.padding,
|
||||
// required this.anchorPosition,
|
||||
// required this.anchorDirection,
|
||||
// });
|
||||
|
||||
// final OverlayPannelRoute route;
|
||||
// final EdgeInsets padding;
|
||||
// final AnchorDirection anchorDirection;
|
||||
// final Offset anchorPosition;
|
||||
|
||||
// @override
|
||||
// bool shouldRelayout(OverlayLayoutDelegate oldDelegate) {
|
||||
// return anchorPosition != oldDelegate.anchorPosition || anchorDirection != oldDelegate.anchorDirection;
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Offset getPositionForChild(Size size, Size childSize) {
|
||||
// // TODO: junlin - calculate child position
|
||||
// return Offset.zero;
|
||||
// }
|
||||
|
||||
// @override
|
||||
// BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
|
||||
// double maxHeight = math.max(0.0, constraints.maxHeight - padding.top - padding.bottom);
|
||||
// double width = constraints.maxWidth;
|
||||
// return BoxConstraints(
|
||||
// minHeight: 0.0,
|
||||
// maxHeight: maxHeight,
|
||||
// minWidth: width,
|
||||
// maxWidth: width,
|
||||
// );
|
||||
// }
|
||||
// }
|
@ -0,0 +1,187 @@
|
||||
// import 'package:flowy_infra_ui/src/overlay/overlay_basis.dart';
|
||||
// import 'package:flowy_infra_ui/src/overlay/overlay_route.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
|
||||
// import 'overlay_hittest.dart';
|
||||
|
||||
// final GlobalKey<OverlayManagerState> _key = GlobalKey<OverlayManagerState>();
|
||||
|
||||
// /// Invoke this method in app generation process
|
||||
// TransitionBuilder overlayManagerBuilder() {
|
||||
// return (context, child) {
|
||||
// return OverlayManager(key: _key, child: child);
|
||||
// };
|
||||
// }
|
||||
|
||||
// class OverlayManager extends StatefulWidget {
|
||||
// const OverlayManager({Key? key, required this.child}) : super(key: key);
|
||||
|
||||
// final Widget? child;
|
||||
|
||||
// static OverlayManagerState of(
|
||||
// BuildContext context, {
|
||||
// bool rootOverlay = false,
|
||||
// }) {
|
||||
// OverlayManagerState? overlayManager;
|
||||
// if (rootOverlay) {
|
||||
// overlayManager = context.findRootAncestorStateOfType<OverlayManagerState>() ?? overlayManager;
|
||||
// } else {
|
||||
// overlayManager = overlayManager ?? context.findAncestorStateOfType<OverlayManagerState>();
|
||||
// }
|
||||
|
||||
// assert(() {
|
||||
// if (overlayManager == null) {
|
||||
// throw FlutterError(
|
||||
// 'Can\'t find overlay manager in current context, please check if already wrapped by overlay manager.',
|
||||
// );
|
||||
// }
|
||||
// return true;
|
||||
// }());
|
||||
// return overlayManager!;
|
||||
// }
|
||||
|
||||
// static OverlayManagerState? maybeOf(
|
||||
// BuildContext context, {
|
||||
// bool rootOverlay = false,
|
||||
// }) {
|
||||
// OverlayManagerState? overlayManager;
|
||||
// if (rootOverlay) {
|
||||
// overlayManager = context.findRootAncestorStateOfType<OverlayManagerState>() ?? overlayManager;
|
||||
// } else {
|
||||
// overlayManager = overlayManager ?? context.findAncestorStateOfType<OverlayManagerState>();
|
||||
// }
|
||||
|
||||
// return overlayManager;
|
||||
// }
|
||||
|
||||
// @override
|
||||
// OverlayManagerState createState() => OverlayManagerState();
|
||||
// }
|
||||
|
||||
// class OverlayManagerState extends State<OverlayManager> {
|
||||
// final Map<String, Map<String, OverlayEntry>> _overlayEntrys = {};
|
||||
// List<OverlayEntry> get _overlays => _overlayEntrys.values.fold<List<OverlayEntry>>(<OverlayEntry>[], (value, items) {
|
||||
// return value..addAll(items.values);
|
||||
// });
|
||||
// OverlayPannelRoute? _overlayRoute;
|
||||
// bool isShowingOverlayRoute = false;
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// OverlayManagerNavigatorObserver.didPushCallback = _handleDidPush;
|
||||
// OverlayManagerNavigatorObserver.didPopCallback = _handleDidPop;
|
||||
// }
|
||||
|
||||
// void insert(Widget widget, String featureKey, String key) {
|
||||
// final overlay = Overlay.of(context);
|
||||
// assert(overlay != null);
|
||||
|
||||
// if (!isShowingOverlayRoute) {
|
||||
// _showOverlayRoutePage(context: context);
|
||||
// }
|
||||
|
||||
// final entry = OverlayEntry(builder: (_) => widget);
|
||||
// _overlayEntrys[featureKey] ??= {};
|
||||
// _overlayEntrys[featureKey]![key] = entry;
|
||||
// overlay!.insert(entry);
|
||||
// }
|
||||
|
||||
// void insertAll(List<Widget> widgets, String featureKey, List<String> keys) {
|
||||
// assert(widgets.isNotEmpty);
|
||||
// assert(widgets.length == keys.length);
|
||||
|
||||
// final overlay = Overlay.of(context);
|
||||
// assert(overlay != null);
|
||||
|
||||
// List<OverlayEntry> entries = [];
|
||||
// _overlayEntrys[featureKey] ??= {};
|
||||
// for (int idx = 0; idx < widgets.length; idx++) {
|
||||
// final entry = OverlayEntry(builder: (_) => widget);
|
||||
// entries.add(entry);
|
||||
// _overlayEntrys[featureKey]![keys[idx]] = entry;
|
||||
// }
|
||||
// overlay!.insertAll(entries);
|
||||
// }
|
||||
|
||||
// void remove(String featureKey, String key) {
|
||||
// if (_overlayEntrys.containsKey(featureKey)) {
|
||||
// final entry = _overlayEntrys[featureKey]!.remove(key);
|
||||
// entry?.remove();
|
||||
// }
|
||||
// }
|
||||
|
||||
// void removeAll(String featureKey) {
|
||||
// if (_overlayEntrys.containsKey(featureKey)) {
|
||||
// final entries = _overlayEntrys.remove(featureKey);
|
||||
// entries?.forEach((_, overlay) {
|
||||
// overlay.remove();
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// assert(widget.child != null);
|
||||
// return GestureDetector(
|
||||
// behavior: _overlayEntrys.isEmpty ? HitTestBehavior.deferToChild : HitTestBehavior.opaque,
|
||||
// onTapDown: _handleTapDown,
|
||||
// child: widget.child,
|
||||
// );
|
||||
// }
|
||||
|
||||
// void _showOverlayRoutePage({
|
||||
// required BuildContext context,
|
||||
// }) {
|
||||
// _overlayRoute = OverlayPannelRoute(
|
||||
// barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||
// );
|
||||
// final navigator = Navigator.of(context);
|
||||
// // TODO: junlin - Use Navigation Overservers
|
||||
// navigator.push(_overlayRoute!);
|
||||
// }
|
||||
|
||||
// void _handleTapDown(TapDownDetails tapDownDetails) {
|
||||
// bool hitOnOverlay = false;
|
||||
// _overlays.forEach((overlay) {});
|
||||
// }
|
||||
|
||||
// void _handleDidPush(Route route, Route? previousRoute) {
|
||||
// if (route is OverlayPannelRoute) {
|
||||
// isShowingOverlayRoute = true;
|
||||
// _showPendingOverlays();
|
||||
// }
|
||||
// }
|
||||
|
||||
// void _handleDidPop(Route route, Route? previousRoute) {
|
||||
// if (previousRoute is OverlayPannelRoute) {
|
||||
// isShowingOverlayRoute = false;
|
||||
// _removeOverlays();
|
||||
// }
|
||||
// }
|
||||
|
||||
// void _showPendingOverlays() {}
|
||||
|
||||
// void _removeOverlays() {}
|
||||
// }
|
||||
|
||||
// class OverlayManagerNavigatorObserver extends NavigatorObserver {
|
||||
// static void Function(Route route, Route? previousRoute)? didPushCallback;
|
||||
// static void Function(Route route, Route? previousRoute)? didPopCallback;
|
||||
|
||||
// @override
|
||||
// void didPush(Route route, Route? previousRoute) {
|
||||
// if (didPushCallback != null) {
|
||||
// didPushCallback!(route, previousRoute);
|
||||
// }
|
||||
// super.didPush(route, previousRoute);
|
||||
// }
|
||||
|
||||
// @override
|
||||
// void didPop(Route route, Route? previousRoute) {
|
||||
// if (didPopCallback != null) {
|
||||
// didPopCallback!(route, previousRoute);
|
||||
// }
|
||||
// super.didPop(route, previousRoute);
|
||||
// }
|
||||
// }
|
@ -0,0 +1,123 @@
|
||||
// import 'dart:ui' show window;
|
||||
|
||||
// import 'package:flowy_infra_ui/src/overlay/overlay_route.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
|
||||
// import 'overlay_.dart';
|
||||
|
||||
// class OverlayPannel extends StatefulWidget {
|
||||
// const OverlayPannel({
|
||||
// Key? key,
|
||||
// this.focusNode,
|
||||
// this.padding = EdgeInsets.zero,
|
||||
// this.anchorDirection = AnchorDirection.topRight,
|
||||
// required this.anchorPosition,
|
||||
// required this.route,
|
||||
// }) : super(key: key);
|
||||
|
||||
// final FocusNode? focusNode;
|
||||
// final EdgeInsetsGeometry padding;
|
||||
// final AnchorDirection anchorDirection;
|
||||
// final Offset anchorPosition;
|
||||
// final OverlayPannelRoute route;
|
||||
|
||||
// @override
|
||||
// _OverlayPannelState createState() => _OverlayPannelState();
|
||||
// }
|
||||
|
||||
// class _OverlayPannelState extends State<OverlayPannel> with WidgetsBindingObserver {
|
||||
// FocusNode? _internalNode;
|
||||
// FocusNode? get focusNode => widget.focusNode ?? _internalNode;
|
||||
// late FocusHighlightMode _focusHighlightMode;
|
||||
// bool _hasPrimaryFocus = false;
|
||||
// late CurvedAnimation _fadeOpacity;
|
||||
// late CurvedAnimation _resize;
|
||||
// OverlayPannelRoute? _overlayRoute;
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// _fadeOpacity = CurvedAnimation(
|
||||
// parent: widget.route.animation!,
|
||||
// curve: const Interval(0.0, 0.25),
|
||||
// reverseCurve: const Interval(0.75, 1.0),
|
||||
// );
|
||||
// _resize = CurvedAnimation(
|
||||
// parent: widget.route.animation!,
|
||||
// curve: const Interval(0.25, 0.5),
|
||||
// reverseCurve: const Threshold(0.0),
|
||||
// );
|
||||
|
||||
// // TODO: junlin - handle focus action or remove it
|
||||
// if (widget.focusNode == null) {
|
||||
// _internalNode ??= _createFocusNode();
|
||||
// }
|
||||
// focusNode!.addListener(_handleFocusChanged);
|
||||
// final FocusManager focusManager = WidgetsBinding.instance!.focusManager;
|
||||
// _focusHighlightMode = focusManager.highlightMode;
|
||||
// focusManager.addHighlightModeListener(_handleFocusHighlightModeChanged);
|
||||
// }
|
||||
|
||||
// @override
|
||||
// void dispose() {
|
||||
// WidgetsBinding.instance!.removeObserver(this);
|
||||
// focusNode!.removeListener(_handleFocusChanged);
|
||||
// WidgetsBinding.instance!.focusManager.removeHighlightModeListener(_handleFocusHighlightModeChanged);
|
||||
// _internalNode?.dispose();
|
||||
// super.dispose();
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return FadeTransition(
|
||||
// opacity: _fadeOpacity,
|
||||
// );
|
||||
// }
|
||||
|
||||
// @override
|
||||
// void didUpdateWidget(OverlayPannel oldWidget) {
|
||||
// super.didUpdateWidget(oldWidget);
|
||||
// if (widget.focusNode != oldWidget.focusNode) {
|
||||
// oldWidget.focusNode?.removeListener(_handleFocusChanged);
|
||||
// if (widget.focusNode == null) {
|
||||
// _internalNode ??= _createFocusNode();
|
||||
// }
|
||||
// _hasPrimaryFocus = focusNode!.hasPrimaryFocus;
|
||||
// focusNode!.addListener(_handleFocusChanged);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // MARK: Focus & Route
|
||||
|
||||
// FocusNode _createFocusNode() {
|
||||
// return FocusNode(debugLabel: '${widget.runtimeType}');
|
||||
// }
|
||||
|
||||
// void _handleFocusChanged() {
|
||||
// if (_hasPrimaryFocus != focusNode!.hasPrimaryFocus) {
|
||||
// setState(() {
|
||||
// _hasPrimaryFocus = focusNode!.hasPrimaryFocus;
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// void _handleFocusHighlightModeChanged(FocusHighlightMode mode) {
|
||||
// if (!mounted) {
|
||||
// return;
|
||||
// }
|
||||
// setState(() {
|
||||
// _focusHighlightMode = mode;
|
||||
// });
|
||||
// }
|
||||
|
||||
// // MARK: Layout
|
||||
|
||||
// Orientation _getOrientation(BuildContext context) {
|
||||
// Orientation? result = MediaQuery.maybeOf(context)?.orientation;
|
||||
// if (result == null) {
|
||||
// final Size size = window.physicalSize;
|
||||
// result = size.width > size.height ? Orientation.landscape : Orientation.portrait;
|
||||
// }
|
||||
// return result;
|
||||
// }
|
||||
// }
|
@ -0,0 +1,55 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
|
||||
// class _OverlayRouteResult {}
|
||||
|
||||
// const Duration _kOverlayDuration = Duration(milliseconds: 0);
|
||||
|
||||
// class OverlayPannelRoute extends PopupRoute<_OverlayRouteResult> {
|
||||
// OverlayPannelRoute({
|
||||
// this.barrierColor,
|
||||
// required this.barrierLabel,
|
||||
// });
|
||||
|
||||
// @override
|
||||
// bool get barrierDismissible => true;
|
||||
|
||||
// @override
|
||||
// Color? barrierColor;
|
||||
|
||||
// @override
|
||||
// String? barrierLabel;
|
||||
|
||||
// @override
|
||||
// Duration get transitionDuration => _kOverlayDuration;
|
||||
|
||||
// @override
|
||||
// Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
|
||||
// return LayoutBuilder(builder: (context, contraints) {
|
||||
// return const _OverlayRoutePage();
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// class _OverlayRoutePage extends StatelessWidget {
|
||||
// const _OverlayRoutePage({
|
||||
// Key? key,
|
||||
// }) : super(key: key);
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// assert(debugCheckHasDirectionality(context));
|
||||
// final TextDirection? textDirection = Directionality.maybeOf(context);
|
||||
// // TODO: junlin - Use overlay pannel to manage focus node
|
||||
|
||||
// return MediaQuery.removePadding(
|
||||
// context: context,
|
||||
// removeTop: true,
|
||||
// removeBottom: true,
|
||||
// removeLeft: true,
|
||||
// removeRight: true,
|
||||
// child: Container(
|
||||
// color: Colors.blue[100],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
@ -1,40 +0,0 @@
|
||||
/// Specifies how overlay are anchored to the SourceWidget
|
||||
enum AnchorDirection {
|
||||
// Corner aligned with a corner of the SourceWidget
|
||||
topLeft,
|
||||
topRight,
|
||||
bottomLeft,
|
||||
bottomRight,
|
||||
|
||||
// Edge aligned with a edge of the SourceWidget
|
||||
topWithLeftAligned,
|
||||
topWithCenterAligned,
|
||||
topWithRightAligned,
|
||||
rightWithTopAligned,
|
||||
rightWithCenterAligned,
|
||||
rightWithBottomAligned,
|
||||
bottomWithLeftAligned,
|
||||
bottomWithCenterAligned,
|
||||
bottomWithRightAligned,
|
||||
leftWithTopAligned,
|
||||
leftWithCenterAligned,
|
||||
leftWithBottomAligned,
|
||||
|
||||
// Custom position
|
||||
custom,
|
||||
}
|
||||
|
||||
/// The behavior of overlay when user tapping system back button
|
||||
enum OnBackBehavior {
|
||||
/// Won't handle the back action
|
||||
none,
|
||||
|
||||
/// Animate to get the user's attention
|
||||
alert,
|
||||
|
||||
/// Intercept the back action and abort directly
|
||||
abort,
|
||||
|
||||
/// Intercept the back action and dismiss overlay
|
||||
dismiss,
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class OverlayHitTestArea extends SingleChildRenderObjectWidget {
|
||||
const OverlayHitTestArea({
|
||||
Key? key,
|
||||
Widget? child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) => RenderOverlayHitTestArea();
|
||||
}
|
||||
|
||||
class RenderOverlayHitTestArea extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
|
||||
@override
|
||||
bool hitTest(BoxHitTestResult result, {required Offset position}) {
|
||||
print('hitTesting');
|
||||
return super.hitTest(result, position: position);
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'overlay_route.dart';
|
||||
import 'overlay_basis.dart';
|
||||
|
||||
class OverlayLayoutDelegate extends SingleChildLayoutDelegate {
|
||||
OverlayLayoutDelegate({
|
||||
required this.route,
|
||||
required this.padding,
|
||||
required this.anchorPosition,
|
||||
required this.anchorDirection,
|
||||
});
|
||||
|
||||
final OverlayPannelRoute route;
|
||||
final EdgeInsets padding;
|
||||
final AnchorDirection anchorDirection;
|
||||
final Offset anchorPosition;
|
||||
|
||||
@override
|
||||
bool shouldRelayout(OverlayLayoutDelegate oldDelegate) {
|
||||
return anchorPosition != oldDelegate.anchorPosition || anchorDirection != oldDelegate.anchorDirection;
|
||||
}
|
||||
|
||||
@override
|
||||
Offset getPositionForChild(Size size, Size childSize) {
|
||||
// TODO: junlin - calculate child position
|
||||
return Offset.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
|
||||
double maxHeight = math.max(
|
||||
0.0,
|
||||
math.min(route.maxHeight, constraints.maxHeight - padding.top - padding.bottom),
|
||||
);
|
||||
double width = math.min(route.maxWidth, constraints.maxWidth);
|
||||
return BoxConstraints(
|
||||
minHeight: 0.0,
|
||||
maxHeight: maxHeight,
|
||||
minWidth: width,
|
||||
maxWidth: width,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,113 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'overlay_hittest.dart';
|
||||
|
||||
final GlobalKey<OverlayManagerState> _key = GlobalKey<OverlayManagerState>();
|
||||
|
||||
/// Invoke this method in app generation process
|
||||
TransitionBuilder overlayManagerBuilder() {
|
||||
return (context, child) {
|
||||
return OverlayManager(key: _key, child: child);
|
||||
};
|
||||
}
|
||||
|
||||
class OverlayManager extends StatefulWidget {
|
||||
const OverlayManager({Key? key, required this.child}) : super(key: key);
|
||||
|
||||
final Widget? child;
|
||||
|
||||
static OverlayManagerState of(
|
||||
BuildContext context, {
|
||||
bool rootOverlay = false,
|
||||
}) {
|
||||
OverlayManagerState? overlayManager;
|
||||
if (rootOverlay) {
|
||||
overlayManager = context.findRootAncestorStateOfType<OverlayManagerState>() ?? overlayManager;
|
||||
} else {
|
||||
overlayManager = overlayManager ?? context.findAncestorStateOfType<OverlayManagerState>();
|
||||
}
|
||||
|
||||
assert(() {
|
||||
if (overlayManager == null) {
|
||||
throw FlutterError(
|
||||
'Can\'t find overlay manager in current context, please check if already wrapped by overlay manager.',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
return overlayManager!;
|
||||
}
|
||||
|
||||
static OverlayManagerState? maybeOf(
|
||||
BuildContext context, {
|
||||
bool rootOverlay = false,
|
||||
}) {
|
||||
OverlayManagerState? overlayManager;
|
||||
if (rootOverlay) {
|
||||
overlayManager = context.findRootAncestorStateOfType<OverlayManagerState>() ?? overlayManager;
|
||||
} else {
|
||||
overlayManager = overlayManager ?? context.findAncestorStateOfType<OverlayManagerState>();
|
||||
}
|
||||
|
||||
return overlayManager;
|
||||
}
|
||||
|
||||
@override
|
||||
OverlayManagerState createState() => OverlayManagerState();
|
||||
}
|
||||
|
||||
class OverlayManagerState extends State<OverlayManager> {
|
||||
final Map<String, Map<String, OverlayEntry>> _overlayEntrys = {};
|
||||
|
||||
void insert(Widget widget, String featureKey, String key) {
|
||||
final overlay = Overlay.of(context);
|
||||
assert(overlay != null);
|
||||
|
||||
final entry = OverlayEntry(builder: (_) => widget);
|
||||
_overlayEntrys[featureKey] ??= {};
|
||||
_overlayEntrys[featureKey]![key] = entry;
|
||||
overlay!.insert(entry);
|
||||
}
|
||||
|
||||
void insertAll(List<Widget> widgets, String featureKey, List<String> keys) {
|
||||
assert(widgets.isNotEmpty);
|
||||
assert(widgets.length == keys.length);
|
||||
|
||||
final overlay = Overlay.of(context);
|
||||
assert(overlay != null);
|
||||
|
||||
List<OverlayEntry> entries = [];
|
||||
_overlayEntrys[featureKey] ??= {};
|
||||
for (int idx = 0; idx < widgets.length; idx++) {
|
||||
final entry = OverlayEntry(builder: (_) => widget);
|
||||
entries.add(entry);
|
||||
_overlayEntrys[featureKey]![keys[idx]] = entry;
|
||||
}
|
||||
overlay!.insertAll(entries);
|
||||
}
|
||||
|
||||
void remove(String featureKey, String key) {
|
||||
if (_overlayEntrys.containsKey(featureKey)) {
|
||||
final entry = _overlayEntrys[featureKey]!.remove(key);
|
||||
entry?.remove();
|
||||
}
|
||||
}
|
||||
|
||||
void removeAll(String featureKey) {
|
||||
if (_overlayEntrys.containsKey(featureKey)) {
|
||||
final entries = _overlayEntrys.remove(featureKey);
|
||||
entries?.forEach((_, overlay) {
|
||||
overlay.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(widget.child != null);
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: OverlayHitTestArea(child: widget.child),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
import 'dart:ui' show window;
|
||||
|
||||
import 'package:flowy_infra_ui/src/overlay/overlay_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'overlay_basis.dart';
|
||||
|
||||
class OverlayPannel extends StatefulWidget {
|
||||
const OverlayPannel({
|
||||
Key? key,
|
||||
this.focusNode,
|
||||
this.padding = EdgeInsets.zero,
|
||||
this.anchorDirection = AnchorDirection.topRight,
|
||||
required this.anchorPosition,
|
||||
required this.route,
|
||||
}) : super(key: key);
|
||||
|
||||
final FocusNode? focusNode;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final AnchorDirection anchorDirection;
|
||||
final Offset anchorPosition;
|
||||
final OverlayPannelRoute route;
|
||||
|
||||
@override
|
||||
_OverlayPannelState createState() => _OverlayPannelState();
|
||||
}
|
||||
|
||||
class _OverlayPannelState extends State<OverlayPannel> with WidgetsBindingObserver {
|
||||
FocusNode? _internalNode;
|
||||
FocusNode? get focusNode => widget.focusNode ?? _internalNode;
|
||||
late FocusHighlightMode _focusHighlightMode;
|
||||
bool _hasPrimaryFocus = false;
|
||||
late CurvedAnimation _fadeOpacity;
|
||||
late CurvedAnimation _resize;
|
||||
OverlayPannelRoute? _overlayRoute;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fadeOpacity = CurvedAnimation(
|
||||
parent: widget.route.animation!,
|
||||
curve: const Interval(0.0, 0.25),
|
||||
reverseCurve: const Interval(0.75, 1.0),
|
||||
);
|
||||
_resize = CurvedAnimation(
|
||||
parent: widget.route.animation!,
|
||||
curve: const Interval(0.25, 0.5),
|
||||
reverseCurve: const Threshold(0.0),
|
||||
);
|
||||
|
||||
// TODO: junlin - handle focus action or remove it
|
||||
if (widget.focusNode == null) {
|
||||
_internalNode ??= _createFocusNode();
|
||||
}
|
||||
focusNode!.addListener(_handleFocusChanged);
|
||||
final FocusManager focusManager = WidgetsBinding.instance!.focusManager;
|
||||
_focusHighlightMode = focusManager.highlightMode;
|
||||
focusManager.addHighlightModeListener(_handleFocusHighlightModeChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance!.removeObserver(this);
|
||||
focusNode!.removeListener(_handleFocusChanged);
|
||||
WidgetsBinding.instance!.focusManager.removeHighlightModeListener(_handleFocusHighlightModeChanged);
|
||||
_internalNode?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: _fadeOpacity,
|
||||
child: widget.route.widgetBuilder(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(OverlayPannel oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.focusNode != oldWidget.focusNode) {
|
||||
oldWidget.focusNode?.removeListener(_handleFocusChanged);
|
||||
if (widget.focusNode == null) {
|
||||
_internalNode ??= _createFocusNode();
|
||||
}
|
||||
_hasPrimaryFocus = focusNode!.hasPrimaryFocus;
|
||||
focusNode!.addListener(_handleFocusChanged);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Focus & Route
|
||||
|
||||
FocusNode _createFocusNode() {
|
||||
return FocusNode(debugLabel: '${widget.runtimeType}');
|
||||
}
|
||||
|
||||
void _handleFocusChanged() {
|
||||
if (_hasPrimaryFocus != focusNode!.hasPrimaryFocus) {
|
||||
setState(() {
|
||||
_hasPrimaryFocus = focusNode!.hasPrimaryFocus;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _handleFocusHighlightModeChanged(FocusHighlightMode mode) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_focusHighlightMode = mode;
|
||||
});
|
||||
}
|
||||
|
||||
// MARK: Layout
|
||||
|
||||
Orientation _getOrientation(BuildContext context) {
|
||||
Orientation? result = MediaQuery.maybeOf(context)?.orientation;
|
||||
if (result == null) {
|
||||
final Size size = window.physicalSize;
|
||||
result = size.width > size.height ? Orientation.landscape : Orientation.portrait;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
import 'package:flowy_infra_ui/src/overlay/overlay_pannel.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'overlay_basis.dart';
|
||||
import 'overlay_layout_delegate.dart';
|
||||
|
||||
class _OverlayRouteResult {}
|
||||
|
||||
const Duration _kOverlayDurationDuration = Duration(milliseconds: 500);
|
||||
|
||||
class OverlayPannelRoute extends PopupRoute<_OverlayRouteResult> {
|
||||
final EdgeInsetsGeometry padding;
|
||||
final AnchorDirection anchorDirection;
|
||||
final Offset anchorPosition;
|
||||
final double maxWidth;
|
||||
final double maxHeight;
|
||||
final WidgetBuilder widgetBuilder;
|
||||
|
||||
OverlayPannelRoute({
|
||||
this.padding = EdgeInsets.zero,
|
||||
required this.anchorDirection,
|
||||
this.barrierColor,
|
||||
required this.barrierLabel,
|
||||
required this.anchorPosition,
|
||||
required this.maxWidth,
|
||||
required this.maxHeight,
|
||||
required this.widgetBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
bool get barrierDismissible => true;
|
||||
|
||||
@override
|
||||
Color? barrierColor;
|
||||
|
||||
@override
|
||||
String? barrierLabel;
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => _kOverlayDurationDuration;
|
||||
|
||||
@override
|
||||
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
|
||||
return LayoutBuilder(builder: (context, contraints) {
|
||||
return _OverlayRoutePage(
|
||||
route: this,
|
||||
anchorDirection: anchorDirection,
|
||||
anchorPosition: anchorPosition,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _OverlayRoutePage extends StatelessWidget {
|
||||
const _OverlayRoutePage({
|
||||
Key? key,
|
||||
required this.route,
|
||||
this.padding = EdgeInsets.zero,
|
||||
required this.anchorDirection,
|
||||
required this.anchorPosition,
|
||||
}) : super(key: key);
|
||||
|
||||
final OverlayPannelRoute route;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final AnchorDirection anchorDirection;
|
||||
final Offset anchorPosition;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
final TextDirection? textDirection = Directionality.maybeOf(context);
|
||||
final OverlayPannel overlayPannel = OverlayPannel(
|
||||
route: route,
|
||||
padding: padding,
|
||||
anchorDirection: anchorDirection,
|
||||
anchorPosition: anchorPosition,
|
||||
);
|
||||
|
||||
return MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
removeBottom: true,
|
||||
removeLeft: true,
|
||||
removeRight: true,
|
||||
child: Builder(
|
||||
builder: (context) => CustomSingleChildLayout(
|
||||
delegate: OverlayLayoutDelegate(
|
||||
route: route,
|
||||
padding: padding.resolve(textDirection),
|
||||
anchorPosition: anchorPosition,
|
||||
anchorDirection: anchorDirection,
|
||||
),
|
||||
child: overlayPannel,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user