Skip to content

[Jetchat] Update to Material 3 #690

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

Merged
merged 9 commits into from
Oct 27, 2021
18 changes: 3 additions & 15 deletions Jetchat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ This sample showcases:
* Text Input and focus management
* Multiple types of animations and transitions
* Saved state across configuration changes
* Basic Material Design theming
* Material Design 3 theming and Material You dynamic color
* UI tests

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

### Basic Material Design theming
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:

[UserInput](app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt)
```kotlin
@Composable
fun getSelectorExpandedColor(): Color {
return if (MaterialTheme.colors.isLight) {
MaterialTheme.colors.compositedOnSurface(0.04f)
} else {
MaterialTheme.colors.elevatedSurface(8.dp)
}
}
```
### Material Design 3 theming and Material You dynamic color
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.

### UI tests
In [androidTest](app/src/androidTest/java/com/example/compose/jetchat) you'll find a suite of UI tests that showcase interesting patterns in Compose:
Expand Down
4 changes: 3 additions & 1 deletion Jetchat/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,12 @@ dependencies {
implementation Libs.AndroidX.Lifecycle.viewModelCompose
implementation Libs.AndroidX.Navigation.fragment
implementation Libs.AndroidX.Navigation.uiKtx
implementation Libs.material
implementation Libs.material3

implementation Libs.AndroidX.Compose.layout
// TODO (M3): Remove this dependency when all components are available
implementation Libs.AndroidX.Compose.material
implementation Libs.AndroidX.Compose.Material3.material3
implementation Libs.AndroidX.Compose.materialIconsExtended
implementation Libs.AndroidX.Compose.tooling
implementation Libs.AndroidX.Compose.uiUtil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material.rememberScaffoldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberScaffoldState
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
Expand All @@ -44,6 +45,7 @@ import kotlinx.coroutines.launch
class NavActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()

@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@

package com.example.compose.jetchat

import androidx.compose.material.AlertDialog
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable

@Composable
Expand All @@ -29,7 +29,7 @@ fun FunctionalityNotAvailablePopup(onDismiss: () -> Unit) {
text = {
Text(
text = "Functionality not available \uD83D\uDE48",
style = MaterialTheme.typography.body2
style = MaterialTheme.typography.bodyMedium
)
},
confirmButton = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,63 +16,58 @@

package com.example.compose.jetchat.components

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.compose.jetchat.R
import com.example.compose.jetchat.theme.JetchatTheme
import com.example.compose.jetchat.theme.elevatedSurface

@Composable
fun JetchatAppBar(
modifier: Modifier = Modifier,
scrollBehavior: TopAppBarScrollBehavior? = null,
onNavIconPressed: () -> Unit = { },
title: @Composable RowScope.() -> Unit,
title: @Composable () -> Unit,
actions: @Composable RowScope.() -> Unit = {}
) {
// This bar is translucent but elevation overlays are not applied to translucent colors.
// Instead we manually calculate the elevated surface color from the opaque color,
// then apply our alpha.
//
// We set the background on the Column rather than the TopAppBar,
// so that the background is drawn behind any padding set on the app bar (i.e. status bar).
val backgroundColor = MaterialTheme.colors.elevatedSurface(3.dp)
Column(
Modifier.background(backgroundColor.copy(alpha = 0.95f))
) {
TopAppBar(
val backgroundColors = TopAppBarDefaults.centerAlignedTopAppBarColors()
val backgroundColor = backgroundColors.containerColor(
scrollFraction = scrollBehavior?.scrollFraction ?: 0f
).value
val foregroundColors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent
)
Box(modifier = Modifier.background(backgroundColor)) {
CenterAlignedTopAppBar(
modifier = modifier,
backgroundColor = Color.Transparent,
elevation = 0.dp, // No shadow needed
contentColor = MaterialTheme.colors.onSurface,
actions = actions,
title = { Row { title() } },
title = title,
scrollBehavior = scrollBehavior,
colors = foregroundColors,
navigationIcon = {
Image(
painter = painterResource(id = R.drawable.ic_jetchat),
JetchatIcon(
contentDescription = stringResource(id = R.string.navigation_drawer_open),
modifier = Modifier
.size(64.dp)
.clickable(onClick = onNavIconPressed)
.padding(horizontal = 16.dp)
.padding(16.dp)
)
}
)
Divider()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,24 @@ import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment.Companion.CenterStart
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
Expand All @@ -58,10 +57,11 @@ fun ColumnScope.JetchatDrawer(onProfileClicked: (String) -> Unit, onChatClicked:
// below the status bar (y-axis)
Spacer(Modifier.statusBarsHeight())
DrawerHeader()
Divider()
DividerItem()
DrawerItemHeader("Chats")
ChatItem("composers", true) { onChatClicked("composers") }
ChatItem("droidcon-nyc", false) { onChatClicked("droidcon-nyc") }
DividerItem(modifier = Modifier.padding(horizontal = 28.dp))
DrawerItemHeader("Recent Profiles")
ProfileItem("Ali Conors (you)", meProfile.photo) { onProfileClicked(meProfile.userId) }
ProfileItem("Taylor Brooks", colleagueProfile.photo) {
Expand All @@ -72,8 +72,7 @@ fun ColumnScope.JetchatDrawer(onProfileClicked: (String) -> Unit, onChatClicked:
@Composable
private fun DrawerHeader() {
Row(modifier = Modifier.padding(16.dp), verticalAlignment = CenterVertically) {
Image(
painter = painterResource(id = R.drawable.ic_jetchat),
JetchatIcon(
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Expand All @@ -86,78 +85,103 @@ private fun DrawerHeader() {
}
@Composable
private fun DrawerItemHeader(text: String) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text(text, style = MaterialTheme.typography.caption, modifier = Modifier.padding(16.dp))
Box(
modifier = Modifier
.heightIn(min = 52.dp)
.padding(horizontal = 28.dp),
contentAlignment = CenterStart
) {
Text(
text,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}

@Composable
private fun ChatItem(text: String, selected: Boolean, onChatClicked: () -> Unit) {
val background = if (selected) {
Modifier.background(MaterialTheme.colors.primary.copy(alpha = 0.08f))
Modifier.background(MaterialTheme.colorScheme.primaryContainer)
} else {
Modifier
}
Row(
modifier = Modifier
.height(48.dp)
.height(56.dp)
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp)
.padding(horizontal = 12.dp)
.clip(CircleShape)
.then(background)
.clip(MaterialTheme.shapes.medium)
.clickable(onClick = onChatClicked),
verticalAlignment = CenterVertically
) {
val iconTint = if (selected) {
MaterialTheme.colors.primary
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium)
MaterialTheme.colorScheme.onSurfaceVariant
}
Icon(
painter = painterResource(id = R.drawable.ic_jetchat),
tint = iconTint,
modifier = Modifier.padding(8.dp),
modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 16.dp),
contentDescription = null
)
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text(
text,
style = MaterialTheme.typography.body2,
color = if (selected) MaterialTheme.colors.primary else LocalContentColor.current,
modifier = Modifier.padding(8.dp)
)
}
Text(
text,
style = MaterialTheme.typography.bodyMedium,
color = if (selected) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurface
},
modifier = Modifier.padding(start = 12.dp)
)
}
}

@Composable
private fun ProfileItem(text: String, @DrawableRes profilePic: Int?, onProfileClicked: () -> Unit) {
Row(
modifier = Modifier
.height(48.dp)
.height(56.dp)
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp)
.clip(MaterialTheme.shapes.medium)
.padding(horizontal = 12.dp)
.clip(CircleShape)
.clickable(onClick = onProfileClicked),
verticalAlignment = CenterVertically
) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
val widthPaddingModifier = Modifier.padding(8.dp).size(24.dp)
if (profilePic != null) {
Image(
painter = painterResource(id = profilePic),
modifier = widthPaddingModifier.then(Modifier.clip(CircleShape)),
contentScale = ContentScale.Crop,
contentDescription = null
)
} else {
Spacer(modifier = widthPaddingModifier)
}
Text(text, style = MaterialTheme.typography.body2, modifier = Modifier.padding(8.dp))
val paddingSizeModifier = Modifier
.padding(start = 16.dp, top = 16.dp, bottom = 16.dp)
.size(24.dp)
if (profilePic != null) {
Image(
painter = painterResource(id = profilePic),
modifier = paddingSizeModifier.then(Modifier.clip(CircleShape)),
contentScale = ContentScale.Crop,
contentDescription = null
)
} else {
Spacer(modifier = paddingSizeModifier)
}
Text(
text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(start = 12.dp)
)
}
}

@Composable
fun DividerItem(modifier: Modifier = Modifier) {
// TODO (M3): No Divider, replace when available
Divider(
modifier = modifier,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
)
}

@Composable
@Preview
fun DrawerPreview() {
Expand Down
Loading