InkWell and GestureDetector, how to make them work? - flutter

I'd like to use a GestureDetector for it's onTapDown callback but also have a nice InkWell splash effect.
Is it possible to use these two together?

If you want to unconditionally handle the pointer down event with no gesture disambiguation, you can make the InkWell a child of a Listener, and set the onPointerDown handler.
For example:
new Listener(
onPointerDown: (e) { print('onPointerDown'); },
child: new InkWell(
child: new Text('Tap me'),
onTap: () { print('onTap'); }
),
),
It might make sense to add an onTapDown handler to InkWell.

You can pass a HitTestBehavior to GestureDetector to make it "non-blocking" by setting the value to "translucent"
Or you can also trigger it yourself using Material.of(context)

InkWell uses a GestureDetector under it's hood:
https://github.com/flutter/flutter/blob/79b5e5bc8af7d9df3374dfe6141653848d1c03ac/packages/flutter/lib/src/material/ink_well.dart#L560
The reason why a GestureDetector isn't able to work well if the InkWell-Widget is, that it sets the behavior on it's internal GestureDetector to "HitTestBehavior.opaque". This prevents "event bubbling" / event capturing on parent widgets (if my understanding is correct). And because the "behavior" is final, we can't change / fix that by our own.
https://github.com/flutter/flutter/blob/79b5e5bc8af7d9df3374dfe6141653848d1c03ac/packages/flutter/lib/src/material/ink_well.dart#L566
As I mentioned in a comment above, I wrapped the InkWell within a widget (which handles other stuff too) and provided a way to pass a callback into it which is executed on the desired event.
This is my example solution:
import 'package:flutter/material.dart';
class CardPreview extends StatefulWidget {
final dynamic data;
final VoidCallback onTap;
const CardPreview(this.data, this.onTap);
CardPreview._(this.data, this.onTap);
factory CardPreview.fromData(dynamic data, VoidCallback onTap) {
return new CardPreview(data, onTap);
}
CardPreview createState() => new CardPreview(this.data, this.onTap);
}
class CardPreviewState extends State<CardPreview> {
final dynamic data;
final VoidCallback onTap;
CardPreviewState(this.data, this.onTap);
Widget buildCard() {
return Material(
color: Colors.transparent,
child: Ink(
child: InkWell(
child: null,
onTap: () {
if (this.onTap == null) {
return;
}
this.onTap();
},
),
),
);
}
#override
Widget build(BuildContext context) {
return Container(
child: buildCard(),
);
}
}

Building on RĂ©mi's suggestion of adding a HitTestBehavior.translucent behavior to your GestureDetector, this is how I would solve your issue:
Material(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTapDown: (details) {
//Do something
},
child: InkWell(
onTap: () {},
child: Container(height: 20.0, width: 20.0, color: Colors.red),
),
),
),
I was able to use something similar to essentially add a onLongPressStart and onLongPressEnd to my InkWell.

InkWell has onTapDown callback now
https://api.flutter.dev/flutter/material/InkResponse/onTapDown.html
https://api.flutter.dev/flutter/material/InkWell/InkWell.html
But, GestureDetector still has more functionality than InkWell, e.g. onSecondaryTap. Thus nesting InkWell and GestureDetector is useful today.
HitTestBehavior.opaque has no impact on child-parent relationship
https://github.com/flutter/flutter/issues/18450#issuecomment-397372078
https://github.com/flutter/flutter/issues/74733#issuecomment-767859584
In short, HitTestBehavior.opaque only prevent sibling behind it from receiving events by return true in HitTest so that its direct parent won't pass on the event to its next child(in reversed order) and return true in HitTest.(Thus, sibling behind its ancestors won't receive events as well. Just like that its ancestors' behaviors are overwritten to opaque.) BUT its ancestors always DO receive events!
Remark: If A is inside B(i.e. A is a descendant of B), then B will never be behind A.
Related source:
https://api.flutter.dev/flutter/rendering/RenderBox/hitTest.html
https://api.flutter.dev/flutter/rendering/RenderProxyBoxWithHitTestBehavior/hitTest.html (RenderProxyBoxWithHitTestBehavior is used by _RenderColoredBox, which is the "color" of Container)
https://api.flutter.dev/flutter/rendering/RenderProxyBoxWithHitTestBehavior/hitTestSelf.html
https://api.flutter.dev/flutter/rendering/RenderBoxContainerDefaultsMixin/defaultHitTestChildren.html
https://api.flutter.dev/flutter/rendering/RenderStack/hitTestChildren.html (Use defaultHitTestChildren)
Acknowledgment: heavily refer to https://github.com/flutter/flutter/issues/18450#issuecomment-601865975
For onTap, only the callback of the "winner" can fire
See https://api.flutter.dev/flutter/widgets/GestureDetector-class.html
Setting GestureDetector.behavior to HitTestBehavior.opaque or HitTestBehavior.translucent has no impact on parent-child relationships: both GestureDetectors send a GestureRecognizer into the gesture arena, only one wins.
Some callbacks (e.g. onTapDown) can fire before a recognizer wins the arena, and others (e.g. onTapCancel) fire even when it loses the arena. Therefore, the parent detector in the example above may call some of its callbacks even though it loses in the arena.
This is tricky:
If winner has onTap or onTapDown, then loser's onTap will never fire, and onTapDown won't fire if the tap is too short such that gesture arena sweeping happens before onTapDown firing.
InkWell has onTap and onTapDown
https://github.com/flutter/flutter/blob/468166c713984941c0e5432b7499a1a05a0a0f61/packages/flutter/lib/src/material/ink_well.dart#L1209-L1219
Thus, to achieve best experience, all primary tap callback should be registered on InkWell. Otherwise, if GestureDetector wins, short tap won't trigger splash, else if InkWell wins, short tap won't trigger Gesture's onTapDown.

Related

Extending a Flutter widget with a GestureDetector

I'm trying to update my Drawer to a NavigationDrawer that uses NavigationDrawerDestination widgets. However, I'm very unhappy with the fact that NavigationDrawerDestinaion's do not have a callback function. The onDestinationSelected function only takes the index of the destination, and nothing else, for handling taps. In my Drawer this is cumbersome as for some users the amount and order of destinations varies.
I cannot wrap the NavigationDrawerDestination in a GestureDetector directly as Flutter expects the parent of a NavigationDrawerDestination to be a NavigationDrawer. Thus, I tried to extend the widget by creating a SidebarDestination like so:
class SidebarDestination extends NavigationDrawerDestination {
const SidebarDestination({
super.key,
required super.icon,
required super.label,
super.selectedIcon,
required this.onTap,
});
final void Function() onTap;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: super.build(context),
);
}
}
A complete code example is available in this DartPad. This no longer raises errors from the framework. However, the GestureDetector in my extension widget does not fire: tapping the destination does not print "This is not printed!" in the console.
How do I correctly implement this?
There are other cases like this tap event win for the inner event.
You can use onPanDown on this case to overcome this situation.
return GestureDetector(
onPanDown:(_)=> onTap(),

Flutter: Programatically perform a button click

How can you programatically perform a button click in Flutter? I basically want to bind a number of buttons to keys on the keyboard, the most important part is that after a key is pressed the corresponding button the visual state of the button is triggered to inform the user which button was pressed.
I am aware you can invoke the onTap function, but that by itself doesn't update the visual state of button. Is there a way to simulate this kind of behavior, I tried to look into MaterialStateController but doesn't feel like it's the right approach.
I was going to say that you could use the MaterialStateController to simulate that the button has been pressed.
This can be done by updating the state controller in some good enough fashion of your choice. Something like this perhaps:
Non-optimized code for this (you will bind to keys on the keyboard instead):
ElevatedButton(
onPressed: () async {
button2StatesController.update(MaterialState.pressed, true);
await Future.delayed(const Duration(milliseconds: 200));
button2StatesController.update(MaterialState.pressed, false);
},
child: const Text('Button 1'),
),
ElevatedButton(
onPressed: () {},
statesController: button2StatesController,
child: const Text('Button 2'),
)
You cannot simulate the InkWell effect (as far as I know) without more intervention. Like creating your own button with your own inkwell animations etc...
I discovered that _InkWellResponseState (internal-) classes have a function called simulateTap. The function is bound against ActivateIntent and by default this intend is fired whenever the user presses Enter or Space
class SimActivateIntent extends ActivateIntent {
const SimActivateIntent (this.model);
final FocusNode model;
}
class SimActivateAction extends Action<SimActivateIntent> {
SimActivateAction ();
#override
void invoke(covariant SimActivateIntentintent) {
Actions.maybeInvoke(intent.model.context!, const ActivateIntent());
}
}
With the intends listed above one can invoke the ActivateIntent on a specific object and simulate a button press. They can bound into the application using Shortcuts & Actions classes.
Map<ShortcutActivator, Intent> get defaultShortcuts {
return <ShortcutActivator, Intent>{
const SingleActivator(LogicalKeyboardKey.backspace):
SimActivateIntent(digitBackspace),
}
}
#override
Widget build(BuildContext context)
{
return Shortcuts(
shortcuts: defaultShortcuts,
child: Actions(
actions: <Type, Action<Intent>>{
SimActivateIntent: SimActivateAction(),
},
child: ...
));
}

Flutter PointerEvent vs OnTap, one works, the other doesn't

I have a stack widget I am trying to create in which:
1. The user touches the widget button triggering a pointerDown event.
2. Pointer down causes a slider type widget to scale from 0 to 100% from behind the button
3. With finger still down, the user drags to select a value on the scale
4. The value is selected by releasing the finger from the screen i.e. pointerUp.
The widget works fine when I use onTap instead of pointerDown in step 1. But when I try to use a pointer down event, the _open method (that manages the scaleUp of the slider) isn't triggered.
I have followed this example almost exactly: https://fireship.io/lessons/flutter-radial-menu-staggered-animations/, but have tried to change the touch event on the floatingActionbuton like this:
Transform.scale(
scale: widget.scale.value,
child: Listener(
onPointerDown: (PointerDownEvent event) {
print('pointer is down');
setState(() {
_open;
});
},
child: FloatingActionButton(child: Icon(Icons.blur_circular), onPressed: () {})),
)
The print part detects and fires the event, but the _open method does not do anything and the menu part does not appear like in the tutorial link.
I am at a loss.
Since the button below the FloatingActionButton also has a listener, The Listener widget doesn't get's the PointerDown event. So to do that you have to change the behaviour to opaque so that both get the events.
Try Something like this:
Listener(
behavior: HitTestBehavior.opaque,
onPointerDown: (PointerDownEvent details){
},
child: ...,
)

HOW CAN I ADD onTap method for custom listview menu

I have an horizontal listview and i want to add click listener for all items which is in listview where should i use onTap method?
this is food_cart.dart
This is food category.dart
app screenshot
You can either use GestureDetector or InkWell. GestureDetector is useful to provide many other gestures like onHorizontalDragDown, onVerticalDragDown etc. While Inkwell is useful to provide ripple effect for the child widget.
InkWell(
onTap: () {
//Perform your logic here
}
child:YOUR_LISTVIEW_ITEM,
)
OR
GestureDetector(
onTap: () {
//Perform your logic here
}
child:YOUR_LISTVIEW_ITEM,
)
Wrap your FoodCard inside an Inkwell,
InkWell(
onTap: () {
//navigate to screen or show a dialog or do anything
Navigator.pushNamed(
context, '/PostDetailsScreen', arguments: mFeedData);
}
child:YOUR_FOOD_CARD,
)

Flutter: how to access context from Dismissible onDismissed

I'm trying to implement undo for a Dismissible list item in Flutter, and having problems accessing a BuildContext.
I have a flutter list, where each item is a card. The card is wrapped in a Dismissible, which allows the user to swipe to dismiss the card. Dismissible automatically removes the item from the list. Dismissible also has an onDismissed event - I'm using this event to update the item in Redux state store (setting an isDismissed flag to true), then show a snackBar which contains an UNDO button.
This is where I'm running into problems. I want the UNDO button to restore the item, by dispatching another action to the Redux store to set isDismissed to false. To do this I need a context, from which to get the store dispatcher. However when I try with the below code, I get an error when clicking on UNDO:
Looking up a deactivated widget's ancestor is unsafe
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard(this.product);
#override
Widget build(BuildContext context) {
return Dismissible(
key: Key(product.id.toString()),
onDismissed: (direction) {
StoreProvider.of<AppState>(context).dispatch(DismissAction(product));
// Then show a snackbar to allow undo
Scaffold.of(context).showSnackBar(
SnackBar(
content: Row(
children: <Widget>[
Expanded(child: Text("Dismissed ${product.title}"),),
FlatButton(
onPressed: () {
// THIS IS WHERE I GET THE ERROR
StoreProvider.of<AppState>(context).dispatch(UndoDismissAction(product));
},
child: Text("UNDO"),
)
],
)
)
);
},
child: Card(
child: ...
)
);
}
}
From what I've read, I think what is going on is that the line StoreProvider.of<AppState>(context) inside the undo button's onPressed action is trying to use a context which belongs to the Card, but because the card has been removed from the list, it no longer exists.
I'm not sure how to do work around this. I've read about flutter keys, and think the answer may be to start passing around some kind of global key, but I can't quite get my head around how that works. I gave it a go and ran into another problem with 'inheritFromWidgetOfExactType' was called on null. Are keys the solution to this problem? If so where do I create the key, do I pass it in to the widget, what type of key should I use etc, or is there a better solution?
Many thanks!
Extract a single copy of the store into a local variable, which will then get captured by all the lambdas below.
#override
Widget build(BuildContext context) {
var store = StoreProvider.of<AppState>(context);
return Dismissible(
...
store.dispatch(DismissAction(product));