当前位置: 首页 > news >正文

Android compose 可见性动画未执行问题修复

接着修改待办事项demo, 动画有问题, 导致初始不显示数据,其实数据库是有数据的。原代码如下:

package com.example.testcompose1 import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.example.testcompose1.data.TodoEntity import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.Collections.rotate @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun TodoListScreen( viewModel: TodoViewModel, // 获取ViewModel实例。 在同一个activity作用域中是单例。 settingsViewModel: SettingsViewModel, onNavigateToDetail: (Int) -> Unit = {} ) { // val screenWidth = LocalConfiguration.current.screenWidthDp.dp.value val configuration = LocalConfiguration.current val density = LocalDensity.current val screenWidthPx = with(density) { configuration.screenWidthDp.dp.toPx() } val offsetX = -(screenWidthPx * 3).toInt() // 从屏幕左侧3倍宽度外滑入 var showInfiniteList by remember { mutableStateOf(false) } if (showInfiniteList) { // 显示无限滚动列表,并提供一个返回按钮 Scaffold( topBar = { TopAppBar( title = { Text("无限滚动列表") }, navigationIcon = { IconButton(onClick = { showInfiniteList = false }) { Icon(Icons.Default.ArrowBack, contentDescription = "返回") } } ) } ) { innerPadding -> // 给 InfiniteListPage 添加内边距 Box(modifier = Modifier.padding(innerPadding)) { InfiniteListPage() } } } else { // 显示原待办事项列表 // 使用 remember 和 mutableStateOf 保存输入框的文本 var text by remember { mutableStateOf("") } // 使用 mutableStateListOf 保存待办项列表 // val todoItems = remember { mutableStateListOf<String>() } // 将 StateFlow 转换为 Compose 可观察的 State // val todoItems by viewModel.todoItems.collectAsState() val todos by viewModel.todos.collectAsState() // 获取协程作用域,用于延迟删除 val scope = rememberCoroutineScope() // 管理每个项的可见性,初始为 true,新添加的项先设为 false,然后立即设为 true val itemVisibility = remember { mutableStateMapOf<Int, Boolean>() } // key改为用id // 同步 itemVisibility 与 todoItems:为新增项添加初始 false,并在下一帧设为 true LaunchedEffect(todos) { todos.forEach { todo -> if (!itemVisibility.containsKey(todo.id)) { // 新项:初始不可见 itemVisibility[todo.id] = false // 等待一帧,然后设为可见,触发进入动画 launch { delay(50) // 短暂延迟,确保重组 itemVisibility[todo.id] = true } } } // 清理已删除的项 // itemVisibility.keys.retainAll(todoItems.toSet()) val currentIds = todos.map { it.id }.toSet() itemVisibility.keys.retainAll(currentIds) } Column(modifier = Modifier.padding(16.dp)) { ThemeSwitch(settingsViewModel) // 添加开关 Spacer(modifier = Modifier.height(8.dp)) // 文本输入框 TextField( value = text, onValueChange = { text = it }, // 反向绑定,视图变化--> 数据变化 label = { Text("输入待办事项") }, colors = TextFieldDefaults.colors( focusedContainerColor = MaterialTheme.colorScheme.surface, // 获得焦点时的背景色 unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant, // 失去焦点时,输入框背景色 focusedIndicatorColor = MaterialTheme.colorScheme.primary, // 输入框底部下划线的颜色。 unfocusedIndicatorColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) ), modifier = Modifier.fillMaxWidth() ) // 添加按钮 Button( onClick = { viewModel.addTodo(text) text = "" }, shape = MaterialTheme.shapes.small, // 使用主题形状 colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primary, // 容器背景色,按钮底色 contentColor = MaterialTheme.colorScheme.onPrimary // 内容颜色,按钮上文字 / 图标的颜色 ), modifier = Modifier.padding(top = 8.dp) ) { Text("添加") } // 显示待办列表 Spacer(modifier = Modifier.height(16.dp)) Text("待办列表", style = MaterialTheme.typography.titleMedium) LazyColumn { items(items = todos ,key = { it.id }) // 使用唯一 id 作为 key,确保动画正确识别 { todo -> val visible = itemVisibility[todo.id] ?: true // 为每个项添加动画。 AnimatedVisibility没起作用 AnimatedVisibility( visible = visible, enter = fadeIn(animationSpec = tween(1500, easing = FastOutSlowInEasing)) + slideInHorizontally( initialOffsetX = { -3000 }, // 固定大偏移量,从左侧 3000 像素外滑入 animationSpec = tween(1500, easing = FastOutSlowInEasing) ) + scaleIn( initialScale = 0.1f, animationSpec = tween(1500, easing = FastOutSlowInEasing) ), exit = fadeOut(animationSpec = tween(500)) + slideOutHorizontally(targetOffsetX = { 200 } , animationSpec = tween(500)) ) { // SideEffect 是一个专门用于执行副作用的可组合函数。它的主要作用是在每次 重组(recomposition) 时,安全地执行那些不直接影响 UI、但需要与外部系统交互的操作(例如日志记录、埋点、更新非 Compose 管理的状态等)。 SideEffect { println("Item ${todo.title} 显示动画执行") } TodoItemRow(todo = todo , onDelete = { // 触发删除动画 itemVisibility[todo.id] = false scope.launch { delay(500) // viewModel.removeItem(item) // deletingItems = deletingItems - item viewModel.deleteTodo(todo) // 清理状态由 LaunchedEffect 的 retainAll 负责 } } ,onToggle = { viewModel.toggleComplete(todo) // 切换完成状态 } , onClick = { onNavigateToDetail(todo.id) }// 点击跳转 ) } } } } } } @Composable fun TodoItemRow( todo: TodoEntity , onDelete: () -> Unit // 添加删除回调,删除逻辑放在上层。即把回调传给里面的按钮。 ,onToggle: () -> Unit , onClick: () -> Unit , modifier: Modifier = Modifier ) { Card( modifier = modifier .fillMaxWidth() .padding(vertical = 4.dp) .clickable { onClick() }, // 现在 modifier 应该会叠加动画修饰符 elevation = CardDefaults.cardElevation( defaultElevation = 2.dp // 这里传你要的默认高度 ), shape = MaterialTheme.shapes.medium, // 使用主题形状 colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ) ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween // 横向布局子元素两端对齐,剩余空白空间平均分配到子元素之间 ) { // 新增Checkbox,切换事项是否已完成的状态 Checkbox( checked = todo.isCompleted, onCheckedChange = { onToggle() } ) Text(text = todo.title ,style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface ,textDecoration = if (todo.isCompleted) TextDecoration.LineThrough else null // LineThrough是中划线 ) IconButton(onClick = onDelete) { Icon(Icons.Default.Delete, contentDescription = "删除" , tint = MaterialTheme.colorScheme.error) } } } } // 主题切换开关 @Composable fun ThemeSwitch(settingsViewModel: SettingsViewModel) { val isDarkTheme by settingsViewModel.isDarkTheme.collectAsState() Row( modifier = Modifier .fillMaxWidth() .clip(MaterialTheme.shapes.medium) .background(MaterialTheme.colorScheme.surface) .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( text = "深色模式", style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface ) Switch( checked = isDarkTheme, onCheckedChange = { settingsViewModel.toggleDarkMode() } ) } } // 为了允许手动切换深色/浅色模式,在应用中保存用户的选择,并在主题中读取. 后面改用DataStore保存 //object ThemeManager { // var isDarkTheme by mutableStateOf(false) // private set // // fun toggleTheme() { // 切换是否为深色主题 // isDarkTheme = !isDarkTheme // } //}

修改后:

package com.example.testcompose1 import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.example.testcompose1.data.TodoEntity import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.Collections.rotate @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun TodoListScreen( viewModel: TodoViewModel, // 获取ViewModel实例。 在同一个activity作用域中是单例。 settingsViewModel: SettingsViewModel, onNavigateToDetail: (Int) -> Unit = {} ) { // val screenWidth = LocalConfiguration.current.screenWidthDp.dp.value val configuration = LocalConfiguration.current val density = LocalDensity.current val screenWidthPx = with(density) { configuration.screenWidthDp.dp.toPx() } val offsetX = -(screenWidthPx * 3).toInt() // 从屏幕左侧3倍宽度外滑入 var showInfiniteList by remember { mutableStateOf(false) } if (showInfiniteList) { // 显示无限滚动列表,并提供一个返回按钮 Scaffold( topBar = { TopAppBar( title = { Text("无限滚动列表") }, navigationIcon = { IconButton(onClick = { showInfiniteList = false }) { Icon(Icons.Default.ArrowBack, contentDescription = "返回") } } ) } ) { innerPadding -> // 给 InfiniteListPage 添加内边距 Box(modifier = Modifier.padding(innerPadding)) { InfiniteListPage() } } } else { // 显示原待办事项列表 // 使用 remember 和 mutableStateOf 保存输入框的文本 var text by remember { mutableStateOf("") } // 使用 mutableStateListOf 保存待办项列表 // val todoItems = remember { mutableStateListOf<String>() } // 将 StateFlow 转换为 Compose 可观察的 State // val todoItems by viewModel.todoItems.collectAsState() val todos by viewModel.todos.collectAsState() // 获取协程作用域,用于延迟删除 val scope = rememberCoroutineScope() // 管理每项的 AnimatedVisibility;新 id 先 false 再 true 以触发进入动画 val itemVisibility = remember { mutableStateMapOf<Int, Boolean>() } // 必须用 collect 持续监听,不能用 LaunchedEffect(todos):Room 每次发射新 List 都会让 // LaunchedEffect 重启并取消子协程,导致 delay(50) 里「设为可见」永远跑不完,界面一直空白。 LaunchedEffect(Unit) { viewModel.todos.collect { current -> current.forEach { todo -> if (!itemVisibility.containsKey(todo.id)) { itemVisibility[todo.id] = false launch { delay(50) itemVisibility[todo.id] = true } } } itemVisibility.keys.retainAll(current.map { it.id }.toSet()) } } Column(modifier = Modifier.padding(16.dp)) { ThemeSwitch(settingsViewModel) // 添加开关 Spacer(modifier = Modifier.height(8.dp)) // 文本输入框 TextField( value = text, onValueChange = { text = it }, // 反向绑定,视图变化--> 数据变化 label = { Text("输入待办事项") }, colors = TextFieldDefaults.colors( focusedContainerColor = MaterialTheme.colorScheme.surface, // 获得焦点时的背景色 unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant, // 失去焦点时,输入框背景色 focusedIndicatorColor = MaterialTheme.colorScheme.primary, // 输入框底部下划线的颜色。 unfocusedIndicatorColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) ), modifier = Modifier.fillMaxWidth() ) // 添加按钮 Button( onClick = { viewModel.addTodo(text) text = "" }, shape = MaterialTheme.shapes.small, // 使用主题形状 colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primary, // 容器背景色,按钮底色 contentColor = MaterialTheme.colorScheme.onPrimary // 内容颜色,按钮上文字 / 图标的颜色 ), modifier = Modifier.padding(top = 8.dp) ) { Text("添加") } // 显示待办列表 Spacer(modifier = Modifier.height(16.dp)) Text("待办列表", style = MaterialTheme.typography.titleMedium) LazyColumn { items(items = todos ,key = { it.id }) // 使用唯一 id 作为 key,确保动画正确识别 { todo -> val visible = itemVisibility[todo.id] ?: true // 为每个项添加动画。 AnimatedVisibility没起作用 AnimatedVisibility( visible = visible, enter = fadeIn(animationSpec = tween(1500, easing = FastOutSlowInEasing)) + slideInHorizontally( initialOffsetX = { -3000 }, // 固定大偏移量,从左侧 3000 像素外滑入 animationSpec = tween(1500, easing = FastOutSlowInEasing) ) + scaleIn( initialScale = 0.1f, animationSpec = tween(1500, easing = FastOutSlowInEasing) ), exit = fadeOut(animationSpec = tween(500)) + slideOutHorizontally(targetOffsetX = { 200 } , animationSpec = tween(500)) ) { // SideEffect 是一个专门用于执行副作用的可组合函数。它的主要作用是在每次 重组(recomposition) 时,安全地执行那些不直接影响 UI、但需要与外部系统交互的操作(例如日志记录、埋点、更新非 Compose 管理的状态等)。 SideEffect { println("Item ${todo.title} 显示动画执行") } TodoItemRow(todo = todo , onDelete = { // 触发删除动画 itemVisibility[todo.id] = false scope.launch { delay(500) // viewModel.removeItem(item) // deletingItems = deletingItems - item viewModel.deleteTodo(todo) // 清理状态由 LaunchedEffect 的 retainAll 负责 } } ,onToggle = { viewModel.toggleComplete(todo) // 切换完成状态 } , onClick = { onNavigateToDetail(todo.id) }// 点击跳转 ) } } } } } } @Composable fun TodoItemRow( todo: TodoEntity , onDelete: () -> Unit // 添加删除回调,删除逻辑放在上层。即把回调传给里面的按钮。 ,onToggle: () -> Unit , onClick: () -> Unit , modifier: Modifier = Modifier ) { Card( modifier = modifier .fillMaxWidth() .padding(vertical = 4.dp) .clickable { onClick() }, // 现在 modifier 应该会叠加动画修饰符 elevation = CardDefaults.cardElevation( defaultElevation = 2.dp // 这里传你要的默认高度 ), shape = MaterialTheme.shapes.medium, // 使用主题形状 colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ) ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween // 横向布局子元素两端对齐,剩余空白空间平均分配到子元素之间 ) { // 新增Checkbox,切换事项是否已完成的状态 Checkbox( checked = todo.isCompleted, onCheckedChange = { onToggle() } ) Text(text = todo.title ,style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface ,textDecoration = if (todo.isCompleted) TextDecoration.LineThrough else null // LineThrough是中划线 ) IconButton(onClick = onDelete) { Icon(Icons.Default.Delete, contentDescription = "删除" , tint = MaterialTheme.colorScheme.error) } } } } // 主题切换开关 @Composable fun ThemeSwitch(settingsViewModel: SettingsViewModel) { val isDarkTheme by settingsViewModel.isDarkTheme.collectAsState() Row( modifier = Modifier .fillMaxWidth() .clip(MaterialTheme.shapes.medium) .background(MaterialTheme.colorScheme.surface) .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( text = "深色模式", style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface ) Switch( checked = isDarkTheme, onCheckedChange = { settingsViewModel.toggleDarkMode() } ) } } // 为了允许手动切换深色/浅色模式,在应用中保存用户的选择,并在主题中读取. 后面改用DataStore保存 //object ThemeManager { // var isDarkTheme by mutableStateOf(false) // private set // // fun toggleTheme() { // 切换是否为深色主题 // isDarkTheme = !isDarkTheme // } //}

关键改动:

没看出原因,借助的cursor修改的,cursor挺强大的,还分析处出问题原因。注释说了,必须用 collect 持续监听,不能用 LaunchedEffect(todos):Room 每次发射新 List 都会让 LaunchedEffect 重启并取消子协程,导致 delay(50) 里「设为可见」永远跑不完,界面一直空白。 ok.

http://www.cnnetsun.cn/news/1696530.html

相关文章:

  • 告别命令行手敲:用Python脚本自动化你的第一个OpenFOAM腔体流动模拟
  • Ubuntu 16.04 图形界面循环登录问题排查指南:从驱动兼容到内核版本适配
  • python实现分离不同人声、wespeaker
  • RANSAC算法
  • 计算机毕业设计:Python新能源车型数据洞察与情感推荐引擎 Django框架 snowNLP 协同过滤推荐算法 requests爬虫 可视化(建议收藏)✅
  • 避坑指南:手把手教你用Java生成定制化词云图(解决中文乱码、背景图加载问题)
  • Python 3 CGI 编程
  • DOM EventListener
  • 别再死磕梯形图了!写给工控新手的ST语言入门指南(附常用指令速查表)
  • 别再用copy /b了!三种制作哥斯拉图片马的保姆级对比(含010 Editor实操)
  • B站缓存视频合并工具:让离线内容重获完整价值
  • Iron Beam高能激光武器系统
  • Gemma 4:Google扔出的开源AI重磅炸弹,手机也能跑的ChatGPT级大脑
  • VS2019项目重构实战:从命名空间到解决方案的全面重命名指南
  • 鸿蒙Flutter混合开发:如何优雅地实现离线TTS/STT的多语言动态切换?
  • 16 指挥AI写数据库SQL代码:增删改查与存储过程实现
  • 大车间降温新选择:蒸发冷省电空调凭关键词优势脱颖而出
  • javaweb小区车辆停车场车位预约管理系统 可视化
  • 从零设计一个AXI Master:手把手教你为Xilinx MIG DDR4控制器编写自定义测试逻辑
  • 宾州大学让AI学会“记忆管理“:多智能体协作破解长期对话难题
  • 残差缩放块改进YOLOv26可学习权重自适应特征融合双重突破
  • 技术文章大纲:用Anaconda驯服AI开发流
  • Canape实战指南:XCP工程配置与调试(一)
  • 算法解析 | 深入EGO Planner:无ESDF的实时避障与轨迹优化
  • YOLOv11 OBB实战:手把手构建旋转目标检测数据集
  • 网络连接故障的常见原因
  • Bert模型
  • 从“炼金术”到“建筑学”:深度学习结构设计的五大范式
  • Go的runtime.GOMAXPROCS:设置最大CPU核心数
  • 零基础新手如何用快马AI一键生成9·1免费版安装教程网站