Skip to content

Commit

Permalink
Simpler, better back button handling.
Browse files Browse the repository at this point in the history
`OverlayDialogHolder` now can provide an `onBackPressed: () -> Unit` function to be called in place of `Dialog.onBackPressed`, and we provide a default implementation that hooks in `OnBackPressedDispatcher`. This works for all kinds of dialogs, not just modal ones, and replaces `ModalScreenOverlayBackButtonHelper`. The other method that was on `ModalScreenOverlayBackButtonHelper`, which put the no-op `backPressedHandler` on the content view to ensure those on layer layers would not fire, is now built into the default implementation of `ScreenOverlayDialogFactory.buildDialogWithContent`, which is designed to be customized anyway.

Also locks down `AlertOverlayDialogFactory`, but moves all the interesting parts into a public function. Should be easier and safer for re-use.
  • Loading branch information
rjrjr committed Jul 28, 2022
1 parent 2594db0 commit 3c394cf
Show file tree
Hide file tree
Showing 12 changed files with 166 additions and 290 deletions.
36 changes: 10 additions & 26 deletions workflow-ui/core-android/api/core-android.api
Original file line number Diff line number Diff line change
Expand Up @@ -429,16 +429,19 @@ public final class com/squareup/workflow1/ui/container/AlertDialogThemeResId : c
public synthetic fun getDefault ()Ljava/lang/Object;
}

public class com/squareup/workflow1/ui/container/AlertOverlayDialogFactory : com/squareup/workflow1/ui/container/OverlayDialogFactory {
public final class com/squareup/workflow1/ui/container/AlertOverlayDialogFactory : com/squareup/workflow1/ui/container/OverlayDialogFactory {
public fun <init> ()V
public fun buildDialog (Lcom/squareup/workflow1/ui/container/AlertOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;
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 fun getType ()Lkotlin/reflect/KClass;
}

public final class com/squareup/workflow1/ui/container/AlertOverlayDialogFactoryKt {
public static final fun toDialogHolder (Landroid/app/AlertDialog;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;
}

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/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
}

Expand Down Expand Up @@ -612,26 +615,6 @@ public final class com/squareup/workflow1/ui/container/LayeredDialogSessions$Sav
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public abstract interface class com/squareup/workflow1/ui/container/ModalScreenOverlayBackButtonHelper {
public static final field Companion Lcom/squareup/workflow1/ui/container/ModalScreenOverlayBackButtonHelper$Companion;
public abstract fun onBackPressed (Landroid/view/View;)Z
public abstract fun onContentViewUpdate (Landroid/view/View;)V
}

public final class com/squareup/workflow1/ui/container/ModalScreenOverlayBackButtonHelper$Companion : com/squareup/workflow1/ui/ViewEnvironmentKey {
public fun getDefault ()Lcom/squareup/workflow1/ui/container/ModalScreenOverlayBackButtonHelper;
public synthetic fun getDefault ()Ljava/lang/Object;
}

public final class com/squareup/workflow1/ui/container/ModalScreenOverlayBackButtonHelper$DefaultImpls {
public static fun onBackPressed (Lcom/squareup/workflow1/ui/container/ModalScreenOverlayBackButtonHelper;Landroid/view/View;)Z
public static fun onContentViewUpdate (Lcom/squareup/workflow1/ui/container/ModalScreenOverlayBackButtonHelper;Landroid/view/View;)V
}

public final class com/squareup/workflow1/ui/container/ModalScreenOverlayBackButtonHelperKt {
public static final fun plus (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/container/ModalScreenOverlayBackButtonHelper;)Lcom/squareup/workflow1/ui/ViewEnvironment;
}

public final class com/squareup/workflow1/ui/container/OverlayArea {
public static final field Companion Lcom/squareup/workflow1/ui/container/OverlayArea$Companion;
public fun <init> (Lkotlinx/coroutines/flow/StateFlow;)V
Expand Down Expand Up @@ -673,6 +656,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 getOnBackPressed ()Lkotlin/jvm/functions/Function0;
public abstract fun getOnUpdateBounds ()Lkotlin/jvm/functions/Function1;
public abstract fun getRunner ()Lkotlin/jvm/functions/Function2;
}
Expand All @@ -691,24 +675,24 @@ 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/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 OverlayDialogHolder (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/app/Dialog;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;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/Function0;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 <init> (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/app/Dialog;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
public fun <init> (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/app/Dialog;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)V
public fun getDialog ()Landroid/app/Dialog;
public fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment;
public fun getOnBackPressed ()Lkotlin/jvm/functions/Function0;
public fun getOnUpdateBounds ()Lkotlin/jvm/functions/Function1;
public fun getRunner ()Lkotlin/jvm/functions/Function2;
}

public class com/squareup/workflow1/ui/container/ScreenOverlayDialogFactory : com/squareup/workflow1/ui/container/OverlayDialogFactory {
public fun <init> (Lkotlin/reflect/KClass;)V
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/container/ScreenOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/ScreenViewHolder;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.squareup.workflow1.ui.container

import android.app.Dialog
import android.content.Context
import android.view.View
import android.widget.EditText
import androidx.activity.ComponentActivity
Expand Down Expand Up @@ -55,24 +54,17 @@ internal class DialogIntegrationTest {
object : ScreenOverlayDialogFactory<ContentRendering, DialogRendering>(
type = DialogRendering::class
) {
override fun buildContent(
viewFactory: ScreenViewFactory<ContentRendering>,
initialContent: ContentRendering,
initialEnvironment: ViewEnvironment,
context: Context
): ScreenViewHolder<ContentRendering> =
super.buildContent(viewFactory, initialContent, initialEnvironment, context).also {
latestContentView = it.view
}

override fun buildDialogWithContent(
initialRendering: DialogRendering,
initialEnvironment: ViewEnvironment,
content: ScreenViewHolder<ContentRendering>
): OverlayDialogHolder<DialogRendering> =
super.buildDialogWithContent(initialRendering, initialEnvironment, content).also {
): OverlayDialogHolder<DialogRendering> {
latestContentView = content.view

return super.buildDialogWithContent(initialRendering, initialEnvironment, content).also {
latestDialog = it.dialog
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public interface ScreenViewHolder<in ScreenT : Screen> {
/**
* The function that is run by [show] to update [view] with a new [Screen] rendering and
* [ViewEnvironment].
*
* Prefer calling [show] to using this directly, to ensure that [Showing] is
* maintained correctly, and [showing] keeps working.
*/
public val runner: ScreenViewRunner<ScreenT>

Expand Down Expand Up @@ -103,6 +106,10 @@ public fun <ScreenT : Screen> ScreenViewHolder<ScreenT>.show(
/**
* Returns the [Screen] most recently used to update the receiver's [view][ScreenViewHolder.view]
* via a call to [show].
*
* Note that the exact type of the returned [Screen] is likely not to match that of
* the receiver's `ScreenT` type parameter, e.g. if a
* [wrapping view factory][ScreenViewFactory.forWrapper] is in use.
*/
@WorkflowUiExperimentalApi
public val ScreenViewHolder<*>.showing: Screen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,84 +16,98 @@ import com.squareup.workflow1.ui.container.AlertOverlay.Event.Canceled
import kotlin.reflect.KClass

/**
* Default [OverlayDialogFactory] for [AlertOverlay].
* Default [OverlayDialogFactory] for [AlertOverlay], uses [AlertDialog].
* See [AlertDialog.toDialogHolder] to use [AlertDialog] for other purposes.
*
* This class is non-final for ease of customization of [AlertOverlay] handling,
* see [OverlayDialogFactoryFinder] for details.
* - To customize [AlertDialog] theming, see [AlertDialogThemeResId]
* - To customize how [AlertOverlay] is handled more generally, set up a
* custom [OverlayDialogFactoryFinder].
*/
@WorkflowUiExperimentalApi
public open class AlertOverlayDialogFactory : OverlayDialogFactory<AlertOverlay> {
internal class AlertOverlayDialogFactory : OverlayDialogFactory<AlertOverlay> {
override val type: KClass<AlertOverlay> = AlertOverlay::class

override fun buildDialog(
initialRendering: AlertOverlay,
initialEnvironment: ViewEnvironment,
context: Context
): OverlayDialogHolder<AlertOverlay> {
return AlertDialog.Builder(context, initialEnvironment[AlertDialogThemeResId])
.create().let { alertDialog ->
for (button in Button.values()) {
// We want to be able to update the alert while it's showing, including to maybe
// show more buttons than were there originally. The API for Android's `AlertDialog`
// makes you think you can do that, but it actually doesn't work. So we force
// `AlertDialog.Builder` to show every possible button; then we hide them all;
// and then we manage their visibility ourselves at update time.
//
// We also don't want Android to tear down the dialog without our say so --
// again, we might need to update the thing. But there is a dismiss call
// built in to click handlers put in place by `AlertDialog`. So, when we're
// preflighting every possible button, we put garbage click handlers in place.
// Then we replace them with our own, again at update time, by setting each live
// button's click handler directly, without letting `AlertDialog` interfere.
//
// https://github.com/square/workflow-kotlin/issues/138
//
// Why " "? An empty string means no button.
alertDialog.setButton(button.toId(), " ") { _, _ -> }
}
): OverlayDialogHolder<AlertOverlay> =
AlertDialog.Builder(context, initialEnvironment[AlertDialogThemeResId])
.create()
.toDialogHolder(initialEnvironment)
}

OverlayDialogHolder(
initialEnvironment = initialEnvironment,
dialog = alertDialog,
onUpdateBounds = null
) { rendering, _ ->
with(alertDialog) {
if (rendering.cancelable) {
setOnCancelListener { rendering.onEvent(Canceled) }
setCancelable(true)
} else {
setCancelable(false)
}
/**
* Wraps the receiver in in an [OverlayDialogHolder] that is able to update its
* buttons as new [AlertOverlay] renderings are received.
*/
@WorkflowUiExperimentalApi
public fun AlertDialog.toDialogHolder(
initialEnvironment: ViewEnvironment
): OverlayDialogHolder<AlertOverlay> {
for (button in Button.values()) {
// We want to be able to update the alert while it's showing, including to maybe
// show more buttons than were there originally. The API for Android's `AlertDialog`
// makes you think you can do that, but it actually doesn't work. So we force
// `AlertDialog.Builder` to show every possible button; then we hide them all;
// and then we manage their visibility ourselves at update time.
//
// We also don't want Android to tear down the dialog without our say so --
// again, we might need to update the thing. But there is a dismiss call
// built in to click handlers put in place by `AlertDialog`. So, when we're
// preflighting every possible button, we put garbage click handlers in place.
// Then we replace them with our own, again at update time, by setting each live
// button's click handler directly, without letting `AlertDialog` interfere.
//
// https://github.com/square/workflow-kotlin/issues/138
//
// Why " "? An empty string means no button.
setButton(button.toId(), " ") { _, _ -> }
}

return OverlayDialogHolder(
initialEnvironment = initialEnvironment,
dialog = this,
onUpdateBounds = null,
onBackPressed = null
) { rendering, _ ->
with(this) {
if (rendering.cancelable) {
setOnCancelListener { rendering.onEvent(Canceled) }
setCancelable(true)
} else {
setCancelable(false)
}

setMessage(rendering.message)
setTitle(rendering.title)
setMessage(rendering.message)
setTitle(rendering.title)

// The buttons won't actually exist until the dialog is showing.
if (isShowing) updateButtonsOnShow(rendering) else setOnShowListener {
updateButtonsOnShow(rendering)
}
}
}
// The buttons won't actually exist until the dialog is showing.
if (isShowing) updateButtonsOnShow(rendering) else setOnShowListener {
updateButtonsOnShow(rendering)
}
}
}
}

private fun Button.toId(): Int = when (this) {
POSITIVE -> DialogInterface.BUTTON_POSITIVE
NEGATIVE -> DialogInterface.BUTTON_NEGATIVE
NEUTRAL -> DialogInterface.BUTTON_NEUTRAL
}
@WorkflowUiExperimentalApi
private fun Button.toId(): Int = when (this) {
POSITIVE -> DialogInterface.BUTTON_POSITIVE
NEGATIVE -> DialogInterface.BUTTON_NEGATIVE
NEUTRAL -> DialogInterface.BUTTON_NEUTRAL
}

private fun AlertDialog.updateButtonsOnShow(rendering: AlertOverlay) {
setOnShowListener(null)
@WorkflowUiExperimentalApi
private fun AlertDialog.updateButtonsOnShow(rendering: AlertOverlay) {
setOnShowListener(null)

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

for (entry in rendering.buttons.entries) {
getButton(entry.key.toId())?.apply {
setOnClickListener { rendering.onEvent(ButtonClicked(entry.key)) }
text = entry.value
visibility = VISIBLE
}
for (entry in rendering.buttons.entries) {
getButton(entry.key.toId())?.apply {
setOnClickListener { rendering.onEvent(ButtonClicked(entry.key)) }
text = entry.value
visibility = VISIBLE
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ internal fun <D : Dialog> D.maintainBounds(
}

@WorkflowUiExperimentalApi
internal fun <D : Dialog> D.maintainBounds(
private fun <D : Dialog> D.maintainBounds(
bounds: StateFlow<Rect>,
onBoundsChange: (Rect) -> Unit
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
import android.view.KeyEvent
import android.view.KeyEvent.ACTION_UP
import android.view.KeyEvent.KEYCODE_BACK
import android.view.KeyEvent.KEYCODE_ESCAPE
import android.view.MotionEvent
import android.view.Window
import androidx.core.view.doOnAttach
Expand Down Expand Up @@ -46,14 +49,17 @@ internal class DialogSession(
* Wrap the given dialog holder to maintain [allowEvents] on each update.
*/
val holder: OverlayDialogHolder<Overlay> = OverlayDialogHolder(
holder.environment, holder.dialog
holder.environment, holder.dialog, holder.onUpdateBounds, holder.onBackPressed
) { overlay, environment ->
allowEvents = !environment[CoveredByModal]
holder.show(overlay, environment)
}

val savedStateRegistryKey = Compatible.keyFor(holder.showing, index.toString())

private val KeyEvent.isBackPress: Boolean
get() = (keyCode == KEYCODE_BACK || keyCode == KEYCODE_ESCAPE) && action == ACTION_UP

fun showDialog(
parentLifecycleOwner: LifecycleOwner,
stateRegistryAggregator: WorkflowSavedStateRegistryAggregator
Expand All @@ -68,7 +74,18 @@ internal class DialogSession(
}

override fun dispatchKeyEvent(event: KeyEvent): Boolean {
return !allowEvents || realWindowCallback.dispatchKeyEvent(event)
// Consume all events if we've been told to do so.
if (!allowEvents) return true

// If there is an onBackPressed handler invoke it instead of allowing
// the normal machinery to call Dialog.onBackPressed.
if (event.isBackPress) {
holder.onBackPressed?.invoke()
return true
}

// Allow the usual call to Dialog.onBackPressed.
return realWindowCallback.dispatchKeyEvent(event)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,19 +155,13 @@ public class LayeredDialogSessions private constructor(
// with the new Overlay at that index. Just update it.
sessions[i].also { it.holder.show(overlay, dialogEnv) }
} else {
// We need a new dialog for this overlay. Time to build it.
// We wrap our Dialog instances in DialogHolder to keep them
// paired with their current overlay rendering and environment.
// It would have been nice to keep those in tags on the Dialog's
// decor view, more consistent with what ScreenViewFactory does,
// but calling Window.getDecorView has side effects, and things
// break if we call it to early. Need to store them somewhere else.
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.
Expand Down
Loading

0 comments on commit 3c394cf

Please sign in to comment.