├── README.md
├── appdelegate_stuff.swift
└── kbtestwidget.dart
/README.md:
--------------------------------------------------------------------------------
1 | # crazy_cool_keyboard_for_flutter
2 | Lets Flutter take control over the iOS soft keyboard position. This allows for a smoother UI experience and lets you swipe the keyboard away too!
3 |
4 | So far it's just a proof of concept and not production ready and is meant to inspire you all. Please don't hesitate to improve on this. For now I don't really have much time to improve it myself unfortunately.
5 |
6 | To install:
7 | - wrap the widget around a Scaffold and set its avoidBottomViewInsets to false
8 | - add the Swift code inside the AppDelegate class in AppDelegate.swift
9 |
10 | Make sure you have textfield or something that triggers the on screen keyboard. You should be able to swipe the keyboard away, just make sure you start swiping from outside the keyboard.
11 |
12 | Here the keyboard bounces using a Flutter bounce curve. You can easily choose you own curve of course:
13 |
14 |
15 |
16 |
17 | In this example you need to drag past half way to trigger a close, or else it bounces back:
18 |
19 |
20 |
--------------------------------------------------------------------------------
/appdelegate_stuff.swift:
--------------------------------------------------------------------------------
1 | var methodChannel: FlutterMethodChannel?
2 |
3 | override func applicationDidBecomeActive(_ application: UIApplication) {
4 |
5 | if methodChannel == nil, let rootViewController = window?.rootViewController as? FlutterViewController {
6 |
7 | methodChannel = FlutterMethodChannel(name: "kbtestwidget", binaryMessenger: rootViewController as! FlutterBinaryMessenger)
8 |
9 | //this method basically sets the y position of the keyboard to what Flutter says it to
10 | methodChannel?.setMethodCallHandler {(call: FlutterMethodCall, result: FlutterResult) -> Void in
11 | if UIApplication.shared.windows.count > 2, let kbWindow = UIApplication.shared.windows.last {
12 | DispatchQueue.main.async(execute: {
13 | kbWindow.frame = CGRect.init(x: kbWindow.frame.minX, y: call.arguments as! CGFloat, width: kbWindow.frame.width, height: kbWindow.frame.height)
14 | })
15 | }
16 | result(true)
17 | }
18 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardNotification(notification:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
19 | }
20 |
21 | }
22 |
23 | @objc func keyboardNotification(notification: NSNotification) {
24 |
25 | if let userInfo = notification.userInfo {
26 |
27 | let startFrame = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue
28 | let endFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
29 | let duration:TimeInterval = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0
30 | let animationCurveRawNSN = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber
31 | let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIView.AnimationOptions.curveEaseInOut.rawValue
32 | let animationCurve:UIView.AnimationOptions = UIView.AnimationOptions(rawValue: animationCurveRaw)
33 |
34 | if let startFrame = startFrame, let endFrame = endFrame {
35 | //print(startFrame.size.height)
36 | //print(endFrame.size.height)
37 |
38 | if startFrame.origin.y == endFrame.origin.y
39 | {
40 | return
41 | }
42 |
43 | if startFrame.origin.y > endFrame.origin.y //keyboard show
44 | {
45 | //tell Flutter the keyboard is showing and its height
46 | methodChannel?.invokeMethod("kbshow", arguments: endFrame.size.height)
47 |
48 | //this moves the keyboard frame down with the same animation curve as it is sliding up, effectively keeping it stationary just below the screen
49 | if UIApplication.shared.windows.count > 2, let kbWindow = UIApplication.shared.windows.last {
50 | UIView.animate(withDuration: duration,
51 | delay: TimeInterval(0.0),
52 | options: animationCurve,
53 | animations: {
54 | kbWindow.frame = CGRect.init(x: kbWindow.frame.minX, y: kbWindow.frame.minY + endFrame.size.height, width: kbWindow.frame.width, height: kbWindow.frame.height)
55 | })
56 | }
57 | return;
58 | }
59 | else //keyboard hide
60 | {
61 | //tell Flutter the keyboard is hiding and its height
62 | methodChannel?.invokeMethod("kbhide", arguments: endFrame.size.height)
63 |
64 | //this moves the keyboard frame up with the same animation curve as it is sliding down, effectively keeping it stationary for about 0.4 seconds before it disappears
65 | if UIApplication.shared.windows.count > 2, let kbWindow = UIApplication.shared.windows.last {
66 | UIView.animate(withDuration: duration,
67 | delay: TimeInterval(0.0),
68 | options: animationCurve,
69 | animations: {
70 | kbWindow.subviews[0].subviews[0].frame = CGRect.init(x: kbWindow.subviews[0].subviews[0].frame.minX+0.0, y: kbWindow.subviews[0].subviews[0].frame.minY - startFrame.size.height, width: kbWindow.subviews[0].subviews[0].frame.width, height: kbWindow.subviews[0].subviews[0].frame.height)
71 | })
72 | }
73 | return;
74 | }
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/kbtestwidget.dart:
--------------------------------------------------------------------------------
1 | class KBTestWidget extends StatefulWidget {
2 | @override
3 | _KBTestWidgetState createState() => _KBTestWidgetState();
4 | KBTestWidget({Key key, this.child});
5 | final Widget child;
6 | }
7 |
8 | class _KBTestWidgetState extends State with SingleTickerProviderStateMixin {
9 | AnimationController _animationController;
10 | Animation _animation;
11 | var _kbHeight = 0.0;
12 | var _kbShown = false;
13 | final _methodChannel = const MethodChannel('kbtestwidget');
14 | final _fps = 60; //TODO get actual hertz somewhere
15 |
16 | @override
17 | void initState() {
18 | super.initState();
19 | _animationController = AnimationController(
20 | vsync: this,
21 | duration: const Duration(milliseconds: 400),
22 | reverseDuration: const Duration(milliseconds: 400),
23 | value: _kbHeight,
24 | );
25 | _setAnimation(begin: 0.0, end: 1.0);
26 |
27 | _methodChannel.setMethodCallHandler(this._didRecieveNativeCall);
28 | }
29 |
30 | @override
31 | void dispose() {
32 | super.dispose();
33 | _animationController.dispose();
34 | }
35 |
36 | _setAnimation({double begin, double end}) {
37 | _animation = Tween(begin: begin, end: end).animate(CurvedAnimation(
38 | parent: _animationController,
39 | curve: Curves.easeOutCubic,
40 | reverseCurve: Curves.easeInCubic,
41 | ));
42 | }
43 |
44 | _startAnimation([bool forward = true]) {
45 | //animation starts at 0, but since it's already at 0 when it starts the first frame is just wasted time, so the value in from: makes it skips this while keeping the curve right
46 | final firstFrameSkip = 1000000 / (_fps * _animationController.duration.inMicroseconds);
47 | if (forward) {
48 | _animationController.forward(from: firstFrameSkip).whenComplete(() => _setAnimation(begin: 0.0, end: 1.0));
49 | } else {
50 | _animationController.reverse(from: 1 - firstFrameSkip);
51 | }
52 | }
53 |
54 | Future _didRecieveNativeCall(MethodCall call) async {
55 | print(call);
56 | _kbHeight = call.arguments;
57 |
58 | if (call.method == 'kbshow') {
59 | _setAnimation(begin: 0.0, end: 1.0);
60 | _startAnimation(true);
61 | _kbShown = true;
62 | } else if (call.method == 'kbhide') {
63 | _startAnimation(false);
64 | _kbShown = false;
65 | FocusManager.instance.primaryFocus.unfocus(); //failsafe just in case the hide is not triggered by actual keyboard hiding
66 | }
67 | }
68 |
69 | @override
70 | Widget build(BuildContext context) {
71 | return GestureDetector(
72 | onPanUpdate: (details) {
73 | if (!_kbShown) return;
74 |
75 | final pos = min(MediaQuery.of(context).size.height - details.localPosition.dy, _kbHeight);
76 | setState(() {
77 | _setAnimation(begin: pos / _kbHeight, end: pos / _kbHeight);
78 | });
79 | },
80 | onPanEnd: (details) {
81 | if (!_kbShown) return;
82 |
83 | if (_animation.value > 0.5) {
84 | _animation = Tween(begin: _animation.value, end: 1.0).animate(CurvedAnimation(
85 | parent: _animationController,
86 | curve: Curves.bounceOut,
87 | reverseCurve: Curves.easeInCubic,
88 | ));
89 | _startAnimation(true);
90 | } else {
91 | _setAnimation(begin: 0.0, end: _animation.value);
92 | FocusManager.instance.primaryFocus.unfocus();
93 | }
94 | },
95 | child: Column(
96 | children: [
97 | Expanded(
98 | child: widget.child,
99 | ),
100 | Material(
101 | color: Colors.blueGrey, //set the keyboard background color here
102 | child: AnimatedBuilder(
103 | animation: _animationController,
104 | builder: (_, child) {
105 | //after Flutter rendered the frame move the native keyboard, still the keyboard may be a frame ahead sometimes (in debug builds anyway), pretty weird
106 | WidgetsBinding.instance.addPostFrameCallback((_) {
107 | _methodChannel.invokeMethod('kbdown', _kbHeight + (_animation.value * _kbHeight) * -1);
108 | });
109 |
110 | return SizedBox(
111 | width: double.infinity,
112 | height: _animation.value * _kbHeight,
113 | child: child,
114 | );
115 | },
116 | ),
117 | )
118 | ],
119 | ),
120 | );
121 | }
122 | }
123 |
--------------------------------------------------------------------------------