1
1
package com.squareup.workflow1.ui.container
2
2
3
- import android.app.Dialog
4
- import android.content.Context
5
3
import android.os.Bundle
6
4
import android.os.Parcel
7
5
import android.os.Parcelable
@@ -14,41 +12,26 @@ import androidx.core.view.doOnDetach
14
12
import androidx.lifecycle.DefaultLifecycleObserver
15
13
import androidx.lifecycle.LifecycleOwner
16
14
import com.squareup.workflow1.ui.Compatible
17
- import com.squareup.workflow1.ui.ViewEnvironment
18
15
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
19
16
import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner
20
17
import com.squareup.workflow1.ui.androidx.WorkflowSavedStateRegistryAggregator
21
- import com.squareup.workflow1.ui.compatible
22
18
23
19
/* *
24
- * Used by [LayeredDialogs] to keep a [Dialog] tied to its [rendering] and [environment].
20
+ * Used by [LayeredDialogs] to manage lifecycle and view persistence concerns for an
21
+ * [OverlayDialogHolder], as well as enforcing modal behavior.
25
22
*/
26
23
@WorkflowUiExperimentalApi
27
- internal class DialogHolder <T : Overlay >(
28
- initialRendering : T ,
29
- initialViewEnvironment : ViewEnvironment ,
24
+ // TODO Rename DialogSession
25
+ internal class DialogHolder (
30
26
index : Int ,
31
- private val context : Context ,
32
- private val factory : OverlayDialogFactory <T >
27
+ holder : OverlayDialogHolder <Overlay >
33
28
) {
34
- val savedStateRegistryKey = Compatible .keyFor(initialRendering, index.toString())
35
-
36
- var rendering: T = initialRendering
37
- private set
38
-
39
- var environment: ViewEnvironment = initialViewEnvironment
40
- private set
41
-
42
- private val modal = initialRendering is ModalOverlay
43
-
44
- private var dialogOrNull: Dialog ? = null
45
-
46
29
// Note similar code in LayeredDialogs
47
30
private var allowEvents = true
48
31
set(value) {
49
32
val was = field
50
33
field = value
51
- dialogOrNull? .window?.takeIf { value != was }?.let { window ->
34
+ holder.dialog .window?.takeIf { value != was }?.let { window ->
52
35
// https://stackoverflow.com/questions/2886407/dealing-with-rapid-tapping-on-buttons
53
36
// If any motion events were enqueued on the main thread, cancel them.
54
37
dispatchCancelEvent { window.superDispatchTouchEvent(it) }
@@ -59,61 +42,73 @@ internal class DialogHolder<T : Overlay>(
59
42
}
60
43
}
61
44
62
- fun show (
45
+ /* *
46
+ * Wrap the given dialog holder to maintain [allowEvents] on each update.
47
+ */
48
+ val holder: OverlayDialogHolder <Overlay > = OverlayDialogHolder (
49
+ holder.environment, holder.dialog
50
+ ) { overlay, environment ->
51
+ allowEvents = ! environment[CoveredByModal ]
52
+ holder.show(overlay, environment)
53
+ }
54
+
55
+ val savedStateRegistryKey = Compatible .keyFor(holder.showing, index.toString())
56
+
57
+ fun showDialog (
63
58
parentLifecycleOwner : LifecycleOwner ,
64
59
stateRegistryAggregator : WorkflowSavedStateRegistryAggregator
65
60
) {
66
- requireDialog().let { dialog ->
67
- dialog.window?.let { window ->
68
- val realWindowCallback = window.callback
69
- window.callback = object : Window .Callback by realWindowCallback {
70
- override fun dispatchTouchEvent (event : MotionEvent ): Boolean {
71
- return ! allowEvents || realWindowCallback.dispatchTouchEvent(event)
72
- }
61
+ val dialog = holder.dialog
73
62
74
- override fun dispatchKeyEvent (event : KeyEvent ): Boolean {
75
- return ! allowEvents || realWindowCallback.dispatchKeyEvent(event)
76
- }
63
+ dialog.window?.let { window ->
64
+ val realWindowCallback = window.callback
65
+ window.callback = object : Window .Callback by realWindowCallback {
66
+ override fun dispatchTouchEvent (event : MotionEvent ): Boolean {
67
+ return ! allowEvents || realWindowCallback.dispatchTouchEvent(event)
68
+ }
69
+
70
+ override fun dispatchKeyEvent (event : KeyEvent ): Boolean {
71
+ return ! allowEvents || realWindowCallback.dispatchKeyEvent(event)
77
72
}
78
73
}
74
+ }
79
75
80
- dialog.show()
81
- dialog.window?.decorView?.also { decorView ->
82
- // Implementations of buildDialog may set their own WorkflowLifecycleOwner on the
83
- // content view, so to avoid interfering with them we also set it here. When the views
84
- // are attached, this will become the parent lifecycle of the one from buildDialog if
85
- // any, and so we can use our lifecycle to destroy-on-detach the dialog hierarchy.
86
- WorkflowLifecycleOwner .installOn(
87
- decorView,
88
- findParentLifecycle = { parentLifecycleOwner.lifecycle }
89
- )
90
- // Ensure that each dialog has its own ViewTreeSavedStateRegistryOwner,
91
- // so views in each dialog layer don't clash with other layers.
92
- stateRegistryAggregator.installChildRegistryOwnerOn(
93
- view = decorView,
94
- key = savedStateRegistryKey
95
- )
96
-
97
- decorView.doOnAttach {
98
- val lifecycle = parentLifecycleOwner.lifecycle
99
- val onDestroy = object : DefaultLifecycleObserver {
100
- override fun onDestroy (owner : LifecycleOwner ) {
101
- dismiss()
102
- }
76
+ dialog.show()
77
+ dialog.window?.decorView?.also { decorView ->
78
+ // Implementations of buildDialog may set their own WorkflowLifecycleOwner on the
79
+ // content view, so to avoid interfering with them we also set it here. When the views
80
+ // are attached, this will become the parent lifecycle of the one from buildDialog if
81
+ // any, and so we can use our lifecycle to destroy-on-detach the dialog hierarchy.
82
+ WorkflowLifecycleOwner .installOn(
83
+ decorView,
84
+ findParentLifecycle = { parentLifecycleOwner.lifecycle }
85
+ )
86
+ // Ensure that each dialog has its own ViewTreeSavedStateRegistryOwner,
87
+ // so views in each dialog layer don't clash with other layers.
88
+ stateRegistryAggregator.installChildRegistryOwnerOn(
89
+ view = decorView,
90
+ key = savedStateRegistryKey
91
+ )
92
+
93
+ decorView.doOnAttach {
94
+ val lifecycle = parentLifecycleOwner.lifecycle
95
+ val onDestroy = object : DefaultLifecycleObserver {
96
+ override fun onDestroy (owner : LifecycleOwner ) {
97
+ dismiss()
103
98
}
104
-
105
- // Android makes a lot of logcat noise if it has to close the window for us. :/
106
- // And no, we can't call ref.dismiss() directly from the doOnDetach lambda,
107
- // that's too late.
108
- // https://github.com/square/workflow/issues/51
109
- lifecycle.addObserver(onDestroy)
110
-
111
- // Note that we are careful not to make the doOnDetach call unless
112
- // we actually get attached. It is common for the dialog to be dismissed
113
- // before it is ever shown, so doOnDetach would never fire and we'd leak the
114
- // onDestroy lambda.
115
- decorView.doOnDetach { lifecycle.removeObserver(onDestroy) }
116
99
}
100
+
101
+ // Android makes a lot of logcat noise if it has to close the window for us. :/
102
+ // And no, we can't call ref.dismiss() directly from the doOnDetach lambda,
103
+ // that's too late.
104
+ // https://github.com/square/workflow/issues/51
105
+ lifecycle.addObserver(onDestroy)
106
+
107
+ // Note that we are careful not to make the doOnDetach call unless
108
+ // we actually get attached. It is common for the dialog to be dismissed
109
+ // before it is ever shown, so doOnDetach would never fire and we'd leak the
110
+ // onDestroy lambda.
111
+ decorView.doOnDetach { lifecycle.removeObserver(onDestroy) }
117
112
}
118
113
}
119
114
}
@@ -122,54 +117,23 @@ internal class DialogHolder<T : Overlay>(
122
117
// The dialog's views are about to be detached, and when that happens we want to transition
123
118
// the dialog view's lifecycle to a terminal state even though the parent is probably still
124
119
// alive.
125
- dialogOrNull?.let { dialog ->
126
- dialog.window?.decorView?.let (WorkflowLifecycleOwner ::get)?.destroyOnDetach()
127
- dialog.dismiss()
128
- }
129
- }
130
-
131
- fun canTakeRendering (rendering : Overlay ): Boolean {
132
- return compatible(this .rendering, rendering)
133
- }
134
-
135
- fun takeRendering (
136
- rendering : Overlay ,
137
- environment : ViewEnvironment
138
- ) {
139
- check(canTakeRendering(rendering)) {
140
- " Expected $this to be able to show rendering $rendering , but that did not match " +
141
- " previous rendering ${this .rendering} ."
142
- }
143
-
144
- @Suppress(" UNCHECKED_CAST" )
145
- this .rendering = rendering as T
146
- this .environment = environment
147
- allowEvents = ! modal || ! environment[CoveredByModal ]
148
-
149
- dialogOrNull?.let { dialog ->
150
- factory.updateDialog(dialog, this .rendering, this .environment)
120
+ with (holder.dialog) {
121
+ window?.decorView?.let (WorkflowLifecycleOwner ::get)?.destroyOnDetach()
122
+ dismiss()
151
123
}
152
124
}
153
125
154
126
internal fun save (): KeyAndBundle ? {
155
- val saved = dialogOrNull? .window?.saveHierarchyState() ? : return null
156
- return KeyAndBundle (Compatible .keyFor(rendering ), saved)
127
+ val saved = holder.dialog .window?.saveHierarchyState() ? : return null
128
+ return KeyAndBundle (Compatible .keyFor(holder.showing ), saved)
157
129
}
158
130
159
131
internal fun restore (keyAndBundle : KeyAndBundle ) {
160
- if (Compatible .keyFor(rendering ) == keyAndBundle.compatibilityKey) {
161
- requireDialog() .window?.restoreHierarchyState(keyAndBundle.bundle)
132
+ if (Compatible .keyFor(holder.showing ) == keyAndBundle.compatibilityKey) {
133
+ holder.dialog .window?.restoreHierarchyState(keyAndBundle.bundle)
162
134
}
163
135
}
164
136
165
- private fun requireDialog (): Dialog {
166
- return dialogOrNull ? : factory.buildDialog(rendering, environment, context)
167
- .also {
168
- dialogOrNull = it
169
- takeRendering(rendering, environment)
170
- }
171
- }
172
-
173
137
internal data class KeyAndBundle (
174
138
internal val compatibilityKey : String ,
175
139
internal val bundle : Bundle
0 commit comments