When I tap a widget, I want that widget and the widget behind it to recognize my tap at the same time. But what happens is only the widget at forefront recognizes the tap.
I've tried this in several ways, with a simple parent/child relation with all the combinations of HitTestBehavior:
Center(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => print('red'),
child: Container(
color: Colors.red,
width: 200,
height: 200,
child: Center(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => print('green'),
child: Container(
color: Colors.green,
width: 100,
height: 100,
),
),
),
),
),
);
Also tried using a Stack. Again, with all possible behaviors:
Stack(
alignment: Alignment.center,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => print('red'),
child: Container(
color: Colors.red,
width: 200,
height: 200,
),
),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => print('green'),
child: Container(
color: Colors.green,
width: 100,
height: 100,
),
),
],
);
I know I can simply use a mutual function for both of the detectors, but the main reason I want to achieve this is when using nested widgets, it is required to add unnecessary implementations to access that functionality.
You can use onPanDown
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => print('green'),//doesnt work
onPanDown: (details) => print('tdgreen'),
child: Container(
color: Colors.green,
width: 100,
height: 100,
),
),
You can check this thread for more.
I have encountered some problems with event handling when using ScrollView and Transform. The layout structure is like this, ScrollView and Transform exist inside Stack.
I want the ScrollView to scroll when scrolling outside the FlatButton in Container(Colors.cyan), event can penetrate to ScrollView.
Click FlatButton onPress to work. In fact, after clicking FlatButton twice, it will no longer move whether you click the initial position or the current position. The FlatButton control moves away from the initial position within the size range, the click event is no longer detected, but I did not understand. the code is as follows:
class EventListener extends StatefulWidget {
#override
_EventListenerState createState() => _EventListenerState();
}
class _EventListenerState extends State<EventListener> {
Offset offset = Offset(0, 0);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("EventListener"),
),
body: Stack(
children: <Widget>[
SingleChildScrollView(
child: Column(
children: <Widget>[
Container(
color: Colors.red,
height: 200,
),
Container(
color: Colors.teal,
height: 300,
),
Container(
color: Colors.orange,
height: 400,
)
],
),
),
Container(
color: Colors.cyan,
width: double.infinity,
height: 400,
alignment: Alignment.center,
child: SizedBox(
width: 100,
height: 100,
child: Transform.translate(
offset: offset,
child: FlatButton(
color: Colors.orange,
onPressed: () {
setState(() {
offset += Offset(50, 50);
});
print('click !');
},
child: Text("translate"),
),
),
),
)
],
),
);
}
}
This is a known difficulty with Buttons and Stacks and I would advise anyone with this kind of problem to look at this discussion on Github.
TL;DR:
When translating a widget, the area which you can tap is made of two things:
The area of the parent widget
The area of the child (the Flatbutton here)
See the picture below:
The usual solution:
Expanding the size of the parent.
Which gives us something like this:
Container(
width: double.infinity,
height: 400,
child: Transform.translate(
offset: offset,
child: SizedBox(
width: 100,
height: 100,
child: FlatButton(
onPressed: () => print('tap button'),
child: Text("translate"),
),
),
),
),
Here you can tap anywhere in the parent Container.
Solution for you
You actually wanted something a bit different: That anything BUT the button is clickable. For that you need:
a GestureDetector being the parent of the clickable area
a FlatButton with a onPressed method which does nothing
So here is the final code if we only want the blue container to be clickable:
import 'package:flutter/material.dart';
main() => runApp(MaterialApp(
home: EventListener(),
));
class EventListener extends StatefulWidget {
#override
_EventListenerState createState() => _EventListenerState();
}
class _EventListenerState extends State<EventListener> {
Offset offset = Offset(0, 0);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("EventListener"),
),
body: Stack(
children: <Widget>[
SingleChildScrollView(
child: Column(
children: <Widget>[
Container(
color: Colors.red,
height: 200,
),
Container(
color: Colors.teal,
height: 300,
),
Container(
color: Colors.orange,
height: 400,
)
],
),
),
GestureDetector(
onTap: () {
setState(() {
offset += Offset(50, 50);
});
},
child: Container(
width: double.infinity,
height: 400,
color: Colors.cyan,
alignment: Alignment.center,
child: Transform.translate(
offset: offset,
child: SizedBox(
width: 100,
height: 100,
child: FlatButton(
color: Colors.orange,
onPressed: () {},
child: Text("translate"),
),
),
),
),
)
],
),
);
}
}
Why this works
As explained previously, the parent being the cyan Container, any area in this container will make the button clickable.
Furthermore adding a GestureDetector on top of this Container allow us to capture any tap within this Container.
So finally here is what happens when you click, if you click:
Outside the cyan Container, nothing happens.
Inside the cyan Container
Outside the button, the GestureController catch the tap and makes the button move
Inside the button, the Button catches the tap, does nothing with it (empty method), and mark this tap as treated which causes it not to bubble up in the tree and therefore the GestureController gets nothing and nothing happens.
Hope this helps you and other understand the tricky way all of this works. Once you embrace it it's kinda beautiful though ;)
Eventough I made the things stated above, it didnt work for me. So I debuged for hours and I found a solution. I hope it helps to somebody.
If your parent widget is Column/Row/Wrap and if you translate your widget to top of another widget, for some reason gesture detector doesn't work. So, you need to remove your Column/Row/Wrap widget to make it work.
this blue button isn't clickable on red area, bottom side clickable
Example Code:
SingleChildScrollView(
controller: _scrollViewController,
child: Column(children: [
Container(
height: 500,
color: Colors.red,
),
Wrap(
children: [
FractionalTranslation(
translation: Offset(0.0, -0.5),
child: FlatButton(
child: Container(
color: Colors.blue,
height: 100,
),
onPressed: () {
print("INGA");
},
)),
],
),])
After the Wrap widget is removed, button becomes fully clickable again.
edit:
I made "I AM" bright neon green, but its mixing with the button colors to a dark green.
I want to display text over two buttons that cover the whole screen, 1 top half and 1 bottom half. I want text to display in the middle of the screen, kind of covering part of each button.
This is my code for the buttons:
Widget build(BuildContext context) {
return Scaffold(
body: Align(
alignment: Alignment.center,
child: Column(
children: <Widget>[
Container(
height: (MediaQuery.of(context).size.height) / 2,
width: MediaQuery.of(context).size.width,
child: RaisedButton(
color: Colors.black87,
child: poorText,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},
),
),
Container(
height: (MediaQuery.of(context).size.height) / 2,
width: MediaQuery.of(context).size.width,
child: RaisedButton(
color: Colors.black87,
child: richText,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ThirdRoute()),
);
},
),
),
],
),
),
);}
I have tried to use stack, but it says too many positional arguments are being used. How would I keep my column layout and layer text over the buttons? If there is another way without column that still allows makes the buttons work that's fine too. Thanks!
You can use a Stack widget to achieve that:
Note. Because the two buttons are aligned to the center, you have to align the text widget also to the center so it can be placed directly ob=ver the two button.
Check the code below:
It works perfectly:
Widget build(BuildContext context) {
return Scaffold(
// use a stack widget
body: Stack(
children: [
// text to display over buttons
Align(
// set alignment to center to place it over the buttons
alignment: Alignment.center,
child: Text(
'Text you want to display over the buttons',
// give it your style
style: TextStyle(
),
),
),
Align(
alignment: Alignment.center,
child: Column(
children: <Widget>[
Container(
height: (MediaQuery.of(context).size.height) / 2,
width: MediaQuery.of(context).size.width,
child: RaisedButton(
color: Colors.black87,
child: poorText,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},
),
),
Container(
height: (MediaQuery.of(context).size.height) / 2,
width: MediaQuery.of(context).size.width,
child: RaisedButton(
color: Colors.black87,
child: richText,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ThirdRoute()),
);
},
),
),
],
),
),
],
),
);}
I hope this helps.
Use 2 Columns widget within a Stack widget. The code below achieves exactly what you want if I understood you well.
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Column(
children: <Widget>[
Container(
height: (MediaQuery.of(context).size.height) / 2,
width: MediaQuery.of(context).size.width,
child: RaisedButton(
color: Colors.blue,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},
),
),
Container(
height: (MediaQuery.of(context).size.height) / 2,
width: MediaQuery.of(context).size.width,
child: RaisedButton(
color: Colors.red,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ThirdRoute()),
);
},
),
),
],
),
Column(
children: <Widget>[
Container(
// set alignment to center to place it over the buttons
height: (MediaQuery.of(context).size.height) / 2,
width: MediaQuery.of(context).size.width,
child: Center(child: Text('Text 1')),
),
Container(
// set alignment to center to place it over the buttons
height: (MediaQuery.of(context).size.height) / 2,
width: MediaQuery.of(context).size.width,
child: Center(child: Text('Text 2')),
),
],
)
],
);
}
Tapping the container triggers the onTap() handler but does not show any ink splash effect.
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new InkWell(
onTap: (){print("tapped");},
child: new Container(
width: 100.0,
height: 100.0,
color: Colors.orange,
),
),
),
);
}
}
I tried putting the InkWell inside the Container as well but in vain.
I think adding color to the container is covering over the ink effect
https://api.flutter.dev/flutter/material/InkWell/InkWell.html
This code seems to work
body: new Center(
child: new Container(
child: new Material(
child: new InkWell(
onTap: (){print("tapped");},
child: new Container(
width: 100.0,
height: 100.0,
),
),
color: Colors.transparent,
),
color: Colors.orange,
),
),
just click the middle square.
Edit: I found the bug report. https://github.com/flutter/flutter/issues/3782
This is actually as expected, though we should update the docs to make it clearer.
What's going on is that the Material spec says that the splashes are actually ink on the Material. So when we splash, what we do is we literally have the Material widget do the splash. If you have something on top of the Material, we splash under it, and you can't see it.
I have wanted to add a "MaterialImage" widget that conceptually prints its image into the Material as well so that then the splashes would be over the image. We could have a MaterialDecoration which does something similar for a Decoration. Or we could have Material itself take a decoration. Right now it takes a color, but we could extend that to taking a whole decoration. It's not clear whether it's really material-spec-compatible to have a material with a gradient, though, so I'm not sure whether we should do that.
In the short run, if you just need a workaround, you can put a Material on top of the container, with the material set to use the "transparency" type, and then put the ink well inside that.
--hixie
Update: Hixie merged a new Ink solution last year. The Ink provides a convenient way to splash over images.
testWidgets('Does the Ink widget render anything', (WidgetTester tester) async {
await tester.pumpWidget(
new Material(
child: new Center(
child: new Ink(
color: Colors.blue,
width: 200.0,
height: 200.0,
child: new InkWell(
splashColor: Colors.green,
onTap: () { },
),
),
),
),
);
Material(
color: Colors.grey[800],
child: Center(
child: Ink.image(
image: AssetImage('cat.jpeg'),
fit: BoxFit.cover,
width: 300.0,
height: 200.0,
child: InkWell(
onTap: () { /* ... */ },
child: Align(
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Text('KITTEN', style: TextStyle(fontWeight: FontWeight.w900, color: Colors.white)),
),
)
),
),
),
)
Please Note: I did not test the new Ink Widget. I coped the code from ink_paint_test.dart and the Ink class docs
https://github.com/Hixie/flutter/blob/1f6531984984f52328e66c0cd500a8d517964564/packages/flutter/test/material/ink_paint_test.dart
https://github.com/flutter/flutter/pull/13900
https://api.flutter.dev/flutter/material/Ink-class.html
Screenshot:
Use Ink widget wrapped in an InkWell.
InkWell(
onTap: () {}, // Handle your onTap
child: Ink(
width: 200,
height: 200,
color: Colors.blue,
),
)
The InkWell widget must have a Material widget as an ancestor otherwise it can't show effects. E.g.:
Material(
child : InkWell(
child : .....
you have to add onTap method to see the actual effects as like
Buttons {RaisedButton,FlatButton etc}.
e.g -> Material(
child : InkWell(
onTap : (){}
child : .....
Let's come to the main points see some examples below and try to understand
the actual concepts of InkWell.
In Below example Material is parent of InkWell with onTap but it still not working. Please try to understand the concept of it. You should provide some margin to container or other widget to show the effects. Actually below code working fine but we can't see because we did not provide any margin or align so it has no space to show the effects.
Widget build(BuildContext context) {
return Center(
child: Material(
child: new InkWell(
onTap: () {
print("tapped");
},
child: new Container(
width: 100.0,
height: 100.0,
color: Colors.orange,
),
),
),
);
}
Below example show InkWell effects only to upwards because we provide space {margin}.
Widget build(BuildContext context) {
return Center(
child: Material(
child: new InkWell(
onTap: () {
print("tapped");
},
child: new Container(
margin: EdgeInsets.only(top: 100.0),
width: 100.0,
height: 100.0,
color: Colors.orange,
),
),
),
);
}
Below exp. show the effects in all the page because center create margin from all the side. Centre align it's child widget from top, left, right and bottom.
Widget build(BuildContext context) {
return Center(
child: Material(
child: new InkWell(
onTap: () {
print("tapped");
},
child: Center(
child: new Container(
width: 100.0,
height: 100.0,
color: Colors.orange,
),
),
),
),
);
}
InkWell() will never show the ripple effect until you add the
onTap : () {}
or any of the callbacks like onDoubleTap, onLongPress etc.
parameter inside the InkWell as it starts listening to your taps only when you specify this parameter.
I have found this solution for me. I think it can help you:
Material(
color: Theme.of(context).primaryColor,
child: InkWell(
splashColor: Theme.of(context).primaryColorLight,
child: Container(
height: 100,
),
onTap: () {},
),
)
Color is given to Material widget. It is the default color of your widget.
You can adjust color of ripple effect using splashColor property of Inkwell.
A better way is to use the Ink widget instead of any other widget.
Instead of defining color inside container you can define it in Ink widget itself.
Below code will work.
Ink(
color: Colors.orange,
child: InkWell(
child: Container(
width: 100,
height: 100,
),
onTap: () {},
),
)
Do not forget to add a onTap: () {} in the InkWell else it will
not show the ripple effect too.
If you want a Ripple Effect on any widget just:
1- Wrap widget with Material and Inkwell.
2- set color to widget from Material.
3- Never set color to the widget you want it to ripple.
Material (
color: const Color(0xcffFF8906),
child: InkWell(
ontap:() { },
child: Container(
color: Colors.transparent,
height: 80,
width: 80,
)
Quick solution in easy words:
This solution works in any place of the widget tree.
Just add additional Material before InkWell, like so:
return Container(
.......code.......
),
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () {},
child: Container(
.......code.......
),
),
),
);
Reference:
https://api.flutter.dev/flutter/material/InkWell-class.html
Section called "The ink splashes aren't visible!"
Wrap the InkWell with Material and use there the color that you need:
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: Material(
color: Colors.orange,
child: new InkWell(
onTap: (){ print("tapped"); },
child: new Container(
width: 100.0,
height: 100.0,
),
),
),
),
);
}
}
This is the best way I found and use it always. You can try it.
Wrap your Widget with InkWell
Wrap InkWell with Material
Set the opacity 0% anyhow. e.g.: color: Colors.white.withOpacity(0.0),
Material(
color: Colors.white.withOpacity(0.0),
child: InkWell(
child: Container(width: 100, height: 100),
onTap: (){print("Wow! Ripple");},
),
)
This is working for me:
Material(
color: Colors.white.withOpacity(0.0),
child: InkWell(
splashColor: Colors.orange,
child: Text('Hello'), // actually here it's a Container wrapping an image
onTap: () {
print('Click');
},
));
After trying many answers here, it was a combination of:
Setting splashColor
Wrapping InkWell in Material(color: Colors.white.withOpacity(0.0), ..)
Thanks to the answers here that make those 2 points
child: Material(
color: Colors.transparent
child: InkWell(
onTap: (){ print("this Ripple!";},
splashColor: Colors.greenAccent,
child: Container(
height:100.0,
color: Colors.white,
),
),
),
Add Material before InkWell and set color to Colors.transparent.
I ran into this same problem trying to create an alternating color of InkWell's in a ListView. In that particular case, there's a simple solution: wrap the alternates in a Container that uses a mostly transparent tint/brightness change -- the InkWell touch animation will still be visible beneath it. No Material needed. Note there are other issues when trying to work around this with a Materal -- e.g., it will override a DefaultTextStyle you're using with the default (it installs an AnimatedDefaultTextStyle) which is a huge pain.
After you added onTap:(){} listener, ripple effect should work fine. It doesn't work if you use BoxShadow() with in the InkWell() widget.
I was able to make my situation work by using a Stack.
Stack(
children: [
MyCustomWidget(), // <--- Put this on bottom
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {},
child: Ink(
width: 100,
height: 100,
),
),
),
],
),
The reason that the other answers on this page weren't working for me is that my custom widget hid the ink effect and I didn't have a plain image (so I couldn't use Ink.image).
Edit:
You still may be able to use Ink.image if you convert the image to the right format.
now using MaterialButton, in newer version flutter
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: MaterialButton(
child: Text(
labelText,
style: TextStyle(fontSize: 22),
),
onPressed: () {},
color: backgroundColor,
height: 45,
minWidth: double.infinity,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(16),
),
),
),
);
}
For handling ripple effect you should use the below rules:
onTap shouldn't be null (or empty).
InkWell must be inside any of the Material Widgets
Single Line:
Use splashColor widget in an InkWell.
InkWell(
onTap: () {}, // Handle your onTap
splashColor: Colors.grey,
child: Container(
width: 200,
height: 200,
),
)
You might have set the properties in theme or parent of the material
ThemeData(
...
splashFactory: NoSplash.splashFactory,
...
)
if you don't want splash effect for specific use it explicitly
InkWell(
splashFactory: NoSplash.splashFactory,
...
child: Row(
...
)
);
Make sure you have not added the below properties in the material theme:
(In my case this was the issue)
splashColor: Colors.transparent
splashFactory: NoSplash.splashFactory
eg:-
MaterialApp(
home:......,
theme: ThemeData(
splashFactory: NoSplash.splashFactory, // Remove this
splashColor: AppColors.transparent, // Remove this OR Change the color
......
),
)
Create a widget that supports tap.
Wrap it in an InkWell widget to manage tap callbacks and ripple
animations.
that's point 2 mentioned in documentation
example:
InkWell(
child: TextButton(
onPressed: () {},
child: Text('Tapped'),
),
),
),
I was hit by a similar problem adding an Inkwell to an existing complex Widget with a Container wrapping a BoxDecoration with a color. By adding the Material and Inkwell in the way suggested the Inkwell was still obscured by the BoxDecoration so I just made the BoxDecoration's color slightly opaque which allowed the Inkwell to be seen
The solution to circular widgets, do like below:
Material(
color: Colors.transparent,
child: Container(
alignment: Alignment.center,
height: 100,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
enableFeedback: true,
iconSize: 40,
icon: Icon(
Icons.skip_previous,
color: Colors.white,
size: 40,
),
onPressed: () {}),
IconButton(
iconSize: 100,
enableFeedback: true,
splashColor: Colors.grey,
icon: Icon(Icons.play_circle_filled, color: Colors.white, size: 100),
padding: EdgeInsets.all(0),
onPressed: () {},
),
IconButton(
enableFeedback: true,
iconSize: 40,
icon: Icon(
Icons.skip_next,
color: Colors.white,
size: 40,
),
onPressed: () {}),
],
),
),
)
For me it was another problem. InkWell was not showing ripple effect when i had onTap function as parameter of my widget defined as below.
Function(Result) onTapItem;
...
onTap: onTapItem(result),
I don't know what's the difference, but next code is working well.
onTap: (){ onTapItem(result); },
Adding transparent color to Material worked in my case:
child: new Material(
color: Colors.transparent
child: new InkWell(
onTap: (){},
child: new Container(
width: 100.0,
height: 100.0,
color: Colors.amber,
),
),
),
Instead of providing the color for the Container widget, provide color for the Material widget.
Material(
color: Colors.orange,
child: InkWell(
onTap: () {
print("tapped");
},
child: Container(
width: 100.0,
height: 100.0,
),
),
)