Skip to content

Commit 24db899

Browse files
committed
Hello ModalScreenOverlay
- Adds `ModalScreenOverlay` - Makes `ModalScreenOverlayDialogFactory` non-abstract, and uses it for `ModalScreenOverlay` by default - Both `ModalScreenOverlayDialogFactory` and `AlertOverlayDialogFactory` are now public open classes, with kdoc explaining that they can be used as base classes for custom renderings. - Improves / corrects kdoc on `OverlayDialogFactoryFinder` and `ScreenViewFactoryFinder`, showing how to use them to customize built in rendering types. - Makes the `LoadingSpinner` in the benchmarked Poetry app use the new hotness, and fixes its animation in the process
1 parent 8991cda commit 24db899

File tree

17 files changed

+193
-78
lines changed

17 files changed

+193
-78
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,31 @@
11
package com.squareup.benchmarks.performance.complex.poetry.views
22

3-
import android.app.Dialog
4-
import android.content.Context
5-
import android.view.ViewGroup.LayoutParams
3+
import android.view.Gravity.CENTER
4+
import android.view.ViewGroup
65
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
6+
import android.widget.FrameLayout
77
import android.widget.ProgressBar
8-
import com.squareup.workflow1.ui.ViewEnvironment
8+
import com.squareup.workflow1.ui.AndroidScreen
9+
import com.squareup.workflow1.ui.ScreenViewFactory
10+
import com.squareup.workflow1.ui.ScreenViewHolder
911
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
10-
import com.squareup.workflow1.ui.container.AndroidOverlay
11-
import com.squareup.workflow1.ui.container.OverlayDialogFactory
1212

1313
@OptIn(WorkflowUiExperimentalApi::class)
14-
object LoaderSpinner : AndroidOverlay<LoaderSpinner> {
15-
override val dialogFactory: OverlayDialogFactory<LoaderSpinner>
16-
get() = object : OverlayDialogFactory<LoaderSpinner> {
17-
override val type = LoaderSpinner::class
18-
19-
override fun buildDialog(
20-
initialRendering: LoaderSpinner,
21-
initialEnvironment: ViewEnvironment,
22-
context: Context
23-
): Dialog = Dialog(context).apply {
24-
setContentView(
25-
ProgressBar(context).apply {
26-
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
27-
isIndeterminate = true
28-
}
29-
)
14+
object LoaderSpinner : AndroidScreen<LoaderSpinner> {
15+
override val viewFactory =
16+
ScreenViewFactory.fromCode<LoaderSpinner> { _, initialEnvironment, context, _ ->
17+
val progressBar = ProgressBar(context).apply {
18+
layoutParams = FrameLayout.LayoutParams(
19+
ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
20+
).apply {
21+
gravity = CENTER
22+
}
23+
isIndeterminate = true
3024
}
3125

32-
override fun updateDialog(
33-
dialog: Dialog,
34-
rendering: LoaderSpinner,
35-
environment: ViewEnvironment
36-
) = Unit
26+
FrameLayout(context).let { view ->
27+
view.addView(progressBar)
28+
ScreenViewHolder(initialEnvironment, view) { _, _ -> }
29+
}
3730
}
3831
}

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

+11-4
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,27 @@ import com.squareup.sample.container.overviewdetail.OverviewDetailScreen
44
import com.squareup.sample.container.panel.ScrimScreen
55
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
66
import com.squareup.workflow1.ui.container.BodyAndModalsScreen
7+
import com.squareup.workflow1.ui.container.ModalScreenOverlay
78

89
@OptIn(WorkflowUiExperimentalApi::class)
9-
typealias MayBeLoadingScreen = BodyAndModalsScreen<ScrimScreen<OverviewDetailScreen>, LoaderSpinner>
10+
typealias MayBeLoadingScreen =
11+
BodyAndModalsScreen<ScrimScreen<OverviewDetailScreen>, ModalScreenOverlay<LoaderSpinner>>
1012

1113
@OptIn(WorkflowUiExperimentalApi::class)
1214
fun MayBeLoadingScreen(
1315
baseScreen: OverviewDetailScreen,
1416
loaders: List<LoaderSpinner> = emptyList()
1517
): MayBeLoadingScreen {
16-
return BodyAndModalsScreen(ScrimScreen(baseScreen, dimmed = loaders.isNotEmpty()), loaders)
18+
return BodyAndModalsScreen(
19+
ScrimScreen(baseScreen, dimmed = loaders.isNotEmpty()),
20+
loaders.map { ModalScreenOverlay(it) }
21+
)
1722
}
1823

1924
@OptIn(WorkflowUiExperimentalApi::class)
20-
val MayBeLoadingScreen.baseScreen: OverviewDetailScreen get() = body.content
25+
val MayBeLoadingScreen.baseScreen: OverviewDetailScreen
26+
get() = body.content
2127

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

benchmarks/performance-poetry/complex-poetry/src/main/res/values/ids.xml

-4
This file was deleted.

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

+7-15
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ package com.squareup.sample.container.panel
22

33
import android.app.Dialog
44
import android.graphics.Rect
5-
import android.graphics.drawable.ColorDrawable
6-
import android.util.TypedValue
75
import android.view.View
86
import com.squareup.sample.container.R
97
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
108
import com.squareup.workflow1.ui.container.ModalScreenOverlayDialogFactory
9+
import com.squareup.workflow1.ui.container.setModalContent
1110

1211
/**
1312
* Android support for [PanelOverlay].
@@ -16,20 +15,13 @@ import com.squareup.workflow1.ui.container.ModalScreenOverlayDialogFactory
1615
internal object PanelOverlayDialogFactory : ModalScreenOverlayDialogFactory<PanelOverlay<*>>(
1716
type = PanelOverlay::class
1817
) {
18+
/**
19+
* Forks the default implementation to apply [R.style.PanelDialog], for
20+
* enter and exit animation.
21+
*/
1922
override fun buildDialogWithContentView(contentView: View): Dialog {
20-
val context = contentView.context
21-
return Dialog(context, R.style.PanelDialog).also { dialog ->
22-
dialog.setContentView(contentView)
23-
24-
// Welcome to Android. Nothing workflow-related here, this is just how one
25-
// finds the window background color for the theme. I sure hope it's better in Compose.
26-
val maybeWindowColor = TypedValue()
27-
context.theme.resolveAttribute(android.R.attr.windowBackground, maybeWindowColor, true)
28-
if (
29-
maybeWindowColor.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT
30-
) {
31-
dialog.window!!.setBackgroundDrawable(ColorDrawable(maybeWindowColor.data))
32-
}
23+
return Dialog(contentView.context, R.style.PanelDialog).also {
24+
it.setModalContent(contentView)
3325
}
3426
}
3527

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

+10-4
Original file line numberDiff line numberDiff line change
@@ -422,11 +422,13 @@ public final class com/squareup/workflow1/ui/container/AlertDialogThemeResId : c
422422
public synthetic fun getDefault ()Ljava/lang/Object;
423423
}
424424

425-
public final class com/squareup/workflow1/ui/container/AlertOverlayDialogFactory : com/squareup/workflow1/ui/container/OverlayDialogFactory {
426-
public static final field INSTANCE Lcom/squareup/workflow1/ui/container/AlertOverlayDialogFactory;
425+
public class com/squareup/workflow1/ui/container/AlertOverlayDialogFactory : com/squareup/workflow1/ui/container/OverlayDialogFactory {
426+
public fun <init> ()V
427427
public fun buildDialog (Lcom/squareup/workflow1/ui/container/AlertOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Landroid/app/AlertDialog;
428428
public synthetic fun buildDialog (Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Landroid/app/Dialog;
429429
public fun getType ()Lkotlin/reflect/KClass;
430+
protected final fun toId (Lcom/squareup/workflow1/ui/container/AlertOverlay$Button;)I
431+
protected final fun updateButtonsOnShow (Landroid/app/AlertDialog;Lcom/squareup/workflow1/ui/container/AlertOverlay;)V
430432
public fun updateDialog (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/container/AlertOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
431433
public synthetic fun updateDialog (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
432434
}
@@ -614,17 +616,21 @@ public final class com/squareup/workflow1/ui/container/ModalAreaKt {
614616
public static final fun plus (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/container/ModalArea;)Lcom/squareup/workflow1/ui/ViewEnvironment;
615617
}
616618

617-
public abstract class com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory : com/squareup/workflow1/ui/container/OverlayDialogFactory {
619+
public class com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory : com/squareup/workflow1/ui/container/OverlayDialogFactory {
618620
public fun <init> (Lkotlin/reflect/KClass;)V
619621
public synthetic fun buildDialog (Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Landroid/app/Dialog;
620622
public final fun buildDialog (Lcom/squareup/workflow1/ui/container/ScreenOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Landroid/app/Dialog;
621-
public abstract fun buildDialogWithContentView (Landroid/view/View;)Landroid/app/Dialog;
623+
public fun buildDialogWithContentView (Landroid/view/View;)Landroid/app/Dialog;
622624
public fun getType ()Lkotlin/reflect/KClass;
623625
public fun updateBounds (Landroid/app/Dialog;Landroid/graphics/Rect;)V
624626
public synthetic fun updateDialog (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
625627
public final fun updateDialog (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/container/ScreenOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
626628
}
627629

630+
public final class com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactoryKt {
631+
public static final fun setModalContent (Landroid/app/Dialog;Landroid/view/View;)V
632+
}
633+
628634
public abstract interface class com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed {
629635
public static final field Companion Lcom/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed$Companion;
630636
public abstract fun onBackPressed (Landroid/view/View;)Z

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

+11-8
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import com.squareup.workflow1.ui.container.EnvironmentScreenViewFactory
1111
* [ViewEnvironment] service object used by [Screen.toViewFactory] to find the right
1212
* [ScreenViewFactory] to build and manage a [View][android.view.View] to display
1313
* [Screen]s of the type of the receiver. The default implementation makes [AndroidScreen]
14-
* work and provides default bindings for [NamedScreen], [EnvironmentScreen], [BackStackScreen],
14+
* work, and provides default bindings for [NamedScreen], [EnvironmentScreen], [BackStackScreen],
1515
* etc.
1616
*
1717
* Here is how this hook could be used to provide a custom view to handle [BackStackScreen]:
@@ -28,24 +28,27 @@ import com.squareup.workflow1.ui.container.EnvironmentScreenViewFactory
2828
* )
2929
*
3030
* object MyFinder : ScreenViewFactoryFinder {
31-
* @Suppress("UNCHECKED_CAST")
32-
* if (rendering is BackStackScreen<*>)
33-
* return MyViewFactory as ScreenViewFactory<ScreenT>
34-
* return super.getViewFactoryForRendering(environment, rendering)
31+
* override fun <ScreenT : Screen> getViewFactoryForRendering(
32+
* environment: ViewEnvironment,
33+
* rendering: ScreenT
34+
* ): ScreenViewFactory<ScreenT> {
35+
* @Suppress("UNCHECKED_CAST")
36+
* if (rendering is BackStackScreen<*>) return MyViewFactory as ScreenViewFactory<ScreenT>
37+
* return super.getViewFactoryForRendering(environment, rendering)
38+
* }
3539
* }
3640
*
3741
* class MyViewModel(savedState: SavedStateHandle) : ViewModel() {
3842
* val renderings: StateFlow<MyRootRendering> by lazy {
39-
* val customized = ViewEnvironment.EMPTY + (ScreenViewFactoryFinder to MyFinder)
43+
* val env = ViewEnvironment.EMPTY + (ScreenViewFactoryFinder to MyFinder)
4044
* renderWorkflowIn(
41-
* workflow = MyRootWorkflow.withEnvironment(customized),
45+
* workflow = MyRootWorkflow.mapRenderings { it.withEnvironment(env) },
4246
* scope = viewModelScope,
4347
* savedStateHandle = savedState
4448
* )
4549
* }
4650
* }
4751
*/
48-
4952
@WorkflowUiExperimentalApi
5053
public interface ScreenViewFactoryFinder {
5154
public fun <ScreenT : Screen> getViewFactoryForRendering(

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

+13-6
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,19 @@ import com.squareup.workflow1.ui.container.AlertOverlay.Button.NEUTRAL
1414
import com.squareup.workflow1.ui.container.AlertOverlay.Button.POSITIVE
1515
import com.squareup.workflow1.ui.container.AlertOverlay.Event.ButtonClicked
1616
import com.squareup.workflow1.ui.container.AlertOverlay.Event.Canceled
17+
import kotlin.reflect.KClass
1718

19+
/**
20+
* Default [OverlayDialogFactory] for [AlertOverlay].
21+
*
22+
* This class is non-final for ease of customization of [AlertOverlay] handling,
23+
* see [OverlayDialogFactoryFinder] for details.
24+
*/
1825
@WorkflowUiExperimentalApi
19-
internal object AlertOverlayDialogFactory : OverlayDialogFactory<AlertOverlay> {
20-
override val type = AlertOverlay::class
26+
public open class AlertOverlayDialogFactory : OverlayDialogFactory<AlertOverlay> {
27+
override val type: KClass<AlertOverlay> = AlertOverlay::class
2128

22-
override fun buildDialog(
29+
open override fun buildDialog(
2330
initialRendering: AlertOverlay,
2431
initialEnvironment: ViewEnvironment,
2532
context: Context
@@ -48,7 +55,7 @@ internal object AlertOverlayDialogFactory : OverlayDialogFactory<AlertOverlay> {
4855
}
4956
}
5057

51-
override fun updateDialog(
58+
open override fun updateDialog(
5259
dialog: Dialog,
5360
rendering: AlertOverlay,
5461
environment: ViewEnvironment
@@ -71,13 +78,13 @@ internal object AlertOverlayDialogFactory : OverlayDialogFactory<AlertOverlay> {
7178
}
7279
}
7380

74-
private fun Button.toId(): Int = when (this) {
81+
protected fun Button.toId(): Int = when (this) {
7582
POSITIVE -> DialogInterface.BUTTON_POSITIVE
7683
NEGATIVE -> DialogInterface.BUTTON_NEGATIVE
7784
NEUTRAL -> DialogInterface.BUTTON_NEUTRAL
7885
}
7986

80-
private fun AlertDialog.updateButtonsOnShow(rendering: AlertOverlay) {
87+
protected fun AlertDialog.updateButtonsOnShow(rendering: AlertOverlay) {
8188
setOnShowListener(null)
8289

8390
for (button in Button.values()) getButton(button.toId()).visibility = GONE

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

+48-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package com.squareup.workflow1.ui.container
33
import android.app.Dialog
44
import android.content.Context
55
import android.graphics.Rect
6+
import android.graphics.drawable.ColorDrawable
7+
import android.util.TypedValue
68
import android.view.KeyEvent
79
import android.view.KeyEvent.ACTION_UP
810
import android.view.KeyEvent.KEYCODE_BACK
@@ -22,30 +24,42 @@ import com.squareup.workflow1.ui.toViewFactory
2224
import kotlin.reflect.KClass
2325

2426
/**
25-
* Convenient base class for building [ScreenOverlay] UIs that are compatible
26-
* with [View.backPressedHandler], and which honor the [ModalArea] constraint
27-
* placed in the [ViewEnvironment] by the standard [BodyAndModalsScreen] container.
27+
* Default [OverlayDialogFactory] for [ModalScreenOverlay].
28+
*
29+
* This class is non-final for ease of customization of [ModalScreenOverlay] handling,
30+
* see [OverlayDialogFactoryFinder] for details. It is also convenient to use as a
31+
* base class for custom [ScreenOverlay] rendering types.
32+
*
33+
* Dialogs built by this class are compatible with [View.backPressedHandler], and
34+
* honor the [ModalArea] constraint placed in the [ViewEnvironment] by the
35+
* standard [BodyAndModalsScreen] container.
2836
*
2937
* Ironically, [Dialog] instances are created with [FLAG_NOT_TOUCH_MODAL], to ensure
3038
* that events outside of the bounds reported by [updateBounds] reach views in
3139
* lower windows. See that method for details.
3240
*/
3341
@WorkflowUiExperimentalApi
34-
public abstract class ModalScreenOverlayDialogFactory<O : ScreenOverlay<*>>(
42+
public open class ModalScreenOverlayDialogFactory<O : ScreenOverlay<*>>(
3543
override val type: KClass<in O>
3644
) : OverlayDialogFactory<O> {
3745

3846
/**
3947
* Called from [buildDialog]. Builds (but does not show) the [Dialog] to
4048
* display a [contentView] built for a [ScreenOverlay.content].
49+
*
50+
* Custom implementations are not required to call `super`.
51+
*
52+
* Default implementation calls [Dialog.setModalContent].
4153
*/
42-
public abstract fun buildDialogWithContentView(contentView: View): Dialog
54+
public open fun buildDialogWithContentView(contentView: View): Dialog {
55+
return Dialog(contentView.context).also { it.setModalContent(contentView) }
56+
}
4357

4458
/**
4559
* If the [ScreenOverlay] displayed by a [dialog] created by this
4660
* factory is contained in a [BodyAndModalsScreen], this method will
47-
* be called to report the bounds of the managing view. It is expected
48-
* that such a dialog will be restricted to those bounds.
61+
* be called to report the bounds of the managing view, as reported by [ModalArea].
62+
* It is expected that such a dialog will be restricted to those bounds.
4963
*
5064
* Honoring this contract makes it easy to define areas of the display
5165
* that are outside of the "shadow" of a modal dialog. Imagine an app
@@ -125,3 +139,30 @@ public abstract class ModalScreenOverlayDialogFactory<O : ScreenOverlay<*>>(
125139
)
126140
}
127141
}
142+
143+
/**
144+
* The default implementation of [ModalScreenOverlayDialogFactory.buildDialogWithContentView].
145+
*
146+
* Sets the [background][Window.setBackgroundDrawable] of the receiver's [Window] based
147+
* on its theme, if any, or else `null`. (Setting the background to `null` ensures the window
148+
* can go full bleed.)
149+
*/
150+
@OptIn(WorkflowUiExperimentalApi::class)
151+
public fun Dialog.setModalContent(contentView: View) {
152+
setCancelable(false)
153+
setContentView(contentView)
154+
155+
// Welcome to Android. Nothing workflow-related here, this is just how one
156+
// finds the window background color for the theme. I sure hope it's better in Compose.
157+
val maybeWindowColor = TypedValue()
158+
context.theme.resolveAttribute(android.R.attr.windowBackground, maybeWindowColor, true)
159+
160+
val background =
161+
if (maybeWindowColor.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT) {
162+
ColorDrawable(maybeWindowColor.data)
163+
} else {
164+
// If we don't at least set it to null, the window cannot go full bleed.
165+
null
166+
}
167+
window!!.setBackgroundDrawable(background)
168+
}

0 commit comments

Comments
 (0)