Last active
July 6, 2024 22:34
-
-
Save akexorcist/7814a32b4bed09216a8fc3a54d7c663f to your computer and use it in GitHub Desktop.
Beak Shape for Jetpack Compose or Compose Multiplatform
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.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() | |
} | |
} | |
} |
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
@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), | |
) | |
) | |
} |
Author
akexorcist
commented
Jun 5, 2024
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment