Skip to content

Commit 9896123

Browse files
[Super Keyboard][Android] - Limit keyboard tracking to between onResume and onPause (Resolves #2622) (#2623)
1 parent 890da5a commit 9896123

File tree

15 files changed

+377
-77
lines changed

15 files changed

+377
-77
lines changed

super_keyboard/android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ buildscript {
99
}
1010

1111
dependencies {
12-
classpath("com.android.tools.build:gradle:7.3.0")
12+
classpath("com.android.tools.build:gradle:8.1.0")
1313
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
1414
}
1515
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.flutterbountyhunters.superkeyboard.super_keyboard
2+
3+
import android.util.Log
4+
5+
object SuperKeyboardLog {
6+
var isLoggingEnabled: Boolean = false
7+
8+
fun d(tag: String, message: String) {
9+
if (isLoggingEnabled) Log.d(tag, message)
10+
}
11+
12+
fun i(tag: String, message: String) {
13+
if (isLoggingEnabled) Log.i(tag, message)
14+
}
15+
16+
fun w(tag: String, message: String) {
17+
if (isLoggingEnabled) Log.w(tag, message)
18+
}
19+
20+
fun e(tag: String, message: String, throwable: Throwable? = null) {
21+
if (isLoggingEnabled) Log.e(tag, message, throwable)
22+
}
23+
24+
fun v(tag: String, message: String) {
25+
if (isLoggingEnabled) Log.v(tag, message)
26+
}
27+
}

super_keyboard/android/src/main/kotlin/com/flutterbountyhunters/superkeyboard/super_keyboard/SuperKeyboardPlugin.kt

Lines changed: 114 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
package com.flutterbountyhunters.superkeyboard.super_keyboard
22

33
import android.app.Activity
4+
import android.util.Log
45
import android.view.View
56
import android.view.ViewGroup
67
import android.view.inputmethod.InputMethodManager
78
import androidx.core.view.OnApplyWindowInsetsListener
89
import androidx.core.view.ViewCompat
910
import androidx.core.view.WindowInsetsAnimationCompat
1011
import androidx.core.view.WindowInsetsCompat
12+
import androidx.lifecycle.DefaultLifecycleObserver
13+
import androidx.lifecycle.Lifecycle
14+
import androidx.lifecycle.LifecycleOwner
1115
import io.flutter.embedding.engine.plugins.FlutterPlugin
1216
import io.flutter.embedding.engine.plugins.activity.ActivityAware
1317
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
18+
import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter
1419
import io.flutter.plugin.common.MethodChannel
1520

1621

@@ -22,9 +27,15 @@ import io.flutter.plugin.common.MethodChannel
2227
*
2328
* Android Docs: https://developer.android.com/develop/ui/views/layout/sw-keyboard
2429
*/
25-
class SuperKeyboardPlugin: FlutterPlugin, ActivityAware, OnApplyWindowInsetsListener {
30+
class SuperKeyboardPlugin: FlutterPlugin, ActivityAware, DefaultLifecycleObserver, OnApplyWindowInsetsListener {
2631
private lateinit var channel : MethodChannel
2732

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+
2839
// The root view within the Android Activity.
2940
private var mainView: View? = null
3041

@@ -35,18 +46,82 @@ class SuperKeyboardPlugin: FlutterPlugin, ActivityAware, OnApplyWindowInsetsList
3546
private var keyboardState: KeyboardState = KeyboardState.Closed
3647

3748
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
49+
SuperKeyboardLog.d("super_keyboard", "Attached to Flutter engine")
3850
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+
}
3965
}
4066

4167
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
4395
}
4496

4597
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
47111
}
48112

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+
50125
mainView = activity.findViewById<ViewGroup>(android.R.id.content)
51126
ime = activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
52127
if (mainView == null) {
@@ -93,8 +168,10 @@ class SuperKeyboardPlugin: FlutterPlugin, ActivityAware, OnApplyWindowInsetsList
93168
) {
94169
// Report whether the keyboard has fully opened or fully closed.
95170
if (keyboardState == KeyboardState.Opening) {
171+
keyboardState = KeyboardState.Open
96172
channel.invokeMethod("keyboardOpened", null)
97173
} else if (keyboardState == KeyboardState.Closing) {
174+
keyboardState = KeyboardState.Closed
98175
channel.invokeMethod("keyboardClosed", null)
99176
}
100177
}
@@ -103,29 +180,51 @@ class SuperKeyboardPlugin: FlutterPlugin, ActivityAware, OnApplyWindowInsetsList
103180
}
104181

105182
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+
106198
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}")
107203

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
109205
// 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.
110209
if (imeVisible && keyboardState != KeyboardState.Opening && keyboardState != KeyboardState.Open) {
210+
SuperKeyboardLog.d("super_keyboard", "Setting keyboard state to Opening")
111211
channel.invokeMethod("keyboardOpening", null)
112212
keyboardState = KeyboardState.Opening
113213
} 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+
}
116223
}
117224

118225
return insets
119226
}
120227

121-
override fun onDetachedFromActivityForConfigChanges() {
122-
stopListeningForKeyboardChanges()
123-
}
124-
125-
override fun onDetachedFromActivity() {
126-
stopListeningForKeyboardChanges()
127-
}
128-
129228
private fun stopListeningForKeyboardChanges() {
130229
if (mainView == null) {
131230
return;
@@ -136,8 +235,6 @@ class SuperKeyboardPlugin: FlutterPlugin, ActivityAware, OnApplyWindowInsetsList
136235

137236
mainView = null
138237
}
139-
140-
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {}
141238
}
142239

143240
private enum class KeyboardState {

super_keyboard/example/.metadata

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This file tracks properties of this Flutter project.
2+
# Used by Flutter tool to assess capabilities and perform upgrades etc.
3+
#
4+
# This file should be version controlled and should not be manually edited.
5+
6+
version:
7+
revision: "17025dd88227cd9532c33fa78f5250d548d87e9a"
8+
channel: "stable"
9+
10+
project_type: app
11+
12+
# Tracks metadata for the flutter migrate command
13+
migration:
14+
platforms:
15+
- platform: root
16+
create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
17+
base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
18+
- platform: android
19+
create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
20+
base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
21+
22+
# User provided section
23+
24+
# List of Local paths (relative to this file) that should be
25+
# ignored by the migrate tool.
26+
#
27+
# Files that are not part of the templates will be ignored by default.
28+
unmanaged_files:
29+
- 'lib/main.dart'
30+
- 'ios/Runner.xcodeproj/project.pbxproj'
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
allprojects {
2+
repositories {
3+
google()
4+
mavenCentral()
5+
}
6+
}
7+
8+
rootProject.buildDir = "../build"
9+
subprojects {
10+
project.buildDir = "${rootProject.buildDir}/${project.name}"
11+
}
12+
subprojects {
13+
project.evaluationDependsOn(":app")
14+
}
15+
16+
tasks.register("clean", Delete) {
17+
delete rootProject.buildDir
18+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
2+
android.useAndroidX=true
3+
android.enableJetifier=true
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
pluginManagement {
2+
def flutterSdkPath = {
3+
def properties = new Properties()
4+
file("local.properties").withInputStream { properties.load(it) }
5+
def flutterSdkPath = properties.getProperty("flutter.sdk")
6+
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
7+
return flutterSdkPath
8+
}()
9+
10+
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
11+
12+
repositories {
13+
google()
14+
mavenCentral()
15+
gradlePluginPortal()
16+
}
17+
}
18+
19+
plugins {
20+
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
21+
id "com.android.application" version "8.2.2" apply false
22+
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
23+
}
24+
25+
include ":app"

super_keyboard/example/ios/Podfile.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ EXTERNAL SOURCES:
2020

2121
SPEC CHECKSUMS:
2222
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
23-
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
24-
super_keyboard: 8e7a8c2e2499f32476103eb3be4069f2545fc92a
23+
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
24+
super_keyboard: 016de6ce9ab826f9a0b185608209d6a3b556d577
2525

2626
PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796
2727

28-
COCOAPODS: 1.14.2
28+
COCOAPODS: 1.16.2

0 commit comments

Comments
 (0)