Skip to content

Commit 032d5a8

Browse files
committed
Introduces ScreenOverlay and ModalScreenOverlayDialogFactory
`ScreenOverlay` marks an `Overlay` with a `Screen` defining its content. `ModalScreenOverlayDialogFactory` can show a `ScreenOverlay` as an `android.app.Dialog`, taking care of updating the content view and managing the Back button. It's the much simpler replacement for `ModalViewContainer`. Also introduces two new `ViewEnvironment` values: - `ModalArea`, which allows dialogs to restrict themselves to a subset of the screen. `ModalScreenOverlayDialogFactory` applies this automatically - `CoveredByModal`, used by `BodyAndModalsContainer` and `LayeredDialogs` to disable touch and keyboard events reliably In sample code, `PanelOverlay` replaces `PanelContainerScreen`, and the Tic Tac Toe sample is updated to use the new hotness. I think the diff of the sample code really highlights how much the new marker interfaces improve our composition story. That said, the `ScrimScreen` bit in the sample is a little rough. In the old code we magically swizzled the scrim into place automatically. This PR seems big enough already, so I'll follow up with a clean up that does something similar and deletes all the deprecated sample code. Fixes #259, #138, #99, #204, #314, #589
1 parent 45ece5a commit 032d5a8

File tree

30 files changed

+997
-282
lines changed

30 files changed

+997
-282
lines changed
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
package com.squareup.sample.container
22

33
import com.squareup.sample.container.overviewdetail.OverviewDetailContainer
4-
import com.squareup.sample.container.panel.PanelContainer
5-
import com.squareup.sample.container.panel.ScrimContainer
6-
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
4+
import com.squareup.sample.container.panel.PanelOverlayDialogFactory
75
import com.squareup.workflow1.ui.ViewRegistry
6+
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
87

98
@OptIn(WorkflowUiExperimentalApi::class)
109
val SampleContainers = ViewRegistry(
11-
BackButtonViewFactory, OverviewDetailContainer, PanelContainer, ScrimContainer
10+
BackButtonViewFactory, OverviewDetailContainer, PanelOverlayDialogFactory, ScrimContainer
1211
)
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package com.squareup.sample.container
2+
3+
import android.animation.ValueAnimator
4+
import android.content.Context
5+
import android.util.AttributeSet
6+
import android.view.View
7+
import android.view.ViewGroup
8+
import androidx.core.content.ContextCompat
9+
import com.squareup.sample.container.panel.ScrimScreen
10+
import com.squareup.workflow1.ui.ManualScreenViewFactory
11+
import com.squareup.workflow1.ui.ScreenViewFactory
12+
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
13+
import com.squareup.workflow1.ui.WorkflowViewStub
14+
import com.squareup.workflow1.ui.bindShowRendering
15+
16+
/**
17+
* A view that renders only its first child, behind a smoke scrim if
18+
* [isDimmed] is true (tablets only). Other children are ignored.
19+
*
20+
* Able to [render][com.squareup.workflow1.ui.showRendering] [ScrimScreen].
21+
*/
22+
internal class ScrimContainer @JvmOverloads constructor(
23+
context: Context,
24+
attributeSet: AttributeSet? = null,
25+
defStyle: Int = 0,
26+
defStyleRes: Int = 0
27+
) : ViewGroup(context, attributeSet, defStyle, defStyleRes) {
28+
private val scrim = object : View(context, attributeSet, defStyle, defStyleRes) {
29+
init {
30+
setBackgroundColor(ContextCompat.getColor(context, R.color.scrim))
31+
}
32+
}
33+
34+
private val child: View
35+
get() = getChildAt(0)
36+
?: error("Child must be set immediately upon creation.")
37+
38+
var isDimmed: Boolean = false
39+
set(value) {
40+
if (field == value) return
41+
field = value
42+
if (!isAttachedToWindow) updateImmediate() else updateAnimated()
43+
}
44+
45+
override fun onAttachedToWindow() {
46+
updateImmediate()
47+
super.onAttachedToWindow()
48+
}
49+
50+
override fun addView(child: View?) {
51+
if (scrim.parent != null) removeView(scrim)
52+
super.addView(child)
53+
super.addView(scrim)
54+
}
55+
56+
override fun onLayout(
57+
changed: Boolean,
58+
l: Int,
59+
t: Int,
60+
r: Int,
61+
b: Int
62+
) {
63+
child.layout(0, 0, measuredWidth, measuredHeight)
64+
scrim.layout(0, 0, measuredWidth, measuredHeight)
65+
}
66+
67+
override fun onMeasure(
68+
widthMeasureSpec: Int,
69+
heightMeasureSpec: Int
70+
) {
71+
child.measure(widthMeasureSpec, heightMeasureSpec)
72+
scrim.measure(widthMeasureSpec, heightMeasureSpec)
73+
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
74+
}
75+
76+
private fun updateImmediate() {
77+
if (isDimmed) scrim.alpha = 1f else scrim.alpha = 0f
78+
}
79+
80+
private fun updateAnimated() {
81+
if (isDimmed) {
82+
ValueAnimator.ofFloat(0f, 1f)
83+
} else {
84+
ValueAnimator.ofFloat(1f, 0f)
85+
}.apply {
86+
duration = resources.getInteger(android.R.integer.config_shortAnimTime)
87+
.toLong()
88+
addUpdateListener { animation -> scrim.alpha = animation.animatedValue as Float }
89+
start()
90+
}
91+
}
92+
93+
@OptIn(WorkflowUiExperimentalApi::class)
94+
companion object : ScreenViewFactory<ScrimScreen<*>> by ManualScreenViewFactory(
95+
type = ScrimScreen::class,
96+
viewConstructor = { initialRendering, initialViewEnvironment, contextForNewView, _ ->
97+
val stub = WorkflowViewStub(contextForNewView)
98+
99+
ScrimContainer(contextForNewView)
100+
.also { view ->
101+
view.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
102+
view.addView(stub)
103+
104+
view.bindShowRendering(
105+
initialRendering, initialViewEnvironment
106+
) { rendering, environment ->
107+
stub.show(rendering.content, environment)
108+
view.isDimmed = rendering.dimmed
109+
}
110+
}
111+
}
112+
)
113+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.squareup.sample.container.panel
2+
3+
import android.app.Dialog
4+
import android.graphics.drawable.ColorDrawable
5+
import android.util.TypedValue
6+
import android.view.View
7+
import com.squareup.sample.container.R
8+
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
9+
import com.squareup.workflow1.ui.container.Bounds
10+
import com.squareup.workflow1.ui.container.ModalScreenOverlayDialogFactory
11+
import com.squareup.workflow1.ui.container.setBounds
12+
13+
@OptIn(WorkflowUiExperimentalApi::class)
14+
internal object PanelOverlayDialogFactory : ModalScreenOverlayDialogFactory<PanelOverlay<*>>(
15+
type = PanelOverlay::class
16+
) {
17+
override fun buildDialogWithContentView(contentView: View): Dialog {
18+
val context = contentView.context
19+
return Dialog(context, R.style.PanelDialog).also { dialog ->
20+
dialog.setContentView(contentView)
21+
22+
val typedValue = TypedValue()
23+
context.theme.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
24+
if (typedValue.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT) {
25+
dialog.window!!.setBackgroundDrawable(ColorDrawable(typedValue.data))
26+
}
27+
}
28+
}
29+
30+
override fun updateBounds(
31+
dialog: Dialog,
32+
bounds: Bounds
33+
) {
34+
val refinedBounds: Bounds = if (!dialog.context.isTablet) {
35+
// On a phone, fill the bounds entirely.
36+
bounds
37+
} else {
38+
if (bounds.height > bounds.width) {
39+
val margin = bounds.height - bounds.width
40+
val topDelta = margin / 2
41+
val bottomDelta = margin - topDelta
42+
bounds.copy(top = bounds.top + topDelta, bottom = bounds.bottom - bottomDelta)
43+
} else {
44+
val margin = bounds.width - bounds.height
45+
val leftDelta = margin / 2
46+
val rightDelta = margin - leftDelta
47+
bounds.copy(left = bounds.left + leftDelta, right = bounds.right - rightDelta)
48+
}
49+
}
50+
dialog.setBounds(refinedBounds)
51+
}
52+
}

samples/containers/common/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ plugins {
55

66
dependencies {
77
implementation(project(":workflow-ui:container-common"))
8+
implementation(project(":workflow-ui:core-android"))
89
implementation(project(":workflow-core"))
910

1011
implementation(Dependencies.Kotlin.Stdlib.jdk6)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.squareup.sample.container.panel
2+
3+
import com.squareup.workflow1.ui.Screen
4+
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
5+
import com.squareup.workflow1.ui.container.ScreenOverlay
6+
7+
@OptIn(WorkflowUiExperimentalApi::class)
8+
class PanelOverlay<T : Screen>(
9+
override val content: T
10+
) : ScreenOverlay<T>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.squareup.sample.container.panel
2+
3+
import com.squareup.workflow1.ui.Compatible
4+
import com.squareup.workflow1.ui.Compatible.Companion.keyFor
5+
import com.squareup.workflow1.ui.Screen
6+
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
7+
8+
/**
9+
* Show a scrim over some [content], which is invisible if [dimmed] is false,
10+
* dark if it is true.
11+
*/
12+
@OptIn(WorkflowUiExperimentalApi::class)
13+
class ScrimScreen<T : Screen>(
14+
val content: T,
15+
val dimmed: Boolean
16+
) : Screen, Compatible {
17+
override val compatibilityKey = keyFor(content, "ScrimScreen")
18+
}

0 commit comments

Comments
 (0)