Fix Android window geometry, selection menu tracking
- SurfaceView windows are always translucent, so adjustResize cannot resize them: the keyboard overlapped the bottom of the window and the top bar sat under the status bar. Consume the insets in app instead: a smali-injected PadInsetsListener (identical block in build_emu.sh and build_phone.sh) shrinks the GioView to statusBarBottom..keyboardTop (or ..navBarTop) on every API 30+ insets dispatch, with setDecorFitsSystemWindows(false) so IME insets are delivered at targetSdk 34. - Fix a change-detection bug in the listener: the height write skipped the topMargin/bottomMargin checks on every IME state transition, so the margins were never applied while the keyboard animated. All three params are now checked with a single requestLayout on any change. - Selection menu now re-anchors to the selected word every frame while it is visible and hides when the word scrolls out of the viewport (positionSelectionMenu + regression test). - Doc: development_plan.md section 14 (invariants, geometry contract, build-script identity invariant). Verified on device: top bar below status bar and clickable, bottom bar flush with keyboard top (view 128..1517 at 1080x2400), BACK dismiss + re-tap re-raise without a show loop, typing saves, menu tracks scrolls.
This commit is contained in:
parent
941285e7cc
commit
dcb96d04d8
|
|
@ -843,3 +843,66 @@ Residual (accepted) invariants:
|
|||
goroutine (input-handler closures run on the owner via `SendInput`).
|
||||
- `Logic.FlushAll` is lock-free by design; it must not run concurrently with
|
||||
`Run` (guaranteed by the `Shutdown` order).
|
||||
|
||||
## 14. Android window geometry + selection menu tracking (2026-08-17)
|
||||
|
||||
Three user-visible bugs, all on the Android target: (1) the word-selection
|
||||
menu did not follow the selected text when scrolling; (2) a large empty gap
|
||||
between the keyboard top and the app's bottom bar when the IME was shown;
|
||||
(3) the top bar rendered under the status bar and could not be clicked.
|
||||
|
||||
**Root cause of (2) and (3): the SurfaceView window cannot be resized by
|
||||
Android.** Gio's `GioView` is a `SurfaceView`; the moment it attaches, the
|
||||
window becomes translucent (the surface has a transparent region), and
|
||||
`windowSoftInputMode=adjustResize` no longer resizes it — the keyboard
|
||||
simply overlaps the window's bottom. No theme, background, or flag change
|
||||
can make the window opaque again. The fix is to consume the insets *in
|
||||
app*: a smali-injected `PadInsetsListener` (see `scripts/build_emu.sh`,
|
||||
identical block in `scripts/build_phone.sh`) sets
|
||||
`setOnApplyWindowInsetsListener` on the GioView in `GioActivity.onCreate`
|
||||
(API 21+) and `setDecorFitsSystemWindows(false)` (API 30+, which delivers
|
||||
IME insets at targetSdk 34). On every insets dispatch the listener shrinks
|
||||
the view:
|
||||
|
||||
- `displayH = displayMetrics.heightPixels + statusTop + navBottom` — the
|
||||
metrics report the *content* height, while IME insets are measured from
|
||||
the absolute display bottom; the two frames must be reconciled before
|
||||
subtracting.
|
||||
- `bottomLimit = displayH − imeInset` (IME shown) or `displayH − navBottom`
|
||||
(IME hidden); `height = bottomLimit − statusTop`, clamped ≥ 0.
|
||||
- `topMargin = statusTop`, `bottomMargin = navBottom`, so the view spans
|
||||
exactly `statusBarBottom..keyboardTop` (or `..navBarTop`).
|
||||
|
||||
On API < 30 there are no per-type insets; the listener degrades to no-op
|
||||
and the keyboard overlaps (accepted limitation — both build scripts target
|
||||
minSdk 16).
|
||||
|
||||
**Layout-params invariant (the subtle bug found while verifying):** the
|
||||
height, topMargin, and bottomMargin change-detection checks must *all* run
|
||||
on every dispatch, with a single `requestLayout` if any of the three
|
||||
changed. The first version `goto`-skipped the margin checks whenever the
|
||||
height changed — which is exactly the transition (keyboard show/hide), so
|
||||
the margins were silently never applied during IME state changes and only
|
||||
accidentally picked up on a later duplicate dispatch. The listener now
|
||||
tracks a changed flag across all three checks.
|
||||
|
||||
**Selection menu tracking (1):** the menu anchor is recomputed every frame
|
||||
in the logic goroutine (`positionSelectionMenu`): while a selection is
|
||||
visible, its screen position is derived from the current layout (byte
|
||||
offset → glyph → window-relative Dp) and the menu re-anchors to it; if the
|
||||
selected word scrolls fully out of the viewport the menu hides instead of
|
||||
stranding. Regression: `internal/editor/selection_menu_track_test.go`.
|
||||
|
||||
**Build-script invariant:** the Python patch heredoc in `build_emu.sh` and
|
||||
`build_phone.sh` must stay byte-identical (both inject the same
|
||||
`PadInsetsListener` class and the same `GioActivity` wiring); a drift would
|
||||
produce an emulator APK that behaves differently from the phone APK. The
|
||||
listener uses only int arithmetic and registers v0–v5 (the smali 22c/11x
|
||||
formats accept only 4-bit register operands; apktool 3.0.3's smali also
|
||||
lacks `cmpg-f`).
|
||||
|
||||
On-device verified (Pixel 6 profile, API 35, 1080×2400): top bar below the
|
||||
status bar and clickable; bottom bar flush with the keyboard top (view
|
||||
spans 128..1517 with the IME shown); BACK dismisses the keyboard and
|
||||
re-tap re-raises it without a re-show loop; typing saves; the selection
|
||||
menu tracks small scrolls and hides when the word leaves the viewport.
|
||||
|
|
|
|||
119
internal/editor/selection_menu_track_test.go
Normal file
119
internal/editor/selection_menu_track_test.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
package editor
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"pad/internal/ui"
|
||||
)
|
||||
|
||||
// dpeq compares two Dp values within 0.01 Dp (all menu geometry is
|
||||
// float32 Dp; this is far below one screen pixel at any density).
|
||||
func dpeq(a, b ui.Dp) bool { return math.Abs(float64(a)-float64(b)) < 0.01 }
|
||||
|
||||
// menuTrackState builds a small-file state: a 200-line buffer ("aa" per
|
||||
// line), a selection on line 5 (bytes [15,17)), and a shaped GlyphLayout
|
||||
// covering the WHOLE buffer (the small-file window is the whole buffer, so
|
||||
// the layout is identical before and after a scroll — the scroll changes
|
||||
// only k/r, exactly as in production). Baselines use the shaper's real
|
||||
// convention (drawWrappedText): ascent + lineHeight*lineIndex, ascent = 14.
|
||||
func menuTrackState(t *testing.T) (lh float64) {
|
||||
const ascent float64 = 14 // FontSize; 0 < ascent < lh is the invariant
|
||||
t.Helper()
|
||||
lh = float64(EffectiveLineHeight())
|
||||
// Long enough that the 1904-Dp test viewport overflows and scrolling is
|
||||
// allowed (EditorLayout clamps ScrollOffset to LastLineY-based maxScroll;
|
||||
// a short file would clamp the test scroll back to 0).
|
||||
lines := 200
|
||||
var buf strings.Builder
|
||||
for i := 0; i < lines; i++ {
|
||||
buf.WriteString("aa\n")
|
||||
}
|
||||
TheState = NewState()
|
||||
TheState.Editor.Buffer = buf.String()
|
||||
SetSelection(15, 17) // "aa" on line 5
|
||||
|
||||
gl := ui.GlyphLayout{LineHeight: EffectiveLineHeight()}
|
||||
for i := 0; i < lines; i++ {
|
||||
for c := 0; c < 2; c++ {
|
||||
gl.ByteOffsets = append(gl.ByteOffsets, i*3+c)
|
||||
gl.X = append(gl.X, ui.Dp(10+10*c))
|
||||
gl.Y = append(gl.Y, ui.Dp(ascent+float64(i)*lh))
|
||||
gl.Advance = append(gl.Advance, 10)
|
||||
}
|
||||
}
|
||||
TheState.Editor.GlyphLayout = gl
|
||||
// LastLineY is normally set by the shaper's layout feedback; emulate it.
|
||||
TheState.LastLineY = ui.Dp(float64(lines) * lh)
|
||||
TheState.Editor.IMEWindowStartByte = 0
|
||||
TheState.ScrollOffset = 0
|
||||
return lh
|
||||
}
|
||||
|
||||
// TestSelectionMenu_FollowsTextAcrossScroll is a regression test for the
|
||||
// selection menu staying at a FIXED SCREEN POSITION while the user scrolled:
|
||||
// the menu must track the selected text (the menu is anchored to the text,
|
||||
// not to where it was first shown). The menu's Y must move down/up by exactly
|
||||
// the scroll delta while the anchor stays in view.
|
||||
func TestSelectionMenu_FollowsTextAcrossScroll(t *testing.T) {
|
||||
lh := menuTrackState(t)
|
||||
|
||||
// Lay out once so EditorRegion (the text region the menu is placed in)
|
||||
// is established, exactly as in production.
|
||||
EditorLayout(ui.Dp(1000), ui.Dp(2000), false)
|
||||
|
||||
showSelectionMenu()
|
||||
e := &TheState.Editor
|
||||
if !e.MenuVisible {
|
||||
t.Fatal("menu not shown for a visible selection")
|
||||
}
|
||||
y0 := e.MenuRect.Y
|
||||
// The menu sits below the anchor's line: lineTop(line 5) = reg.Y + 5*lh
|
||||
// (reg.Y = 10+52, the anchor's visual line index is 5).
|
||||
wantY0 := ui.Dp(62) + ui.Dp(5*lh) + EffectiveLineHeight() + 8
|
||||
if !dpeq(y0, wantY0) {
|
||||
t.Fatalf("initial menu Y = %v, want %v (below the anchor line)", y0, wantY0)
|
||||
}
|
||||
|
||||
// Scroll down by exactly 2 lines and lay out again.
|
||||
TheState.ScrollOffset = ui.Dp(2 * lh)
|
||||
EditorLayout(ui.Dp(1000), ui.Dp(2000), false)
|
||||
|
||||
// The anchor moved up 2 lines on screen; the menu must follow by exactly
|
||||
// the same amount.
|
||||
want := wantY0 - ui.Dp(2*lh)
|
||||
if got := e.MenuRect.Y; !dpeq(got, want) {
|
||||
t.Fatalf("menu Y after 2-line scroll = %v, want %v (menu must track the text)", got, want)
|
||||
}
|
||||
if !e.MenuVisible {
|
||||
t.Fatal("menu hidden while its anchor is still in view")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSelectionMenu_ClampsWhenTextLeavesView checks the small-file edge: the
|
||||
// buffer is the whole window, so the anchor never "leaves"; when the selected
|
||||
// text scrolls off the top, the menu pins to the top of the window (it never
|
||||
// detaches into mid-screen stale territory).
|
||||
func TestSelectionMenu_ClampsWhenTextLeavesView(t *testing.T) {
|
||||
lh := menuTrackState(t)
|
||||
|
||||
EditorLayout(ui.Dp(1000), ui.Dp(2000), false)
|
||||
|
||||
showSelectionMenu()
|
||||
e := &TheState.Editor
|
||||
if !e.MenuVisible {
|
||||
t.Fatal("menu not shown")
|
||||
}
|
||||
|
||||
// Scroll 10 lines: line 5 is 4 lines ABOVE the viewport top.
|
||||
TheState.ScrollOffset = ui.Dp(10 * lh)
|
||||
EditorLayout(ui.Dp(1000), ui.Dp(2000), false)
|
||||
|
||||
if !e.MenuVisible {
|
||||
t.Fatal("menu hidden, but small-file layout can still track it (pinned)")
|
||||
}
|
||||
if got := e.MenuRect.Y; !dpeq(got, 8) {
|
||||
t.Fatalf("menu Y = %v, want 8 (pinned to the window top when the text is above the viewport)", got)
|
||||
}
|
||||
}
|
||||
|
|
@ -763,13 +763,18 @@ func hideSelectionMenu() {
|
|||
e.MenuRect = ui.Region{}
|
||||
}
|
||||
|
||||
// showSelectionMenu positions the copy/cut/paste menu below the line that
|
||||
// contains the selection end (or caret) and recomputes the items. Copy and
|
||||
// Cut are offered only while a selection is active; Paste always.
|
||||
func showSelectionMenu() {
|
||||
e := &TheState.Editor
|
||||
if e.TooLarge || len(e.GlyphLayout.ByteOffsets) == 0 {
|
||||
return
|
||||
// positionSelectionMenu places the copy/cut/paste menu below the visual line
|
||||
// containing the anchor (selection end or caret), clamped to the window. It
|
||||
// is called from showSelectionMenu and from EditorLayout on every frame
|
||||
// while the menu is visible, so the menu tracks the selected text when the
|
||||
// user scrolls: the menu is anchored to the TEXT (an absolute buffer offset
|
||||
// mapped through the live screen geometry), not to the screen position where
|
||||
// it was first shown. It reports false when the anchor is outside the shaped
|
||||
// window (scrolled away): there is no layout to anchor to, and the caller
|
||||
// hides the menu. Re-anchoring the selection (double-tap) re-shows it.
|
||||
func positionSelectionMenu(e *EditorState) bool {
|
||||
if e.TooLarge || len(e.GlyphLayout.ByteOffsets) == 0 || len(e.MenuItems) == 0 {
|
||||
return false
|
||||
}
|
||||
anchor := e.CursorPosition
|
||||
if e.SelectionEnd > anchor {
|
||||
|
|
@ -777,26 +782,14 @@ func showSelectionMenu() {
|
|||
}
|
||||
glyphX, lineTop, ok := bytePosToScreenXY(anchor)
|
||||
if !ok {
|
||||
return
|
||||
return false
|
||||
}
|
||||
var winW, winH float64
|
||||
if TheState.scale > 0 {
|
||||
winW = float64(ui.ToDp(ui.Px(TheState.PixelWidth), TheState.scale))
|
||||
winH = float64(ui.ToDp(ui.Px(TheState.PixelHeight), TheState.scale))
|
||||
}
|
||||
var items []ui.MenuItem
|
||||
add := func(icon, label string) {
|
||||
items = append(items, ui.MenuItem{
|
||||
Icon: icon, Label: label,
|
||||
X: menuItemW * ui.Dp(len(items)), Y: 0, W: menuItemW, H: menuH,
|
||||
})
|
||||
}
|
||||
if selActive() {
|
||||
add("copy", "Copy")
|
||||
add("cut", "Cut")
|
||||
}
|
||||
add("paste", "Paste")
|
||||
menuW := menuItemW * ui.Dp(len(items))
|
||||
menuW := menuItemW * ui.Dp(len(e.MenuItems))
|
||||
mx := glyphX - float64(menuW)/2
|
||||
if mx < 8 {
|
||||
mx = 8
|
||||
|
|
@ -812,8 +805,33 @@ func showSelectionMenu() {
|
|||
my = 8
|
||||
}
|
||||
e.MenuRect = ui.Region{X: ui.Dp(mx), Y: ui.Dp(my), W: menuW, H: menuH}
|
||||
return true
|
||||
}
|
||||
|
||||
// showSelectionMenu recomputes the menu items and positions the menu below
|
||||
// the line containing the selection end (or caret). Copy and Cut are
|
||||
// offered only while a selection is active; Paste always.
|
||||
func showSelectionMenu() {
|
||||
e := &TheState.Editor
|
||||
if e.TooLarge || len(e.GlyphLayout.ByteOffsets) == 0 {
|
||||
return
|
||||
}
|
||||
var items []ui.MenuItem
|
||||
add := func(icon, label string) {
|
||||
items = append(items, ui.MenuItem{
|
||||
Icon: icon, Label: label,
|
||||
X: menuItemW * ui.Dp(len(items)), Y: 0, W: menuItemW, H: menuH,
|
||||
})
|
||||
}
|
||||
if selActive() {
|
||||
add("copy", "Copy")
|
||||
add("cut", "Cut")
|
||||
}
|
||||
add("paste", "Paste")
|
||||
e.MenuItems = items
|
||||
e.MenuVisible = true
|
||||
if positionSelectionMenu(e) {
|
||||
e.MenuVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
// bytePosToScreenXY returns app-local Dp coordinates for absByte: the X of
|
||||
|
|
@ -852,16 +870,33 @@ func bytePosToScreenXY(absByte int) (glyphX, lineTop float64, ok bool) {
|
|||
idx = len(layout.ByteOffsets) - 1
|
||||
}
|
||||
gx := float64(reg.X) + float64(layout.X[idx])
|
||||
// layout.Y is the window-relative baseline; the baseline offset within a
|
||||
// line (the ascent) is always < lineHeight, so int(baseline/lineHeight)
|
||||
// is the window-relative visual line index. The window is drawn shifted
|
||||
// up by r' (scrollVisualDecompose), so the line top on screen is
|
||||
// reg.Y - r' + visualLine*lineHeight.
|
||||
// layout.Y is the baseline relative to the layout's first line. The
|
||||
// shaper places baselines at ascent + lineHeight*lineIndex with
|
||||
// 0 < ascent < lineHeight (see drawWrappedText: ascent = FontSize, lineH
|
||||
// = FontSize*1.2), so baseline/lineHeight = lineIndex + frac with a frac
|
||||
// bounded away from both 0 and 1 — TRUNCATION yields the layout-relative
|
||||
// visual line index, robustly (the frac margin is ~0.2*lineHeight, far
|
||||
// above float32 noise). Do NOT round to nearest here: with this font's
|
||||
// frac ≈ 0.83, +0.5 rounding floors to the line BELOW.
|
||||
visualLine := int(float64(layout.Y[idx]) / lineHeight)
|
||||
if visualLine < 0 {
|
||||
visualLine = 0
|
||||
}
|
||||
_, r := scrollVisualDecompose()
|
||||
// The window is drawn shifted up by r' (scrollVisualDecompose). For
|
||||
// chunked files the layout is WINDOW-relative (its first line is the
|
||||
// window's first line), so visualLine is already relative to the
|
||||
// viewport top. For small (string) files the layout covers the WHOLE
|
||||
// buffer and the draw shifts by the full scroll offset: subtract the
|
||||
// window-start line k so the mapping agrees with the drawn geometry
|
||||
// (before this, the menu/caret mapping sat k lines off on scrolled
|
||||
// small files).
|
||||
k, r := scrollVisualDecompose()
|
||||
if TheState.Editor.ChunkedBuffer == nil {
|
||||
// May go negative: the anchor is above the viewport. That is the
|
||||
// intended off-screen follow-through; callers clamp the resulting
|
||||
// position (e.g. the menu pins to the window edge via my < 8).
|
||||
visualLine -= k
|
||||
}
|
||||
lt := float64(reg.Y) - r + float64(visualLine)*lineHeight
|
||||
return gx, lt, true
|
||||
}
|
||||
|
|
@ -1848,6 +1883,14 @@ func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element {
|
|||
|
||||
elems := []ui.Element{statusBar, editorElem, bottomBar}
|
||||
// The selection menu is added last so it draws on top of the editor.
|
||||
// Re-anchor it to the selection's live screen position every frame so it
|
||||
// follows the text while scrolling (it used to stay where it was first
|
||||
// shown, detaching from the selection).
|
||||
if TheState.Editor.MenuVisible {
|
||||
if !positionSelectionMenu(&TheState.Editor) {
|
||||
hideSelectionMenu()
|
||||
}
|
||||
}
|
||||
if TheState.Editor.MenuVisible {
|
||||
elems = append(elems, ui.NewMenu("selection_menu", TheState.Editor.MenuRect, TheState.Editor.MenuItems, func(data any) {
|
||||
if pt, ok := data.(ui.Point); ok {
|
||||
|
|
|
|||
|
|
@ -108,64 +108,155 @@ listener = '''
|
|||
|
||||
|
||||
# virtual methods
|
||||
# Resizes the GioView so the app draws above the visible keyboard instead of
|
||||
# behind it, and re-applies the status/nav bar insets as margins (the window
|
||||
# is edge-to-edge via setDecorFitsSystemWindows(false)).
|
||||
#
|
||||
# Coordinate frames: the window is full screen, but
|
||||
# DisplayMetrics.heightPixels is the app CONTENT height (status bar and nav
|
||||
# bar excluded). The typed insets are measured from the absolute display
|
||||
# edges, so the absolute display height is reconstructed as
|
||||
# contentH + statusTop + navBottom.
|
||||
#
|
||||
# v0 = statusTop, v1 = navBottom, v2 = imeInset, v3 = display height /
|
||||
# bottomLimit / height (int, sequential), v4 = object scratch, v5 = int
|
||||
# scratch. All registers <= v6 (const/16, if-* and invoke register lists are
|
||||
# 4-bit).
|
||||
.method public onApplyWindowInsets(Landroid/view/View;Landroid/view/WindowInsets;)Landroid/view/WindowInsets;
|
||||
.locals 4
|
||||
.locals 6
|
||||
|
||||
# int imeBottom = 0;
|
||||
const/4 v0, 0x0
|
||||
|
||||
# if (Build.VERSION.SDK_INT >= 30)
|
||||
# imeBottom = insets.getInsets(WindowInsets.Type.ime()).bottom;
|
||||
sget v1, Landroid/os/Build$VERSION;->SDK_INT:I
|
||||
const/4 v1, 0x0
|
||||
|
||||
const/16 v2, 0x1e
|
||||
const/4 v2, 0x0
|
||||
|
||||
if-lt v1, v2, :cond_pad_noime
|
||||
# content height (valid on every path; the API < 30 branch leaves the
|
||||
# inset values at 0)
|
||||
iget-object v4, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
|
||||
|
||||
const/16 v1, 0x8
|
||||
invoke-virtual {v4}, Landroid/view/View;->getResources()Landroid/content/res/Resources;
|
||||
|
||||
invoke-virtual {p2, v1}, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets;
|
||||
move-result-object v4
|
||||
|
||||
move-result-object v1
|
||||
invoke-virtual {v4}, Landroid/content/res/Resources;->getDisplayMetrics()Landroid/util/DisplayMetrics;
|
||||
|
||||
iget v0, v1, Landroid/graphics/Insets;->bottom:I
|
||||
move-result-object v4
|
||||
|
||||
iget v3, v4, Landroid/util/DisplayMetrics;->heightPixels:I
|
||||
|
||||
sget v4, Landroid/os/Build$VERSION;->SDK_INT:I
|
||||
|
||||
const/16 v5, 0x1e
|
||||
|
||||
# API < 30: no typed insets - fall through with all insets 0.
|
||||
if-lt v4, v5, :goto_pad_geom
|
||||
|
||||
# statusTop = insets.getInsets(Type.statusBars()).top
|
||||
const/4 v4, 0x1
|
||||
|
||||
invoke-virtual {p2, v4}, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets;
|
||||
|
||||
move-result-object v4
|
||||
|
||||
iget v0, v4, Landroid/graphics/Insets;->top:I
|
||||
|
||||
# navBottom = insets.getInsets(Type.navigationBars()).bottom
|
||||
const/4 v4, 0x6
|
||||
|
||||
invoke-virtual {p2, v4}, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets;
|
||||
|
||||
move-result-object v4
|
||||
|
||||
iget v1, v4, Landroid/graphics/Insets;->bottom:I
|
||||
|
||||
# imeInset = insets.getInsets(Type.ime()).bottom
|
||||
const/16 v4, 0x8
|
||||
|
||||
invoke-virtual {p2, v4}, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets;
|
||||
|
||||
move-result-object v4
|
||||
|
||||
iget v2, v4, Landroid/graphics/Insets;->bottom:I
|
||||
|
||||
:goto_pad_geom
|
||||
# absolute display height
|
||||
add-int v3, v3, v0
|
||||
|
||||
add-int v3, v3, v1
|
||||
|
||||
# bottomLimit: where the view bottom must not go below.
|
||||
# IME present: display bottom minus the IME inset (reaches the visible
|
||||
# keyboard top; the keyboard also covers the nav area).
|
||||
# otherwise: display bottom minus the nav bar.
|
||||
if-lez v2, :cond_pad_noime
|
||||
|
||||
sub-int v3, v3, v2
|
||||
|
||||
goto :goto_pad_bottom
|
||||
|
||||
:cond_pad_noime
|
||||
# int h = displayHeight - imeBottom;
|
||||
iget-object v1, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
|
||||
sub-int v3, v3, v1
|
||||
|
||||
invoke-virtual {v1}, Landroid/view/View;->getResources()Landroid/content/res/Resources;
|
||||
:goto_pad_bottom
|
||||
# height = bottomLimit - statusTop, clamped to >= 0
|
||||
sub-int v3, v3, v0
|
||||
|
||||
move-result-object v1
|
||||
if-gez v3, :cond_pad_pos
|
||||
|
||||
invoke-virtual {v1}, Landroid/content/res/Resources;->getDisplayMetrics()Landroid/util/DisplayMetrics;
|
||||
const/4 v3, 0x0
|
||||
|
||||
move-result-object v1
|
||||
:cond_pad_pos
|
||||
# apply to LayoutParams (height + topMargin = statusTop, bottomMargin =
|
||||
# navBottom)
|
||||
iget-object v4, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
|
||||
|
||||
iget v1, v1, Landroid/util/DisplayMetrics;->heightPixels:I
|
||||
invoke-virtual {v4}, Landroid/view/View;->getLayoutParams()Landroid/view/ViewGroup$LayoutParams;
|
||||
|
||||
sub-int v1, v1, v0
|
||||
move-result-object v4
|
||||
|
||||
# if (mView.getLayoutParams().height != h) { ... = h; requestLayout(); }
|
||||
iget-object v2, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
|
||||
check-cast v4, Landroid/view/ViewGroup$MarginLayoutParams;
|
||||
|
||||
invoke-virtual {v2}, Landroid/view/View;->getLayoutParams()Landroid/view/ViewGroup$LayoutParams;
|
||||
# v2 (free after geometry) = changed flag
|
||||
const/4 v2, 0x0
|
||||
|
||||
move-result-object v2
|
||||
iget v5, v4, Landroid/view/ViewGroup$LayoutParams;->height:I
|
||||
|
||||
iget v3, v2, Landroid/view/ViewGroup$LayoutParams;->height:I
|
||||
if-eq v5, v3, :cond_pad_checktop
|
||||
|
||||
if-eq v3, v1, :cond_pad_done
|
||||
iput v3, v4, Landroid/view/ViewGroup$LayoutParams;->height:I
|
||||
|
||||
iput v1, v2, Landroid/view/ViewGroup$LayoutParams;->height:I
|
||||
const/4 v2, 0x1
|
||||
|
||||
iget-object v3, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
|
||||
:cond_pad_checktop
|
||||
iget v5, v4, Landroid/view/ViewGroup$MarginLayoutParams;->topMargin:I
|
||||
|
||||
invoke-virtual {v3}, Landroid/view/View;->requestLayout()V
|
||||
if-eq v5, v0, :cond_pad_checkbot
|
||||
|
||||
iput v0, v4, Landroid/view/ViewGroup$MarginLayoutParams;->topMargin:I
|
||||
|
||||
const/4 v2, 0x1
|
||||
|
||||
:cond_pad_checkbot
|
||||
iget v5, v4, Landroid/view/ViewGroup$MarginLayoutParams;->bottomMargin:I
|
||||
|
||||
if-eq v5, v1, :cond_pad_layout
|
||||
|
||||
iput v1, v4, Landroid/view/ViewGroup$MarginLayoutParams;->bottomMargin:I
|
||||
|
||||
const/4 v2, 0x1
|
||||
|
||||
:cond_pad_layout
|
||||
if-eqz v2, :cond_pad_done
|
||||
|
||||
iget-object v5, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
|
||||
|
||||
invoke-virtual {v5}, Landroid/view/View;->requestLayout()V
|
||||
|
||||
:cond_pad_done
|
||||
return-object p2
|
||||
.end method
|
||||
|
||||
'''
|
||||
(base / "smali" / "org" / "gioui" / "PadInsetsListener.smali").write_text(listener)
|
||||
|
||||
|
|
|
|||
|
|
@ -101,64 +101,155 @@ listener = '''
|
|||
|
||||
|
||||
# virtual methods
|
||||
# Resizes the GioView so the app draws above the visible keyboard instead of
|
||||
# behind it, and re-applies the status/nav bar insets as margins (the window
|
||||
# is edge-to-edge via setDecorFitsSystemWindows(false)).
|
||||
#
|
||||
# Coordinate frames: the window is full screen, but
|
||||
# DisplayMetrics.heightPixels is the app CONTENT height (status bar and nav
|
||||
# bar excluded). The typed insets are measured from the absolute display
|
||||
# edges, so the absolute display height is reconstructed as
|
||||
# contentH + statusTop + navBottom.
|
||||
#
|
||||
# v0 = statusTop, v1 = navBottom, v2 = imeInset, v3 = display height /
|
||||
# bottomLimit / height (int, sequential), v4 = object scratch, v5 = int
|
||||
# scratch. All registers <= v6 (const/16, if-* and invoke register lists are
|
||||
# 4-bit).
|
||||
.method public onApplyWindowInsets(Landroid/view/View;Landroid/view/WindowInsets;)Landroid/view/WindowInsets;
|
||||
.locals 4
|
||||
.locals 6
|
||||
|
||||
# int imeBottom = 0;
|
||||
const/4 v0, 0x0
|
||||
|
||||
# if (Build.VERSION.SDK_INT >= 30)
|
||||
# imeBottom = insets.getInsets(WindowInsets.Type.ime()).bottom;
|
||||
sget v1, Landroid/os/Build$VERSION;->SDK_INT:I
|
||||
const/4 v1, 0x0
|
||||
|
||||
const/16 v2, 0x1e
|
||||
const/4 v2, 0x0
|
||||
|
||||
if-lt v1, v2, :cond_pad_noime
|
||||
# content height (valid on every path; the API < 30 branch leaves the
|
||||
# inset values at 0)
|
||||
iget-object v4, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
|
||||
|
||||
const/16 v1, 0x8
|
||||
invoke-virtual {v4}, Landroid/view/View;->getResources()Landroid/content/res/Resources;
|
||||
|
||||
invoke-virtual {p2, v1}, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets;
|
||||
move-result-object v4
|
||||
|
||||
move-result-object v1
|
||||
invoke-virtual {v4}, Landroid/content/res/Resources;->getDisplayMetrics()Landroid/util/DisplayMetrics;
|
||||
|
||||
iget v0, v1, Landroid/graphics/Insets;->bottom:I
|
||||
move-result-object v4
|
||||
|
||||
iget v3, v4, Landroid/util/DisplayMetrics;->heightPixels:I
|
||||
|
||||
sget v4, Landroid/os/Build$VERSION;->SDK_INT:I
|
||||
|
||||
const/16 v5, 0x1e
|
||||
|
||||
# API < 30: no typed insets - fall through with all insets 0.
|
||||
if-lt v4, v5, :goto_pad_geom
|
||||
|
||||
# statusTop = insets.getInsets(Type.statusBars()).top
|
||||
const/4 v4, 0x1
|
||||
|
||||
invoke-virtual {p2, v4}, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets;
|
||||
|
||||
move-result-object v4
|
||||
|
||||
iget v0, v4, Landroid/graphics/Insets;->top:I
|
||||
|
||||
# navBottom = insets.getInsets(Type.navigationBars()).bottom
|
||||
const/4 v4, 0x6
|
||||
|
||||
invoke-virtual {p2, v4}, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets;
|
||||
|
||||
move-result-object v4
|
||||
|
||||
iget v1, v4, Landroid/graphics/Insets;->bottom:I
|
||||
|
||||
# imeInset = insets.getInsets(Type.ime()).bottom
|
||||
const/16 v4, 0x8
|
||||
|
||||
invoke-virtual {p2, v4}, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets;
|
||||
|
||||
move-result-object v4
|
||||
|
||||
iget v2, v4, Landroid/graphics/Insets;->bottom:I
|
||||
|
||||
:goto_pad_geom
|
||||
# absolute display height
|
||||
add-int v3, v3, v0
|
||||
|
||||
add-int v3, v3, v1
|
||||
|
||||
# bottomLimit: where the view bottom must not go below.
|
||||
# IME present: display bottom minus the IME inset (reaches the visible
|
||||
# keyboard top; the keyboard also covers the nav area).
|
||||
# otherwise: display bottom minus the nav bar.
|
||||
if-lez v2, :cond_pad_noime
|
||||
|
||||
sub-int v3, v3, v2
|
||||
|
||||
goto :goto_pad_bottom
|
||||
|
||||
:cond_pad_noime
|
||||
# int h = displayHeight - imeBottom;
|
||||
iget-object v1, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
|
||||
sub-int v3, v3, v1
|
||||
|
||||
invoke-virtual {v1}, Landroid/view/View;->getResources()Landroid/content/res/Resources;
|
||||
:goto_pad_bottom
|
||||
# height = bottomLimit - statusTop, clamped to >= 0
|
||||
sub-int v3, v3, v0
|
||||
|
||||
move-result-object v1
|
||||
if-gez v3, :cond_pad_pos
|
||||
|
||||
invoke-virtual {v1}, Landroid/content/res/Resources;->getDisplayMetrics()Landroid/util/DisplayMetrics;
|
||||
const/4 v3, 0x0
|
||||
|
||||
move-result-object v1
|
||||
:cond_pad_pos
|
||||
# apply to LayoutParams (height + topMargin = statusTop, bottomMargin =
|
||||
# navBottom)
|
||||
iget-object v4, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
|
||||
|
||||
iget v1, v1, Landroid/util/DisplayMetrics;->heightPixels:I
|
||||
invoke-virtual {v4}, Landroid/view/View;->getLayoutParams()Landroid/view/ViewGroup$LayoutParams;
|
||||
|
||||
sub-int v1, v1, v0
|
||||
move-result-object v4
|
||||
|
||||
# if (mView.getLayoutParams().height != h) { ... = h; requestLayout(); }
|
||||
iget-object v2, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
|
||||
check-cast v4, Landroid/view/ViewGroup$MarginLayoutParams;
|
||||
|
||||
invoke-virtual {v2}, Landroid/view/View;->getLayoutParams()Landroid/view/ViewGroup$LayoutParams;
|
||||
# v2 (free after geometry) = changed flag
|
||||
const/4 v2, 0x0
|
||||
|
||||
move-result-object v2
|
||||
iget v5, v4, Landroid/view/ViewGroup$LayoutParams;->height:I
|
||||
|
||||
iget v3, v2, Landroid/view/ViewGroup$LayoutParams;->height:I
|
||||
if-eq v5, v3, :cond_pad_checktop
|
||||
|
||||
if-eq v3, v1, :cond_pad_done
|
||||
iput v3, v4, Landroid/view/ViewGroup$LayoutParams;->height:I
|
||||
|
||||
iput v1, v2, Landroid/view/ViewGroup$LayoutParams;->height:I
|
||||
const/4 v2, 0x1
|
||||
|
||||
iget-object v3, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
|
||||
:cond_pad_checktop
|
||||
iget v5, v4, Landroid/view/ViewGroup$MarginLayoutParams;->topMargin:I
|
||||
|
||||
invoke-virtual {v3}, Landroid/view/View;->requestLayout()V
|
||||
if-eq v5, v0, :cond_pad_checkbot
|
||||
|
||||
iput v0, v4, Landroid/view/ViewGroup$MarginLayoutParams;->topMargin:I
|
||||
|
||||
const/4 v2, 0x1
|
||||
|
||||
:cond_pad_checkbot
|
||||
iget v5, v4, Landroid/view/ViewGroup$MarginLayoutParams;->bottomMargin:I
|
||||
|
||||
if-eq v5, v1, :cond_pad_layout
|
||||
|
||||
iput v1, v4, Landroid/view/ViewGroup$MarginLayoutParams;->bottomMargin:I
|
||||
|
||||
const/4 v2, 0x1
|
||||
|
||||
:cond_pad_layout
|
||||
if-eqz v2, :cond_pad_done
|
||||
|
||||
iget-object v5, p0, Lorg/gioui/PadInsetsListener;->mView:Landroid/view/View;
|
||||
|
||||
invoke-virtual {v5}, Landroid/view/View;->requestLayout()V
|
||||
|
||||
:cond_pad_done
|
||||
return-object p2
|
||||
.end method
|
||||
|
||||
'''
|
||||
(base / "smali" / "org" / "gioui" / "PadInsetsListener.smali").write_text(listener)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user