在Android Jetpack Compose中实现带Notch与Cutout的票券式布局
在Android Jetpack Compose中实现带Notch与Cutout的票券式布局
嗨~作为Compose新手,想要做这种带缺口和镂空的票券布局其实完全不用慌,咱们从最基础的核心思路开始一步步来!
核心思路
Compose里实现这种自定义形状的布局,关键就是自定义Shape(形状)——票券的顶部Notch、侧边Cutout都是形状的一部分,再配合阴影Modifier就能做出你想要的质感效果。
第一步:自定义票券形状
先创建一个继承自Shape的类,通过Path来绘制带缺口的完整轮廓。你可以根据需求调整缺口的大小参数:
class TicketShape( private val notchRadius: Dp = 16.dp, private val cutoutRadius: Dp = 24.dp ) : Shape { override fun createOutline( size: Size, layoutDirection: LayoutDirection, density: Density ): Outline { val notchRadiusPx = with(density) { notchRadius.toPx() } val cutoutRadiusPx = with(density) { cutoutRadius.toPx() } val path = Path().apply { // 从顶部左侧缺口的右侧开始绘制 moveTo(notchRadiusPx, 0f) // 绘制顶部直线到右侧缺口左侧 lineTo(size.width - notchRadiusPx, 0f) // 绘制顶部右侧的凹形Notch arcTo( rect = Rect( left = size.width - 2 * notchRadiusPx, top = -notchRadiusPx, right = size.width, bottom = notchRadiusPx ), startAngleDegrees = 90f, sweepAngleDegrees = 180f, forceMoveTo = false ) // 绘制右侧直线到底部Cutout上方 lineTo(size.width, size.height - cutoutRadiusPx) // 绘制右下角的Cutout缺口 arcTo( rect = Rect( left = size.width - cutoutRadiusPx, top = size.height - 2 * cutoutRadiusPx, right = size.width + cutoutRadiusPx, bottom = size.height ), startAngleDegrees = 0f, sweepAngleDegrees = 180f, forceMoveTo = false ) // 绘制底部直线到左侧Cutout右侧 lineTo(cutoutRadiusPx, size.height) // 绘制左下角的Cutout缺口 arcTo( rect = Rect( left = -cutoutRadiusPx, top = size.height - 2 * cutoutRadiusPx, right = cutoutRadiusPx, bottom = size.height ), startAngleDegrees = 0f, sweepAngleDegrees = -180f, forceMoveTo = false ) // 绘制左侧直线到顶部Notch下方 lineTo(0f, notchRadiusPx) // 绘制顶部左侧的凹形Notch arcTo( rect = Rect( left = 0f, top = -notchRadiusPx, right = 2 * notchRadiusPx, bottom = notchRadiusPx ), startAngleDegrees = 270f, sweepAngleDegrees = 180f, forceMoveTo = false ) close() // 闭合路径 } return Outline.Generic(path) } }
第二步:使用自定义形状并添加阴影
接下来就可以在布局里用这个形状啦,配合Modifier.shadow()让阴影跟着票券形状走,再加上背景色和内部内容:
@Composable fun TicketLayout() { // 实例化自定义形状,可调整缺口大小 val ticketShape = TicketShape(notchRadius = 16.dp, cutoutRadius = 20.dp) Box( modifier = Modifier .size(width = 300.dp, height = 400.dp) // 添加阴影,阴影形状和票券形状保持一致 .shadow( elevation = 8.dp, shape = ticketShape, spotColor = Color(0x40000000), ambientColor = Color(0x40000000) ) // 设置白色背景,同样使用自定义形状 .background(Color.White, shape = ticketShape) .padding(16.dp) ) { // 这里放票券内部的内容,比如标题、详情等 Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize() ) { Text("我的电影票", fontSize = 20.sp, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(16.dp)) Text("场次:2024-05-20 19:30", fontSize = 14.sp) Spacer(modifier = Modifier.height(8.dp)) Text("座位:5排12座", fontSize = 14.sp) } } }
新手小提示
- 要是对
Path的坐标计算有点懵,你可以把每一段lineTo和arcTo的步骤拆开来,单独看每一段的绘制效果,慢慢就有手感啦~ - 要是需要中间的撕痕式镂空,只需要在自定义Shape的
Path里,在票券中间的位置再添加一组半圆缺口就行,方法和侧边的Cutout完全一样。 - 调整阴影的
elevation值可以改变阴影的深浅和扩散范围,多试几次就能找到你想要的质感~
内容来源于stack exchange




