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..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,11 +54,11 @@ 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 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 +118,13 @@ 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.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 import com.itsaky.androidide.utils.isAtLeastR import com.itsaky.androidide.utils.resolveAttr import com.itsaky.androidide.viewmodel.ApkInstallationViewModel @@ -374,7 +379,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,47 +488,22 @@ 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) { - 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) { @@ -558,13 +537,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 @@ -696,14 +668,33 @@ 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 { 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 = @@ -755,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() @@ -1111,6 +1108,7 @@ abstract class BaseEditorActivity : ContentTranslatingDrawerLayout.TranslationBehavior.FULL setScrimColor(Color.TRANSPARENT) } + drawerSidebar.applyRootSystemInsetsAsPadding(applyTop = true) } } 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 new file mode 100644 index 0000000000..9ceaa18586 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/utils/WindowInsetsExtensions.kt @@ -0,0 +1,132 @@ +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 +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) + +/** + * 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() + + ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + + 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 { 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() + } +} 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 @@ + + + +