[infra_ui][overlay] Merge overlay anchor feature

This commit is contained in:
Jaylen Bian 2021-08-01 21:35:58 +08:00
commit d7a3e7b2ec
3 changed files with 365 additions and 66 deletions

View file

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

View file

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

View file

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