Created
October 23, 2024 14:50
-
-
Save logickoder/827bc0463532b542b8c6a89678839997 to your computer and use it in GitHub Desktop.
ChatBubbleShape
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!" | |
) | |
} | |
} | |
} | |
} | |
} |
Author
logickoder
commented
Oct 23, 2024
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment