[infra_ui][overlay] Impl overlay manager based on stack

This commit is contained in:
Jaylen Bian 2021-07-28 16:37:37 +08:00
parent e7a6a41437
commit 9508ad84e4
15 changed files with 586 additions and 447 deletions

View File

@ -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(),
);
}
}

View File

@ -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'),
),
],

View File

@ -3,3 +3,6 @@ export 'basis.dart';
// Keyboard
export 'src/keyboard/keyboard_visibility_detector.dart';
// Overlay
export 'src/flowy_overlay/flowy_overlay.dart';

View File

@ -3,3 +3,6 @@ export 'basis.dart';
// Keyboard
export 'src/keyboard/keyboard_visibility_detector.dart';
// Overlay
export 'src/flowy_overlay/flowy_overlay.dart';

View File

@ -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();
}
}

View File

@ -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,
// );
// }
// }

View File

@ -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);
// }
// }

View File

@ -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;
// }
// }

View File

@ -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],
// ),
// );
// }
// }

View File

@ -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,
}

View File

@ -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);
}
}

View File

@ -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,
);
}
}

View File

@ -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),
);
}
}

View File

@ -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;
}
}

View File

@ -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,
),
),
);
}
}