Reputation: 2643
I am working on an app that requires a custom overlay that is positioned based on the click co-ordinates. The overlay should then fill the rest of the space from the position. The fill space direction bases on the distance from the click point to the top or bottom the direction with the larger distance is used and should close on click outside it's surface.
I have looked at the Overlay Widget however i don't seem to understand how i can use it to achieve the described property.
So far i have created a StatefulWidget which contains the Overlay description.
class ScriptureDisplay extends StatefulWidget {
@override
_ScriptureDisplayState createState() => _ScriptureDisplayState();
}
class _ScriptureDisplayState extends State<ScriptureDisplay> {
OverlayEntry _overlayEntry;
OverlayEntry _createOverlayEntry() {
RenderBox renderBox = context.findRenderObject();
var size = renderBox.size;
var offset = renderBox.localToGlobal(Offset.zero);
return OverlayEntry(
builder: (context) => Positioned(
left: offset.dx,
top: offset.dy + size.height + 5.0,
width: size.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
nip(),
body(context, offset.dy),
],
),
));
}
@override
Widget build(BuildContext context) {
return RaisedButton(
child: Text("Click Me"),
onPressed: (){
this._overlayEntry = this._createOverlayEntry();
Overlay.of(context).insert(this._overlayEntry);
},
);
}
Widget body(BuildContext context, double offset) {
return Material(
borderRadius: BorderRadius.all(
Radius.circular(8.0),
),
// color: this.color,
elevation: 4.0,
child: Container(
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ListView(
padding: EdgeInsets.zero,
shrinkWrap: true,
children: ["First", "Second", "Third"]
.map((String s) => ListTile(
subtitle: Text(s),
))
.toList(growable: false),
),
),
);
}
Widget nip() {
return FittedBox(
child: Container(
height: 20.0,
width: 20.0,
margin: EdgeInsets.only(left:10),
child: CustomPaint(
painter: OpenPainter(),
),
),
);
}
}
So how do i pickup and calculate current click point relative to the entire screen so i can make the decision for the Overlay direction.
Upvotes: 3
Views: 11098
Reputation: 1715
OverlayEntry
is indeed a good choice to achieve what you mentioned. You could wrap your widget, which should create this overlay on for example a long press, with an GestureRecognizer
. A lot of callbacks can be used with this widget which also contain the position like LongPressStartDetails
. There you can calculate the position and size of the OverlayEntry
based on globalPosition
and the device size (MediaQuery.of(context).size
).
Tip: to remove the OverlayEntry
you have to call remove()
on the OverlayEntry
widget itself! So keep a reference to it.
Detailed Answer
After some patching together of responses from the comments, I came up with the following solution.
I used a Stack Widget with an initial container of maximum width and height wrapped in a GestureDetector to detect any clicks outside the actual widget. Clicking this Container would call the _overlayEntry.remove();
to close the Overlay.
I then compared the distance of the Tap Widget with the Screen Center to determine if the Overlay should be above it or below it.
I finally used a Positioned Widget with top calculated to befit the selected Overlay position. Finally i calculated the Overlay body height to be the distance from the tap to the screen ends.
enum OVERLAY_POSITION { TOP, BOTTOM }
class ScriptureDisplay extends StatefulWidget {
@override
_ScriptureDisplayState createState() => _ScriptureDisplayState();
}
class _ScriptureDisplayState extends State<ScriptureDisplay> {
TapDownDetails _tapDownDetails;
OverlayEntry _overlayEntry;
OVERLAY_POSITION _overlayPosition;
double _statusBarHeight;
double _toolBarHeight;
OverlayEntry _createOverlayEntry() {
RenderBox renderBox = context.findRenderObject();
var size = renderBox.size;
var offset = renderBox.localToGlobal(Offset.zero);
var globalOffset = renderBox.localToGlobal(_tapDownDetails.globalPosition);
_statusBarHeight = MediaQuery.of(context).padding.top;
// TODO: Calculate ToolBar Height Using MediaQuery
_toolBarHeight = 50;
var screenHeight = MediaQuery.of(context).size.height;
var remainingScreenHeight = screenHeight - _statusBarHeight - _toolBarHeight;
if (globalOffset.dy > remainingScreenHeight / 2) {
_overlayPosition = OVERLAY_POSITION.TOP;
} else {
_overlayPosition = OVERLAY_POSITION.BOTTOM;
}
return OverlayEntry(builder: (context) {
return Stack(
children: <Widget>[
GestureDetector(
onTap: () {
_overlayEntry.remove();
},
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Colors.blueGrey.withOpacity(0.1),
),
),
Positioned(
left: 10,
top: _overlayPosition == OVERLAY_POSITION.TOP
? _statusBarHeight + _toolBarHeight
: offset.dy + size.height - 5.0,
width: MediaQuery.of(context).size.width - 20,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// ignore: sdk_version_ui_as_code
if (_overlayPosition == OVERLAY_POSITION.BOTTOM)
nip(),
body(context, offset.dy),
// ignore: sdk_version_ui_as_code
if (_overlayPosition == OVERLAY_POSITION.TOP)
nip(),
],
),
)
],
);
});
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(top: 400, left: 10, right: 100),
child: GestureDetector(
child: Text("C"),
onTapDown: (TapDownDetails tapDown) {
setState(() {
_tapDownDetails = tapDown;
});
this._overlayEntry = this._createOverlayEntry();
Overlay.of(context).insert(this._overlayEntry);
},
),
);
}
Widget body(BuildContext context, double offset) {
return Material(
borderRadius: BorderRadius.all(
Radius.circular(8.0),
),
elevation: 4.0,
child: Container(
width: MediaQuery.of(context).size.width,
height: _overlayPosition == OVERLAY_POSITION.BOTTOM
? MediaQuery.of(context).size.height -
_tapDownDetails.globalPosition.dy -
20
: _tapDownDetails.globalPosition.dy -
_toolBarHeight -
_statusBarHeight -
15,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ListView(
padding: EdgeInsets.zero,
shrinkWrap: true,
children: [
"First",
"Second",
"Third",
"First",
"Second",
"Third",
"First",
"Second",
"Third"
]
.map((String s) => ListTile(
subtitle: Text(s),
))
.toList(growable: false),
),
),
);
}
Widget nip() {
return Container(
height: 10.0,
width: 10.0,
margin: EdgeInsets.only(left: _tapDownDetails.globalPosition.dx),
child: CustomPaint(
painter: OpenPainter(_overlayPosition),
),
);
}
}
class OpenPainter extends CustomPainter {
final OVERLAY_POSITION overlayPosition;
OpenPainter(this.overlayPosition);
@override
void paint(Canvas canvas, Size size) {
switch (overlayPosition) {
case OVERLAY_POSITION.TOP:
var paint = Paint()
..style = PaintingStyle.fill
..color = Colors.white
..isAntiAlias = true;
_drawThreeShape(canvas,
first: Offset(0, 0),
second: Offset(20, 0),
third: Offset(10, 15),
size: size,
paint: paint);
break;
case OVERLAY_POSITION.BOTTOM:
var paint = Paint()
..style = PaintingStyle.fill
..color = Colors.white
..isAntiAlias = true;
_drawThreeShape(canvas,
first: Offset(15, 0),
second: Offset(0, 20),
third: Offset(30, 20),
size: size,
paint: paint);
break;
}
canvas.save();
canvas.restore();
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
void _drawThreeShape(Canvas canvas,
{Offset first, Offset second, Offset third, Size size, paint}) {
var path1 = Path()
..moveTo(first.dx, first.dy)
..lineTo(second.dx, second.dy)
..lineTo(third.dx, third.dy);
canvas.drawPath(path1, paint);
}
void _drawTwoShape(Canvas canvas,
{Offset first, Offset second, Size size, paint}) {
var path1 = Path()
..moveTo(first.dx, first.dy)
..lineTo(second.dx, second.dy);
canvas.drawPath(path1, paint);
}
}
Upvotes: 6