Is it possible in flutter to have the bottom sheet partially viewable at an initial state and then be able to either expand/dismiss?
I've included a screenshot of an example that Google Maps implements.
Use the DraggableScrollableSheet widget with the Stack widget:
Here's the gist for the entire page in this^ GIF, or try the Codepen.
Here's the structure of the entire page:
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
CustomGoogleMap(),
CustomHeader(),
DraggableScrollableSheet(
initialChildSize: 0.30,
minChildSize: 0.15,
builder: (BuildContext context, ScrollController scrollController) {
return SingleChildScrollView(
controller: scrollController,
child: CustomScrollViewContent(),
);
},
),
],
),
);
}
In the Stack:
- The Google map is the lower most layer.
- The Header (search field + horizontally scrolling chips) is above the map.
- The DraggableBottomSheet is above the Header.
Some useful parameters as defined in draggable_scrollable_sheet.dart:
/// The initial fractional value of the parent container's height to use when
/// displaying the widget.
///
/// The default value is `0.5`.
final double initialChildSize;
/// The minimum fractional value of the parent container's height to use when
/// displaying the widget.
///
/// The default value is `0.25`.
final double minChildSize;
/// The maximum fractional value of the parent container's height to use when
/// displaying the widget.
///
/// The default value is `1.0`.
final double maxChildSize;
Edit: Thank you #Alejandro for pointing out the typo in the widget name :)
I will be implementing the same behaviour in the next few weeks and I will be referring to the backdrop implementation in Flutter Gallery, I was able to modify it previously to swipe to display and hide (with a peek area).
To be precise you can replicate the desired effect by changing this line of code in backdrop_demo.dart from Flutter Gallery :
void _handleDragUpdate(DragUpdateDetails details) {
if (_controller.isAnimating)// || _controller.status == AnimationStatus.completed)
return;
_controller.value -= details.primaryDelta / (_backdropHeight ?? details.primaryDelta);
}
I have just commented the controller status check to allow the panel to be swipe-able.
I know this isn't the complete implementation you are looking for, but I hope this helps you in any way.
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 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'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,
...
How do i implement the swipe from the left to go back gesture in flutter? Not sure if it was already implemented automatically for iOS, but I wanted it for Android as well (as things are becoming gesture based).
Use CupertinoPageRoute to make it work on Android;
import 'package:flutter/cupertino.dart';
(as answered on How to implement swipe to previous page in Flutter?)
You could set your Theme.platform to TargetPlatform.ios. This will make use that the swipe back gesture is used on every device.
You can use CupertinoPageRoute() as Tom O'Sullivan said above.
However, if you want to customize it (eg. using custom transition duration) using PageRouteBuilders and get the same swipe to go back gesture, then you can override buildTransitions().
For iOS, the default PageTransitionBuilder is CupertinoPageTransitionsBuilder(). So we can use that in buildTransitions(). This automatically give us the swipe right to go back gesture.
Here's some sample code for the CustomPageRouteBuilder:
class CustomPageRouteBuilder<T> extends PageRoute<T> {
final RoutePageBuilder pageBuilder;
final PageTransitionsBuilder matchingBuilder = const CupertinoPageTransitionsBuilder(); // Default iOS/macOS (to get the swipe right to go back gesture)
// final PageTransitionsBuilder matchingBuilder = const FadeUpwardsPageTransitionsBuilder(); // Default Android/Linux/Windows
CustomPageRouteBuilder({this.pageBuilder});
#override
Color get barrierColor => null;
#override
String get barrierLabel => null;
#override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return pageBuilder(context, animation, secondaryAnimation);
}
#override
bool get maintainState => true;
#override
Duration get transitionDuration => Duration(milliseconds: 900); // Can give custom Duration, unlike in MaterialPageRoute
#override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return matchingBuilder.buildTransitions<T>(this, context, animation, secondaryAnimation, child);
}
}
Then to go to a new page:
GestureDetector(
onTap: () => Navigator.push(
context,
CustomPageRouteBuilder(pageBuilder: (context, animation, secondaryAnimation) => NewScreen()),
),
child: ...,
)
You can set the platform of your theme (and darkTheme) to TargetPlatform.iOS, you can set the pageTransitionsTheme of your themes to,
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: CupertinoPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
},
),
and you can load the new page using CupertinoPageRoute ... and none of that will work until you make sure to use Navigator.push (instead of Navigator.pushReplacement) to get to that new screen! I hope this helps anyone out there who was working with existing transitions and didn't notice this crucial detail. :)
Use this plugin:
https://pub.dev/packages/cupertino_back_gesture
A Flutter package to set custom width of iOS back swipe gesture area.
For basic use:
import 'package:cupertino_back_gesture/cupertino_back_gesture.dart';
BackGestureWidthTheme(
backGestureWidth: BackGestureWidth.fraction(1 / 2),
child: MaterialApp(
theme: ThemeData(
pageTransitionsTheme: PageTransitionsTheme(
builders: {
//this is default transition
//TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
//You can set iOS transition on Andoroid
TargetPlatform.android: CupertinoPageTransitionsBuilderCustomBackGestureWidth(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilderCustomBackGestureWidth(),
},
),
),
home: MainPage(),
),
)
More details on plugin's page
in my case, the solution turned out to be very simple. I just used context.push('screen') instead of context.go('/screen')
This should not be implemented on Android since it makes interactions inconsistent across the OS.
Swiping from the screens edge to go back is not something that Android wants you to implement, so you should better don't do it.
This question already has answers here:
Flutter Keyboard listen on hide and show
(8 answers)
Closed last year.
I have a BottomNavigationBar at the upper-most level of my application. I want to detect keyboard open and close basically anywhere in the app/subtree, so I can show and hide the BottomNavigationBar whenever the keyboard is visible.
This is a general issue and may not be directly related to the BottomNavigationBar. In other words, abstract from the BottomNavigationBar :-)
To check for keyboard visibility, just check for the viewInsets property anywhere in the widget tree. The keyboard is hidden when viewInsets.bottom is equal to zero.
You can check for the viewInsets with MediaQuery like:
MediaQuery.of(context).viewInsets.bottom
I just created a Flutter plugin to notify about keyboard open and close events. It works both on Android and iOS.
keyboard_visibility
import 'package:keyboard_visibility/keyboard_visibility.dart';
#override
void initState() {
super.initState();
KeyboardVisibilityNotification().addNewListener(
onChange: (bool visible) {
print(visible);
},
);
}
You can use WidgetsBinding.instance.window.viewInsets.bottom. If its value is greater than 0.0 then the keyboard is visible.
if(WidgetsBinding.instance.window.viewInsets.bottom > 0.0)
{
// Keyboard is visible.
}
else
{
// Keyboard is not visible.
}
You can use the keyboard_visibility package to do this effectively. I've used it, and it works like a charm.
To install
dependencies:
keyboard_visibility: ^0.5.2
Usage
import 'package:keyboard_visibility/keyboard_visibility.dart';
#protected
void initState() {
super.initState();
KeyboardVisibilityNotification().addNewListener(
onChange: (bool visible) {
print(visible);
},
);
}
It also supports listeners like show/hide.
Here is the link.
In your StatefullWidget, create a variable:
bool _keyboardVisible = false;
Then initialize that variable in the build widget;
#override
Widget build(BuildContext context) {
_keyboardVisible = MediaQuery.of(context).viewInsets.bottom != 0;
return child;
}
This is my solution, which uses WidgetsBindingObserver to observe window size changes, and determine whether the keyboard is hidden based on this.
/// My widget state,it can remove the focus to end editing when the keyboard is hidden.
class MyWidgetState extends State<MyWidget> with WidgetsBindingObserver {
/// Determine whether the keyboard is hidden.
Future<bool> get keyboardHidden async {
// If the embedded value at the bottom of the window is not greater than 0, the keyboard is not displayed.
final check = () => (WidgetsBinding.instance?.window.viewInsets.bottom ?? 0) <= 0;
// If the keyboard is displayed, return the result directly.
if (!check()) return false;
// If the keyboard is hidden, in order to cope with the misjudgment caused by the keyboard display/hidden animation process, wait for 0.1 seconds and then check again and return the result.
return await Future.delayed(Duration(milliseconds: 100), () => check());
}
#override
void initState() {
super.initState();
// Used to obtain the change of the window size to determine whether the keyboard is hidden.
WidgetsBinding.instance?.addObserver(this);
}
#override
void dispose() {
// stop Observing the window size changes.
WidgetsBinding.instance?.removeObserver(this);
super.dispose();
}
#override
void didChangeMetrics() {
// When the window insets changes, the method will be called by the system, where we can judge whether the keyboard is hidden.
// If the keyboard is hidden, unfocus to end editing.
keyboardHidden.then((value) => value ? FocusManager.instance.primaryFocus?.unfocus() : null);
}
}
You can use MediaQuery.of(context).viewInsets.bottom. Just look at the documentation below.
/// The parts of the display that are completely obscured by system UI, /// typically by the device's keyboard. /// /// When a
mobile device's keyboard is visible viewInsets.bottom ///
corresponds to the top of the keyboard. /// /// This value is
independent of the [padding]: both values are /// measured from the
edges of the [MediaQuery] widget's bounds. The /// bounds of the top
level MediaQuery created by [WidgetsApp] are the /// same as the
window (often the mobile device screen) that contains the app. ///
/// See also: /// /// * [MediaQueryData], which provides some
additional detail about this /// property and how it differs from
[padding]. final EdgeInsets viewInsets;
You can use
Flutter keyboard visibility plugin
#override
Widget build(BuildContext context) {
return KeyboardVisibilityBuilder(
builder: (context, isKeyboardVisible) {
return Text(
'The keyboard is: ${isKeyboardVisible ? 'VISIBLE' : 'NOT VISIBLE'}',
);
}
);
With Flutter 2.0 and null safety, I use this package - it has no streams, pure Dart, gives additional information about keyboard height, etc.
flutter_keyboard_size 1.0.0+4
I used a workaround. I added a focusNode to the input and added a listener to that.
See the implementation here add focus listener to input.
I found an easier solution here:
Put the DesiredBottomWidget in a Stack() with a Positioned(top: somevalue), and it will be hidden when the keyboard appears.
Example:
Stack(
"Somewidget()",
Positioned(
top: "somevalue",
child: "DesiredBottomWidget()"),
),