How to receive both onTapDown (eagerly) and onDoubleTap? - flutter

I'd like to perform some interaction:
when an initial touch (onTapDown) happens (e.g. clear existing selections), AND
when a double tap happens (onDoubleTap) happens (e.g. highlight a new selection)
Using a GestureDetector:
if only register a onTapDown, the callback is called immediately at the first touch
if I register both onTapDown and onDoubleTap, and the user performs a simple tap, it takes some considerable time until the onTapDown event is called (1/2 a second?). If I update the display following the tap, for the user, this feels like rendering jank -- but it in fact just getting the event to late.
Is there a way to eagerly receive onTapDown AND onDoubleTap? (Its OK, in fact preferred, if I get 2 events on a double tap).
import 'package:flutter/material.dart';
// 1. double tap the green box
// 2. single tap the green box again and that it takes a long
// time to update the text
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) => MaterialApp(
title: 'Flutter Playground',
home: Material(child: SafeArea(child: content())));
Widget content() {
final controller = TextEditingController(text: "-");
return Center(
child: Column(children: [
GestureDetector(
onTapDown: (_) {
controller.text = "tap down";
// print("[${DateTime.now().millisecondsSinceEpoch}] tap down");
},
onDoubleTap: () {
controller.text = "double tap ";
// print("[${DateTime.now().millisecondsSinceEpoch}] double tap");
},
child: Container(
color: Colors.green,
width: 200,
height: 200,
child: Center(child: Text("Tap me")))),
Container(
color: Colors.red,
width: 200,
height: 200,
child: Center(child: TextField(controller: controller)))
]));
}
}
main() => runApp(MyApp());

A hacky way to do it I suppose, but you can use a Listener for onPointerDown. The code below should log tap down twice when you double-tap:
import 'package:flutter/material.dart';
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) => MaterialApp(title: 'Flutter Playground', home: Material(child: SafeArea(child: content())));
Widget content() {
final controller = TextEditingController(text: "-");
return Center(
child: Column(
children: [
GestureDetector(
onDoubleTap: () {
controller.text = "double tap";
print("double tap");
},
child: Listener(
onPointerDown: (_) {
controller.text = "tap down";
print("tap down");
},
child: Container(
color: Colors.green,
width: 200,
height: 200,
child: Center(
child: Text("Tap me"),
),
),
)),
Container(
color: Colors.red,
width: 200,
height: 200,
child: Center(
child: TextField(controller: controller),
),
)
],
),
);
}
}
main() => runApp(MyApp());

Use gesture_x_detector plugin.
XGestureDetector(
bypassTapEventOnDoubleTap: true,
doubleTapTimeConsider: 170
child: ...
)

Related

Use onPan GestureDetector inside a SingleChildScrollView

Problem
So I have a GestureDetector widget inside SingleChildScrollView (The scroll view is to accommodate smaller screens). The GestureDetector is listening for pan updates.
When the SingleChildScrollView is "in use" the GestureDetector cannot receive pan updates as the "dragging" input from the user is forwarded to the SingleChildScrollView.
What I want
Make the child GestureDetector have priority over the SingleChildScrollView when dragging on top of the GestureDetector -- but still have functionality of scrolling SingleChildScrollView outside GestureDetector.
Example
If you copy/paste this code into dart pad you can see what I mean. When the gradient container is large the SingleChildScrollView is not active -- you are able to drag the blue box and see the updates in the console. However, once you press the switch button the container becomes smaller and the SingleChildScrollView becomes active. You are now no longer able to get pan updates in the console only able to scroll the container.
Sidenote: It seems that if you drag on the blue box quickly you are able to get drag updates but slowly dragging it just scrolls the container. I'm not sure if that's a bug or a feature but I'm not able to reproduce the same result in my production app.
import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
const MyWidget({Key? key}) : super(key: key);
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
bool enabled = false;
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: enabled ? 200 : 400,
child: SingleChildScrollView(
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment(0.8, 0.0),
colors: <Color>[Color(0xffee0000), Color(0xffeeee00)],
tileMode: TileMode.repeated,
),
),
height: 400,
width: 200,
child: Center(
child: GestureDetector(
onPanUpdate: (details) => print(details),
child: Container(
height: 100,
width: 100,
color: Colors.blue,
child: Center(
child: Text(
"Drag\nme",
textAlign: TextAlign.center,
),
),
),
),
),
),
),
),
ElevatedButton(
onPressed: () => setState(() {
enabled = !enabled;
}),
child: Text("Switch"))
],
);
}
}
As #PatrickMahomes said this answer (by #Chris) will solve the problem. However, it will only check if the drag is in line with the GestureDetector. So a full solution would be this:
bool _dragOverMap = false;
GlobalKey _pointerKey = new GlobalKey();
_checkDrag(Offset position, bool up) {
if (!up) {
// find your widget
RenderBox box = _pointerKey.currentContext.findRenderObject();
//get offset
Offset boxOffset = box.localToGlobal(Offset.zero);
// check if your pointerdown event is inside the widget (you could do the same for the width, in this case I just used the height)
if (position.dy > boxOffset.dy &&
position.dy < boxOffset.dy + box.size.height) {
// check x dimension aswell
if (position.dx > boxOffset.dx &&
position.dx < boxOffset.dx + box.size.width) {
setState(() {
_dragOverMap = true;
});
}
}
} else {
setState(() {
_dragOverMap = false;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Scroll Test"),
),
body: new Listener(
onPointerUp: (ev) {
_checkDrag(ev.position, true);
},
onPointerDown: (ev) {
_checkDrag(ev.position, false);
},
child: ListView(
// if dragging over your widget, disable scroll, otherwise allow scrolling
physics:
_dragOverMap ? NeverScrollableScrollPhysics() : ScrollPhysics(),
children: [
ListTile(title: Text("Tile to scroll")),
Divider(),
ListTile(title: Text("Tile to scroll")),
Divider(),
ListTile(title: Text("Tile to scroll")),
Divider(),
// Your widget that you want to prevent to scroll the Listview
Container(
key: _pointerKey, // key for finding the widget
height: 300,
width: double.infinity,
child: FlutterMap(
// ... just as example, could be anything, in your case use the color picker widget
),
),
],
),
),
);
}
}

RefreshIndicator not working with SingleChildScrollView as child

I am trying to use RefreshIndicator to reload a list I show on my home screen. The code looks similar to this:
class Home extends StatefulWidget {
#override
_StartHomeState createState() => _StartHomeState();
}
class _StartHomeState extends State<Home> {
EventsList events;
#override
void initState() {
super.initState();
events = EventsList();
}
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomPadding: false,
body: RefreshIndicator(
onRefresh: resfreshEventList,
child: SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Column(
children: [
HomeTopBar(),
events,
],
),
),
),
);
}
Future<Null> resfreshEventList() async {
await Future.delayed(Duration(seconds: 2));
setState(() {
events = EventsList();
});
return null;
}
}
EventsList is another stateful widget that will call an API and map the response to a list of widgets. I have tried setting the physics property of the SingleChildScrollView as mentioned here: https://github.com/flutter/flutter/issues/22180 but no luck. Using ListView instead of the SingleChildScrollView doesn't work either.
It seems to be working fine in this example When I pull to refresh then resfreshEventList gets fired and also setState is working without any problem.
Here is the code which I am using:
import 'package:flutter/material.dart';
class Home extends StatefulWidget {
#override
_StartHomeState createState() => _StartHomeState();
}
class _StartHomeState extends State<Home> {
// EventsList events;
int number = 0;
#override
// void initState() {
// super.initState();
// events = EventsList();
// }
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("RefreshIndicator Example"),
),
resizeToAvoidBottomPadding: false,
body: RefreshIndicator(
onRefresh: resfreshEventList,
child: SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Column(
children: [
// HomeTopBar(),
// events,
Container(
height: 200,
width: 200,
color: Colors.red,
child: Center(
child: Text(number.toString()),
),
),
Divider(),
Container(
height: 200,
width: 200,
color: Colors.red,
child: Center(
child: Text(number.toString()),
),
),
Divider(),
Container(
height: 200,
width: 200,
color: Colors.red,
child: Center(
child: Text(number.toString()),
),
),
Divider(),
Container(
height: 200,
width: 200,
color: Colors.red,
child: Center(
child: Text(number.toString()),
),
),
Divider(),
Container(
height: 200,
width: 200,
color: Colors.red,
child: Center(
child: Text(number.toString()),
),
)
],
),
),
),
));
}
Future<Null> resfreshEventList() async {
// await Future.delayed(Duration(seconds: 2));
// setState(() {
// events = EventsList();
// });
setState(() {
number = number + 1;
});
print("Refresh Pressed");
return null;
}
}
Output:

How to get rid of overflow-error on AnimatedContainer?

I implemented a MaterialBanner. I created a slide-up-effect once the user pushes the dismiss-button. Everything works ok, except for the overflow-error 'bottom overflowed by .. pixels', which appears when you click the dismiss button. The number of pixels in the error message counts down to zero as the bottom slides up. How can I solve this last issue? I expected the MaterialBanner to respect the maxHeight of the BoxConstraint instead of overflowing.
AnimatedContainer buildAnimatedBanner(AuthViewModel vm) {
return AnimatedContainer(
constraints: BoxConstraints(maxHeight: _heightBanner),
duration: Duration(seconds: 3),
child: MaterialBanner(
backgroundColor: Colors.green,
leading: Icon(EvilIcons.bell,
size: 28, color: AppTheme.appTheme().colorScheme.onBackground),
content: Text('Please check your inbox to verify email ${vm.email}'),
actions: <Widget>[
FlatButton(
child: Text(
"Send again",
style: TextStyle(
color: AppTheme.appTheme().colorScheme.onBackground),
),
onPressed: () {},
),
FlatButton(
child: Text(
"Dismiss",
style: TextStyle(
color: AppTheme.appTheme().colorScheme.onBackground),
),
onPressed: () => setState(() => _heightBanner = 0),
),
],
),
);
}
It's overflowing because while the container is reducing in height, the content of the banner is still being rendered in full. You'll still see this error for as long as the material banner is still in view.
Looking at your code, I'm thinking the purpose of the AnimatedContainer is to make a smooth transition when the height of the child (MaterialBanner) changes.
You can use an AnimatedSize instead. It'll automatically handle the size change transitions for you and you don't have to worry about Overflow error.
It also provides an alignment param you can use to determine the direction of the transition.
Below is a code. Demo can be found in this codepen.
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: MyApp(),
),
);
}
class MyApp extends StatefulWidget {
MyWidget createState() => MyWidget();
}
class MyWidget extends State<MyApp> with SingleTickerProviderStateMixin {
int itemCount = 2;
bool pressed = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: AnimatedSize(
vsync: this,
duration: Duration(milliseconds: 500),
alignment: Alignment.topCenter,
child: Container(
color: Colors.red,
// height: 300,
width: 300,
child: ListView.builder(
itemCount: itemCount,
shrinkWrap: true,
itemBuilder: (context, index) {
return ListTile(
title: Text("$index"),
);
}
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState((){
itemCount = pressed ? 6 : 4;
pressed = !pressed;
});
}
),
);
}
}

Button OnPressed is not working in Flutter

I have issue with the Raised button click. The method inside OnPressed() is not getting called. Ideally on the OnPressed() method i would like to have the pop up or a slider shown. I have created the example to show the problem currently faced.
The main.dart file calls Screen2()
import 'package:flutter/material.dart';
//import 'package:flutter_app/main1.dart';
import 'screen2.dart';
void main() => runApp(Lesson1());
class Lesson1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Screen2(),
);
}
}
and in Screen2()i have just have a RaisedButton and OnPressed() it needs to call the function ButtonPressed().
import 'package:flutter/material.dart';
class Screen2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.blue,
title: Text('Screen 2'),
),
body: Center(
child: RaisedButton(
color: Colors.blue,
child: Text('Go Back To Screen 1'),
onPressed: () {
print('centrebutton');
ButtonPressed();
},
),
),
);
}
}
class ButtonPressed extends StatefulWidget {
#override
_ButtonPressedState createState() => _ButtonPressedState();
}
class _ButtonPressedState extends State<ButtonPressed> {
#override
Widget build(BuildContext context) {
print ('inside button press');
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.red,
title: Text('Screen 3'),
),
// create a popup to show some msg.
);
}
}
On clicking the Raised button the print statement ('centerbutton') gets printed.
But the ButtonPressed() method is not getting called .
I am not able to see the print msg('inside button press') in the console. Pl. let me what could be the reason for ButtonPressed method not getting called. Attached the snapshot for your reference.
You are calling a Widget on your RaisedButton's onPressed method. Your widget is getting called but will not render anywhere in the screen.
You should call a function for processing your data in a tap event. But you are calling a widget or say a UI view.
If you want to navigate to the respective screen then you should use navigator.
For ex :
onPressed: () {
print('centrebutton');
Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) => ButtonPressed()));
},
This could be a cause: If you have an asset as a child of your button, and that asset does not exist, the button onpressed will not work.
Solution: remove the asset.
Example:
return RaisedButton(
splashColor: Colors.grey,
onPressed: () {
},
child: Padding(
padding: const EdgeInsets.fromLTRB(0, 10, 0, 10),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Image(image: AssetImage("assets/google_logo.png"), height: 35.0), //remove this line
Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
'Sign in with Google',
style: TextStyle(
fontSize: 20,
color: Colors.grey,
),
),
)
],
),
),
);

Custom image in a button in Flutter

I'm trying to create a button which will do some action when pressed, suppose it calls a function _pressedButton() also if the user taps on it (or taps and hold), the image changes.
Consider this like a button with a custom picture on pressed.
In the follow-up, can I also change the image based on some external factor?
eg. if some function A returns true, show image 1, else show image 2
You can learn all about buttons and state in the Flutter interactivity tutorial.
For example, here is a button that shows a different cat every time each time it is clicked.
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
home: new HomePage(),
));
}
class HomePage extends StatefulWidget {
HomePageState createState() => new HomePageState();
}
class HomePageState extends State<HomePage> {
String _url = getNewCatUrl();
static String getNewCatUrl() {
return 'http://thecatapi.com/api/images/get?format=src&type=jpg&size=small'
'#${new DateTime.now().millisecondsSinceEpoch}';
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Cat Button'),
),
body: new Center(
child: new FloatingActionButton(
onPressed: () {
setState(() {
_url = getNewCatUrl();
});
},
child: new ConstrainedBox(
constraints: new BoxConstraints.expand(),
child: new Image.network(_url, fit: BoxFit.cover, gaplessPlayback: true),
),
),
),
);
}
}
A handy way to achieve what you want is by using GestureDetector()
GestureDetector(
onTap: () {
//desired action command
},
child: Container(
alignment: Alignment.center,
width: 100,
height: 100,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Image.asset('assets/icons/user.png'),
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50),
color: Colors.teal,
),
),
),
Result:
GestureDetector() basically mimics a button.