Skip to content

ADFA-2646 | Fix window insets accumulation and landscape appbar behavior#1114

Merged
jatezzz merged 4 commits intostagefrom
fix/ADFA-2646-window-insets-accumulation
Mar 25, 2026
Merged

ADFA-2646 | Fix window insets accumulation and landscape appbar behavior#1114
jatezzz merged 4 commits intostagefrom
fix/ADFA-2646-window-insets-accumulation

Conversation

@jatezzz
Copy link
Copy Markdown
Collaborator

@jatezzz jatezzz commented Mar 24, 2026

Description

This PR refactors how window insets are handled in BaseEditorActivity to fix several UI bugs related to app recreation, landscape mode, and the bottom navigation bar.

Instead of manually calculating and accumulating padding values, we introduced a caching mechanism in WindowInsetsExtensions.kt that safely stores the view's original padding using a dedicated resource ID (R.id.tag_initial_padding). This guarantees that system insets are always added to a clean baseline, preventing the UI from overlapping the status bar or leaving gaps above the bottom navigation bar when the app is rotated or recreated.

Details

  • Sidebar: Migrated to the new applyRootSystemInsetsAsPadding extension to prevent infinite padding growth.
Screen_Recording_20260324_164005_One.UI.Home.mp4

Ticket

ADFA-2646

…pe mode

Refactor inset extensions to cache initial padding and prevent redundant padding calculations
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 24, 2026

📝 Walkthrough

Release Notes - Window Insets Accumulation & Landscape AppBar Fix (PR #1114)

  • Fixed window insets accumulation on activity recreation by caching each view’s original padding via a new InitialPadding + getOrStoreInitialPadding() mechanism to prevent padding from growing across repeated inset applications.
  • Refactored inset handling in BaseEditorActivity to a stable flow:
    • Replaced manual incremental sidebar/top-padding logic and sidebarLastInsetTop tracking with applyStandardInsets(systemBars) and delegated immersive-mode adjustments to binding.applyImmersiveModeInsets(systemBars).
    • Re-applies insets on configuration changes (requests ViewCompat.requestApplyInsets, recalculates system bar insets from root, reapplies insets and reanchors bottom sheet).
  • Improved AppBar behavior in landscape:
    • Added applyResponsiveAppBarInsets(appbarContent) to apply top insets selectively (remove top padding for appbar root in landscape while applying insets to inner content), avoiding status bar overlap.
    • Swipe-reveal top padding now toggles between editorAppbarContent (landscape) and editorAppBarLayout (portrait) to keep animations aligned with app bar layout.
  • Prevented sidebar/drawer padding drift:
    • Introduced applyRootSystemInsetsAsPadding(...) to apply system insets using each view’s stored baseline padding, preventing infinite padding growth (used for drawer/sidebar).
  • Resolved swipe-reveal animation issues impacted by inset/padding behavior by aligning which view receives top padding and keeping bottom-sheet anchoring consistent after orientation changes.
  • New bottom-sheet control: EditorBottomSheet.resetOffsetAnchor() to clear anchor offsets and restore collapsed sizing and padding after an anchored state is removed.
  • Added resource id @id/tag_initial_padding to store view padding baseline.

Files changed (high level)

  • Added app/src/main/java/com/itsaky/androidide/utils/WindowInsetsExtensions.kt — new utilities and ContentEditorBinding extensions (InitialPadding, getOrStoreInitialPadding, applyResponsiveAppBarInsets, applyRootSystemInsetsAsPadding, applyImmersiveModeInsets, refresh/reset/apply bottom-sheet anchor helpers).
  • Updated app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt — refactored inset application flow and configuration-change handling.
  • Updated app/src/main/java/com/itsaky/androidide/ui/EditorBottomSheet.kt — added resetOffsetAnchor().
  • Added resources/src/main/res/values/ids.xml — @id/tag_initial_padding.

API / Public surface

  • New data class: InitialPadding(left, top, right, bottom)
  • New View extensions: fun View.getOrStoreInitialPadding(), fun View.applyResponsiveAppBarInsets(appbarContent: View), fun View.applyRootSystemInsetsAsPadding(...)
  • New ContentEditorBinding extensions: applyImmersiveModeInsets(Insets), refreshBottomSheetAnchor(), resetBottomSheetAnchor(), applyBottomSheetAnchorForOrientation(orientation)
  • New EditorBottomSheet API: fun resetOffsetAnchor()

⚠️ Risks & Best-practice notes

  • View tag mutation for caching (View.setTag(R.id.tag_initial_padding, ...)):
    • Storing mutable state on view tags couples layout state to view instances and can cause issues with view recycling, reused view hierarchies, or unexpected tag collisions if the same id is reused elsewhere.
    • Consider documenting lifecycle expectations or migrating to an explicit per-View lifecycle store (e.g., ViewModel/SavedState or a dedicated wrapper) for recyclable views.
  • Timing-dependent inset fallback:
    • applyRootSystemInsetsAsPadding posts a deferred requestApplyInsets when root insets are not immediately available. This relies on view attach/timing behavior and could be brittle on some devices or OEMs; tests should cover slow attach scenarios.
  • Orientation/configuration coverage:
    • Orientation checks are performed at inset-application time. If the system does not emit new insets on certain config changes, explicit reapplication depends on onConfigurationChanged path — ensure all relevant flows trigger ViewCompat.requestApplyInsets().
  • Silent removal of previous APIs:
    • applySidebarInsets and sidebarLastInsetTop tracking were removed. Any external or subclass code relying on those internal behaviors will need updates.
  • Cached baseline staleness:
    • getOrStoreInitialPadding assumes baseline padding is set before first inset application. If code mutates padding after caching, subsequent inset applications will use stale baselines and produce incorrect layout.

Testing recommendations

  • Verify no padding accumulation across:
    • multiple activity recreations (rotation, process death + restore),
    • repeated open/close of drawer/sidebar,
    • entering/exiting immersive modes.
  • Test app bar and swipe-reveal animations in portrait and landscape, including rapid rotation while animations are in flight.
  • Test on a variety of devices: display cutouts/notches, gesture navigation, and OEM-modified window-inset behaviors.
  • Test view recycling scenarios (if sidebar/drawer views are reused) to ensure tag-based padding caching doesn’t leak state across instances.

Walkthrough

Refactors window-inset handling: replaces orientation-dependent inset logic in BaseEditorActivity with new orientation-aware View and ContentEditorBinding extensions, stores initial paddings to avoid accumulation, and adds bottom-sheet anchor reset behavior.

Changes

Cohort / File(s) Summary
Editor activity inset flow
app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt
Replaced prior inset application (orientation-dependent top padding, sidebar incremental recompute) with simplified applyStandardInsets(systemBars) and calls to extensions: applyResponsiveAppBarInsets, applyRootSystemInsetsAsPadding, and delegated applyImmersiveModeInsets. Reapplies insets on configuration changes and reanchors bottom sheet.
Window-insets utilities & binding extensions
app/src/main/java/com/itsaky/androidide/utils/WindowInsetsExtensions.kt
New utilities: InitialPadding data class, View.getOrStoreInitialPadding(), View.applyResponsiveAppBarInsets(...), View.applyRootSystemInsetsAsPadding(...), and ContentEditorBinding extensions (applyImmersiveModeInsets, refreshBottomSheetAnchor, resetBottomSheetAnchor, applyBottomSheetAnchorForOrientation) to preserve initial padding and apply system-bar insets responsively.
Bottom sheet anchor control
app/src/main/java/com/itsaky/androidide/ui/EditorBottomSheet.kt
Added resetOffsetAnchor() to clear previously set anchor offsets and restore collapsed/expanded offsets and associated padding/height adjustments.
Resource ids
resources/src/main/res/values/ids.xml
Added exported resource id @id/tag_initial_padding used to store initial padding on views to prevent accumulation.

Sequence Diagram(s)

sequenceDiagram
    participant Activity as BaseEditorActivity
    participant ViewUtils as WindowInsetsExtensions
    participant Binding as ContentEditorBinding
    participant BottomSheet as EditorBottomSheet

    Activity->>ViewUtils: request applyInsets / requestApplyInsets()
    ViewUtils->>Binding: applyResponsiveAppBarInsets / applyRootSystemInsetsAsPadding
    Binding->>Binding: applyImmersiveModeInsets(systemBars)
    Binding->>BottomSheet: applyBottomSheetAnchorForOrientation(orientation)
    BottomSheet->>BottomSheet: setOffsetAnchor() / resetOffsetAnchor()
    Activity->>Activity: onConfigurationChanged -> request reapply and reanchor
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • itsaky-adfa
  • Daniel-ADFA
  • dara-abijo-adfa

Poem

🐰 I nudged the padding, stored each tiny gap,
Now bars and sheets fit snug in every app.
Portrait or wide — I hop and I prune,
Insets aligned under the code-moon 🌙

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main changes: fixing window insets accumulation and landscape appbar behavior.
Description check ✅ Passed The description is directly related to the changeset, explaining the refactoring of window inset handling, the caching mechanism, and the specific issues being addressed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ADFA-2646-window-insets-accumulation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt`:
- Line 691: The app bar gets double-padded because
applyResponsiveAppBarInsets(...) applies the portrait top inset to
editorAppBarLayout while the swipe-reveal drag animation still targets
editorAppbarContent; update the code so the inset and the swipe-reveal
padding/translation target the same view: either call
applyResponsiveAppBarInsets(content.editorAppbarContent) to move the inset to
the child, or change the swipe-reveal animation to animate
content.editorAppBarLayout instead of content.editorAppbarContent (make the
choice consistently everywhere where swipe-reveal padding/translation is
applied, e.g., in the drag/animation helper that references
editorAppbarContent).

In `@app/src/main/java/com/itsaky/androidide/utils/WindowInsetsExtensions.kt`:
- Around line 58-83: applyRootSystemInsetsAsPadding currently samples
ViewCompat.getRootWindowInsets only once on attach (via applyNow and a single
posted retry) and therefore doesn't react to later inset changes; update it to
register an inset listener (similar to applyResponsiveAppBarInsets) using
ViewCompat.setOnApplyWindowInsetsListener or View.setOnApplyWindowInsetsListener
so that getOrStoreInitialPadding() is combined with the incoming
WindowInsetsCompat on every onApplyWindowInsets call and updatePadding is
invoked each time; keep the existing doOnAttach logic to initialize initial
padding but remove the one-shot getRootWindowInsets sampling in favor of the
reactive listener and ensure you convert the incoming WindowInsets to insets via
getInsets(WindowInsetsCompat.Type.systemBars()) before applying
left/top/right/bottom based on applyLeft/applyTop/applyRight/applyBottom.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6f0ad1b9-d06f-4656-b5ae-ee99bd4f4770

📥 Commits

Reviewing files that changed from the base of the PR and between d277a9a and cbbb087.

📒 Files selected for processing (3)
  • app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt
  • app/src/main/java/com/itsaky/androidide/utils/WindowInsetsExtensions.kt
  • resources/src/main/res/values/ids.xml

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt (1)

676-687: Consider adding a retry limit to prevent unbounded recursion.

If ViewCompat.getRootWindowInsets(root) returns null, the method posts itself recursively without a guard. While this is unlikely to loop indefinitely in practice, adding a simple retry counter would make this more defensive.

🛡️ Optional: Add retry limit
+private var insetsRetryCount = 0
+private val maxInsetsRetries = 3
+
 private fun reapplySystemBarInsetsFromRoot() {
     val root = _binding?.root ?: return
     val rootInsets = ViewCompat.getRootWindowInsets(root)
     if (rootInsets == null) {
-        root.post { reapplySystemBarInsetsFromRoot() }
+        if (insetsRetryCount < maxInsetsRetries) {
+            insetsRetryCount++
+            root.post { reapplySystemBarInsetsFromRoot() }
+        }
         return
     }
+    insetsRetryCount = 0
 
     val systemBars = rootInsets.getInsets(WindowInsetsCompat.Type.systemBars())
     applyStandardInsets(systemBars)
     applyImmersiveModeInsets(systemBars)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt`
around lines 676 - 687, reapplySystemBarInsetsFromRoot currently reposts itself
whenever ViewCompat.getRootWindowInsets(root) is null which can cause unbounded
retries; add a retry limit (e.g., maxRetries = 5) tracked either by an optional
parameter to reapplySystemBarInsetsFromRoot(retryCount: Int = 0) or a private
field, increment the counter each time you post the retry, and stop reposting
once the limit is reached (optionally log a warning); keep the existing
applyStandardInsets(systemBars) and applyImmersiveModeInsets(systemBars) calls
unchanged when insets are available.
app/src/main/java/com/itsaky/androidide/utils/WindowInsetsExtensions.kt (1)

36-52: Consider preserving initial padding for robustness.

The function directly sets padding to 0 or insets.top without accounting for any initial top padding the views might have. If editorAppBarLayout or editorAppbarContent ever have non-zero initial top padding (from XML or programmatic setup), it would be lost after rotation.

This may be intentional if those views are guaranteed to have zero initial padding, but using getOrStoreInitialPadding() here would make the function more robust.

♻️ Optional: Preserve initial padding
 fun View.applyResponsiveAppBarInsets(appbarContent: View) {
+    val initialParent = getOrStoreInitialPadding()
+    val initialContent = appbarContent.getOrStoreInitialPadding()
+
     ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
         val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
         val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
 
         if (isLandscape) {
-            view.updatePadding(top = 0)
-            appbarContent.updatePadding(top = insets.top)
+            view.updatePadding(top = initialParent.top)
+            appbarContent.updatePadding(top = initialContent.top + insets.top)
         } else {
-            view.updatePadding(top = insets.top)
-            appbarContent.updatePadding(top = 0)
+            view.updatePadding(top = initialParent.top + insets.top)
+            appbarContent.updatePadding(top = initialContent.top)
         }
         windowInsets
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/itsaky/androidide/utils/WindowInsetsExtensions.kt`
around lines 36 - 52, The applyResponsiveAppBarInsets function wipes any
pre-existing top padding by setting it to 0 or insets.top; change it to preserve
each view's initial top padding by reading/storing the original top padding (use
a helper like getOrStoreInitialPadding or similar) and then apply
updatePadding(top = initialTop + insets.top) for the view that needs the inset
and updatePadding(top = initialTop) for the other case; update references to
view and appbarContent in applyResponsiveAppBarInsets so they use the stored
initialTop values instead of hard-coded 0 or insets.top.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt`:
- Around line 676-687: reapplySystemBarInsetsFromRoot currently reposts itself
whenever ViewCompat.getRootWindowInsets(root) is null which can cause unbounded
retries; add a retry limit (e.g., maxRetries = 5) tracked either by an optional
parameter to reapplySystemBarInsetsFromRoot(retryCount: Int = 0) or a private
field, increment the counter each time you post the retry, and stop reposting
once the limit is reached (optionally log a warning); keep the existing
applyStandardInsets(systemBars) and applyImmersiveModeInsets(systemBars) calls
unchanged when insets are available.

In `@app/src/main/java/com/itsaky/androidide/utils/WindowInsetsExtensions.kt`:
- Around line 36-52: The applyResponsiveAppBarInsets function wipes any
pre-existing top padding by setting it to 0 or insets.top; change it to preserve
each view's initial top padding by reading/storing the original top padding (use
a helper like getOrStoreInitialPadding or similar) and then apply
updatePadding(top = initialTop + insets.top) for the view that needs the inset
and updatePadding(top = initialTop) for the other case; update references to
view and appbarContent in applyResponsiveAppBarInsets so they use the stored
initialTop values instead of hard-coded 0 or insets.top.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c8c938bb-9294-4923-9133-0a3d724a4d53

📥 Commits

Reviewing files that changed from the base of the PR and between cbbb087 and 38021a9.

📒 Files selected for processing (3)
  • app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt
  • app/src/main/java/com/itsaky/androidide/ui/EditorBottomSheet.kt
  • app/src/main/java/com/itsaky/androidide/utils/WindowInsetsExtensions.kt

@jatezzz jatezzz merged commit f011e8b into stage Mar 25, 2026
2 checks passed
@jatezzz jatezzz deleted the fix/ADFA-2646-window-insets-accumulation branch March 25, 2026 20:40
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.

2 participants