Skip to content

Commit 6221e49

Browse files
committed
More flexible modality.
Introduces the `ModalOverlay` interface. Moves as much code as possible from `BodyAndModalsContainer` to `LayeredDialogs`, and allows them to turn on modal event blocking behavior per managed `Overlay` instance, rather than always enforcing it. Very sad that I wasn't able to get rid of `BodyAndOverlaysContainer.onAttachedToWindow`, but the SavedStateRegistry timing is just too fussy to allow it. Git was dumb about diff'ing across renames, so they aren't all done in this commit for ease of review. The rest of the renames follow.
1 parent 3f7c5c8 commit 6221e49

File tree

16 files changed

+224
-162
lines changed

16 files changed

+224
-162
lines changed

benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/views/MayBeLoadingScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ val MayBeLoadingScreen.baseScreen: OverviewDetailScreen
2727

2828
@OptIn(WorkflowUiExperimentalApi::class)
2929
val MayBeLoadingScreen.loaders: List<LoaderSpinner>
30-
get() = modals.map { it.content }
30+
get() = overlays.map { it.content }

samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelOverlay.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ package com.squareup.sample.container.panel
22

33
import com.squareup.workflow1.ui.Screen
44
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
5+
import com.squareup.workflow1.ui.container.ModalOverlay
56
import com.squareup.workflow1.ui.container.ScreenOverlay
67

78
@OptIn(WorkflowUiExperimentalApi::class)
89
class PanelOverlay<T : Screen>(
910
override val content: T
10-
) : ScreenOverlay<T>
11+
) : ScreenOverlay<T>, ModalOverlay

samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/TicTacToeWorkflow.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class TicTacToeWorkflow(
5858
renderState: MainState,
5959
context: RenderContext
6060
): BodyAndModalsScreen<ScrimScreen<*>, *> {
61-
val bodyAndModals: BodyAndModalsScreen<*, *> = when (renderState) {
61+
val bodyAndOverlays: BodyAndModalsScreen<*, *> = when (renderState) {
6262
is Authenticating -> {
6363
val authBackStack = context.renderChild(authWorkflow) { handleAuthResult(it) }
6464
// We always show an empty GameScreen behind the PanelOverlay that
@@ -101,8 +101,8 @@ class TicTacToeWorkflow(
101101
}
102102

103103
// Add the scrim. Dim it only if there is a panel showing.
104-
val dim = bodyAndModals.modals.any { modal -> modal is PanelOverlay<*> }
105-
return bodyAndModals.mapBody { body -> ScrimScreen(body, dimmed = dim) }
104+
val dim = bodyAndOverlays.overlays.any { modal -> modal is PanelOverlay<*> }
105+
return bodyAndOverlays.mapBody { body -> ScrimScreen(body, dimmed = dim) }
106106
}
107107

108108
override fun snapshotState(state: MainState): Snapshot = state.toSnapshot()

samples/tictactoe/common/src/test/java/com/squareup/sample/mainworkflow/TicTacToeWorkflowTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class TicTacToeWorkflowTest {
6464
BackStackScreen<Screen>(S(wrapped))
6565

6666
private val BodyAndModalsScreen<ScrimScreen<*>, *>.panels: List<PanelOverlay<*>>
67-
get() = modals.mapNotNull { it as? PanelOverlay<*> }
67+
get() = overlays.mapNotNull { it as? PanelOverlay<*> }
6868

6969
private fun authWorkflow(
7070
screen: String = DEFAULT_AUTH

workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidDialogBounds.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import android.view.View
77
import android.view.Window
88
import com.squareup.workflow1.ui.ViewEnvironment
99
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
10-
import com.squareup.workflow1.ui.environment
1110
import kotlinx.coroutines.CoroutineScope
1211
import kotlinx.coroutines.Dispatchers
1312
import kotlinx.coroutines.cancel
@@ -40,7 +39,7 @@ internal fun <D : Dialog> D.maintainBounds(
4039
environment: ViewEnvironment,
4140
onBoundsChange: (D, Rect) -> Unit
4241
) {
43-
maintainBounds(environment[ModalArea].bounds, onBoundsChange)
42+
maintainBounds(environment[OverlayArea].bounds, onBoundsChange)
4443
}
4544

4645
@WorkflowUiExperimentalApi

workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndModalsContainer.kt

Lines changed: 18 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.squareup.workflow1.ui.container
22

33
import android.content.Context
4-
import android.graphics.Rect
54
import android.os.Parcel
65
import android.os.Parcelable
76
import android.os.Parcelable.Creator
@@ -10,124 +9,72 @@ import android.view.KeyEvent
109
import android.view.MotionEvent
1110
import android.view.ViewGroup
1211
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
13-
import android.view.ViewTreeObserver.OnGlobalLayoutListener
1412
import android.widget.FrameLayout
1513
import com.squareup.workflow1.ui.Compatible
16-
import com.squareup.workflow1.ui.Compatible.Companion.keyFor
1714
import com.squareup.workflow1.ui.R
1815
import com.squareup.workflow1.ui.ScreenViewFactory
1916
import com.squareup.workflow1.ui.ScreenViewHolder
2017
import com.squareup.workflow1.ui.ScreenViewHolder.Companion.Showing
2118
import com.squareup.workflow1.ui.ViewEnvironment
2219
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
2320
import com.squareup.workflow1.ui.WorkflowViewStub
24-
import com.squareup.workflow1.ui.androidx.WorkflowAndroidXSupport
25-
import kotlinx.coroutines.flow.MutableStateFlow
2621

2722
@WorkflowUiExperimentalApi
23+
// TODO Rename this BodyAndOverlaysContainer
2824
internal class BodyAndModalsContainer @JvmOverloads constructor(
2925
context: Context,
3026
attributeSet: AttributeSet? = null,
3127
defStyle: Int = 0,
3228
defStyleRes: Int = 0
3329
) : FrameLayout(context, attributeSet, defStyle, defStyleRes) {
3430
/**
35-
* Unique identifier for this view for SavedStateRegistry purposes. Based on the
36-
* [Compatible.keyFor] the current rendering. Taking this approach allows
37-
* feature developers to take control over naming, e.g. by wrapping renderings
38-
* with [NamedScreen][com.squareup.workflow1.ui.NamedScreen].
3931
*/
4032
private lateinit var savedStateParentKey: String
4133

4234
private val baseViewStub: WorkflowViewStub = WorkflowViewStub(context).also {
4335
addView(it, ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT))
4436
}
4537

46-
private val dialogs = LayeredDialogs(view = this, modal = true)
47-
48-
// The bounds of this view in global (display) coordinates, as reported
49-
// by getGlobalVisibleRect.
50-
//
51-
// Made available to managed ModalScreenOverlayDialogFactory instances
52-
// via the ModalArea key in ViewEnvironment. When this updates their
53-
// updateBounds methods will fire. They should resize themselves to
54-
// avoid covering peers of this view.
55-
private val bounds = MutableStateFlow(Rect())
56-
private val boundsRect = Rect()
57-
58-
private val boundsListener = OnGlobalLayoutListener {
59-
if (getGlobalVisibleRect(boundsRect) && boundsRect != bounds.value) {
60-
bounds.value = Rect(boundsRect)
61-
}
62-
// Should we close the dialogs if getGlobalVisibleRect returns false?
63-
// https://github.com/square/workflow-kotlin/issues/599
64-
}
65-
66-
// Note similar code in DialogHolder.
67-
private var allowEvents = true
68-
set(value) {
69-
val was = field
70-
field = value
71-
if (value != was) {
72-
// https://stackoverflow.com/questions/2886407/dealing-with-rapid-tapping-on-buttons
73-
// If any motion events were enqueued on the main thread, cancel them.
74-
dispatchCancelEvent { super.dispatchTouchEvent(it) }
75-
// When we cancel, have to warn things like RecyclerView that handle streams
76-
// of motion events and eventually dispatch input events (click, key pressed, etc.)
77-
// based on them.
78-
cancelPendingInputEvents()
79-
}
80-
}
38+
private val dialogs = LayeredDialogs.forView(
39+
view = this,
40+
superDispatchTouchEvent = { super.dispatchTouchEvent(it) }
41+
)
8142

8243
fun update(
8344
newScreen: BodyAndModalsScreen<*, *>,
8445
viewEnvironment: ViewEnvironment
8546
) {
86-
savedStateParentKey = keyFor(viewEnvironment[Showing])
87-
88-
val showingModals = newScreen.modals.isNotEmpty()
47+
savedStateParentKey = Compatible.keyFor(viewEnvironment[Showing])
8948

90-
// There is a long wait from when we show a dialog until it starts blocking
91-
// events for us. To compensate we ignore all touches while any dialogs exist.
92-
allowEvents = !showingModals
93-
94-
val baseEnv = if (showingModals) viewEnvironment + (CoveredByModal to true) else viewEnvironment
95-
baseViewStub.show(newScreen.body, baseEnv)
96-
97-
// Allow modal dialogs to restrict themselves to cover only this view.
98-
val dialogsEnv = if (showingModals) viewEnvironment + ModalArea(bounds) else viewEnvironment
99-
100-
dialogs.update(newScreen.modals, dialogsEnv)
49+
dialogs.update(newScreen.overlays, viewEnvironment) { env ->
50+
baseViewStub.show(newScreen.body, env)
51+
}
10152
}
10253

10354
override fun onAttachedToWindow() {
104-
super.onAttachedToWindow()
105-
boundsListener.onGlobalLayout()
106-
viewTreeObserver.addOnGlobalLayoutListener(boundsListener)
55+
// I tried to move this to the attachStateChangeListener in LayeredDialogs.Companion.forView,
56+
// but that fires too late and we crash with the dreaded
57+
// "You can consumeRestoredStateForKey only after super.onCreate of corresponding component".
10758

59+
super.onAttachedToWindow()
10860
// Wire up dialogs to our parent SavedStateRegistry.
109-
val parentRegistryOwner = WorkflowAndroidXSupport.stateRegistryOwnerFromViewTreeOrContext(this)
110-
dialogs.attachToParentRegistryOwner(savedStateParentKey, parentRegistryOwner)
61+
dialogs.onAttachedToWindow(savedStateParentKey, this)
11162
}
11263

11364
override fun onDetachedFromWindow() {
11465
// Disconnect dialogs from our parent SavedStateRegistry so that it doesn't get asked
11566
// to save state anymore.
116-
dialogs.detachFromParentRegistry()
117-
// Don't leak the dialogs if we're suddenly yanked out of view.
118-
// https://github.com/square/workflow-kotlin/issues/314
119-
dialogs.update(emptyList(), ViewEnvironment.EMPTY)
120-
viewTreeObserver.removeOnGlobalLayoutListener(boundsListener)
121-
bounds.value = Rect()
67+
dialogs.onDetachedFromWindow()
68+
12269
super.onDetachedFromWindow()
12370
}
12471

12572
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
126-
return !allowEvents || super.dispatchTouchEvent(event)
73+
return !dialogs.allowEvents || super.dispatchTouchEvent(event)
12774
}
12875

12976
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
130-
return !allowEvents || super.dispatchKeyEvent(event)
77+
return !dialogs.allowEvents || super.dispatchKeyEvent(event)
13178
}
13279

13380
override fun onSaveInstanceState(): Parcelable {

workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/CoveredByModal.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
55

66
/**
77
* True in views managed by [BodyAndModalsScreen] when their events are being blocked
8-
* by a modal [Overlay].
8+
* by a [ModalOverlay].
99
*/
1010
@WorkflowUiExperimentalApi
1111
internal object CoveredByModal : ViewEnvironmentKey<Boolean>(type = Boolean::class) {

workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/DialogHolder.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,12 @@ import com.squareup.workflow1.ui.compatible
2222

2323
/**
2424
* Used by [LayeredDialogs] to keep a [Dialog] tied to its [rendering] and [environment].
25-
*
26-
* @param modal if true, ignore keyboard and touch events when [CoveredByModal] is also true
2725
*/
2826
@WorkflowUiExperimentalApi
2927
internal class DialogHolder<T : Overlay>(
3028
initialRendering: T,
3129
initialViewEnvironment: ViewEnvironment,
3230
index: Int,
33-
private val modal: Boolean,
3431
private val context: Context,
3532
private val factory: OverlayDialogFactory<T>
3633
) {
@@ -42,9 +39,11 @@ internal class DialogHolder<T : Overlay>(
4239
var environment: ViewEnvironment = initialViewEnvironment
4340
private set
4441

42+
private val modal = initialRendering is ModalOverlay
43+
4544
private var dialogOrNull: Dialog? = null
4645

47-
// Note similar code in BodyAndModalsContainer
46+
// Note similar code in LayeredDialogs
4847
private var allowEvents = true
4948
set(value) {
5049
val was = field

0 commit comments

Comments
 (0)