VocabVerse背单词应用故事生成界面优化1
书接上回,我们设计好了“故事生成”的deepseek接口,并且利用这个故事来生成漫画,并且返回到客户端。那我们的思路就是把 漫画生成的服务器接口部署暴露 ,将故事格式化时候传递到服务器接口 ,然后将以上步骤打包。接下来我们就面临一些小问题来完善这个故事界面完善的思路就很简单了:①把我们生成的故事的界面,太过冗杂,可以进行少了优化。比如选定单词的选举和故事界面的相对大小②数据和真实数据库的连接明确了
VocabVerse背单词应用故事生成界面优化
书接上回,我们设计好了“故事生成”的deepseek接口,并且利用这个故事来生成漫画,并且返回到客户端。
那我们的思路就是把 漫画生成的服务器接口部署暴露 ,将故事格式化时候传递到服务器接口 ,然后将以上步骤打包。
接下来我们就面临一些小问题来完善这个故事界面
完善的思路就很简单了:
①把我们生成的故事的界面,太过冗杂,可以进行少了优化。比如选定单词的选举和故事界面的相对大小
②数据和真实数据库的连接
明确了需求,那我们就可以着手了
本次我们可以处理第一步
step1 界面优化动态效果
一、改造
我们可以查看我们之前设计的界面的代码:
@Composable
fun DeepSeekChatScreen(
modifier: Modifier = Modifier,
viewModel: DeepSeekViewModel = hiltViewModel()
) {
val words = viewModel.spellingList
// 使用Set来记录选中的单词,支持多选
val selectedWords = remember { mutableStateOf(emptySet<String>()) }
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
// 单词选择区域
// 单词选择区域 - 添加卡片容器
Surface(
modifier = Modifier
.weight(0.77f)
.fillMaxWidth(),
shape = MaterialTheme.shapes.medium,
shadowElevation = 4.dp // 修正参数名
) {
//省略其他代码
}
Spacer(Modifier.height(8.dp)) // 添加垂直间隔
// 结果显示区域
ChatHistory(
history = viewModel.uiState.fullResponse,
currentResponse = viewModel.uiState.currentResponse,
isLoading = viewModel.uiState.isLoading,
modifier = Modifier.weight(1f)
)
// 底部按钮区域
Column( // 用Column包裹两个Row(第1处改动)
modifier = Modifier
.padding(vertical = 2.dp)
.drawBehind { // 添加虚线绘制(第2处改动)
drawRect(
color =Color(0xFF2C7F3E) ,
style = Stroke(
width = 1.dp.toPx(),
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 15f), 0f)
)
)
}
)
// 查询按钮
}
}
@Composable
fun WordButton(
word: String,
isSelected: Boolean,
onClick: () -> Unit
) {
Surface(
modifier = Modifier
.height(48.dp)
.padding(4.dp),
shape = RoundedCornerShape(20.dp), // 1. 改为圆形胶囊形状
color = if (isSelected) Color(0xFF4CAF50) else Color(0xFFE8F5E9), // 2. 修改选中颜色
border = BorderStroke(1.dp, Color(0xFF81C784)), // 3. 添加浅绿色边框
onClick = onClick
) {
Text(
text = word,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
color = if (isSelected) Color.White else Color(0xFF2E7D32), // 4. 调整文字颜色
fontSize = 14.sp,
fontWeight = FontWeight.Medium // 增加字重
)
}
}
我们可以看到,无论是单词选择区域
Surface(
modifier = Modifier
.weight(0.77f)
.fillMaxWidth(),
shape = MaterialTheme.shapes.medium,
shadowElevation = 4.dp
) {
// ...
}
还是聊天区域
ChatHistory(
history = viewModel.uiState.fullResponse,
currentResponse = viewModel.uiState.currentResponse,
isLoading = viewModel.uiState.isLoading,
modifier = Modifier.weight(1f)
)
可以看到modifier = Modifier.weight(XXXX)
的权重都是固定值
那么我们设计时候,如果连接的数据库的单词多了,显示就会出问题,比如刚刚step1提到的,如果我们把累计的单词加入这个界面,这个势必会随着时间的推移越来越多,显然不合理
那么我们可以研究,像chatgpt一样,询问的时候,对话的窗口就会动态拉大。
但是我们研究modifier = Modifier.weight(XXXX)
的源码
可以看到他接收的权重参数只是一个浮点数而已,怎么做得到动态变化呢?
这时候自然有对策,那就是animateFloatAsState
但是animateFloatAsState
类型又不是float,怎么实现呢?
在 Jetpack Compose 中,Modifier.weight()
可以接收 animateFloatAsState
是因为 Compose 的声明式特性和状态管理机制。让我们深入理解这一点:
1. Compose 的声明式特性
Jetpack Compose 是基于状态的 UI 框架,当状态变化时,Compose 会自动重组(recompose)相关的 UI 部分。animateFloatAsState
返回的是一个 State<Float>
对象,Compose 可以观察这个状态的变化并触发重组。
2. Modifier.weight()
的参数类型
weight()
函数接收一个 Float
类型的参数。而 animateFloatAsState
返回的是一个 State<Float>
,但在 Kotlin 中,State
对象可以通过属性委托(by
关键字)或直接调用 .value
来获取其存储的值。Compose 在重组时会自动读取 .value
,所以它实际上传递的是一个 Float
。
3. 动画状态的工作机制
当使用 animateFloatAsState
:
val weight by animateFloatAsState(targetValue)
实际上是:
- 声明了一个可观察的
State<Float>
- 当
targetValue
变化时,动画系统会平滑地过渡到新值 - 在每一帧,Compose 都会读取当前动画值并触发重组
4. 为什么可以这样用
Modifier.weight(weight)
在内部只是使用这个 Float 值来计算布局权重。由于 animateFloatAsState
在动画过程中会不断产生新的 Float 值并触发重组,所以权重会平滑过渡,从而实现动态效果。
5. 底层原理
在 Compose 的布局阶段:
- 动画系统每帧更新
State<Float>
的值 - Compose 检测到状态变化,触发重组
Layout
或Column
/Row
等布局组件会重新计算子项的位置和大小- 由于权重值在动画,布局会平滑变化
6. 对比传统视图系统
在传统 Android 视图中,要实现类似效果需要:
- 手动计算 View 的大小
- 使用
ValueAnimator
并手动更新 LayoutParams - 请求重新布局
而 Compose 的声明式方式让这一切变得非常简单,只需声明状态如何变化,框架处理其余工作。
示例代码
@Composable
fun DynamicWeightExample() {
var expanded by remember { mutableStateOf(false) }
// 动画权重值
val weight by animateFloatAsState(
targetValue = if (expanded) 0.7f else 0.3f,
animationSpec = tween(500)
)
Column(Modifier.fillMaxSize()) {
Box(
Modifier
.weight(weight) // 这里使用动画权重值
.background(Color.Red)
)
Box(
Modifier
.weight(1 - weight)
.background(Color.Blue)
)
Button(onClick = { expanded = !expanded }) {
Text("Toggle")
}
}
}
在这个例子中,点击按钮会平滑地调整红蓝两个区域的比例,这就是 animateFloatAsState
与 Modifier.weight()
配合使用的典型场景。
为DeepSeekChatScreen
添加动态界面调整功能,使得单词按钮栏和聊天栏的大小能根据是否有聊天内容自动调整。以下是改进方案:
修改后的Composable函数
@Composable
fun DeepSeekChatScreen(
modifier: Modifier = Modifier,
viewModel: DeepSeekViewModel = hiltViewModel()
) {
val words = viewModel.spellingList
val selectedWords = remember { mutableStateOf(emptySet<String>()) }
// 根据是否有聊天内容决定权重分配
val hasChatContent = viewModel.uiState.fullResponse.isNotEmpty() ||
viewModel.uiState.currentResponse.isNotEmpty() ||
viewModel.uiState.isLoading
val wordSectionWeight by animateFloatAsState(
targetValue = if (hasChatContent) 0.5f else 0.8f,
animationSpec = tween(durationMillis = 300)
)
val chatSectionWeight by animateFloatAsState(
targetValue = if (hasChatContent) 1.3f else 0.5f,
animationSpec = tween(durationMillis = 300)
)
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
// 单词选择区域 - 动态权重
Surface(
modifier = Modifier
.weight(wordSectionWeight)
.fillMaxWidth(),
shape = MaterialTheme.shapes.medium,
shadowElevation = 4.dp
) {
// ... 原有单词选择区域内容不变 ...
}
Spacer(Modifier.height(8.dp))
// 聊天区域 - 动态权重
ChatHistory(
history = viewModel.uiState.fullResponse,
currentResponse = viewModel.uiState.currentResponse,
isLoading = viewModel.uiState.isLoading,
modifier = Modifier.weight(chatSectionWeight)
)
// 底部按钮区域 - 保持不变
Column(
modifier = Modifier
.padding(vertical = 2.dp)
.drawBehind {
drawRect(
color = Color(0xFF2C7F3E),
style = Stroke(
width = 1.dp.toPx(),
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 15f), 0f)
)
)
}
.padding(4.dp),
verticalArrangement = Arrangement.spacedBy(1.dp)
) {
// ... 原有按钮区域内容不变 ...
}
}
}
这样当聊天内容出现或消失时,界面会平滑过渡,提供更好的用户体验。
这种动态调整的方案既满足了需求,又保持了代码的简洁性和可维护性。
二、参考:Kotlin 的 animateFloatAsState
函数
animateFloatAsState
是 Jetpack Compose 中的一个动画 API,用于创建一个可以自动动画过渡的 State
对象,当目标值改变时,它会平滑地从当前值动画过渡到新值。
1.基本用法
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.runtime.getValue
val alpha: Float by animateFloatAsState(
targetValue = if (enabled) 1f else 0.5f,
animationSpec = tween(durationMillis = 300)
)
2.参数说明
targetValue
: 动画的目标值(Float 类型)animationSpec
: 动画规格,定义动画如何执行(默认是spring()
)- 常用规格:
tween()
: 指定持续时间和缓动曲线spring()
: 基于物理的弹簧动画keyframes()
: 关键帧动画repeatable()
: 可重复的动画
- 常用规格:
finishedListener
: 动画完成时的回调(可选)
3.示例
@Composable
fun AnimatedButton() {
var enabled by remember { mutableStateOf(false) }
// 动画透明度值
val alpha: Float by animateFloatAsState(
targetValue = if (enabled) 1f else 0.5f,
animationSpec = tween(durationMillis = 500)
)
Button(
onClick = { enabled = !enabled },
modifier = Modifier.alpha(alpha)
) {
Text("Toggle")
}
}
4.特点
- 声明式:只需指定目标值和动画规格,Compose 会处理动画过程
- 可组合:可以在任何 Composable 函数中使用
- 自动取消:如果目标值在动画过程中再次改变,当前动画会被取消并开始新动画
- 高效:使用 Compose 的状态管理,只在需要时重组
5.类似函数
Compose 还提供了其他类似的动画状态函数:
animateColorAsState
- 用于颜色动画animateDpAsState
- 用于尺寸动画animateIntAsState
- 用于整数值动画animate*AsState
系列函数都遵循相同的模式
animateFloatAsState
是实现简单属性动画的最便捷方式之一,特别适合 UI 元素的透明度、比例、旋转等属性的动画效果。
根据这个原理完全可以设计一个可复用的公共组件。可以创建一个通用的 AnimatedWeightColumn
组件,封装权重动画逻辑,使它在不同场景下都能方便使用。
三、设计公共组件方案
1. 创建基础动画权重组件
/**
* 带权重动画的Column组件
* @param mainContentWeight 主内容区域的权重目标值
* @param secondaryContentWeight 次要内容区域的权重目标值
* @param animationSpec 动画规格
* @param mainContent 主内容区域
* @param secondaryContent 次要内容区域
*/
@Composable
fun AnimatedWeightColumn(
mainContentWeight: Float,
secondaryContentWeight: Float,
animationSpec: AnimationSpec<Float> = tween(durationMillis = 300),
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.SpaceBetween,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
mainContent: @Composable () -> Unit,
secondaryContent: @Composable () -> Unit
) {
val animatedMainWeight by animateFloatAsState(
targetValue = mainContentWeight,
animationSpec = animationSpec
)
val animatedSecondaryWeight by animateFloatAsState(
targetValue = secondaryContentWeight,
animationSpec = animationSpec
)
Column(
modifier = modifier,
verticalArrangement = verticalArrangement,
horizontalAlignment = horizontalAlignment
) {
Box(
modifier = Modifier
.weight(animatedMainWeight)
.fillMaxWidth()
) {
mainContent()
}
Box(
modifier = Modifier
.weight(animatedSecondaryWeight)
.fillMaxWidth()
) {
secondaryContent()
}
}
}
2. 创建针对聊天场景的专用组件
/**
* 专门为聊天界面优化的动画权重布局
* @param hasContent 是否有内容决定权重分配
* @param mainContentWeightWithContent 有内容时主内容权重
* @param mainContentWeightWithoutContent 无内容时主内容权重
* @param secondaryContentWeightWithContent 有内容时次要内容权重
* @param secondaryContentWeightWithoutContent 无内容时次要内容权重
*/
@Composable
fun ChatAnimatedLayout(
hasContent: Boolean,
modifier: Modifier = Modifier,
mainContentWeightWithContent: Float = 0.5f,
mainContentWeightWithoutContent: Float = 0.8f,
secondaryContentWeightWithContent: Float = 1.3f,
secondaryContentWeightWithoutContent: Float = 0.5f,
animationSpec: AnimationSpec<Float> = tween(durationMillis = 300),
mainContent: @Composable () -> Unit,
secondaryContent: @Composable () -> Unit
) {
AnimatedWeightColumn(
mainContentWeight = if (hasContent) mainContentWeightWithContent else mainContentWeightWithoutContent,
secondaryContentWeight = if (hasContent) secondaryContentWeightWithContent else secondaryContentWeightWithoutContent,
animationSpec = animationSpec,
modifier = modifier,
mainContent = mainContent,
secondaryContent = secondaryContent
)
}
3. 使用示例 - 重构原代码
@Composable
fun DeepSeekChatScreen(
modifier: Modifier = Modifier,
viewModel: DeepSeekViewModel = hiltViewModel()
) {
val words = viewModel.spellingList
val selectedWords = remember { mutableStateOf(emptySet<String>()) }
val hasChatContent = viewModel.uiState.fullResponse.isNotEmpty() ||
viewModel.uiState.currentResponse.isNotEmpty() ||
viewModel.uiState.isLoading
ChatAnimatedLayout(
hasContent = hasChatContent,
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
// 主内容区域 - 单词选择
Surface(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.medium,
shadowElevation = 4.dp
) {
// ... 单词选择区域内容 ...
}
} {
// 次要内容区域 - 聊天历史
ChatHistory(
history = viewModel.uiState.fullResponse,
currentResponse = viewModel.uiState.currentResponse,
isLoading = viewModel.uiState.isLoading,
modifier = Modifier.fillMaxWidth()
)
// 底部按钮区域
Column(
modifier = Modifier
.padding(vertical = 2.dp)
.drawBehind {
drawRect(
color = Color(0xFF2C7F3E),
style = Stroke(
width = 1.dp.toPx(),
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 15f), 0f)
)
)
}
.padding(4.dp),
verticalArrangement = Arrangement.spacedBy(1.dp)
) {
// ... 按钮区域内容 ...
}
}
}
设计优势
- 复用性:可以在任何需要权重动画布局的地方使用
- 可配置性:可以灵活调整权重值和动画参数
- 类型安全:通过参数明确表达设计意图
- 维护性:动画逻辑集中管理,修改只需调整一处
- 可扩展性:可以轻松添加更多区域或配置选项
参考效果:
更多推荐
所有评论(0)