Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
7.7 KiB
Fix Submit button not visible in passcode view in split screen
Problem
On Android in split screen, the "Enter Passcode" view shows the title and the keypad but no Cancel/Submit buttons — the app cannot be unlocked with a passcode without leaving split screen.
The same PasscodeView also backs SetAppPasscodeView, so "New Passcode" /
"Confirm passcode" (Privacy & security → passcode, and the passcode onboarding in
AppLock.setPasscode) are affected identically.
Desktop is affected too whenever the window is short (below ~590dp of height at a typical width), since desktop always uses the vertical layout.
Cause
Two independent defects, both invisible at full screen.
1. The keypad consumes the space the buttons need
PasscodeView.VerticalLayout is a Column of three children — title, keypad,
buttons Row — and the keypad's size is derived from the constraints it is offered
(PasswordEntry.kt):
val s = if (appPlatform.isAndroid) minOf(maxWidth, maxHeight) / 4 - 1.dp else ...
Column measures unweighted children in order, each with maxHeight reduced by what
the previous ones took, so the keypad — the second child — is offered all
remaining height and a 4-row grid then occupies exactly that. The buttons Row is
measured last, from what is left, and Modifier.heightIn(min = 70.dp) cannot rescue
it: heightIn constrains its target into the incoming constraints, so a 70dp minimum
against a 1dp maximum yields 1dp.
At full screen the bug is dormant because maxWidth is the smaller term there — the
keypad is sized by width and the leftover height goes to the buttons. It appears as
soon as the remaining height drops below the width, which every split-screen half does.
Measured with a Compose harness that renders these composables at fixed window sizes
(411dp wide, density 1, Surface(Modifier.fillMaxSize()) as in LocalAuthentication.kt):
| window | keypad maxHeight |
key size | buttons Row height | Submit |
|---|---|---|---|---|
| 411×914 (full screen) | 805 | 101.75 | 70 @ y=763 | 108×40 ✓ |
| 411×520 | 411 | 101.75 | 0 | 108×0 |
| 411×480 | 371 | 91.75 | 0 | 108×0 |
| 411×445 (split half) | 336 | 83.0 | 1 @ y=444 | 108×1 |
| 411×400 | 291 | 71.75 | 0 | 108×0 |
HorizontalLayout has the same defect in the width axis: its keypad is 4 keys wide
and sized minOf(maxWidth, maxHeight) / 3.5f, so the keypad's width grows with the
available height, and the Cancel/Submit Column — the second child of the Row
— gets the remainder. At 411×365 it is offered maxWidth = 24dp and Submit measures
0×0; at 411×445 it is offered 0dp and Submit is placed at x=441, outside a
411dp-wide window.
This branch matters in split screen because windowOrientation() reads
Configuration.orientation, which is computed from the activity window bounds, not
the device: a split half of a tall phone is PORTRAIT (~411×445) while a half of a
16:9 phone is LANDSCAPE (~411×365). Both layouts are reachable, and both were broken.
2. No window insets
MainActivity calls enableEdgeToEdge(), and this screen never applied insets — at
full screen SpaceEvenly happened to leave the buttons ~80dp clear of the navigation
bar. Once the buttons are laid out correctly in a split half, they land at the window
edge: with fix 1 alone, Submit occupies y=390…430 of a 445dp window, and a 48dp
3-button navigation bar covers y=397…445 — 33 of its 40dp, so the button is drawn
but not reliably tappable in the bottom split window.
Fix
Modifier.weight(1f, fill = false) on the keypad in the vertical layout and on the
keypad column in the horizontal layout (PasscodeView.kt), which requires a modifier
parameter on PasscodeEntry (PasswordEntry.kt):
PasscodeEntry(passcode, true, Modifier.weight(1f, fill = false))
Column/Row measure weighted children after all unweighted ones, so the title
and the buttons are measured first at their natural size and the keypad receives what
is left — the inverse of the current order, with no size constant to keep in sync.
fill = false keeps the keypad at its own size rather than stretching it, and because
a weighted child makes the container expand to the incoming bounded maximum, the
SpaceEvenly / SpaceBetween distribution at full screen is unchanged.
Plus Modifier.systemBarsPadding() on both layout roots.
Measured after the fix:
| window | branch | key size | Submit |
|---|---|---|---|
| 411×445, 24dp status bar (top split half) | vertical | 59.5 | 108×40 @ y=390…430 ✓ |
| 411×445, 48dp nav bar (bottom split half) | vertical | 53.5 | 108×40 @ y=342…382, clear of the bar ✓ |
| 411×365, 48dp nav bar | horizontal | 29.3 | 108×40 @ x=263…371 ✓ |
| 411×914 (full screen) | vertical | 101.75 (unchanged) | 108×40, column y=24…866 |
The insets go on the Column/Row themselves rather than a wrapping Box: Material
Surface lays its content out with propagateMinConstraints = true, so the layout root
receives the window size as a minimum. A plain Box in between drops that minimum
(its own default is false), the Column becomes wrap-content, SpaceEvenly has no
slack left to distribute, and the whole screen top-aligns — measured as the buttons
moving from y=763 to y=544 at full screen. Keeping the padding on the root preserves
the propagation.
Scope / non-goals
- Both files are
commonMain, so the fix covers Android and desktop; on desktopsystemBarsPadding()resolves to zero insets and only theweightchange has an effect (short desktop windows). - The keypad necessarily gets smaller in a small window: 53.5–59.5dp keys in a portrait split half, 29.3dp in the 411×365 horizontal case, where the title, reason and passcode text consume ~170dp before the keypad is measured. Making the passcode screen itself more compact below some height (smaller title, dropping the reason line) is a separate design change and is not attempted here.
systemBarsPadding()also shrinks the keypad in full-screen landscape (79.6 → 59.0dp). That is the same 72dp the bars always occupied — previously the keypad's bottom row extended ~8dp under the navigation bar — but it is a visible change on a screen that did not show the reported bug.- Not changed:
windowOrientation()branching onConfiguration.orientation. Selecting the layout from the measured aspect ratio (as iOS does) would be a better fit for resizable windows, but it changes behaviour on every device rather than fixing this bug.
iOS
Not affected, and for a structural reason worth recording: iOS picks its layout from the
measured geometry rather than an orientation flag —
if g.size.width < g.size.height * 2 / 3 (PasscodeView.swift) — so a short, wide
window (the geometry that breaks Android's vertical layout) selects iOS's horizontal
layout, whose keypad is sized from height (s = height / 5) with the buttons column
explicitly bounded to height / 5 * 3 * 0.97. The vertical layout's width-driven keypad
(s = width / 3) is only ever used when the window is at least 1.5× taller than wide,
where it fits by construction. No iOS change required.
Verification
- Compose measurement harness at the window sizes tabulated above, before and after, for both layout branches and for 24dp/48dp/72dp inset combinations.
- Android arm64 debug APK (
bash ~/build/android.sh) — manual check of Submit in both the top and the bottom split-screen half, and no visual change at full screen. - Linux x86_64 AppImage (
bash ~/build/linux.sh) — manual check with the window resized short, confirming the desktop vertical layout keeps Cancel/Submit visible. - Also exercise "New Passcode" / "Confirm passcode" (
SetAppPasscodeView) in split screen, which sharePasscodeView.