-
Notifications
You must be signed in to change notification settings - Fork 101
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
Introduces ScreenOverlay and ModalScreenOverlayDialogFactory #595
Conversation
eed7a6a
to
0bb52cb
Compare
...low-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidDialogBounds.kt
Outdated
Show resolved
Hide resolved
...low-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidDialogBounds.kt
Outdated
Show resolved
Hide resolved
...low-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidDialogBounds.kt
Outdated
Show resolved
Hide resolved
...low-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidDialogBounds.kt
Outdated
Show resolved
Hide resolved
...iners/android/src/main/java/com/squareup/sample/container/panel/PanelOverlayDialogFactory.kt
Outdated
Show resolved
Hide resolved
0bb52cb
to
eb8f205
Compare
.../core-android/src/main/java/com/squareup/workflow1/ui/container/AlertOverlayDialogFactory.kt
Outdated
Show resolved
Hide resolved
ce0b3be
to
a1e8335
Compare
I think I actually need to address that in this PR. Panel is a pretty high fidelity |
Or maybe the thing to do is undelete the old PanelContainer and get this merged, since the PR is already too big. Then follow up with updating all the other samples that use the old modal stuff. |
a1e8335
to
032d5a8
Compare
Green enough, failure is due to ##590. |
...iners/android/src/main/java/com/squareup/sample/container/panel/PanelOverlayDialogFactory.kt
Show resolved
Hide resolved
samples/containers/android/src/main/java/com/squareup/sample/container/SampleContainers.kt
Show resolved
Hide resolved
...core-android/src/main/java/com/squareup/workflow1/ui/container/ScreenOverlayOnBackPressed.kt
Outdated
Show resolved
Hide resolved
workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/Bounds.kt
Outdated
Show resolved
Hide resolved
032d5a8
to
15d629d
Compare
15d629d
to
061c609
Compare
Update deletes some more unused stuff. |
val typedValue = TypedValue() | ||
context.theme.resolveAttribute(android.R.attr.windowBackground, typedValue, true) | ||
if (typedValue.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT) { | ||
dialog.window!!.setBackgroundDrawable(ColorDrawable(typedValue.data)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😵 . Is this a common thing we do? Could use a why comment for me at least.
Also could the val be maybeWindowBackgroundColour
not typedValue
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sample code, so don't worry too much. And I'm afraid it's pretty common Android boilerplate -- I'd even argue idiomatic. Compose can't come fast enough.
But yes, that's a better name, and a comment can help. Not inclined to write a dissertation, though, b/c it's just how Android life is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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))
}
...iners/android/src/main/java/com/squareup/sample/container/panel/PanelOverlayDialogFactory.kt
Show resolved
Hide resolved
samples/containers/common/src/main/java/com/squareup/sample/container/panel/ScrimScreen.kt
Outdated
Show resolved
Hide resolved
data class RunGameRendering( | ||
val gameScreen: Screen, | ||
val namePrompt: ScreenOverlay<*>? = null, | ||
val alerts: List<AlertOverlay> = emptyList() | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️
samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt
Outdated
Show resolved
Hide resolved
|
||
override fun onDetachedFromWindow() { | ||
job?.cancel() | ||
job = null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should use the job
and then leak the scope until callbacks are cleared off the window. Was there a reason you went this way?
Why not just create the scope onAttach
and use that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because I'm still really bad at Coroutines. Thanks.
Note that we use the same technique in WorkflowLayout.takeWhileAttached
, will fix that too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this look right?
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) }
.launchIn(it)
}
}
override fun onDetachedFromWindow() {
scope?.cancel()
scope = null
}
}
private val boundsListener = OnGlobalLayoutListener { | ||
getGlobalVisibleRect(boundsRect) | ||
if (!boundsRect.matches(bounds.value)) bounds.value = boundsRect.toBounds() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here we are just extracting the 'Global Visible Rect' right? - what does that represent?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's the bounds of the view in global coordinates -- the screen's coordinate system, not the parent view's. But I just noticed that this method returns false "If the view is completely clipped or translated out". I imagine the contents of the Rect
are nonsense in that case.
Not even sure what to do if that happens. Probably should close all the dialogs, treat it like onDetach
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. Sounds good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// The bounds of this view in global (display) coordinates, as reported
// by getGlobalVisibleRect.
//
// Made available to managed ModalScreenOverlayDialogFactory instances
// via the ModalArea key in ViewEnvironment. When this updates their
// updateBounds methods will fire. They should resize themselves to
// avoid covering peers of this view.
private val bounds = MutableStateFlow(Rect())
private val boundsRect = Rect()
private val boundsListener = OnGlobalLayoutListener {
if (getGlobalVisibleRect(boundsRect) && boundsRect != bounds.value) {
bounds.value = Rect(boundsRect)
}
// Should we close the dialogs if getGlobalVisibleRect returns false?
// https://github.com/square/workflow-kotlin/issues/599
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that I punted handling a false
return in any interesting way. We've been shipping the equivalent code for more than a year and it's not been an issue. I'd like to dig into that case separately and carefully -- see if it's actually an issue at all.
workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/Bounds.kt
Outdated
Show resolved
Hide resolved
workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/DialogHolder.kt
Show resolved
Hide resolved
...android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory.kt
Outdated
Show resolved
Hide resolved
c7d84cf
to
727928c
Compare
Updated to honor the feedback from @steve-the-edwards, especially around eliminating Also notice the updated commit message, and improved kdoc on PTAL |
f1fa90d
to
1092624
Compare
@steve-the-edwards Oops, I was too quick to ask PTAL, I missed some of your comments. Will ping again soon. |
Actually, okay to review. Tracking the most interesting thing I missed separately, #600. |
`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`. `ModalScreenOverlayDialogFactory` works with `BodyAndModalsContainer` and `LayeredDialogs` to perform two important tricks: - `updateBounds(Dialog, Rect)`, which allows dialogs to be restrictred to a subset of the screen. Implemented via the private `ModalArea` value in `ViewEnvironment` - Touch and keyboard events are blocked by views that are covered by dialogs managed by a `ModalScreenOverlayDialogFactory`. Same for dialogs with a lower z order. We use another private `ViewEnvironment` value for that, `CoveredByModal`. 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
1092624
to
b455dde
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome!
?: TakeTurnsProps.newGame(renderState.playerInfo) | ||
) { stopPlaying(it) } | ||
|
||
RunGameRendering(takeTurnsScreen) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I was suggesting to name the arg here still
RunGameRendering(takeTurnsScreen) | |
RunGameRendering(gameScreen = takeTurnsScreen) |
but its not a big deal.
// 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 handers 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh - now I understand. This is very helpful.
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) } | ||
.launchIn(it) | ||
} | ||
} | ||
|
||
override fun onDetachedFromWindow() { | ||
scope?.cancel() | ||
scope = null | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏻 .
Other than the injected immediate main dispatcher - but I see thats covered by #600 .
If we want debugging help we could add a name into the context for the scope.
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) } | |
.launchIn(it) | |
} | |
} | |
override fun onDetachedFromWindow() { | |
scope?.cancel() | |
scope = null | |
} | |
} | |
window.callback = object : Window.Callback by window.callback { | |
var scope: CoroutineScope? = null | |
override fun onAttachedToWindow() { | |
val id = window.decorView.id | |
scope = CoroutineScope(Dispatchers.Main.immediate + CoroutineName(id)).also { | |
bounds.onEach { b -> onBoundsChange(this@maintainBounds, b) } | |
.launchIn(it) | |
} | |
} | |
override fun onDetachedFromWindow() { | |
scope?.cancel() | |
scope = null | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the CoroutineName()
idea a lot. I think we can do better than the id, though, those things are so opaque. Remember that I snuck the content view into a R.id.workflow_modal_dialog_content
on the DecorView
. If that's present I can use the classname of its rendering, say. And if it's not, maybe the class name of the dialog itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/classname/toString()/, 10x more useful. I was shying away from that since it can be big, and that burned us in workflowAction {}
. But I think that's okay for infrequent events like creating a dialog.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Meh, I'm over engineering this.
Gonna merge as is and add a note to #600 to consider useful scope name at the same time.
ScreenOverlay
marks anOverlay
with aScreen
defining its content.ModalScreenOverlayDialogFactory
can show aScreenOverlay
as anandroid.app.Dialog
, taking care of updating the content view and managingthe Back button. It's the much simpler replacement for
ModalViewContainer
.ModalScreenOverlayDialogFactory
works withBodyAndModalsContainer
and
LayeredDialogs
to perform two important tricks:updateBounds(Dialog, Rect)
, which allows dialogs to be restrictred to a subsetof the screen. Implemented via the private
ModalArea
value inViewEnvironment
Touch and keyboard events are blocked by views that are covered by
dialogs managed by a
ModalScreenOverlayDialogFactory
. Same fordialogs with a lower z order. We use another private
ViewEnvironment
value for that,
CoveredByModal
.In sample code,
PanelOverlay
replacesPanelContainerScreen
, and the TicTac 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 oldcode 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