如何在Android Jetpack Compose中使用渲染效果打造令人惊艳的视觉体验

学习示例:如何使用渲染效果来改变UI界面
效果图

引言

Jetpack Compose提供了各种工具和组件来构建引人入胜的UI,而在Compose中较为鲜为人知的一个宝藏是RenderEffect。

在这篇博文中,我们将通过创建一些渲染效果的酷炫示例来探索RenderEffect。

什么是RenderEffect?

RenderEffect允许你向UI组件应用视觉效果。这些效果可以包括模糊、自定义着色器或者你能想象到的任何其他视觉转换。然而,RenderEffect仅适用于API 31及以上版本。

在我们的示例中,我们将使用RenderEffect为我们的可扩展浮动按钮和一些额外组件创建模糊和着色器效果。

开始

BlurContainer

在第一阶段,让我们介绍“BlurContainer”。这个独特的组件为我们的用户界面增添了额外的视觉优雅和吸引力,创造出令人惊叹的视觉效果。

它包含一个自定义模糊修饰符,将我们的渲染效果提升到了新的水平。

@Composable
fun BlurContainer(
    modifier: Modifier = Modifier,
    blur: Float = 60f,
    component: @Composable BoxScope.() -> Unit,
    content: @Composable BoxScope.() -> Unit = {},
) {
    Box(modifier, contentAlignment = Alignment.Center) {
        Box(
            modifier = Modifier
                .customBlur(blur),
            content = component,
        )
        Box(
            contentAlignment = Alignment.Center
        ) {
            content()
        }
    }
}

fun Modifier.customBlur(blur: Float) = this.then(
    graphicsLayer {
        if (blur > 0f)
            renderEffect = RenderEffect
                .createBlurEffect(
                    blur,
                    blur,
                    Shader.TileMode.DECAL,
                )
                .asComposeRenderEffect()
    }
)
  • “customBlur”修饰符扩展接受一个模糊参数,用于指定模糊效果的强度。
  • 它被用来向可组合应用graphicsLayer,进而使用RenderEffect. createBlurEffect来应用模糊效果。graphicsLayer被用来将渲染效果应用到可组合中。
    以下是模糊效果的样子:
    BlurContainer’s Effect

通过这个修饰符,我们可以通过将其链接到现有的修饰符,轻松地向任何可组合添加模糊效果。

将渲染效果应用于父容器

为此,我们将使用自定义着色器——RuntimeShader和Jetpack Compose的graphicsLayer来在父容器中实现所需的视觉效果。
在我们深入了解渲染效果是如何应用之前,让我们先了解一下如何初始化RuntimeShader

@Language("AGSL")
const val ShaderSource = """
    uniform shader composable;
    
    uniform float visibility;
    
    half4 main(float2 cord) {
        half4 color = composable.eval(cord);
        color.a = step(visibility, color.a);
        return color;
    }
"""

val runtimeShader = remember {
    RuntimeShader(ShaderSource)
}

在这段代码片段中,我们创建了一个RuntimeShader实例。remember函数确保着色器只被初始化一次,避免不必要的开销。我们将自定义的着色器源代码(ShaderSource)传递给RuntimeShader的构造函数。

我们的ShaderSource是渲染效果的关键部分。它是用一种名为AGSL(Android Graphics Shading Language)的着色器语言编写的。让我们更仔细地看一下:

  • uniform shader composable:这一行声明了一个名为"composable"的统一着色器变量。如果我们想要应用渲染效果,这个变量将被用来取样可组合元素的颜色。
  • uniform float visibility:我们声明了一个名为"visibility"的统一浮点数变量。这个变量通过指定一个阈值来控制着色器效果的强度。
  • half4 main(float2 cord):main函数是着色器的入口点。它接受一个2D坐标(cord),并以half4的形式返回一个颜色,表示具有红色、绿色、蓝色和alpha分量的颜色。
  • half4 color = composable.eval(cord):在这里,我们从给定坐标处的"composable"着色器统一变量中取样颜色。
  • color.a = step(visibility, color.a):我们通过根据"visibility"阈值将alpha分量(color.a)设置为0或1来应用着色器效果。
  • return color:最后,我们返回修改后的颜色。

在JetLagged应用程序的compose-samples中查看AGSL Shader。

https://github.com/android/compose-samples/tree/main/JetLagged

应用渲染效果

有了我们的RuntimeShaderShaderSource准备就绪,现在我们可以使用graphicsLayer来应用渲染效果了:

Box(
    modifier
        .graphicsLayer {
            runtimeShader.setFloatUniform("visibility", 0.2f)
            renderEffect = RenderEffect
                .createRuntimeShaderEffect(
                    runtimeShader, "composable"
                )
                .asComposeRenderEffect()
        },
    content = content,
)

以下是它的工作原理:

  • runtimeShader.setFloatUniform("visibility", 0.2f):我们设置着色器中的"visibility"统一变量,以控制效果的强度。在这种情况下,我们将其设置为0.2f,但您可以调整此值以实现您期望的效果。
  • renderEffect = RenderEffect.createRuntimeShaderEffect(...):我们使用createRuntimeShaderEffect方法创建一个RenderEffect。该方法接受我们的runtimeShader和名称"composable",该名称对应于ShaderSource中的着色器变量。
  • asComposeRenderEffect():我们使用asComposeRenderEffect()RenderEffect转换为适用于Compose的格式。

通过在graphicsLayer中应用这个渲染效果,我们可以在包含在Box中的UI组件上实现着色器效果。

为了将所有这些元素整合在一起并无缝地应用我们的渲染效果,我们将创建一个名为ShaderContainer的可组合元素,具体如下:

@Language("AGSL")
const val Source = """
    uniform shader composable;
    
    uniform float visibility;
    
    half4 main(float2 cord) {
        half4 color = composable.eval(cord);
        color.a = step(visibility, color.a);
        return color;
    }
"""

@Composable
fun ShaderContainer(
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.() -> Unit,
) {
    val runtimeShader = remember {
        RuntimeShader(Source)
    }
    Box(
        modifier
            .graphicsLayer {
                runtimeShader.setFloatUniform("visibility", 0.2f)
                renderEffect = RenderEffect
                    .createRuntimeShaderEffect(
                        runtimeShader, "composable"
                    )
                    .asComposeRenderEffect()
            },
        content = content
    )
}

这是BlurContainer包裹在ShaderContainer中产生的视觉效果:
Parent

现在我们已经成功地通过ShaderContainerBlurContainer为我们的渲染效果奠定了基础,是时候通过打造ExtendedFabRenderEffect将它们整合在一起了。这个可组合元素将成为我们的可展开浮动按钮和动态渲染效果的核心。

ExtendedFabRenderEffect

ExtendedFabRenderEffect可组合元素负责协调整个用户界面、按钮展开的动画和处理渲染效果。让我们深入探讨它的工作原理,以及它如何创造视觉上令人愉悦的用户体验。

流畅的动画

创建流畅而流畅的动画对于精致的用户体验至关重要。我们应用alpha动画来实现这一点:

alpha动画管理按钮的透明度。当展开为true时,按钮变得完全不透明;否则,它们逐渐消失。与偏移动画一样,我们使用animateFloatAsState函数和适当的参数来确保平滑的过渡。

var expanded: Boolean by remember {
    mutableStateOf(false)
}

val alpha by animateFloatAsState(
  targetValue = if (expanded) 1f else 0f,
  animationSpec = tween(durationMillis = 1000, easing = LinearEasing),
  label = ""
)

组合效果

现在,我们将渲染效果ShaderContainer与按钮组合在一起,以创建一个统一的用户界面。在ShaderContaine内部,我们放置了几个ButtonComponent可组合元素,每个代表一个具有特定图标和交互的按钮。

ShaderContainer(
    modifier = Modifier.fillMaxSize()
) {

    ButtonComponent(
        Modifier.padding(
            paddingValues = PaddingValues(
                bottom = 80.dp
            ) * FastOutSlowInEasing
                .transform((alpha))
        ),
        onClick = {
            expanded = !expanded
        }
    ) {
        Icon(
            imageVector = Icons.Default.Edit,
            contentDescription = null,
            tint = Color.White,
            modifier = Modifier.alpha(alpha)
        )
    }

    ButtonComponent(
        Modifier.padding(
            paddingValues = PaddingValues(
                bottom = 160.dp
            ) * FastOutSlowInEasing.transform(alpha)
        ),
        onClick = {
            expanded = !expanded
        }
    ) {
        Icon(
            imageVector = Icons.Default.LocationOn,
            contentDescription = null,
            tint = Color.White,
            modifier = Modifier.alpha(alpha)
        )
    }

    ButtonComponent(
        Modifier.padding(
            paddingValues = PaddingValues(
                bottom = 240.dp
            ) * FastOutSlowInEasing.transform(alpha)
        ),
        onClick = {
            expanded = !expanded
        }
    ) {
        Icon(
            imageVector = Icons.Default.Delete,
            contentDescription = null,
            tint = Color.White,
            modifier = Modifier.alpha(alpha)
        )
    }

    ButtonComponent(
        Modifier.align(Alignment.BottomEnd),
        onClick = {
            expanded = !expanded
        },
    ) {
        val rotation by animateFloatAsState(
            targetValue = if (expanded) 45f else 0f,
            label = "",
            animationSpec = tween(1000, easing = FastOutSlowInEasing)
        )
        Icon(
            imageVector = Icons.Default.Add,
            contentDescription = null,
            modifier = Modifier.rotate(rotation),
            tint = Color.White
        )
    }
}

有了这个设置,ShaderContainer充当了我们按钮的背景,渲染效果通过ButtonComponent可组合元素无缝地应用到按钮上。alpha修饰符确保按钮根据展开状态变得可见或不可见,从而创建出精致而动态的用户界面。

按钮组件结构

ButtonComponent旨在封装可展开菜单中的每个按钮,它提供了定制按钮外观和行为的灵活性。

以下是ButtonComponent的结构:

@Composable
fun BoxScope.ButtonComponent(
    modifier: Modifier = Modifier,
    background: Color = Color.Black,
    onClick: () -> Unit,
    content: @Composable BoxScope.() -> Unit
) {
    // Applying the Blur Effect with the BlurContainer
    BlurContainer(
        modifier = modifier
            .clickable(
                interactionSource = remember {
                    MutableInteractionSource()
                },
                indication = null,
                onClick = onClick,
            )
            .align(Alignment.BottomEnd),
        component = {
            Box(
                Modifier
                    .size(40.dp)
                    .background(color = background, CircleShape)
            )
        }
    ) {
        // Content (Icon or other elements) inside the button
        Box(
            Modifier.size(80.dp),
            content = content,
            contentAlignment = Alignment.Center,
        )
    }
}

这样,我们已经从以上的代码中实现了期望的效果!
ExtendedFabRenderEffect

TextRenderEffect

TextRenderEffect的核心是动态文本显示。我们将使用一系列激励性的短语和引语来呈现给用户。这些短语将包括"实现你的目标"、"追逐你的梦想"等情感。

val animateTextList =
    listOf(
        "\"Reach your goals\"",
        "\"Achieve your dreams\"",
        "\"Be happy\"",
        "\"Be healthy\"",
        "\"Get rid of depression\"",
        "\"Overcome loneliness\""
    )

我们将创建一个textToDisplay状态变量来保存并显示这些短语,从而创建一个动态的序列。

文本动画化

为了使文本显示更吸引人,我们将利用一些关键动画:

  • 模糊效果:我们将对文本应用模糊效果。模糊值从0到30再返回0进行动画处理,采用线性缓动动画。这会创造出微妙而迷人的视觉效果,增强了文本的外观。
  • 文本转换:我们将使用LaunchedEffect循环显示短语列表中的每个短语,每个短语显示一定时间。当textToDisplay发生变化时,将发生一个scaleIn动画,呈现新文本的放大效果;随着过渡的结束,会应用一个scaleOut效果。这提供了一个视觉上令人愉悦的方式来引入和退出文本。

与ShaderContainer的完全集成

@Composable
fun TextRenderEffect() {

    val animateTextList =
        listOf(
            "\"Reach your goals\"",
            "\"Achieve your dreams\"",
            "\"Be happy\"",
            "\"Be healthy\"",
            "\"Get rid of depression\"",
            "\"Overcome loneliness\""
        )

    var index by remember {
        mutableIntStateOf(0)
    }

    var textToDisplay by remember {
        mutableStateOf("")
    }
    
    val blur = remember { Animatable(0f) }

    LaunchedEffect(textToDisplay) {
        blur.animateTo(30f, tween(easing = LinearEasing))
        blur.animateTo(0f, tween(easing = LinearEasing))
    }

    LaunchedEffect(key1 = animateTextList) {
        while (index <= animateTextList.size) {
            textToDisplay = animateTextList[index]
            delay(3000)
            index = (index + 1) % animateTextList.size
        }
    }

    ShaderContainer(
        modifier = Modifier.fillMaxSize()
    ) {
        BlurContainer(
            modifier = Modifier.fillMaxSize(),
            blur = blur.value,
            component = {
                AnimatedContent(
                    targetState = textToDisplay,
                    modifier = Modifier
                        .fillMaxWidth(),
                    transitionSpec = {
                        (scaleIn()).togetherWith(
                            scaleOut()
                        )
                    }, label = ""
                ) { text ->
                    Text(
                        modifier = Modifier
                            .fillMaxWidth(),
                        text = text,
                        style = MaterialTheme.typography.headlineLarge,
                        color = MaterialTheme.colorScheme.onPrimaryContainer,
                        textAlign = TextAlign.Center
                    )
                }
            }
        ) {}
    }
}

TextRenderEffect

ImageRenderEffect

在Jetpack Compose中,我们继续探索RenderEffect,这次着眼于引人入胜的ImageRenderEffect。这个可组合项通过引入动态图像过渡和迷人的渲染效果,将图像渲染提升到了一个新的层次。让我们深入了解其构造方式以及它如何增强视觉体验。

动态图像过渡

ImageRenderEffect的核心在于其能够以视觉上吸引人的方式在图像之间进行过渡。为了演示这一点,我们将设定一个基本情景,即在点击事件中,两个图像ic_firstic_second将交替显示。

var image by remember {
    mutableIntStateOf(R.drawable.ic_first)
}

图像状态变量保存当前显示的图像,用户只需简单点击按钮,就可以在两者之间切换。

打造引人入胜的效果

模糊效果:就像我们之前的示例一样,我们对图像应用模糊效果。模糊值从0到100再返回0进行动画处理,创造出令人着迷的视觉效果,增强了图像过渡效果。

val blur = remember { Animatable(0f) }
LaunchedEffect(image) {
    blur.animateTo(100f, tween(easing = LinearEasing))
    blur.animateTo(0f, tween(easing = LinearEasing))
}
  • 图像过渡:图像过渡的核心是AnimatedContent可组合项。它处理图像之间的平滑过渡,结合了fadeInscaleIn效果,用于进入场景的图像,以及fadeOutscaleOut效果,用于退出场景的图像。
AnimatedContent(
    targetState = image,
    modifier = Modifier.fillMaxWidth(),
    transitionSpec = {
        (fadeIn(tween(easing = LinearEasing)) + scaleIn(
            tween(1_000, easing = LinearEasing)
        )).togetherWith(
            fadeOut(
                tween(1_000, easing = LinearEasing)
            ) + scaleOut(
                tween(1_000, easing = LinearEasing)
            )
        )
    }, label = ""
) { image ->
    Image(
        painter = painterResource(id = image),
        modifier = Modifier.size(200.dp),
        contentDescription = ""
    )
}

ShaderContainer的完美集成

就像我们之前的示例一样,ImageRenderEffect也集成在ShaderContainer中。这使我们能够融合图像过渡和渲染效果,创造出引人入胜、沉浸式的视觉体验。

@Composable
fun ImageRenderEffect() {

    var image by remember {
        mutableIntStateOf(R.drawable.ic_first)
    }

    val blur = remember { Animatable(0f) }

    LaunchedEffect(image) {
        blur.animateTo(100f, tween(easing = LinearEasing))
        blur.animateTo(0f, tween(easing = LinearEasing))
    }

    Column(
        modifier = Modifier
            .wrapContentSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
    ) {

        ShaderContainer(
            modifier = Modifier
                .animateContentSize()
                .clipToBounds()
                .fillMaxWidth()
        ) {

            BlurContainer(
                modifier = Modifier.fillMaxWidth(),
                blur = blur.value,
                component = {
                    AnimatedContent(
                        targetState = image,
                        modifier = Modifier
                            .fillMaxWidth(),
                        transitionSpec = {
                            (fadeIn(tween(easing = LinearEasing)) + scaleIn(
                                tween(
                                    1_000,
                                    easing = LinearEasing
                                )
                            )).togetherWith(
                                fadeOut(
                                    tween(
                                        1_000,
                                        easing = LinearEasing
                                    )
                                ) + scaleOut(
                                    tween(
                                        1_000,
                                        easing = LinearEasing
                                    )
                                )
                            )
                        }, label = ""
                    ) { image ->
                        Image(
                            painter = painterResource(id = image),
                            modifier = Modifier
                                .size(200.dp),
                            contentDescription = ""
                        )
                    }
                }) {}
        }

        Spacer(modifier = Modifier.height(20.dp))

        Button(
            onClick = {
                image =
                    if (image == R.drawable.ic_first) R.drawable.ic_second else R.drawable.ic_first
            },
            colors = ButtonDefaults.buttonColors(
                containerColor = Color.Black
            )
        ) {
            Text("Change Image")
        }
    }
}

ImageRenderEffect

结论

通过了解ShaderContainerBlurContainerShaderSource以及customBlur修饰符,您拥有了在Jetpack Compose应用程序中创建令人惊叹的渲染效果的工具。这些元素为探索和尝试各种视觉效果和自定义着色器提供了基础,为您的UI设计打开了创意可能性的世界。

Github

https://github.com/cp-megh-l/render-effect-jetpack-compose

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐