How do I get tap locations (GestureDetector)? - dart

I'm trying to implement double-tap-to-zoom in my zoomable_images plugin but the GestureTapCallback doesn't provide the tap location information.
Ideally the offset would be returned by the callback. Is there another API for this?

You can provide a GestureTapDownCallback callback as the onTapDown argument of the GestureDetector constructor. The GestureTapDownCallback takes a TapDownDetails argument that includes the global position of the tap. You can then convert it to relative coordinates using BuildContext.findRenderObject and RenderBox.globalToLocal:
Offset _tapPosition;
void _handleTapDown(TapDownDetails details) {
final RenderBox referenceBox = context.findRenderObject();
setState(() {
_tapPosition = referenceBox.globalToLocal(details.globalPosition);
});
}
#override
Widget build(BuildContext context) {
return new GestureDetector(
/* ... */
onTapDown: _handleTapDown,
);
}
In your onDoubleTap handler, you can reference _tapPosition to find out where the most recent tap was located.
For an example of this in action, see InkWell.

as of [✓] Flutter (Channel stable, 2.5.3)
GestureDetector(
onTapDown: (details) {
var position = details.globalPosition;
// you can also check out details.localPosition;
if (position.dx < MediaQuery.of(context).size.width / 2){
// tap left side
} else {
// tap rigth size
}
},
child: SomeChildWidget(),
),

If you want to handle double taps, you'll need to store the tap position from the onDoubleTapDown and then work with onDoubleTap:
late Offset _doubleTapPosition;
...
onDoubleTap: () {
//do your stuff with _doubleTapPosition here
},
onDoubleTapDown: (details) {
final RenderBox box = context.findRenderObject() as RenderBox;
_doubleTapPosition = box.globalToLocal(details.globalPosition);
},
Original answer

Related

Flutter accelerometer/gyroscope sensor lag

I've been trying to implement a gyroscope image viewer using the sensors package, however, the result seems to be very laggy. I have found a similar project on YouTube which is trying to achieve a similar goal, but as you can see in the video the animation is also very laggy.
The following code is simply outputting the data from the event, I notice how the data is being updated lags like 50ms in between updates.
Is there a way to smoothen the animation or update the data faster? Or is this a Flutter limitation?
NOTE:
I have tried --release version as suggested by other posts but the result stays the same.
import 'package:sensors/sensors.dart';
class MyGyro extends StatefulWidget {
final Widget child;
MyGyro({this.child});
#override
_MyGyroState createState() => _MyGyroState();
}
class _MyGyroState extends State<MyGyro> {
double gyroX = 0;
double gyroY = 0;
#override
void initState() {
super.initState();
gyroscopeEvents.listen((GyroscopeEvent event) {
setState(() {
gyroX = ((event.x * 100).round() / 100).clamp(-1.0, 1.0) * -1;
gyroY = ((event.y * 100).round() / 100).clamp(-1.0, 1.0);
});
});
}
#override
Widget build(BuildContext context) {
return Container(
height: 100,
width: 100,
child: Transform.translate(
offset: Offset(gyroY, 0),
child: Container(
child: Center(
child: Column(
children: [Text("X: ${gyroX}"), Text("Y: ${gyroY}"),],
),
),
),
),
);
}
}
I have found that is purely the problem of the sensors package I was using, either they have hard coded a slower interval when listening to the sensor event, or they are just using the default interval by the IOS channel.
So, I have found another package called flutter_sensors which had solved the problem. It's a very simple API to access the sensor events, but it allows you to change the interval.

Flutter: How to keep gesture events state after opening a simple dialog

I have a grid of Images and I want to open a simple dialog when I long press an Image and to be closed automatically when my finger no longer contacts with the screen (like Instagram quick image preview).
I attached LongPress event to all the images and it works fine so a dialog opens up when I long press an image however when I put my finger up nothing happens even though I attached events like onTapUp, onLongPressEnd, onPointerUp Because of the new opened dialog, All of those events are lost and no longer fires up.
I tried to add the pointer up events to the opened dialog instead but there is a catch, I must tap and release again in order to make it work because Flutter unable to recognize that my finger is already in contact with screen and the opened dialog caused flutter to forget about this fact.
You can insert an OverlayEntry into the Overlay stack by using Overlay.of(context).insert(overlayEntry).
In this overlay, you can catch gestures when required and take actions accordingly. As overlays always sit on top of anything else, the dialog will not cancel your long press gesture and you will be able to respond to longPressEnd.
You will only need to calculate which image has been pressed or use the Offset's provided by onTapDown and the position of the images.
To get the global position of your images, you can assign GlobalKey's to your images and get their global positions in the following way:
final RenderBox renderBox = globalKey.currentContext.findRenderObject() as RenderBox;
final Offset position = renderBox.localToGlobal(Offset.zero);
final Size size = renderBox.size;
To get the position of your long press, you will need to store the position of onTapDown:
onTapDown: (details) => position = details.globalPosition
Now you have everything you need to figure out which bounds the long press happened in.
I found a way to make it work. It can be done with Overlay Widget.
In the widget with GestureDetector, when onLongPress is called, create an OverlayEntry object with your dialog, and insert it into Overlay.
When onLongPressEnd is called, call the remove function of OverlayEntry object.
// Implement a function to create OverlayEntry
OverlayEntry getMyOverlayEntry({
#required BuildContext context,
SomeData someData,
}) {
return OverlayEntry(
builder: (context) {
return AlertDialog(child: SomeWidgetAgain());
}
);
}
// In the widget where you want to support long press feature
OverlayEntry myOverayEntry;
GestureDetector(
onLongPress: () {
myOverayEntry = getMyOverlayEntry(context: context, someData: someData);
Overlay.of(context).insert(myOverayEntry);
},
onLongPressEnd: (details) => myOverayEntry?.remove(),
child: SomeWidgerHere(),
)
Here's the gist on Github:
https://gist.github.com/plateaukao/79aa39854dc4eabf1220bdfa9a0334b6
You can use AnimatedContainer and put a GestureDetector inside.
change width and height using setState and it's done.
Center(
child: AnimatedContainer(
width: containerWidth,
height: containerHeight,
color: Colors.red,
duration: Duration(seconds: 1),
child: GestureDetector(
onLongPress: (){
print("Long Press");
setState(() {
containerWidth = 200;
containerHeight = 200;
});
},
onLongPressUp: (){
print("On Long Press UP");
setState(() {
containerWidth = 100;
containerHeight = 100;
});
},
),
),
)

Flutter - How do I run a method after the Slider interaction has ended?

I'm currently learning Flutter and I'm having some trouble showing a Snackbar after the interaction with the slider has ended (in other words, the final value was set when the user lifts their finger off the slider). I can't call my _showSnackBar() method in onChange because the snackbar is created and shown many times, one after the other.
Is there something I can do to call a method only after the interaction has finished? I was thinking of making a pull request and add something like onInteractionEnded callback property, but I would like to find out of there is another way first.
Here is my code for reference.
class _MySliderState extends State<MySlider> {
int _value = 2;
#override
Widget build(BuildContext context) {
return Slider(
min: 0.0,
max: 4.0,
divisions: 4,
value: (_value * 1.0),
onChanged: (double value) {
setState(() {
_value = value ~/ 1;
});
_showSnackBar();
},
);
}
void _showSnackBar() {
var snackbar = SnackBar(content: const Text('Slider value changed'));
Scaffold.of(context).showSnackBar(snackbar);
}
}
Thanks.
onChangeStart and onChangeEnd was added to Slider very recently
https://github.com/flutter/flutter/pull/17298
The change should available in master already.

Event when ScrollController get attached

I'm trying to display a widget once I have info about the max scroll extent. I can find that number if I assign an instance of ScrollController to the controller property of a scrollable widget.
My problem is that the ScrollController gets attached to the scrollable widget during the build, so I can not use the max scroll extent number before the first build. Thus what I was trying to do is display an empty Container in the first build and then switch that empty Container with the widget I actually want. Something like this:
_scrollController.positions.length == 0 ? new Container() : new Align(
alignment: Alignment.center,
child: new Container(
width: constraints.maxWidth,
height: 50.0,
color: Colors.black,
)
)
Now this does not work of course because _scrollController.positions.length will be 0 at the beginning and nowhere do I call setState when this value changes (when the controller gets attached).
So my question: Is there a place where I can get notified whenever the ScrollController gets attached to a scrollable widget? Or is there a better approach for this?
If the scrollable is widget.child.
#override
Widget build(BuildContext context) {
return new NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: widget.child,
);
}
bool _handleScrollNotification(ScrollNotification notification) {
if (notification is ScrollUpdateNotification || notification is OverscrollNotification) {
widget.child.update(notification.metrics);
}
return false;
}
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => afterFirstLayout(context));
}
void afterFirstLayout(BuildContext context) {
applyInitialScrollPosition();
}
void applyInitialScrollPosition() {
ScrollController scrollControler = widget.child.controller;
ScrollPosition position = scrollControler.position;
ScrollNotification notification = ScrollUpdateNotification(
metrics: FixedScrollMetrics(
minScrollExtent: position.minScrollExtent,
maxScrollExtent: position.maxScrollExtent,
pixels: position.pixels,
viewportDimension: position.viewportDimension,
axisDirection: position.axisDirection),
context: null,
scrollDelta: 0.0);
_handleScrollNotification(notification);
}
The child must extends ChangeNotifier and has an update method:
void update(ScrollMetrics metrics) {
assert(metrics != null);
_lastMetrics = metrics; // Save the metrics.
notifyListeners();
}
All this only works if a scroll controller has explicitly been defined for the scrollable (widget.child).

How to get offset of a current widget

I am trying to draw a widget whenever a user presses the screen.
Currently I am doing this by storing a list of widgets and when ontapup is fired on the gesture i am adding to a list of widgets.
Widget build(BuildContext context) {
Widget draw = new Text("A");
List<Widget> children = new List<Widget>();
return new Scaffold(
appBar: new AppBar(
title: const Text('Heading'),
leading: new Icon(Icons.question_answer),
),
body: new GestureDetector(
onTapUp: (details) {
setState(() {
children.add(new Positioned(
left: details.globalPosition.dx,
top: details.globalPosition.dy,
child: draw,
));
});
},
child: new Stack(children: children)
...
So my code is working I am drawing the widget when I click but my problem is that when adding the new Positioned() to stack the position is based on the screen which does not include the appbar offset. Is there a way to get the stacks initial x/y position? Or is there a way to get the appbars height? How do I get a widgets position or height/width?
Ok for anyone else who has the same issue I needed to create my own widget and use
context.findRenderObject()
and
globalToLocal()
Just FYI global to local did not work while in the one solution I needed to make it its own widget.
To get the offset of a widget, you must get the renderObject, cast it as a RenderBox, and then convert it's local position to a global position. Like this:
#override
Widget build(BuildContext context) {
RenderBox renderBox = context.findRenderObject();
Offset widgetOffset = renderBox.localToGlobal(Offset.zero);
print("X: ${widgetOffset.dx}");
print("Y: ${widgetOffset.dy}");
}
If you need the position of a child widget, you can wrap that child in a LayoutBuilder, like this:
Container(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints box) {
RenderBox renderBox = context.findRenderObject();
Offset widgetOffset = renderBox.localToGlobal(Offset.zero);
print("X: ${widgetOffset.dx}");
print("Y: ${widgetOffset.dy}");
}
)
)

Resources