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()"),
),
Related
I'm writing an overlay package for flutter for tutorial walk-through.
in order to know which holes to make in the overlay to make the widgets behind it fully visible, the function requires a list of global keys and I use them to fetch location and size.
while the overlay is still visible, the widgets can be animated and move to a different location. is there a way to detect that in order to re-draw the overlay?
I know that if I add a mixin of WidgetsBindingObserver to the widgets, I can check that with didChangeMetrics(). the problem is that I don't want the user to modify is original code. is there a way to observe widgets metrics without changing their code?
my code is at https://github.com/kfirufk/tuxin_tutorial_overlay
thanks!
so thanks to #pskink amazing support I was able to properly resolve this issue.
I already have the Global Keys of the widgets I want to monitor for changes in size and position, so I don't need to drill down through all the widgets in the app.
so I'm adding a persistent frame callback and in there I check that that overlay being shown actually have holes to show widgets behind it, and if it doesת I use the _sizeVisitor() function to detect if any of the widgets position or size where modified, and if they where, I redraw the widget.
again.. thank you so much #pskink!
void _detectWidgetPositionNSizeChange() {
var bindings = WidgetsFlutterBinding.ensureInitialized();
bindings.addPersistentFrameCallback((d) {
if (visibleOverlayPage != null &&
(visibleOverlayPage.disabledVisibleWidgetsCount +
visibleOverlayPage.enabledVisibleWidgetsCount >
0)) {
for (final widgetGlobalKey in visibleOverlayPage.widgetsGlobalKeys) {
if (_sizeVisitor(widgetGlobalKey)) {
redrawCurrentOverlay();
break;
}
}
}
});
}
bool _sizeVisitor(GlobalKey elementKey) {
if (elementKey.currentContext != null) {
final RenderBox renderBox = elementKey.currentContext.findRenderObject();
bool isChanged = false;
Rect newRect = renderBox.localToGlobal(Offset.zero) & renderBox.size;
_rectMap.update(elementKey, (oldRect) {
if (newRect != oldRect) {
isChanged = true;
}
return newRect;
}, ifAbsent: () => newRect);
return isChanged;
} else {
return false;
}
}
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.
I'm currently learning Flutter and I'm having some trouble showing a Snackbar after the interaction with the slider has ended (in other words, the final value was set when the user lifts their finger off the slider). I can't call my _showSnackBar() method in onChange because the snackbar is created and shown many times, one after the other.
Is there something I can do to call a method only after the interaction has finished? I was thinking of making a pull request and add something like onInteractionEnded callback property, but I would like to find out of there is another way first.
Here is my code for reference.
class _MySliderState extends State<MySlider> {
int _value = 2;
#override
Widget build(BuildContext context) {
return Slider(
min: 0.0,
max: 4.0,
divisions: 4,
value: (_value * 1.0),
onChanged: (double value) {
setState(() {
_value = value ~/ 1;
});
_showSnackBar();
},
);
}
void _showSnackBar() {
var snackbar = SnackBar(content: const Text('Slider value changed'));
Scaffold.of(context).showSnackBar(snackbar);
}
}
Thanks.
onChangeStart and onChangeEnd was added to Slider very recently
https://github.com/flutter/flutter/pull/17298
The change should available in master already.
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.
I'm currently developing a mobile application with JavaFX, using GluonHQ and JavaFXPorts. One of my screens contains a listview as you can see from the screenshot below, which was taken from my iPhone 6.
I have noticed the following problems with the scrollbar in mobile devices:
The first time i touch the screen the scroll bar appears a bit off place and then moves to the correct right position. This just happens quickly only the first time. (Screenshot)
I noticed that the scrollbar appears every time i touch the screen and not only when I touch and drag. On native iOS applications the scrollbar appears only when you touch and drag. If you keep your finger on screen and then remove it the scrollbar does not appear.
The scrollbar always takes some time to disappear when I remove my finger from the screen, whilst in native apps it disappears instantly.
Can anyone help me on fixing these issues. How can you define the time the scrollbar appears before it hides again?
You can experience this situation by just creating a ListView and load it with some items.
UPDATE
Thanks to the answer of Jose Pereda below, I have managed to overcome all three problems described above. Here is the code I used to reach the desired results. Watch this short video to get a quick idea of how the new scrolling bar appears and behaves. Again, Jose, you are the boss! Please go ahead with any comments for improvement.
public class ScrollBarView {
public static void changeView(ListView<?> listView) {
listView.skinProperty().addListener(new ChangeListener<Object>() {
private StackPane thumb;
private ScrollBar scrollBar;
boolean touchReleased = true, inertia = false;
#Override
public void changed(ObservableValue<? extends Object> observable, Object oldValue, Object newValue) {
scrollBar = (ScrollBar) listView.lookup(".scroll-bar");
// "hide" thumb as soon as the scroll ends
listView.setOnScrollFinished(e -> {
if (thumb != null) {
touchReleased = true;
playAnimation();
} // if
});
// Fix for 1. When user touches first time, the bar is set invisible so that user cannot see it is
// placed in the wrong position.
listView.setOnTouchPressed(e -> {
if (thumb == null) {
thumb = (StackPane) scrollBar.lookup(".thumb");
thumb.setOpacity(0);
initHideBarAnimation();
} // if
});
// Try to play animation whenever an inertia scroll takes place
listView.addEventFilter(ScrollEvent.SCROLL, e -> {
inertia = e.isInertia();
playAnimation();
});
// As soon as the scrolling starts the thumb become visible.
listView.setOnScrollStarted(e -> {
sbTouchTimeline.stop();
thumb.setOpacity(1);
touchReleased = false;
});
} // changed
private Timeline sbTouchTimeline;
private KeyFrame sbTouchKF1, sbTouchKF2;
// Initialize the animation that hides the thumb when no scrolling takes place.
private void initHideBarAnimation() {
if (sbTouchTimeline == null) {
sbTouchTimeline = new Timeline();
sbTouchKF1 = new KeyFrame(Duration.millis(50), new KeyValue(thumb.opacityProperty(), 1));
sbTouchKF2 = new KeyFrame(Duration.millis(200), (e) -> inertia = false, new KeyValue(thumb.opacityProperty(), 0));
sbTouchTimeline.getKeyFrames().addAll(sbTouchKF1, sbTouchKF2);
} // if
} // initHideBarAnimation
// Play animation whenever touch is released, and when an inertia scroll is running but thumb reached its bounds.
private void playAnimation() {
if(touchReleased)
if(!inertia || (scrollBar.getValue() != 0.0 && scrollBar.getValue() != 1))
sbTouchTimeline.playFromStart();
} // playAnimation()
});
} // changeView
} // ScrollBarView
As mentioned in the comments, the first issue is known, and for now it hasn't been fixed. The problem seems to be related to the initial width of the scrollbar (20 pixels as in desktop), and then is set to 8 pixels (as in touch enabled devices), and moved to its final position with this visible shift of 12 pixels to the right.
As for the second and third issues, if you don't want to patch and build the JDK yourself, it is possible to override the default behavior, as the ScrollBar control is part of the VirtualFlow control of a ListView, and both can be found on runtime via lookups.
Once you have the control, you can play with its visibility according to your needs. The only problem with this property is that it is already bound and constantly called from the layoutChildren method.
This is quite a hacky solution, but it works for both 2) and 3):
public class BasicView extends View {
private final ListView<String> listView;
private ScrollBar scrollbar;
private StackPane thumb;
public BasicView(String name) {
super(name);
listView = new ListView<>();
// add your items
final InvalidationListener skinListener = new InvalidationListener() {
#Override
public void invalidated(Observable observable) {
if (listView.getSkin() != null) {
listView.skinProperty().removeListener(this);
scrollbar = (ScrollBar) listView.lookup(".scroll-bar");
listView.setOnScrollFinished(e -> {
if (thumb != null) {
// "hide" thumb as soon as scroll/drag ends
thumb.setStyle("-fx-background-color: transparent;");
}
});
listView.setOnScrollStarted(e -> {
if (thumb == null) {
thumb = (StackPane) scrollbar.lookup(".thumb");
}
if (thumb != null) {
// "show" thumb again only when scroll/drag starts
thumb.setStyle("-fx-background-color: #898989;");
}
});
}
}
};
listView.skinProperty().addListener(skinListener);
setCenter(listView);
}
}