1
1
package com.flutterbountyhunters.superkeyboard.super_keyboard
2
2
3
3
import android.app.Activity
4
+ import android.util.Log
4
5
import android.view.View
5
6
import android.view.ViewGroup
6
7
import android.view.inputmethod.InputMethodManager
7
8
import androidx.core.view.OnApplyWindowInsetsListener
8
9
import androidx.core.view.ViewCompat
9
10
import androidx.core.view.WindowInsetsAnimationCompat
10
11
import androidx.core.view.WindowInsetsCompat
12
+ import androidx.lifecycle.DefaultLifecycleObserver
13
+ import androidx.lifecycle.Lifecycle
14
+ import androidx.lifecycle.LifecycleOwner
11
15
import io.flutter.embedding.engine.plugins.FlutterPlugin
12
16
import io.flutter.embedding.engine.plugins.activity.ActivityAware
13
17
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
18
+ import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter
14
19
import io.flutter.plugin.common.MethodChannel
15
20
16
21
@@ -22,9 +27,15 @@ import io.flutter.plugin.common.MethodChannel
22
27
*
23
28
* Android Docs: https://developer.android.com/develop/ui/views/layout/sw-keyboard
24
29
*/
25
- class SuperKeyboardPlugin : FlutterPlugin , ActivityAware , OnApplyWindowInsetsListener {
30
+ class SuperKeyboardPlugin : FlutterPlugin , ActivityAware , DefaultLifecycleObserver , OnApplyWindowInsetsListener {
26
31
private lateinit var channel : MethodChannel
27
32
33
+ private var binding: ActivityPluginBinding ? = null
34
+
35
+ // The Activity's lifecycle, which reports things like when the Android
36
+ // app comes into the foreground from the background.
37
+ private var lifecycle: Lifecycle ? = null
38
+
28
39
// The root view within the Android Activity.
29
40
private var mainView: View ? = null
30
41
@@ -35,18 +46,82 @@ class SuperKeyboardPlugin: FlutterPlugin, ActivityAware, OnApplyWindowInsetsList
35
46
private var keyboardState: KeyboardState = KeyboardState .Closed
36
47
37
48
override fun onAttachedToEngine (flutterPluginBinding : FlutterPlugin .FlutterPluginBinding ) {
49
+ SuperKeyboardLog .d(" super_keyboard" , " Attached to Flutter engine" )
38
50
channel = MethodChannel (flutterPluginBinding.binaryMessenger, " super_keyboard_android" )
51
+
52
+ channel.setMethodCallHandler { call, result ->
53
+ when (call.method) {
54
+ " startLogging" -> {
55
+ SuperKeyboardLog .isLoggingEnabled = true
56
+ result.success(null )
57
+ }
58
+ " stopLogging" -> {
59
+ SuperKeyboardLog .isLoggingEnabled = false
60
+ result.success(null )
61
+ }
62
+ else -> result.notImplemented()
63
+ }
64
+ }
39
65
}
40
66
41
67
override fun onAttachedToActivity (binding : ActivityPluginBinding ) {
42
- startListeningForKeyboardChanges(binding.activity)
68
+ SuperKeyboardLog .d(" super_keyboard" , " Attached to Flutter Activity" )
69
+ this .binding = binding
70
+ startListeningToActivityLifecycle()
71
+ }
72
+
73
+ override fun onResume (owner : LifecycleOwner ) {
74
+ SuperKeyboardLog .d(" super_keyboard" , " Activity Resumed - keyboard state: $keyboardState " )
75
+ startListeningForKeyboardChanges(binding!! )
76
+
77
+ // Specifically in the case of an app resuming, it's possible that the keyboard
78
+ // went from open to closed without us getting a chance to report it. Check if we're
79
+ // closed and if we are, tell the app.
80
+ val insets = ViewCompat .getRootWindowInsets(mainView!! ) ? : return
81
+ if (insets.getInsets(WindowInsetsCompat .Type .ime()).bottom == 0 && keyboardState != KeyboardState .Closed ) {
82
+ keyboardState = KeyboardState .Closed
83
+ channel.invokeMethod(" keyboardClosed" , null )
84
+ }
85
+ }
86
+
87
+ override fun onPause (owner : LifecycleOwner ) {
88
+ SuperKeyboardLog .d(" super_keyboard" , " Activity Paused - keyboard state: $keyboardState " )
89
+ stopListeningForKeyboardChanges()
90
+ }
91
+
92
+ override fun onDetachedFromActivityForConfigChanges () {
93
+ stopListeningToActivityLifecycle()
94
+ this .binding = null
43
95
}
44
96
45
97
override fun onReattachedToActivityForConfigChanges (binding : ActivityPluginBinding ) {
46
- startListeningForKeyboardChanges(binding.activity)
98
+ startListeningToActivityLifecycle()
99
+ this .binding = binding
100
+ }
101
+
102
+ override fun onDetachedFromActivity () {
103
+ SuperKeyboardLog .d(" super_keyboard" , " Detached from Flutter activity" )
104
+ stopListeningToActivityLifecycle()
105
+ this .binding = null
106
+ }
107
+
108
+ override fun onDetachedFromEngine (binding : FlutterPlugin .FlutterPluginBinding ) {
109
+ SuperKeyboardLog .d(" super_keyboard" , " Detached from Flutter engine" )
110
+ this .binding = null
47
111
}
48
112
49
- private fun startListeningForKeyboardChanges (activity : Activity ) {
113
+ private fun startListeningToActivityLifecycle () {
114
+ lifecycle = FlutterLifecycleAdapter .getActivityLifecycle(binding!! )
115
+ lifecycle!! .addObserver(this )
116
+ }
117
+
118
+ private fun stopListeningToActivityLifecycle () {
119
+ lifecycle!! .removeObserver(this );
120
+ }
121
+
122
+ private fun startListeningForKeyboardChanges (binding : ActivityPluginBinding ) {
123
+ val activity = binding.activity
124
+
50
125
mainView = activity.findViewById<ViewGroup >(android.R .id.content)
51
126
ime = activity.getSystemService(Activity .INPUT_METHOD_SERVICE ) as InputMethodManager
52
127
if (mainView == null ) {
@@ -93,8 +168,10 @@ class SuperKeyboardPlugin: FlutterPlugin, ActivityAware, OnApplyWindowInsetsList
93
168
) {
94
169
// Report whether the keyboard has fully opened or fully closed.
95
170
if (keyboardState == KeyboardState .Opening ) {
171
+ keyboardState = KeyboardState .Open
96
172
channel.invokeMethod(" keyboardOpened" , null )
97
173
} else if (keyboardState == KeyboardState .Closing ) {
174
+ keyboardState = KeyboardState .Closed
98
175
channel.invokeMethod(" keyboardClosed" , null )
99
176
}
100
177
}
@@ -103,29 +180,51 @@ class SuperKeyboardPlugin: FlutterPlugin, ActivityAware, OnApplyWindowInsetsList
103
180
}
104
181
105
182
override fun onApplyWindowInsets (v : View , insets : WindowInsetsCompat ): WindowInsetsCompat {
183
+ SuperKeyboardLog .d(" super_keyboard" , " onApplyWindowInsets()" )
184
+ if (lifecycle!! .currentState == Lifecycle .State .CREATED ) {
185
+ // For at least Android API 34, we receive conflicting reports about IME visibility
186
+ // when the app is being backgrounded. First we're told the IME isn't visible, then
187
+ // we're told that it is. In theory, the IME should never be visible when in the CREATED
188
+ // state, so we explicitly tell the app that the keyboard is closed here.
189
+ if (keyboardState != KeyboardState .Closed ) {
190
+ SuperKeyboardLog .d(" super_keyboard" , " Activity is in CREATED state - telling app that keyboard is closed" )
191
+ keyboardState = KeyboardState .Closed
192
+ channel.invokeMethod(" keyboardClosed" , null )
193
+ }
194
+
195
+ return insets
196
+ }
197
+
106
198
val imeVisible = insets.isVisible(WindowInsetsCompat .Type .ime())
199
+ SuperKeyboardLog .d(" super_keyboard" , " Is IME visible? $imeVisible " )
200
+ SuperKeyboardLog .d(" super_keyboard" , " Lifecycle state: ${lifecycle!! .currentState} " )
201
+
202
+ SuperKeyboardLog .d(" super_keyboard" , " Insets: ${insets.getInsets(WindowInsetsCompat .Type .ime()).bottom} " )
107
203
108
- // Note: We only identify opening/closing here. The opened/closed completion
204
+ // Note: We primarily only identify opening/closing here. The opened/closed completion
109
205
// is identified by the window insets animation callback.
206
+ //
207
+ // The exception is that when the Activity resumes, the keyboard might jump immediately
208
+ // to "closed". We catch that situation by looking for a `0` bottom inset.
110
209
if (imeVisible && keyboardState != KeyboardState .Opening && keyboardState != KeyboardState .Open ) {
210
+ SuperKeyboardLog .d(" super_keyboard" , " Setting keyboard state to Opening" )
111
211
channel.invokeMethod(" keyboardOpening" , null )
112
212
keyboardState = KeyboardState .Opening
113
213
} else if (! imeVisible && keyboardState != KeyboardState .Closing && keyboardState != KeyboardState .Closed ) {
114
- channel.invokeMethod(" keyboardClosing" , null )
115
- keyboardState = KeyboardState .Closing
214
+ if (insets.getInsets(WindowInsetsCompat .Type .ime()).bottom == 0 ) {
215
+ SuperKeyboardLog .d(" super_keyboard" , " Setting keyboard state to Closed" )
216
+ channel.invokeMethod(" keyboardClosed" , null )
217
+ keyboardState = KeyboardState .Closed
218
+ } else {
219
+ SuperKeyboardLog .d(" super_keyboard" , " Setting keyboard state to Closing" )
220
+ channel.invokeMethod(" keyboardClosing" , null )
221
+ keyboardState = KeyboardState .Closing
222
+ }
116
223
}
117
224
118
225
return insets
119
226
}
120
227
121
- override fun onDetachedFromActivityForConfigChanges () {
122
- stopListeningForKeyboardChanges()
123
- }
124
-
125
- override fun onDetachedFromActivity () {
126
- stopListeningForKeyboardChanges()
127
- }
128
-
129
228
private fun stopListeningForKeyboardChanges () {
130
229
if (mainView == null ) {
131
230
return ;
@@ -136,8 +235,6 @@ class SuperKeyboardPlugin: FlutterPlugin, ActivityAware, OnApplyWindowInsetsList
136
235
137
236
mainView = null
138
237
}
139
-
140
- override fun onDetachedFromEngine (binding : FlutterPlugin .FlutterPluginBinding ) {}
141
238
}
142
239
143
240
private enum class KeyboardState {
0 commit comments