mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-24 14:47:13 -04:00
[infra_ui][overlay] Merge overlay anchor feature
This commit is contained in:
commit
d7a3e7b2ec
3 changed files with 365 additions and 66 deletions
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../home/demo_item.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 {
|
class OverlayScreen extends StatelessWidget {
|
||||||
const OverlayScreen({Key? key}) : super(key: key);
|
const OverlayScreen({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@ -28,33 +42,114 @@ class OverlayScreen extends StatelessWidget {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Overlay Demo'),
|
title: const Text('Overlay Demo'),
|
||||||
),
|
),
|
||||||
body: Column(
|
body: ChangeNotifierProvider(
|
||||||
children: [
|
create: (context) => OverlayDemoAnchorDirection(AnchorDirection.rightWithTopAligned),
|
||||||
Flexible(
|
child: Builder(builder: (providerContext) {
|
||||||
child: Padding(
|
return Center(
|
||||||
padding: const EdgeInsets.all(16.0),
|
child: ConstrainedBox(
|
||||||
child: Container(
|
constraints: const BoxConstraints.tightFor(width: 500),
|
||||||
height: 300.0,
|
child: Column(
|
||||||
decoration: BoxDecoration(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
borderRadius: BorderRadius.circular(15.0),
|
children: [
|
||||||
color: Colors.grey[200],
|
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:dartz/dartz.dart' show Tuple3;
|
import 'package:dartz/dartz.dart' show Tuple3;
|
||||||
|
import 'package:flowy_infra_ui/src/flowy_overlay/overlay_layout_delegate.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Specifies how overlay are anchored to the SourceWidget
|
/// Specifies how overlay are anchored to the SourceWidget
|
||||||
|
@ -27,7 +28,18 @@ enum AnchorDirection {
|
||||||
custom,
|
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 {
|
enum OnBackBehavior {
|
||||||
/// Won't handle the back action
|
/// Won't handle the back action
|
||||||
none,
|
none,
|
||||||
|
@ -110,14 +122,55 @@ class FlowyOverlay extends StatefulWidget {
|
||||||
class FlowyOverlayState extends State<FlowyOverlay> {
|
class FlowyOverlayState extends State<FlowyOverlay> {
|
||||||
List<Tuple3<Widget, String, FlowyOverlayDelegate?>> _overlayList = [];
|
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 Widget widget,
|
||||||
required String identifier,
|
required String identifier,
|
||||||
FlowyOverlayDelegate? delegate,
|
FlowyOverlayDelegate? delegate,
|
||||||
}) {
|
}) {
|
||||||
setState(() {
|
_showOverlay(
|
||||||
_overlayList.add(Tuple3(widget, identifier, delegate));
|
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) {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final overlays = _overlayList.map((ele) => ele.value1);
|
final overlays = _overlayList.map((ele) => ele.value1);
|
||||||
|
|
|
@ -1,43 +1,142 @@
|
||||||
// import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
// import 'dart:ui';
|
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 {
|
class OverlayLayoutDelegate extends SingleChildLayoutDelegate {
|
||||||
// OverlayLayoutDelegate({
|
OverlayLayoutDelegate({
|
||||||
// required this.route,
|
required this.anchorRect,
|
||||||
// required this.padding,
|
required this.anchorDirection,
|
||||||
// required this.anchorPosition,
|
required this.overlapBehaviour,
|
||||||
// required this.anchorDirection,
|
});
|
||||||
// });
|
|
||||||
|
|
||||||
// final OverlayPannelRoute route;
|
final Rect anchorRect;
|
||||||
// final EdgeInsets padding;
|
final AnchorDirection anchorDirection;
|
||||||
// final AnchorDirection anchorDirection;
|
final OverlapBehaviour overlapBehaviour;
|
||||||
// final Offset anchorPosition;
|
|
||||||
|
|
||||||
// @override
|
@override
|
||||||
// bool shouldRelayout(OverlayLayoutDelegate oldDelegate) {
|
bool shouldRelayout(OverlayLayoutDelegate oldDelegate) {
|
||||||
// return anchorPosition != oldDelegate.anchorPosition || anchorDirection != oldDelegate.anchorDirection;
|
return anchorRect != oldDelegate.anchorRect ||
|
||||||
// }
|
anchorDirection != oldDelegate.anchorDirection ||
|
||||||
|
overlapBehaviour != oldDelegate.overlapBehaviour;
|
||||||
|
}
|
||||||
|
|
||||||
// @override
|
@override
|
||||||
// Offset getPositionForChild(Size size, Size childSize) {
|
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
|
||||||
// // TODO: junlin - calculate child position
|
switch (overlapBehaviour) {
|
||||||
// return Offset.zero;
|
case OverlapBehaviour.none:
|
||||||
// }
|
return constraints.loosen();
|
||||||
|
case OverlapBehaviour.stretch:
|
||||||
|
// TODO: junlin - resize when overlapBehaviour == .stretch
|
||||||
|
return constraints.loosen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// @override
|
@override
|
||||||
// BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
|
Offset getPositionForChild(Size size, Size childSize) {
|
||||||
// double maxHeight = math.max(0.0, constraints.maxHeight - padding.top - padding.bottom);
|
Offset position;
|
||||||
// double width = constraints.maxWidth;
|
switch (anchorDirection) {
|
||||||
// return BoxConstraints(
|
case AnchorDirection.topLeft:
|
||||||
// minHeight: 0.0,
|
position = Offset(
|
||||||
// maxHeight: maxHeight,
|
anchorRect.left - childSize.width,
|
||||||
// minWidth: width,
|
anchorRect.top - childSize.height,
|
||||||
// maxWidth: width,
|
);
|
||||||
// );
|
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)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue