Skip to content

Commit 138d8d2

Browse files
committed
refactor(editor): clean up fullscreen state handling
1 parent 69f14ec commit 138d8d2

File tree

2 files changed

+154
-48
lines changed

2 files changed

+154
-48
lines changed

app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ abstract class BaseEditorActivity :
177177

178178
private val fileManagerViewModel by viewModels<FileManagerViewModel>()
179179
private var feedbackButtonManager: FeedbackButtonManager? = null
180-
private var fullscreenController: FullscreenController? = null
180+
private var fullscreenManager: FullscreenManager? = null
181181
private val topEdgeThreshold by lazy { SizeUtils.dp2px(TOP_EDGE_SWIPE_THRESHOLD_DP) }
182182

183183
var isDestroying = false
@@ -455,8 +455,8 @@ abstract class BaseEditorActivity :
455455
editorBottomSheet = null
456456
gestureDetector = null
457457

458-
fullscreenController?.destroy()
459-
fullscreenController = null
458+
fullscreenManager?.destroy()
459+
fullscreenManager = null
460460

461461
_binding = null
462462

@@ -629,7 +629,7 @@ abstract class BaseEditorActivity :
629629
setupFullscreenObserver()
630630
setupViews()
631631

632-
fullscreenController = FullscreenController(
632+
fullscreenManager = FullscreenManager(
633633
contentBinding = content,
634634
bottomSheetBehavior = editorBottomSheet!!,
635635
closeDrawerAction = {
@@ -677,13 +677,14 @@ abstract class BaseEditorActivity :
677677
window?.decorView?.let { ViewCompat.requestApplyInsets(it) }
678678
reapplySystemBarInsetsFromRoot()
679679
_binding?.content?.applyBottomSheetAnchorForOrientation(newConfig.orientation)
680-
fullscreenController?.render(editorViewModel.isFullscreen, animate = false)
680+
fullscreenManager?.render(editorViewModel.isFullscreen, animate = false)
681681
}
682682

683683
private fun reapplySystemBarInsetsFromRoot() {
684684
val root = _binding?.root ?: return
685685
val rootInsets = ViewCompat.getRootWindowInsets(root)
686686
if (rootInsets == null) {
687+
// Insets can be temporarily unavailable right after a configuration change.
687688
root.post { reapplySystemBarInsetsFromRoot() }
688689
return
689690
}
@@ -1232,7 +1233,7 @@ abstract class BaseEditorActivity :
12321233
lifecycleScope.launch {
12331234
repeatOnLifecycle(Lifecycle.State.STARTED) {
12341235
editorViewModel.uiState.collectLatest { uiState ->
1235-
fullscreenController?.render(uiState.isFullscreen, animate = true)
1236+
fullscreenManager?.render(uiState.isFullscreen, animate = true)
12361237
}
12371238
}
12381239
}
@@ -1473,27 +1474,37 @@ abstract class BaseEditorActivity :
14731474

14741475
val startedNearTopEdge = e1.y < topEdgeThreshold
14751476
val startedNearBottomEdge = e1.y > bottomEdgeThreshold
1476-
1477-
if (isVerticalSwipe && hasVerticalVelocity && startedNearTopEdge && hasDownFlingDistance) {
1478-
if (editorViewModel.isFullscreen) {
1479-
editorViewModel.exitFullscreen()
1480-
return true
1481-
}
1477+
val isTopEdgeDismissFling = isVerticalSwipe &&
1478+
hasVerticalVelocity &&
1479+
startedNearTopEdge &&
1480+
hasDownFlingDistance
1481+
val isBottomEdgeDismissFling = isVerticalSwipe &&
1482+
hasVerticalVelocity &&
1483+
startedNearBottomEdge &&
1484+
hasUpFlingDistance
1485+
val isDrawerOpenFling = hasRightFlingDistance &&
1486+
hasHorizontalVelocity &&
1487+
isHorizontalSwipe
1488+
1489+
// Fullscreen mode can be dismissed with an inward fling from either vertical edge.
1490+
if (isTopEdgeDismissFling && editorViewModel.isFullscreen) {
1491+
editorViewModel.exitFullscreen()
1492+
return true
14821493
}
14831494

1484-
if (isVerticalSwipe && hasVerticalVelocity && startedNearBottomEdge && hasUpFlingDistance) {
1485-
if (editorViewModel.isFullscreen) {
1486-
editorViewModel.exitFullscreen()
1487-
return true
1488-
}
1495+
if (isBottomEdgeDismissFling && editorViewModel.isFullscreen) {
1496+
editorViewModel.exitFullscreen()
1497+
return true
14891498
}
14901499

1500+
// Preserve the editor interaction area; drawer gestures are only enabled on the empty state.
14911501
val noFilesOpen = content.viewContainer.displayedChild == 1
14921502
if (!noFilesOpen) {
14931503
return false
14941504
}
14951505

1496-
if (hasRightFlingDistance && hasHorizontalVelocity && isHorizontalSwipe) {
1506+
// Filter out diagonal flings so only an intentional right swipe opens the drawer.
1507+
if (isDrawerOpenFling) {
14971508
binding.editorDrawerLayout.openDrawer(GravityCompat.START)
14981509
return true
14991510
}

app/src/main/java/com/itsaky/androidide/activities/editor/FullscreenController.kt renamed to app/src/main/java/com/itsaky/androidide/activities/editor/FullscreenManager.kt

Lines changed: 125 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,102 @@ import com.itsaky.androidide.R
99
import com.itsaky.androidide.databinding.ContentEditorBinding
1010
import kotlin.math.abs
1111

12-
class FullscreenController(
12+
class FullscreenManager(
1313
private val contentBinding: ContentEditorBinding,
1414
private val bottomSheetBehavior: BottomSheetBehavior<out View?>,
1515
private val closeDrawerAction: () -> Unit,
1616
private val onFullscreenToggleRequested: () -> Unit,
1717
) {
18+
private sealed interface FullscreenUiState {
19+
val isFullscreen: Boolean
20+
21+
data object Fullscreen : FullscreenUiState {
22+
override val isFullscreen = true
23+
}
24+
25+
data object Windowed : FullscreenUiState {
26+
override val isFullscreen = false
27+
}
28+
29+
companion object {
30+
fun from(isFullscreen: Boolean): FullscreenUiState {
31+
return if (isFullscreen) Fullscreen else Windowed
32+
}
33+
}
34+
}
35+
36+
private sealed interface FullscreenRenderCommand {
37+
val targetState: FullscreenUiState
38+
val animate: Boolean
39+
40+
fun apply(manager: FullscreenManager)
41+
42+
data class EnterFullscreen(
43+
override val animate: Boolean,
44+
) : FullscreenRenderCommand {
45+
override val targetState = FullscreenUiState.Fullscreen
46+
47+
override fun apply(manager: FullscreenManager) {
48+
manager.applyFullscreen(animate)
49+
}
50+
}
51+
52+
data class ExitFullscreen(
53+
override val animate: Boolean,
54+
) : FullscreenRenderCommand {
55+
override val targetState = FullscreenUiState.Windowed
56+
57+
override fun apply(manager: FullscreenManager) {
58+
manager.applyNonFullscreen(animate)
59+
}
60+
}
61+
62+
data class Refresh(
63+
override val targetState: FullscreenUiState,
64+
) : FullscreenRenderCommand {
65+
override val animate = false
66+
67+
override fun apply(manager: FullscreenManager) {
68+
if (targetState.isFullscreen) {
69+
manager.applyFullscreen(animate = false)
70+
} else {
71+
manager.applyNonFullscreen(animate = false)
72+
}
73+
}
74+
}
75+
76+
companion object {
77+
fun resolve(
78+
currentState: FullscreenUiState,
79+
targetState: FullscreenUiState,
80+
animate: Boolean,
81+
): FullscreenRenderCommand {
82+
val shouldAnimate = animate && currentState != targetState
83+
84+
if (!shouldAnimate) {
85+
return Refresh(targetState)
86+
}
87+
88+
return if (targetState.isFullscreen) {
89+
EnterFullscreen(animate = true)
90+
} else {
91+
ExitFullscreen(animate = true)
92+
}
93+
}
94+
}
95+
}
96+
1897
private val topBar = contentBinding.editorAppBarLayout
1998
private val appBarContent = contentBinding.editorAppbarContent
2099
private val editorContainer = contentBinding.editorContainer
21100
private val fullscreenToggle = contentBinding.btnFullscreenToggle
22101

23102
private var isBound = false
24103
private var isTransitioning = false
25-
private var currentFullscreen = false
104+
private var currentState: FullscreenUiState = FullscreenUiState.Windowed
26105
private var defaultSkipCollapsed = false
106+
private var transitionToken = 0L
107+
private var pendingTransitionToken = 0L
27108

28109
private val transitionDurationMs = 350L
29110

@@ -68,28 +149,22 @@ class FullscreenController(
68149
bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback)
69150
bottomSheetBehavior.skipCollapsed = defaultSkipCollapsed
70151
fullscreenToggle.removeCallbacks(clearTransitioningRunnable)
152+
isTransitioning = false
71153
}
72154

73155
fun render(isFullscreen: Boolean, animate: Boolean) {
74-
val stateChanged = currentFullscreen != isFullscreen
75-
val shouldAnimate = animate && stateChanged
76-
currentFullscreen = isFullscreen
77-
isTransitioning = shouldAnimate
78-
79-
if (isFullscreen) {
80-
applyFullscreen(shouldAnimate)
81-
} else {
82-
applyNonFullscreen(shouldAnimate)
83-
}
84-
85-
syncToggleUi(isFullscreen)
156+
val targetState = FullscreenUiState.from(isFullscreen)
157+
val command =
158+
FullscreenRenderCommand.resolve(
159+
currentState = currentState,
160+
targetState = targetState,
161+
animate = animate,
162+
)
86163

87-
if (shouldAnimate) {
88-
fullscreenToggle.removeCallbacks(clearTransitioningRunnable)
89-
fullscreenToggle.postDelayed(clearTransitioningRunnable, transitionDurationMs)
90-
} else {
91-
isTransitioning = false
92-
}
164+
currentState = command.targetState
165+
syncTransitionState(command)
166+
command.apply(this)
167+
syncToggleUi(command.targetState)
93168
}
94169

95170
private fun setupScrollFlags() {
@@ -101,17 +176,35 @@ class FullscreenController(
101176
}
102177

103178
private fun handleBottomSheetStateChange(newState: Int) {
104-
if (newState == BottomSheetBehavior.STATE_COLLAPSED && !currentFullscreen) {
179+
val isCollapsedInWindowedMode =
180+
newState == BottomSheetBehavior.STATE_COLLAPSED && !currentState.isFullscreen
181+
val isSheetRevealedWhileFullscreen =
182+
(newState == BottomSheetBehavior.STATE_EXPANDED ||
183+
newState == BottomSheetBehavior.STATE_HALF_EXPANDED) &&
184+
currentState.isFullscreen &&
185+
!isTransitioning
186+
187+
if (isCollapsedInWindowedMode) {
105188
bottomSheetBehavior.isHideable = false
106189
}
107190

108-
if (newState == BottomSheetBehavior.STATE_EXPANDED ||
109-
newState == BottomSheetBehavior.STATE_HALF_EXPANDED
110-
) {
111-
if (currentFullscreen && !isTransitioning) {
112-
onFullscreenToggleRequested()
113-
}
191+
if (isSheetRevealedWhileFullscreen) {
192+
onFullscreenToggleRequested()
193+
}
194+
}
195+
196+
private fun syncTransitionState(command: FullscreenRenderCommand) {
197+
fullscreenToggle.removeCallbacks(clearTransitioningRunnable)
198+
199+
if (!command.animate) {
200+
isTransitioning = false
201+
transitionToken++
202+
return
114203
}
204+
205+
isTransitioning = true
206+
pendingTransitionToken = ++transitionToken
207+
fullscreenToggle.postDelayed(clearTransitioningRunnable, transitionDurationMs)
115208
}
116209

117210
private fun applyFullscreen(animate: Boolean) {
@@ -145,8 +238,8 @@ class FullscreenController(
145238
}
146239
}
147240

148-
private fun syncToggleUi(isFullscreen: Boolean) {
149-
if (isFullscreen) {
241+
private fun syncToggleUi(state: FullscreenUiState) {
242+
if (state.isFullscreen) {
150243
fullscreenToggle.setImageResource(R.drawable.ic_fullscreen_exit)
151244
fullscreenToggle.contentDescription =
152245
contentBinding.root.context.getString(R.string.desc_exit_fullscreen)
@@ -158,6 +251,8 @@ class FullscreenController(
158251
}
159252

160253
private val clearTransitioningRunnable = Runnable {
161-
isTransitioning = false
254+
if (pendingTransitionToken == transitionToken) {
255+
isTransitioning = false
256+
}
162257
}
163258
}

0 commit comments

Comments
 (0)