Skip to content

Instantly share code, notes, and snippets.

@logickoder
Created October 23, 2024 14:50
Show Gist options
  • Save logickoder/827bc0463532b542b8c6a89678839997 to your computer and use it in GitHub Desktop.
Save logickoder/827bc0463532b542b8c6a89678839997 to your computer and use it in GitHub Desktop.
ChatBubbleShape
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.thalajaat.launcher.core.designsystem.theme.LauncherTheme
enum class ChatBubbleShapeTickOrientation {
Start, End
}
class ChatBubbleShape(
private val cornerRadius: Dp,
private val tickOrientation: ChatBubbleShapeTickOrientation,
private val tailScale: Float = 1f // Allows customization of tail size
) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
val path = Path()
val cornerRadius = with(density) { cornerRadius.toPx() }
val tailWidth = with(density) { 16.dp.toPx() } * tailScale
val tailHeight = with(density) { 16.dp.toPx() } * tailScale
// Determine if the tail should be on the right side
val isRightSide =
(tickOrientation == ChatBubbleShapeTickOrientation.End && layoutDirection == LayoutDirection.Ltr) ||
(tickOrientation == ChatBubbleShapeTickOrientation.Start && layoutDirection == LayoutDirection.Rtl)
path.apply {
// Start from top-left
moveTo(cornerRadius, 0f)
// Top edge
lineTo(size.width - cornerRadius, 0f)
// Top-right corner (0 radius if tail is on this side)
val topRightRadius = if (isRightSide) cornerRadius * 0f else cornerRadius
arcTo(
Rect(
left = size.width - 2 * topRightRadius,
top = 0f,
right = size.width,
bottom = 2 * topRightRadius
),
270f,
90f,
false
)
// Right edge with tail if needed
if (isRightSide) {
// WhatsApp-style tail on right
lineTo(size.width + (tailWidth * 0.6f), 0f)
quadraticTo(
x1 = size.width + tailWidth,
y1 = 0f,
x2 = size.width,
y2 = tailHeight
)
}
lineTo(size.width, size.height - cornerRadius)
// Bottom-right corner
arcTo(
Rect(
left = size.width - 2 * cornerRadius,
top = size.height - 2 * cornerRadius,
right = size.width,
bottom = size.height
),
0f,
90f,
false
)
// Bottom edge
lineTo(cornerRadius, size.height)
// Bottom-left corner
arcTo(
Rect(
left = 0f,
top = size.height - 2 * cornerRadius,
right = 2 * cornerRadius,
bottom = size.height
),
90f,
90f,
false
)
when (isRightSide) {
// Top-left corner (0 radius if tail is on this side)
true -> {
lineTo(0f, cornerRadius)
arcTo(
Rect(
left = 0f,
top = 0f,
right = 2 * cornerRadius,
bottom = 2 * cornerRadius
),
180f,
90f,
false
)
}
// WhatsApp-style tail on left
else -> {
lineTo(0f, tailHeight)
moveTo(cornerRadius, 0f)
lineTo(-(tailWidth * 0.6f), 0f)
quadraticTo(
x1 = -tailWidth,
y1 = 0f,
x2 = 0f,
y2 = tailHeight,
)
}
}
close()
}
return Outline.Generic(path)
}
}
@Preview(showBackground = true)
@Composable
private fun ChatBubbleShapePreview() = MaterialTheme {
Surface {
Column(
modifier = Modifier
.width(300.dp)
.height(200.dp)
.padding(20.dp)
) {
Card(
modifier = Modifier
.width(200.dp)
.height(60.dp)
.align(Alignment.End)
.shadow(
elevation = 2.dp,
shape = ChatBubbleShape(
cornerRadius = 16.dp,
tickOrientation = ChatBubbleShapeTickOrientation.End
),
spotColor = Color(0x1A000000)
),
shape = ChatBubbleShape(
cornerRadius = 16.dp,
tickOrientation = ChatBubbleShapeTickOrientation.End
),
colors = CardDefaults.cardColors(
containerColor = Color.Blue
)
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier
.align(Alignment.Center)
.padding(16.dp),
text = "Hello world"
)
}
}
// Received message (left side)
Card(
modifier = Modifier
.width(200.dp)
.height(60.dp)
.align(Alignment.Start)
.shadow(
elevation = 2.dp,
shape = ChatBubbleShape(
cornerRadius = 16.dp,
tickOrientation = ChatBubbleShapeTickOrientation.Start
),
spotColor = Color(0x1A000000)
),
shape = ChatBubbleShape(
cornerRadius = 16.dp,
tickOrientation = ChatBubbleShapeTickOrientation.Start
),
colors = CardDefaults.cardColors(containerColor = Color.Red)
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier
.align(Alignment.Center)
.padding(16.dp),
text = "Hi there!"
)
}
}
}
}
}
@logickoder
Copy link
Author

image

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