From 8d1228d23fac06959b7031fe449507ad7ad381fa Mon Sep 17 00:00:00 2001 From: Ray Ryan Date: Mon, 12 Jun 2023 13:59:51 -0700 Subject: [PATCH] `fun ComponentDialog.setContent()` replaces `ScreenOverlayDialogFactory` With AndroidX 1.6.0, `ComponentDialog` serves as its own `LifecycleOwner` and `OnBackPressedDispatcherOwner`. To take advantage of this we introduce a new `ComponentDialog.setContent` extension function, deprecate `ScreenOverlayDialogFactory`, and deprecate our hooks for customizing `Dialog` back press handling. Related kdoc is improved, and a factory function is bound to `OverlayDialogFactory.Companion`. --- .../panel/PanelOverlayDialogFactory.kt | 36 +++----- .../compose/ComposeViewTreeIntegrationTest.kt | 38 ++++---- workflow-ui/core-android/api/core-android.api | 8 ++ .../ui/container/DialogIntegrationTest.kt | 24 ++---- .../ui/container/AlertOverlayDialogFactory.kt | 1 + .../workflow1/ui/container/AndroidOverlay.kt | 14 ++- .../ui/container/BodyAndOverlaysContainer.kt | 3 - .../ui/container/ComponentDialogSetContent.kt | 65 ++++++++++++++ .../workflow1/ui/container/DialogSession.kt | 2 + .../ui/container/FullScreenModalFactory.kt | 25 ++++++ .../ui/container/OverlayDialogFactory.kt | 86 +++++++++++++++++-- .../container/OverlayDialogFactoryFinder.kt | 6 +- .../ui/container/OverlayDialogHolder.kt | 3 + .../ui/container/RealOverlayDialogHolder.kt | 3 + .../container/ScreenOverlayDialogFactory.kt | 85 +----------------- 15 files changed, 244 insertions(+), 155 deletions(-) create mode 100644 workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ComponentDialogSetContent.kt create mode 100644 workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/FullScreenModalFactory.kt diff --git a/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelOverlayDialogFactory.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelOverlayDialogFactory.kt index 1627d792c3..0ae4de3555 100644 --- a/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelOverlayDialogFactory.kt +++ b/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelOverlayDialogFactory.kt @@ -1,42 +1,36 @@ package com.squareup.sample.container.panel -import android.app.Dialog +import android.content.Context import android.graphics.Rect +import androidx.appcompat.app.AppCompatDialog import com.squareup.sample.container.R import com.squareup.workflow1.ui.Screen -import com.squareup.workflow1.ui.ScreenViewHolder import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.container.OverlayDialogFactory import com.squareup.workflow1.ui.container.OverlayDialogHolder -import com.squareup.workflow1.ui.container.ScreenOverlayDialogFactory import com.squareup.workflow1.ui.container.setBounds import com.squareup.workflow1.ui.container.setContent -import com.squareup.workflow1.ui.show +import kotlin.reflect.KClass /** * Android support for [PanelOverlay]. */ @OptIn(WorkflowUiExperimentalApi::class) -internal object PanelOverlayDialogFactory : - ScreenOverlayDialogFactory>( - type = PanelOverlay::class - ) { - /** - * Forks the default implementation to apply [R.style.PanelDialog] for - * enter and exit animation, and to customize [bounds][OverlayDialogHolder.onUpdateBounds]. - */ - override fun buildDialogWithContent( +internal object PanelOverlayDialogFactory : OverlayDialogFactory> { + override val type: KClass> = PanelOverlay::class + + override fun buildDialog( initialRendering: PanelOverlay, initialEnvironment: ViewEnvironment, - content: ScreenViewHolder + context: Context ): OverlayDialogHolder> { - val dialog = Dialog(content.view.context, R.style.PanelDialog) - dialog.setContent(content) + val dialog = AppCompatDialog(context, R.style.PanelDialog) + + val realHolder = dialog.setContent(initialRendering, initialEnvironment) - return OverlayDialogHolder( - initialEnvironment = initialEnvironment, - dialog = dialog, - onUpdateBounds = { bounds -> + return object : OverlayDialogHolder> by realHolder { + override val onUpdateBounds: ((Rect) -> Unit) = { bounds -> val refinedBounds: Rect = if (!dialog.context.isTablet) { // On a phone, fill the bounds entirely. bounds @@ -62,8 +56,6 @@ internal object PanelOverlayDialogFactory : dialog.setBounds(refinedBounds) } - ) { overlayRendering, environment -> - content.show(overlayRendering.content, environment) } } } diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt index 333ab33559..81245b9e50 100644 --- a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt +++ b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt @@ -3,6 +3,7 @@ package com.squareup.workflow1.ui.compose import android.content.Context import android.view.View import android.view.ViewGroup +import androidx.activity.ComponentDialog import androidx.compose.foundation.clickable import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable @@ -36,8 +37,9 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.container.AndroidOverlay import com.squareup.workflow1.ui.container.BackStackScreen import com.squareup.workflow1.ui.container.BodyAndOverlaysScreen +import com.squareup.workflow1.ui.container.OverlayDialogFactory import com.squareup.workflow1.ui.container.ScreenOverlay -import com.squareup.workflow1.ui.container.ScreenOverlayDialogFactory +import com.squareup.workflow1.ui.container.setContent import com.squareup.workflow1.ui.internal.test.IdleAfterTestRule import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule import com.squareup.workflow1.ui.internal.test.WorkflowUiTestActivity @@ -374,7 +376,7 @@ internal class ComposeViewTreeIntegrationTest { it.setRendering( BodyAndOverlaysScreen( EmptyRendering, - TestModal(BackStackScreen(EmptyRendering, firstScreen)) + TestOverlay(BackStackScreen(EmptyRendering, firstScreen)) ) ) } @@ -426,9 +428,9 @@ internal class ComposeViewTreeIntegrationTest { it.setRendering( BodyAndOverlaysScreen( EmptyRendering, - TestModal(firstScreen), - TestModal(secondScreen), - TestModal(thirdScreen) + TestOverlay(firstScreen), + TestOverlay(secondScreen), + TestOverlay(thirdScreen) ) ) } @@ -488,11 +490,11 @@ internal class ComposeViewTreeIntegrationTest { it.setRendering( BodyAndOverlaysScreen( EmptyRendering, - TestModal(BackStackScreen(EmptyRendering, layer0Screen0)), + TestOverlay(BackStackScreen(EmptyRendering, layer0Screen0)), // A SavedStateRegistry is set up for each modal. Each registry needs a unique name, // and these names default to their `Compatible.keyFor` value. When we show two // of the same type at the same time, we need to give them unique names. - TestModal(NamedScreen(BackStackScreen(EmptyRendering, layer1Screen0), "another")), + TestOverlay(NamedScreen(BackStackScreen(EmptyRendering, layer1Screen0), "another")), ) ) } @@ -514,11 +516,11 @@ internal class ComposeViewTreeIntegrationTest { it.setRendering( BodyAndOverlaysScreen( EmptyRendering, - TestModal(BackStackScreen(EmptyRendering, layer0Screen0, layer0Screen1)), + TestOverlay(BackStackScreen(EmptyRendering, layer0Screen0, layer0Screen1)), // A SavedStateRegistry is set up for each modal. Each registry needs a unique name, // and these names default to their `Compatible.keyFor` value. When we show two // of the same type at the same time, we need to give them unique names. - TestModal( + TestOverlay( NamedScreen(BackStackScreen(EmptyRendering, layer1Screen0, layer1Screen1), "another") ), ) @@ -556,11 +558,11 @@ internal class ComposeViewTreeIntegrationTest { it.setRendering( BodyAndOverlaysScreen( EmptyRendering, - TestModal(BackStackScreen(EmptyRendering, layer0Screen0)), + TestOverlay(BackStackScreen(EmptyRendering, layer0Screen0)), // A SavedStateRegistry is set up for each modal. Each registry needs a unique name, // and these names default to their `Compatible.keyFor` value. When we show two // of the same type at the same time, we need to give them unique names. - TestModal(NamedScreen(BackStackScreen(EmptyRendering, layer1Screen0), "another")), + TestOverlay(NamedScreen(BackStackScreen(EmptyRendering, layer1Screen0), "another")), ) ) } @@ -575,15 +577,15 @@ internal class ComposeViewTreeIntegrationTest { setRendering(BackStackScreen(EmptyRendering, backstack.asList())) } - data class TestModal( + data class TestOverlay( override val content: Screen - ) : ScreenOverlay, AndroidOverlay { - override fun map(transform: (Screen) -> ContentU) = error("Not implemented") + ) : ScreenOverlay, AndroidOverlay { + override fun map(transform: (Screen) -> U) = error("Not implemented") - override val dialogFactory = object : ScreenOverlayDialogFactory( - TestModal::class - ) { - } + override val dialogFactory = + OverlayDialogFactory { initialRendering, initialEnvironment, context: Context -> + ComponentDialog(context).setContent(initialRendering, initialEnvironment) + } } data class TestComposeRendering( diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api index 3773724250..8257786a02 100644 --- a/workflow-ui/core-android/api/core-android.api +++ b/workflow-ui/core-android/api/core-android.api @@ -363,6 +363,10 @@ public final class com/squareup/workflow1/ui/container/BackStackContainer$SavedS public synthetic fun newArray (I)[Ljava/lang/Object; } +public final class com/squareup/workflow1/ui/container/ComponentDialogSetContentKt { + public static final fun setContent (Landroidx/activity/ComponentDialog;Lcom/squareup/workflow1/ui/container/ScreenOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder; +} + public final class com/squareup/workflow1/ui/container/LayeredDialogSessions { public static final field Companion Lcom/squareup/workflow1/ui/container/LayeredDialogSessions$Companion; public synthetic fun (Landroid/content/Context;Lkotlinx/coroutines/flow/StateFlow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -393,9 +397,13 @@ public final class com/squareup/workflow1/ui/container/LayeredDialogSessions$Sav } public abstract interface class com/squareup/workflow1/ui/container/OverlayDialogFactory : com/squareup/workflow1/ui/ViewRegistry$Entry { + public static final field Companion Lcom/squareup/workflow1/ui/container/OverlayDialogFactory$Companion; public abstract fun buildDialog (Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder; } +public final class com/squareup/workflow1/ui/container/OverlayDialogFactory$Companion { +} + public abstract interface class com/squareup/workflow1/ui/container/OverlayDialogFactoryFinder { public static final field Companion Lcom/squareup/workflow1/ui/container/OverlayDialogFactoryFinder$Companion; public abstract fun getDialogFactoryForRendering (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/container/Overlay;)Lcom/squareup/workflow1/ui/container/OverlayDialogFactory; diff --git a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/DialogIntegrationTest.kt b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/DialogIntegrationTest.kt index 2c282438c4..63b15b6f47 100644 --- a/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/DialogIntegrationTest.kt +++ b/workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/DialogIntegrationTest.kt @@ -2,11 +2,11 @@ package com.squareup.workflow1.ui.container import android.app.Dialog import android.text.SpannableStringBuilder -import android.view.View import android.view.ViewGroup.LayoutParams import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.EditText import androidx.activity.ComponentActivity +import androidx.activity.ComponentDialog import androidx.lifecycle.Lifecycle.State.DESTROYED import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches @@ -53,7 +53,6 @@ internal class DialogIntegrationTest { } } - private var latestContentView: View? = null private var latestDialog: Dialog? = null private inner class DialogRendering( @@ -65,22 +64,10 @@ internal class DialogIntegrationTest { override val compatibilityKey = name - override val dialogFactory = - object : ScreenOverlayDialogFactory( - type = DialogRendering::class - ) { - override fun buildDialogWithContent( - initialRendering: DialogRendering, - initialEnvironment: ViewEnvironment, - content: ScreenViewHolder - ): OverlayDialogHolder { - latestContentView = content.view - - return super.buildDialogWithContent(initialRendering, initialEnvironment, content).also { - latestDialog = it.dialog - } - } - } + override val dialogFactory = OverlayDialogFactory { r, e, c -> + val dialog = ComponentDialog(c).also { latestDialog = it } + dialog.setContent(r, e) + } } @Test fun showOne() { @@ -96,7 +83,6 @@ internal class DialogIntegrationTest { } onView(withText("content")).inRoot(isDialog()).check(matches(isDisplayed())) - assertThat(latestContentView).isNotNull() assertThat(latestDialog).isNotNull() assertThat(latestDialog!!.isShowing).isTrue() } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AlertOverlayDialogFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AlertOverlayDialogFactory.kt index 531bc9c310..bd7de8a99d 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AlertOverlayDialogFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AlertOverlayDialogFactory.kt @@ -17,6 +17,7 @@ import kotlin.reflect.KClass /** * Default [OverlayDialogFactory] for [AlertOverlay], uses [AlertDialog]. + * * See [AlertDialog.toDialogHolder] to use [AlertDialog] for other purposes. * * - To customize [AlertDialog] theming, see [AlertDialogThemeResId] diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidOverlay.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidOverlay.kt index 5b8839d537..c8d77d0f9b 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidOverlay.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidOverlay.kt @@ -13,7 +13,19 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi * * - [ScreenOverlay] for dialogs whose content is defined by a wrapped * [Screen][com.squareup.workflow1.ui.Screen] instance. And in this case, - * also note [ScreenOverlayDialogFactory]. + * also note the [ComponentDialog.setContent][setContent] extension function. + * + * For example: + * + * data class MyModal( + * override val content: C + * ) : ScreenOverlay, ModalOverlay, AndroidOverlay> { + * override val dialogFactory = OverlayDialogFactory> { r, e, c -> + * AppCompatDialog(c).setContent(r, e) + * } + * + * override fun map(transform: (C) -> D) = MyModal(transform(content)) + * } * * This is the simplest way to introduce a [Dialog][android.app.Dialog] workflow driven UI, * but using it requires your workflows code to reside in Android modules, instead diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndOverlaysContainer.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndOverlaysContainer.kt index 5a472b65d4..ea5ef4c41c 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndOverlaysContainer.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndOverlaysContainer.kt @@ -27,9 +27,6 @@ import com.squareup.workflow1.ui.screen * work as possible is delegated to the public [LayeredDialogSessions] * support class, to make it practical to write custom forks should * the need arise. - * - * See [ScreenOverlayDialogFactory] for a general overview of how - * Workflow's [android.app.Dialog] support actually works. */ @WorkflowUiExperimentalApi internal class BodyAndOverlaysContainer @JvmOverloads constructor( diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ComponentDialogSetContent.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ComponentDialogSetContent.kt new file mode 100644 index 0000000000..e5bd5a6af8 --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ComponentDialogSetContent.kt @@ -0,0 +1,65 @@ +package com.squareup.workflow1.ui.container + +import android.graphics.drawable.ColorDrawable +import android.util.TypedValue +import android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND +import androidx.activity.ComponentDialog +import androidx.activity.setViewTreeOnBackPressedDispatcherOwner +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner +import com.squareup.workflow1.ui.show +import com.squareup.workflow1.ui.startShowing +import com.squareup.workflow1.ui.toViewFactory + +/** + * Given a [ComponentDialog], wrap it in an [OverlayDialogHolder] that can drive + * the Dialog's content via instances of a particular type of [ScreenOverlay]. + * + * Dialogs managed this way are compatible with + * [View.setBackHandler][com.squareup.workflow1.ui.setBackHandler], + * and honor the [OverlayArea] and [CoveredByModal] values placed in + * the [ViewEnvironment] by the standard [BodyAndOverlaysScreen] container. + */ +@WorkflowUiExperimentalApi +public fun > ComponentDialog.setContent( + overlay: O, + environment: ViewEnvironment +): OverlayDialogHolder { + val contentHolder = overlay.content.toViewFactory(environment) + .startShowing(overlay.content, environment, context) { view, doStart -> + view.setViewTreeOnBackPressedDispatcherOwner(this@setContent) + WorkflowLifecycleOwner.installOn(view) { + this@setContent.lifecycle + } + doStart() + } + + setCancelable(false) + setContentView(contentHolder.view) + + // Welcome to Android. Nothing workflow-related here, this is just how one + // finds the window background color for the theme. I sure hope it's better in Compose. + val maybeWindowColor = TypedValue() + context.theme.resolveAttribute(android.R.attr.windowBackground, maybeWindowColor, true) + + val background = + if (maybeWindowColor.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT) { + ColorDrawable(maybeWindowColor.data) + } else { + // If we don't at least set it to null, the window cannot go full bleed. + null + } + with(window!!) { + setBackgroundDrawable(background) + clearFlags(FLAG_DIM_BEHIND) + } + + return OverlayDialogHolder( + initialEnvironment = environment, + dialog = this, + ) { newOverlay, newEnvironment -> + contentHolder.show(newOverlay.content, newEnvironment) + } +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/DialogSession.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/DialogSession.kt index 037f2ab500..5e53c5f263 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/DialogSession.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/DialogSession.kt @@ -69,6 +69,7 @@ internal class DialogSession( /** * Wrap the given dialog holder to maintain [allowEvents] on each update. */ + @Suppress("DEPRECATION") private val holder: OverlayDialogHolder = OverlayDialogHolder( holder.environment, holder.dialog, @@ -109,6 +110,7 @@ internal class DialogSession( return !allowEvents || realWindowCallback.dispatchTouchEvent(event) } + @Suppress("DEPRECATION") override fun dispatchKeyEvent(event: KeyEvent): Boolean { // Consume all events if we've been told to do so. if (!allowEvents) return true diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/FullScreenModalFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/FullScreenModalFactory.kt new file mode 100644 index 0000000000..c9e16142ac --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/FullScreenModalFactory.kt @@ -0,0 +1,25 @@ +package com.squareup.workflow1.ui.container + +import android.content.Context +import androidx.activity.ComponentDialog +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +/** + * Default [OverlayDialogFactory] for the standard [FullScreenModal] rendering class. + * Nothing more than a direct call to [ComponentDialog.setContent]. + * + * To provide a custom binding for [FullScreenModal], see [OverlayDialogFactoryFinder]. + */ +@WorkflowUiExperimentalApi +internal class FullScreenModalFactory() : OverlayDialogFactory> { + override val type = FullScreenModal::class + + override fun buildDialog( + initialRendering: FullScreenModal, + initialEnvironment: ViewEnvironment, + context: Context + ): OverlayDialogHolder> = + ComponentDialog(context).setContent(initialRendering, initialEnvironment) +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogFactory.kt index a2cc4aa5e3..088ac8eeff 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogFactory.kt @@ -8,18 +8,58 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi /** * Factory for [Dialog] instances that can show renderings of type [OverlayT] : [Overlay]. - * - * Implement this interface directly for rendering types that are completely self-descriptive, - * like [AlertOverlay]. For dialogs that wrap a [Screen][com.squareup.workflow1.ui.Screen] - * for their content, implement [ScreenOverlayDialogFactory] interface and use the - * [ScreenOverlay] rendering type. + * See [setContent] for ease of implementing [ScreenOverlay] factories. * * To minimize boilerplate, have your rendering classes implement [AndroidOverlay] to associate * them with appropriate an appropriate [OverlayDialogFactory]. For more flexibility, and to * avoid coupling your workflow directly to the Android runtime, see [ViewRegistry]. * - * See the kdoc on [ScreenOverlayDialogFactory] for a fuller description of how - * [Dialog]s are placed on the screen, and their impact on UI events. + * ## Details of [Dialog] management + * + * There is a lot of machinery provided to give control over the placement of + * [Dialog] windows, and to ensure that [ModalOverlay] dialogs behave as expected (i.e., + * that events in the Activity window are blocked the instant a modal [Dialog] is shown). + * + * For placement, consider a layout where we want the option to show a tutorial bar below + * the main UI. + * + * +-------------------------+ + * | | + * | BodyAndOverlaysScreen | + * | | + * +-------------------------+ + * | TutorialBarScreen | + * +-------------------------+ + * + * Suppose we have custom dialogs that we want to cover the entire screen, except when + * tutorial is running -- the tutorial bar should always be visible, and should always + * be able to field touch events. + * + * To support this case we provide the [OverlayArea] value in the [ViewEnvironment]. + * When a [BodyAndOverlaysScreen] includes [overlays][BodyAndOverlaysScreen.overlays], + * the [OverlayArea] holds the bounds of the view created to display the + * [body screen][BodyAndOverlaysScreen.body]. Well behaved dialogs created to + * display those [Overlay] renderings look for [OverlayArea] value and restrict + * themselves to the reported bounds. + * + * Another [ViewEnvironment] value is maintained to support modality: [CoveredByModal]. + * When this value is true, it indicates that a dialog window driven by a [ModalOverlay] + * is in play over the view, or is about to be, and so touch and click events should be + * ignored. This is necessary because there is a long period after a call to + * [Dialog.show][android.app.Dialog.show] before the new Dialog window will start + * intercepting events -- an issue that has preoccupied Square for a + * [very long time](https://stackoverflow.com/questions/2886407/dealing-with-rapid-tapping-on-buttons).) + * The default container view driven by [BodyAndOverlaysScreen] automatically honors + * [CoveredByModal]. + * + * All of this is driven by the [LayeredDialogSessions] support class, which can also be used to + * create custom [BodyAndOverlaysScreen] container views. To put such a custom container + * in play, see [OverlayDialogFactoryFinder]. + * + * It is important to note that the modal behavior described here is all keyed to the + * [ModalOverlay] interface, not its parent type [Overlay]. Rendering types that declare the + * latter but not the former can be used to create dialogs for non-modal windows like toasts + * and tool tips. */ @WorkflowUiExperimentalApi public interface OverlayDialogFactory : ViewRegistry.Entry { @@ -29,8 +69,40 @@ public interface OverlayDialogFactory : ViewRegistry.Entry + + public companion object { + public inline operator fun invoke( + crossinline buildDialog: ( + initialRendering: OverlayT, + initialEnvironment: ViewEnvironment, + context: Context + ) -> OverlayDialogHolder + ): OverlayDialogFactory { + return object : OverlayDialogFactory { + override val type = OverlayT::class + + override fun buildDialog( + initialRendering: OverlayT, + initialEnvironment: ViewEnvironment, + context: Context + ): OverlayDialogHolder = + buildDialog(initialRendering, initialEnvironment, context) + } + } + } } +/** + * Use the [OverlayDialogFactory] in [environment] to return the [OverlayDialogFactory] bound to the + * type of the receiving [Overlay]. + * + * It is rare to call this method directly. Instead the most common path is to rely on + * the default container `View` bound to [BodyAndOverlaysScreen]. If you need to build + * your own replacement for that `View`, you should be able to delegate most of the + * work to [LayeredDialogSessions], which will call this method for you. And see + * [OverlayDialogFactoryFinder] to change the default binding [BodyAndOverlaysScreen] + * to your custom `View`. + */ @WorkflowUiExperimentalApi public fun T.toDialogFactory( environment: ViewEnvironment diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogFactoryFinder.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogFactoryFinder.kt index f2425bf4b5..a495a05158 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogFactoryFinder.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogFactoryFinder.kt @@ -5,7 +5,6 @@ import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewEnvironmentKey import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import kotlin.reflect.KClass /** * [ViewEnvironment] service object used by [Overlay.toDialogFactory] to find the right @@ -26,9 +25,8 @@ public interface OverlayDialogFactoryFinder { ?: (rendering as? AlertOverlay)?.let { AlertOverlayDialogFactory() as OverlayDialogFactory } - ?: (rendering as? FullScreenModal)?.let { - ScreenOverlayDialogFactory(FullScreenModal::class as KClass>) - as OverlayDialogFactory + ?: (rendering as? FullScreenModal<*>)?.let { + FullScreenModalFactory() as OverlayDialogFactory } ?: throw IllegalArgumentException( "An OverlayDialogFactory should have been registered to display $rendering, " + diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogHolder.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogHolder.kt index 853c2b012f..ed6dcdc983 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogHolder.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogHolder.kt @@ -54,6 +54,9 @@ public interface OverlayDialogHolder { * and invokes its [onBackPressed][androidx.activity.OnBackPressedDispatcher.onBackPressed] * method. */ + @Deprecated( + "This will be deleted in the next release, use ComponentDialog and OnBackPressedDispatcher." + ) public val onBackPressed: (() -> Unit)? public companion object { diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/RealOverlayDialogHolder.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/RealOverlayDialogHolder.kt index 2e8b941a38..d9148ed304 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/RealOverlayDialogHolder.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/RealOverlayDialogHolder.kt @@ -10,6 +10,9 @@ internal class RealOverlayDialogHolder( initialEnvironment: ViewEnvironment, override val dialog: Dialog, override val onUpdateBounds: ((Rect) -> Unit)?, + @Deprecated( + "This will be deleted in the next release, use ComponentDialog and OnBackPressedDispatcher." + ) override val onBackPressed: (() -> Unit)?, runnerFunction: (rendering: OverlayT, environment: ViewEnvironment) -> Unit ) : OverlayDialogHolder { diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ScreenOverlayDialogFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ScreenOverlayDialogFactory.kt index 60faad54bb..cd818d943a 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ScreenOverlayDialogFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ScreenOverlayDialogFactory.kt @@ -1,10 +1,11 @@ +@file:Suppress("DEPRECATION") + package com.squareup.workflow1.ui.container import android.app.Dialog import android.content.Context import android.graphics.drawable.ColorDrawable import android.util.TypedValue -import android.view.Window import android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL import com.squareup.workflow1.ui.Screen @@ -17,74 +18,7 @@ import com.squareup.workflow1.ui.startShowing import com.squareup.workflow1.ui.toViewFactory import kotlin.reflect.KClass -/** - * Extensible base implementation of [OverlayDialogFactory] for [ScreenOverlay] - * types. Also serves as the default factory for [FullScreenModal]. - * (Use a custom [OverlayDialogFactoryFinder] to customize the presentation - * of [FullScreenModal].) - * - * Dialogs built by this class are compatible with - * [View.backPressedHandler][com.squareup.workflow1.ui.backPressedHandler], - * and honor the [OverlayArea] and [CoveredByModal] values placed in - * the [ViewEnvironment] by the standard [BodyAndOverlaysScreen] container. - * - * ## Details of [Dialog] management - * - * There is a lot of machinery in play to to provide control over the placement of - * [Dialog] windows, and to ensure that [ModalOverlay] dialogs behave as expected (i.e., - * that events in the Activity window are blocked the instant a modal [Dialog] is shown). - * - * For placement, consider a layout where we want the option to show a tutorial bar below - * the main UI. - * - * +-------------------------+ - * | | - * | BodyAndOverlaysScreen | - * | | - * +-------------------------+ - * | TutorialBarScreen | - * +-------------------------+ - * - * Suppose we have custom dialogs that we want to cover the entire screen, except when - * tutorial is running -- the tutorial bar should always be visible, and should always - * be able to field touch events. - * - * To support this case we provide the [OverlayArea] value in the [ViewEnvironment]. - * When a [BodyAndOverlaysScreen] includes [overlays][BodyAndOverlaysScreen.overlays], - * the [OverlayArea] holds the bounds of the view created to display the - * [body screen][BodyAndOverlaysScreen.body]. Well behaved dialogs created to - * display those [Overlay] renderings look for [OverlayArea] value and restrict - * themselves to the reported bounds. - * - * Another [ViewEnvironment] value is maintained to support modality: [CoveredByModal]. - * When this value is true, it indicates that a dialog window driven by a [ModalOverlay] - * is in play over the view, or is about to be, and so touch and click events should be - * ignored. This is necessary because there is a long period after a call to - * [Dialog.show][android.app.Dialog.show] before the new Dialog window will start - * intercepting events -- an issue that has preoccupied Square for a - * [very long time](https://stackoverflow.com/questions/2886407/dealing-with-rapid-tapping-on-buttons).) - * - * The default container view driven by [BodyAndOverlaysScreen] automatically honors - * [CoveredByModal]. Dialog windows built by [ScreenOverlayDialogFactory] also honor - * [CoveredByModal] when there is a [ModalOverlay]-driven dialog with a higher Z index in play. - * All of this is driven by the [LayeredDialogSessions] support class, which can also be used to - * create custom [BodyAndOverlaysScreen] container views. To put such a custom container - * in play, see [OverlayDialogFactoryFinder]. - * - * Modality also has implications for the handling of the Android back button. - * While a modal dialog is shown, - * [back press handlers][com.squareup.workflow1.ui.backPressedHandler] on covered - * views and windows should not fire. [ScreenOverlayDialogFactory] takes care - * of that requirement by default, presuming that your app uses Jetpack - * [OnBackPressedDispatcher][androidx.activity.OnBackPressedDispatcher]. If that is not - * the case, override [buildDialogWithContent] and provide an alternative `onBackPressed` - * implementation when you call [OverlayDialogHolder]. - * - * It is important to note that the modal behavior described here is all keyed to the - * [ModalOverlay] interface, not its parent type [Overlay]. Rendering types that declare the - * latter but not the former can be used to create dialogs for non-modal windows like toasts - * and tool tips. - */ +@Deprecated("Use ComponentDialog.setContent") @WorkflowUiExperimentalApi public open class ScreenOverlayDialogFactory>( override val type: KClass @@ -141,18 +75,7 @@ public open class ScreenOverlayDialogFactory>( } } -/** - * Used by the default implementation of [ScreenOverlayDialogFactory.buildDialogWithContent] - * to show [contentHolder]. - * - * - Makes the receiver [non-cancelable][Dialog.setCancelable] - * - * - Sets the [background][Window.setBackgroundDrawable] of the receiver's [Window] based - * on its theme, if any, or else `null`. (Setting the background to `null` ensures the window - * can go full bleed.) - * - * - Disables dimming. - */ +@Deprecated("Use the ComponentDialog.setContent extension instead.") @OptIn(WorkflowUiExperimentalApi::class) public fun Dialog.setContent(contentHolder: ScreenViewHolder<*>) { setCancelable(false)