Propagate click behind a widget - flutter

I have a Stack with two widgets inside.
I'm trying to detect the click on the bottom widget of the Stack, which is behind the top one.
I am using HitTestBehavior.translucent, but it only work if the GestureDetector doesn't have any child.
This is a simplified version of what i need in my app. I have a Stack which contains many tiny Cards on the screen, and there's one canvas on top of them all. Despite the canvas, I need to be able to tap on the Cards to change its content. I thought using a translucent behavior would solve my problem but it does not.
EDIT : Also, the first GestureDetector will always be in a Positioned widget.
class TestPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SizedBox(
height: 800,
width: 400,
child: Stack(
children: [
/// 2. ... and trigger the onTap function of this widget
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
print('TAP BOTTOM');
},
child: Container(
height: 500,
width: 400,
color: Colors.deepOrange,
),
),
/// 1. I'm Trying to clic here...
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: null,
child: Container(
height: 300,
width: 400,
color: Colors.deepPurple,
),
),
],
),
),
// ),
),
);
}
}

I have a sample code with which you can achieve this:
​class TestPage extends StatelessWidget {
  #override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: SizedBox(
          height: 800,
          width: 400,
          child: Stack(
            children: [
              /// 2. ... and trigger the onTap function of this widget (WIDGET_2)
              GestureDetector(
                behavior: HitTestBehavior.opaque,
                onTap: () {
                  print('TAP BOTTOM');
                },
                child: Container(
                  height: 500,
                  width: 400,
                  color: Colors.deepOrange,
                ),
              ),
              /// Able to tap bottom
              IgnorePointer(
                ignoring: true,
                child: Container(
                  height: 300,
                  width: 400,
                  color: Colors.deepPurple,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
Also sorry for posting late here.

The answer was given by VrajGohil on this issue :
https://github.com/flutter/flutter/issues/77596
Here is his solution :
class TestPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SizedBox(
height: 800,
width: 400,
child: Stack(
children: [
/// 2. ... and trigger the onTap function of this widget (WIDGET_2)
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
print('TAP BOTTOM');
},
child: Container(
height: 500,
width: 400,
color: Colors.deepOrange,
),
),
/// Able to tap bottom
IgnorePointer(
ignoring: true,
child: Container(
height: 300,
width: 400,
color: Colors.deepPurple,
),
),
],
),
),
),
);
}
}

Related

Register tap on empty space around widget

Working on a flutter web project. I have a row which has 3 widgets:
From left to right:
Sidebar
Sidebar content
body
Widget _buildBody() {
final screenwidth = MediaQuery.of(context).size.width;
editpanel = screenwidth * 0.3;
final editor = ViewProvider.of(context).isEditPanelOpen
? (screenwidth - sidebar - editpanel)
: (screenwidth - sidebar);
final ViewProvider viewProvider = Provider.of<ViewProvider>(context);
return Row(
Sidebar()
_loadSidebarContent(bloc.editPanelIndex),
_sidebarHandler(viewProvider),
Center(
child: SizedBox(
width: editor * 0.8,
child: Center(
child: MyWidget(),
),
),
),
],
);
}
I need to register tap if user taps on anything except the Appbar, Sidebar, Sidebarcontent, on MyWidget.
So I wrapped the entire scaffold with gesture detector and tried using IgnorePointer for the specific widgets.
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
.. call some specific function
},
child: Scaffold(
backgroundColor: Colors.white,
appBar: PreferredSize(
preferredSize: Size(
MediaQuery.of(context).size.width,
height + 80,
),
child: IgnorePointer(
child: Appbar(),
ignoring: true,
),
),
body: _buildBody(),
),
);
}
Issue is: MyWidget is getting ignored all the time. I don't want to fire the specificFunc() when user taps on any of the: Appbar, Sidebar, Sidebarcontent, or MyWidget.
Basically if user taps the white space around MyWidget specificFunction will be called
Wrap the whole Scaffold widget with GestureDector is not a good idea.
Instead wrap the container (white space around your button) with the detector and supply the button as a child.
In the following sample, the amber area is your white one. Tapping the amber area, and the button produces a separate log.
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: () {
if (kDebugMode) {
print('Amber area tapped!');
}
},
child: Container(
color: Colors.amber,
width: 400,
height: 400,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextButton(
style: ButtonStyle(
foregroundColor:
MaterialStateProperty.all<Color>(Colors.blue),
backgroundColor: MaterialStateProperty.all<Color>(
Colors.white)),
onPressed: () {
if (kDebugMode) {
print('Button clicked.');
}
},
child: const Text('A Button'),
),
],
)),
),
)
],
),
),
);
}
}
You could use a stack (https://api.flutter.dev/flutter/widgets/Stack-class.html) and wrap the widget at the very bottom of the stack with a gesture detector.
To position the other widgets correctly, you could use the Positioned widget.
Instead of ignorePointer you should be using AbsorbPointer which will absorb the pointer and not pass it to the content below it

Flutter PageView Scroll but only for widgets and not entire pages

Today I have a question, not providing any code.
I would like to create a tiktok like experience for scrolling through my appfeed in flutter. However I don't want to scroll an entire page when I swipe, only to the next widget in the ListView/PageView. I am only able to swipe an entire page with pageview and I'm only able to scroll normally on Listview. Is there any solution for my request? I hope I clarified enough what I mean. Instagram offers such an experience on its Search. Is there any possibility how one could realize something like that?
Please help.
Use ListView.builder inside the Container with the 500px height and the ListView.builder will have the children posts thus you stay inside the same feed with the ability to swipe thru posts vertically or horizontally.
Check the following link for a tutorial : https://www.youtube.com/watch?v=baA_J5tUtEU
You can change the scrolling by setting scrollDirection: Axis.horizontal or Axis.vertical inside the ListView.buidler
Hope this answers your question.
So what you want to do is to use a Stack widget and then put the pageview, just as you would if you were creating an onboarding screen with flutter, something like this
import 'package:flutter/material.dart';
class OnBoarding extends StatefulWidget {
#override
_OnBoardingState createState() => _OnBoardingState();
}
class _OnBoardingState extends State<OnBoarding> {
PageController? controller;
int currentIndex = 0;
#override
void initState() {
controller = PageController();
super.initState();
}
#override
void dispose() {
controller!.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Stack(
children: [
PageView(
scrollDirection: Axis.vertical,
onPageChanged: onchahged,
controller: controller,
children: [
Container(
child: Image.network(
'https://picsum.photos/200/300',
fit: BoxFit.fill,
),
),
Container(
child: Image.network(
'https://picsum.photos/200/300',
fit: BoxFit.fill,
),
),
Container(
child: Image.network(
'https://picsum.photos/200/300',
fit: BoxFit.fill,
),
),
],
),
Positioned(
bottom: 30,
right: 10,
child: Column(
children: [
Icon(
Icons.ac_unit,
size: 30,
color: Colors.white,
),
SizedBox(
height: 10,
),
Icon(
Icons.image,
size: 30,
color: Colors.white,
),
SizedBox(
height: 10,
),
Icon(
Icons.person,
size: 30,
color: Colors.white,
),
SizedBox(
height: 10,
),
Icon(
Icons.person_add,
size: 30,
color: Colors.white,
),
],
),
)
],
),
);
}
onchahged(int index) {
setState(() {
currentIndex = index;
});
}
}
Here's what it looks like
For the specific Widget you must to wrap it with a GestureDetector and prevent from the current widget being scrollable.
An example:
GestureDetector(
onHorizontalDragUpdate: (_) {},
child: WidgetToNotBeingScrollableInTheHorizontal(),
);

How to force a widget to recreate itself after a state changed?

I am learning how to interact with images in Flutter, and while I was trying photo_view I met a weird interaction which made me wonder how widgets work.
So I have 2 images. First one with its height > its width, and the second one the other way around.
But I want to display them both in a square, so this is what my code looks like :
Widget _getPhotoView(String path) => AspectRatio(
aspectRatio: 1,
child: ClipRect(
child: PhotoView(
minScale: PhotoViewComputedScale.covered,
imageProvider: AssetImage(path),
),
),
);
Which is placed in this widgets tree :
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
Container(
height: MediaQuery.of(context).size.width,
child: _onStart
? _getPhotoView("assets/images/ig3.jpg")
: _getPhotoView("assets/images/ig4.jpg"),
),
Container(
color: Colors.yellow,
child: FlatButton(
onPressed: () {
setState(() {
_onStart = !_onStart;
});
},
child: Text("Switch images")),
),
Container(
height: MediaQuery.of(context).size.width,
child: _onStart
? _getPhotoView("assets/images/ig4.jpg")
: _getPhotoView("assets/images/ig3.jpg"),
),
Container(
color: Colors.yellow,
child: FlatButton(
onPressed: () {
setState(() {
_onStart = !_onStart;
});
},
child: Text("Switch images")),
)
],
),
);
I get the result I want on start :
But if I click on the button and switch the path of the images, I get this :
(if I switch again I get back to the first screen)
When the state of a widget has changed, isn't the widget suppose to recreate itself?
I am new to flutter and I don't really understand why this is happening and how to fix it...
The easiest solution to force widget recreation is to use keys. Something like that should help (if I understood your problem well):
Widget _getPhotoView(String path) => AspectRatio(
aspectRatio: 1,
child: ClipRect(
child: PhotoView(
key: ValueKey(path),
minScale: PhotoViewComputedScale.covered,
imageProvider: AssetImage(path),
),
),
);

AnimatedContainer - How do I expand only one and the rest stay below?

So there's something I'm working on and I want to have a list of these "capsules" (rounded rectangle containers). When the user taps on any given one of them, it expands to the full screen, while the rest stay on a lower layer and don't do anything.
I'm using AnimatedContainer and GestureDetector to change their state. When there's only one, it works perfectly for what I want to do. Meanwhile, as soon as I add more in a Column, because it's a single Widget I coded inside a GestureDetector with a single boolean, they all open at the same time. And I understand that even if I code them separately, it will basically just push the surrounding ones out of the way, not open above them. How would I deal with this?
I tried searching this and couldn't find anything helpful. Hopefully the answer to this will help future projects too.
bool chatCapsuleTapped = false;
bool hasFullSize = false;
#override
Widget build(BuildContext context) {
Widget _chatCapsuleAnimation() {
return GestureDetector(
onTap: () {
setState(() {
chatCapsuleTapped = !chatCapsuleTapped;
hasFullSize = true;
});
},
child: AnimatedContainer(
width: !chatCapsuleTapped ? 350 : MediaQuery.of(context).size.width,
height: !chatCapsuleTapped ? 75 : MediaQuery.of(context).size.height,
//color: !chatCapsuleTapped ? Colors.grey.withOpacity(1) : Colors.grey,
decoration: BoxDecoration(
color: !chatCapsuleTapped ? Colors.grey.shade500 : Colors.grey.shade300,
borderRadius: !chatCapsuleTapped ? BorderRadius.circular(40) : BorderRadius.circular(0),
),
child: !chatCapsuleTapped ? Container(child: Container(),) : Container(),
duration: Duration(milliseconds: 500),
curve: Curves.fastOutSlowIn,
),
);
}
return Scaffold(
body: Container(
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_chatCapsuleAnimation(),
],
),
),
);
}
} ```
You can use Hero:
Place each widget inside a Hero widget, assign it a tag based on the index.
Then have a Full-Screen page, which contains the bigger version of the widget, but with the same tag as of the tapped item.
Sample Grabbed from here, you can paste it in DartPad
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Basic Hero Animation'),
),
body: SingleChildScrollView(
child: Column(
children: List<Widget>.generate(5, (index) {
return InkWell(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Full-Screen Page'),
),
body: Container(
child: Hero(
// TAG should be same as the tapped item's index
tag: index.toString(),
child: SizedBox(
child: Container(
color: Colors.grey[(index + 1) * 100]),
),
),
),
);
},
),
);
},
child: Hero(
// Assigning tag of item as its index in the list
tag: index.toString(),
child: Container(
height: 200, color: Colors.grey[(index + 1) * 100]),
));
}))),
);
}
}
I've put the destination page within the scope of the main file for simplicity, but you can make a seperate Widget and accept index as parameter for the Bigger Hero's tag

GestureDetector in Stack blocked by foreground widget. HitTestBehavior.translucent not working as expected

I am trying to create a multi layered screen using a stack which allows the user to interact with both the foreground and background widgets.
I have started a basic outline for the screen however even when using HitTestBehavior.translucent the console only prints red pressed when pressing the translucent red container. I would like for console to print both strings when red/purple container is pressed.
Below is the code I used.
#override
Widget build(BuildContext context) {
final children = <Widget>[
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
debugPrint('blue pressed');
},
child: Container(
height: 500,
width: MediaQuery.of(context).size.width,
color: Colors.blue,
),
),
];
if (exercise.instructions.isNotEmpty) {
children.add(
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
debugPrint('red pressed');
},
child: Container(
height: 400,
width: MediaQuery.of(context).size.width,
color: Colors.red.withAlpha(100),
),
),
);
}
return Stack(
alignment: Alignment.bottomCenter,
children: children,
);
}
Below is the screen produced: