feat: use custom follower

This commit is contained in:
Vincent Chan 2022-08-31 15:36:47 +08:00
parent d79a7cb194
commit 14c1959d63
3 changed files with 464 additions and 11 deletions

View File

@ -0,0 +1,84 @@
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
class PopoverCompositedTransformFollower extends CompositedTransformFollower {
const PopoverCompositedTransformFollower({
super.key,
required super.link,
super.showWhenUnlinked = true,
super.offset = Offset.zero,
super.targetAnchor = Alignment.topLeft,
super.followerAnchor = Alignment.topLeft,
super.child,
});
@override
PopoverRenderFollowerLayer createRenderObject(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return PopoverRenderFollowerLayer(
screenSize: screenSize,
link: link,
showWhenUnlinked: showWhenUnlinked,
offset: offset,
leaderAnchor: targetAnchor,
followerAnchor: followerAnchor,
);
}
@override
void updateRenderObject(
BuildContext context, PopoverRenderFollowerLayer renderObject) {
final screenSize = MediaQuery.of(context).size;
renderObject
..screenSize = screenSize
..link = link
..showWhenUnlinked = showWhenUnlinked
..offset = offset
..leaderAnchor = targetAnchor
..followerAnchor = followerAnchor;
}
}
class PopoverRenderFollowerLayer extends RenderFollowerLayer {
Size screenSize;
PopoverRenderFollowerLayer({
required super.link,
super.showWhenUnlinked = true,
super.offset = Offset.zero,
super.leaderAnchor = Alignment.topLeft,
super.followerAnchor = Alignment.topLeft,
super.child,
required this.screenSize,
});
@override
void paint(PaintingContext context, Offset offset) {
super.paint(context, offset);
final global = localToGlobal(offset);
if (link.leader == null) {
return;
}
if (link.leader!.offset.dx + link.leaderSize!.width + size.width >
screenSize.width) {
debugPrint("over flow");
}
debugPrint(
"right: ${link.leader!.offset.dx + link.leaderSize!.width + size.width}, screen with: ${screenSize.width}");
// debugPrint(
// "offset: $offset, global: $global, link: ${link.leader?.offset}, link size: ${link.leaderSize}");
// debugPrint("follower size: ${this.size}, screen size: ${this.screenSize}");
}
}
class EdgeFollowerLayer extends FollowerLayer {
EdgeFollowerLayer({
required super.link,
super.showWhenUnlinked = true,
super.unlinkedOffset = Offset.zero,
super.linkedOffset = Offset.zero,
});
}

View File

@ -0,0 +1,339 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import './popover.dart';
class PopoverLayoutDelegate extends SingleChildLayoutDelegate {
PopoverLink link;
PopoverDirection direction;
PopoverLayoutDelegate({
required this.link,
required this.direction,
});
@override
bool shouldRelayout(PopoverLayoutDelegate oldDelegate) {
if (direction != oldDelegate.direction) {
return true;
}
if (link != oldDelegate.link) {
return true;
}
if (link.leaderOffset != oldDelegate.link.leaderOffset) {
return true;
}
if (link.leaderSize != oldDelegate.link.leaderSize) {
return true;
}
return false;
}
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return constraints.loosen();
// assert(link.leaderSize != null);
// // if (link.leaderSize == null) {
// // return constraints.loosen();
// // }
// final anchorRect = Rect.fromLTWH(
// link.leaderOffset!.dx,
// link.leaderOffset!.dy,
// link.leaderSize!.width,
// link.leaderSize!.height,
// );
// BoxConstraints childConstraints;
// switch (direction) {
// case PopoverDirection.topLeft:
// childConstraints = BoxConstraints.loose(Size(
// anchorRect.left,
// anchorRect.top,
// ));
// break;
// case PopoverDirection.topRight:
// childConstraints = BoxConstraints.loose(Size(
// constraints.maxWidth - anchorRect.right,
// anchorRect.top,
// ));
// break;
// case PopoverDirection.bottomLeft:
// childConstraints = BoxConstraints.loose(Size(
// anchorRect.left,
// constraints.maxHeight - anchorRect.bottom,
// ));
// break;
// case PopoverDirection.bottomRight:
// childConstraints = BoxConstraints.loose(Size(
// constraints.maxWidth - anchorRect.right,
// constraints.maxHeight - anchorRect.bottom,
// ));
// break;
// case PopoverDirection.center:
// childConstraints = BoxConstraints.loose(Size(
// constraints.maxWidth,
// constraints.maxHeight,
// ));
// break;
// case PopoverDirection.topWithLeftAligned:
// childConstraints = BoxConstraints.loose(Size(
// constraints.maxWidth - anchorRect.left,
// anchorRect.top,
// ));
// break;
// case PopoverDirection.topWithCenterAligned:
// childConstraints = BoxConstraints.loose(Size(
// constraints.maxWidth,
// anchorRect.top,
// ));
// break;
// case PopoverDirection.topWithRightAligned:
// childConstraints = BoxConstraints.loose(Size(
// anchorRect.right,
// anchorRect.top,
// ));
// break;
// case PopoverDirection.rightWithTopAligned:
// childConstraints = BoxConstraints.loose(Size(
// constraints.maxWidth - anchorRect.right,
// constraints.maxHeight - anchorRect.top,
// ));
// break;
// case PopoverDirection.rightWithCenterAligned:
// childConstraints = BoxConstraints.loose(Size(
// constraints.maxWidth - anchorRect.right,
// constraints.maxHeight,
// ));
// break;
// case PopoverDirection.rightWithBottomAligned:
// childConstraints = BoxConstraints.loose(Size(
// constraints.maxWidth - anchorRect.right,
// anchorRect.bottom,
// ));
// break;
// case PopoverDirection.bottomWithLeftAligned:
// childConstraints = BoxConstraints.loose(Size(
// anchorRect.left,
// constraints.maxHeight - anchorRect.bottom,
// ));
// break;
// case PopoverDirection.bottomWithCenterAligned:
// childConstraints = BoxConstraints.loose(Size(
// constraints.maxWidth,
// constraints.maxHeight - anchorRect.bottom,
// ));
// break;
// case PopoverDirection.bottomWithRightAligned:
// childConstraints = BoxConstraints.loose(Size(
// anchorRect.right,
// constraints.maxHeight - anchorRect.bottom,
// ));
// break;
// case PopoverDirection.leftWithTopAligned:
// childConstraints = BoxConstraints.loose(Size(
// anchorRect.left,
// constraints.maxHeight - anchorRect.top,
// ));
// break;
// case PopoverDirection.leftWithCenterAligned:
// childConstraints = BoxConstraints.loose(Size(
// anchorRect.left,
// constraints.maxHeight,
// ));
// break;
// case PopoverDirection.leftWithBottomAligned:
// childConstraints = BoxConstraints.loose(Size(
// anchorRect.left,
// anchorRect.bottom,
// ));
// break;
// case PopoverDirection.custom:
// childConstraints = constraints.loosen();
// break;
// default:
// throw UnimplementedError();
// }
// return childConstraints;
}
@override
Offset getPositionForChild(Size size, Size childSize) {
if (link.leaderSize == null) {
return Offset.zero;
}
final anchorRect = Rect.fromLTWH(
link.leaderOffset!.dx,
link.leaderOffset!.dy,
link.leaderSize!.width,
link.leaderSize!.height,
);
Offset position;
switch (direction) {
case PopoverDirection.topLeft:
position = Offset(
anchorRect.left - childSize.width,
anchorRect.top - childSize.height,
);
break;
case PopoverDirection.topRight:
position = Offset(
anchorRect.right,
anchorRect.top - childSize.height,
);
break;
case PopoverDirection.bottomLeft:
position = Offset(
anchorRect.left - childSize.width,
anchorRect.bottom,
);
break;
case PopoverDirection.bottomRight:
position = Offset(
anchorRect.right,
anchorRect.bottom,
);
break;
case PopoverDirection.center:
position = anchorRect.center;
break;
case PopoverDirection.topWithLeftAligned:
position = Offset(
anchorRect.left,
anchorRect.top - childSize.height,
);
break;
case PopoverDirection.topWithCenterAligned:
position = Offset(
anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0,
anchorRect.top - childSize.height,
);
break;
case PopoverDirection.topWithRightAligned:
position = Offset(
anchorRect.right - childSize.width,
anchorRect.top - childSize.height,
);
break;
case PopoverDirection.rightWithTopAligned:
position = Offset(anchorRect.right, anchorRect.top);
break;
case PopoverDirection.rightWithCenterAligned:
position = Offset(
anchorRect.right,
anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0,
);
break;
case PopoverDirection.rightWithBottomAligned:
position = Offset(
anchorRect.right,
anchorRect.bottom - childSize.height,
);
break;
case PopoverDirection.bottomWithLeftAligned:
position = Offset(
anchorRect.left,
anchorRect.bottom,
);
break;
case PopoverDirection.bottomWithCenterAligned:
position = Offset(
anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0,
anchorRect.bottom,
);
break;
case PopoverDirection.bottomWithRightAligned:
position = Offset(
anchorRect.right - childSize.width,
anchorRect.bottom,
);
break;
case PopoverDirection.leftWithTopAligned:
position = Offset(
anchorRect.left - childSize.width,
anchorRect.top,
);
break;
case PopoverDirection.leftWithCenterAligned:
position = Offset(
anchorRect.left - childSize.width,
anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0,
);
break;
case PopoverDirection.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)),
);
}
}
class PopoverTarget extends SingleChildRenderObjectWidget {
final PopoverLink link;
const PopoverTarget({
super.key,
super.child,
required this.link,
});
@override
PopoverTargetRenderBox createRenderObject(BuildContext context) {
return PopoverTargetRenderBox(
link: link,
);
}
@override
void updateRenderObject(
BuildContext context, PopoverTargetRenderBox renderObject) {
renderObject.link = link;
}
}
class PopoverTargetRenderBox extends RenderProxyBox {
PopoverLink link;
PopoverTargetRenderBox({required this.link, RenderBox? child}) : super(child);
@override
bool get alwaysNeedsCompositing => true;
@override
void performLayout() {
super.performLayout();
link.leaderSize = size;
}
@override
void paint(PaintingContext context, Offset offset) {
link.leaderOffset = localToGlobal(Offset.zero);
super.paint(context, offset);
}
@override
void detach() {
link.leaderOffset = null;
link.leaderSize = null;
super.detach();
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<PopoverLink>('link', link));
}
}
class PopoverLink {
Offset? leaderOffset;
Size? leaderSize;
}

View File

@ -1,6 +1,8 @@
import 'package:appflowy_popover/layout.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import './follower.dart';
class PopoverMutex {
PopoverState? state;
@ -23,6 +25,31 @@ class PopoverTriggerActionFlags {
static int hover = 0x02;
}
enum PopoverDirection {
// Corner aligned with a corner of the SourceWidget
topLeft,
topRight,
bottomLeft,
bottomRight,
center,
// Edge aligned with a edge of the SourceWidget
topWithLeftAligned,
topWithCenterAligned,
topWithRightAligned,
rightWithTopAligned,
rightWithCenterAligned,
rightWithBottomAligned,
bottomWithLeftAligned,
bottomWithCenterAligned,
bottomWithRightAligned,
leftWithTopAligned,
leftWithCenterAligned,
leftWithBottomAligned,
custom,
}
class Popover extends StatefulWidget {
final Widget child;
final PopoverController? controller;
@ -33,6 +60,7 @@ class Popover extends StatefulWidget {
final Widget Function(BuildContext context) popupBuilder;
final int triggerActions;
final PopoverMutex? mutex;
final PopoverDirection direction;
final void Function()? onClose;
const Popover({
@ -45,6 +73,7 @@ class Popover extends StatefulWidget {
this.targetAnchor = Alignment.topLeft,
this.followerAnchor = Alignment.topLeft,
this.triggerActions = 0,
this.direction = PopoverDirection.rightWithTopAligned,
this.mutex,
this.onClose,
}) : super(key: key);
@ -54,7 +83,7 @@ class Popover extends StatefulWidget {
}
class PopoverState extends State<Popover> {
final LayerLink layerLink = LayerLink();
final PopoverLink popoverLink = PopoverLink();
OverlayEntry? _overlayEntry;
bool hasMask = true;
@ -95,14 +124,15 @@ class PopoverState extends State<Popover> {
));
}
children.add(CompositedTransformFollower(
link: layerLink,
showWhenUnlinked: false,
offset: widget.offset ?? Offset.zero,
targetAnchor: widget.targetAnchor,
followerAnchor: widget.followerAnchor,
child: widget.popupBuilder(context),
));
children.add(
CustomSingleChildLayout(
delegate: PopoverLayoutDelegate(
direction: widget.direction,
link: popoverLink,
),
child: widget.popupBuilder(context),
),
);
return Stack(children: children);
});
@ -150,8 +180,8 @@ class PopoverState extends State<Popover> {
@override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: layerLink,
return PopoverTarget(
link: popoverLink,
child: MouseRegion(
onEnter: _handleTargetPointerEnter,
child: Listener(