diff --git a/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart b/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart index b6a62f64cd..ae1b955f01 100644 --- a/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart +++ b/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart @@ -1,5 +1,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import '../home/demo_item.dart'; @@ -19,6 +20,19 @@ class OverlayItem extends DemoItem { } } +class OverlayDemoAnchorDirection extends ChangeNotifier { + OverlayDemoAnchorDirection(this._anchorDirection); + + AnchorDirection _anchorDirection; + + AnchorDirection get anchorDirection => _anchorDirection; + + set anchorDirection(AnchorDirection value) { + _anchorDirection = value; + notifyListeners(); + } +} + class OverlayScreen extends StatelessWidget { const OverlayScreen({Key? key}) : super(key: key); @@ -28,33 +42,114 @@ class OverlayScreen extends StatelessWidget { appBar: AppBar( title: const Text('Overlay Demo'), ), - body: Column( - children: [ - Flexible( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Container( - height: 300.0, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15.0), - color: Colors.grey[200], - ), + body: ChangeNotifierProvider( + create: (context) => OverlayDemoAnchorDirection(AnchorDirection.rightWithTopAligned), + child: Builder(builder: (providerContext) { + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 48.0), + ElevatedButton( + onPressed: () { + final windowSize = MediaQuery.of(context).size; + FlowyOverlay.of(context).insertCustom( + widget: Positioned( + left: windowSize.width / 2.0 - 100, + top: 200, + child: SizedBox( + width: 200, + height: 100, + child: Card( + color: Colors.green[200], + child: GestureDetector( + // ignore: avoid_print + onTapDown: (_) => print('Hello Flutter'), + child: const Center(child: FlutterLogo(size: 100)), + ), + ), + ), + ), + identifier: 'overlay_flutter_logo', + delegate: null, + ); + }, + child: const Text('Show Overlay'), + ), + const SizedBox(height: 24.0), + DropdownButton( + value: providerContext.watch().anchorDirection, + onChanged: (AnchorDirection? newValue) { + if (newValue != null) { + providerContext.read().anchorDirection = newValue; + } + }, + items: AnchorDirection.values.map((AnchorDirection classType) { + return DropdownMenuItem(value: classType, child: Text(classType.toString())); + }).toList(), + ), + const SizedBox(height: 24.0), + Builder(builder: (buttonContext) { + return SizedBox( + height: 100, + child: ElevatedButton( + onPressed: () { + FlowyOverlay.of(context).insertWithAnchor( + widget: SizedBox( + width: 100, + height: 50, + child: Card( + color: Colors.grey[200], + child: GestureDetector( + // ignore: avoid_print + onTapDown: (_) => print('Hello Flutter'), + child: const Center(child: FlutterLogo(size: 50)), + ), + ), + ), + identifier: 'overlay_anchored_card', + delegate: null, + anchorContext: buttonContext, + anchorDirection: providerContext.read().anchorDirection, + ); + }, + child: const Text('Show Anchored Overlay'), + ), + ); + }), + const SizedBox(height: 24.0), + ElevatedButton( + onPressed: () { + final windowSize = MediaQuery.of(context).size; + FlowyOverlay.of(context).insertWithRect( + widget: SizedBox( + width: 200, + height: 100, + child: Card( + color: Colors.orange[200], + child: GestureDetector( + // ignore: avoid_print + onTapDown: (_) => print('Hello Flutter'), + child: const Center(child: FlutterLogo(size: 100)), + ), + ), + ), + identifier: 'overlay_positioned_card', + delegate: null, + anchorPosition: Offset(0, windowSize.height - 200), + anchorSize: Size.zero, + anchorDirection: providerContext.read().anchorDirection, + ); + }, + child: const Text('Show Positioned Overlay'), + ), + ], ), ), - ), - ElevatedButton( - onPressed: () { - FlowyOverlay.of(context).insert( - widget: const FlutterLogo( - size: 200, - ), - identifier: 'overlay_flutter_logo', - delegate: null, - ); - }, - child: const Text('Show Overlay'), - ), - ], + ); + }), )); } } diff --git a/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart b/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart index 237b6bea2f..a12988f0a5 100644 --- a/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart +++ b/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart @@ -1,4 +1,5 @@ import 'package:dartz/dartz.dart' show Tuple3; +import 'package:flowy_infra_ui/src/flowy_overlay/overlay_layout_delegate.dart'; import 'package:flutter/material.dart'; /// Specifies how overlay are anchored to the SourceWidget @@ -27,7 +28,18 @@ enum AnchorDirection { custom, } -/// The behavior of overlay when user tapping system back button +// TODO: junlin - support overlap behaviour +/// [Unsupported] The behaviour of overlay when overlap with anchor widget +enum OverlapBehaviour { + /// Maintain overlay size, which may cover the anchor widget. + none, + + /// Resize overlay to avoid overlaping the anchor widget. + stretch, +} + +// TODO: junlin - support route pop handler +/// [Unsupported] The behavior of overlay when user tapping system back button enum OnBackBehavior { /// Won't handle the back action none, @@ -110,14 +122,55 @@ class FlowyOverlay extends StatefulWidget { class FlowyOverlayState extends State { List> _overlayList = []; - void insert({ + /// Insert a overlay widget which frame is set by the widget, not the component. + /// Be sure to specify the offset and size using the `Postition` widget. + void insertCustom({ required Widget widget, required String identifier, FlowyOverlayDelegate? delegate, }) { - setState(() { - _overlayList.add(Tuple3(widget, identifier, delegate)); - }); + _showOverlay( + widget: widget, + identifier: identifier, + shouldAnchor: false, + delegate: delegate, + ); + } + + void insertWithRect({ + required Widget widget, + required String identifier, + required Offset anchorPosition, + required Size anchorSize, + AnchorDirection? anchorDirection, + FlowyOverlayDelegate? delegate, + }) { + _showOverlay( + widget: widget, + identifier: identifier, + shouldAnchor: true, + delegate: delegate, + anchorPosition: anchorPosition, + anchorSize: anchorSize, + anchorDirection: anchorDirection, + ); + } + + void insertWithAnchor({ + required Widget widget, + required String identifier, + required BuildContext anchorContext, + AnchorDirection? anchorDirection, + FlowyOverlayDelegate? delegate, + }) { + _showOverlay( + widget: widget, + identifier: identifier, + shouldAnchor: true, + delegate: delegate, + anchorContext: anchorContext, + anchorDirection: anchorDirection, + ); } void remove(String identifier) { @@ -142,6 +195,58 @@ class FlowyOverlayState extends State { } } + void _showOverlay({ + required Widget widget, + required String identifier, + required bool shouldAnchor, + Offset? anchorPosition, + Size? anchorSize, + AnchorDirection? anchorDirection, + BuildContext? anchorContext, + OverlapBehaviour? overlapBehaviour, + FlowyOverlayDelegate? delegate, + }) { + Widget overlay = widget; + + if (shouldAnchor) { + assert( + anchorPosition != null || anchorContext != null, + 'Must provide `anchorPosition` or `anchorContext` to locating overlay.', + ); + Offset targetAnchorPosition = anchorPosition ?? Offset.zero; + Size targetAnchorSize = anchorSize ?? Size.zero; + if (anchorContext != null) { + RenderObject renderObject = anchorContext.findRenderObject()!; + assert( + renderObject is RenderBox, + 'Unexpect non-RenderBox render object caught.', + ); + final renderBox = renderObject as RenderBox; + targetAnchorPosition = renderBox.localToGlobal(Offset.zero); + targetAnchorSize = renderBox.size; + } + final anchorRect = Rect.fromLTWH( + targetAnchorPosition.dx, + targetAnchorPosition.dy, + targetAnchorSize.width, + targetAnchorSize.height, + ); + overlay = CustomSingleChildLayout( + delegate: OverlayLayoutDelegate( + anchorRect: anchorRect, + anchorDirection: + shouldAnchor ? anchorDirection ?? AnchorDirection.rightWithTopAligned : AnchorDirection.custom, + overlapBehaviour: overlapBehaviour ?? OverlapBehaviour.stretch, + ), + child: widget, + ); + } + + setState(() { + _overlayList.add(Tuple3(overlay, identifier, delegate)); + }); + } + @override Widget build(BuildContext context) { final overlays = _overlayList.map((ele) => ele.value1); diff --git a/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_layout_delegate.dart b/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_layout_delegate.dart index bfec790c73..fbf5bca6f7 100644 --- a/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_layout_delegate.dart +++ b/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_layout_delegate.dart @@ -1,43 +1,142 @@ -// import 'dart:math' as math; -// import 'dart:ui'; +import 'dart:math' as math; +import 'dart:ui'; -// import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; -// import 'flowy_overlay.dart'; +import 'flowy_overlay.dart'; -// class OverlayLayoutDelegate extends SingleChildLayoutDelegate { -// OverlayLayoutDelegate({ -// required this.route, -// required this.padding, -// required this.anchorPosition, -// required this.anchorDirection, -// }); +class OverlayLayoutDelegate extends SingleChildLayoutDelegate { + OverlayLayoutDelegate({ + required this.anchorRect, + required this.anchorDirection, + required this.overlapBehaviour, + }); -// final OverlayPannelRoute route; -// final EdgeInsets padding; -// final AnchorDirection anchorDirection; -// final Offset anchorPosition; + final Rect anchorRect; + final AnchorDirection anchorDirection; + final OverlapBehaviour overlapBehaviour; -// @override -// bool shouldRelayout(OverlayLayoutDelegate oldDelegate) { -// return anchorPosition != oldDelegate.anchorPosition || anchorDirection != oldDelegate.anchorDirection; -// } + @override + bool shouldRelayout(OverlayLayoutDelegate oldDelegate) { + return anchorRect != oldDelegate.anchorRect || + anchorDirection != oldDelegate.anchorDirection || + overlapBehaviour != oldDelegate.overlapBehaviour; + } -// @override -// Offset getPositionForChild(Size size, Size childSize) { -// // TODO: junlin - calculate child position -// return Offset.zero; -// } + @override + BoxConstraints getConstraintsForChild(BoxConstraints constraints) { + switch (overlapBehaviour) { + case OverlapBehaviour.none: + return constraints.loosen(); + case OverlapBehaviour.stretch: + // TODO: junlin - resize when overlapBehaviour == .stretch + return constraints.loosen(); + } + } -// @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, -// ); -// } -// } + @override + Offset getPositionForChild(Size size, Size childSize) { + Offset position; + switch (anchorDirection) { + case AnchorDirection.topLeft: + position = Offset( + anchorRect.left - childSize.width, + anchorRect.top - childSize.height, + ); + break; + case AnchorDirection.topRight: + position = Offset( + anchorRect.right, + anchorRect.top - childSize.height, + ); + break; + case AnchorDirection.bottomLeft: + position = Offset( + anchorRect.left - childSize.width, + anchorRect.bottom, + ); + break; + case AnchorDirection.bottomRight: + position = Offset( + anchorRect.right, + anchorRect.bottom, + ); + break; + case AnchorDirection.topWithLeftAligned: + position = Offset( + anchorRect.left, + anchorRect.top - childSize.height, + ); + break; + case AnchorDirection.topWithCenterAligned: + position = Offset( + anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0, + anchorRect.top - childSize.height, + ); + break; + case AnchorDirection.topWithRightAligned: + position = Offset( + anchorRect.right - childSize.width, + anchorRect.top - childSize.height, + ); + break; + case AnchorDirection.rightWithTopAligned: + position = Offset(anchorRect.right, anchorRect.top); + break; + case AnchorDirection.rightWithCenterAligned: + position = Offset( + anchorRect.right, + anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0, + ); + break; + case AnchorDirection.rightWithBottomAligned: + position = Offset( + anchorRect.right, + anchorRect.bottom - childSize.height, + ); + break; + case AnchorDirection.bottomWithLeftAligned: + position = Offset( + anchorRect.left, + anchorRect.bottom, + ); + break; + case AnchorDirection.bottomWithCenterAligned: + position = Offset( + anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0, + anchorRect.bottom, + ); + break; + case AnchorDirection.bottomWithRightAligned: + position = Offset( + anchorRect.right - childSize.width, + anchorRect.bottom, + ); + break; + case AnchorDirection.leftWithTopAligned: + position = Offset( + anchorRect.left - childSize.width, + anchorRect.top, + ); + break; + case AnchorDirection.leftWithCenterAligned: + position = Offset( + anchorRect.left - childSize.width, + anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0, + ); + break; + case AnchorDirection.leftWithBottomAligned: + position = Offset( + anchorRect.left - childSize.width, + anchorRect.bottom - childSize.height, + ); + break; + default: + throw UnimplementedError(); + } + return Offset( + math.max(0.0, math.min(size.width - childSize.width, position.dx)), + math.max(0.0, math.min(size.height - childSize.height, position.dy)), + ); + } +}