Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,38 +1,31 @@
package com.squareup.benchmarks.performance.complex.poetry.views

import android.app.Dialog
import android.content.Context
import android.view.ViewGroup.LayoutParams
import android.view.Gravity.CENTER
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import android.widget.ProgressBar
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.AndroidScreen
import com.squareup.workflow1.ui.ScreenViewFactory
import com.squareup.workflow1.ui.ScreenViewHolder
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.container.AndroidOverlay
import com.squareup.workflow1.ui.container.OverlayDialogFactory

@OptIn(WorkflowUiExperimentalApi::class)
object LoaderSpinner : AndroidOverlay<LoaderSpinner> {
override val dialogFactory: OverlayDialogFactory<LoaderSpinner>
get() = object : OverlayDialogFactory<LoaderSpinner> {
override val type = LoaderSpinner::class

override fun buildDialog(
initialRendering: LoaderSpinner,
initialEnvironment: ViewEnvironment,
context: Context
): Dialog = Dialog(context).apply {
setContentView(
ProgressBar(context).apply {
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
isIndeterminate = true
}
)
object LoaderSpinner : AndroidScreen<LoaderSpinner> {
override val viewFactory =
ScreenViewFactory.fromCode<LoaderSpinner> { _, initialEnvironment, context, _ ->
val progressBar = ProgressBar(context).apply {
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
).apply {
gravity = CENTER
}
isIndeterminate = true
}

override fun updateDialog(
dialog: Dialog,
rendering: LoaderSpinner,
environment: ViewEnvironment
) = Unit
FrameLayout(context).let { view ->
view.addView(progressBar)
ScreenViewHolder(initialEnvironment, view) { _, _ -> }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@ import com.squareup.sample.container.overviewdetail.OverviewDetailScreen
import com.squareup.sample.container.panel.ScrimScreen
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.container.BodyAndModalsScreen
import com.squareup.workflow1.ui.container.ModalScreenOverlay

@OptIn(WorkflowUiExperimentalApi::class)
typealias MayBeLoadingScreen = BodyAndModalsScreen<ScrimScreen<OverviewDetailScreen>, LoaderSpinner>
typealias MayBeLoadingScreen =
BodyAndModalsScreen<ScrimScreen<OverviewDetailScreen>, ModalScreenOverlay<LoaderSpinner>>

@OptIn(WorkflowUiExperimentalApi::class)
fun MayBeLoadingScreen(
baseScreen: OverviewDetailScreen,
loaders: List<LoaderSpinner> = emptyList()
): MayBeLoadingScreen {
return BodyAndModalsScreen(ScrimScreen(baseScreen, dimmed = loaders.isNotEmpty()), loaders)
return BodyAndModalsScreen(
ScrimScreen(baseScreen, dimmed = loaders.isNotEmpty()),
loaders.map { ModalScreenOverlay(it) }
)
}

@OptIn(WorkflowUiExperimentalApi::class)
val MayBeLoadingScreen.baseScreen: OverviewDetailScreen get() = body.content
val MayBeLoadingScreen.baseScreen: OverviewDetailScreen
get() = body.content

@OptIn(WorkflowUiExperimentalApi::class)
val MayBeLoadingScreen.loaders: List<LoaderSpinner> get() = modals
val MayBeLoadingScreen.loaders: List<LoaderSpinner>
get() = modals.map { it.content }

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ package com.squareup.sample.container.panel

import android.app.Dialog
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.util.TypedValue
import android.view.View
import com.squareup.sample.container.R
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.container.ModalScreenOverlayDialogFactory
import com.squareup.workflow1.ui.container.setModalContent

/**
* Android support for [PanelOverlay].
Expand All @@ -16,20 +15,13 @@ import com.squareup.workflow1.ui.container.ModalScreenOverlayDialogFactory
internal object PanelOverlayDialogFactory : ModalScreenOverlayDialogFactory<PanelOverlay<*>>(
type = PanelOverlay::class
) {
/**
* Forks the default implementation to apply [R.style.PanelDialog], for
* enter and exit animation.
*/
override fun buildDialogWithContentView(contentView: View): Dialog {
val context = contentView.context
return Dialog(context, R.style.PanelDialog).also { dialog ->
dialog.setContentView(contentView)

// 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)
if (
maybeWindowColor.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT
) {
dialog.window!!.setBackgroundDrawable(ColorDrawable(maybeWindowColor.data))
}
return Dialog(contentView.context, R.style.PanelDialog).also {
it.setModalContent(contentView)
}
}

Expand Down
14 changes: 10 additions & 4 deletions workflow-ui/core-android/api/core-android.api
Original file line number Diff line number Diff line change
Expand Up @@ -422,11 +422,13 @@ public final class com/squareup/workflow1/ui/container/AlertDialogThemeResId : c
public synthetic fun getDefault ()Ljava/lang/Object;
}

public final class com/squareup/workflow1/ui/container/AlertOverlayDialogFactory : com/squareup/workflow1/ui/container/OverlayDialogFactory {
public static final field INSTANCE Lcom/squareup/workflow1/ui/container/AlertOverlayDialogFactory;
public 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;)Landroid/app/AlertDialog;
public synthetic fun buildDialog (Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Landroid/app/Dialog;
public fun getType ()Lkotlin/reflect/KClass;
protected final fun toId (Lcom/squareup/workflow1/ui/container/AlertOverlay$Button;)I
protected final fun updateButtonsOnShow (Landroid/app/AlertDialog;Lcom/squareup/workflow1/ui/container/AlertOverlay;)V
public fun updateDialog (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/container/AlertOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
public synthetic fun updateDialog (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
}
Expand Down Expand Up @@ -614,17 +616,21 @@ public final class com/squareup/workflow1/ui/container/ModalAreaKt {
public static final fun plus (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/container/ModalArea;)Lcom/squareup/workflow1/ui/ViewEnvironment;
}

public abstract class com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory : com/squareup/workflow1/ui/container/OverlayDialogFactory {
public class com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory : com/squareup/workflow1/ui/container/OverlayDialogFactory {
public fun <init> (Lkotlin/reflect/KClass;)V
public synthetic fun buildDialog (Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Landroid/app/Dialog;
public final fun buildDialog (Lcom/squareup/workflow1/ui/container/ScreenOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Landroid/app/Dialog;
public abstract fun buildDialogWithContentView (Landroid/view/View;)Landroid/app/Dialog;
public fun buildDialogWithContentView (Landroid/view/View;)Landroid/app/Dialog;
public fun getType ()Lkotlin/reflect/KClass;
public fun updateBounds (Landroid/app/Dialog;Landroid/graphics/Rect;)V
public synthetic fun updateDialog (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
public final fun updateDialog (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/container/ScreenOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
}

public final class com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactoryKt {
public static final fun setModalContent (Landroid/app/Dialog;Landroid/view/View;)V
}

public abstract interface class com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed {
public static final field Companion Lcom/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed$Companion;
public abstract fun onBackPressed (Landroid/view/View;)Z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import com.squareup.workflow1.ui.container.EnvironmentScreenViewFactory
* [ViewEnvironment] service object used by [Screen.toViewFactory] to find the right
* [ScreenViewFactory] to build and manage a [View][android.view.View] to display
* [Screen]s of the type of the receiver. The default implementation makes [AndroidScreen]
* work and provides default bindings for [NamedScreen], [EnvironmentScreen], [BackStackScreen],
* work, and provides default bindings for [NamedScreen], [EnvironmentScreen], [BackStackScreen],
* etc.
*
* Here is how this hook could be used to provide a custom view to handle [BackStackScreen]:
Expand All @@ -28,24 +28,27 @@ import com.squareup.workflow1.ui.container.EnvironmentScreenViewFactory
* )
*
* object MyFinder : ScreenViewFactoryFinder {
* @Suppress("UNCHECKED_CAST")
* if (rendering is BackStackScreen<*>)
* return MyViewFactory as ScreenViewFactory<ScreenT>
* return super.getViewFactoryForRendering(environment, rendering)
* override fun <ScreenT : Screen> getViewFactoryForRendering(
* environment: ViewEnvironment,
* rendering: ScreenT
* ): ScreenViewFactory<ScreenT> {
* @Suppress("UNCHECKED_CAST")
* if (rendering is BackStackScreen<*>) return MyViewFactory as ScreenViewFactory<ScreenT>
* return super.getViewFactoryForRendering(environment, rendering)
* }
* }
*
* class MyViewModel(savedState: SavedStateHandle) : ViewModel() {
* val renderings: StateFlow<MyRootRendering> by lazy {
* val customized = ViewEnvironment.EMPTY + (ScreenViewFactoryFinder to MyFinder)
* val env = ViewEnvironment.EMPTY + (ScreenViewFactoryFinder to MyFinder)
* renderWorkflowIn(
* workflow = MyRootWorkflow.withEnvironment(customized),
* workflow = MyRootWorkflow.mapRenderings { it.withEnvironment(env) },
* scope = viewModelScope,
* savedStateHandle = savedState
* )
* }
* }
*/

@WorkflowUiExperimentalApi
public interface ScreenViewFactoryFinder {
public fun <ScreenT : Screen> getViewFactoryForRendering(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,19 @@ import com.squareup.workflow1.ui.container.AlertOverlay.Button.NEUTRAL
import com.squareup.workflow1.ui.container.AlertOverlay.Button.POSITIVE
import com.squareup.workflow1.ui.container.AlertOverlay.Event.ButtonClicked
import com.squareup.workflow1.ui.container.AlertOverlay.Event.Canceled
import kotlin.reflect.KClass

/**
* Default [OverlayDialogFactory] for [AlertOverlay].
*
* This class is non-final for ease of customization of [AlertOverlay] handling,
* see [OverlayDialogFactoryFinder] for details.
*/
@WorkflowUiExperimentalApi
internal object AlertOverlayDialogFactory : OverlayDialogFactory<AlertOverlay> {
override val type = AlertOverlay::class
public open class AlertOverlayDialogFactory : OverlayDialogFactory<AlertOverlay> {
override val type: KClass<AlertOverlay> = AlertOverlay::class

override fun buildDialog(
open override fun buildDialog(
initialRendering: AlertOverlay,
initialEnvironment: ViewEnvironment,
context: Context
Expand Down Expand Up @@ -48,7 +55,7 @@ internal object AlertOverlayDialogFactory : OverlayDialogFactory<AlertOverlay> {
}
}

override fun updateDialog(
open override fun updateDialog(
dialog: Dialog,
rendering: AlertOverlay,
environment: ViewEnvironment
Expand All @@ -71,13 +78,13 @@ internal object AlertOverlayDialogFactory : OverlayDialogFactory<AlertOverlay> {
}
}

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

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

for (button in Button.values()) getButton(button.toId()).visibility = GONE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ 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
import android.view.KeyEvent.ACTION_UP
import android.view.KeyEvent.KEYCODE_BACK
Expand All @@ -22,30 +24,42 @@ import com.squareup.workflow1.ui.toViewFactory
import kotlin.reflect.KClass

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

/**
* Called from [buildDialog]. Builds (but does not show) the [Dialog] to
* display a [contentView] built for a [ScreenOverlay.content].
*
* Custom implementations are not required to call `super`.
*
* Default implementation calls [Dialog.setModalContent].
*/
public abstract fun buildDialogWithContentView(contentView: View): Dialog
public open fun buildDialogWithContentView(contentView: View): Dialog {
return Dialog(contentView.context).also { it.setModalContent(contentView) }
}

/**
* If the [ScreenOverlay] displayed by a [dialog] created by this
* factory is contained in a [BodyAndModalsScreen], this method will
* be called to report the bounds of the managing view. It is expected
* that such a dialog will be restricted to those bounds.
* be called to report the bounds of the managing view, as reported by [ModalArea].
* It is expected that such a dialog will 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
Expand Down Expand Up @@ -125,3 +139,30 @@ public abstract class ModalScreenOverlayDialogFactory<O : ScreenOverlay<*>>(
)
}
}

/**
* The default implementation of [ModalScreenOverlayDialogFactory.buildDialogWithContentView].
*
* 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.)
*/
@OptIn(WorkflowUiExperimentalApi::class)
public fun Dialog.setModalContent(contentView: View) {
setCancelable(false)
setContentView(contentView)

// 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
}
window!!.setBackgroundDrawable(background)
}
Loading