Skip to content

Instantly share code, notes, and snippets.

@Mikkareem
Last active December 11, 2023 14:18
Show Gist options
  • Save Mikkareem/8a58bc9a3a5518ff0c612395d90206e5 to your computer and use it in GitHub Desktop.
Save Mikkareem/8a58bc9a3a5518ff0c612395d90206e5 to your computer and use it in GitHub Desktop.
Chat app ui using Jetpack compose
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationEndReason
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowLeft
import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.techullurgy.composeuisapplication.R
import com.techullurgy.composeuisapplication.math.map
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
private val grayColor = Color(0xff8a95b3)
@Composable
fun ChatDetailsScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
TopBar()
Spacer(modifier = Modifier.height(12.dp))
DateText()
Spacer(modifier = Modifier.height(8.dp))
MessagesSection(
modifier = Modifier
.padding(horizontal = 8.dp)
.weight(1f),
messageDetails = listOf(
MessageDetail(
ownerType = OwnerType.Me,
message = "Hi, How are you?",
time = "Just now"
),
MessageDetail(
ownerType = OwnerType.Other,
message = "Hi, How are you?, Please recollect your knowledge to the next level",
time = "Just now"
),
MessageDetail(
ownerType = OwnerType.Other,
message = "Hi, How are you?",
time = "Just now"
),
MessageDetail(
ownerType = OwnerType.Me,
message = "Hi, How are you?",
time = "Just now"
),
MessageDetail(
ownerType = OwnerType.Me,
message = "Hi, How are you?",
time = "Just now"
),
MessageDetail(
ownerType = OwnerType.Me,
message = "please",
time = "Just now"
)
)
)
BottomBar(
modifier = Modifier.padding(8.dp)
)
}
}
@Composable
private fun MessagesSection(
messageDetails: List<MessageDetail>,
modifier: Modifier = Modifier
) {
Column(modifier = modifier, verticalArrangement = Arrangement.Bottom) {
for(i in messageDetails.indices) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = when(messageDetails[i].ownerType) {
is OwnerType.Me -> Arrangement.End
is OwnerType.Other -> Arrangement.Start
},
modifier = Modifier.fillMaxWidth()
) {
val canShowProfilePicture = (
(i == messageDetails.size - 1) ||
((i+1) < messageDetails.size &&
messageDetails[i].ownerType != messageDetails[i+1].ownerType)
)
when(messageDetails[i].ownerType) {
is OwnerType.Me -> {
MeMessage(
canShowProfilePicture = canShowProfilePicture,
messageDetail = messageDetails[i]
)
}
is OwnerType.Other -> {
OtherMessage(
canShowProfilePicture = canShowProfilePicture,
messageDetail = messageDetails[i]
)
}
}
}
Spacer(modifier = Modifier.height(4.dp))
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
modifier = Modifier.fillMaxWidth()
) {
val alpha = 0.4f
ProfilePicture(isOnline = true, color = Color.Magenta, alpha = alpha)
Spacer(modifier = Modifier.width(8.dp))
TypingIndicator(alpha = alpha)
}
}
}
@Composable
private fun MeMessage(
canShowProfilePicture: Boolean,
messageDetail: MessageDetail,
modifier: Modifier = Modifier
) {
Row(modifier = modifier) {
Spacer(modifier = Modifier.width(50.dp))
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) {
Box(
modifier = Modifier
.clip(
RoundedCornerShape(
topStartPercent = 35,
topEndPercent = 35,
bottomEndPercent = if (canShowProfilePicture) 0 else 35,
bottomStartPercent = 35
)
)
.background(Color.Black)
.padding(16.dp)
) {
Text(text = messageDetail.message, color = Color.White)
}
}
Spacer(modifier = Modifier.width(8.dp))
if(canShowProfilePicture) {
ProfilePicture(isOnline = true)
} else {
Spacer(modifier = Modifier.width(50.dp))
}
}
}
@Composable
private fun OtherMessage(
canShowProfilePicture: Boolean,
messageDetail: MessageDetail,
modifier: Modifier = Modifier
) {
Row(modifier = modifier) {
if(canShowProfilePicture) {
ProfilePicture(isOnline = true, color = Color.Magenta)
} else {
Spacer(modifier = Modifier.width(50.dp))
}
Spacer(modifier = Modifier.width(8.dp))
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterStart) {
Box(
modifier = Modifier
.clip(
RoundedCornerShape(
topStartPercent = 35,
topEndPercent = 35,
bottomEndPercent = 35,
bottomStartPercent = if (canShowProfilePicture) 0 else 35
)
)
.background(grayColor)
.padding(16.dp)
) {
Text(text = messageDetail.message)
}
}
Spacer(modifier = Modifier.width(50.dp))
}
}
@Composable
private fun TopBar() {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = object: Arrangement.Horizontal {
override fun Density.arrange(
totalSize: Int,
sizes: IntArray,
layoutDirection: LayoutDirection,
outPositions: IntArray
) {
outPositions[1] = 0
}
},
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(0, 0, 35, 35))
.background(grayColor)
.padding(vertical = 16.dp)
) {
IconButton(onClick = { /*TODO*/ }) {
Icon(imageVector = Icons.Default.KeyboardArrowLeft, contentDescription = null, modifier = Modifier.size(48.dp))
}
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
ProfilePicture(isOnline = true, color = Color.Magenta)
Spacer(modifier = Modifier.width(8.dp))
Text(text = "Jennie Davis", fontSize = 18.sp, fontWeight = FontWeight.Bold)
}
}
}
@Composable
private fun ProfilePicture(
isOnline: Boolean,
color: Color = Color.Black,
alpha: Float = 1f
) {
Box {
Box(
modifier = Modifier
.size(50.dp)
.clip(CircleShape)
.background(color.copy(alpha = alpha))
)
AnimatedVisibility(visible = isOnline, modifier = Modifier.align(Alignment.BottomEnd)) {
Box(
modifier = Modifier
.size(15.dp)
.clip(CircleShape)
.background(Color.Green.copy(alpha = alpha))
.border(2.dp, Color.White, CircleShape)
)
}
}
}
@Composable
private fun DateText(
dateStr: String = "12 MARCH 2023"
) {
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Text(text = dateStr, color = grayColor, textAlign = TextAlign.Center)
}
}
@Composable
private fun BottomBar(
modifier: Modifier = Modifier
) {
Row(modifier = modifier) {
var messageValue by remember { mutableStateOf(" e") }
TextField(
value = messageValue,
onValueChange = { messageValue = it },
textStyle = LocalTextStyle.current.copy(fontSize = 20.sp),
shape = RoundedCornerShape(50),
placeholder = {
Text(text = "Enter Message...")
},
trailingIcon = {
Crossfade(targetState = messageValue.isNotBlank(), label = "") {
if(it) {
IconButton(
modifier = Modifier
.clip(CircleShape)
.background(Color.Black)
.padding(4.dp)
.graphicsLayer { rotationZ = -45f },
onClick = { /*TODO*/ }
) {
Icon(imageVector = Icons.Default.Send, contentDescription = null, tint = Color.White)
}
} else {
Row(modifier = Modifier.padding(end = 16.dp)) {
Icon(
painter = painterResource(id = R.drawable.baseline_mic_24),
contentDescription = null,
modifier = Modifier.size(32.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Icon(
painter = painterResource(id = R.drawable.baseline_attach_file_24),
contentDescription = null,
modifier = Modifier
.size(32.dp)
.graphicsLayer { rotationZ = 45f }
)
}
}
}
},
colors = TextFieldDefaults.colors(
unfocusedContainerColor = grayColor,
focusedContainerColor = grayColor
),
modifier = Modifier.fillMaxWidth()
)
}
}
@Composable
private fun TypingIndicator(
totalDots: Int = 3,
alpha: Float = 1f
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(35))
.background(grayColor.copy(alpha = alpha))
.padding(16.dp)
) {
val animationProgresses = remember { List(totalDots) { Animatable(0f) } }
LaunchedEffect(key1 = Unit) {
launch {
while (true) {
animationProgresses.forEach {
launch {
val result = it.animateTo(1f)
if(result.endReason == AnimationEndReason.Finished) {
it.animateTo(0f)
}
}
delay(100)
}
delay(700)
}
}
}
Row(
modifier = Modifier.height(25.dp),
verticalAlignment = Alignment.Bottom
) {
repeat(totalDots) {
Box(
modifier = Modifier
.offset {
IntOffset(
x = 0,
y = map(
FastOutSlowInEasing.transform(animationProgresses[it].value),
0f,
1f,
0f,
-10.dp.toPx()
).toInt()
)
}
.size(13.dp)
.clip(CircleShape)
.background(Color.Black.copy(alpha = alpha))
)
}
}
}
}
private sealed interface OwnerType {
object Me: OwnerType
object Other: OwnerType
}
private data class MessageDetail(
val ownerType: OwnerType,
val message: String,
val time: String
)
@Preview
@Composable
fun ChatDetailsScreenPreview() {
ChatDetailsScreen()
}
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MailOutline
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Phone
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Badge
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
private val grayColor = Color(0xff8a95b3)
@Composable
private fun TopAppBar(
modifier: Modifier = Modifier
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp)
) {
Box(modifier = Modifier
.size(60.dp)
.clip(CircleShape)
.background(color = Color.Black))
Spacer(modifier = Modifier.width(16.dp))
Text(text = "Messages", textAlign = TextAlign.Center, fontSize = 20.sp, modifier = Modifier.weight(1f))
IconButton(onClick = { /*TODO*/ }) {
Icon(imageVector = Icons.Default.Search, contentDescription = null, tint = grayColor)
}
}
}
@Composable
private fun TopNavigationBar() {
val topNavigationItems = listOf("Recent", "Favorites", "Work", "All")
val selectedIndex = 0
Row {
topNavigationItems.forEachIndexed { index, it ->
TopNavigationItem(
name = it,
isSelected = selectedIndex == index,
modifier = Modifier.weight(1f)
)
}
}
}
@Composable
private fun TopNavigationItem(
name: String,
isSelected: Boolean,
modifier: Modifier = Modifier
) {
Box(modifier = modifier, contentAlignment = Alignment.Center) {
Text(
text = name,
textAlign = TextAlign.Center,
fontSize = 18.sp,
color = if (isSelected) Color.Black else grayColor,
modifier = Modifier.fillMaxWidth()
)
}
}
@Composable
fun ChatScreen() {
val chatList = remember {
listOf(
MessageItemState(title = "Jennie Davis", subtitle = "Hi!", time = "1m ago", isOnline = true, newMessagesCount = 2),
MessageItemState(title = "Lucy Brown", subtitle = "Good point, dear)", time = "35m ago"),
MessageItemState(title = "James Wilson", subtitle = "Ok, Let's be in touch.", time = "1h ago"),
MessageItemState(title = "Janine Taylor", subtitle = "I'll be waiting for you there.", time = "3h ago"),
MessageItemState(title = "Dan Miller", subtitle = "Ok.", time = "5h ago"),
MessageItemState(title = "Sam Tinko", subtitle = "Oh, thanks", time = "1d ago")
)
}
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.White)
) {
TopAppBar()
TopNavigationBar()
Column(modifier = Modifier.weight(1f)) {
chatList.forEach {
MessageItem(messageItemState = it)
}
EncryptionMessage()
}
BottomBar()
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MessageItem(
messageItemState: MessageItemState
) {
Column {
Spacer(modifier = Modifier.height(8.dp))
Divider()
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = 16.dp,
vertical = 8.dp
)
) {
ProfilePicture(messageItemState.isOnline)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(1f)
) {
Text(text = messageItemState.title, fontSize = 18.sp)
Text(text = messageItemState.subtitle, color = grayColor, fontSize = 16.sp)
}
Spacer(modifier = Modifier.width(4.dp))
Column(
horizontalAlignment = Alignment.End
) {
Text(text = messageItemState.time, color = grayColor)
Spacer(modifier = Modifier.height(8.dp))
AnimatedVisibility(visible = messageItemState.newMessagesCount > 0) {
Badge(
containerColor = Color.Green
) {
Text(text = messageItemState.newMessagesCount.toString(), fontSize = 15.sp)
}
}
}
}
}
}
private data class MessageItemState(
val title: String,
val subtitle: String,
val time: String,
val isOnline: Boolean = false,
val newMessagesCount: Int = 0,
val profilePicUrl: String? = null
)
@Composable
private fun ProfilePicture(
isOnline: Boolean,
) {
Box {
Box(
modifier = Modifier
.size(50.dp)
.clip(CircleShape)
.background(Color.Black)
)
AnimatedVisibility(visible = isOnline, modifier = Modifier.align(Alignment.BottomEnd)) {
Box(
modifier = Modifier
.size(15.dp)
.clip(CircleShape)
.background(Color.Green)
.border(2.dp, Color.White, CircleShape)
)
}
}
}
@Composable
private fun BottomBar(
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(45, 45, 0, 0))
.background(Color.Black)
.padding(horizontal = 16.dp, vertical = 24.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceAround
) {
Icon(imageVector = Icons.Default.MailOutline, contentDescription = null, tint = Color.White)
Icon(imageVector = Icons.Default.Phone, contentDescription = null, tint = grayColor)
Icon(imageVector = Icons.Default.Person, contentDescription = null, tint = grayColor)
Icon(imageVector = Icons.Default.Settings, contentDescription = null, tint = grayColor)
}
}
@Composable
private fun EncryptionMessage() {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.fillMaxWidth(0.7f)
) {
Divider()
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "The messages are protected by the end-to-end encryption",
fontSize = 16.sp,
color = grayColor,
textAlign = TextAlign.Center
)
}
}
}
@Preview
@Composable
private fun ChatScreenPreview() {
ChatScreen()
}
@Mikkareem
Copy link
Author

chatui.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment