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

3178

积分

0

好友

442

主题
发表于 昨天 03:46 | 查看: 1| 回复: 0

Tubelight Effect in Compose 标题效果图

厌倦了常规的界面开发?今天我们来点不一样的,一起用Jetpack Compose实现一个视觉上相当酷炫的“霓虹灯管”特效。这个效果不需要复杂的OpenGL知识,仅仅利用Compose自带的绘图API和动画就能轻松达成。

第一步:构建核心光晕渐变

光源是这个效果的核心。在Compose中,Brush.sweepGradient是一个理想的选择,它能创建一个类似雷达扫描的圆锥形渐变。

我们首先定义一个颜色数组,让颜色从中心的主发光色平滑过渡到边缘的透明:

val sweepGradient = Brush.sweepGradient(
       colorStops = arrayOf(
           0.0f to mainGlowColor,      // 中心是高亮色
           0.49f to Color.Transparent, // 快速过渡到透明
           1.0f to Color.Transparent,   // 保持透明
       ),
       center = centerOffset
   )

设置好角度渐变后,我们就能得到一个基础的发光锥形效果。

基础的sweepGradient光锥效果

第二步:镜像翻转,形成对称灯管

目前的光效是单侧的,看起来更像手电筒。为了形成对称的“灯管”效果,我们需要进行镜像翻转。

这可以通过在Modifier.drawBehind中结合scale变换轻松实现,将X轴缩放设置为-1即可:

onDrawBehind {
       scale(
           scaleX = if (flip) -1f else 1f, // 翻转 X 轴
           scaleY = 1f
       ) {
           drawRect(
               brush = sweepGradient,
               blendMode = BlendMode.Plus // 关键点:叠加模式
           )
       }
   }

这里有一个关键细节:blendMode = BlendMode.Plus。这种混合模式决定了新绘制的内容如何与画布已有内容结合。在我们的场景中,它让光线像真实光源一样叠加在背景上,从而产生发光感而非简单的覆盖。

运行后,对称的灯管雏形就出现了。

经过镜像翻转后的对称灯管效果

第三步:精确计算光源中心偏移量

为了精准控制光效的绘制起点(即灯管的位置),我们需要计算centerOffset

size.width是绘图区域的宽度,我们用它减去灯管宽度的一半来定位。因为绘制是从中心点开始的,所以所有计算都基于这个中心点。这个参数在后续添加动画时也会用到。

val centerOffset = Offset(
    x = size.width - tubeWidth,
    y = startY
)

val sweepGradient = Brush.sweepGradient(
    colorStops = arrayOf(
        0.0f to mainGlowColor,
        0.49f to Color.Transparent,
        1.0f to Color.Transparent,
    ),
    center = centerOffset
)

将以上步骤整合,一个可组合函数的基本框架如下:

@Composable
fun Light(
    mainGlowColor: Color = Color(0xFF00DEFF),
    flip: Boolean = true,
    startY: Float,
    modifier: Modifier,
    halfTubeWidth: Float,
) {

    Box(
        modifier = modifier
            .drawWithCache {
                val centerOffset = Offset(
                    x = size.width - halfTubeWidth,
                    y = startY
                )

                val sweepGradient1 = Brush.sweepGradient(
                    colorStops = arrayOf(
                        0.0f to mainGlowColor,
                        0.49f to Color.Transparent,
                        1.0f to Color.Transparent,
                    ),
                    center = centerOffset
                )

                onDrawBehind {
                    scale(
                        scaleX = if (flip) -1f else 1f,
                        scaleY = 1f
                    ) {
                        drawRect(
                            brush = sweepGradient1,
                            blendMode = BlendMode.Plus
                        )
                    }
                }
            }
    )
}

第四步:为灯管注入灵魂——动画

静态的灯管好看,但动态的才拥有灵魂。我们可以通过动画改变halfTubeWidth或渐变colorStops的值来实现淡入或光线扩散的效果。

例如,可以专门为光线扩散创建一个独立的动画变量来控制渐变过渡点:

val sweepGradient = Brush.sweepGradient(
    colorStops = arrayOf(
        0.0f to mainGlowColor,
        animationProgress * 0.49f to Color.Transparent, // 动画控制透明起始点
        1.0f to Color.Transparent,
    ),
    center = centerOffset
)

添加动画后,灯管便有了“点亮”的生命力。

灯管点亮动画效果GIF

第五步:效果优化与最终打磨

为了追求更极致的视觉效果,在最终版本中可以进行两处关键优化:

  1. 增加色彩层次:添加另一个不同颜色的渐变层叠,让光效的色彩更丰富、更有层次感。
  2. 边缘柔化:我们不希望光效生硬地覆盖整个区域。可以通过添加一个从白色到透明的垂直渐变遮罩,并应用BlendMode.DstIn混合模式,来实现非常自然的边缘羽化效果。
val start = 20.dp.toPx()
val end = 450.dp.toPx()
val mask = Brush.verticalGradient(
    colorStops = arrayOf(
        0f to Color.White,
        start / size.height to Color.White,
        (start + (end - start) * 0.25f) / size.height to Color.White.copy(alpha = 0.7f),
        (start + (end - start) * 0.55f) / size.height to Color.White.copy(alpha = 0.35f),
        (start + (end - start) * 0.7f) / size.height to Color.White.copy(alpha = 0.15f),
        end / size.height to Color.Transparent,
        1f to Color.Transparent
    )
)
onDrawBehind {
    scale(
        scaleX = if (flip) -1f else 1f,
        scaleY = 1f
    ) {
        drawRect(
            brush = sweepGradient1,
            blendMode = BlendMode.Plus
        )
        drawRect(
            brush = sweepGradient2, // 第二个颜色层
            blendMode = BlendMode.Plus
        )
    }
    drawRect(
        brush = mask,
        blendMode = BlendMode.DstIn, // 应用遮罩实现柔化边缘
        alpha = 0.98f
    )
}

特别提醒:在使用透明度叠加和复杂混合模式时,别忘了添加compositingStrategy来正确离屏合成,否则可能会出现意外的渲染问题。

.graphicsLayer {
    compositingStrategy = CompositingStrategy.Offscreen
}

经过以上优化,并通过组合不同的颜色参数,我们便能得到非常迷人的光影效果。

多种颜色的最终灯管效果展示

第六步:完整代码实现

以下是整理好的、可直接使用的最终代码,包含了动画和边缘优化:

Light.kt (灯管可组合函数):

@Composable
fun Light(
    mainGlowColor: Color = Color(0xFFFF9800),
    glowColor: Color = Color(0xFFE91E63),
    flip: Boolean = true,
    startY: Float,
    progress: Float,
    modifier: Modifier,
    halfTubeWidth: Float,
) {
    val animationProgress = lerp(0.5f, 1f, easeIn(progress))
    Box(
        modifier = modifier
            .alpha(easeIn(progress))
            .graphicsLayer {
                compositingStrategy = CompositingStrategy.Offscreen
            }
            .drawWithCache {
                val centerOffset = Offset(
                    x = size.width - halfTubeWidth,
                    y = startY
                )
                val sweepGradient1 = Brush.sweepGradient(
                    colorStops = arrayOf(
                        0.0f to mainGlowColor,
                        animationProgress * 0.49f to Color.Transparent,
                        1.0f to Color.Transparent,
                    ),
                    center = centerOffset
                )
                val sweepGradient2 = Brush.sweepGradient(
                    colorStops = arrayOf(
                        0.0f to glowColor,
                        0.0f to glowColor.copy(alpha = animationProgress * 0.65f),
                        animationProgress * 0.35f to Color.Transparent,
                        1.0f to Color.Transparent,
                    ),
                    center = centerOffset
                )
                val start = 20.dp.toPx()
                val end = 450.dp.toPx()
                val mask = Brush.verticalGradient(
                    colorStops = arrayOf(
                        0f to Color.White,
                        start / size.height to Color.White,
                        (start + (end - start) * 0.25f) / size.height to Color.White.copy(alpha = 0.7f),
                        (start + (end - start) * 0.55f) / size.height to Color.White.copy(alpha = 0.35f),
                        (start + (end - start) * 0.7f) / size.height to Color.White.copy(alpha = 0.15f),
                        end / size.height to Color.Transparent,
                        1f to Color.Transparent
                    )
                )
                onDrawBehind {
                    scale(
                        scaleX = if (flip) -1f else 1f,
                        scaleY = 1f
                    ) {
                        drawRect(
                            brush = sweepGradient1,
                            blendMode = BlendMode.Plus
                        )
                        drawRect(
                            brush = sweepGradient2,
                            blendMode = BlendMode.Plus
                        )
                    }
                    drawRect(
                        brush = mask,
                        blendMode = BlendMode.DstIn,
                        alpha = 0.98f
                    )
                }
            }
    )
}

主界面调用示例:

package com.experiment.tubelighteffect

import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.experiment.tubelighteffect.tubelight.Light

@Composable
fun TubelightEffect(modifier: Modifier) {
    var clicked by remember { mutableStateOf(false) }
    val animProgress by animateFloatAsState(
        targetValue = if (clicked) 1f else 0f,
        animationSpec = tween(
            400,
            easing = FastOutSlowInEasing
        ),
        label = "click"
    )
    val animProgress2 by animateFloatAsState(
        targetValue = if (clicked) 1f else 0f,
        animationSpec = tween(
            700,
            easing = FastOutSlowInEasing
        ),
        label = "click"
    )
    var value by remember { mutableFloatStateOf(1.0f) }
    var value2 by remember { mutableFloatStateOf(1.0f) }
    val startY = 160f + 16 * animProgress
    val progress = animProgress2
    val tubeWidth = 300f * animProgress
    Box {
        Column(
            modifier = modifier
                .align(Alignment.BottomCenter),
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Button(
                modifier = Modifier.padding(bottom = 32.dp),
                onClick = {
                    clicked = !clicked
                }
            ) {
                Text("Start")
            }
        }
        Text(
            modifier = modifier
                .align(Alignment.TopCenter)
                .padding(top = 32.dp),
            text = "Tubelight Effect",
            color = Color(0xFFFF9800),
            style = MaterialTheme.typography.titleLarge
        )
        Row(
            modifier = modifier.fillMaxSize()
        ) {
            Light(
                modifier = Modifier
                    .weight(1f)
                    .fillMaxHeight(),
                flip = false,
                startY = startY,
                progress = progress,
                halfTubeWidth = tubeWidth
            )
            Light(
                modifier = Modifier
                    .weight(1f)
                    .fillMaxHeight(),
                startY = startY,
                progress = progress,
                halfTubeWidth = tubeWidth
            )
        }
    }
}

@Composable
@Preview
fun IneffectualnessPreview() {
    TubelightEffect(Modifier)
}

希望这篇教程能为你下一个炫酷的App界面带来灵感。这种对视觉效果的精雕细琢,正是前端与移动端开发中令人着迷的部分。如果你有更多关于UI动效的奇思妙想,欢迎在云栈社区与更多开发者交流探讨。

原文链接https://proandroiddev.com/how-to-create-a-tubelight-effect-in-android-compose-2383befc47b1




上一篇:从Docker部署到AI集成:n8n入门实战指南与本地模型调用
下一篇:从 pip 到 uv:Python 包管理工具的性能对比与实战迁移
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-2-9 01:57 , Processed in 0.302892 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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