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 b6964bb625..1627d792c3 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 @@ -5,9 +5,13 @@ import android.graphics.Rect 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.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 /** * Android support for [PanelOverlay]. @@ -18,41 +22,48 @@ internal object PanelOverlayDialogFactory : type = PanelOverlay::class ) { /** - * Forks the default implementation to apply [R.style.PanelDialog], for - * enter and exit animation. + * Forks the default implementation to apply [R.style.PanelDialog] for + * enter and exit animation, and to customize [bounds][OverlayDialogHolder.onUpdateBounds]. */ - override fun buildDialogWithContent(content: ScreenViewHolder): Dialog { - return Dialog(content.view.context, R.style.PanelDialog).also { - it.setContent(content) - } - } + override fun buildDialogWithContent( + initialRendering: PanelOverlay, + initialEnvironment: ViewEnvironment, + content: ScreenViewHolder + ): OverlayDialogHolder> { + val dialog = Dialog(content.view.context, R.style.PanelDialog) + dialog.setContent(content) - override fun updateBounds( - dialog: Dialog, - bounds: Rect - ) { - val refinedBounds: Rect = if (!dialog.context.isTablet) { - // On a phone, fill the bounds entirely. - bounds - } else { - if (bounds.height() > bounds.width()) { - val margin = bounds.height() - bounds.width() - val topDelta = margin / 2 - val bottomDelta = margin - topDelta - Rect(bounds).apply { - top = bounds.top + topDelta - bottom = bounds.bottom - bottomDelta - } - } else { - val margin = bounds.width() - bounds.height() - val leftDelta = margin / 2 - val rightDelta = margin - leftDelta - Rect(bounds).apply { - left = bounds.left + leftDelta - right = bounds.right - rightDelta + return OverlayDialogHolder( + initialEnvironment = initialEnvironment, + dialog = dialog, + onUpdateBounds = { bounds -> + val refinedBounds: Rect = if (!dialog.context.isTablet) { + // On a phone, fill the bounds entirely. + bounds + } else { + if (bounds.height() > bounds.width()) { + val margin = bounds.height() - bounds.width() + val topDelta = margin / 2 + val bottomDelta = margin - topDelta + Rect(bounds).apply { + top = bounds.top + topDelta + bottom = bounds.bottom - bottomDelta + } + } else { + val margin = bounds.width() - bounds.height() + val leftDelta = margin / 2 + val rightDelta = margin - leftDelta + Rect(bounds).apply { + left = bounds.left + leftDelta + right = bounds.right - rightDelta + } + } } + + dialog.setBounds(refinedBounds) } + ) { overlayRendering, environment -> + content.show(overlayRendering.content, environment) } - super.updateBounds(dialog, refinedBounds) } } 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 df2eeb2d2d..73d20e7fc9 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 @@ -1,6 +1,5 @@ package com.squareup.workflow1.ui.compose -import android.app.Dialog import android.content.Context import android.view.View import android.view.ViewGroup @@ -569,9 +568,6 @@ internal class ComposeViewTreeIntegrationTest { override val dialogFactory = object : ScreenOverlayDialogFactory( TestModal::class ) { - override fun buildDialogWithContent(content: ScreenViewHolder): Dialog { - return Dialog(content.view.context).apply { setContentView(content.view) } - } } } diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api index 15d1032612..a7fbfcc5ca 100644 --- a/workflow-ui/core-android/api/core-android.api +++ b/workflow-ui/core-android/api/core-android.api @@ -436,8 +436,8 @@ public class com/squareup/workflow1/ui/container/AlertOverlayDialogFactory : com } public final class com/squareup/workflow1/ui/container/AndroidDialogBoundsKt { - public static final fun maintainBounds (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function2;)V - public static final fun maintainBounds (Landroid/app/Dialog;Lkotlinx/coroutines/flow/StateFlow;Lkotlin/jvm/functions/Function2;)V + public static final fun maintainBounds (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function1;)V + public static final fun maintainBounds (Landroid/app/Dialog;Lkotlinx/coroutines/flow/StateFlow;Lkotlin/jvm/functions/Function1;)V public static final fun setBounds (Landroid/app/Dialog;Landroid/graphics/Rect;)V } @@ -672,6 +672,7 @@ public abstract interface class com/squareup/workflow1/ui/container/OverlayDialo public static final field Companion Lcom/squareup/workflow1/ui/container/OverlayDialogHolder$Companion; public abstract fun getDialog ()Landroid/app/Dialog; public abstract fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment; + public abstract fun getOnUpdateBounds ()Lkotlin/jvm/functions/Function1; public abstract fun getRunner ()Lkotlin/jvm/functions/Function2; } @@ -689,16 +690,18 @@ public final class com/squareup/workflow1/ui/container/OverlayDialogHolder$Compa } public final class com/squareup/workflow1/ui/container/OverlayDialogHolderKt { - public static final fun OverlayDialogHolder (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/app/Dialog;Lkotlin/jvm/functions/Function2;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder; + 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; + 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; public static final fun canShow (Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;Lcom/squareup/workflow1/ui/container/Overlay;)Z public static final fun getShowing (Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;)Lcom/squareup/workflow1/ui/container/Overlay; public static final fun show (Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)V } public final class com/squareup/workflow1/ui/container/RealOverlayDialogHolder : com/squareup/workflow1/ui/container/OverlayDialogHolder { - public fun (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/app/Dialog;Lkotlin/jvm/functions/Function2;)V + public fun (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/app/Dialog;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V public fun getDialog ()Landroid/app/Dialog; public fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment; + public fun getOnUpdateBounds ()Lkotlin/jvm/functions/Function1; public fun getRunner ()Lkotlin/jvm/functions/Function2; } @@ -707,9 +710,8 @@ public class com/squareup/workflow1/ui/container/ScreenOverlayDialogFactory : co 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; public synthetic fun buildDialog (Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder; public final fun buildDialog (Lcom/squareup/workflow1/ui/container/ScreenOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder; - public fun buildDialogWithContent (Lcom/squareup/workflow1/ui/ScreenViewHolder;)Landroid/app/Dialog; + 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; public fun getType ()Lkotlin/reflect/KClass; - public fun updateBounds (Landroid/app/Dialog;Landroid/graphics/Rect;)V } public final class com/squareup/workflow1/ui/container/ScreenOverlayDialogFactoryKt { 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 a33ce470df..b7b38efa21 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 @@ -52,7 +52,11 @@ public open class AlertOverlayDialogFactory : OverlayDialogFactory alertDialog.setButton(button.toId(), " ") { _, _ -> } } - OverlayDialogHolder(initialEnvironment, alertDialog) { rendering, _ -> + OverlayDialogHolder( + initialEnvironment = initialEnvironment, + dialog = alertDialog, + onUpdateBounds = null + ) { rendering, _ -> with(alertDialog) { if (rendering.cancelable) { setOnCancelListener { rendering.onEvent(Canceled) } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidDialogBounds.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidDialogBounds.kt index 68852d50a4..33e08e89bb 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidDialogBounds.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidDialogBounds.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.onEach * [bounds] is expected to be in global display coordinates, * e.g. as returned from [View.getGlobalVisibleRect]. * - * @see ScreenOverlayDialogFactory.updateBounds + * @see OverlayDialogHolder.onUpdateBounds */ @WorkflowUiExperimentalApi public fun Dialog.setBounds(bounds: Rect) { @@ -37,7 +37,7 @@ public fun Dialog.setBounds(bounds: Rect) { @WorkflowUiExperimentalApi internal fun D.maintainBounds( environment: ViewEnvironment, - onBoundsChange: (D, Rect) -> Unit + onBoundsChange: (Rect) -> Unit ) { maintainBounds(environment[OverlayArea].bounds, onBoundsChange) } @@ -45,7 +45,7 @@ internal fun D.maintainBounds( @WorkflowUiExperimentalApi internal fun D.maintainBounds( bounds: StateFlow, - onBoundsChange: (D, Rect) -> Unit + onBoundsChange: (Rect) -> Unit ) { val window = requireNotNull(window) { "Dialog must be attached to a window." } window.callback = object : Window.Callback by window.callback { @@ -53,7 +53,7 @@ internal fun D.maintainBounds( override fun onAttachedToWindow() { scope = CoroutineScope(Dispatchers.Main.immediate).also { - bounds.onEach { b -> onBoundsChange(this@maintainBounds, b) } + bounds.onEach { b -> onBoundsChange(b) } .launchIn(it) } } @@ -65,5 +65,5 @@ internal fun D.maintainBounds( } // If already attached, set the bounds eagerly. - if (window.peekDecorView()?.isAttachedToWindow == true) onBoundsChange(this, bounds.value) + if (window.peekDecorView()?.isAttachedToWindow == true) onBoundsChange(bounds.value) } 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 013ef35ec8..a928945184 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 @@ -26,7 +26,7 @@ internal class DialogSession( index: Int, holder: OverlayDialogHolder ) { - // Note similar code in LayeredDialogs + // Note similar code in LayeredDialogSessions private var allowEvents = true set(value) { val was = field diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/LayeredDialogSessions.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/LayeredDialogSessions.kt index 200f4af94b..66a27c0c9e 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/LayeredDialogSessions.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/LayeredDialogSessions.kt @@ -163,6 +163,10 @@ public class LayeredDialogSessions private constructor( overlay.toDialogFactory(dialogEnv) .buildDialog(overlay, dialogEnv, context) .let { holder -> + holder.onUpdateBounds?.let { updateBounds -> + holder.dialog.maintainBounds(holder.environment) { b -> updateBounds(b) } + } + DialogSession(i, holder).also { newSession -> // Prime the pump, make the first call to OverlayDialog.show to update // the new dialog to reflect the first rendering. @@ -286,7 +290,7 @@ public class LayeredDialogSessions private constructor( context = view.context, bounds = bounds, cancelEvents = { - // Note similar code in DialogHolder. + // Note similar code in DialogSession. // https://stackoverflow.com/questions/2886407/dealing-with-rapid-tapping-on-buttons // If any motion events were enqueued on the main thread, cancel them. 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 731a4e6180..8edb6929a2 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 @@ -1,6 +1,7 @@ package com.squareup.workflow1.ui.container import android.app.Dialog +import android.graphics.Rect import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.ViewEnvironmentKey @@ -29,6 +30,20 @@ public interface OverlayDialogHolder { */ public val runner: (rendering: OverlayT, environment: ViewEnvironment) -> Unit + /** + * Optional function called to report the bounds of the managing container view, + * as reported by [OverlayArea]. Well behaved [Overlay] dialogs are expected to + * be restricted to those bounds, to the extent practical -- you probably want to ignore + * this for AlertDialog, e.g. + * + * Honoring this contract makes it easy to define areas of the display + * that are outside of the "shadow" of a modal dialog. Imagine an app + * with a status bar that should not be covered by modals. + * + * Default implementation provided by the factory function below calls [Dialog.setBounds]. + */ + public val onUpdateBounds: ((Rect) -> Unit)? + public companion object { /** * Default value returned for the [InOverlay] [ViewEnvironmentKey], and therefore the @@ -87,7 +102,8 @@ public val OverlayDialogHolder<*>.showing: Overlay public fun OverlayDialogHolder( initialEnvironment: ViewEnvironment, dialog: Dialog, + onUpdateBounds: ((Rect) -> Unit)? = { dialog.setBounds(it) }, runner: (rendering: OverlayT, environment: ViewEnvironment) -> Unit ): OverlayDialogHolder { - return RealOverlayDialogHolder(initialEnvironment, dialog, runner) + return RealOverlayDialogHolder(initialEnvironment, dialog, onUpdateBounds, runner) } 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 6c6d982a8f..eff45a4eab 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 @@ -1,6 +1,7 @@ package com.squareup.workflow1.ui.container import android.app.Dialog +import android.graphics.Rect import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi @@ -8,6 +9,7 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi internal class RealOverlayDialogHolder( initialEnvironment: ViewEnvironment, override val dialog: Dialog, + override val onUpdateBounds: ((Rect) -> 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 04f65968be..954d20751d 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 @@ -2,7 +2,6 @@ package com.squareup.workflow1.ui.container import android.app.Dialog import android.content.Context -import android.graphics.Rect import android.graphics.drawable.ColorDrawable import android.util.TypedValue import android.view.KeyEvent @@ -61,11 +60,6 @@ import kotlin.reflect.KClass * display those [Overlay] renderings look for [OverlayArea] value and restrict * themselves to the reported bounds. * - * Dialogs created via [ScreenOverlayDialogFactory] implementations honor [OverlayArea] - * automatically. [updateBounds] is called as the [OverlayArea] changes, and the - * default implementation of that method sets created dialog windows to fill the given area -- - * not necessarily the entire display. - * * 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 @@ -109,42 +103,33 @@ public open class ScreenOverlayDialogFactory>( initialEnvironment: ViewEnvironment, context: Context ): ScreenViewHolder { - return viewFactory - .startShowing(initialContent, initialEnvironment, context) + return viewFactory.startShowing(initialContent, initialEnvironment, context) } /** * Build the [Dialog] for the [content] that was just created by [buildContent]. - * Open to allow customization, typically theming. - * - * The default implementation delegates all work to the provided [Dialog.setContent] - * extension function. Subclasses need not call `super`. + * Open to allow customization, typically theming, subclasses need not call `super`. + * - Note that the default implementation calls the provided [Dialog.setContent] + * extension for typical setup. + * - Be sure to call [ScreenViewHolder.show] from [OverlayDialogHolder.runner]. */ - public open fun buildDialogWithContent(content: ScreenViewHolder): Dialog { - return Dialog(content.view.context).also { it.setContent(content) } + public open fun buildDialogWithContent( + initialRendering: O, + initialEnvironment: ViewEnvironment, + content: ScreenViewHolder + ): OverlayDialogHolder { + return OverlayDialogHolder( + initialEnvironment, Dialog(content.view.context).also { it.setContent(content) } + ) { overlayRendering, environment -> + content.show(overlayRendering.content, environment) + } } /** - * This method will be called to report the bounds of the managing container view, - * as reported by [OverlayArea]. Well behaved [ScreenOverlay] dialogs are expected to - * be restricted to those bounds. - * - * Honoring this contract makes it easy to define areas of the display - * that are outside of the "shadow" of a modal dialog. Imagine an app - * with a status bar that should not be covered by modals. - * - * The default implementation calls straight through to the [Dialog.setBounds] function - * provided below. Custom implementations are not required to call `super`. - * - * @see Dialog.setBounds + * Locked down implementation enforces [ModalOverlay] and supports + * [ModalScreenOverlayBackButtonHelper]. Delegates to [buildContent] to create the content view + * and [buildDialogWithContent] to create the [Dialog]. */ - public open fun updateBounds( - dialog: Dialog, - bounds: Rect - ) { - dialog.setBounds(bounds) - } - final override fun buildDialog( initialRendering: O, initialEnvironment: ViewEnvironment, @@ -159,8 +144,12 @@ public open class ScreenOverlayDialogFactory>( val contentViewHolder = buildContent(contentViewFactory, initialRendering.content, initialEnvironment, context) - return buildDialogWithContent(contentViewHolder).let { dialog -> - val window = requireNotNull(dialog.window) { "Dialog must be attached to a window." } + return buildDialogWithContent( + initialRendering, + initialEnvironment, + contentViewHolder + ).also { holder -> + val window = requireNotNull(holder.dialog.window) { "Dialog must be attached to a window." } if (modal) { val realWindowCallback = window.callback @@ -183,13 +172,6 @@ public open class ScreenOverlayDialogFactory>( // notion of its modality. Even a modal dialog should only block events within // the appropriate bounds, but Android makes them block everywhere. window.setFlags(FLAG_NOT_TOUCH_MODAL, FLAG_NOT_TOUCH_MODAL) - - // Keep an eye on the bounds StateFlow(Rect) put in place by [LayeredDialogSessions]. - dialog.maintainBounds(contentViewHolder.environment) { d, b -> updateBounds(d, Rect(b)) } - - OverlayDialogHolder(initialEnvironment, dialog) { overlayRendering, environment -> - contentViewHolder.show(overlayRendering.content, environment) - } } }