You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose自定义BottomNavigationAppBar的两个问题求助

自定义Jetpack Compose BottomNavigationBar问题解决方案

问题1:贝塞尔曲线不对称且被截断

问题分析

当前IndentPath的核心问题:

  • 起始点start.x = -90f超出rect左边界,导致曲线左侧被clip截断
  • 控制点坐标完全不对称,且依赖固定maxX/maxY映射,无法适配动态rect尺寸,引发曲线变形
  • 曲线终点与控制点逻辑混乱,未以indent中心为对称基准

解决方案

重构IndentPath,以indent中心为对称点,用相对比例定义曲线,确保对称且不超出边界:

class IndentPath(private val rect: Rect) {
    // 以indent中心为原点的相对坐标映射(x范围-1到1,y范围0到1)
    private fun translateRelative(x: Float, y: Float): PointF {
        val centerX = rect.centerX()
        val topY = rect.top
        return PointF(
            centerX + x * rect.width / 2,
            topY + y * rect.height
        )
    }

    fun createPath(): Path {
        val path = Path()
        // 起点/终点:indent左右端点(与bar底部对齐)
        val start = translateRelative(-1f, 0f)
        val end = translateRelative(1f, 0f)
        // 曲线最低点(钟形底部)
        val bottom = translateRelative(0f, 1f)
        
        // 对称控制点:保证左右曲线完全镜像
        val leftControl1 = translateRelative(-0.8f, 0.2f)
        val leftControl2 = translateRelative(-0.2f, 0.9f)
        val rightControl1 = translateRelative(0.2f, 0.9f)
        val rightControl2 = translateRelative(0.8f, 0.2f)

        path.moveTo(start.x, start.y)
        path.cubicTo(leftControl1.x, leftControl1.y, leftControl2.x, leftControl2.y, bottom.x, bottom.y)
        path.cubicTo(rightControl1.x, rightControl1.y, rightControl2.x, rightControl2.y, end.x, end.y)

        return path
    }
}

额外注意:确保StraightIndent的indentWidth与导航项宽度匹配,避免曲线范围超出项边界。


问题2:切换图标时的图标/Logo切换动画

实现思路

利用Jetpack Compose的AnimatedContent结合缩放、透明度动画,实现新旧图标的过渡效果,通过prevSelectedIndex记录上一选中项触发跨项动画。

解决方案

修改ColorButton组件,添加图标切换动画:

@Composable
fun ColorButton(
    modifier: Modifier = Modifier,
    prevSelectedIndex: Int,
    selectedIndex: Int,
    index: Int,
    onClick: () -> Unit,
    icon: @Composable () -> Unit,
    selectedIcon: @Composable () -> Unit, // 新增选中状态图标
    contentDescription: String,
    animationType: AnimationType,
    background: Color
) {
    val isSelected = selectedIndex == index

    Box(
        modifier = modifier
            .clickable(onClick = onClick)
            .padding(16.dp),
        contentAlignment = Alignment.Center
    ) {
        AnimatedContent(
            targetState = isSelected,
            transitionSpec = {
                // 切换动画:选中时新图标放大淡入,旧图标缩小淡出;取消选中则反向
                if (targetState) {
                    scaleIn(spring(stiffness = Spring.StiffnessMedium)) + fadeIn() with
                    scaleOut(targetScale = 0.7f) + fadeOut()
                } else {
                    scaleIn(initialScale = 0.7f) + fadeIn() with
                    scaleOut(targetScale = 1.2f) + fadeOut()
                }.using(SizeTransform(clip = false))
            }
        ) { selected ->
            if (selected) selectedIcon() else icon()
        }
    }
}

使用示例

在MyCustomAnimatedBottomNavBar中传入选中状态图标:

ColorButton(
    // ...其他参数
    icon = { Icon(Icons.Default.Home, contentDescription = contentDescription) },
    selectedIcon = { Icon(Icons.Default.HomeFilled, contentDescription = contentDescription) }
)

若需图标到Logo的变形动画,可使用AnimatedVectorDrawable配合rememberAnimatedVectorDrawable实现矢量图过渡。


内容的提问来源于stack exchange,提问作者Josef M

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 13:26:03