Skip to content

Commit 686b550

Browse files
author
Nick Rout
authored
Merge pull request #690 from ricknout/jetchat-material3
[Jetchat] Update to Material 3
2 parents 59c5b82 + 3bb93bf commit 686b550

26 files changed

Lines changed: 704 additions & 470 deletions

File tree

Jetchat/README.md

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ This sample showcases:
1818
* Text Input and focus management
1919
* Multiple types of animations and transitions
2020
* Saved state across configuration changes
21-
* Basic Material Design theming
21+
* Material Design 3 theming and Material You dynamic color
2222
* UI tests
2323

2424
<img src="screenshots/jetchat.gif"/>
@@ -58,20 +58,8 @@ The sample uses the
5858
### Saved state across configuration changes
5959
Some composable state survives activity or process recreation, like `currentInputSelector` in [UserInput](app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt).
6060

61-
### Basic Material Design theming
62-
Jetchat follows the Material Design principles and uses the `MaterialTheme` ambient, with custom light and dark themes. In some cases colors it might be necessary to create additional colors, that can be specified as an overlay or combination of two, or as a specific elevation in dark mode. Jetchat uses some convenient extensions on the Material palette and can be used as follows:
63-
64-
[UserInput](app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt)
65-
```kotlin
66-
@Composable
67-
fun getSelectorExpandedColor(): Color {
68-
return if (MaterialTheme.colors.isLight) {
69-
MaterialTheme.colors.compositedOnSurface(0.04f)
70-
} else {
71-
MaterialTheme.colors.elevatedSurface(8.dp)
72-
}
73-
}
74-
```
61+
### Material Design 3 theming and Material You dynamic color
62+
Jetchat follows the [Material Design 3](https://m3.material.io) principles and uses the `MaterialTheme` composable and M3 components. On Android 12+ Jetchat supports Material You dynamic color, which extracts a custom color scheme from the device wallpaper. Jetchat uses a custom, branded color scheme as a fallback. It also implements custom typography using the Karla and Montserrat font families.
7563

7664
### UI tests
7765
In [androidTest](app/src/androidTest/java/com/example/compose/jetchat) you'll find a suite of UI tests that showcase interesting patterns in Compose:

Jetchat/app/build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,12 @@ dependencies {
100100
implementation Libs.AndroidX.Lifecycle.viewModelCompose
101101
implementation Libs.AndroidX.Navigation.fragment
102102
implementation Libs.AndroidX.Navigation.uiKtx
103-
implementation Libs.material
103+
implementation Libs.material3
104104

105105
implementation Libs.AndroidX.Compose.layout
106+
// TODO (M3): Remove this dependency when all components are available
106107
implementation Libs.AndroidX.Compose.material
108+
implementation Libs.AndroidX.Compose.Material3.material3
107109
implementation Libs.AndroidX.Compose.materialIconsExtended
108110
implementation Libs.AndroidX.Compose.tooling
109111
implementation Libs.AndroidX.Compose.uiUtil

Jetchat/app/src/main/java/com/example/compose/jetchat/NavActivity.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import android.os.Bundle
2020
import androidx.activity.compose.setContent
2121
import androidx.activity.viewModels
2222
import androidx.appcompat.app.AppCompatActivity
23-
import androidx.compose.material.rememberScaffoldState
23+
import androidx.compose.material3.ExperimentalMaterial3Api
24+
import androidx.compose.material3.rememberScaffoldState
2425
import androidx.compose.runtime.CompositionLocalProvider
2526
import androidx.compose.runtime.LaunchedEffect
2627
import androidx.compose.runtime.collectAsState
@@ -44,6 +45,7 @@ import kotlinx.coroutines.launch
4445
class NavActivity : AppCompatActivity() {
4546
private val viewModel: MainViewModel by viewModels()
4647

48+
@OptIn(ExperimentalMaterial3Api::class)
4749
override fun onCreate(savedInstanceState: Bundle?) {
4850
super.onCreate(savedInstanceState)
4951

Jetchat/app/src/main/java/com/example/compose/jetchat/UiExtras.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616

1717
package com.example.compose.jetchat
1818

19-
import androidx.compose.material.AlertDialog
20-
import androidx.compose.material.MaterialTheme
21-
import androidx.compose.material.Text
22-
import androidx.compose.material.TextButton
19+
import androidx.compose.material3.AlertDialog
20+
import androidx.compose.material3.MaterialTheme
21+
import androidx.compose.material3.Text
22+
import androidx.compose.material3.TextButton
2323
import androidx.compose.runtime.Composable
2424

2525
@Composable
@@ -29,7 +29,7 @@ fun FunctionalityNotAvailablePopup(onDismiss: () -> Unit) {
2929
text = {
3030
Text(
3131
text = "Functionality not available \uD83D\uDE48",
32-
style = MaterialTheme.typography.body2
32+
style = MaterialTheme.typography.bodyMedium
3333
)
3434
},
3535
confirmButton = {

Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatAppBar.kt

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,63 +16,58 @@
1616

1717
package com.example.compose.jetchat.components
1818

19-
import androidx.compose.foundation.Image
2019
import androidx.compose.foundation.background
2120
import androidx.compose.foundation.clickable
22-
import androidx.compose.foundation.layout.Column
23-
import androidx.compose.foundation.layout.Row
21+
import androidx.compose.foundation.layout.Box
2422
import androidx.compose.foundation.layout.RowScope
2523
import androidx.compose.foundation.layout.padding
26-
import androidx.compose.material.Divider
27-
import androidx.compose.material.MaterialTheme
28-
import androidx.compose.material.Text
29-
import androidx.compose.material.TopAppBar
24+
import androidx.compose.foundation.layout.size
25+
import androidx.compose.material3.CenterAlignedTopAppBar
26+
import androidx.compose.material3.Text
27+
import androidx.compose.material3.TopAppBarDefaults
28+
import androidx.compose.material3.TopAppBarScrollBehavior
3029
import androidx.compose.runtime.Composable
3130
import androidx.compose.ui.Modifier
3231
import androidx.compose.ui.graphics.Color
33-
import androidx.compose.ui.res.painterResource
3432
import androidx.compose.ui.res.stringResource
3533
import androidx.compose.ui.tooling.preview.Preview
3634
import androidx.compose.ui.unit.dp
3735
import com.example.compose.jetchat.R
3836
import com.example.compose.jetchat.theme.JetchatTheme
39-
import com.example.compose.jetchat.theme.elevatedSurface
4037

4138
@Composable
4239
fun JetchatAppBar(
4340
modifier: Modifier = Modifier,
41+
scrollBehavior: TopAppBarScrollBehavior? = null,
4442
onNavIconPressed: () -> Unit = { },
45-
title: @Composable RowScope.() -> Unit,
43+
title: @Composable () -> Unit,
4644
actions: @Composable RowScope.() -> Unit = {}
4745
) {
48-
// This bar is translucent but elevation overlays are not applied to translucent colors.
49-
// Instead we manually calculate the elevated surface color from the opaque color,
50-
// then apply our alpha.
51-
//
52-
// We set the background on the Column rather than the TopAppBar,
53-
// so that the background is drawn behind any padding set on the app bar (i.e. status bar).
54-
val backgroundColor = MaterialTheme.colors.elevatedSurface(3.dp)
55-
Column(
56-
Modifier.background(backgroundColor.copy(alpha = 0.95f))
57-
) {
58-
TopAppBar(
46+
val backgroundColors = TopAppBarDefaults.centerAlignedTopAppBarColors()
47+
val backgroundColor = backgroundColors.containerColor(
48+
scrollFraction = scrollBehavior?.scrollFraction ?: 0f
49+
).value
50+
val foregroundColors = TopAppBarDefaults.centerAlignedTopAppBarColors(
51+
containerColor = Color.Transparent,
52+
scrolledContainerColor = Color.Transparent
53+
)
54+
Box(modifier = Modifier.background(backgroundColor)) {
55+
CenterAlignedTopAppBar(
5956
modifier = modifier,
60-
backgroundColor = Color.Transparent,
61-
elevation = 0.dp, // No shadow needed
62-
contentColor = MaterialTheme.colors.onSurface,
6357
actions = actions,
64-
title = { Row { title() } },
58+
title = title,
59+
scrollBehavior = scrollBehavior,
60+
colors = foregroundColors,
6561
navigationIcon = {
66-
Image(
67-
painter = painterResource(id = R.drawable.ic_jetchat),
62+
JetchatIcon(
6863
contentDescription = stringResource(id = R.string.navigation_drawer_open),
6964
modifier = Modifier
65+
.size(64.dp)
7066
.clickable(onClick = onNavIconPressed)
71-
.padding(horizontal = 16.dp)
67+
.padding(16.dp)
7268
)
7369
}
7470
)
75-
Divider()
7671
}
7772
}
7873

Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatDrawer.kt

Lines changed: 68 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,24 @@ import androidx.annotation.DrawableRes
2020
import androidx.compose.foundation.Image
2121
import androidx.compose.foundation.background
2222
import androidx.compose.foundation.clickable
23+
import androidx.compose.foundation.layout.Box
2324
import androidx.compose.foundation.layout.Column
2425
import androidx.compose.foundation.layout.ColumnScope
2526
import androidx.compose.foundation.layout.Row
2627
import androidx.compose.foundation.layout.Spacer
2728
import androidx.compose.foundation.layout.fillMaxWidth
2829
import androidx.compose.foundation.layout.height
30+
import androidx.compose.foundation.layout.heightIn
2931
import androidx.compose.foundation.layout.padding
3032
import androidx.compose.foundation.layout.size
3133
import androidx.compose.foundation.shape.CircleShape
32-
import androidx.compose.material.ContentAlpha
3334
import androidx.compose.material.Divider
34-
import androidx.compose.material.Icon
35-
import androidx.compose.material.LocalContentAlpha
36-
import androidx.compose.material.LocalContentColor
37-
import androidx.compose.material.MaterialTheme
38-
import androidx.compose.material.Surface
39-
import androidx.compose.material.Text
35+
import androidx.compose.material3.Icon
36+
import androidx.compose.material3.MaterialTheme
37+
import androidx.compose.material3.Surface
38+
import androidx.compose.material3.Text
4039
import androidx.compose.runtime.Composable
41-
import androidx.compose.runtime.CompositionLocalProvider
40+
import androidx.compose.ui.Alignment.Companion.CenterStart
4241
import androidx.compose.ui.Alignment.Companion.CenterVertically
4342
import androidx.compose.ui.Modifier
4443
import androidx.compose.ui.draw.clip
@@ -58,10 +57,11 @@ fun ColumnScope.JetchatDrawer(onProfileClicked: (String) -> Unit, onChatClicked:
5857
// below the status bar (y-axis)
5958
Spacer(Modifier.statusBarsHeight())
6059
DrawerHeader()
61-
Divider()
60+
DividerItem()
6261
DrawerItemHeader("Chats")
6362
ChatItem("composers", true) { onChatClicked("composers") }
6463
ChatItem("droidcon-nyc", false) { onChatClicked("droidcon-nyc") }
64+
DividerItem(modifier = Modifier.padding(horizontal = 28.dp))
6565
DrawerItemHeader("Recent Profiles")
6666
ProfileItem("Ali Conors (you)", meProfile.photo) { onProfileClicked(meProfile.userId) }
6767
ProfileItem("Taylor Brooks", colleagueProfile.photo) {
@@ -72,8 +72,7 @@ fun ColumnScope.JetchatDrawer(onProfileClicked: (String) -> Unit, onChatClicked:
7272
@Composable
7373
private fun DrawerHeader() {
7474
Row(modifier = Modifier.padding(16.dp), verticalAlignment = CenterVertically) {
75-
Image(
76-
painter = painterResource(id = R.drawable.ic_jetchat),
75+
JetchatIcon(
7776
contentDescription = null,
7877
modifier = Modifier.size(24.dp)
7978
)
@@ -86,78 +85,103 @@ private fun DrawerHeader() {
8685
}
8786
@Composable
8887
private fun DrawerItemHeader(text: String) {
89-
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
90-
Text(text, style = MaterialTheme.typography.caption, modifier = Modifier.padding(16.dp))
88+
Box(
89+
modifier = Modifier
90+
.heightIn(min = 52.dp)
91+
.padding(horizontal = 28.dp),
92+
contentAlignment = CenterStart
93+
) {
94+
Text(
95+
text,
96+
style = MaterialTheme.typography.bodySmall,
97+
color = MaterialTheme.colorScheme.onSurfaceVariant
98+
)
9199
}
92100
}
93101

94102
@Composable
95103
private fun ChatItem(text: String, selected: Boolean, onChatClicked: () -> Unit) {
96104
val background = if (selected) {
97-
Modifier.background(MaterialTheme.colors.primary.copy(alpha = 0.08f))
105+
Modifier.background(MaterialTheme.colorScheme.primaryContainer)
98106
} else {
99107
Modifier
100108
}
101109
Row(
102110
modifier = Modifier
103-
.height(48.dp)
111+
.height(56.dp)
104112
.fillMaxWidth()
105-
.padding(horizontal = 8.dp, vertical = 4.dp)
113+
.padding(horizontal = 12.dp)
114+
.clip(CircleShape)
106115
.then(background)
107-
.clip(MaterialTheme.shapes.medium)
108116
.clickable(onClick = onChatClicked),
109117
verticalAlignment = CenterVertically
110118
) {
111119
val iconTint = if (selected) {
112-
MaterialTheme.colors.primary
120+
MaterialTheme.colorScheme.primary
113121
} else {
114-
MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium)
122+
MaterialTheme.colorScheme.onSurfaceVariant
115123
}
116124
Icon(
117125
painter = painterResource(id = R.drawable.ic_jetchat),
118126
tint = iconTint,
119-
modifier = Modifier.padding(8.dp),
127+
modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 16.dp),
120128
contentDescription = null
121129
)
122-
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
123-
Text(
124-
text,
125-
style = MaterialTheme.typography.body2,
126-
color = if (selected) MaterialTheme.colors.primary else LocalContentColor.current,
127-
modifier = Modifier.padding(8.dp)
128-
)
129-
}
130+
Text(
131+
text,
132+
style = MaterialTheme.typography.bodyMedium,
133+
color = if (selected) {
134+
MaterialTheme.colorScheme.primary
135+
} else {
136+
MaterialTheme.colorScheme.onSurface
137+
},
138+
modifier = Modifier.padding(start = 12.dp)
139+
)
130140
}
131141
}
132142

133143
@Composable
134144
private fun ProfileItem(text: String, @DrawableRes profilePic: Int?, onProfileClicked: () -> Unit) {
135145
Row(
136146
modifier = Modifier
137-
.height(48.dp)
147+
.height(56.dp)
138148
.fillMaxWidth()
139-
.padding(horizontal = 8.dp, vertical = 4.dp)
140-
.clip(MaterialTheme.shapes.medium)
149+
.padding(horizontal = 12.dp)
150+
.clip(CircleShape)
141151
.clickable(onClick = onProfileClicked),
142152
verticalAlignment = CenterVertically
143153
) {
144-
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
145-
val widthPaddingModifier = Modifier.padding(8.dp).size(24.dp)
146-
if (profilePic != null) {
147-
Image(
148-
painter = painterResource(id = profilePic),
149-
modifier = widthPaddingModifier.then(Modifier.clip(CircleShape)),
150-
contentScale = ContentScale.Crop,
151-
contentDescription = null
152-
)
153-
} else {
154-
Spacer(modifier = widthPaddingModifier)
155-
}
156-
Text(text, style = MaterialTheme.typography.body2, modifier = Modifier.padding(8.dp))
154+
val paddingSizeModifier = Modifier
155+
.padding(start = 16.dp, top = 16.dp, bottom = 16.dp)
156+
.size(24.dp)
157+
if (profilePic != null) {
158+
Image(
159+
painter = painterResource(id = profilePic),
160+
modifier = paddingSizeModifier.then(Modifier.clip(CircleShape)),
161+
contentScale = ContentScale.Crop,
162+
contentDescription = null
163+
)
164+
} else {
165+
Spacer(modifier = paddingSizeModifier)
157166
}
167+
Text(
168+
text,
169+
style = MaterialTheme.typography.bodyMedium,
170+
color = MaterialTheme.colorScheme.onSurface,
171+
modifier = Modifier.padding(start = 12.dp)
172+
)
158173
}
159174
}
160175

176+
@Composable
177+
fun DividerItem(modifier: Modifier = Modifier) {
178+
// TODO (M3): No Divider, replace when available
179+
Divider(
180+
modifier = modifier,
181+
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
182+
)
183+
}
184+
161185
@Composable
162186
@Preview
163187
fun DrawerPreview() {

0 commit comments

Comments
 (0)