Skip to content

Android keyboard performance patch#155

Open
PovilasKorop wants to merge 1 commit into
NativePHP:mainfrom
PovilasKorop:main
Open

Android keyboard performance patch#155
PovilasKorop wants to merge 1 commit into
NativePHP:mainfrom
PovilasKorop:main

Conversation

@PovilasKorop
Copy link
Copy Markdown

Hey guys,
First, warning - it's a vibe-coded patch with Opus, as I'm not a mobile/Java developer, so please take it either "as is", or some parts of it, or just as an idea. But it actually solved a real issue reported by real audience.

When I launched my app Checklisty, some people reported slow keyboard movement on Android.

Screenshot 2026-06-05 at 12 31 23 Screenshot 2026-06-05 at 12 32 39 Screenshot 2026-06-05 at 12 32 56

I didn't reproduced it at first, but then tried on older phones, and the slowness became apparent on Samsung Galaxy S20+ and Galaxy A55. It didn't appear on the newer Samsung Galaxy S23.

Here's the video of the slowness:

NativePHP-Keyboard-Slowness_timebolted.mp4

Or link to Google Drive if the file upload above doesn't work:
https://drive.google.com/file/d/1b-dFfkWce9I_R43-wBwFRPvOnZbfGrR1/view?usp=drive_link

And here are the comments from Claude Code related to this PR.
As I mentioned I'm not a pro here, but I thought I would still contribute THIS after the investigation and Claude Code session than just reporting the issue.

Hope that helps!


Android Keyboard Performance Patch (NativePHP Mobile)

Fixes the slow / clunky / glitchy on-screen keyboard animation reported on Android.
All changes live in one file:

Background: what the recording showed

Frame-by-frame analysis of a screen recording (full frame rate) measured:

  • Keyboard open animation: ~175–185 ms, consistent across every open (no real cold-start penalty in duration).
  • Every open began with a brief white blank gap: the bottom navigation slid away the instant the keyboard became visible (150 ms), but the keyboard itself needs ~185 ms to arrive — leaving an empty, bright‑white region in between. On the first open this gap was ~83 ms; on later opens ~35–42 ms.
  • During the keyboard animation, an inset listener was firing on every animation frame and re-running a large evaluateJavascript each time.

So two root causes: (a) per-frame main-thread work during the animation, and (b) a white gap caused by the bottom nav vacating its space before the keyboard covered it.


Change 1 — Skip redundant safe-area JS injection on every keyboard frame

Why: setOnApplyWindowInsetsListener fires on every frame of the keyboard (IME) animation, because the IME inset changes each frame. The listener unconditionally called injectSafeAreaInsets(...), which runs a ~70-line evaluateJavascript (removes a <style>, recreates it, writes 4 CSS variables, adds a class) against the WebView. That meant ~60 heavy JS injections per single keyboard open/close, clogging the main thread and making the keyboard feel slow/janky. The system bars do not change while the keyboard animates, so this work is entirely redundant. We guard it so it only runs when the system-bar insets actually change.

1a. New field — after template line 67

File: resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/MainActivity.kt

     private var pendingInsets: Insets? = null
+    // Last applied system-bar insets, used to skip redundant safe-area JS injection
+    // on every keyboard-animation frame (the IME inset changes each frame, but the
+    // system bars do not).
+    private var lastSystemBars: Insets? = null
     private var showSplash by mutableStateOf(true)

1b. Guard the injection — template lines 94–97

File: resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/MainActivity.kt

-            // Inject CSS custom properties into WebView if ready
-            if (::webViewManager.isInitialized) {
+            // Inject CSS custom properties into WebView if ready.
+            // Only re-inject when the system bars actually change. This listener fires
+            // on EVERY frame of the keyboard (IME) animation; without this guard the
+            // heavy evaluateJavascript below would run ~60x per keyboard open/close,
+            // making the keyboard feel slow and janky.
+            if (::webViewManager.isInitialized && systemBars != lastSystemBars) {
+                lastSystemBars = systemBars
                 injectSafeAreaInsets(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
             }

Change 2 — Resize the WebView once, not every frame

Why: WindowInsets.ime is the animated inset value — it changes every frame, so the WebView's bottom padding (and therefore its size) was re-applied on every frame, forcing a full web-page reflow per frame. WindowInsets.imeAnimationTarget is the final keyboard height, so the WebView resizes once when the keyboard toggles while the system still animates the keyboard sliding over the content. This removes ~60 per-frame WebView reflows.

2a. Use the animation target — template line 890

File: resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/MainActivity.kt

                                 .consumeWindowInsets(paddingValues)
-                                .windowInsetsPadding(WindowInsets.ime),
+                                // Use imeAnimationTarget (final keyboard height) instead of
+                                // ime (per-frame animated value) so the WebView resizes ONCE
+                                // when the keyboard toggles, rather than reflowing the web page
+                                // on every animation frame. The system still animates the
+                                // keyboard sliding over the content.
+                                .windowInsetsPadding(WindowInsets.imeAnimationTarget),

2b. Opt in to the experimental API — before MainScreen() at template line 829

imeAnimationTarget is annotated @ExperimentalLayoutApi, so the composable that uses it must opt in or the build fails with "The API of this layout is experimental…".

File: resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/MainActivity.kt

+    @OptIn(ExperimentalLayoutApi::class)
     @Composable
     private fun MainScreen() {

Change 3 — Don't flash white during the keyboard/reflow gap

Why: During the brief gap (and any WebView reflow), the WebView's default white background was showing as a bright flash. Making the WebView transparent and putting a theme-matched solid color behind it means the gap shows a matching color instead of a jarring white flash — a big improvement in dark mode especially.

3a. Transparent WebView — in the factory, after template line 114

File: resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/MainActivity.kt

             settings.mediaPlaybackRequiresUserGesture = false
+            // Transparent so that during a reflow/keyboard transition the themed
+            // background behind the WebView shows through instead of a white flash.
+            setBackgroundColor(android.graphics.Color.TRANSPARENT)
         }

3b. Themed backdrop behind the WebView — template line 830

File: resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/MainActivity.kt

     private fun MainScreen() {
-        Box(Modifier.fillMaxSize()) {
+        // Solid themed backdrop behind the (transparent) WebView so any keyboard/
+        // reflow gap shows a matching color instead of a bright white flash.
+        Box(
+            Modifier
+                .fillMaxSize()
+                .background(if (isSystemInDarkTheme()) Color.Black else Color.White)
+        ) {

Change 4 — Keep the bottom nav in place during keyboard transitions

Why: This is the direct cause of the white gap the recording revealed. The bottom nav slid out (150 ms) the instant the keyboard became visible, but the keyboard takes ~185 ms to appear — so for ~40–80 ms there was an empty region (nav already gone, keyboard not yet there). Keeping the nav visible lets the keyboard slide up over it as one continuous motion, eliminating the gap.

Trade-off: the bottom nav now stays visible just above the keyboard while typing instead of disappearing. If that is undesirable, an alternative is to hide it instantly (no slide) or delay the hide until the keyboard is up — but keeping it visible was the simplest fix that removed the gap.

4a. Remove the now-unused state read — template line 964

File: resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/MainActivity.kt

     private fun BottomNavigationContent() {
-        val isKeyboardVisible by NativeUIState.isKeyboardVisible
         val bottomNavData by NativeUIState.bottomNavData

Note: NativeUIState.setKeyboardVisible(...) is still called elsewhere (it also injects the keyboard-visible body class for CSS), so the state itself stays; only this consumer is removed.

4b. Always show the nav — template lines 971–973

File: resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/MainActivity.kt

-        // Animate bottom nav visibility - slide down when keyboard opens
+        // Keep the bottom nav in place during keyboard transitions. Previously it
+        // slid out the instant the keyboard became visible (150ms), but the keyboard
+        // takes ~185ms to appear, leaving an empty white gap that made the keyboard
+        // look slow/glitchy. Keeping it visible lets the keyboard slide up over it
+        // as a single, continuous motion.
         AnimatedVisibility(
-            visible = !isKeyboardVisible,
+            visible = true,

Summary

# What Template line(s) Why
1 Guard injectSafeAreaInsets with lastSystemBars 67, 94–97 Stop ~60 heavy JS injections per keyboard animation
2 imeAnimationTarget + @OptIn 829, 890 Resize/reflow the WebView once instead of every frame
3 Transparent WebView + themed backdrop 114, 830 No white flash during the reflow/keyboard gap
4 Bottom nav stays visible 964, 971–973 Removes the white gap (nav no longer vacates before keyboard arrives)

Not addressed (out of scope): the first-show input latency (IME / WebView cold-start before anything happens) — that is system warm-up, not in this code path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant