Skip to content

Long lived branch with Overlay refinements #839

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 28, 2022
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
Expand Up @@ -16,6 +16,7 @@ import com.squareup.benchmarks.performance.complex.poetry.cyborgs.waitForPoetry
import com.squareup.benchmarks.performance.complex.poetry.instrumentation.RenderPassCountingInterceptor
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith

Expand Down Expand Up @@ -75,6 +76,7 @@ class RenderPassTest {
runRenderPassCounter(COMPLEX_NO_INITIALIZING, useFrameTimeout = true)
}

@Ignore("#841")
@Test fun renderPassCounterFrameTimeoutComplexNoInitializingStateHighFrequencyEvents() {
runRenderPassCounter(COMPLEX_NO_INITIALIZING_HIGH_FREQUENCY, useFrameTimeout = true)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand All @@ -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<Screen>): Dialog {
return Dialog(content.view.context, R.style.PanelDialog).also {
it.setContent(content)
}
}
override fun buildDialogWithContent(
initialRendering: PanelOverlay<Screen>,
initialEnvironment: ViewEnvironment,
content: ScreenViewHolder<Screen>
): OverlayDialogHolder<PanelOverlay<Screen>> {
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)
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -569,9 +568,6 @@ internal class ComposeViewTreeIntegrationTest {
override val dialogFactory = object : ScreenOverlayDialogFactory<Screen, TestModal>(
TestModal::class
) {
override fun buildDialogWithContent(content: ScreenViewHolder<Screen>): Dialog {
return Dialog(content.view.context).apply { setContentView(content.view) }
}
}
}

Expand Down
17 changes: 10 additions & 7 deletions workflow-ui/core-android/api/core-android.api
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,8 @@ public abstract interface class com/squareup/workflow1/ui/ViewStarter {
public final class com/squareup/workflow1/ui/WorkflowLayout : android/widget/FrameLayout {
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;)V
public synthetic fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun show (Lcom/squareup/workflow1/ui/Screen;)V
public final fun show (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
public static synthetic fun show$default (Lcom/squareup/workflow1/ui/WorkflowLayout;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;ILjava/lang/Object;)V
public final fun start (Landroidx/lifecycle/Lifecycle;Lkotlinx/coroutines/flow/Flow;Landroidx/lifecycle/Lifecycle$State;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
public final fun start (Landroidx/lifecycle/Lifecycle;Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow1/ui/ViewRegistry;)V
public final fun start (Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
Expand Down Expand Up @@ -436,8 +437,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
}

Expand Down Expand Up @@ -672,6 +673,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;
}

Expand All @@ -689,16 +691,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 <init> (Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/app/Dialog;Lkotlin/jvm/functions/Function2;)V
public fun <init> (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;
}

Expand All @@ -707,9 +711,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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,13 @@ internal class DialogIntegrationTest {
}

override fun buildDialogWithContent(
initialRendering: DialogRendering,
initialEnvironment: ViewEnvironment,
content: ScreenViewHolder<ContentRendering>
) = super.buildDialogWithContent(content).also { latestDialog = it }
): OverlayDialogHolder<DialogRendering> =
super.buildDialogWithContent(initialRendering, initialEnvironment, content).also {
latestDialog = it.dialog
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import androidx.lifecycle.Lifecycle.State
import androidx.lifecycle.Lifecycle.State.STARTED
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.repeatOnLifecycle
import com.squareup.workflow1.ui.container.EnvironmentScreen
import com.squareup.workflow1.ui.container.withEnvironment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
Expand All @@ -28,7 +26,9 @@ import kotlinx.coroutines.launch

/**
* A view that can be driven by a stream of [Screen] renderings passed to its [take] method.
* To configure the [ViewEnvironment] in play, use [EnvironmentScreen] as your root rendering type.
*
* Suitable for use as the content view of an [Activity][android.app.Activity.setContentView],
* or [Fragment][androidx.fragment.app.Fragment.onCreateView].
*
* [id][setId] defaults to [R.id.workflow_layout], as a convenience to ensure that
* view persistence will work without requiring authors to be immersed in Android arcana.
Expand Down Expand Up @@ -60,8 +60,11 @@ public class WorkflowLayout(
* [take] than to call this method directly. It is exposed to allow clients to
* make their own choices about how exactly to consume a stream of renderings.
*/
public fun show(rootScreen: Screen) {
showing.show(rootScreen, rootScreen.withEnvironment().environment)
public fun show(
rootScreen: Screen,
environment: ViewEnvironment = ViewEnvironment.EMPTY
) {
showing.show(rootScreen, environment)
restoredChildState?.let { restoredState ->
restoredChildState = null
showing.actual.restoreHierarchyState(restoredState)
Expand All @@ -72,6 +75,12 @@ public class WorkflowLayout(
* This is the most common way to bootstrap a [Workflow][com.squareup.workflow1.Workflow]
* driven UI. Collects [renderings] and calls [show] with each one.
*
* To configure a root [ViewEnvironment], use
* [EnvironmentScreen][com.squareup.workflow1.ui.container.EnvironmentScreen] as your
* root rendering type, perhaps via
* [withEnvironment][com.squareup.workflow1.ui.container.withEnvironment] or
* [withRegistry][com.squareup.workflow1.ui.container.withRegistry].
*
* @param [lifecycle] the lifecycle that defines when and how this view should be updated.
* Typically this comes from `ComponentActivity.lifecycle` or `Fragment.lifecycle`.
* @param [repeatOnLifecycle] the lifecycle state in which renderings should be actively
Expand All @@ -85,7 +94,7 @@ public class WorkflowLayout(
// Just like https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
lifecycle.coroutineScope.launch {
lifecycle.repeatOnLifecycle(repeatOnLifecycle) {
renderings.collect { show(it.withEnvironment()) }
renderings.collect { show(it) }
}
}
}
Expand Down Expand Up @@ -127,7 +136,7 @@ public class WorkflowLayout(
public fun start(
lifecycle: Lifecycle,
renderings: Flow<Any>,
repeatOnLifecycle: Lifecycle.State = Lifecycle.State.STARTED,
repeatOnLifecycle: State = STARTED,
environment: ViewEnvironment = ViewEnvironment.EMPTY
) {
// Just like https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ public open class AlertOverlayDialogFactory : OverlayDialogFactory<AlertOverlay>
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) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -37,23 +37,23 @@ public fun Dialog.setBounds(bounds: Rect) {
@WorkflowUiExperimentalApi
internal fun <D : Dialog> D.maintainBounds(
environment: ViewEnvironment,
onBoundsChange: (D, Rect) -> Unit
onBoundsChange: (Rect) -> Unit
) {
maintainBounds(environment[OverlayArea].bounds, onBoundsChange)
}

@WorkflowUiExperimentalApi
internal fun <D : Dialog> D.maintainBounds(
bounds: StateFlow<Rect>,
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 {
var scope: CoroutineScope? = null

override fun onAttachedToWindow() {
scope = CoroutineScope(Dispatchers.Main.immediate).also {
bounds.onEach { b -> onBoundsChange(this@maintainBounds, b) }
bounds.onEach { b -> onBoundsChange(b) }
.launchIn(it)
}
}
Expand All @@ -65,5 +65,5 @@ internal fun <D : Dialog> 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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ internal class DialogSession(
index: Int,
holder: OverlayDialogHolder<Overlay>
) {
// Note similar code in LayeredDialogs
// Note similar code in LayeredDialogSessions
private var allowEvents = true
set(value) {
val was = field
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ import kotlinx.coroutines.flow.StateFlow
* layouts if [BodyAndOverlaysScreen] or the default [BodyAndOverlaysContainer] bound
* to it are too restrictive.
*
* Provides an [allowEvents] field that reflects the presence or absence of Dialogs driven
* by [ModalOverlay], and makes [OverlayArea] available in the [ViewEnvironment],
* which in turn drives calls to [ScreenOverlayDialogFactory.updateBounds].
* - Provides an [allowEvents] field that reflects the presence or absence of Dialogs driven
* by [ModalOverlay]
*
* Provides a [ViewTreeLifecycleOwner] per managed Dialog, and view persistence support,
* both for classic [View.onSaveInstanceState] and
* Jetpack [SavedStateRegistry][androidx.savedstate.SavedStateRegistry].
* - Makes [OverlayArea] available in the [ViewEnvironment],
* and uses it to drive calls to [OverlayDialogHolder.onUpdateBounds].
*
* - Provides a [ViewTreeLifecycleOwner] per managed Dialog, and view persistence support,
* both for classic [View.onSaveInstanceState] and
* Jetpack [SavedStateRegistry][androidx.savedstate.SavedStateRegistry].
*
* ## Lifecycle of a managed [Dialog][android.app.Dialog]
*
Expand Down Expand Up @@ -72,7 +74,7 @@ import kotlinx.coroutines.flow.StateFlow
*
* @param bounds made available to managed dialogs via the [OverlayArea]
* [ViewEnvironmentKey][com.squareup.workflow1.ui.ViewEnvironmentKey],
* which drives [ScreenOverlayDialogFactory.updateBounds].
* which drives [OverlayDialogHolder.onUpdateBounds].
*
* @param cancelEvents function to be called when a modal session starts -- that is,
* when [update] is first called with a [ModalOverlay] member, or called again with
Expand Down Expand Up @@ -163,6 +165,9 @@ 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.
Expand Down Expand Up @@ -286,7 +291,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.
Expand Down
Loading