Skip to content

Commit d71b50a

Browse files
authored
Merge pull request #4398 from CruGlobal/circuitBanners
GT-2584: Migrate Banners to Circuit Presenter/UI pattern
2 parents 85940b9 + dfc83c4 commit d71b50a

30 files changed

Lines changed: 679 additions & 318 deletions

File tree

Lines changed: 5 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -1,169 +1,11 @@
11
package org.cru.godtools.ui.banner
22

3-
import androidx.compose.foundation.layout.Column
4-
import androidx.compose.foundation.layout.fillMaxWidth
5-
import androidx.compose.foundation.layout.heightIn
6-
import androidx.compose.material3.HorizontalDivider
7-
import androidx.compose.material3.Icon
8-
import androidx.compose.material3.LocalContentColor
9-
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
10-
import androidx.compose.material3.MaterialTheme
11-
import androidx.compose.material3.Surface
12-
import androidx.compose.material3.Text
13-
import androidx.compose.material3.TextButton
14-
import androidx.compose.runtime.Composable
15-
import androidx.compose.runtime.CompositionLocalProvider
16-
import androidx.compose.ui.Modifier
17-
import androidx.compose.ui.draw.alpha
18-
import androidx.compose.ui.graphics.Color
19-
import androidx.compose.ui.graphics.painter.Painter
20-
import androidx.compose.ui.layout.Layout
21-
import androidx.compose.ui.layout.layoutId
22-
import androidx.compose.ui.unit.Constraints
23-
import androidx.compose.ui.unit.Dp
24-
import androidx.compose.ui.unit.IntOffset
25-
import androidx.compose.ui.unit.constrain
26-
import androidx.compose.ui.unit.dp
27-
import androidx.compose.ui.unit.offset
3+
import com.slack.circuit.runtime.CircuitUiState
284

29-
@Composable
30-
internal fun Banner(
31-
text: String,
32-
primaryButton: String,
33-
modifier: Modifier = Modifier,
34-
primaryAction: () -> Unit = {},
35-
secondaryButton: String? = null,
36-
secondaryAction: () -> Unit = {},
37-
icon: Painter? = null,
38-
iconTint: Color = if (icon != null) LocalContentColor.current else Color.Unspecified,
39-
) = Surface(modifier = modifier.fillMaxWidth()) {
40-
Column {
41-
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) {
42-
val iconNode = "icon"
43-
val textNode = "text"
44-
val primaryActionNode = "primaryAction"
45-
val secondaryActionNode = "secondaryAction"
5+
object Banner {
6+
enum class Type { TOOL_LIST_FAVORITES, TUTORIAL_FEATURES }
467

47-
Layout({
48-
Text(text, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.layoutId(textNode))
49-
TextButton(
50-
onClick = primaryAction,
51-
modifier = Modifier
52-
.layoutId(primaryActionNode)
53-
.heightIn(min = 36.dp)
54-
) { Text(primaryButton) }
55-
if (secondaryButton != null) {
56-
TextButton(
57-
onClick = secondaryAction,
58-
modifier = Modifier
59-
.layoutId(secondaryActionNode)
60-
.heightIn(min = 36.dp)
61-
) { Text(secondaryButton) }
62-
}
63-
if (icon != null) {
64-
Icon(icon, contentDescription = null, tint = iconTint, modifier = Modifier.layoutId(iconNode))
65-
}
66-
}) { measurables, constraints ->
67-
require(constraints.hasBoundedWidth) { "Banner requires a bounded width" }
68-
69-
val textMargin = 16.dp.roundToPx()
70-
val actionMargin = 8.dp.roundToPx()
71-
val iconSize = 40.dp.roundToPx()
72-
73-
val iconPlaceable = measurables.firstOrNull { it.layoutId == iconNode }
74-
?.measure(constraints.constrain(Constraints.fixed(iconSize, iconSize)))
75-
val textPlaceable = measurables.first { it.layoutId == textNode }.measure(
76-
constraints
77-
.offset(horizontal = iconPlaceable?.let { 0 - textMargin - it.width } ?: 0)
78-
.offset(horizontal = -2 * textMargin)
79-
)
80-
val primaryActionPlaceable = measurables.first { it.layoutId == primaryActionNode }
81-
.measure(constraints.offset(horizontal = -2 * actionMargin))
82-
val secondaryActionPlaceable = measurables.firstOrNull { it.layoutId == secondaryActionNode }
83-
?.measure(constraints.offset(horizontal = -2 * actionMargin))
84-
85-
val bannerWidth = constraints.maxWidth
86-
87-
when {
88-
// single line layout
89-
iconPlaceable == null &&
90-
textMargin + textPlaceable.width + 36.dp.roundToPx() +
91-
primaryActionPlaceable.width + actionMargin +
92-
(secondaryActionPlaceable?.let { it.width + actionMargin } ?: 0) < bannerWidth -> {
93-
val bannerHeight = maxOf(
94-
textPlaceable.height,
95-
primaryActionPlaceable.height,
96-
secondaryActionPlaceable?.height ?: 0
97-
) + 10.dp.roundToPx() + actionMargin
98-
99-
// calculate placeable positions
100-
val centerLine = (bannerHeight + 2.dp.roundToPx()) / 2
101-
val primaryActionPosition = IntOffset(
102-
bannerWidth - actionMargin - primaryActionPlaceable.width,
103-
centerLine - (primaryActionPlaceable.height / 2)
104-
)
105-
val secondaryActionPosition = IntOffset(
106-
primaryActionPosition.x - actionMargin - (secondaryActionPlaceable?.width ?: 0),
107-
centerLine - ((secondaryActionPlaceable?.height ?: 0) / 2)
108-
)
109-
110-
layout(bannerWidth, bannerHeight) {
111-
textPlaceable.placeRelative(textMargin, centerLine - (textPlaceable.height / 2))
112-
primaryActionPlaceable.placeRelative(primaryActionPosition)
113-
secondaryActionPlaceable?.placeRelative(secondaryActionPosition)
114-
}
115-
}
116-
117-
// default layout
118-
else -> {
119-
val iconPosition = when (iconPlaceable) {
120-
null -> IntOffset.Zero
121-
else -> IntOffset(textMargin, textMargin)
122-
}
123-
val textPosition = when (iconPlaceable) {
124-
null -> IntOffset(textMargin, textMargin)
125-
else -> IntOffset(iconPosition.x + iconPlaceable.width + textMargin, textMargin)
126-
}
127-
val primaryActionPosition = IntOffset(
128-
bannerWidth - actionMargin - primaryActionPlaceable.width,
129-
maxOf(
130-
iconPlaceable?.let { iconPosition.y + it.height } ?: 0,
131-
textPosition.y + textPlaceable.height,
132-
) + 12.dp.roundToPx()
133-
)
134-
val secondaryActionPosition = when (secondaryActionPlaceable) {
135-
null -> IntOffset.Zero
136-
137-
else -> {
138-
val sameLinePosition = IntOffset(
139-
primaryActionPosition.x - actionMargin - secondaryActionPlaceable.width,
140-
primaryActionPosition.y
141-
)
142-
val nextLinePosition = IntOffset(
143-
bannerWidth - actionMargin - secondaryActionPlaceable.width,
144-
primaryActionPosition.y + primaryActionPlaceable.height + actionMargin
145-
)
146-
sameLinePosition.takeUnless { it.x < actionMargin } ?: nextLinePosition
147-
}
148-
}
149-
150-
val bannerHeight = maxOf(
151-
iconPlaceable?.let { iconPosition.y + it.height + textMargin } ?: 0,
152-
textPosition.y + textPlaceable.height + textMargin,
153-
primaryActionPosition.y + primaryActionPlaceable.height + actionMargin,
154-
secondaryActionPlaceable?.let { secondaryActionPosition.y + it.height + actionMargin } ?: 0,
155-
)
156-
157-
layout(bannerWidth, bannerHeight) {
158-
iconPlaceable?.placeRelative(iconPosition)
159-
textPlaceable.placeRelative(textPosition)
160-
primaryActionPlaceable.placeRelative(primaryActionPosition)
161-
secondaryActionPlaceable?.placeRelative(secondaryActionPosition)
162-
}
163-
}
164-
}
165-
}
166-
}
167-
HorizontalDivider(modifier = Modifier.alpha(0.12f))
8+
interface UiState : CircuitUiState {
9+
val type: Type
16810
}
16911
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.cru.godtools.ui.banner
2+
3+
import dagger.Binds
4+
import dagger.Module
5+
import dagger.hilt.InstallIn
6+
import dagger.hilt.components.SingletonComponent
7+
import org.cru.godtools.ui.banner.favoritetools.FavoriteToolsBannerPresenter
8+
import org.cru.godtools.ui.banner.tutorial.TutorialFeaturesBannerPresenter
9+
10+
@Module
11+
@InstallIn(SingletonComponent::class)
12+
interface BannerModule {
13+
@Binds
14+
fun favoriteToolsBannerPresenter(
15+
presenter: FavoriteToolsBannerPresenter,
16+
): BannerPresenter<FavoriteToolsBannerPresenter.UiState>
17+
18+
@Binds
19+
fun tutorialFeaturesBannerPresenter(
20+
presenter: TutorialFeaturesBannerPresenter,
21+
): BannerPresenter<TutorialFeaturesBannerPresenter.UiState>
22+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.cru.godtools.ui.banner
2+
3+
import androidx.compose.runtime.Composable
4+
5+
interface BannerPresenter<T : Banner.UiState> {
6+
@Composable
7+
fun present(): T?
8+
}

app/src/main/kotlin/org/cru/godtools/ui/banner/BannerType.kt

Lines changed: 0 additions & 3 deletions
This file was deleted.

app/src/main/kotlin/org/cru/godtools/ui/banner/Banners.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,24 @@ import androidx.compose.foundation.layout.heightIn
99
import androidx.compose.runtime.Composable
1010
import androidx.compose.ui.Modifier
1111
import androidx.compose.ui.unit.dp
12+
import org.cru.godtools.ui.banner.favoritetools.FavoriteToolsBannerLayout
13+
import org.cru.godtools.ui.banner.favoritetools.FavoriteToolsBannerPresenter
14+
import org.cru.godtools.ui.banner.tutorial.TutorialFeaturesBannerLayout
15+
import org.cru.godtools.ui.banner.tutorial.TutorialFeaturesBannerPresenter
1216

1317
@Composable
14-
internal fun Banners(banner: () -> BannerType?, modifier: Modifier = Modifier) = Box(modifier.heightIn(min = 1.dp)) {
18+
internal fun Banners(state: Banner.UiState?, modifier: Modifier = Modifier) = Box(modifier.heightIn(min = 1.dp)) {
1519
AnimatedContent(
16-
targetState = banner(),
20+
targetState = state,
1721
transitionSpec = {
1822
slideInVertically(initialOffsetY = { -it }) togetherWith slideOutVertically(targetOffsetY = { -it })
1923
},
2024
label = "Banner Visibility",
25+
contentKey = { it?.type },
2126
) {
2227
when (it) {
23-
BannerType.TOOL_LIST_FAVORITES -> FavoriteToolsBanner()
24-
BannerType.TUTORIAL_FEATURES -> TutorialFeaturesBanner()
28+
is FavoriteToolsBannerPresenter.UiState -> FavoriteToolsBannerLayout(it)
29+
is TutorialFeaturesBannerPresenter.UiState -> TutorialFeaturesBannerLayout(it)
2530
else -> Unit
2631
}
2732
}

app/src/main/kotlin/org/cru/godtools/ui/banner/FavoriteToolsBanner.kt

Lines changed: 0 additions & 32 deletions
This file was deleted.

app/src/main/kotlin/org/cru/godtools/ui/banner/Banner+Preview.kt renamed to app/src/main/kotlin/org/cru/godtools/ui/banner/MaterialBanner+Preview.kt

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,31 @@ import org.cru.godtools.R
99

1010
@Composable
1111
@Preview(showBackground = true)
12-
private fun SingleLineBanner() = Banner("Single Line", primaryButton = "Confirm", secondaryButton = "Dismiss")
12+
private fun SingleLineMaterialBanner() = MaterialBanner(
13+
"Single Line",
14+
primaryButton = "Confirm",
15+
secondaryButton = "Dismiss"
16+
)
1317

1418
@Composable
1519
@Preview(showBackground = true)
16-
private fun DefaultBanner() = Banner(
20+
private fun DefaultMaterialBanner() = MaterialBanner(
1721
LoremIpsum(20).values.first().replace("\n", " "),
1822
primaryButton = "Confirm",
1923
secondaryButton = "Dismiss",
2024
)
2125

2226
@Composable
2327
@Preview(showBackground = true)
24-
private fun BannerWithActionsOnSeparateLines() = Banner(
28+
private fun MaterialBannerWithActionsOnSeparateLines() = MaterialBanner(
2529
"Short Message",
2630
primaryButton = "Confirm Primary Action",
2731
secondaryButton = LoremIpsum(7).values.first(),
2832
)
2933

3034
@Composable
3135
@Preview(showBackground = true)
32-
private fun BannerIcon() = Banner(
36+
private fun MaterialBannerIcon() = MaterialBanner(
3337
LoremIpsum(20).values.first().replace("\n", " "),
3438
primaryButton = "Confirm",
3539
secondaryButton = "Dismiss",
@@ -39,7 +43,7 @@ private fun BannerIcon() = Banner(
3943

4044
@Composable
4145
@Preview(showBackground = true)
42-
private fun BannerIconShortText() = Banner(
46+
private fun MaterialBannerIconShortText() = MaterialBanner(
4347
"Single Line",
4448
primaryButton = "Confirm",
4549
secondaryButton = "Dismiss",
@@ -49,7 +53,7 @@ private fun BannerIconShortText() = Banner(
4953

5054
@Composable
5155
@Preview(showBackground = true)
52-
private fun BannerIconShortTextLongActions() = Banner(
56+
private fun MaterialBannerIconShortTextLongActions() = MaterialBanner(
5357
"Single Line",
5458
primaryButton = "Confirm Primary Action",
5559
secondaryButton = "Dismiss Secondary Action",

0 commit comments

Comments
 (0)