Skip to content

Commit 239ec29

Browse files
committed
OverlayDialogHolder.onUpdateBounds replaces ScreenOverlayDialogFactory.updateBounds
I finally tried to use `ScreenOverlayDialogFactory.updateBounds` and was reminded once again why it's bad to create interfaces that build something and then take it back later to be updated. If you want the renderings to be involved in setting the bounds policy, there was no way to smuggle that information along with the Dialog returned from `buildDialogWithContent`, so that `updateBounds` could honor it. Also, it was always pretty ugly that the bounds hook was special to the `ScreenOverlay` world. The fix is make bounds maintenance part of the job of the `OverlayDialogHolder` interface. Square reviewers will notice that we're now an even more faithful rip off of Mosaic's DialogRunner -- hi @helios175.
1 parent d19ae7a commit 239ec29

File tree

10 files changed

+110
-88
lines changed

10 files changed

+110
-88
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ android.useAndroidX=true
88
systemProp.org.gradle.internal.publish.checksums.insecure=true
99

1010
GROUP=com.squareup.workflow1
11-
VERSION_NAME=1.8.0-beta06-SNAPSHOT
11+
VERSION_NAME=1.8.0-rjrjrbeta06-SNAPSHOT
1212

1313
POM_DESCRIPTION=Square Workflow
1414

samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelOverlayDialogFactory.kt

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@ import android.graphics.Rect
55
import com.squareup.sample.container.R
66
import com.squareup.workflow1.ui.Screen
77
import com.squareup.workflow1.ui.ScreenViewHolder
8+
import com.squareup.workflow1.ui.ViewEnvironment
89
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
10+
import com.squareup.workflow1.ui.container.OverlayDialogHolder
911
import com.squareup.workflow1.ui.container.ScreenOverlayDialogFactory
12+
import com.squareup.workflow1.ui.container.setBounds
1013
import com.squareup.workflow1.ui.container.setContent
14+
import com.squareup.workflow1.ui.show
1115

1216
/**
1317
* Android support for [PanelOverlay].
@@ -18,41 +22,48 @@ internal object PanelOverlayDialogFactory :
1822
type = PanelOverlay::class
1923
) {
2024
/**
21-
* Forks the default implementation to apply [R.style.PanelDialog], for
22-
* enter and exit animation.
25+
* Forks the default implementation to apply [R.style.PanelDialog] for
26+
* enter and exit animation, and to customize [bounds][OverlayDialogHolder.onUpdateBounds].
2327
*/
24-
override fun buildDialogWithContent(content: ScreenViewHolder<Screen>): Dialog {
25-
return Dialog(content.view.context, R.style.PanelDialog).also {
26-
it.setContent(content)
27-
}
28-
}
28+
override fun buildDialogWithContent(
29+
initialRendering: PanelOverlay<Screen>,
30+
initialEnvironment: ViewEnvironment,
31+
content: ScreenViewHolder<Screen>
32+
): OverlayDialogHolder<PanelOverlay<Screen>> {
33+
val dialog = Dialog(content.view.context, R.style.PanelDialog)
34+
dialog.setContent(content)
2935

30-
override fun updateBounds(
31-
dialog: Dialog,
32-
bounds: Rect
33-
) {
34-
val refinedBounds: Rect = if (!dialog.context.isTablet) {
35-
// On a phone, fill the bounds entirely.
36-
bounds
37-
} else {
38-
if (bounds.height() > bounds.width()) {
39-
val margin = bounds.height() - bounds.width()
40-
val topDelta = margin / 2
41-
val bottomDelta = margin - topDelta
42-
Rect(bounds).apply {
43-
top = bounds.top + topDelta
44-
bottom = bounds.bottom - bottomDelta
45-
}
46-
} else {
47-
val margin = bounds.width() - bounds.height()
48-
val leftDelta = margin / 2
49-
val rightDelta = margin - leftDelta
50-
Rect(bounds).apply {
51-
left = bounds.left + leftDelta
52-
right = bounds.right - rightDelta
36+
return OverlayDialogHolder(
37+
initialEnvironment = initialEnvironment,
38+
dialog = dialog,
39+
onUpdateBounds = { bounds ->
40+
val refinedBounds: Rect = if (!dialog.context.isTablet) {
41+
// On a phone, fill the bounds entirely.
42+
bounds
43+
} else {
44+
if (bounds.height() > bounds.width()) {
45+
val margin = bounds.height() - bounds.width()
46+
val topDelta = margin / 2
47+
val bottomDelta = margin - topDelta
48+
Rect(bounds).apply {
49+
top = bounds.top + topDelta
50+
bottom = bounds.bottom - bottomDelta
51+
}
52+
} else {
53+
val margin = bounds.width() - bounds.height()
54+
val leftDelta = margin / 2
55+
val rightDelta = margin - leftDelta
56+
Rect(bounds).apply {
57+
left = bounds.left + leftDelta
58+
right = bounds.right - rightDelta
59+
}
60+
}
5361
}
62+
63+
dialog.setBounds(refinedBounds)
5464
}
65+
) { overlayRendering, environment ->
66+
content.show(overlayRendering.content, environment)
5567
}
56-
super.updateBounds(dialog, refinedBounds)
5768
}
5869
}

workflow-ui/core-android/api/core-android.api

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -436,8 +436,8 @@ public class com/squareup/workflow1/ui/container/AlertOverlayDialogFactory : com
436436
}
437437

438438
public final class com/squareup/workflow1/ui/container/AndroidDialogBoundsKt {
439-
public static final fun maintainBounds (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function2;)V
440-
public static final fun maintainBounds (Landroid/app/Dialog;Lkotlinx/coroutines/flow/StateFlow;Lkotlin/jvm/functions/Function2;)V
439+
public static final fun maintainBounds (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function1;)V
440+
public static final fun maintainBounds (Landroid/app/Dialog;Lkotlinx/coroutines/flow/StateFlow;Lkotlin/jvm/functions/Function1;)V
441441
public static final fun setBounds (Landroid/app/Dialog;Landroid/graphics/Rect;)V
442442
}
443443

@@ -672,6 +672,7 @@ public abstract interface class com/squareup/workflow1/ui/container/OverlayDialo
672672
public static final field Companion Lcom/squareup/workflow1/ui/container/OverlayDialogHolder$Companion;
673673
public abstract fun getDialog ()Landroid/app/Dialog;
674674
public abstract fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment;
675+
public abstract fun getOnUpdateBounds ()Lkotlin/jvm/functions/Function1;
675676
public abstract fun getRunner ()Lkotlin/jvm/functions/Function2;
676677
}
677678

@@ -689,16 +690,18 @@ public final class com/squareup/workflow1/ui/container/OverlayDialogHolder$Compa
689690
}
690691

691692
public final class com/squareup/workflow1/ui/container/OverlayDialogHolderKt {
692-
public static final fun OverlayDialogHolder (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/app/Dialog;Lkotlin/jvm/functions/Function2;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;
693+
public static final fun OverlayDialogHolder (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/app/Dialog;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;
694+
public static synthetic fun OverlayDialogHolder$default (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/app/Dialog;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;
693695
public static final fun canShow (Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;Lcom/squareup/workflow1/ui/container/Overlay;)Z
694696
public static final fun getShowing (Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;)Lcom/squareup/workflow1/ui/container/Overlay;
695697
public static final fun show (Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
696698
}
697699

698700
public final class com/squareup/workflow1/ui/container/RealOverlayDialogHolder : com/squareup/workflow1/ui/container/OverlayDialogHolder {
699-
public fun <init> (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/app/Dialog;Lkotlin/jvm/functions/Function2;)V
701+
public fun <init> (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/app/Dialog;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
700702
public fun getDialog ()Landroid/app/Dialog;
701703
public fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment;
704+
public fun getOnUpdateBounds ()Lkotlin/jvm/functions/Function1;
702705
public fun getRunner ()Lkotlin/jvm/functions/Function2;
703706
}
704707

@@ -707,9 +710,8 @@ public class com/squareup/workflow1/ui/container/ScreenOverlayDialogFactory : co
707710
public fun buildContent (Lcom/squareup/workflow1/ui/ScreenViewFactory;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Lcom/squareup/workflow1/ui/ScreenViewHolder;
708711
public synthetic fun buildDialog (Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;
709712
public final fun buildDialog (Lcom/squareup/workflow1/ui/container/ScreenOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;
710-
public fun buildDialogWithContent (Lcom/squareup/workflow1/ui/ScreenViewHolder;)Landroid/app/Dialog;
713+
public fun buildDialogWithContent (Lcom/squareup/workflow1/ui/container/ScreenOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/ScreenViewHolder;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;
711714
public fun getType ()Lkotlin/reflect/KClass;
712-
public fun updateBounds (Landroid/app/Dialog;Landroid/graphics/Rect;)V
713715
}
714716

715717
public final class com/squareup/workflow1/ui/container/ScreenOverlayDialogFactoryKt {

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ public open class AlertOverlayDialogFactory : OverlayDialogFactory<AlertOverlay>
5252
alertDialog.setButton(button.toId(), " ") { _, _ -> }
5353
}
5454

55-
OverlayDialogHolder(initialEnvironment, alertDialog) { rendering, _ ->
55+
OverlayDialogHolder(
56+
initialEnvironment = initialEnvironment,
57+
dialog = alertDialog,
58+
onUpdateBounds = null
59+
) { rendering, _ ->
5660
with(alertDialog) {
5761
if (rendering.cancelable) {
5862
setOnCancelListener { rendering.onEvent(Canceled) }

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.onEach
1919
* [bounds] is expected to be in global display coordinates,
2020
* e.g. as returned from [View.getGlobalVisibleRect].
2121
*
22-
* @see ScreenOverlayDialogFactory.updateBounds
22+
* @see OverlayDialogHolder.onUpdateBounds
2323
*/
2424
@WorkflowUiExperimentalApi
2525
public fun Dialog.setBounds(bounds: Rect) {
@@ -37,23 +37,23 @@ public fun Dialog.setBounds(bounds: Rect) {
3737
@WorkflowUiExperimentalApi
3838
internal fun <D : Dialog> D.maintainBounds(
3939
environment: ViewEnvironment,
40-
onBoundsChange: (D, Rect) -> Unit
40+
onBoundsChange: (Rect) -> Unit
4141
) {
4242
maintainBounds(environment[OverlayArea].bounds, onBoundsChange)
4343
}
4444

4545
@WorkflowUiExperimentalApi
4646
internal fun <D : Dialog> D.maintainBounds(
4747
bounds: StateFlow<Rect>,
48-
onBoundsChange: (D, Rect) -> Unit
48+
onBoundsChange: (Rect) -> Unit
4949
) {
5050
val window = requireNotNull(window) { "Dialog must be attached to a window." }
5151
window.callback = object : Window.Callback by window.callback {
5252
var scope: CoroutineScope? = null
5353

5454
override fun onAttachedToWindow() {
5555
scope = CoroutineScope(Dispatchers.Main.immediate).also {
56-
bounds.onEach { b -> onBoundsChange(this@maintainBounds, b) }
56+
bounds.onEach { b -> onBoundsChange(b) }
5757
.launchIn(it)
5858
}
5959
}
@@ -65,5 +65,5 @@ internal fun <D : Dialog> D.maintainBounds(
6565
}
6666

6767
// If already attached, set the bounds eagerly.
68-
if (window.peekDecorView()?.isAttachedToWindow == true) onBoundsChange(this, bounds.value)
68+
if (window.peekDecorView()?.isAttachedToWindow == true) onBoundsChange(bounds.value)
6969
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ internal class DialogSession(
2626
index: Int,
2727
holder: OverlayDialogHolder<Overlay>
2828
) {
29-
// Note similar code in LayeredDialogs
29+
// Note similar code in LayeredDialogSessions
3030
private var allowEvents = true
3131
set(value) {
3232
val was = field

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ public class LayeredDialogSessions private constructor(
163163
overlay.toDialogFactory(dialogEnv)
164164
.buildDialog(overlay, dialogEnv, context)
165165
.let { holder ->
166+
holder.onUpdateBounds?.let { updateBounds ->
167+
holder.dialog.maintainBounds(holder.environment) { b -> updateBounds(b) }
168+
}
169+
166170
DialogSession(i, holder).also { newSession ->
167171
// Prime the pump, make the first call to OverlayDialog.show to update
168172
// the new dialog to reflect the first rendering.

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

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

33
import android.app.Dialog
4+
import android.graphics.Rect
45
import com.squareup.workflow1.ui.Screen
56
import com.squareup.workflow1.ui.ViewEnvironment
67
import com.squareup.workflow1.ui.ViewEnvironmentKey
@@ -29,6 +30,21 @@ public interface OverlayDialogHolder<in OverlayT : Overlay> {
2930
*/
3031
public val runner: (rendering: OverlayT, environment: ViewEnvironment) -> Unit
3132

33+
34+
/**
35+
* Optional function called to report the bounds of the managing container view,
36+
* as reported by [OverlayArea]. Well behaved [Overlay] dialogs are expected to
37+
* be restricted to those bounds, to the extent practical -- you probably want to ignore
38+
* this for AlertDialog, e.g.
39+
*
40+
* Honoring this contract makes it easy to define areas of the display
41+
* that are outside of the "shadow" of a modal dialog. Imagine an app
42+
* with a status bar that should not be covered by modals.
43+
*
44+
* Default implementation provided by the factory function below calls [Dialog.setBounds].
45+
*/
46+
public val onUpdateBounds: ((Rect) -> Unit)?
47+
3248
public companion object {
3349
/**
3450
* Default value returned for the [InOverlay] [ViewEnvironmentKey], and therefore the
@@ -87,7 +103,8 @@ public val OverlayDialogHolder<*>.showing: Overlay
87103
public fun <OverlayT : Overlay> OverlayDialogHolder(
88104
initialEnvironment: ViewEnvironment,
89105
dialog: Dialog,
106+
onUpdateBounds: ((Rect) -> Unit)? = { dialog.setBounds(it) },
90107
runner: (rendering: OverlayT, environment: ViewEnvironment) -> Unit
91108
): OverlayDialogHolder<OverlayT> {
92-
return RealOverlayDialogHolder(initialEnvironment, dialog, runner)
109+
return RealOverlayDialogHolder(initialEnvironment, dialog, onUpdateBounds, runner)
93110
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package com.squareup.workflow1.ui.container
22

33
import android.app.Dialog
4+
import android.graphics.Rect
45
import com.squareup.workflow1.ui.ViewEnvironment
56
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
67

78
@WorkflowUiExperimentalApi
89
internal class RealOverlayDialogHolder<OverlayT : Overlay>(
910
initialEnvironment: ViewEnvironment,
1011
override val dialog: Dialog,
12+
override val onUpdateBounds: ((Rect) -> Unit)?,
1113
runnerFunction: (rendering: OverlayT, environment: ViewEnvironment) -> Unit
1214
) : OverlayDialogHolder<OverlayT> {
1315

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

Lines changed: 24 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.squareup.workflow1.ui.container
22

33
import android.app.Dialog
44
import android.content.Context
5-
import android.graphics.Rect
65
import android.graphics.drawable.ColorDrawable
76
import android.util.TypedValue
87
import android.view.KeyEvent
@@ -61,11 +60,6 @@ import kotlin.reflect.KClass
6160
* display those [Overlay] renderings look for [OverlayArea] value and restrict
6261
* themselves to the reported bounds.
6362
*
64-
* Dialogs created via [ScreenOverlayDialogFactory] implementations honor [OverlayArea]
65-
* automatically. [updateBounds] is called as the [OverlayArea] changes, and the
66-
* default implementation of that method sets created dialog windows to fill the given area --
67-
* not necessarily the entire display.
68-
*
6963
* Another [ViewEnvironment] value is maintained to support modality: [CoveredByModal].
7064
* When this value is true, it indicates that a dialog window driven by a [ModalOverlay]
7165
* is in play over the view, or is about to be, and so touch and click events should be
@@ -109,42 +103,33 @@ public open class ScreenOverlayDialogFactory<S : Screen, O : ScreenOverlay<S>>(
109103
initialEnvironment: ViewEnvironment,
110104
context: Context
111105
): ScreenViewHolder<S> {
112-
return viewFactory
113-
.startShowing(initialContent, initialEnvironment, context)
106+
return viewFactory.startShowing(initialContent, initialEnvironment, context)
114107
}
115108

116109
/**
117110
* Build the [Dialog] for the [content] that was just created by [buildContent].
118-
* Open to allow customization, typically theming.
119-
*
120-
* The default implementation delegates all work to the provided [Dialog.setContent]
121-
* extension function. Subclasses need not call `super`.
111+
* Open to allow customization, typically theming, subclasses need not call `super`.
112+
* - Note that the default implementation calls the provided [Dialog.setContent]
113+
* extension for typical setup.
114+
* - Be sure to call [ScreenViewHolder.show] from [OverlayDialogHolder.runner].
122115
*/
123-
public open fun buildDialogWithContent(content: ScreenViewHolder<S>): Dialog {
124-
return Dialog(content.view.context).also { it.setContent(content) }
116+
public open fun buildDialogWithContent(
117+
initialRendering: O,
118+
initialEnvironment: ViewEnvironment,
119+
content: ScreenViewHolder<S>
120+
): OverlayDialogHolder<O> {
121+
return OverlayDialogHolder(
122+
initialEnvironment, Dialog(content.view.context).also { it.setContent(content) }
123+
) { overlayRendering, environment ->
124+
content.show(overlayRendering.content, environment)
125+
}
125126
}
126127

127128
/**
128-
* This method will be called to report the bounds of the managing container view,
129-
* as reported by [OverlayArea]. Well behaved [ScreenOverlay] dialogs are expected to
130-
* be restricted to those bounds.
131-
*
132-
* Honoring this contract makes it easy to define areas of the display
133-
* that are outside of the "shadow" of a modal dialog. Imagine an app
134-
* with a status bar that should not be covered by modals.
135-
*
136-
* The default implementation calls straight through to the [Dialog.setBounds] function
137-
* provided below. Custom implementations are not required to call `super`.
138-
*
139-
* @see Dialog.setBounds
129+
* Locked down implementation enforces [ModalOverlay] and supports
130+
* [ModalScreenOverlayBackButtonHelper]. Delegates to [buildContent] to create the content view
131+
* and [buildDialogWithContent] to create the [Dialog].
140132
*/
141-
public open fun updateBounds(
142-
dialog: Dialog,
143-
bounds: Rect
144-
) {
145-
dialog.setBounds(bounds)
146-
}
147-
148133
final override fun buildDialog(
149134
initialRendering: O,
150135
initialEnvironment: ViewEnvironment,
@@ -159,8 +144,12 @@ public open class ScreenOverlayDialogFactory<S : Screen, O : ScreenOverlay<S>>(
159144
val contentViewHolder =
160145
buildContent(contentViewFactory, initialRendering.content, initialEnvironment, context)
161146

162-
return buildDialogWithContent(contentViewHolder).let { dialog ->
163-
val window = requireNotNull(dialog.window) { "Dialog must be attached to a window." }
147+
return buildDialogWithContent(
148+
initialRendering,
149+
initialEnvironment,
150+
contentViewHolder
151+
).also { holder ->
152+
val window = requireNotNull(holder.dialog.window) { "Dialog must be attached to a window." }
164153

165154
if (modal) {
166155
val realWindowCallback = window.callback
@@ -183,13 +172,6 @@ public open class ScreenOverlayDialogFactory<S : Screen, O : ScreenOverlay<S>>(
183172
// notion of its modality. Even a modal dialog should only block events within
184173
// the appropriate bounds, but Android makes them block everywhere.
185174
window.setFlags(FLAG_NOT_TOUCH_MODAL, FLAG_NOT_TOUCH_MODAL)
186-
187-
// Keep an eye on the bounds StateFlow(Rect) put in place by [LayeredDialogSessions].
188-
dialog.maintainBounds(contentViewHolder.environment) { d, b -> updateBounds(d, Rect(b)) }
189-
190-
OverlayDialogHolder(initialEnvironment, dialog) { overlayRendering, environment ->
191-
contentViewHolder.show(overlayRendering.content, environment)
192-
}
193175
}
194176
}
195177

0 commit comments

Comments
 (0)