Register tap on empty space around widget - flutter

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

Related

Propagate click behind a widget

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,
),
),
],
),
),
),
);
}
}

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

Flutter: How to get accessibility focus to custom appbar

If you use the default Material scaffold with the default appbar, and you open your app using TalkBack, the accessibility focus moves automatically to the first item in the appbar (usually, the "Back" button).
I would like my appbar to be taller than the default, so I've created a custom one and pass it to the scaffold:
class CustomAppbar extends StatelessWidget implements PreferredSizeWidget {
#override
Widget build(BuildContext context) {
return Container(
height: 120,
child: Semantics(
focused: true,
child: RaisedButton(
child: Text('Test'),
onPressed: () {},
),
),
);
}
#override
Size get preferredSize => Size.fromHeight(120);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppbar(),
body: SafeArea(
child: Stack(children: <Widget>[
Column(children: <Widget>[
_buildContent(),
]),
]),
),
);
}
The problem is, if I don't use the default AppBar, I can't move the focus to the first button, even if I wrap it with Semantics.
How is the default Scaffold handling the automatic focus? I've searched in the code and couldn't find it.
You can wrap your AppBar with a Semantic node and set focused:true.
Read more here:
https://api.flutter.dev/flutter/semantics/SemanticsProperties/focused.html
So flutter provides a widget PreferredSize to us in which we can override its height and width, I solved my problem in this way
Widget appBar() {
return PreferredSize(
preferredSize: Size.fromHeight(120.0),
child: AppBar(
automaticallyImplyLeading: false,
elevation: 0,
backgroundColor: Colors.blue,
flexibleSpace: SafeArea(
child: Container(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 16
),
child: Container(
// make any type of view
),
),
),
),
);
}

Flutter Scaffold.of(context).openDrawer() doesn't work

I want to open a drawer after pushing on the custom button in BottomMenu I have trouble with Scaffold.of(context).openDrawer(), it doesn't work. My BottomMenu is a separate widget class. As I understand, it doesn't work because it's a separate context. How can I get the right context? Or perhaps someone knows another solution.
Here my code reproducer:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(title: 'Flutter Drawer'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
bottomNavigationBar: BottomMenu(),
endDrawer: SizedBox(
width: double.infinity,
child: Drawer(
elevation: 16,
child: Container(
color: Colors.black,
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
ListTile(
title: Text('Some context here',
style: TextStyle(color: Colors.white))),
ListTile(
title: Text('Some context here',
style: TextStyle(color: Colors.white))),
],
),
),
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Call Drawer form menu reproducer',
)
],
),
),
);
}
}
class BottomMenu extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Wrap(
alignment: WrapAlignment.center,
children: <Widget>[
Divider(color: Colors.black, height: 1),
Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
InkWell(
borderRadius: new BorderRadius.circular(20.0),
customBorder: Border.all(color: Colors.black),
child: Container(
padding: EdgeInsets.only(
left: 3, right: 6, bottom: 15, top: 11),
child: Row(
children: <Widget>[
Icon(Icons.menu),
Text('Show menu', style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
],
),
),
onTap: () {
Scaffold.of(context).openDrawer();
},
),
],
),
),
],
),
);
}
}
In my case, this worked.
return Scaffold(
key: _scaffoldKey,
endDrawerEnableOpenDragGesture: false, // This!
appBar: AppBar(
iconTheme: IconThemeData(color: Colors.white),
leading: IconButton(
icon: Icon(Icons.menu, size: 36),
onPressed: () => _scaffoldKey.currentState.openDrawer(), // And this!
),
),
drawer: DrawerHome(),
....
and _scaffoldKey must be initialized as,
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
under the class.
The problem is that you specified endDrawer on Scaffold yet you're calling Scaffold.of(context).openDrawer().
openDrawer() documentation states:
If the scaffold has a non-null Scaffold.drawer, this function will cause the drawer to begin its entrance animation.
Since your drawer is null, nothing happens.
In contrast, openEndDrawer() informs us:
If the scaffold has a non-null Scaffold.endDrawer, this function will cause the end side drawer to begin its entrance animation.
Since your endDrawer is not null you should use openEndDrawer() method. Alternatively, if you don't care which side the drawer slides in from, you can use drawer instead of endDrawer when building Scaffold.
My problem solved that instead of
Scaffold.of(context).openEndDrawer()
I give key to Scaffold and then I call by state like below
_scaffoldkey.currentState.openEndDrawer()
It solved my problem I hope It also works for you
Scaffold.of(context).openEndDrawer()
The Problem
This issue can occur when you do not use the correct BuildContext when calling Scaffold.of(context).openDrawer() (or openEndDrawer()).
Easiest Solution
Simply wrap whatever calls openDrawer() (or openEndDrawer()) with a Builder widget. This will give it a working context.
Minimal Working Example
// your build method
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: Builder(builder: (context) { // this uses the new context to open the drawer properly provided by the Builder
return FloatingActionButton(onPressed: (() => Scaffold.of(context).openDrawer()));
}),
drawer: const Drawer(
child: Text("MY DRAWER"),
),
);
}
Similar problem here. Clicked on button and nothing happened. The problem is I was using the context of the widget that instantiated Scaffold. Not the context of a child of Scaffold.
Here is how I solved it:
// body: Column(
// children: <Widget>[
// Row(
// children: <Widget>[
// IconButton(
// icon: Icon(Icons.filter_list),
// onPressed: () => Scaffold.of(context).openEndDrawer(), (wrong context)
// ),
// ],
// ),
// ],
// )
To:
body: Builder(
builder: (context) => Column(
children: <Widget>[
Row(
children: <Widget>[
IconButton(
icon: Icon(Icons.filter_list),
onPressed: () => Scaffold.of(context).openEndDrawer(),
),
],
),
],
)),
),
Assign Drawer to drawer property in scaffold. Wrap your specific Widget/Button(where you want to open drawer on its click method) with Builder. Use below method on click property:
enter image description here
Scaffold.of(context).openDrawer();
If you have the appbar widget with an action button to launch the drawer and the drawer is never pushed please remember that you need to define after appbar: ... the endDrawer: YOURAppDrawerWIDGET(), or else using the Scaffold.of(context).openEndDrawer() will not work.
Scaffold(
appBar: AppBar(title: Text(_title)),
endDrawer: AppDrawer(), // <-- this is required or else it will not know what is opening
body: SingleChildScrollView(
///...

Determine Scroll Widget height

I would like to determine if a 'scrollable' widget indeed needs to scroll. I would ultimately like to show something like 'Scroll for more'. How do I achieve this?
I could use a combination of LayoutBuilder and a ScrollController. However, ScrollController gives me maxScrollExtent and minScrollExtent only after any event - like say for example user trying to scroll
I would like to know during 'build' so that I can determine to show 'Scroll for more' or not depending on the screen dimensions
class _MyHomePageState extends State<MyHomePage> {
int value = 0;
String text = 'You have pushed the button 0 times\n';
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
flex: 9,
child: SingleChildScrollView(
child: Text(text),
),
),
Expanded(
flex: 1,
child: Text('Scroll for more')),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
value += 1;
text = text + 'You have pushed the button $value times \n';
});
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
I would like to dynamically display
Text('Scroll for more'),
only if the SingleChildScrollView needs to be scrolled to view the entire content. Please note, I am just giving the above code as example.
In my real case, I have a StreamBuilder inside a SingleChildScrollView and I cannot determine how much of data is going to be flowing in from the stream. I also do not have any button or tap or any gesture to access the controller and setState
Thanks in advance for any help
class _MyHomePageState extends State<MyHomePage> {
bool showMore = false;
final scrollController = ScrollController();
#override
Widget build(BuildContext context) {
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(() {
showMore = scrollController.position.maxScrollExtent > 0;
});
});
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
flex: 9,
child: SingleChildScrollView(
controller: scrollController,
child: SizedBox(height: 650, child: Text('blah')),
),
),
if (showMore) Expanded(flex: 1, child: Text('Scroll for more')),
],
),
),
);
}
}