From cbbb087dc5492a5cb7a904ec908b7296c8777c8b Mon Sep 17 00:00:00 2001 From: John Trujillo Date: Tue, 24 Mar 2026 16:42:42 -0500 Subject: [PATCH 1/2] fix: resolve window insets accumulation on app recreation and landscape mode Refactor inset extensions to cache initial padding and prevent redundant padding calculations --- .../activities/editor/BaseEditorActivity.kt | 35 +++----- .../utils/WindowInsetsExtensions.kt | 84 +++++++++++++++++++ resources/src/main/res/values/ids.xml | 4 + 3 files changed, 99 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/com/itsaky/androidide/utils/WindowInsetsExtensions.kt create mode 100644 resources/src/main/res/values/ids.xml diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt index fe90bb19a6..be68b4b60b 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt @@ -58,7 +58,6 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.fragment.app.Fragment -import com.itsaky.androidide.utils.applyBottomWindowInsetsPadding import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -118,8 +117,11 @@ import com.itsaky.androidide.utils.FlashType import com.itsaky.androidide.utils.InstallationResultHandler.onResult import com.itsaky.androidide.utils.IntentUtils import com.itsaky.androidide.utils.MemoryUsageWatcher +import com.itsaky.androidide.utils.applyResponsiveAppBarInsets +import com.itsaky.androidide.utils.applyRootSystemInsetsAsPadding import com.itsaky.androidide.utils.flashError import com.itsaky.androidide.utils.flashMessage +import com.itsaky.androidide.utils.getOrStoreInitialPadding import com.itsaky.androidide.utils.isAtLeastR import com.itsaky.androidide.utils.resolveAttr import com.itsaky.androidide.viewmodel.ApkInstallationViewModel @@ -374,7 +376,6 @@ abstract class BaseEditorActivity : private val flingVelocityThreshold by lazy { SizeUtils.dp2px(100f) } private var editorAppBarInsetTop: Int = 0 - private var sidebarLastInsetTop: Int = 0 companion object { private const val TAG = "ResizePanelDebugger" @@ -484,28 +485,18 @@ abstract class BaseEditorActivity : val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()) val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - applyStandardInsets(systemBars, insets) + applyStandardInsets(systemBars) applyImmersiveModeInsets(systemBars) handleKeyboardInsets(imeInsets) } - private fun applyStandardInsets(systemBars: Insets, windowInsets: WindowInsetsCompat) { - val content = _binding?.content ?: return - val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - - if (isLandscape) { - content.editorAppBarLayout.updatePadding(top = 0) - content.editorAppbarContent.updatePadding(top = systemBars.top) - } else { - content.editorAppBarLayout.updatePadding(top = systemBars.top) - content.editorAppbarContent.updatePadding(top = 0) - } - + private fun applyStandardInsets(systemBars: Insets) { immersiveController?.onSystemBarInsetsChanged(systemBars.top) - applySidebarInsets(systemBars) - _binding?.root?.applyBottomWindowInsetsPadding(windowInsets) + val root = _binding?.root ?: return + val initial = root.getOrStoreInitialPadding() + root.updatePadding(bottom = initial.bottom + systemBars.bottom) } private fun applyImmersiveModeInsets(systemBars: Insets) { @@ -558,13 +549,6 @@ abstract class BaseEditorActivity : editorAppBarInsetTop = insets.top } - private fun applySidebarInsets(systemBars: Insets) { - val sidebar = _binding?.drawerSidebar ?: return - val baseTop = sidebar.paddingTop - sidebarLastInsetTop - sidebarLastInsetTop = systemBars.top - sidebar.updatePadding(top = baseTop + systemBars.top) - } - @Subscribe(threadMode = MAIN) open fun onInstallationResult(event: InstallationEvent.InstallationResultEvent) { val intent = event.intent @@ -704,6 +688,8 @@ abstract class BaseEditorActivity : text = editorViewModel.getProjectName() } + content.editorAppBarLayout.applyResponsiveAppBarInsets(content.editorAppbarContent) + // Set up the drawer toggle on the title toolbar (where the hamburger menu should be) content.titleToolbar.apply { val toggle = @@ -1111,6 +1097,7 @@ abstract class BaseEditorActivity : ContentTranslatingDrawerLayout.TranslationBehavior.FULL setScrimColor(Color.TRANSPARENT) } + drawerSidebar.applyRootSystemInsetsAsPadding(applyTop = true) } } diff --git a/app/src/main/java/com/itsaky/androidide/utils/WindowInsetsExtensions.kt b/app/src/main/java/com/itsaky/androidide/utils/WindowInsetsExtensions.kt new file mode 100644 index 0000000000..5032c4db33 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/utils/WindowInsetsExtensions.kt @@ -0,0 +1,84 @@ +package com.itsaky.androidide.utils + +import android.content.res.Configuration +import android.view.View +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.doOnAttach +import androidx.core.view.updatePadding +import com.itsaky.androidide.R + +data class InitialPadding(val left: Int, val top: Int, val right: Int, val bottom: Int) + +/** + * Gets or stores the view's original padding to prevent infinite accumulation when applying insets. + * + * @return The original [InitialPadding]. + */ +fun View.getOrStoreInitialPadding(): InitialPadding { + return (getTag(R.id.tag_initial_padding) as? InitialPadding) + ?: InitialPadding(paddingLeft, paddingTop, paddingRight, paddingBottom).also { + setTag(R.id.tag_initial_padding, it) + } +} + +/** + * Applies top window insets responsively. Hides the AppBar in landscape mode and adjusts [appbarContent]. + * Forces an inset request on attach to prevent drawing behind system bars after activity recreation. + * + * @param appbarContent The inner content view to pad in landscape mode. + */ +fun View.applyResponsiveAppBarInsets(appbarContent: View) { + 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) + } else { + view.updatePadding(top = insets.top) + appbarContent.updatePadding(top = 0) + } + windowInsets + } + + doOnAttach { it.requestApplyInsets() } +} + +/** + * Applies root system window insets as padding, preserving the view's initial padding. + * Useful for deeply nested views (like DrawerLayouts) where standard inset listeners fail. + * + * @param applyLeft Apply left inset. + * @param applyTop Apply top inset. + * @param applyRight Apply right inset. + * @param applyBottom Apply bottom inset. + */ +fun View.applyRootSystemInsetsAsPadding( + applyLeft: Boolean = false, + applyTop: Boolean = false, + applyRight: Boolean = false, + applyBottom: Boolean = false +) { + val initial = getOrStoreInitialPadding() + + fun applyNow() { + val rootInsets = ViewCompat.getRootWindowInsets(this) ?: return + val insets = rootInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + + updatePadding( + left = initial.left + if (applyLeft) insets.left else 0, + top = initial.top + if (applyTop) insets.top else 0, + right = initial.right + if (applyRight) insets.right else 0, + bottom = initial.bottom + if (applyBottom) insets.bottom else 0 + ) + } + + doOnAttach { + applyNow() + if (ViewCompat.getRootWindowInsets(this) == null) { + post { applyNow() } + } + } +} diff --git a/resources/src/main/res/values/ids.xml b/resources/src/main/res/values/ids.xml new file mode 100644 index 0000000000..48008b803b --- /dev/null +++ b/resources/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ + + + + From 38021a92c83fca14daa6c5c72c50c759da371586 Mon Sep 17 00:00:00 2001 From: John Trujillo Date: Wed, 25 Mar 2026 11:01:59 -0500 Subject: [PATCH 2/2] fix: resolve inset handling and swipe-reveal animation issues --- .../activities/editor/BaseEditorActivity.kt | 45 ++++++++----- .../itsaky/androidide/ui/EditorBottomSheet.kt | 13 ++++ .../utils/WindowInsetsExtensions.kt | 66 ++++++++++++++++--- 3 files changed, 98 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt index be68b4b60b..5a9e6f5bfc 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt @@ -54,6 +54,7 @@ import androidx.appcompat.app.ActionBarDrawerToggle import androidx.collection.MutableIntIntMap import androidx.core.graphics.Insets import androidx.core.view.GravityCompat +import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding @@ -118,7 +119,9 @@ import com.itsaky.androidide.utils.InstallationResultHandler.onResult import com.itsaky.androidide.utils.IntentUtils import com.itsaky.androidide.utils.MemoryUsageWatcher import com.itsaky.androidide.utils.applyResponsiveAppBarInsets +import com.itsaky.androidide.utils.applyImmersiveModeInsets import com.itsaky.androidide.utils.applyRootSystemInsetsAsPadding +import com.itsaky.androidide.utils.applyBottomSheetAnchorForOrientation import com.itsaky.androidide.utils.flashError import com.itsaky.androidide.utils.flashMessage import com.itsaky.androidide.utils.getOrStoreInitialPadding @@ -500,22 +503,7 @@ abstract class BaseEditorActivity : } private fun applyImmersiveModeInsets(systemBars: Insets) { - val content = _binding?.content ?: return - val baseMargin = SizeUtils.dp2px(16f) - val isRtl = content.root.layoutDirection == View.LAYOUT_DIRECTION_RTL - val endInset = if (isRtl) systemBars.left else systemBars.right - - content.btnToggleTopBar.updateLayoutParams { - topMargin = baseMargin + systemBars.top - marginEnd = baseMargin + endInset - } - - content.btnToggleBottomBar.updateLayoutParams { - bottomMargin = baseMargin + systemBars.bottom - marginEnd = baseMargin + endInset - } - - content.bottomSheet.updatePadding(top = systemBars.top) + _binding?.content?.applyImmersiveModeInsets(systemBars) } private fun handleKeyboardInsets(imeInsets: Insets) { @@ -680,8 +668,25 @@ abstract class BaseEditorActivity : override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) immersiveController?.onConfigurationChanged(newConfig) + window?.decorView?.let { ViewCompat.requestApplyInsets(it) } + reapplySystemBarInsetsFromRoot() + _binding?.content?.applyBottomSheetAnchorForOrientation(newConfig.orientation) } + private fun reapplySystemBarInsetsFromRoot() { + val root = _binding?.root ?: return + val rootInsets = ViewCompat.getRootWindowInsets(root) + if (rootInsets == null) { + root.post { reapplySystemBarInsetsFromRoot() } + return + } + + val systemBars = rootInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + applyStandardInsets(systemBars) + applyImmersiveModeInsets(systemBars) + } + + private fun setupToolbar() { // Set the project name in the title TextView content.root.findViewById(R.id.title_text)?.apply { @@ -741,7 +746,13 @@ abstract class BaseEditorActivity : val insetsTop = systemBarInsets?.top ?: 0 val topInset = (insetsTop * (1f - progress)).roundToInt() - content.editorAppbarContent.updatePadding(top = topInset) + val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + + if (isLandscape) { + content.editorAppbarContent.updatePadding(top = topInset) + } else { + content.editorAppBarLayout.updatePadding(top = topInset) + } memUsageView.chart.updateLayoutParams { topMargin = (insetsTop * progress).roundToInt() diff --git a/app/src/main/java/com/itsaky/androidide/ui/EditorBottomSheet.kt b/app/src/main/java/com/itsaky/androidide/ui/EditorBottomSheet.kt index f912b5b68a..b40edf3958 100644 --- a/app/src/main/java/com/itsaky/androidide/ui/EditorBottomSheet.kt +++ b/app/src/main/java/com/itsaky/androidide/ui/EditorBottomSheet.kt @@ -366,6 +366,19 @@ constructor( view.viewTreeObserver.addOnGlobalLayoutListener(listener) } + fun resetOffsetAnchor() { + anchorOffset = 0 + behavior.peekHeight = collapsedHeight.roundToInt() + behavior.expandedOffset = 0 + binding.root.updatePadding(bottom = insetBottom) + binding.headerContainer.apply { + updatePaddingRelative(bottom = insetBottom) + updateLayoutParams { + height = (collapsedHeight + insetBottom).roundToInt() + } + } + } + fun onSlide(sheetOffset: Float) { val heightScale = if (sheetOffset >= COLLAPSE_HEADER_AT_OFFSET) { diff --git a/app/src/main/java/com/itsaky/androidide/utils/WindowInsetsExtensions.kt b/app/src/main/java/com/itsaky/androidide/utils/WindowInsetsExtensions.kt index 5032c4db33..9ceaa18586 100644 --- a/app/src/main/java/com/itsaky/androidide/utils/WindowInsetsExtensions.kt +++ b/app/src/main/java/com/itsaky/androidide/utils/WindowInsetsExtensions.kt @@ -7,6 +7,11 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.doOnAttach import androidx.core.view.updatePadding import com.itsaky.androidide.R +import com.itsaky.androidide.databinding.ContentEditorBinding +import com.blankj.utilcode.util.SizeUtils +import androidx.core.graphics.Insets +import android.view.ViewGroup +import androidx.core.view.updateLayoutParams data class InitialPadding(val left: Int, val top: Int, val right: Int, val bottom: Int) @@ -63,22 +68,65 @@ fun View.applyRootSystemInsetsAsPadding( ) { val initial = getOrStoreInitialPadding() - fun applyNow() { - val rootInsets = ViewCompat.getRootWindowInsets(this) ?: return - val insets = rootInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) - updatePadding( + view.updatePadding( left = initial.left + if (applyLeft) insets.left else 0, top = initial.top + if (applyTop) insets.top else 0, right = initial.right + if (applyRight) insets.right else 0, bottom = initial.bottom + if (applyBottom) insets.bottom else 0 ) + windowInsets } - doOnAttach { - applyNow() - if (ViewCompat.getRootWindowInsets(this) == null) { - post { applyNow() } - } + doOnAttach { it.requestApplyInsets() } +} + +/** + * Applies immersive mode insets to editor UI elements that float near system bars. + * + * Keeps toggle buttons and bottom sheet aligned with system bars (status/nav). + */ +fun ContentEditorBinding.applyImmersiveModeInsets(systemBars: Insets) { + val baseMargin = SizeUtils.dp2px(16f) + val isRtl = root.layoutDirection == View.LAYOUT_DIRECTION_RTL + val endInset = if (isRtl) systemBars.left else systemBars.right + + btnToggleTopBar.updateLayoutParams { + topMargin = baseMargin + systemBars.top + marginEnd = baseMargin + endInset + } + + btnToggleBottomBar.updateLayoutParams { + bottomMargin = baseMargin + systemBars.bottom + marginEnd = baseMargin + endInset + } + + bottomSheet.updatePadding(top = systemBars.top) +} + +/** + * Recomputes bottom sheet offsets based on the current app bar height. + */ +fun ContentEditorBinding.refreshBottomSheetAnchor() { + bottomSheet.setOffsetAnchor(editorAppBarLayout) +} + +/** + * Allows the bottom sheet to expand fully (no app bar anchor). + */ +fun ContentEditorBinding.resetBottomSheetAnchor() { + bottomSheet.resetOffsetAnchor() +} + +/** + * Applies the correct bottom sheet anchor based on orientation. + */ +fun ContentEditorBinding.applyBottomSheetAnchorForOrientation(orientation: Int) { + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + refreshBottomSheetAnchor() + } else { + resetBottomSheetAnchor() } }