fix(native): the sign-in bar's room for the keyboard holds, and Android 7 to 10 see the keyboard

On iOS the keyboard plugin sizes the body and zeroes the scroll inset in a
frame update that runs after the keyboard is up, so the room made for it was
taken back. The inset is now kept while a sign-in page shows, the body gets its
height back after that update, and with the keyboard down the page can scroll
past the bar. On Android before 11 the keyboard went unseen in the insets
Capacitor passes on, so the bar stayed over the form; it is read off the
window's own insets now.
This commit is contained in:
Maurice
2026-11-10 09:50:29 +01:00
parent 57c9c9d5f2
commit 7506eaa4aa
2 changed files with 68 additions and 26 deletions
@@ -10,7 +10,6 @@ import android.view.KeyboardShortcutGroup;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
@@ -146,9 +145,14 @@ public class MainActivity extends BridgeActivity {
root.addView(bar, params);
// Above the gesture bar, and above the keyboard while it is up.
bar.setOnApplyWindowInsetsListener((v, insets) -> {
boolean modern = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R;
int bottom = modern ? insets.getInsets(WindowInsets.Type.systemBars()).bottom : insets.getStableInsetBottom();
boolean imeVisible = modern ? insets.isVisible(WindowInsets.Type.ime()) : insets.getSystemWindowInsetBottom() > bottom * 3;
// Read off the window's own insets: Capacitor hands the views below the
// decor a copy with the system bars zeroed, and before Android 11 that
// takes the keyboard with it.
androidx.core.view.WindowInsetsCompat window = androidx.core.view.ViewCompat.getRootWindowInsets(getWindow().getDecorView());
int bottom = window != null
? window.getInsets(androidx.core.view.WindowInsetsCompat.Type.navigationBars()).bottom
: insets.getStableInsetBottom();
boolean imeVisible = window != null && window.isVisible(androidx.core.view.WindowInsetsCompat.Type.ime());
if (imeVisible != keyboardUp) {
keyboardUp = imeVisible;
updateSignInBar();
@@ -66,17 +66,28 @@ class TrekBridgeViewController: CAPBridgeViewController {
/// which TREK's own script makes everywhere else (native/keepFieldInView.ts).
private var signInBar: UIView?
private var urlObservation: NSKeyValueObservation?
private var insetObservation: NSKeyValueObservation?
private var onSignInPage = false
private var keyboardUp = false
/// How far the keyboard reaches into the view while it is up.
private var keyboardRoom: CGFloat = 0
/// Whether the bottom inset is this controller's, so leaving the page puts it back.
private var ownsInset = false
private var bodyReset: DispatchWorkItem?
private func watchSignInPages() {
urlObservation = webView?.observe(\.url, options: [.initial, .new]) { [weak self] webView, _ in
let signIn = TrekServerStore.isSignInHost(webView.url)
DispatchQueue.main.async { self?.signInPageChanged(signIn) }
}
// The keyboard plugin zeroes the scroll inset on every frame change it
// makes, the last one a while after the keyboard is up. On a sign-in
// page the inset wanted here is put back each time.
insetObservation = webView?.scrollView.observe(\.contentInset, options: [.new]) { [weak self] _, _ in
DispatchQueue.main.async { self?.applyInset() }
}
let center = NotificationCenter.default
center.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
center.addObserver(self, selector: #selector(keyboardDidShow(_:)), name: UIResponder.keyboardDidShowNotification, object: nil)
center.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
center.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}
@@ -89,34 +100,61 @@ class TrekBridgeViewController: CAPBridgeViewController {
/// Out of the way while the keyboard is up: the form needs the room.
private func updateSignInBar() {
signInBar?.isHidden = !(onSignInPage && !keyboardUp)
applyInset()
}
@objc private func keyboardWillShow() {
keyboardUp = true
updateSignInBar()
}
/// The keyboard plugin sizes the body to the visible area and resets the
/// scroll inset on every frame change, which left a foreign page with no way
/// to scroll a button under the keyboard into view. After its last change,
/// the page gets its height back and as much room below as the keyboard takes.
@objc private func keyboardDidShow(_ note: Notification) {
guard onSignInPage, let frame = note.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
let room = max(0, frame.height - view.safeAreaInsets.bottom)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
guard let self, self.onSignInPage, self.keyboardUp, let webView = self.webView else { return }
webView.evaluateJavaScript("document.body.style.height = ''", completionHandler: nil)
webView.scrollView.contentInset.bottom = room
webView.scrollView.verticalScrollIndicatorInsets.bottom = room
/// On a sign-in page the page can scroll past what covers its end: the
/// keyboard while it is up, the bar otherwise. TREK's own pages are left alone.
private func applyInset() {
guard let scrollView = webView?.scrollView else { return }
let wanted: CGFloat
if onSignInPage {
if keyboardUp {
wanted = keyboardRoom
} else if let bar = signInBar {
view.layoutIfNeeded()
wanted = max(0, view.bounds.height - bar.frame.minY)
} else {
wanted = 0
}
ownsInset = true
} else if ownsInset {
wanted = 0
ownsInset = false
} else {
return
}
if scrollView.contentInset.bottom != wanted { scrollView.contentInset.bottom = wanted }
if scrollView.verticalScrollIndicatorInsets.bottom != wanted { scrollView.verticalScrollIndicatorInsets.bottom = wanted }
}
/// The keyboard plugin also sizes the body to the area above the keyboard,
/// after the keyboard's animation and a further 0.2 s. On a sign-in page the
/// body gets its height back once that has run, so the button under the
/// keyboard can be scrolled into view through the inset above.
@objc private func keyboardWillShow(_ note: Notification) {
keyboardUp = true
if let frame = note.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
keyboardRoom = max(0, view.bounds.height - view.convert(frame, from: nil).minY)
}
bodyReset?.cancel()
if onSignInPage {
let duration = note.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double ?? 0.25
let reset = DispatchWorkItem { [weak self] in
guard let self, self.onSignInPage, self.keyboardUp, let webView = self.webView else { return }
webView.evaluateJavaScript("document.body.style.height = ''", completionHandler: nil)
self.applyInset()
}
bodyReset = reset
DispatchQueue.main.asyncAfter(deadline: .now() + duration + 0.35, execute: reset)
}
updateSignInBar()
}
@objc private func keyboardWillHide() {
keyboardUp = false
bodyReset?.cancel()
updateSignInBar()
guard onSignInPage, let webView else { return }
webView.scrollView.contentInset.bottom = 0
webView.scrollView.verticalScrollIndicatorInsets.bottom = 0
}
private func makeSignInBar() -> UIView {