Skip to content

Commit 3cec6d2

Browse files
authored
Merge pull request #913 from square/ray/better-modals
Drastically simplifies ModalViewContainer.
2 parents 6f5809a + 4ff8867 commit 3cec6d2

File tree

25 files changed

+406
-211
lines changed

25 files changed

+406
-211
lines changed

kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/SampleContainers.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ package com.squareup.sample.container
1717

1818
import com.squareup.sample.container.masterdetail.MasterDetailContainer
1919
import com.squareup.sample.container.panel.PanelContainer
20+
import com.squareup.sample.container.panel.ScrimContainer
2021
import com.squareup.workflow.ui.ViewRegistry
2122

22-
val SampleContainers = ViewRegistry(MasterDetailContainer, PanelContainer, BackButtonScreen.Binding)
23+
val SampleContainers = ViewRegistry(
24+
BackButtonScreen.Binding, MasterDetailContainer, PanelContainer, ScrimContainer
25+
)

kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/panel/Contexts.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import android.view.Display
2020
import android.view.WindowManager
2121
import com.squareup.sample.container.R
2222

23+
val Context.isPortrait: Boolean get() = resources.getBoolean(R.bool.is_portrait)
24+
2325
val Context.isTablet: Boolean get() = resources.getBoolean(R.bool.is_tablet)
2426

2527
val Context.windowManager: WindowManager

kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelContainer.kt

Lines changed: 48 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -15,78 +15,70 @@
1515
*/
1616
package com.squareup.sample.container.panel
1717

18+
import android.app.Dialog
1819
import android.content.Context
1920
import android.graphics.drawable.ColorDrawable
2021
import android.util.AttributeSet
2122
import android.util.DisplayMetrics
2223
import android.util.TypedValue
23-
import android.view.View.MeasureSpec.EXACTLY
24-
import android.view.View.MeasureSpec.makeMeasureSpec
25-
import android.widget.FrameLayout
24+
import android.view.View
25+
import android.view.ViewGroup
26+
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
2627
import com.squareup.sample.container.R
27-
import com.squareup.workflow.ui.ModalContainer
28+
import com.squareup.workflow.ui.BuilderBinding
29+
import com.squareup.workflow.ui.ModalViewContainer
2830
import com.squareup.workflow.ui.ViewBinding
29-
import kotlin.math.min
31+
import com.squareup.workflow.ui.bindShowRendering
3032

3133
/**
3234
* Used by Tic Tac Workflow sample to show its [PanelContainerScreen]s.
33-
*
34-
* [ModalContainer.forContainerScreen] does most of the heavy lifting. We give
35-
* it a `modalDecorator` that wraps the given views in one that sizes itself
36-
* based on the screen size. The result looks suspiciously like the modal
37-
* flow container in Square PoS.
38-
*/
39-
object PanelContainer : ViewBinding<PanelContainerScreen<*, *>>
40-
by ModalContainer.forContainerScreen(
41-
R.id.panel_container,
42-
// This theme defines custom enter and exit animation styles for panel windows.
43-
dialogThemeResId = R.style.PanelDialog,
44-
modalDecorator = { panelBody ->
45-
PanelBodyWrapper(panelBody.context)
46-
.apply { addView(panelBody) }
47-
})
48-
49-
/**
50-
* [FrameLayout] that calculates its size based on the screen size -- to fill the screen on
51-
* phones, or make a square based on the shorter screen dimension on tablets. Handy
52-
* for showing a `Dialog` window that is set to `WRAP_CONTENT`, like those created by
53-
* [ModalContainer.forContainerScreen].
35+
* Extends [ModalViewContainer] to make the dialog square on Tablets, and
36+
* give it an opaque background.
5437
*/
55-
internal class PanelBodyWrapper
56-
@JvmOverloads constructor(
38+
class PanelContainer @JvmOverloads constructor(
5739
context: Context,
58-
attributeSet: AttributeSet? = null
59-
) : FrameLayout(context, attributeSet) {
60-
init {
61-
val typedValue = TypedValue()
62-
context.theme.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
63-
if (typedValue.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT) {
64-
@Suppress("DEPRECATION")
65-
background = ColorDrawable(typedValue.data)
66-
}
67-
}
40+
attributeSet: AttributeSet? = null,
41+
defStyle: Int = 0,
42+
defStyleRes: Int = 0
43+
) : ModalViewContainer(context, attributeSet, defStyle, defStyleRes) {
44+
override fun buildDialogForView(view: View): Dialog {
45+
return Dialog(context, R.style.PanelDialog).also { dialog ->
46+
dialog.setContentView(view)
6847

69-
/** For use only by [onMeasure]. Instantiated here to avoid allocation during measure. */
70-
private val displayMetrics = DisplayMetrics()
48+
val typedValue = TypedValue()
49+
context.theme.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
50+
if (typedValue.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT) {
51+
dialog.window!!.setBackgroundDrawable(ColorDrawable(typedValue.data))
52+
}
7153

72-
override fun onMeasure(
73-
widthMeasureSpec: Int,
74-
heightMeasureSpec: Int
75-
) {
76-
context.display.getMetrics(displayMetrics)
77-
val calculatedWidthSpec: Int
78-
val calculatedHeightSpec: Int
54+
// Use setLayout to control window size. Note that it must be
55+
// called after setContentView.
56+
//
57+
// Default layout values are MATCH_PARENT in both dimens, which is
58+
// perfect for phone.
7959

80-
if (context.isTablet) {
81-
val size = min(displayMetrics.widthPixels, displayMetrics.heightPixels)
60+
if (context.isTablet) {
61+
val displayMetrics = DisplayMetrics().also {
62+
dialog.context.display.getMetrics(it)
63+
}
8264

83-
calculatedWidthSpec = makeMeasureSpec(size, EXACTLY)
84-
calculatedHeightSpec = makeMeasureSpec(size, EXACTLY)
85-
} else {
86-
calculatedWidthSpec = makeMeasureSpec(displayMetrics.widthPixels, EXACTLY)
87-
calculatedHeightSpec = makeMeasureSpec(displayMetrics.heightPixels, EXACTLY)
65+
if (context.isPortrait) {
66+
dialog.window!!.setLayout(displayMetrics.widthPixels, displayMetrics.widthPixels)
67+
} else {
68+
dialog.window!!.setLayout(displayMetrics.heightPixels, displayMetrics.heightPixels)
69+
}
70+
}
8871
}
89-
90-
super.onMeasure(calculatedWidthSpec, calculatedHeightSpec)
9172
}
73+
74+
companion object : ViewBinding<PanelContainerScreen<*, *>> by BuilderBinding(
75+
type = PanelContainerScreen::class,
76+
viewConstructor = { initialRendering, initialHints, contextForNewView, _ ->
77+
PanelContainer(contextForNewView).apply {
78+
id = R.id.panel_container
79+
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
80+
bindShowRendering(initialRendering, initialHints, ::update)
81+
}
82+
}
83+
)
9284
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2020 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.squareup.sample.container.panel
17+
import android.animation.ValueAnimator
18+
import android.content.Context
19+
import android.util.AttributeSet
20+
import android.view.View
21+
import android.view.ViewGroup
22+
import com.squareup.sample.container.R
23+
import com.squareup.workflow.ui.BuilderBinding
24+
import com.squareup.workflow.ui.ViewBinding
25+
import com.squareup.workflow.ui.WorkflowViewStub
26+
import com.squareup.workflow.ui.bindShowRendering
27+
28+
/**
29+
* A view that renders only its first child, behind a smoke scrim if
30+
* [isDimmed] is true (tablets only). Other children are ignored.
31+
*
32+
* Able to [render][com.squareup.workflow.ui.showRendering] [ScrimContainerScreen].
33+
*/
34+
class ScrimContainer @JvmOverloads constructor(
35+
context: Context,
36+
attributeSet: AttributeSet? = null,
37+
defStyle: Int = 0,
38+
defStyleRes: Int = 0
39+
) : ViewGroup(context, attributeSet, defStyle, defStyleRes) {
40+
private val scrim = object : View(context, attributeSet, defStyle, defStyleRes) {
41+
init {
42+
@Suppress("DEPRECATION")
43+
setBackgroundColor(resources.getColor(R.color.scrim))
44+
}
45+
}
46+
47+
private val child: View
48+
get() = getChildAt(0)
49+
?: error("Child must be set immediately upon creation.")
50+
51+
var isDimmed: Boolean = false
52+
set(value) {
53+
if (field == value) return
54+
field = value
55+
if (!isAttachedToWindow) updateImmediate() else updateAnimated()
56+
}
57+
58+
override fun onAttachedToWindow() {
59+
updateImmediate()
60+
super.onAttachedToWindow()
61+
}
62+
63+
override fun addView(child: View?) {
64+
if (scrim.parent != null) removeView(scrim)
65+
super.addView(child)
66+
super.addView(scrim)
67+
}
68+
69+
override fun onLayout(
70+
changed: Boolean,
71+
l: Int,
72+
t: Int,
73+
r: Int,
74+
b: Int
75+
) {
76+
child.layout(0, 0, measuredWidth, measuredHeight)
77+
scrim.layout(0, 0, measuredWidth, measuredHeight)
78+
}
79+
80+
override fun onMeasure(
81+
widthMeasureSpec: Int,
82+
heightMeasureSpec: Int
83+
) {
84+
child.measure(widthMeasureSpec, heightMeasureSpec)
85+
scrim.measure(widthMeasureSpec, heightMeasureSpec)
86+
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
87+
}
88+
89+
private fun updateImmediate() {
90+
if (isDimmed) scrim.alpha = 1f else scrim.alpha = 0f
91+
}
92+
93+
private fun updateAnimated() {
94+
if (isDimmed) {
95+
ValueAnimator.ofFloat(0f, 1f)
96+
} else {
97+
ValueAnimator.ofFloat(1f, 0f)
98+
}.apply {
99+
duration = resources.getInteger(android.R.integer.config_shortAnimTime)
100+
.toLong()
101+
addUpdateListener { animation -> scrim.alpha = animation.animatedValue as Float }
102+
start()
103+
}
104+
}
105+
106+
companion object : ViewBinding<ScrimContainerScreen<*>> by BuilderBinding(
107+
type = ScrimContainerScreen::class,
108+
viewConstructor = { initialRendering, initialContainerHints, contextForNewView, _ ->
109+
val stub = WorkflowViewStub(contextForNewView)
110+
111+
ScrimContainer(contextForNewView)
112+
.apply {
113+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
114+
addView(stub)
115+
116+
bindShowRendering(initialRendering, initialContainerHints) { rendering, hints ->
117+
stub.update(rendering.wrapped, hints)
118+
isDimmed = rendering.dimmed
119+
}
120+
}
121+
}
122+
)
123+
}

kotlin/samples/containers/android/src/main/res/anim-sw600dp/panel_enter.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
-->
1717
<set xmlns:android="http://schemas.android.com/apk/res/android"
1818
android:interpolator="@android:anim/decelerate_interpolator"
19-
android:duration="@android:integer/config_mediumAnimTime"
19+
android:duration="@android:integer/config_shortAnimTime"
2020
>
2121

2222
<scale

kotlin/samples/containers/android/src/main/res/anim-sw600dp/panel_exit.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
-->
1717
<set xmlns:android="http://schemas.android.com/apk/res/android"
1818
android:interpolator="@android:anim/accelerate_interpolator"
19-
android:duration="@android:integer/config_mediumAnimTime"
19+
android:duration="@android:integer/config_shortAnimTime"
2020
>
2121

2222
<scale

kotlin/samples/containers/android/src/main/res/anim/panel_enter.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
~ limitations under the License.
1616
-->
1717
<translate xmlns:android="http://schemas.android.com/apk/res/android"
18-
android:duration="@android:integer/config_mediumAnimTime"
18+
android:duration="@android:integer/config_shortAnimTime"
1919
android:fromYDelta="100%p"
2020
android:interpolator="@android:anim/decelerate_interpolator"
2121
android:toYDelta="0"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
~ Copyright 2020 Square Inc.
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ http://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
<resources>
18+
<bool name="is_portrait">false</bool>
19+
</resources>

kotlin/samples/containers/android/src/main/res/values/bools.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@
1515
~ limitations under the License.
1616
-->
1717
<resources>
18+
<bool name="is_portrait">true</bool>
1819
<bool name="is_tablet">false</bool>
1920
</resources>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
~ Copyright 2020 Square Inc.
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ http://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
<resources>
18+
<color name="scrim">#cc000000</color>
19+
</resources>

0 commit comments

Comments
 (0)