增加智能体头像裁剪页面

- 新增 `AgentImageCropScreen.kt` 用于智能体头像的裁剪和预览。
- 调整导航逻辑,添加智能体时跳转到新的 `AgentImageCrop` 路由。
- 原有的 `ImageCropScreen` 专门用于处理个人资料头像的裁剪。
- `AddAgentViewModel` 中移除 `isFromAddAgent` 标志,通过不同的导航路由区分头像裁剪来源。
This commit is contained in:
2025-08-31 23:18:33 +08:00
parent 00824ff7b4
commit ba7eeeca90
5 changed files with 243 additions and 21 deletions

View File

@@ -35,6 +35,7 @@ import com.aiosman.ravenow.ui.account.AccountSetting
import com.aiosman.ravenow.ui.account.RemoveAccountScreen import com.aiosman.ravenow.ui.account.RemoveAccountScreen
import com.aiosman.ravenow.ui.account.ResetPasswordScreen import com.aiosman.ravenow.ui.account.ResetPasswordScreen
import com.aiosman.ravenow.ui.agent.AddAgentScreen import com.aiosman.ravenow.ui.agent.AddAgentScreen
import com.aiosman.ravenow.ui.agent.AgentImageCropScreen
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
import com.aiosman.ravenow.ui.chat.ChatAiScreen import com.aiosman.ravenow.ui.chat.ChatAiScreen
import com.aiosman.ravenow.ui.chat.ChatScreen import com.aiosman.ravenow.ui.chat.ChatScreen
@@ -103,6 +104,7 @@ sealed class NavigationRoute(
data object ChatGroup : NavigationRoute("ChatGroup/{id}/{name}/{avatar}") data object ChatGroup : NavigationRoute("ChatGroup/{id}/{name}/{avatar}")
data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen") data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen")
data object ImageCrop : NavigationRoute("ImageCrop") data object ImageCrop : NavigationRoute("ImageCrop")
data object AgentImageCrop : NavigationRoute("AgentImageCrop")
data object AccountSetting : NavigationRoute("AccountSetting") data object AccountSetting : NavigationRoute("AccountSetting")
data object AboutScreen : NavigationRoute("AboutScreen") data object AboutScreen : NavigationRoute("AboutScreen")
data object AddAgent : NavigationRoute("AddAgent") data object AddAgent : NavigationRoute("AddAgent")
@@ -449,6 +451,13 @@ fun NavigationController(
ImageCropScreen() ImageCropScreen()
} }
} }
composable(route = NavigationRoute.AgentImageCrop.route) {
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
AgentImageCropScreen()
}
}
composable(route = NavigationRoute.AccountSetting.route) { composable(route = NavigationRoute.AccountSetting.route) {
CompositionLocalProvider( CompositionLocalProvider(
LocalAnimatedContentScope provides this, LocalAnimatedContentScope provides this,

View File

@@ -65,6 +65,8 @@ fun AddAgentScreen() {
var agnetDescError by remember { mutableStateOf<String?>(null) } var agnetDescError by remember { mutableStateOf<String?>(null) }
var errorMessage by remember { mutableStateOf<String?>(null) } var errorMessage by remember { mutableStateOf<String?>(null) }
fun onNameChange(value: String) { fun onNameChange(value: String) {
model.name = value.trim() model.name = value.trim()
agnetNameError = when { agnetNameError = when {
@@ -147,8 +149,8 @@ fun AddAgentScreen() {
.background(appColors.main) .background(appColors.main)
.align(Alignment.BottomEnd) .align(Alignment.BottomEnd)
.noRippleClickable { .noRippleClickable {
model.isFromAddAgent = true println("AddAgent: Navigating to AgentImageCrop")
navController.navigate(NavigationRoute.ImageCrop.route) navController.navigate(NavigationRoute.AgentImageCrop.route)
}, },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {

View File

@@ -0,0 +1,222 @@
package com.aiosman.ravenow.ui.agent
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
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.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.image.cropview.CropType
import com.image.cropview.EdgeType
import com.image.cropview.ImageCrop
import kotlinx.coroutines.launch
import java.io.InputStream
/**
* 专门用于智能体头像裁剪的页面
*/
@Composable
fun AgentImageCropScreen() {
var imageCrop by remember { mutableStateOf<ImageCrop?>(null) }
var croppedBitmap by remember { mutableStateOf<Bitmap?>(null) }
val context = LocalContext.current
val configuration = LocalConfiguration.current
var imageWidthInDp by remember { mutableStateOf(0) }
var imageHeightInDp by remember { mutableStateOf(0) }
val density = LocalDensity.current
val navController = LocalNavController.current
val imagePickLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri: Uri? ->
uri?.let {
val bitmap = uriToBitmap(context = context, uri = it)
if (bitmap != null) {
val aspectRatio = bitmap.height.toFloat() / bitmap.width.toFloat()
imageHeightInDp = (imageWidthInDp.toFloat() * aspectRatio).toInt()
imageCrop = ImageCrop(bitmap)
}
}
if (uri == null) {
navController.popBackStack()
}
}
val systemUiController = rememberSystemUiController()
LaunchedEffect(Unit) {
systemUiController.setStatusBarColor(darkIcons = false, color = Color.Black)
imagePickLauncher.launch("image/*")
}
DisposableEffect(Unit) {
onDispose {
imageCrop = null
systemUiController.setStatusBarColor(darkIcons = true, color = Color.White)
}
}
Column(
modifier = Modifier.background(Color.Black).fillMaxSize()
) {
StatusBarSpacer()
// 头部工具栏
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp)
) {
Image(
painter = painterResource(R.drawable.rider_pro_back_icon),
contentDescription = null,
modifier = Modifier.clickable {
navController.popBackStack()
},
colorFilter = ColorFilter.tint(Color.White)
)
Spacer(modifier = Modifier.weight(1f))
// 确认按钮
Icon(
Icons.Default.Check,
contentDescription = null,
tint = if (croppedBitmap != null) Color.Green else Color.White,
modifier = Modifier.clickable {
if (croppedBitmap != null) {
// 如果已经有裁剪结果,直接返回
println("AgentImageCrop: Using existing cropped bitmap")
AddAgentViewModel.croppedBitmap = croppedBitmap
AddAgentViewModel.viewModelScope.launch {
AddAgentViewModel.updateAgentAvatar(context)
navController.popBackStack()
}
} else {
// 进行裁剪
imageCrop?.let {
val bitmap = it.onCrop()
croppedBitmap = bitmap
println("AgentImageCrop: Cropped bitmap created: ${bitmap != null}")
}
}
}
)
}
// 裁剪预览区域
Box(
modifier = Modifier.fillMaxWidth().padding(24.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(imageHeightInDp.dp)
.onGloballyPositioned { coordinates ->
with(density) {
imageWidthInDp = coordinates.size.width.toDp().value.toInt()
}
}
) {
imageCrop?.ImageCropView(
modifier = Modifier.fillMaxSize(),
guideLineColor = Color.White,
guideLineWidth = 2.dp,
edgeCircleSize = 5.dp,
cropType = CropType.SQUARE,
edgeType = EdgeType.CIRCULAR
)
}
}
// 裁剪结果预览
croppedBitmap?.let { bitmap ->
Spacer(modifier = Modifier.height(24.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.background(
Color.Black.copy(alpha = 0.8f),
shape = RoundedCornerShape(12.dp)
)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "✅ 裁剪完成",
color = Color.Green,
fontSize = 16.sp
)
Spacer(modifier = Modifier.height(12.dp))
CustomAsyncImage(
context,
bitmap,
modifier = Modifier
.size(100.dp)
.clip(CircleShape)
.background(Color.Gray.copy(alpha = 0.3f), CircleShape),
contentDescription = "智能体头像预览",
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "点击 ✓ 确认使用此头像",
color = Color.White.copy(alpha = 0.8f),
fontSize = 12.sp
)
}
}
}
}
fun uriToBitmap(context: Context, uri: Uri): Bitmap? {
return try {
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
BitmapFactory.decodeStream(inputStream)
} catch (e: Exception) {
e.printStackTrace()
null
}
}

View File

@@ -114,18 +114,9 @@ fun ImageCropScreen() {
imageCrop?.let { imageCrop?.let {
val bitmap = it.onCrop() val bitmap = it.onCrop()
println("ImageCrop: Cropped bitmap created: ${bitmap != null}") println("ImageCrop: Cropped bitmap created: ${bitmap != null}")
if (AddAgentViewModel.isFromAddAgent) {
println("ImageCrop: Setting bitmap to AddAgentViewModel") // 专门处理个人资料头像
// 如果是从AddAgent页面跳转过来的 println("ImageCrop: Setting bitmap to AccountEditViewModel for user profile")
AddAgentViewModel.croppedBitmap = bitmap
AddAgentViewModel.viewModelScope.launch {
AddAgentViewModel.updateAgentAvatar(context)
AddAgentViewModel.isFromAddAgent = false
navController.popBackStack()
}
} else {
println("ImageCrop: Setting bitmap to AccountEditViewModel")
// 默认处理AccountEdit
AccountEditViewModel.croppedBitmap = bitmap AccountEditViewModel.croppedBitmap = bitmap
AccountEditViewModel.viewModelScope.launch { AccountEditViewModel.viewModelScope.launch {
AccountEditViewModel.updateUserProfile(context) AccountEditViewModel.updateUserProfile(context)
@@ -133,7 +124,6 @@ fun ImageCropScreen() {
} }
} }
} }
}
) )
} }
Box( Box(

View File

@@ -137,8 +137,7 @@ fun Agent() {
.size(36.dp) .size(36.dp)
.noRippleClickable { .noRippleClickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) { if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
// 设置标志,表示新增智能体后不需要刷新 // 导航到添加智能体页面
com.aiosman.ravenow.ui.agent.AddAgentViewModel.isFromAddAgent = true
navController.navigate( navController.navigate(
NavigationRoute.AddAgent.route NavigationRoute.AddAgent.route
) )