From f8d087808199acfea8ce9152d4a838855c9fac51 Mon Sep 17 00:00:00 2001 From: John Trujillo Date: Fri, 20 Mar 2026 16:18:14 -0500 Subject: [PATCH 1/3] feat: implement landscape immersive mode for editor toolbars Adds LandscapeImmersiveController, touch observation, and UI toggles to handle auto-hiding toolbars. --- .../activities/editor/BaseEditorActivity.kt | 60 ++- .../editor/LandscapeImmersiveController.kt | 356 ++++++++++++++++++ .../res/drawable/bg_hollow_green_circle.xml | 8 + .../main/res/layout-land/content_editor.xml | 117 +++--- app/src/main/res/layout/content_editor.xml | 22 ++ .../ui/TouchObservingLinearLayout.kt | 19 + 6 files changed, 534 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/com/itsaky/androidide/activities/editor/LandscapeImmersiveController.kt create mode 100644 app/src/main/res/drawable/bg_hollow_green_circle.xml create mode 100644 common/src/main/java/com/itsaky/androidide/ui/TouchObservingLinearLayout.kt 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 de15bb31c4..c959f689d0 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 @@ -172,6 +172,7 @@ abstract class BaseEditorActivity : private val fileManagerViewModel by viewModels() private var feedbackButtonManager: FeedbackButtonManager? = null + private var immersiveController: LandscapeImmersiveController? = null var isDestroying = false protected set @@ -448,6 +449,9 @@ abstract class BaseEditorActivity : editorBottomSheet = null gestureDetector = null + immersiveController?.destroy() + immersiveController = null + _binding = null if (isDestroying) { @@ -480,11 +484,47 @@ abstract class BaseEditorActivity : val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()) val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - _binding?.content?.editorAppBarLayout?.updatePadding(top = systemBars.top) + applyStandardInsets(systemBars, insets) + + applyImmersiveModeInsets(systemBars) + + handleKeyboardInsets(imeInsets) + } + + private fun applyStandardInsets(systemBars: Insets, windowInsets: WindowInsetsCompat) { + val content = _binding?.content ?: return + + val appBarContent = content.editorAppbarContent + if (appBarContent != null) { + content.editorAppBarLayout.updatePadding(top = 0) + appBarContent.updatePadding(top = systemBars.top) + } else { + content.editorAppBarLayout.updatePadding(top = systemBars.top) + } + + immersiveController?.onSystemBarInsetsChanged(systemBars.top) applySidebarInsets(systemBars) - - _binding?.root?.applyBottomWindowInsetsPadding(insets) + _binding?.root?.applyBottomWindowInsetsPadding(windowInsets) + } + + private fun applyImmersiveModeInsets(systemBars: Insets) { + val content = _binding?.content ?: return + val baseMargin = SizeUtils.dp2px(16f) + content.btnToggleTopBar.updateLayoutParams { + topMargin = baseMargin + systemBars.top + marginEnd = baseMargin + systemBars.right + } + + content.btnToggleBottomBar.updateLayoutParams { + bottomMargin = baseMargin + systemBars.bottom + marginEnd = baseMargin + systemBars.right + } + + content.bottomSheet.updatePadding(top = systemBars.top) + } + + private fun handleKeyboardInsets(imeInsets: Insets) { val isImeVisible = imeInsets.bottom > 0 _binding?.content?.bottomSheet?.setImeVisible(isImeVisible) @@ -612,6 +652,15 @@ abstract class BaseEditorActivity : setupStateObservers() setupViews() + immersiveController = LandscapeImmersiveController( + contentBinding = content, + bottomSheetBehavior = editorBottomSheet!!, + coroutineScope = lifecycleScope, + ).also { + it.bind() + it.onConfigurationChanged(resources.configuration) + } + setupContainers() setupDiagnosticInfo() @@ -643,6 +692,7 @@ abstract class BaseEditorActivity : override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) + immersiveController?.onConfigurationChanged(newConfig) } private fun setupToolbar() { @@ -792,6 +842,7 @@ abstract class BaseEditorActivity : } override fun onPause() { + immersiveController?.onPause() super.onPause() memoryUsageWatcher.listener = null memoryUsageWatcher.stopWatching(false) @@ -1299,7 +1350,8 @@ abstract class BaseEditorActivity : slideOffset: Float, ) { content.apply { - val editorScale = 1 - slideOffset * (1 - EDITOR_CONTAINER_SCALE_FACTOR) + val safeOffset = slideOffset.coerceAtLeast(0f) + val editorScale = 1 - safeOffset * (1 - EDITOR_CONTAINER_SCALE_FACTOR) this.bottomSheet.onSlide(slideOffset) this.viewContainer.scaleX = editorScale this.viewContainer.scaleY = editorScale diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/LandscapeImmersiveController.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/LandscapeImmersiveController.kt new file mode 100644 index 0000000000..d4d92f0044 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/LandscapeImmersiveController.kt @@ -0,0 +1,356 @@ +package com.itsaky.androidide.activities.editor + +import android.annotation.SuppressLint +import android.content.res.Configuration +import android.view.MotionEvent +import android.view.View +import android.view.View.OnLayoutChangeListener +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.core.view.updatePadding +import androidx.core.view.updateLayoutParams +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.appbar.AppBarLayout +import com.itsaky.androidide.databinding.ContentEditorBinding +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.math.roundToInt + +/** + * Controls immersive behavior for the editor in landscape mode. + * + * Top bar: + * - expands/collapses through AppBarLayout behavior + * - supports auto-hide after being shown + * - pauses auto-hide while the user is interacting with the top bar + * + * Bottom bar: + * - remains backed by BottomSheetBehavior + * - can be visually hidden by translating the collapsed peek area + */ +class LandscapeImmersiveController( + contentBinding: ContentEditorBinding, + private val bottomSheetBehavior: BottomSheetBehavior, + private val coroutineScope: CoroutineScope, +) { + private val topBar = contentBinding.editorAppBarLayout + private val appBarContent = runCatching { contentBinding.editorAppbarContent }.getOrNull() + private val viewContainer = contentBinding.viewContainer + private val editorContainer = contentBinding.editorContainer + private val bottomSheet = contentBinding.bottomSheet + private val topToggle = contentBinding.btnToggleTopBar + private val bottomToggle = contentBinding.btnToggleBottomBar + + private var autoHideJob: Job? = null + private var isBound = false + + private var isLandscape = false + private var isTopBarRequestedVisible = true + private var isBottomBarRequestedVisible = true + private var isBottomBarShown = true + private var isPendingBottomBarHideAfterCollapse = false + private var isUserInteractingWithTopBar = false + + private var statusBarTopInset = 0 + private var currentAppBarOffset = 0 + private var lastKnownScrollRange = 0 + + private val topBarOffsetListener = + AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset -> + currentAppBarOffset = verticalOffset + lastKnownScrollRange = appBarLayout.totalScrollRange + updateEditorTopInset() + } + + private val appBarLayoutChangeListener = + OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + if (!isLandscape) return@OnLayoutChangeListener + + val newScrollRange = topBar.totalScrollRange + if (newScrollRange == lastKnownScrollRange) return@OnLayoutChangeListener + + lastKnownScrollRange = newScrollRange + + if (!isTopBarRequestedVisible) { + topBar.post { + collapseTopBarWithoutAnimation() + updateEditorTopInset() + } + } + } + + private val bottomSheetCallback = + object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheetView: View, newState: Int) { + when (newState) { + BottomSheetBehavior.STATE_EXPANDED, + BottomSheetBehavior.STATE_HALF_EXPANDED -> onBottomBarExpandedOrHalfExpanded() + + BottomSheetBehavior.STATE_COLLAPSED -> onBottomBarCollapsed() + + BottomSheetBehavior.STATE_DRAGGING, + BottomSheetBehavior.STATE_SETTLING -> Unit + + BottomSheetBehavior.STATE_HIDDEN -> { + isBottomBarShown = false + } + } + } + + override fun onSlide(bottomSheetView: View, slideOffset: Float) = Unit + } + + init { + setupClickListeners() + } + + private val topBarTouchObserver: (MotionEvent) -> Unit = { event -> + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> onTopBarInteractionStarted() + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> onTopBarInteractionEnded() + } + } + + @SuppressLint("ClickableViewAccessibility") + fun bind() { + if (isBound) return + isBound = true + + topBar.addOnOffsetChangedListener(topBarOffsetListener) + appBarContent?.addOnLayoutChangeListener(appBarLayoutChangeListener) + bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback) + appBarContent?.onTouchEventObserved = topBarTouchObserver + } + + fun onPause() { + cancelAutoHide() + isUserInteractingWithTopBar = false + cancelBottomSheetAnimation() + } + + @SuppressLint("ClickableViewAccessibility") + fun destroy() { + onPause() + + if (!isBound) return + isBound = false + + topToggle.setOnClickListener(null) + bottomToggle.setOnClickListener(null) + + topBar.removeOnOffsetChangedListener(topBarOffsetListener) + appBarContent?.removeOnLayoutChangeListener(appBarLayoutChangeListener) + bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback) + appBarContent?.onTouchEventObserved = null + } + + fun onConfigurationChanged(newConfig: Configuration) { + isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE + if (isLandscape) enableImmersiveMode() else disableImmersiveMode() + updateEditorBottomInset() + } + + fun onSystemBarInsetsChanged(topInset: Int) { + statusBarTopInset = topInset + updateEditorTopInset() + } + + private fun setupClickListeners() { + topToggle.setOnClickListener { + if (!isLandscape) return@setOnClickListener + if (isTopBarRequestedVisible) hideTopBar() else showTopBar(autoHide = true) + } + + bottomToggle.setOnClickListener { + if (!isLandscape) return@setOnClickListener + if (isBottomBarShown) hideBottomBar() else showBottomBar() + } + } + + private fun onTopBarInteractionStarted() { + if (!isLandscape || !isTopBarRequestedVisible) return + isUserInteractingWithTopBar = true + cancelAutoHide() + } + + private fun onTopBarInteractionEnded() { + if (!isLandscape || !isTopBarRequestedVisible) return + isUserInteractingWithTopBar = false + scheduleTopBarAutoHide() + } + + private fun onBottomBarExpandedOrHalfExpanded() { + cancelBottomSheetAnimation() + setBottomSheetTranslation(0f) + isBottomBarShown = true + isBottomBarRequestedVisible = true + isPendingBottomBarHideAfterCollapse = false + updateEditorBottomInset() + } + + private fun onBottomBarCollapsed() { + if (isPendingBottomBarHideAfterCollapse && !isBottomBarRequestedVisible) { + isPendingBottomBarHideAfterCollapse = false + applyHiddenBottomBarTranslation(animate = true) + return + } + + cancelBottomSheetAnimation() + setBottomSheetTranslation(0f) + isBottomBarShown = true + updateEditorBottomInset() + } + + private fun enableImmersiveMode() { + setTogglesVisible(true) + + topBar.post { hideTopBar(animate = false) } + bottomSheet.post { hideBottomBar(animate = false) } + } + + private fun disableImmersiveMode() { + cancelAutoHide() + isUserInteractingWithTopBar = false + setTogglesVisible(false) + showTopBar(autoHide = false, animate = false) + showBottomBar(animate = false) + updateEditorBottomInset() + } + + private fun setTogglesVisible(visible: Boolean) { + topToggle.isVisible = visible + bottomToggle.isVisible = visible + } + + private fun showTopBar(autoHide: Boolean, animate: Boolean = true) { + cancelAutoHide() + isTopBarRequestedVisible = true + topBar.setExpanded(true, animate) + if (autoHide) scheduleTopBarAutoHide() + } + + private fun hideTopBar(animate: Boolean = true) { + cancelAutoHide() + isUserInteractingWithTopBar = false + isTopBarRequestedVisible = false + topBar.setExpanded(false, animate) + } + + private fun collapseTopBarWithoutAnimation() { + topBar.setExpanded(false, false) + currentAppBarOffset = -topBar.totalScrollRange + } + + private fun scheduleTopBarAutoHide() { + if (isUserInteractingWithTopBar || !isTopBarRequestedVisible) return + + autoHideJob = coroutineScope.launch { + delay(TOP_BAR_AUTO_HIDE_DELAY_MS) + if (!isUserInteractingWithTopBar && isTopBarRequestedVisible) { + hideTopBar() + } + } + } + + private fun cancelAutoHide() { + autoHideJob?.cancel() + autoHideJob = null + } + + private fun showBottomBar(animate: Boolean = true) { + isBottomBarRequestedVisible = true + isBottomBarShown = true + isPendingBottomBarHideAfterCollapse = false + + updateEditorBottomInset() + ensureBottomSheetCollapsed() + animateBottomSheetTranslation(to = 0f, animate = animate) + } + + private fun hideBottomBar(animate: Boolean = true) { + isBottomBarRequestedVisible = false + isBottomBarShown = false + updateEditorBottomInset() + + if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { + applyHiddenBottomBarTranslation(animate) + return + } + + isPendingBottomBarHideAfterCollapse = true + ensureBottomSheetCollapsed() + } + + private fun ensureBottomSheetCollapsed() { + if (bottomSheetBehavior.state != BottomSheetBehavior.STATE_COLLAPSED) { + bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + } + } + + private fun applyHiddenBottomBarTranslation(animate: Boolean) { + animateBottomSheetTranslation( + to = bottomSheetBehavior.peekHeight.toFloat(), + animate = animate, + ) + } + + private fun animateBottomSheetTranslation(to: Float, animate: Boolean) { + cancelBottomSheetAnimation() + if (animate) { + bottomSheet.animate() + .translationY(to) + .setDuration(BOTTOM_BAR_ANIMATION_DURATION_MS) + .start() + } else { + setBottomSheetTranslation(to) + } + } + + private fun cancelBottomSheetAnimation() { + bottomSheet.animate().cancel() + } + + private fun setBottomSheetTranslation(value: Float) { + bottomSheet.translationY = value + } + + private fun updateEditorBottomInset() { + val bottomMargin = if (isLandscape && isBottomBarShown) { + bottomSheetBehavior.peekHeight + } else { + 0 + } + + editorContainer.updateLayoutParams { + if (bottomMargin != this.bottomMargin) { + this.bottomMargin = bottomMargin + } + } + } + + /** + * Applies the editor top inset progressively as the app bar collapses, + * avoiding a jump at the end of the animation. + */ + private fun updateEditorTopInset() { + val topPadding = when { + !isLandscape || lastKnownScrollRange <= 0 -> 0 + else -> { + val collapseFraction = + (-currentAppBarOffset.toFloat() / lastKnownScrollRange.toFloat()) + .coerceIn(0f, 1f) + (statusBarTopInset * collapseFraction).roundToInt() + } + } + + viewContainer.updatePadding(top = topPadding) + } + + private companion object { + const val TOP_BAR_AUTO_HIDE_DELAY_MS = 3500L + const val BOTTOM_BAR_ANIMATION_DURATION_MS = 200L + } +} diff --git a/app/src/main/res/drawable/bg_hollow_green_circle.xml b/app/src/main/res/drawable/bg_hollow_green_circle.xml new file mode 100644 index 0000000000..4afdaace9e --- /dev/null +++ b/app/src/main/res/drawable/bg_hollow_green_circle.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/app/src/main/res/layout-land/content_editor.xml b/app/src/main/res/layout-land/content_editor.xml index 20a0336483..8d4d362df5 100644 --- a/app/src/main/res/layout-land/content_editor.xml +++ b/app/src/main/res/layout-land/content_editor.xml @@ -20,8 +20,7 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/realContainer" android:layout_width="match_parent" - android:layout_height="match_parent" - android:animateLayoutChanges="true"> + android:layout_height="match_parent"> - - + android:orientation="vertical" + app:layout_scrollFlags="scroll|enterAlways|snap"> - + - - + + + + + + + + + android:paddingBottom="0dp" /> - + - - - + android:indeterminate="true" /> - - - + @@ -158,4 +165,26 @@ android:id="@+id/diagnosticInfo" layout="@layout/layout_diagnostic_info" /> + + + + diff --git a/app/src/main/res/layout/content_editor.xml b/app/src/main/res/layout/content_editor.xml index ff4415253f..e62820c1a6 100644 --- a/app/src/main/res/layout/content_editor.xml +++ b/app/src/main/res/layout/content_editor.xml @@ -155,4 +155,26 @@ android:id="@+id/diagnosticInfo" layout="@layout/layout_diagnostic_info" /> + + + + \ No newline at end of file diff --git a/common/src/main/java/com/itsaky/androidide/ui/TouchObservingLinearLayout.kt b/common/src/main/java/com/itsaky/androidide/ui/TouchObservingLinearLayout.kt new file mode 100644 index 0000000000..f9b6376f71 --- /dev/null +++ b/common/src/main/java/com/itsaky/androidide/ui/TouchObservingLinearLayout.kt @@ -0,0 +1,19 @@ +package com.itsaky.androidide.ui + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.widget.LinearLayout + +class TouchObservingLinearLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : LinearLayout(context, attrs) { + + var onTouchEventObserved: ((MotionEvent) -> Unit)? = null + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + onTouchEventObserved?.invoke(ev) + return super.dispatchTouchEvent(ev) + } +} From e22d8fa1bf75cb9a1466f5c776bff8a2f4cb214a Mon Sep 17 00:00:00 2001 From: John Trujillo Date: Mon, 23 Mar 2026 17:03:11 -0500 Subject: [PATCH 2/3] fix: enhance immersive toggles and fix auto-hide lifecycle --- .../activities/editor/BaseEditorActivity.kt | 1 + .../editor/LandscapeImmersiveController.kt | 16 ++++-- .../main/res/layout-land/content_editor.xml | 32 +++++++----- app/src/main/res/layout/content_editor.xml | 50 +++++++++++-------- resources/src/main/res/values/strings.xml | 2 + 5 files changed, 65 insertions(+), 36 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 c959f689d0..332ac2c6a9 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 @@ -852,6 +852,7 @@ abstract class BaseEditorActivity : } override fun onResume() { + immersiveController?.onResume() super.onResume() invalidateOptionsMenu() diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/LandscapeImmersiveController.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/LandscapeImmersiveController.kt index d4d92f0044..417d881eb6 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/LandscapeImmersiveController.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/LandscapeImmersiveController.kt @@ -131,6 +131,12 @@ class LandscapeImmersiveController( cancelBottomSheetAnimation() } + fun onResume() { + if (isLandscape && isTopBarRequestedVisible && !isUserInteractingWithTopBar) { + scheduleTopBarAutoHide() + } + } + @SuppressLint("ClickableViewAccessibility") fun destroy() { onPause() @@ -166,7 +172,7 @@ class LandscapeImmersiveController( bottomToggle.setOnClickListener { if (!isLandscape) return@setOnClickListener - if (isBottomBarShown) hideBottomBar() else showBottomBar() + if (isBottomBarShown) hideBottomBar() else showBottomBar(expandHalfWay = true) } } @@ -260,13 +266,17 @@ class LandscapeImmersiveController( autoHideJob = null } - private fun showBottomBar(animate: Boolean = true) { + private fun showBottomBar(animate: Boolean = true, expandHalfWay: Boolean = false) { isBottomBarRequestedVisible = true isBottomBarShown = true isPendingBottomBarHideAfterCollapse = false updateEditorBottomInset() - ensureBottomSheetCollapsed() + + if (expandHalfWay) { + bottomSheetBehavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED + } else { ensureBottomSheetCollapsed() } + animateBottomSheetTranslation(to = 0f, animate = animate) } diff --git a/app/src/main/res/layout-land/content_editor.xml b/app/src/main/res/layout-land/content_editor.xml index 8d4d362df5..f30bc81e71 100644 --- a/app/src/main/res/layout-land/content_editor.xml +++ b/app/src/main/res/layout-land/content_editor.xml @@ -165,25 +165,33 @@ android:id="@+id/diagnosticInfo" layout="@layout/layout_diagnostic_info" /> - - diff --git a/app/src/main/res/layout/content_editor.xml b/app/src/main/res/layout/content_editor.xml index e62820c1a6..9d8721ac63 100644 --- a/app/src/main/res/layout/content_editor.xml +++ b/app/src/main/res/layout/content_editor.xml @@ -155,26 +155,34 @@ android:id="@+id/diagnosticInfo" layout="@layout/layout_diagnostic_info" /> - - - + + + \ No newline at end of file diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index ab300671cc..1383fa10f7 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -832,6 +832,8 @@ Breakpoint Project name Project Options + Toggle top bar + Toggle bottom bar Plugin Manager From 1def2f425664527f46ab82ad419455b3c173976b Mon Sep 17 00:00:00 2001 From: John Trujillo Date: Tue, 24 Mar 2026 10:34:57 -0500 Subject: [PATCH 3/3] fix: resolve immersive mode state issues on screen rotation --- .../activities/editor/BaseEditorActivity.kt | 28 +++-- .../editor/LandscapeImmersiveController.kt | 74 ++++++++++--- app/src/main/res/layout/content_editor.xml | 103 ++++++++++-------- 3 files changed, 128 insertions(+), 77 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 332ac2c6a9..fe90bb19a6 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 @@ -493,14 +493,15 @@ abstract class BaseEditorActivity : private fun applyStandardInsets(systemBars: Insets, windowInsets: WindowInsetsCompat) { val content = _binding?.content ?: return + val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - val appBarContent = content.editorAppbarContent - if (appBarContent != null) { - content.editorAppBarLayout.updatePadding(top = 0) - appBarContent.updatePadding(top = systemBars.top) - } else { - content.editorAppBarLayout.updatePadding(top = systemBars.top) - } + 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) + } immersiveController?.onSystemBarInsetsChanged(systemBars.top) applySidebarInsets(systemBars) @@ -510,15 +511,17 @@ 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 + systemBars.right + marginEnd = baseMargin + endInset } content.btnToggleBottomBar.updateLayoutParams { bottomMargin = baseMargin + systemBars.bottom - marginEnd = baseMargin + systemBars.right + marginEnd = baseMargin + endInset } content.bottomSheet.updatePadding(top = systemBars.top) @@ -750,9 +753,10 @@ abstract class BaseEditorActivity : _binding?.apply { contentCard.progress = progress val insetsTop = systemBarInsets?.top ?: 0 - content.editorAppBarLayout.updatePadding( - top = (insetsTop * (1f - progress)).roundToInt(), - ) + val topInset = (insetsTop * (1f - progress)).roundToInt() + + content.editorAppbarContent.updatePadding(top = topInset) + memUsageView.chart.updateLayoutParams { topMargin = (insetsTop * progress).roundToInt() } diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/LandscapeImmersiveController.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/LandscapeImmersiveController.kt index 417d881eb6..201c88b5ee 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/LandscapeImmersiveController.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/LandscapeImmersiveController.kt @@ -36,7 +36,7 @@ class LandscapeImmersiveController( private val coroutineScope: CoroutineScope, ) { private val topBar = contentBinding.editorAppBarLayout - private val appBarContent = runCatching { contentBinding.editorAppbarContent }.getOrNull() + private val appBarContent = contentBinding.editorAppbarContent private val viewContainer = contentBinding.viewContainer private val editorContainer = contentBinding.editorContainer private val bottomSheet = contentBinding.bottomSheet @@ -56,6 +56,8 @@ class LandscapeImmersiveController( private var statusBarTopInset = 0 private var currentAppBarOffset = 0 private var lastKnownScrollRange = 0 + private val defaultEditorBottomMargin = + (editorContainer.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin private val topBarOffsetListener = AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset -> @@ -69,18 +71,24 @@ class LandscapeImmersiveController( if (!isLandscape) return@OnLayoutChangeListener val newScrollRange = topBar.totalScrollRange - if (newScrollRange == lastKnownScrollRange) return@OnLayoutChangeListener + val scrollRangeChanged = newScrollRange != lastKnownScrollRange lastKnownScrollRange = newScrollRange - if (!isTopBarRequestedVisible) { - topBar.post { - collapseTopBarWithoutAnimation() - updateEditorTopInset() - } + val isVisuallyOutOfSync = !isTopBarRequestedVisible && currentAppBarOffset > -newScrollRange + + if (scrollRangeChanged || isVisuallyOutOfSync) { + topBar.post(::enforceCollapsedStateIfNeeded) } } + private fun enforceCollapsedStateIfNeeded() { + if (isTopBarRequestedVisible) return + + collapseTopBarWithoutAnimation() + updateEditorTopInset() + } + private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheetView: View, newState: Int) { @@ -120,15 +128,22 @@ class LandscapeImmersiveController( isBound = true topBar.addOnOffsetChangedListener(topBarOffsetListener) - appBarContent?.addOnLayoutChangeListener(appBarLayoutChangeListener) + appBarContent.addOnLayoutChangeListener(appBarLayoutChangeListener) bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback) - appBarContent?.onTouchEventObserved = topBarTouchObserver + appBarContent.onTouchEventObserved = topBarTouchObserver } fun onPause() { cancelAutoHide() isUserInteractingWithTopBar = false cancelBottomSheetAnimation() + setBottomSheetTranslation( + if (!isBottomBarShown && bottomSheetBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { + bottomSheetBehavior.peekHeight.toFloat() + } else { + 0f + }, + ) } fun onResume() { @@ -148,14 +163,30 @@ class LandscapeImmersiveController( bottomToggle.setOnClickListener(null) topBar.removeOnOffsetChangedListener(topBarOffsetListener) - appBarContent?.removeOnLayoutChangeListener(appBarLayoutChangeListener) + appBarContent.removeOnLayoutChangeListener(appBarLayoutChangeListener) bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback) - appBarContent?.onTouchEventObserved = null + appBarContent.onTouchEventObserved = null } fun onConfigurationChanged(newConfig: Configuration) { isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE - if (isLandscape) enableImmersiveMode() else disableImmersiveMode() + + appBarContent.updateLayoutParams { + scrollFlags = if (isLandscape) { + AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or + AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS or + AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP + } else { + 0 + } + } + + if (isLandscape) { + enableImmersiveMode() + } else { + disableImmersiveMode() + } + updateEditorBottomInset() } @@ -221,8 +252,17 @@ class LandscapeImmersiveController( cancelAutoHide() isUserInteractingWithTopBar = false setTogglesVisible(false) - showTopBar(autoHide = false, animate = false) - showBottomBar(animate = false) + + isTopBarRequestedVisible = true + topBar.setExpanded(true, false) + + isBottomBarRequestedVisible = true + isBottomBarShown = true + isPendingBottomBarHideAfterCollapse = false + + cancelBottomSheetAnimation() + setBottomSheetTranslation(0f) + updateEditorBottomInset() } @@ -328,10 +368,10 @@ class LandscapeImmersiveController( } private fun updateEditorBottomInset() { - val bottomMargin = if (isLandscape && isBottomBarShown) { - bottomSheetBehavior.peekHeight + val bottomMargin = if (isLandscape) { + if (isBottomBarShown) bottomSheetBehavior.peekHeight else 0 } else { - 0 + defaultEditorBottomMargin } editorContainer.updateLayoutParams { diff --git a/app/src/main/res/layout/content_editor.xml b/app/src/main/res/layout/content_editor.xml index 9d8721ac63..d1eb312308 100644 --- a/app/src/main/res/layout/content_editor.xml +++ b/app/src/main/res/layout/content_editor.xml @@ -31,63 +31,70 @@ android:fitsSystemWindows="false" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$Behavior"> - - + + - - + + + + + + + + android:paddingBottom="0dp" /> + - - - - - - + android:indeterminate="true" /> - + +