瀏覽代碼

[infra_ui][overlay] Merge overlay anchor feature

Jaylen Bian 3 年之前
父節點
當前提交
d7a3e7b2ec

+ 120 - 25
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<AnchorDirection>(
+                      value: providerContext.watch<OverlayDemoAnchorDirection>().anchorDirection,
+                      onChanged: (AnchorDirection? newValue) {
+                        if (newValue != null) {
+                          providerContext.read<OverlayDemoAnchorDirection>().anchorDirection = newValue;
+                        }
+                      },
+                      items: AnchorDirection.values.map((AnchorDirection classType) {
+                        return DropdownMenuItem<AnchorDirection>(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<OverlayDemoAnchorDirection>().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<OverlayDemoAnchorDirection>().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'),
-            ),
-          ],
+            );
+          }),
         ));
   }
 }

+ 110 - 5
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<FlowyOverlay> {
   List<Tuple3<Widget, String, FlowyOverlayDelegate?>> _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<FlowyOverlay> {
     }
   }
 
+  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);

+ 135 - 36
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)),
+    );
+  }
+}