I have a GestureDetector that´s responsible for dragging a container up and down, to change the height. The contents of the container may be too long, so the content must be scrolled.
I can´t figure out how to dispatch the touch event to the correct component, I tried it with a IgnorePointer and change the ignoring property.
class _SlideSheetState extends State<SlideSheet>
bool _ignoreScrolling = true;
GestureDetector(
onVerticalDragUpdate: (DragUpdateDetails details) {
if(isDraggedUp) {
setState(() {
_ignoreScrolling = false
});
}
// update height of container, omitted for simplicity
},
child: NotificationListener(
onNotification: (ScrollNotification notification) {
if(notification is OverscrollNotification) {
if(notification.overscroll < 0) {
// the scrollview is scrolled to top
setState(() {
_ignoreScrolling = true;
});
}
}
},
child: IgnorePointer(
ignoring: _ignoreScrolling,
child: SingleChildScrollView(
physics: ClampingScrollPhysics(),
child: Container(
// ...
)
)
)
)
Does anybody know a good way to dispatch touch events up or down the Widget tree? Because in my solution, obviously, you always have to make one touch event just to change the "listener" from GestureDetector to SingleChildScrollView, which is annoying for the user, to say the least.
I was working on the same kind of widget today. You don't need the GestureDetector containing the NotificationListener. It's redundant and from my experience overrides the scrollListener within it or under it (depending on if you place it in a parent/child scenario or a stack scenario). Handle everything within the NotificationListener itself. Including updating your container's height. If you need the scrollable container to grow before you can scroll then I put mine in a stack with an "expanded" bool which then reactively built a gesture detector on top of the scroll container. Then when it was expanded I used the NotificationListener to handle it's drag displacement.
Stack(children:[
NotificationListener(/* scroll view stuff */),
expanded ? GestureDetector() : Container()
]);
Related
I've got a simple AnimatedWidget with one child widget.
AnimatedContainer(
duration: Duration(milliseconds: 2000),
curve: Curves.bounceOut,
decoration: BoxDecoration(
color: Colors.purple,
),
child: FlutterLogo(
size: _boxSize,
),
),
where _boxSize is being animated like so:
void _startAnimation() => setState(() {
_boxSize *= 1.7;
});
AnimatedContainer is not working for child widgets, however. You need to change direct properties of AnimatedContainer for the animation to work.
This is in compliance with documentation:
The [AnimatedContainer] will automatically animate between the old
and new values of properties when they change using the provided curve
and duration. Properties that are null are not animated.
Its child and descendants are not animated.
What is the equivalent of AnimatedContainer which is ALSO ABLE to animate its children?
There are few widgets which will animate the child. You can swap the new flutter logo widget with preferred size using AnimatedSwitcher Widget.
AnimatedSwitcher - This widget will swap the child widget with a new widget.
AnimatedPositioned - It'll change the position of the child from the stack widget whenever the given position changes.
AnimatedAlign - Animated version of align which will change the alignment of the child whenever the given alignment changes.
AnimatedCrossFade - It fades between two children and animate itself between their sizes.
There is no magic widget which would simply recursively animate all children. But I think what you want is an implicitly animated widget. ie. you change the constructor parameters of a widget, and as it changes it animates from one value to the next.
The easiest way is probably the ImplicitlyAnimatedWidget with a AnimatedWidgetBaseState. So for your example to animate a boxSize attribute this could look like:
class AnimatedFlutterLogo extends ImplicitlyAnimatedWidget {
const AnimatedFlutterLogo({Key key, #required this.boxSize, #required Duration duration})
: super(key: key, duration: duration);
final double boxSize;
#override
ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState() => _AnimatedFlutterLogoState();
}
class _AnimatedFlutterLogoState extends AnimatedWidgetBaseState<AnimatedFlutterLogo> {
Tween<double> _boxSize;
#override
void forEachTween(visitor) {
_boxSize = visitor(_boxSize, widget.boxSize, (dynamic value) => Tween<double>(begin: value));
}
#override
Widget build(BuildContext context) {
return Container(
child: FlutterLogo(
size: _boxSize?.evaluate(animation),
),
);
}
}
which is imho already pretty concise, the only real boilerplate is basically the forEachTween(visitor) method which has to create Tween objects for all properties you'd like to animate.
I have a grid of Images and I want to open a simple dialog when I long press an Image and to be closed automatically when my finger no longer contacts with the screen (like Instagram quick image preview).
I attached LongPress event to all the images and it works fine so a dialog opens up when I long press an image however when I put my finger up nothing happens even though I attached events like onTapUp, onLongPressEnd, onPointerUp Because of the new opened dialog, All of those events are lost and no longer fires up.
I tried to add the pointer up events to the opened dialog instead but there is a catch, I must tap and release again in order to make it work because Flutter unable to recognize that my finger is already in contact with screen and the opened dialog caused flutter to forget about this fact.
You can insert an OverlayEntry into the Overlay stack by using Overlay.of(context).insert(overlayEntry).
In this overlay, you can catch gestures when required and take actions accordingly. As overlays always sit on top of anything else, the dialog will not cancel your long press gesture and you will be able to respond to longPressEnd.
You will only need to calculate which image has been pressed or use the Offset's provided by onTapDown and the position of the images.
To get the global position of your images, you can assign GlobalKey's to your images and get their global positions in the following way:
final RenderBox renderBox = globalKey.currentContext.findRenderObject() as RenderBox;
final Offset position = renderBox.localToGlobal(Offset.zero);
final Size size = renderBox.size;
To get the position of your long press, you will need to store the position of onTapDown:
onTapDown: (details) => position = details.globalPosition
Now you have everything you need to figure out which bounds the long press happened in.
I found a way to make it work. It can be done with Overlay Widget.
In the widget with GestureDetector, when onLongPress is called, create an OverlayEntry object with your dialog, and insert it into Overlay.
When onLongPressEnd is called, call the remove function of OverlayEntry object.
// Implement a function to create OverlayEntry
OverlayEntry getMyOverlayEntry({
#required BuildContext context,
SomeData someData,
}) {
return OverlayEntry(
builder: (context) {
return AlertDialog(child: SomeWidgetAgain());
}
);
}
// In the widget where you want to support long press feature
OverlayEntry myOverayEntry;
GestureDetector(
onLongPress: () {
myOverayEntry = getMyOverlayEntry(context: context, someData: someData);
Overlay.of(context).insert(myOverayEntry);
},
onLongPressEnd: (details) => myOverayEntry?.remove(),
child: SomeWidgerHere(),
)
Here's the gist on Github:
https://gist.github.com/plateaukao/79aa39854dc4eabf1220bdfa9a0334b6
You can use AnimatedContainer and put a GestureDetector inside.
change width and height using setState and it's done.
Center(
child: AnimatedContainer(
width: containerWidth,
height: containerHeight,
color: Colors.red,
duration: Duration(seconds: 1),
child: GestureDetector(
onLongPress: (){
print("Long Press");
setState(() {
containerWidth = 200;
containerHeight = 200;
});
},
onLongPressUp: (){
print("On Long Press UP");
setState(() {
containerWidth = 100;
containerHeight = 100;
});
},
),
),
)
I completed the Flutter NameGenerator code lab and wanted to extend it to remove items directly from the "Saved suggestions list".
To do so, I've added the onTap handler below which removes the pair from the list.
However, the list doesn't update until I navigate back and reopen the screen again.
How do I immediately update the list on the second screen?
void _pushSaved() {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
final Iterable<ListTile> tiles = _saved.map((WordPair pair) {
return ListTile(
title: Text(
pair.asPascalCase,
style: _biggerFont,
),
onTap: () => setState(() {
_saved.remove(pair);
}),
);
});
final List<Widget> divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
return Scaffold(
appBar: AppBar(
title: const Text('Saved Suggestions'),
),
body: new ListView(children: divided),
);
}),
);
}
Why your code doesn't work
The reason your list doesn't update is that it's a different screen pushed on the Navigator.
Because your _pushSaved method is inside the original screen, you call setState on that screen and rebuild all the widgets of the original screen.
The pushed screen isn't affected because it's not a child of your original screen.
Rather, the original screen told the Navigator to create a new screen, so it's some subtree of the Navigator of your MaterialApp and not accessible to you.
Solution
Accessing the same live data on different screens is something that's not that easy to do just with StatefulWidgets.
Basically, your project has grown complex enough so that it's time to think about a more sophisticated state management solution.
Here's a video from Google I/O about state management that you could check out for some inspiration.
I'm using Dismissible to dismiss the items, but when an item is dismissed I get default boring animation. Is there a way to change that animation like Gmail does?
Example:
My own animation (not smooth)
So, in my animation, you can see slight pause when the item is deleted and next item coming up on the screen taking up old item position.
That's the default animation of Dismissible.
List<String> content;
ListView.builder(
itemCount: content.length,
itemBuilder: (context, index) {
return Dismissible(
key: ValueKey(content[index]),
onDismissed: (_) {
setState(() {
content = List.from(content)..removeAt(index);
});
},
background: Container(color: Colors.green),
child: ListTile(
title: Text(content[index]),
),
);
},
)
Thanks to #Rémi Rousselet for his efforts.
Finally I found the reason for that ugly animation. Never use itemExtent when you are planning to use Dismissible. I was mad, I used it.
I've changed the question from center the Tab to customize the position of the Tab.
Because I noticed that the Tab is actually Centered and according to the document, it just adding paddings.
But if you want a TabBar whenever it avoid safe area or not the elements in tabs are aligned centered (automatically), I can't find a way now.
look at the screenshot above, the icons/titles successfully avoid the black bar area (by extend the height of Tabbar), but not vertically center aligned. I tried put a Center element to wrap the tabar to tab widget, It extend the height to the whole screen.
I also tried wrap the tab in a Container widget and get the height property set, but it seems all the height are avoid the safe area (by add the a const).
And I tried wrap the whole Scaffold with a SafeArea widget, but the safe area become black and we actually wish the tab bat become higher and then layout element in this higher container, but not just move the container.
So the question might be How can I custom the position of the Tab widget. because the Tabbar is actually centered, but it layout child elements by avoid the safe area, so it's not visually vertical-aligned.
my code is:
#override
Widget build(BuildContext context) {
final icons = [Icons.library_books, Icons.photo, Icons.toc];
return Scaffold(
bottomNavigationBar: Material(
color: Theme.of(context).primaryColor,
child: SafeArea(child: TabBar(
controller: _tabController,
tabs: _titles.map((t) {
final i = _titles.indexOf(t);
return Tab(
text: t,
icon: Icon(icons[i]),
);
}).toList(),
),),
),
body: TabBarView(
controller: _tabController,
children: <Widget>[
PostListPage(
userid: widget.userid,
username: widget.username,
),
AlbumListPage(
userid: widget.userid,
username: widget.username,
),
TodoListPage(
userid: widget.userid,
username: widget.username,
),
],
)
);
}
}
Ok, I think I understand what you want now and I'm afraid it's not achievable.
If you don't use the SafeArea widget, the bar has the normal height at the full bottom and everything is centered. It has the disadvantage that it is over the black area in iPhone X.
But if you want to avoid the black bar area, you have to use the SafeArea (as you do) which adds some padding and pushes everything up a little, thus avoiding the bottom "unsafe" area.
So, it is the padding added by SafeArea which prevents you from pushing down (centering) the titles and icons.
A possible option is to wrap the SafeArea with a Container with a black color so the unsafe area is black.
child: Container(
color: Colors.black,
child: SafeArea(child: TabBar(
controller: _tabController,
...