Jetpack compose中实现流畅的Theme选择器动画

news2024/9/26 3:30:45

Jetpack compose中实现流畅的Theme Picker动画

Jetpack Compose改变了我们在Android上管理主题的方式。它提供了更大的灵活性,使我们能够以更多的方式定义用户界面(UI)。此外,Compose中的动画系统使我们能够轻松创建令人印象深刻和愉悦的UI动画。

在本教程中,我将结合这两个特性,创建一个在主题之间过渡的动画。最终结果将如下所示:

为了创建这个动画,我们将使用AnimatedContent。它是一种工具,可以根据状态显示不同的内容。当状态发生变化时,内容会平稳地从前一个状态过渡到新的状态。首先,我们应该创建一个包含当前主题数据的对象,并将其作为状态传递进去。

data class CustomTheme(
    val primaryColor: Color,
    val backgroundColor: Color,
    val textColor: Color,
    val image: Int,
)

val darkTheme = CustomTheme(
    primaryColor = Color(0xFFE9B518),
    backgroundColor = Color(0xFF111111),
    textColor = Color(0xFFE8C660),
    image = R.drawable.dark,
)

val lightTheme = CustomTheme(
    primaryColor = Color(0xFFFFFFFF),
    backgroundColor = Color(0xFFF1F1F1),
    textColor = Color(0xFF232526),
    image = R.drawable.light,
)

val pinkTheme = CustomTheme(
    primaryColor = Color(0xFFF01EE5),
    backgroundColor = Color(0xFF110910),
    textColor = Color(0xFFEE8CE1),
    image = R.drawable.pink,
)

在这段代码中,我创建了一个数据类和三个具有不同颜色的主题。

现在,我们可以使用AnimatedContent,并将此对象设置为状态。

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun ThemePicker() {
    var theme by remember { mutableStateOf(lightTheme) }
    AnimatedContent(
        targetState = theme,
        modifier = Modifier
            .background(Color.Black)
            .fillMaxSize(),
    ) { currentTheme ->
        Surface(
            modifier = Modifier
                .fillMaxSize(),
            color = currentTheme.backgroundColor
        ) {
            Box {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(300.dp)
                ) {
                    Image(
                        painter = painterResource(id = currentTheme.image),
                        contentDescription = "headerImage",
                        contentScale = ContentScale.Crop,
                    )
                    Box(
                        modifier = Modifier
                            .fillMaxSize()
                            .background(
                                brush = Brush.verticalGradient(
                                    colors = listOf(
                                        Color.Transparent,
                                        currentTheme.backgroundColor.copy(alpha = .2f),
                                        currentTheme.backgroundColor
                                    )
                                )
                            )
                    )
                }

                Row(
                    modifier = Modifier
                        .align(Alignment.Center),
                    horizontalArrangement = Arrangement.Center,
                    verticalAlignment = Alignment.CenterVertically,
                ) {

                    ThemeButton(
                        theme = lightTheme,
                        currentTheme = currentTheme,
                        text = "Light",
                    ) {
                        theme = lightTheme
                    }

                    ThemeButton(
                        theme = darkTheme,
                        currentTheme = currentTheme,
                        text = "Dark",
                    ) {
                        theme = darkTheme
                    }

                    ThemeButton(
                        theme = pinkTheme,
                        currentTheme = currentTheme,
                        text = "Pink",
                    ) {
                        theme = pinkTheme
                    }
                }
            }
        }
    }
}

初始状态的主题被设置并赋予AnimatedContent。在内容中,我们使用currentTheme来为用户界面进行样式设置。使用currentTheme而不只是"theme"是很重要的,这样在状态改变时可以避免过去内容的突然变化。用户界面包括一个头部图片和三个按钮,用于在不同主题之间进行切换。在这个阶段,我们将会看到以下这样的动画:

这是与AnimatedContent一起提供的标准动画。

transitionSpec = {  
    fadeIn(  
        initialAlpha = 0f,  
        animationSpec = tween(100)  
    ) with fadeOut(  
        targetAlpha = .9f,  
        animationSpec = tween(800)  
    ) + scaleOut(  
        targetScale = .95f,  
        animationSpec = tween(800)  
    )  
}

可以,但我们需要修改它以实现最终结果中的循环揭示动画。

为了在AnimatedContent中实现所需的效果,我们需要一种特定的动画。新内容将快速淡入,而旧内容则会逐渐淡出并在较长时间内进行缩小。新内容快速淡入的目的是为了在揭示动画中立即开始。在AnimatedContent中切换状态时,新内容被视为完全新的组件,这会触发其自己的LaunchedEffect。我们将从这一点开始启动动画,并利用该值在新内容上进行一个圆形裁剪动画。

...
    var theme by remember { mutableStateOf(lightTheme) }
    var animationOffset by remember { mutableStateOf(Offset(0f, 0f)) }

    AnimatedContent(
        ...
    ) { currentTheme ->

        val configuration = LocalConfiguration.current
        val screenHeight = configuration.screenHeightDp.dp * 0.49f

        val revealSize = remember { Animatable(1f) }
        LaunchedEffect(key1 = "reveal", block = {
            if (animationOffset.x > 0f) {
                revealSize.snapTo(0f)
                revealSize.animateTo(
                    1f,
                    tween(800)
                )
            } else revealSize.snapTo(1f)
        })

        Box(
            modifier = Modifier
                .fillMaxSize()
                .clip(CirclePath(revealSize.value, animationOffset))
        ){  
        Surface(
...

animationOffset状态确定圆形动画的起始位置。我们将在ThemeButton内部设置它。revealSize控制剪裁新内容的动画圆的大小。在LaunchedEffect中,如果有有效的起始点,我们会启动圆形剪裁动画。如果没有有效的起始点,则表示这是打开屏幕时的初始组合,我们只需将动画快速切换到结尾。然后,我们用剪裁框将Surface包围起来。需要注意的是,我们使用自定义形状而不是默认的CircleShape,后者是带有大半径的圆角矩形。我需要一个与默认形状无法实现的不同外观。

class CirclePath(
    private val progress: Float,
    private val origin: Offset = Offset(0f, 0f),
): Shape {

    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val center = Offset(
            x = size.center.x - ((size.center.x - origin.x) * (1f - progress)),
            y = size.center.y - ((size.center.y - origin.y) * (1f - progress)),
        )
        val radius = (sqrt(
            size.height * size.height + size.width * size.width
        ) * .5f) * progress

        return Outline.Generic(
            Path().apply {
                addOval(
                    Rect(
                        center = center,
                        radius = radius
                    )
                )
            }
        )
    }

}

The CirclePath形状需要一个十进制数来表示当前进度和动画的起始点。这些值连同大小一起用于创建一个动画,显示出覆盖整个内容的圆形形状。最后,当点击按钮时,我们需要指定动画应该从哪里开始。这些信息存储在ThemeButton中,并在点击按钮时发送。

@Composable
fun ThemeButton(
    theme: CustomTheme,
    currentTheme: CustomTheme,
    text: String = "Pink Theme",
    onClick: (Offset) -> Unit = {}
) {
    val isSelected = theme == currentTheme
    var offset: Offset = remember { Offset(0f, 0f) }
    Column(
        horizontalAlignment = Alignment
            .CenterHorizontally,
    ) {
        Box(
            modifier = Modifier
                .onGloballyPositioned {
                    offset = Offset(
                        x = it.positionInWindow().x + it.size.width / 2,
                        y = it.positionInWindow().y + it.size.height / 2,
                    )
                }
                .size(110.dp)
                .border(
                    4.dp,
                    color = if (isSelected) theme.primaryColor else Color.Transparent,
                    shape = CircleShape
                )
                .padding(8.dp)
                .background(color = theme.primaryColor, shape = CircleShape)
                .clip(CircleShape)
                .clickable { onClick(offset) }
        ) {
            Image(
                painter = painterResource(id = theme.image),
                contentDescription = "themeImage",
                contentScale = ContentScale.Crop,
                modifier = Modifier.fillMaxSize()
            )
        }
        Spacer(modifier = Modifier.height(8.dp))
        Text(
            text = text.uppercase(),
            color = currentTheme.textColor,
            fontWeight = FontWeight.Bold,
            fontSize = 14.sp,
            modifier = Modifier
                .padding(2.dp)
                .alpha(if (isSelected) 1f else .5f)
        )
    }
}

这是对ThemButton的解释。当点击按钮时,距离中心的距离被发送。我们可以将这个距离作为开始圆形展示动画的起点,就像这样:

ThemeButton(  
    ...  
) {  
    animationOffset = it  
	theme = lightTheme  
}  
  
ThemeButton(  
    ...
) {  
    animationOffset = it  
	theme = darkTheme  
}  
  
ThemeButton(  
    ... 
) {  
    animationOffset = it  
	theme = pinkTheme  
}

那就这样!我们为选择主题创建了一种特别的动画,我们的用户一定会喜欢。

此外,我还为底部样式创建了一个主题选择器,提供了额外的功能。
最终效果展示

GitHub

https://github.com/sanathsajeevakumara/ThemePickerAnimation

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/688778.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何训练全自动的安卓ai脚本(yolov5 为例) 实现游戏应用自动屏幕识别点击

必要资源 yolo训练方法,调参要点:https://docs.ultralytics.com/ncnn yolov5 示例:https://github.com/nihui/ncnn-android-yolov5在线模型转换:https://convertmodel.com/ 硬件配备 32G 内存, 2060 英伟达显卡 操作步骤 1.准备好数据集&#xff0c…

管理类联考——逻辑——知识篇——形式逻辑——五、联言选言——haimian

联言&选言 考点分析 考点分析 削弱 年度 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023题量6222111 联言 本质定义 联言命题是断定两种或两种以上事物情况同时存在的命题,用“A并且B”表示,逻辑符号为A ∧ B。 若“A ∧ B”为真…

为什么uCOSii的栈顶不再是0x20000000

我将FreeRTOS的工程文件移植到基于uCOSii系统中,发现两个系统生成的栈顶地址不一样,即使栈的大小相同,都是用Keil编译器,差别很大。见下图: Stack_Size EQU 0x00001000; 以前一直使用FreeRTOS系统&#xff0c…

简单易懂:Vue3框架三天速成(一)

前言:学习Vue框架首先需要具备基本的HTML5、CSS3、JavaScript基础,了解基本概念以及用法再来学习Vue会事半功倍! 一、初识Vue Vue.js(读音 /vjuː/, 类似于 view) 是一套构建用户界面的渐进式框架。Vue 只关注视图层&a…

【服务器数据恢复】raid5故障导致LUN无法访问的数据恢复案例

服务器数据恢复环境: 一台服务器中有一组由数块SAS硬盘组建的RAID5阵列,阵列中有1块热备盘,上层部署OA以及Oracle数据库。 服务器故障: 该磁盘阵列中有2块硬盘出现故障先后离线,RAID5阵列瘫痪,上层LUN无法…

使用记事本编写第一个GO程序

开发环境: go1.18.3 记事本 先来看一下要编写的第一个hello,world Go程序 package main import "fmt"func main() {/* this is my first Go program*/fmt.Println("hello,world") } 第一行代码 package main定义了域名,你必须在源文…

设计模式->观察者设计模式和订阅者发布者设计模式的区别

设计模式->观察者设计模式和订阅者发布者设计模式的区别 一、先复习一下观察者设计模式的相关定义,优点,以及缺点1.定义观察者模式的三个典型例子 2.优点3.缺点4.观察者设计模式的主要角色5.代码举例完整代码 二、回答问题:观察者设计模式和订阅者发布者设计模式的区别 一、…

【Java-SpringBoot+Vue+MySql】项目开发综合—经验总结

目录 框架: 编程思维: MVC架构: 前端——组件式开发 开发思路梳理: 后端—— 前端—— 效果图 信息列表: 修改用户​编辑 新增用户 删除用户 数据清空 批量上传 框架: 后端:JAVA-SpringBoot2.6、包管理器M…

13.RocketMQ之消息的存储与发送

1. 消息存储 1.1 消息存储 分布式队列因为有高可靠性的要求,所以数据要进行持久化存储。 消息生成者发送消息Broker收到消息,将消息进行持久化,在存储中新增一条记录返回ACK给生产者Broker消息给对应的消费者,然后等待消费者返回A…

Keras-4-深度学习用于计算机视觉-猫狗数据集训练卷积网络

0. 说明: 本篇学习记录主要包括:《Python深度学习》的第5章(深度学习用于计算机视觉)的第2节(在小型数据集上从头开始训练一个卷积神经网络)内容。 相关知识点: 从头训练卷积网络&#xff1b…

AI 绘画用 Stable Diffusion 图生图局部重绘功能给美女换装(这是我能看的嘛)

昨天带大家一起装好了 Stable Diffusion 的环境,今天就来带大家一起体验一下 Stable Diffusion 的局部重绘功能。 没装好环境的可以看上一篇:AI 绘画基于 Kaggle 10 分钟搭建 Stable Diffusion(保姆级教程) Stable Diffusion 的…

可重入,可打断,公平锁,条件变量原理解读

目录 可重入原理 可打断原理 不可打断模式 可打断模式 公平锁实现原理 条件变量实现原理 await 流程 signal 流程 可重入原理 什么是可重入:当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁…

阿里刚换帅,京东忙换将:新时代号角吹响

6月26日早间,京东物流在港交所发布公告称,京东物流CEO余睿因个人身体原因辞任执行董事、首席执行官及授权代表,原京东产发CEO胡伟将担任京东物流CEO。 同时,据《科创板日报》报道,京东集团将新成立创新零售部&#xf…

【论文笔记】Fast Segment Anything

我说个数:一个月5篇基于Fast Segment Anything的改进的论文就会出现哈哈哈哈。 1.介绍 1.1 挑战 SAM架构的主要部分Transformer(ViT)模型相关的大量计算资源需求,这给其实际部署带来了障碍 1.2 任务解耦 将分段任意任务解耦为…

正确认识:1189194-65-7,DOTA-CH2-Alkynyl (TFA salt),试剂的结构式和CAS

文章关键词:双功能螯合剂,大环配体,标记螯合剂修饰 【产品描述】 DOTA-CH2-Alkynyl (TFA salt)中TFA是一种强酸。它可以质子化任何氨基。盐酸也是这样。在纯化多肽过程中的反相HPLC,有一种技术是阴离子交换。将多肽加载在柱子上&a…

MySql基础教程(三):创建数据表、数据增删改查、删除数据表

MySql基础教程(三):创建数据表、数据增删改查、删除数据表 1、创建数据表 创建MySQL数据表需要以下信息: 表名表字段名定义每个表字段 1.1 语法 下面是创建MySQL数据表的SQL通用语法: CREATE TABLE table_name (column_name column_typ…

无线蓝牙通信有关(NRF2401模块)的功耗,通道频率等

参考: ISM频段 Industrial Scientific Medical,ISM(工业、科学、医疗)频段为国际电信联盟(ITU)《无线电规则》定义的指定无线电频段。 Frequency-Shift Keying 数字调制技术(FSK调制) 将需要…

又是一年毕业季,准备好踏入职场了吗?

文章目录 一、大学时光二、给毕业生的一些建议三、职场中的经验分享四、程序员未来职业规划 一、大学时光 作为一名程序员,大学时光是我职业生涯中最重要的时期之一。这四年的大学,我不仅学到了计算机科学的理论知识,还积累了丰富的编程经验…

tqdm:python的简单可视化进度

tqdm:python的简单可视化进度 说明 ​ 本篇文章的主要目的是快速上手使用,而不是解析源码。 目录结构 文章目录 tqdm:python的简单可视化进度1. 应用场景2. 库安装3. 方法速览4. 案例5. 总结 1. 应用场景 ​ 进度条应用的场景很多&#xff0…

YOLOv8独家原创改进:独家首发最新原创XIoU_NMS改进点,改进有效可以直接当做自己的原创改进点来写,提升网络模型性能、收敛速度和鲁棒性

💡该教程为属于《芒果书》📚系列,包含大量的原创首发改进方式, 所有文章都是全网首发原创改进内容🚀 💡本篇文章为YOLOv8独家原创改进:独家首发最新原创XIoU_NMS改进点,改进有效可以直接当做自己的原创改进点来写,提升网络模型性能、收敛速度和鲁棒性。 💡对自己…