GestureDetector is not working on top of TabBarView - flutter

I want to detect the gestures on the TabBarView so I wrapped the TabBarView in a GestureDetector widget, but it doesn't register any kind of gesture. And swiping to different tabs works. I just want a detect the gestures.
TabController _tabController;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(//I have 3 tabs in here at AppBar.bottum),
///This is where I need help with the GestureDetector not working.
body: GestureDetector(
onHorizontalDragStart: (DragStartDetails details) {
print('Start : ');
print(details);
},
child: TabBarView(controller: _tabController, children: <Widget>[
Tab(icon: Icon(Icons.category)),
Tab(icon: Icon(Icons.home)),
Tab(icon: Icon(Icons.star)),
]),
),
);
}

Nested Gesture Widgets
The reason you are having this issue is that both of those widgets receive touch input and when you have two widgets that receive touch input, long story short the child wins that battle. Here is the long story. So both of your inputs from your TabBarView and GestureDetector are sent to what is called a GestureArena. There the arena takes into account multiple different factors but the end of the story is the child always wins. You can fix this issue by defining your own RawGestureDetector with its own GestureFactory which will change the way the arena performs.
RawGestureDetector(
gestures: {
AllowMultipleGestureRecognizer: GestureRecognizerFactoryWithHandlers<
AllowMultipleGestureRecognizer>(
() => AllowMultipleGestureRecognizer(),
(AllowMultipleGestureRecognizer instance) {
instance.onTap = () => print('Episode 4 is best! (parent container) ');
},
)
},
behavior: HitTestBehavior.opaque,
//Parent Container
child: Container(
color: Colors.blueAccent,
child: Center(
//Wraps the second container in RawGestureDetector
child: RawGestureDetector(
gestures: {
AllowMultipleGestureRecognizer:
GestureRecognizerFactoryWithHandlers<
AllowMultipleGestureRecognizer>(
() => AllowMultipleGestureRecognizer(), //constructor
(AllowMultipleGestureRecognizer instance) { //initializer
instance.onTap = () => print('Episode 8 is best! (nested container)');
},
)
},
//Creates the nested container within the first.
child: Container(
color: Colors.yellowAccent,
width: 300.0,
height: 400.0,
),
),
),
),
);
class AllowMultipleGestureRecognizer extends TapGestureRecognizer {
#override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}
I want to give all credit to Nash the author of Flutter Deep Dive: Gestures this is a great article I highly recommend you check it out.

Change the behavior of GestureDetector
Same type of problem happens when you try to wrok with Stack and GestureDetector. The simple way to solve this problem is to change the behavior of GestureDetector.
TabController _tabController;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(//I have 3 tabs in here at AppBar.bottum),
body: GestureDetector(
// Add This Line in Code.
behavior: HitTestBehavior.translucent,
onHorizontalDragStart: (DragStartDetails details) {
print('Start : ');
print(details);
},
child: TabBarView(controller: _tabController, children: <Widget>[
Tab(icon: Icon(Icons.category)),
Tab(icon: Icon(Icons.home)),
Tab(icon: Icon(Icons.star)),
],
),
),
);
}
I found this form How to make a GestureDetector capture taps inside a Stack?.
Do tell us if this work for you of not. In my case, for Stack and TabBarView I used it directly on the children of the both Widgets, you may need to change the behavior of GestureDetector to something else or use it on the childrens of the TabBarView.

Related

Color of a widget inside a Stack is always slightly transparent

I display a custom-made bottom app bar in a Stack because of keyboard padding reasons. The custom widget is fully opaque as it should be until it's a child of a Stack in which case, the content behind it starts to be visible since the color's opacity somehow changes.
As you can see, it's only the "main" color that's transparent. Icons remain opaque.
This is the build method of my custom BottomBar widget which is then just regularly put into a Stack. I have tried using a Material and even a simple Container in place of the BottomAppBar widget but the results are the same.
#override
Widget build(BuildContext context) {
return BottomAppBar(
color: Colors.blue.withOpacity(1),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(
icon: Icon(MdiIcons.plusBoxOutline),
onPressed: () {},
),
Text('Edited 11:57'),
IconButton(
icon: Icon(MdiIcons.dotsVertical),
onPressed: () {},
),
],
),
);
}
Can you interact with the BottomAppBar ? It looks like an order problem. Try to put the BottomAppBar as last in the Stack children.
Note that BottomAppBar doesn't have a constant size, if you did not add it to Scaffold bottomNavigationBar named parameter has a size if this is not null. Below is peace of code in Scaffold dart file:
double bottomNavigationBarTop;
if (hasChild(_ScaffoldSlot.bottomNavigationBar)) {
final double bottomNavigationBarHeight = layoutChild(_ScaffoldSlot.bottomNavigationBar, fullWidthConstraints).height;
bottomWidgetsHeight += bottomNavigationBarHeight;
bottomNavigationBarTop = math.max(0.0, bottom - bottomWidgetsHeight);
positionChild(_ScaffoldSlot.bottomNavigationBar, Offset(0.0, bottomNavigationBarTop));
}
You can even develop your own Widget without BottomAppBar but if you want things like centerDocked and things like circular notched, you will have to do more stuff (anyway you have flexibility to custom design the way you want).
Here is a simple example to do that(one way to do that):
import 'package:flutter/material.dart';
class CustomBottomBar extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
Container(
margin: EdgeInsets.only(bottom: 50),
color: Colors.greenAccent, // if you want this color under bottom bar add the margin to list view
child: ListView.builder(
itemCount: 100,
itemBuilder: (_, int index) => Text("Text $index"),
),
),
Positioned(
bottom: 0,
child: Container(
color: Colors.amber.withOpacity(.5),
width: MediaQuery.of(context).size.width,
height: 50,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(4, (int index) => Text("Text $index")), // you can make these clickable by wrapping with InkWell or any gesture widget
),
),
),
],
),
);
}
}

ListView lagging only when widgets in the list are Video_Player widgets

I'm trying to achieve a scrollable List with videos. I'm using the video_player widget and wrapping the player in a Card with a simple button.
Now i noticed that whenever I use ListView.builder with videos in the list, it is extremely lagging, especially when scrolling back up. i'm posting a GiF bellow if you would like to see the behaviour.
I have this problem ONLY when I have Videos in the list.
If I replace the videos with a simple Image widget, the scrolling is smooth and runs as intended. (Also provided a GiF below)
When I scroll through the list I get this message in my Console:
flutter: Another exception was thrown: setState() or markNeedsBuild() called during build.
And I think (but not sure) that this is the cause of the problem, maybe the way I implemented the video_player plugin (?)
class VideoPlayPause extends StatefulWidget {
VideoPlayPause(this.controller);
final VideoPlayerController controller;
#override
_VideoPlayPauseState createState() => _VideoPlayPauseState();
}
class _VideoPlayPauseState extends State<VideoPlayPause> {
//This Part here
_VideoPlayPauseState() {
listener = () {
setState(() {});
};
}
Maybe its setting state every time I scroll ?
I tried flutter run --release but saw no difference at all.
I'm running the app on a physical Iphone X.
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('MVP_Test1'),
),
body: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: videoUrl.length,
itemBuilder: (context, i) {
return Card(
child: Column(
children: <Widget>[
Column(
children: <Widget>[
const ListTile(
leading: Icon(Icons.live_tv),
title: Text("Nature"),
),
Stack(
alignment: FractionalOffset.bottomRight +
const FractionalOffset(-0.1, -0.1),
children: <Widget>[
// If I replace this with Image.asset("..."), the scrolling is very smooth.
AssetPlayerLifeCycle(
videoUrl[i],
(BuildContext context,
VideoPlayerController controller) =>
AspectRatioVideo(controller)),
]),
],
),
ButtonTheme.bar(
child: ButtonBar(
children: <Widget>[
FlatButton(
child: const Text('ADD VIDEO'),
onPressed: () {
/* ... */
},
),
],
),
),
],
),
);
},
),
);
}
Result when I run Flutter Analyze
flutter analyze
Analyzing mvp_1...
No issues found! (ran in 1.9s)
You can see how when scrolling back up it looks like the app is skipping frames or something. Here's a video:
https://giphy.com/gifs/u48BNQ13r15Zay5SnN
Here's a video with photos instead of videos:
https://giphy.com/gifs/236RKyA8y1pfecmR1d

Keyboard is still showing when changing the to other Tab in a TabBarView

In one tab I have a TextFormField and in the other only a list of texts. When I select the Text field the keyboard is open, then I jump to the second Tab and the keyboard is still displayed.
I can even write and when I go back to the Tab 1 I see why I typed.
Do you know how can I give an action to the second Tab in order to take the focus out from the text field?
DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text('Manage Products'),
bottom: TabBar(tabs: <Widget>[
Tab(icon: Icon(Icons.create), text: 'Create Product'),
Tab(icon: Icon(Icons.list), text: 'My Products'),
]),
),
body: TabBarView(
children: <Widget>[
ProductEditPage(addProduct: addProduct),
ProductListPage(products, updateProduct),
],
)),
);
Tab1
Tab2
SOLVING CODE
After applying #nick.tdr suggestion an example code can be as follow:
class _Test extends State<Test> with TickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: 2);
_tabController.addListener(() {
if (_tabController.indexIsChanging) {
FocusScope.of(context).requestFocus(new FocusNode());
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('2 Tabs'),
bottom: TabBar(controller: _tabController, tabs: <Widget>[
Tab(text: 'Tab with Text Field'),
Tab(text: 'Empty Tab'),
]),
),
body: TabBarView(
controller: _tabController,
children: <Widget>[
Container(
child: TextFormField(
decoration: InputDecoration(labelText: 'Title'),
),
),
Container()
],
),
);
}
}
You can add gesture detector to you scaffold and remove the focus. But that won't work for the tabs. For the tabs you need to do the following:
controller.addListener((){
if(controller.indexIsChanging)
{
FocusScope.of(context).detach();
}
});
Where controller is your tab controller. Hope that helps
I improved #nick.tdr 's answer.
For the tabs you need to do the following;
controller.addListener((){
if(controller.indexIsChanging)
{
FocusScope.of(context).unfocus();
}
});
If you want to work this when swiping between tabs instead of clicking tab buttons, try the following;
controller.addListener((){
FocusScope.of(context).unfocus();
});
Where the controller is your tab controller.
For me I found the best way is to request a new FocusNode on tabChange listener that has been set in didChangeDependencies() callback:
in build() method:
TabBar(
controller: tabController,
.
.
.
),
didChangeDependencies callback:
#override
void didChangeDependencies() {
setState(() {
tabController.addListener(handleTabChange);
});
super.didChangeDependencies();
}
The listener implementation:
handleTabChange() {
// do whatever handling required first
setState(() {
FocusScope.of(context).requestFocus(new FocusNode());
});
}
I think wrapping up your whole Scaffold body into a GestureDetector should solve your problem.
new Scaffold(
body: new GestureDetector(
onTap: () {
// call this method here to hide keyboard
FocusScope.of(context).requestFocus(new FocusNode());
},
child: new Container(
/*Remaining code goes here*/
)
)
This simply gains focus on the widget you tapped on removing focus from previous one.

Flutter ReorderableListView doesn't work with TextFields

The widgets in my ReorderableListView are essentially TextFields. When long pressing on a widget, after the time when the long press should cause the widget to "hover," instead the TextField receives focus. How can I make the drag & drop effect take precedence over the TextField? I would still like a normal tap to activate the TextField.
The code below demonstrates my issue.
I also tried to use this unofficial flutter_reorderable_list package. (To test this one, replace the Text widget on this line of the example code with a TextField.)
I'm willing to use any ugly hacks to get this working, including modifying the Flutter source code!
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final children = List<Widget>();
for (var i = 0; i < 5; i++) {
children.add(Container(
color: Colors.pink, // Only the pink area activates drag & drop
key: Key("$i"),
height: 50.0,
child: Container(
color: Colors.grey,
margin: EdgeInsets.only(left: 50),
child: TextField(),
),
));
}
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: ReorderableListView(
children: children,
onReorder: (oldIndex, newIndex) => null,
),
),
),
);
}
}
You need to do multiple things in there to fix this.
First disable the default handler in ReorderableListView by setting buildDefaultDragHandles: false in its properties.
Wrap you child widget inside ReorderableDragStartListener widget like this
ReorderableDragStartListener(
index: i,
child: Container(
color: Colors.grey,
margin: EdgeInsets.only(left: 50),
child: TextFormField(initialValue: "Child $i", ),
),
),
Then inside this ReorderableDragStartListener wrap your child in InkWell and AbsorbPointer. Then use FocusNode to focus inner TextField on single tap.
Like this
InkWell(
onTap: () => _focusNode.requestFocus(),
onLongPress: () {
print("long pressed");
},
child: AbsorbPointer(
child: TextFormField(initialValue: "Child $i", focusNode: _focusNode,),
),
),
You need to create multiple FocusNode for all the items in list. You can do this by using List or by simpling creating a new FocusNode inside the loop.
Complete code example here https://dartpad.dev/?id=e75b493dae1287757c5e1d77a0dc73f1

How do I prevent onTapDown from being triggered on a parent widgets GestureDetector?

I have a Stack in which several widget can be dragged around. In addition, the container that the Stack is in has a GestureDetector to trigger on onTapDown and onTapUp. I want those onTap events only to be triggered when the user taps outside of the widget in the Stack. I've tried the following code:
class Gestures extends StatefulWidget {
#override
State<StatefulWidget> createState() => _GesturesState();
}
class _GesturesState extends State<Gestures> {
Color background;
Offset pos;
#override
void initState() {
super.initState();
pos = Offset(10.0, 10.0);
}
#override
Widget build(BuildContext context) => GestureDetector(
onTapDown: (_) => setState(() => background = Colors.green),
onTapUp: (_) => setState(() => background = Colors.grey),
onTapCancel: () => setState(() => background = Colors.grey),
child: Container(
color: background,
child: Stack(
children: <Widget>[
Positioned(
top: pos.dy,
left: pos.dx,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanUpdate: _onPanUpdate,
// onTapDown: (_) {}, Doesn't affect the problem
child: Container(
width: 30.0,
height: 30.0,
color: Colors.red,
),
),
)
],
),
),
);
void _onPanUpdate(DragUpdateDetails details) {
RenderBox renderBox = context.findRenderObject();
setState(() {
pos = renderBox.globalToLocal(details.globalPosition);
});
}
}
However, when starting to drag the widget, the onTap of the outermost container is triggered as well, making the background momentarily go green in this case. Settings HitTestBehavior.opaque doesn't seem to work like I'd expect. Neither does adding a handler for onTapDown to the widget in the Stack.
So, how do I prevent onTapDown from being triggered on the outermost GestureDetector when the user interacts with the widget inside of the Stack?
Update:
An even simpler example of the problem I'm encountering:
GestureDetector(
onTapDown: (_) {
print("Green");
},
child: Container(
color: Colors.green,
width: 300.0,
height: 300.0,
child: Center(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (_) {
print("Red");
},
child: Container(
color: Colors.red,
width: 50.0,
height: 50.0,
),
),
),
),
);
When I tap and hold the red container, both "Red" and "Green" are printed even though the inner GestureDetector has HitTestBehavior.opaque.
In this answer, I'll solve the simpler example you have given. You are creating the following Widget hierarchy:
- GestureDetector // green
- Container
- Center
- GestureDetector // red
- Container
Therefore the red GestureDetector is a child Widget of the green GestureDetector. The green GestureDetector has the default HitTestBehavior: HitTestBehavior.deferToChild. That is why onTapDown is fired for both containers.
Targets that defer to their children receive events within their
bounds only if one of their children is hit by the hit test.
Instead, you can use a Stack to build your UI:
- Stack
- GestureDetector // green
- Container
- GestureDetector // red
- Container
This structure would result in the follwing sourcecode. It looks the same, but the behavior is the one you desired:
Stack(
alignment: Alignment.center,
children: <Widget>[
GestureDetector(
onTapDown: (_) {
print("Green");
},
child: Container(
color: Colors.green,
width: 300.0,
height: 300.0,
),
),
GestureDetector(
onTapDown: (_) {
print("Red");
},
child: Container(
color: Colors.red,
width: 50.0,
height: 50.0,
),
)
],
)
I found that I could get the behavior that I wanted (making a widget appear transparent to it parent, while still responding to pointer events) by creating a render object with hit test behavior like this:
#override
bool hitTest(BoxHitTestResult result, {#required Offset position}) {
// forward hits to our child:
final hit = super.hitTest(result, position: position);
// but report to our parent that we are not hit when `transparent` is true:
return false;
}
I've published a package with a widget having this behavior here: https://pub.dev/packages/transparent_pointer.
I use RiverPod, and have succeeded with these steps. This is a general process, so should work for all use cases (and with your state manager)
(The use case here is to prevent a ListView from scrolling, when I select text on a widget using the mouse).
Create a notifier
final textSelectionInProgress = StateProvider<bool>((ref) {
return false;
});
When there is an action (in my case onTap) on the widget, wrap widget with a Focus, or use the focusNode of the widget, and the following code in initState
#override
initState() {
super.initState();
focusN = FocusNode();
focusN.addListener(() {
if (focusN.hasFocus) {
widget.ref.read(textSelectionInProgress.notifier).state = true;
} else {
widget.ref.read(textSelectionInProgress.notifier).state = false;
}
});
}
Ensure you add this in the onDispose:
#override
void dispose() {
widget.ref.read(textSelectionInProgress.notifier).state = false;
super.Dispose();
}
Add listener in the build of the widget you want to stop scrolling
bool textSelectionOn = ref.watch(textSelectionInProgress);
Set ScrollPhysics appropriately
physics: textSelectionOn
? NeverScrollableScrollPhysics()
: <you choice>