Skip to content

Instantly share code, notes, and snippets.

@akexorcist
Last active July 6, 2024 22:34
Show Gist options
  • Save akexorcist/7814a32b4bed09216a8fc3a54d7c663f to your computer and use it in GitHub Desktop.
Save akexorcist/7814a32b4bed09216a8fc3a54d7c663f to your computer and use it in GitHub Desktop.
Beak Shape for Jetpack Compose or Compose Multiplatform
import androidx.annotation.FloatRange
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
enum class BeakDirection {
Top, Bottom, Left, Right;
}
class BeakShape(
@FloatRange(from = .0, to = 1.0) private val ratio: Float,
private val direction: BeakDirection = BeakDirection.Top,
) : Shape {
override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline {
val width = size.width.toDouble()
val height = size.height.toDouble()
val centerX = width / 2
val centerY = height / 2
val path = createBeakPath(width.toFloat(), height.toFloat(), centerX.toFloat(), centerY.toFloat(), direction)
return Outline.Generic(path)
}
private fun createBeakPath(
width: Float,
height: Float,
centerX: Float,
centerY: Float,
direction: BeakDirection
): Path {
val diagonal = when (direction) {
BeakDirection.Left,
BeakDirection.Right -> sqrt(width.pow(2) + centerY.pow(2))
BeakDirection.Top,
BeakDirection.Bottom -> sqrt(centerX.pow(2) + height.pow(2))
}
val targetDiagonal = diagonal * (1 - ratio)
val radian = atan2(
when (direction) {
BeakDirection.Left,
BeakDirection.Right -> width
BeakDirection.Top,
BeakDirection.Bottom -> centerX
},
when (direction) {
BeakDirection.Left,
BeakDirection.Right -> centerY
BeakDirection.Top,
BeakDirection.Bottom -> height
}
)
val curvePointX = sin(radian) * targetDiagonal
val curvePointY = cos(radian) * targetDiagonal
return Path().apply {
when (direction) {
BeakDirection.Top -> {
moveTo(0f, height)
cubicTo(centerX, 0f, centerX, 0f, (centerX - curvePointX), curvePointY)
cubicTo(centerX, 0f, centerX, 0f, (centerX + curvePointX), curvePointY)
lineTo(width, height)
}
BeakDirection.Bottom -> {
moveTo(0f, 0f)
cubicTo(centerX, height, centerX, height, (centerX - curvePointX), height - curvePointY)
cubicTo(centerX, height, centerX, height, (centerX + curvePointX), height - curvePointY)
lineTo(width, 0f)
}
BeakDirection.Left -> {
moveTo(width, 0f)
cubicTo(0f, centerY, 0f, centerY, curvePointX, (centerY - curvePointY))
cubicTo(0f, centerY, 0f, centerY, curvePointX, (centerY + curvePointY))
lineTo(width, height)
}
BeakDirection.Right -> {
moveTo(0f, 0f)
cubicTo(width, centerY, width, centerY, width - curvePointX, (centerY - curvePointY))
cubicTo(width, centerY, width, centerY, width - curvePointX, (centerY + curvePointY))
lineTo(0f, height)
}
}
close()
}
}
}
@Preview(widthDp = 400, heightDp = 2000)
@Composable
private fun AnyCustomShapePreview() {
val directions = listOf(
BeakDirection.Top,
BeakDirection.Bottom,
BeakDirection.Left,
BeakDirection.Right,
)
val sizes = listOf(
70.dp to 100.dp,
100.dp to 100.dp,
150.dp to 100.dp,
)
val ratios = listOf(
1.0f,
0.75f,
0.5f,
0.25f,
0.0f,
)
MaterialTheme {
Column(
modifier = Modifier.background(Color(0xFF111111)),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
directions.forEachIndexed { index, direction ->
Text(text = "$direction", color = Color.White, modifier = Modifier
.align(Alignment.Start)
.offset(x = 92.dp), fontWeight = FontWeight.Bold)
ratios.forEach { ratio ->
Row {
sizes.forEachIndexed { index, (width, height) ->
AnyCustomShapeContainer(
modifier = Modifier
.width(width)
.height(height),
ratio = ratio,
direction = direction,
)
if (index != sizes.lastIndex)
Spacer(modifier = Modifier.size(16.dp))
}
}
}
if (index != directions.lastIndex)
Spacer(modifier = Modifier.size(48.dp))
}
}
}
}
@Composable
fun AnyCustomShapeContainer(
modifier: Modifier,
ratio: Float,
direction: BeakDirection,
) {
Column {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Ratio = $ratio",
fontSize = 10.sp,
color = Color.White
)
Spacer(modifier = Modifier.height(4.dp))
AnyCustomShape(modifier = modifier, ratio = ratio, direction = direction)
Spacer(modifier = Modifier.height(8.dp))
}
}
@Composable
fun AnyCustomShape(
modifier: Modifier,
ratio: Float,
direction: BeakDirection,
) {
Spacer(
modifier = modifier
.background(
color = Color.White.copy(alpha = 0.1f)
)
.background(
color = Color.White,
shape = BeakShape(ratio = ratio, direction = direction),
)
)
}
@akexorcist
Copy link
Author

image
image
image
image

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