From da1e4352127b3db2e43e771bd48559ba6559ac8f Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Mon, 15 Jun 2026 12:35:45 -0400 Subject: [PATCH 1/3] Fix for swiping bottom sheet tabs incorrectly opening file menu --- .../itsaky/androidide/ui/EditorBottomSheet.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) 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 cb75b20da4..0d060a22e2 100644 --- a/app/src/main/java/com/itsaky/androidide/ui/EditorBottomSheet.kt +++ b/app/src/main/java/com/itsaky/androidide/ui/EditorBottomSheet.kt @@ -30,8 +30,10 @@ import android.widget.RelativeLayout import androidx.activity.viewModels import androidx.annotation.GravityInt import androidx.core.graphics.Insets +import androidx.core.view.GravityCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.drawerlayout.widget.DrawerLayout import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.core.view.updatePaddingRelative @@ -123,6 +125,7 @@ constructor( private val buildOutputViewModel by (context as FragmentActivity).viewModels() private lateinit var mediator: TabLayoutMediator private var shareJob: Job? = null + private var drawerLockCallback: BottomSheetBehavior.BottomSheetCallback? = null companion object { private val log = LoggerFactory.getLogger(EditorBottomSheet::class.java) @@ -260,6 +263,26 @@ constructor( insets.getInsets(WindowInsetsCompat.Type.mandatorySystemGestures()) insets } + + // Lock the start-edge drawer swipe whenever the bottom sheet is open. + // DrawerLayout's edge tracker otherwise fires when a tab-strip drag + // drifts toward the screen's left edge. Hamburger taps still work — + // LOCK_MODE_LOCKED_CLOSED only blocks user gestures, not openDrawer(). + drawerLockCallback = object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + val drawer = (context as FragmentActivity).findViewById(R.id.editor_drawerLayout) + ?: return + val lockMode = when (newState) { + BottomSheetBehavior.STATE_COLLAPSED, + BottomSheetBehavior.STATE_HIDDEN, + -> DrawerLayout.LOCK_MODE_UNLOCKED + else -> DrawerLayout.LOCK_MODE_LOCKED_CLOSED + } + drawer.setDrawerLockMode(lockMode, GravityCompat.START) + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit + }.also { behavior.addBottomSheetCallback(it) } } override fun onDetachedFromWindow() { @@ -269,6 +292,11 @@ constructor( mediator.detach() } + drawerLockCallback?.let { behavior.removeBottomSheetCallback(it) } + drawerLockCallback = null + (context as FragmentActivity).findViewById(R.id.editor_drawerLayout) + ?.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START) + binding.tabs.clearOnTabSelectedListeners() binding.shareOutputFab.setOnClickListener(null) binding.shareOutputFab.setOnLongClickListener(null) From 665c4b9ca9887cc0ea5ed48539b1317da54dc49f Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Mon, 15 Jun 2026 13:35:51 -0400 Subject: [PATCH 2/3] ADFA-4320 Skip drawer fling-open when swiping bottom sheet tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tab-strip flings have enough horizontal velocity to trigger the editor activity's drawer-open gesture detector, opening the file tree when the user is just navigating between bottom-pane tabs. In dispatchTouchEvent, check on ACTION_DOWN whether the touch falls inside the bottom sheet TabLayout's global visible rect; if so, stop forwarding the touch sequence to the gesture detector. The hamburger button and the existing no-files horizontal-fling shortcut remain untouched. Reverts the bottom-sheet-state drawer lock from da1e43521 — that approach broke the hamburger because ActionBarDrawerToggle treats LOCK_MODE_LOCKED_CLOSED as "do not open" and the lock also fired on gestures outside the tab strip. --- .../activities/editor/BaseEditorActivity.kt | 21 ++++++++++++-- .../itsaky/androidide/ui/EditorBottomSheet.kt | 28 ------------------- 2 files changed, 18 insertions(+), 31 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 5205da894e..1416b1a66f 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 @@ -22,6 +22,7 @@ import android.content.Intent import android.content.ServiceConnection import android.content.res.Configuration import android.graphics.Color +import android.graphics.Rect import android.graphics.drawable.GradientDrawable import android.os.Build import android.os.Bundle @@ -402,6 +403,8 @@ abstract class BaseEditorActivity : private var gestureDetector: GestureDetector? = null private val flingDistanceThreshold by lazy { SizeUtils.dp2px(100f) } private val flingVelocityThreshold by lazy { SizeUtils.dp2px(100f) } + private var suppressDrawerGesture = false + private val bottomSheetTabsHitRect = Rect() private var editorAppBarInsetTop: Int = 0 @@ -1613,14 +1616,26 @@ abstract class BaseEditorActivity : } override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { - // Pass the event to our gesture detector first if (ev != null) { - gestureDetector?.onTouchEvent(ev) + // A fling that begins on the bottom-sheet tab strip is the user + // scrolling tabs, not asking for the file tree. Skip the drawer + // gesture detector for the whole touch sequence in that case. + if (ev.actionMasked == MotionEvent.ACTION_DOWN) { + suppressDrawerGesture = isTouchOnBottomSheetTabs(ev) + } + if (!suppressDrawerGesture) { + gestureDetector?.onTouchEvent(ev) + } } - // Then, let the default dispatching happen return super.dispatchTouchEvent(ev) } + private fun isTouchOnBottomSheetTabs(ev: MotionEvent): Boolean { + val tabs = content.bottomSheet.binding.tabs + if (!tabs.getGlobalVisibleRect(bottomSheetTabsHitRect)) return false + return bottomSheetTabsHitRect.contains(ev.rawX.toInt(), ev.rawY.toInt()) + } + private fun showTooltip(tag: String) { TooltipManager.showIdeCategoryTooltip( context = this, 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 0d060a22e2..cb75b20da4 100644 --- a/app/src/main/java/com/itsaky/androidide/ui/EditorBottomSheet.kt +++ b/app/src/main/java/com/itsaky/androidide/ui/EditorBottomSheet.kt @@ -30,10 +30,8 @@ import android.widget.RelativeLayout import androidx.activity.viewModels import androidx.annotation.GravityInt import androidx.core.graphics.Insets -import androidx.core.view.GravityCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat -import androidx.drawerlayout.widget.DrawerLayout import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.core.view.updatePaddingRelative @@ -125,7 +123,6 @@ constructor( private val buildOutputViewModel by (context as FragmentActivity).viewModels() private lateinit var mediator: TabLayoutMediator private var shareJob: Job? = null - private var drawerLockCallback: BottomSheetBehavior.BottomSheetCallback? = null companion object { private val log = LoggerFactory.getLogger(EditorBottomSheet::class.java) @@ -263,26 +260,6 @@ constructor( insets.getInsets(WindowInsetsCompat.Type.mandatorySystemGestures()) insets } - - // Lock the start-edge drawer swipe whenever the bottom sheet is open. - // DrawerLayout's edge tracker otherwise fires when a tab-strip drag - // drifts toward the screen's left edge. Hamburger taps still work — - // LOCK_MODE_LOCKED_CLOSED only blocks user gestures, not openDrawer(). - drawerLockCallback = object : BottomSheetBehavior.BottomSheetCallback() { - override fun onStateChanged(bottomSheet: View, newState: Int) { - val drawer = (context as FragmentActivity).findViewById(R.id.editor_drawerLayout) - ?: return - val lockMode = when (newState) { - BottomSheetBehavior.STATE_COLLAPSED, - BottomSheetBehavior.STATE_HIDDEN, - -> DrawerLayout.LOCK_MODE_UNLOCKED - else -> DrawerLayout.LOCK_MODE_LOCKED_CLOSED - } - drawer.setDrawerLockMode(lockMode, GravityCompat.START) - } - - override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit - }.also { behavior.addBottomSheetCallback(it) } } override fun onDetachedFromWindow() { @@ -292,11 +269,6 @@ constructor( mediator.detach() } - drawerLockCallback?.let { behavior.removeBottomSheetCallback(it) } - drawerLockCallback = null - (context as FragmentActivity).findViewById(R.id.editor_drawerLayout) - ?.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START) - binding.tabs.clearOnTabSelectedListeners() binding.shareOutputFab.setOnClickListener(null) binding.shareOutputFab.setOnLongClickListener(null) From 53f843fd6da4ec24994aac443ddf9fa4d15545be Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Mon, 15 Jun 2026 14:14:04 -0400 Subject: [PATCH 3/3] ADFA-4320 Move tab-strip check into onFling and harden against teardown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code review surfaced three issues with the previous implementation: 1. content.bottomSheet.binding.tabs reads a binding chain whose root getter throws IllegalStateException once _binding is cleared in preDestroy. Touches dispatched between preDestroy and window teardown would crash. Switched to contentOrNull, the file's existing defensive helper. 2. The dispatchTouchEvent flag was set only on ACTION_DOWN, so ACTION_POINTER_DOWN (multi-touch) could not re-evaluate it; a second finger could leak past or stay stuck-suppressed. 3. The check belonged with the other fling filters (startedNearTopEdge, noFilesOpen, etc.) in onFling, not in dispatchTouchEvent — and onFling already tracks the down event as e1, so no extra state is needed. Folded the check into the isDrawerOpenFling branch using e1. Dropped suppressDrawerGesture and bottomSheetTabsHitRect fields; restored dispatchTouchEvent to its pre-PR form. The hit-test now runs only when a drawer-open fling is actually detected, not on every ACTION_DOWN. --- .../activities/editor/BaseEditorActivity.kt | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 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 1416b1a66f..a0ee361bcb 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 @@ -403,8 +403,6 @@ abstract class BaseEditorActivity : private var gestureDetector: GestureDetector? = null private val flingDistanceThreshold by lazy { SizeUtils.dp2px(100f) } private val flingVelocityThreshold by lazy { SizeUtils.dp2px(100f) } - private var suppressDrawerGesture = false - private val bottomSheetTabsHitRect = Rect() private var editorAppBarInsetTop: Int = 0 @@ -1604,7 +1602,9 @@ abstract class BaseEditorActivity : } // Filter out diagonal flings so only an intentional right swipe opens the drawer. - if (isDrawerOpenFling) { + // A horizontal fling that started on the bottom-sheet tab strip is the user + // scrolling tabs, not asking for the drawer. + if (isDrawerOpenFling && !isTouchOnBottomSheetTabs(e1)) { binding.editorDrawerLayout.openDrawer(GravityCompat.START) return true } @@ -1616,24 +1616,19 @@ abstract class BaseEditorActivity : } override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { + // Pass the event to our gesture detector first if (ev != null) { - // A fling that begins on the bottom-sheet tab strip is the user - // scrolling tabs, not asking for the file tree. Skip the drawer - // gesture detector for the whole touch sequence in that case. - if (ev.actionMasked == MotionEvent.ACTION_DOWN) { - suppressDrawerGesture = isTouchOnBottomSheetTabs(ev) - } - if (!suppressDrawerGesture) { - gestureDetector?.onTouchEvent(ev) - } + gestureDetector?.onTouchEvent(ev) } + // Then, let the default dispatching happen return super.dispatchTouchEvent(ev) } private fun isTouchOnBottomSheetTabs(ev: MotionEvent): Boolean { - val tabs = content.bottomSheet.binding.tabs - if (!tabs.getGlobalVisibleRect(bottomSheetTabsHitRect)) return false - return bottomSheetTabsHitRect.contains(ev.rawX.toInt(), ev.rawY.toInt()) + val tabs = contentOrNull?.bottomSheet?.binding?.tabs ?: return false + val rect = Rect() + if (!tabs.getGlobalVisibleRect(rect)) return false + return rect.contains(ev.rawX.toInt(), ev.rawY.toInt()) } private fun showTooltip(tag: String) {