Draggable feedback widget gets an offset when InteractiveViewer is zoomed in - flutter

I'm trying to Drag a widget on top of an InteractiveViewer.
The code works fine when the scale is 1.0.
However, if zoomed in, when I drag the circle:
the feedback widget is offset by a few pixels to the bottom right
when I let go, the circle moves up
Why does that happen?
Here's a demo illustrating what I'm talking about:
Below is the code of this app
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
enum _Action { scale, pan }
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
const _defaultMarkerSize = 48.0;
class _MyHomePageState extends State<MyHomePage> {
Offset _pos = Offset.zero; // Position could go from -1 to 1 in both directions
_Action action; // pan or pinch, useful to know if we need to scale down pin
final _transformationController = TransformationController();
#override
Widget build(BuildContext context) {
final scale = _transformationController.value.getMaxScaleOnAxis();
final size = _defaultMarkerSize / scale;
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: InteractiveViewer(
transformationController: _transformationController,
maxScale: 5,
minScale: 1,
child: Stack(
children: [
Center(child: Image.asset('image/board.png')),
DraggablePin(
pos: _pos,
size: size,
onDragEnd: (details) {
final matrix = Matrix4.inverted(_transformationController.value);
final height = AppBar().preferredSize.height;
final sceneY = details.offset.dy - height;
final viewportPoint = MatrixUtils.transformPoint(
matrix,
Offset(details.offset.dx, sceneY) + Offset(_defaultMarkerSize / 2, _defaultMarkerSize / 2),
);
final screenSize = MediaQuery.of(context).size;
final x = viewportPoint.dx * 2 / screenSize.width - 1;
final y = viewportPoint.dy * 2 / screenSize.height - 1;
setState(() {
_pos = Offset(x, y);
});
},
),
],
),
onInteractionStart: (details) {
// No need to call setState as we don't need to rebuild
action = null;
},
onInteractionUpdate: (details) {
if (action == null) {
if (details.scale == 1)
action = _Action.pan;
else
action = _Action.scale;
}
if (action == _Action.scale) {
// Need to resize the pins so that they keep the same size
setState(() {});
}
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.restore),
onPressed: () {
setState(() {
_pos = Offset.zero;
});
},
),
);
}
}
class DraggablePin extends StatelessWidget {
final Offset pos;
final double size;
final void Function(DraggableDetails) onDragEnd;
const DraggablePin({this.pos, this.size, this.onDragEnd, Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final offset = size / 2;
Widget pinWidget = Pin(size);
final screenSize = MediaQuery.of(context).size;
final height = screenSize.height - AppBar().preferredSize.height;
pinWidget = Draggable(
child: pinWidget,
feedback: Pin(_defaultMarkerSize),
childWhenDragging: Container(),
onDragEnd: onDragEnd,
);
return Positioned(
top: pos.dy * height / 2 + height / 2 - offset,
left: pos.dx * screenSize.width / 2 + screenSize.width / 2 - offset,
child: pinWidget,
);
}
}
class Pin extends StatelessWidget {
const Pin(this.size, {Key key}) : super(key: key);
final double size;
#override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: Center(
child: Ink(
decoration: ShapeDecoration(
color: Colors.green,
shape: const CircleBorder(),
),
child: IconButton(
constraints: BoxConstraints.tightFor(width: size, height: size),
padding: const EdgeInsets.all(0),
iconSize: size,
splashRadius: size / 2,
splashColor: Colors.white,
icon: Container(),
onPressed: () {},
),
),
),
);
}
}

Related

HOW TO CREATE NUMBERING WITH BOX DECORATION LIKE THIS IN FLUTTER

HOW TO CREATE NUMBERING WITH BOX DECORATION LIKE THIS IN FLUTTER
You need create a CustomPaint widget.
You can see this video for underestand how work it:
https://www.youtube.com/watch?v=kp14Y4uHpHs
Not exactly try the below code using this package.
import 'package:flutter/material.dart';
import 'dart:math' as math;
class StarClipper extends CustomClipper<Path> {
StarClipper(this.numberOfPoints);
/// The number of points of the star
final int numberOfPoints ;
#override
Path getClip(Size size) {
double width = size.width;
print(width);
double halfWidth = width / 2;
double bigRadius = halfWidth;
double radius = halfWidth / 1.3;
double degreesPerStep = _degToRad(360 / numberOfPoints);
double halfDegreesPerStep = degreesPerStep / 2;
var path = Path();
double max = 2 * math.pi;
path.moveTo(width, halfWidth);
for (double step = 0; step < max; step += degreesPerStep) {
path.lineTo(halfWidth + bigRadius * math.cos(step),
halfWidth + bigRadius * math.sin(step));
path.lineTo(halfWidth + radius * math.cos(step + halfDegreesPerStep),
halfWidth + radius * math.sin(step + halfDegreesPerStep));
}
path.close();
return path;
}
num _degToRad(num deg) => deg * (math.pi / 180.0);
#override
bool shouldReclip(CustomClipper<Path> oldClipper) {
StarClipper oldie = oldClipper as StarClipper;
return numberOfPoints != oldie.numberOfPoints;
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
height: 200,
width: 200,
child: ClipPath(
clipper: StarClipper(8),
child: Container(
decoration: BoxDecoration(
color: Colors.blue,
),
height: 150,
child: Center(child: Text("+1", style: TextStyle(fontSize: 50),)),
),
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
the output is the below image.

Flutter hero animation between widgets not screens

Hero animation is the best for navigating between screen, but I need same animation between widgets. Like one card moving another place for example: Product Card moves to shoppingcart and something else. Thanks for answers!
Try this one, add_to_cart_animation:
import 'package:add_to_cart_animation/add_to_cart_animation.dart';
import 'package:add_to_cart_animation/add_to_cart_icon.dart';
import 'package:flutter/material.dart';
import 'list_item.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Add To Cart Animation',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Add To Cart Animation'),
debugShowCheckedModeBanner: false,
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// We can detech the location of the card by this GlobalKey<CartIconKey>
GlobalKey<CartIconKey> gkCart = GlobalKey<CartIconKey>();
late Function(GlobalKey) runAddToCardAnimation;
var _cartQuantityItems = 0;
#override
Widget build(BuildContext context) {
return AddToCartAnimation(
// To send the library the location of the Cart icon
gkCart: gkCart,
rotation: true,
dragToCardCurve: Curves.easeIn,
dragToCardDuration: const Duration(milliseconds: 1000),
previewCurve: Curves.linearToEaseOut,
previewDuration: const Duration(milliseconds: 500),
previewHeight: 30,
previewWidth: 30,
opacity: 0.85,
initiaJump: false,
receiveCreateAddToCardAnimationMethod: (addToCardAnimationMethod) {
// You can run the animation by addToCardAnimationMethod, just pass trough the the global key of the image as parameter
this.runAddToCardAnimation = addToCardAnimationMethod;
},
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
centerTitle: false,
actions: [
// Improvement/Suggestion 4.4 -> Adding 'clear-cart-button'
IconButton(
icon: Icon(Icons.cleaning_services),
onPressed: () {
_cartQuantityItems = 0;
gkCart.currentState!.runClearCartAnimation();
},
),
SizedBox(width: 16),
AddToCartIcon(
key: gkCart,
icon: Icon(Icons.shopping_cart),
colorBadge: Colors.red,
),
SizedBox(
width: 16,
)
],
),
body: ListView(
children: [
AppListItem(onClick: listClick, index: 1),
AppListItem(onClick: listClick, index: 2),
AppListItem(onClick: listClick, index: 3),
AppListItem(onClick: listClick, index: 4),
AppListItem(onClick: listClick, index: 5),
AppListItem(onClick: listClick, index: 6),
AppListItem(onClick: listClick, index: 7),
],
),
),
);
}
// Improvement/Suggestion 4.4 -> Running AddTOCartAnimation BEFORE runCArtAnimation
void listClick(GlobalKey gkImageContainer) async {
await runAddToCardAnimation(gkImageContainer);
await gkCart.currentState!.runCartAnimation((++_cartQuantityItems).toString());
}
}
OR
[not null safety]
this is a sample of add to cart, add_cart_parabola:
import 'dart:ui';
import 'package:add_cart_parabola/add_cart_parabola.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
GlobalKey floatKey = GlobalKey();
GlobalKey rootKey = GlobalKey();
Offset floatOffset ;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_){
RenderBox renderBox = floatKey.currentContext.findRenderObject();
floatOffset = renderBox.localToGlobal(Offset.zero);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
key: rootKey,
width: double.infinity,
height: double.infinity,
color: Colors.grey,
child: ListView(
children: List.generate(40, (index){
return generateItem(index);
}).toList(),
),
),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.yellow,
key: floatKey,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
Widget generateItem(int index){
Text text = Text("item $index",style: TextStyle(fontSize:
25),);
Offset temp;
return GestureDetector(
onPanDown: (details){
temp = new Offset(details.globalPosition.dx, details.globalPosition
.dy);
},
onTap: (){
Function callback ;
setState(() {
OverlayEntry entry = OverlayEntry(
builder: (ctx){
return ParabolaAnimateWidget(rootKey,temp,floatOffset,
Icon(Icons.cancel,color: Colors.greenAccent,),callback,);
}
);
callback = (status){
if(status == AnimationStatus.completed){
entry?.remove();
}
};
Overlay.of(rootKey.currentContext).insert(entry);
});
},
child: Container(
color: Colors.orange,
child: text,
),
);
}
}
For animating widget in the same screen you can use AnimatedPositioned widget see the below code
import 'dart:math';
import 'package:flutter/material.dart';
class AnimatedPositionedDemo extends StatefulWidget {
const AnimatedPositionedDemo({Key? key}) : super(key: key);
static String routeName = 'animated_positioned';
#override
_AnimatedPositionedDemoState createState() => _AnimatedPositionedDemoState();
}
class _AnimatedPositionedDemoState extends State<AnimatedPositionedDemo> {
late double topPosition;
late double leftPosition;
double generateTopPosition(double top) => Random().nextDouble() * top;
double generateLeftPosition(double left) => Random().nextDouble() * left;
#override
void initState() {
super.initState();
topPosition = generateTopPosition(30);
leftPosition = generateLeftPosition(30);
}
void changePosition(double top, double left) {
setState(() {
topPosition = generateTopPosition(top);
leftPosition = generateLeftPosition(left);
});
}
#override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final appBar = AppBar(title: const Text('AnimatedPositioned'));
final topPadding = MediaQuery.of(context).padding.top;
// AnimatedPositioned animates changes to a widget's position within a Stack
return Scaffold(
appBar: appBar,
body: SizedBox(
height: size.height,
width: size.width,
child: Stack(
children: [
AnimatedPositioned(
top: topPosition,
left: leftPosition,
duration: const Duration(seconds: 1),
child: InkWell(
onTap: () => changePosition(
size.height -
(appBar.preferredSize.height + topPadding + 50),
size.width - 150),
child: Container(
alignment: Alignment.center,
width: 150,
height: 50,
child: Text(
'Click Me',
style: TextStyle(
color:
Theme.of(context).buttonTheme.colorScheme!.onPrimary,
),
),
color: Theme.of(context).primaryColor,
),
),
),
],
),
),
);
}
}
I hope it works for you
For Animated widgets, flutter team has provided a video on youtube here
And you can read all about them on their website here

Stack position not accurate

I want to add red blinking dot on the container when it is tapped, but the dot position is not accurate.
How to fix?
MyApp
import 'package:flutter/material.dart';
import 'blinking_dot.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
double posx;
double posy;
void onTapDown(BuildContext context, TapDownDetails details) {
print('${details.globalPosition}');
final RenderBox box = context.findRenderObject();
final Offset localOffset = box.globalToLocal(details.globalPosition);
setState(() {
posx = localOffset.dx;
posy = localOffset.dy;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: GestureDetector(
onTapDown: (TapDownDetails details) => onTapDown(context, details),
child: Stack(
children: <Widget>[
Container(
height: double.infinity,width: double.infinity,
padding: EdgeInsets.all(10),
child: Image.asset("assets/img.jpg")),
Positioned(
child: BlinkingDot(),
left: posx,
top: posy,
)
],
)));
}
}
blinking_dot.dart
import 'package:flutter/material.dart';
class BlinkingDot extends StatefulWidget {
#override
_BlinkingDotState createState() => _BlinkingDotState();
}
class _BlinkingDotState extends State<BlinkingDot>
with SingleTickerProviderStateMixin {
AnimationController _animationController;
#override
void initState() {
_animationController =
new AnimationController(vsync: this, duration: Duration(seconds: 1));
_animationController.repeat();
super.initState();
}
#override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _animationController,
child: Container(
height: 15,
width: 15,
child: FloatingActionButton(
backgroundColor: Colors.redAccent,
)));
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
Output
posy = localOffset.dy- MediaQuery.of(context).padding.top - kToolbarHeight;
also you need to decrease offset by half of the red dot size
in your case if will something like this
posx = localOffset.dx - 7.5;
posy = localOffset.dy- MediaQuery.of(context).padding.top - kToolbarHeight - 7.5;
Are you looking like this?
Home page
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
double posx;
double posy;
void onTapDown(BuildContext context, TapDownDetails details) {
print('${details.globalPosition}');
final RenderBox box = context.findRenderObject();
final Offset localOffset = box.globalToLocal(details.globalPosition);
setState(() {
posx = localOffset.dx;
posy = localOffset.dy-70.0;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("widget.title"),
),
body: GestureDetector(
onTapDown: (TapDownDetails details) => onTapDown(context, details),
child: Stack(
children: <Widget>[
Container(
height: double.infinity,
width: double.infinity,
padding: EdgeInsets.all(10),
child: Image.asset("assets/img.jpg")),
Positioned(
child: BlinkingDot(),
left: posx,
top: posy,
)
],
),
));
}
}
BlinkingDot page
class BlinkingDot extends StatefulWidget {
#override
_BlinkingDotState createState() => _BlinkingDotState();
}
class _BlinkingDotState extends State<BlinkingDot>
with SingleTickerProviderStateMixin {
AnimationController _animationController;
#override
void initState() {
_animationController =
new AnimationController(vsync: this, duration: Duration(seconds: 1));
_animationController.repeat();
super.initState();
}
#override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _animationController,
child: Container(
height: 15,
width: 15,
child: FloatingActionButton(
onPressed: () {},
backgroundColor: Colors.redAccent,
)));
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
It's because you did not consider the following
You need to subtract AppBar height from dy.
You need to subtract the circle radius from both dx and dy.
You need to subtract the top padding from dy and left padding from
dx.
Do the following to get the expected result
posx = localOffset.dx - MediaQuery.of(context).padding.left - circleRadius;
posy = localOffset.dy -MediaQuery.of(context).padding.top - circleRadius - kToolbarHeight;
Here is the complete snippet
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
//int _counter = 0;
double posx;
double posy;
final circleRadius = 7.5;
void onTapDown(BuildContext context, TapDownDetails details) {
print('${details.globalPosition}');
final RenderBox box = context.findRenderObject();
final Offset localOffset = box.globalToLocal(details.globalPosition);
setState(() {
posx =
localOffset.dx - MediaQuery.of(context).padding.left - circleRadius;
posy = localOffset.dy -MediaQuery.of(context).padding.top - circleRadius - kToolbarHeight;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: GestureDetector(
onTapDown: (TapDownDetails details) => onTapDown(context, details),
child: Stack(
children: <Widget>[
Container(
height: double.infinity,
width: double.infinity,
padding: EdgeInsets.all(10),
child: Image.asset("assets/img.jpg")),
Positioned(
child: BlinkingDot(circleRadius: circleRadius),
left: posx,
top: posy,
)
],
)));
}
}
class BlinkingDot extends StatefulWidget {
final double circleRadius;
const BlinkingDot({Key key, this.circleRadius}) : super(key: key);
#override
_BlinkingDotState createState() => _BlinkingDotState();
}
class _BlinkingDotState extends State<BlinkingDot>
with SingleTickerProviderStateMixin {
AnimationController _animationController;
#override
void initState() {
_animationController =
new AnimationController(vsync: this, duration: Duration(seconds: 1));
_animationController.repeat();
super.initState();
}
#override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _animationController,
child: Container(
height: widget.circleRadius * 2,
width: widget.circleRadius * 2,
child: FloatingActionButton(
onPressed: () {},
backgroundColor: Colors.redAccent,
)));
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
See the live demo here.

Flutter manage dx of Offset by swiping to right or left

in Flutter application i want to divide width of screen to 10 part and when user swipe to right or left i could detect each part of this swipe, for example
after divide screen to 10 part i have a variable named screenParts as double which that has 0.0 by default, when user swipe to right the variable value should be plus 1 part and swipe to left should be minus the variable value minus
this variable value should be between 0.0 and 1, you could consider Tween<double>
double screenParts = 0.0;
final double screenWidth = MediaQuery.of(context).size.width / 10;
i want to use this value inside into this part of code:
return GestureDetector(
onPanUpdate: (details) {
if (details.delta.dx > 0) {
if (screenParts / screenWidth == 0) screenParts = screenParts += 0.1;
} else if (details.delta.dx < 0) {
if (screenParts / screenWidth == 0) screenParts = screenParts -= 0.1;
}
setState(() {});
},
child: SafeArea(
child: FadeTransition(
opacity: CurvedAnimation(parent: animation, curve: Curves.fastLinearToSlowEaseIn),
child: SlideTransition(
position: Tween<Offset>(
begin: animateDirection,
end: Offset(screenParts, 0), //<---- this part
).animate(CurvedAnimation(
parent: animation,
curve: Curves.fastLinearToSlowEaseIn,
)),
// ignore: void_checks
child: Material(elevation: 8.0, child: child)),
),
),
);
it means i try to manage dx of Offset by screenParts value with swiping to right or left
is this what you are looking for?
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
body: SafeArea(
child: MyHomePage(),
),
),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
double position = 0.0;
#override
void initState() {
super.initState();
}
void handleXChange(double deltaX) {
setState(() {
position = deltaX / MediaQuery.of(context).size.width;
});
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Text(position.toString()),
DragArea(
handleXChange: handleXChange,
),
],
);
}
}
class DragArea extends StatefulWidget {
const DragArea({Key key, #required this.handleXChange}) : super(key: key);
final void Function(double newX) handleXChange;
#override
_DragAreaState createState() => _DragAreaState();
}
class _DragAreaState extends State<DragArea> {
double initX;
void onPanStart(DragStartDetails details) {
initX = details.globalPosition.dx;
}
void onPanUpdate(DragUpdateDetails details) {
var x = details.globalPosition.dx;
var deltaX = x - initX;
widget.handleXChange(deltaX);
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onPanStart: onPanStart,
onPanUpdate: onPanUpdate,
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.blueAccent,
),
),
),
);
}
}

Flutter PopupMenuButton onLongPressed

I'm trying to show a menu context on a custom widget I created when it is long pressed(on tap has another behaviour).
I tried to use GestureDetector with onLongPress and use the function showMenu but it shows the menu in the corner, not over the widget pressed. I've seen a workaround to get the position of the widget and pass it to the showMenu but it looks messy to me.
return new GestureDetector(
child: _defaultBuild(),
onTap: onTap,
onLongPress: () {
showMenu(
items: <PopupMenuEntry>[
PopupMenuItem(
//value: this._index,
child: Row(
children: <Widget>[
Text("Context item1")
],
),
)
],
context: context,
position: _getPosition(context)
);
}
);
RelativeRect _getPosition(BuildContext context) {
final RenderBox bar = context.findRenderObject();
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
final RelativeRect position = RelativeRect.fromRect(
Rect.fromPoints(
bar.localToGlobal(bar.size.bottomRight(Offset.zero), ancestor: overlay),
bar.localToGlobal(bar.size.bottomRight(Offset.zero), ancestor: overlay),
),
Offset.zero & overlay.size,
);
return position;
}
I've tried also to use PopupMenuButton but I wasn't able to show the menu onLongPressed.
Any ideas?
showMenu() works well on my end. It looks like the issue has something to do with how the menu is being positioned with RelativeRect. Instead of RelativeRect.fromRect(), I used RelativeRect.fromSize() on mine.
RelativeRect _getRelativeRect(GlobalKey key){
return RelativeRect.fromSize(
_getWidgetGlobalRect(key), const Size(200, 200));
}
Rect _getWidgetGlobalRect(GlobalKey key) {
final RenderBox renderBox =
key.currentContext!.findRenderObject() as RenderBox;
var offset = renderBox.localToGlobal(Offset.zero);
debugPrint('Widget position: ${offset.dx} ${offset.dy}');
return Rect.fromLTWH(offset.dx / 3.1, offset.dy * 1.05,
renderBox.size.width, renderBox.size.height);
}
Here's a complete sample that you can try.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final widgetKey = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: GestureDetector(
key: widgetKey,
child: Container(
height: 60,
width: 120,
color: Colors.lightBlueAccent,
child: const Center(child: Text('Show Menu')),
),
onLongPress: () {
showMenu(
items: <PopupMenuEntry>[
PopupMenuItem(
//value: this._index,
child: Row(
children: const [Text("Context item 1")],
),
)
],
context: context,
position: _getRelativeRect(widgetKey),
);
},
),
),
);
}
RelativeRect _getRelativeRect(GlobalKey key){
return RelativeRect.fromSize(
_getWidgetGlobalRect(key), const Size(200, 200));
}
Rect _getWidgetGlobalRect(GlobalKey key) {
final RenderBox renderBox =
key.currentContext!.findRenderObject() as RenderBox;
var offset = renderBox.localToGlobal(Offset.zero);
debugPrint('Widget position: ${offset.dx} ${offset.dy}');
return Rect.fromLTWH(offset.dx / 3.1, offset.dy * 1.05,
renderBox.size.width, renderBox.size.height);
}
}