找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4040

积分

0

好友

529

主题
发表于 4 天前 | 查看: 20| 回复: 0

探索如何在 Android 应用中实现一个具有高级视觉吸引力的底部导航栏?仅靠标准视图组合可能难以创造出那种细腻的动态光影效果。本文将深入探讨如何利用 Android 图形着色语言 AGSL,在 Jetpack Compose 中构建一个会发光的底部导航栏,为你的应用界面增添赛博朋克般的科技感。

之前,我们已经探讨过如何使用 AGSL 着色器来创建沉浸式的落地页体验。在这篇文章中,我们将把这些概念应用到一个更具体的组件上,一步步构建一个会发光的底部导航栏,使用着色器为应用菜单创造一种动态、高级的光照效果。

案例研究:发光导航栏组件

本次案例研究的目标,是改造一个标准的底部导航界面,通过 AGSL 着色器赋予其独特的发光视觉效果。让我们从核心的着色器脚本开始,看看如何通过数学计算实现坐标变换和光线衰减。

const val GLOW_SHADER = """  
    uniform float2 resolution;  
    uniform float progress;   
    uniform float3 color;   
    half4 main(float2 coords) {  
        float2 center = resolution * 0.5;  
        float dist = distance(coords, center);  
        float maxRadius = resolution.x * 0.6;  
        float glow = 1.0 - smoothstep(0.0, maxRadius, dist);  
        glow = glow * glow;  
        float alpha = glow * progress * 0.4;  
        return half4(color, alpha);  
    }  
"""

下面是对上述代码的技术解析:我们创建了一个从绘图区域中心发出的径向发光效果。它利用距离场来计算每个像素的亮度,从而实现从中心到边缘的柔和淡出。

这些是从 Kotlin 代码传递到着色器的动态参数:

  • uniform float2 resolution → UI组件(画布/盒子)的宽度和高度(以像素为单位)。这是坐标归一化的关键。
  • uniform float progress → 一个在 0.0 到 1.0 之间变化的值,用于控制发光的强度或不透明度,通常与动画状态绑定。
  • uniform float3 color → 发光的 RGB 颜色值。

这个着色器的核心依赖于距离场计算

  • *`center = resolution 0.5** → 将分辨率减半(x/2, y/2)`,计算出绘图区域的精确中心点。
  • distance(coords, center) →  distance() 函数计算当前像素坐标 coords 到中心点的距离。这创建了一个圆形的距离场:靠近中心的像素 dist 值较低,远离中心的则较高。

接下来的代码定义了光线衰减的柔和度:

  • maxRadius → 将发光效果限制在组件宽度 60% 的半径范围内。
  • smoothstep →  smoothstep(0.0, maxRadius, dist) 在中心(dist0)返回 0.0,并在边缘(distmaxRadius)平滑地插值到 1.0
  • 1.0 - smoothstep(…) → 我们翻转这个逻辑,使中心值变为 1.0(最亮),边缘值变为 0.0(不可见)。

glow = glow * glow; 这行代码是对伽马值或衰减曲线的简单调整。对值进行平方会使衰减曲线更陡峭,让中间亮度的区域下降得更快,从而防止发光看起来过于线性或平淡,使其视觉效果更加自然。

最后一步,我们将空间发光逻辑与动画进度结合,计算出最终的颜色透明度:

  • alpha 计算 → 透明度是三个因子的乘积:glow(空间衰减)、progress(动画状态)和常数 0.4。这个常数作为强度的上限,确保发光不透明度不超过40%,保持效果的微妙感。
  • half4(color, alpha) → 函数返回一个 half4 (RGBA) 类型的颜色,将计算出的 alpha 透明度应用到输入的 color 上。

创建自定义底部菜单项

着色器脚本准备就绪后,我们需要一个承载它的界面。接下来构建一个 BottomNavigationItem 可组合项。这段代码处理选中状态逻辑,并将我们的 GLOW_SHADER 包装在 RuntimeShader 中进行渲染。

@Composable  
fun RowScope.BottomNavigationItem(  
    icon: ImageVector,  
    label: String,  
    selected: Boolean,  
    onClick: () -> Unit  
) {  
    val selectionProgress by animateFloatAsState(  
        targetValue = if (selected) 1.5f else 0f,  
        animationSpec = tween(durationMillis = 300),  
        label = "GlowAnimation"  
    )  
    val scale by animateFloatAsState(  
        targetValue = if (selected) 1.5f else 1.0f,  
        animationSpec = spring(  
            dampingRatio = Spring.DampingRatioHighBouncy,  
            stiffness = Spring.StiffnessMedium  
        ),  
        label = "BounceAnimation"  
    )  
    val primaryColor = MaterialTheme.colorScheme.primary  
    val iconColor = if (selected) MaterialTheme.colorScheme.primary else TextGray  
    val shaderModifier = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {  
        val shader = remember { RuntimeShader(GLOW_SHADER) }  
        Modifier.drawWithCache {  
            val brush = ShaderBrush(shader)  
            shader.setFloatUniform("resolution", size.width, size.height)  
            shader.setFloatUniform("progress", selectionProgress)  
            shader.setFloatUniform("color", primaryColor.red, primaryColor.green, primaryColor.blue)  
            onDrawBehind {  
                drawRect(brush)  
            }  
        }  
    } else {  
        Modifier  
    }  
    Column(  
        horizontalAlignment = Alignment.CenterHorizontally,  
        verticalArrangement = Arrangement.Center,  
        modifier = Modifier  
            .fillMaxHeight()  
            .weight(1f)  
            .clickable { onClick() }  
            .then(shaderModifier)  
    ) {  
        Icon(  
            imageVector = icon,  
            contentDescription = label,  
            tint = iconColor,  
            modifier = Modifier  
                .size(26.dp)  
                .graphicsLayer {  
                    scaleX = scale  
                    scaleY = scale  
                }  
        )  
    }  
}

我们来分解一下上面的代码:

动画状态管理

  • selectionProgress → 使用一个标准的补间动画,该值(0f1.5f)会传递给着色器,控制发光的不透明度和扩散范围。
  • scale → 使用一个高弹性的弹簧动画,在选中时放大图标,赋予其一种有趣的、触觉般的反馈感。

着色器实现细节

  • 版本检查 →  RuntimeShader 从 Android 13 (Tiramisu) 开始引入。我们将逻辑包装在版本检查中,以确保向后兼容。在不支持的旧设备上,发光效果将不会渲染,实现优雅降级。
  • drawWithCache → 出于性能考虑,我们使用这个修饰符而非标准的 drawBehind。它允许我们创建一次 ShaderBrush 并复用,仅在尺寸或状态改变时更新 Uniforms。
  • onDrawBehind → 关键点在于,我们在内容后面绘制着色器,这样发光效果就会出现在图标的下方作为背景。
  • Uniform 传值:
    • resolution → 传递 size.widthsize.height,告知着色器绘图区域的中心位置。
    • progress → 传递动画的 selectionProgress 值。
    • color → 从 Compose 主题的 primaryColor 中提取红、绿、蓝分量。

最终,我们将 shaderModifier 应用到 Column 容器上。Icon 使用 .graphicsLayer { … } 来高效地应用缩放动画,避免触发重新布局。最终得到一个能够处理点击、渲染背景发光(在支持的设备上)并容纳弹跳图标的可组合项。

组装最终的自定义底部导航栏

各个组件准备就绪后,让我们来组装最终的 BottomNavBar。这一步,我们将发光的菜单项与标准的 BottomAppBar 结合,并集成一个居中的浮动操作按钮。我们使用简单的负向偏移量和 FAB 上的边框来创造出“悬浮”在导航栏上的视错觉,无需复杂的路径裁剪。

@Composable  
fun BottomNavBar(onFabClick: () -> Unit) {  
    var selectedTab by remember { mutableIntStateOf(0) }  
    BottomAppBar(  
        modifier = Modifier.clip(RoundedCornerShape(topStart = 30.dp, topEnd = 30.dp)),  
        containerColor = MaterialTheme.colorScheme.surface,  
        contentColor = TextGray,  
        tonalElevation = 8.dp  
    ) {  
        BottomNavigationItem(  
            icon = Icons.Default.CalendarMonth,  
            label = "Tasks",  
            selected = selectedTab == 0,  
            onClick = { selectedTab = 0 }  
        )  
        Box(  
            modifier = Modifier.weight(1f),  
            contentAlignment = Alignment.Center  
        ) {  
            FloatingActionButton(  
                onClick = onFabClick,  
                containerColor = MaterialTheme.colorScheme.primary,  
                contentColor = MaterialTheme.colorScheme.onPrimary,  
                shape = CircleShape,  
                modifier = Modifier  
                    .offset(y = (-10).dp)  
                    .size(56.dp)  
                    .border(4.dp, MaterialTheme.colorScheme.surface, CircleShape)  
            ) {  
                Icon(  
                    imageVector = Icons.Default.Add,  
                    contentDescription = "Add Task",  
                    modifier = Modifier.size(24.dp)  
                )  
            }  
        }  
        BottomNavigationItem(  
            icon = Icons.Default.Search,  
            label = "Search",  
            selected = selectedTab == 1,  
            onClick = { selectedTab = 1 }  
        )  
    }  
}

将所有部分组合起来,我们便得到了一个完全交互式的、带有动态发光效果的现代化底部导航栏。这种在 Android 开发中结合声明式 UI 与高性能图形的方法,极大地拓展了 Jetpack Compose 的视觉表达能力。

要点总结

构建自定义 UI 不仅是摆放元素,更是塑造用户体验的“感觉”。通过使用 AGSL,我们实现了一种如果仅用标准 Canvas 绘图命令会非常复杂且耗费资源的视觉效果。我们创建出了一种柔和、非线性的发光效果,能够即时响应用户交互,同时保持了 Compose 代码的整洁与模块化。这种 Shader + Modifier 的模式是一种可复用的技术,你可以将其轻松应用到应用中的按钮、卡片或加载状态上,为你的产品带来独特的视觉个性。

如果你对如何在移动应用开发中实现更多酷炫的 UI 动效和图形技术感兴趣,欢迎到 云栈社区 的移动开发板块与其他开发者交流探讨。

原文链接https://proandroiddev.com/building-a-glowing-bottom-navigation-with-agsl-shaders-6a5faa547e09




上一篇:小米三年600亿押注AI,人形机器人已进入汽车工厂“实习”
下一篇:OpenClaw多Agent金融量化实战:协作配置与部署避坑指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-7 21:16 , Processed in 0.829999 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表