自定义NavigationItem,新增群组创建页面

This commit is contained in:
weber
2025-08-12 19:06:56 +08:00
parent 697af504b7
commit 2d518cbd68
18 changed files with 941 additions and 104 deletions

View File

@@ -17,6 +17,7 @@ open class AppThemeData(
var loadingText: Color,
var disabledBackground: Color,
var background: Color,
var secondaryBackground: Color,
var decentBackground: Color,
var divider: Color,
var inputBackground: Color,
@@ -46,6 +47,7 @@ class LightThemeColors : AppThemeData(
loadingText = Color(0xffffffff),
disabledBackground = Color(0xFFD0D0D0),
background = Color(0xFFFFFFFF),
secondaryBackground = Color(0xFFF7f7f7),
divider = Color(0xFFEbEbEb),
inputBackground = Color(0xFFF7f7f7),
inputBackground2 = Color(0xFFFFFFFF),
@@ -76,6 +78,7 @@ class DarkThemeColors : AppThemeData(
loadingText = Color(0xff000000),
disabledBackground = Color(0xFF3A3A3A),
background = Color(0xFF121212),
secondaryBackground = Color(0xFF1C1C1C),
divider = Color(0xFF282828),
inputBackground = Color(0xFF1C1C1C),
inputBackground2 = Color(0xFF1C1C1C),

View File

@@ -32,6 +32,7 @@ import com.aiosman.ravenow.ui.account.AccountEditScreen2
import com.aiosman.ravenow.ui.account.AccountSetting
import com.aiosman.ravenow.ui.account.ResetPasswordScreen
import com.aiosman.ravenow.ui.agent.AddAgentScreen
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
import com.aiosman.ravenow.ui.chat.ChatAiScreen
import com.aiosman.ravenow.ui.chat.ChatScreen
import com.aiosman.ravenow.ui.comment.CommentsScreen
@@ -98,6 +99,7 @@ sealed class NavigationRoute(
data object AccountSetting : NavigationRoute("AccountSetting")
data object AboutScreen : NavigationRoute("AboutScreen")
data object AddAgent : NavigationRoute("AddAgent")
data object CreateGroupChat : NavigationRoute("CreateGroupChat")
}
@@ -424,6 +426,18 @@ fun NavigationController(
AddAgentScreen()
}
composable(
route = NavigationRoute.CreateGroupChat.route,
enterTransition = {
fadeIn(animationSpec = tween(durationMillis = 0))
},
exitTransition = {
fadeOut(animationSpec = tween(durationMillis = 0))
}
) {
CreateGroupChatScreen()
}
}
}

View File

@@ -30,12 +30,14 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.composables.form.FormTextInput
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@@ -82,13 +84,18 @@ fun AccountEditScreen2() {
}
}
StatusBarMaskLayout(
modifier = Modifier.background(color = appColors.background).padding(horizontal = 16.dp),
darkIcons = !AppState.darkMode,
maskBoxBackgroundColor = appColors.background
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = appColors.background),
horizontalAlignment = Alignment.CenterHorizontally
) {
StatusBarSpacer()
//StatusBarSpacer()
Box(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
) {
@@ -184,7 +191,7 @@ fun AccountEditScreen2() {
}
}
}
}}
}

View File

@@ -0,0 +1,70 @@
package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable
fun AiAgentListScreen(
searchText: String,
selectedMemberIds: Set<String> = emptySet(),
onMemberSelect: (GroupMember) -> Unit
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
var aiAgents by remember { mutableStateOf<List<GroupMember>>(emptyList()) }
var isLoading by remember { mutableStateOf(false) }
// 加载AI智能体数据
LaunchedEffect(Unit) {
isLoading = true
aiAgents = CreateGroupChatViewModel.getAiAgents()
isLoading = false
}
val filteredAgents = if (searchText.isEmpty()) {
aiAgents
} else {
aiAgents.filter { it.name.contains(searchText, ignoreCase = true) }
}
if (isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "加载中...",
color = AppColors.secondaryText,
fontSize = 14.sp
)
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(filteredAgents) { agent ->
MemberItem(
member = agent,
isSelected = selectedMemberIds.contains(agent.id),
onSelect = { onMemberSelect(agent) }
)
}
}
}
}

View File

@@ -0,0 +1,397 @@
package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.*
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
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.aiosman.ravenow.ui.composables.TabItem
import com.aiosman.ravenow.ui.composables.TabSpacer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch
// 成员数据类
data class GroupMember(
val id: String,
val name: String,
val avatar: String
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CreateGroupChatScreen() {
val AppColors = LocalAppTheme.current
val navController = LocalNavController.current
val systemUiController = rememberSystemUiController()
val context = LocalContext.current
// 状态管理
var groupName by remember { mutableStateOf(TextFieldValue("")) }
var searchText by remember { mutableStateOf(TextFieldValue("")) }
var selectedMembers by remember { mutableStateOf(listOf<GroupMember>()) }
var selectedMemberIds by remember { mutableStateOf<Set<String>>(emptySet()) }
var pagerState = rememberPagerState(pageCount = { 2 })
var scope = rememberCoroutineScope()
// 清除错误信息
LaunchedEffect(groupName.text, searchText.text) {
if (CreateGroupChatViewModel.errorMessage != null) {
CreateGroupChatViewModel.clearError()
}
}
// 监听页面切换清除当前tab的选中状态但保留已选成员列表
LaunchedEffect(pagerState.currentPage) {
// 不清除selectedMemberIds因为我们需要保持跨tab的选中状态
// 这样用户可以在AI智能体和朋友之间切换选中的状态会保持
}
val navigationBarPaddings = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
LaunchedEffect(Unit) {
systemUiController.setNavigationBarColor(Color.Transparent)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(bottom = navigationBarPaddings)
) {
// 错误提示
CreateGroupChatViewModel.errorMessage?.let { error ->
Box(
modifier = Modifier
.fillMaxWidth()
.background(AppColors.error.copy(alpha = 0.1f))
.padding(16.dp)
) {
Text(
text = error,
color = AppColors.error,
fontSize = 14.sp
)
}
}
StatusBarSpacer()
// 头部
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 返回按钮
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = "back",
modifier = Modifier
.size(24.dp)
.noRippleClickable {
navController.popBackStack()
},
colorFilter = ColorFilter.tint(AppColors.text)
)
// 标题
Text(
text = stringResource(R.string.create_group_chat),
fontSize = 17.sp,
fontWeight = FontWeight.W700,
color = AppColors.text,
modifier = Modifier
.weight(1f)
.padding(start = 16.dp)
)
// 一键创建按钮
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.noRippleClickable {
// 一键创建逻辑
}
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic),
contentDescription = "quick create",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(AppColors.main)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = stringResource(R.string.quick_create),
fontSize = 14.sp,
color = AppColors.main
)
}
}
// 搜索栏
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.background(
color = AppColors.inputBackground,
shape = RoundedCornerShape(8.dp)
)
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
BasicTextField(
value = searchText,
onValueChange = { searchText = it },
textStyle = androidx.compose.ui.text.TextStyle(
color = AppColors.text,
fontSize = 14.sp
),
modifier = Modifier.fillMaxWidth(),
decorationBox = { innerTextField ->
Row(
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_nav_search),
contentDescription = "search",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(AppColors.secondaryText)
)
Spacer(modifier = Modifier.width(8.dp))
if (searchText.text.isEmpty()) {
Text(
text = "搜索",
color = AppColors.secondaryText,
fontSize = 14.sp
)
}
innerTextField()
}
}
)
}
// 群聊名称输入框
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "群聊名称",
fontSize = 14.sp,
color = AppColors.text,
modifier = Modifier.width(80.dp)
)
BasicTextField(
value = groupName,
onValueChange = { groupName = it },
textStyle = androidx.compose.ui.text.TextStyle(
color = AppColors.text,
fontSize = 14.sp
),
modifier = Modifier
.weight(1f)
.background(
color = AppColors.inputBackground,
shape = RoundedCornerShape(8.dp)
)
.padding(horizontal = 12.dp, vertical = 8.dp)
)
}
// 已选成员列表
if (selectedMembers.isNotEmpty()) {
// 显示选中成员数量
Text(
text = "已选择 ${selectedMembers.size} 个成员",
fontSize = 14.sp,
color = AppColors.secondaryText,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
)
LazyRow(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(selectedMembers) { member ->
Box {
CustomAsyncImage(
context = context,
imageUrl = member.avatar,
contentDescription = member.name,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
)
// 删除按钮
Box(
modifier = Modifier
.size(20.dp)
.background(AppColors.error, CircleShape)
.align(Alignment.TopEnd)
.noRippleClickable {
// 删除成员时同时更新选中状态
val (newSelectedMemberIds, newSelectedMembers) = CreateGroupChatViewModel.removeSelectedMember(
member, selectedMemberIds, selectedMembers
)
selectedMemberIds = newSelectedMemberIds
selectedMembers = newSelectedMembers
},
contentAlignment = Alignment.Center
) {
Text(
text = "×",
color = Color.White,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
}
}
}
}
}
// Tab切换
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
TabItem(
text = stringResource(R.string.chat_ai),
isSelected = pagerState.currentPage == 0,
onClick = {
scope.launch {
pagerState.animateScrollToPage(0)
}
}
)
TabSpacer()
TabItem(
text = stringResource(R.string.chat_friend),
isSelected = pagerState.currentPage == 1,
onClick = {
scope.launch {
pagerState.animateScrollToPage(1)
}
}
)
}
// 内容区域
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
when (it) {
0 -> {
// AI智能体列表
AiAgentListScreen(
searchText = searchText.text,
selectedMemberIds = selectedMemberIds,
onMemberSelect = { member ->
val (newSelectedMemberIds, newSelectedMembers) = CreateGroupChatViewModel.toggleMemberSelection(
member, selectedMemberIds, selectedMembers
)
selectedMemberIds = newSelectedMemberIds
selectedMembers = newSelectedMembers
}
)
}
1 -> {
// 朋友列表
FriendListScreen(
searchText = searchText.text,
selectedMemberIds = selectedMemberIds,
onMemberSelect = { member ->
val (newSelectedMemberIds, newSelectedMembers) = CreateGroupChatViewModel.toggleMemberSelection(
member, selectedMemberIds, selectedMembers
)
selectedMemberIds = newSelectedMemberIds
selectedMembers = newSelectedMembers
}
)
}
}
}
// 创建群聊按钮
Button(
onClick = {
// 创建群聊逻辑
if (groupName.text.isNotEmpty() && selectedMembers.isNotEmpty()) {
scope.launch {
val success = CreateGroupChatViewModel.createGroupChat(
groupName = groupName.text,
memberIds = selectedMembers.map { it.id },
context = context
)
if (success) {
navController.popBackStack()
}
}
}
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = AppColors.main,
contentColor = AppColors.mainText
),
shape = RoundedCornerShape(8.dp),
enabled = groupName.text.isNotEmpty() && selectedMembers.isNotEmpty() && !CreateGroupChatViewModel.isLoading
) {
if (CreateGroupChatViewModel.isLoading) {
Text(
text = "创建中...",
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
} else {
Text(
text = stringResource(R.string.create_group_chat),
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
}
}
}
}

View File

@@ -0,0 +1,133 @@
package com.aiosman.ravenow.ui.group
import android.content.Context
import android.icu.util.Calendar
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.paging.PagingData
import androidx.paging.map
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.data.AccountNotice
import com.aiosman.ravenow.data.AccountService
import com.aiosman.ravenow.data.AccountServiceImpl
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.entity.CommentEntity
import com.aiosman.ravenow.exp.formatChatTime
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.navigateToChat
import com.aiosman.ravenow.utils.TrtcHelper
import com.tencent.imsdk.v2.V2TIMConversation
import com.tencent.imsdk.v2.V2TIMConversationResult
import com.tencent.imsdk.v2.V2TIMManager
import com.tencent.imsdk.v2.V2TIMMessage
import com.tencent.imsdk.v2.V2TIMValueCallback
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine
object CreateGroupChatViewModel : ViewModel() {
val accountService: AccountService = AccountServiceImpl()
val userService: UserService = UserServiceImpl()
// 状态管理
var isLoading by mutableStateOf(false)
var errorMessage by mutableStateOf<String?>(null)
// 获取AI智能体列表
suspend fun getAiAgents(): List<GroupMember> {
return try {
// TODO: 从API获取AI智能体列表
listOf(
GroupMember("1", "AI助手", "https://example.com/avatar1.jpg"),
GroupMember("2", "智能客服", "https://example.com/avatar2.jpg"),
GroupMember("3", "翻译助手", "https://example.com/avatar3.jpg"),
GroupMember("4", "写作助手", "https://example.com/avatar4.jpg"),
GroupMember("5", "编程助手", "https://example.com/avatar5.jpg"),
GroupMember("6", "设计助手", "https://example.com/avatar6.jpg")
)
} catch (e: Exception) {
errorMessage = "获取AI智能体列表失败: ${e.message}"
emptyList()
}
}
// 获取朋友列表
suspend fun getFriends(): List<GroupMember> {
return try {
// TODO: 从API获取朋友列表
listOf(
GroupMember("7", "张三", "https://example.com/avatar7.jpg"),
GroupMember("8", "李四", "https://example.com/avatar8.jpg"),
GroupMember("9", "王五", "https://example.com/avatar9.jpg"),
GroupMember("10", "赵六", "https://example.com/avatar10.jpg"),
GroupMember("11", "钱七", "https://example.com/avatar11.jpg"),
GroupMember("12", "孙八", "https://example.com/avatar12.jpg")
)
} catch (e: Exception) {
errorMessage = "获取朋友列表失败: ${e.message}"
emptyList()
}
}
// 创建群聊
suspend fun createGroupChat(
groupName: String,
memberIds: List<String>,
context: Context
): Boolean {
return try {
isLoading = true
// TODO: 实现创建群聊的API调用
// 这里应该调用实际的API来创建群聊
// 模拟API调用延迟
kotlinx.coroutines.delay(1000)
isLoading = false
true
} catch (e: Exception) {
isLoading = false
errorMessage = "创建群聊失败: ${e.message}"
false
}
}
// 清除错误信息
fun clearError() {
errorMessage = null
}
// 添加成员到选中列表
fun addSelectedMember(member: GroupMember, selectedMemberIds: Set<String>, selectedMembers: List<GroupMember>): Pair<Set<String>, List<GroupMember>> {
val newSelectedMemberIds = selectedMemberIds + member.id
val newSelectedMembers = if (selectedMembers.none { it.id == member.id }) {
selectedMembers + member
} else {
selectedMembers
}
return Pair(newSelectedMemberIds, newSelectedMembers)
}
// 从选中列表移除成员
fun removeSelectedMember(member: GroupMember, selectedMemberIds: Set<String>, selectedMembers: List<GroupMember>): Pair<Set<String>, List<GroupMember>> {
val newSelectedMemberIds = selectedMemberIds - member.id
val newSelectedMembers = selectedMembers.filter { it.id != member.id }
return Pair(newSelectedMemberIds, newSelectedMembers)
}
// 切换成员选中状态
fun toggleMemberSelection(member: GroupMember, selectedMemberIds: Set<String>, selectedMembers: List<GroupMember>): Pair<Set<String>, List<GroupMember>> {
return if (selectedMemberIds.contains(member.id)) {
removeSelectedMember(member, selectedMemberIds, selectedMembers)
} else {
addSelectedMember(member, selectedMemberIds, selectedMembers)
}
}
}

View File

@@ -0,0 +1,70 @@
package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable
fun FriendListScreen(
searchText: String,
selectedMemberIds: Set<String> = emptySet(),
onMemberSelect: (GroupMember) -> Unit
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
var friends by remember { mutableStateOf<List<GroupMember>>(emptyList()) }
var isLoading by remember { mutableStateOf(false) }
// 加载朋友数据
LaunchedEffect(Unit) {
isLoading = true
friends = CreateGroupChatViewModel.getFriends()
isLoading = false
}
val filteredFriends = if (searchText.isEmpty()) {
friends
} else {
friends.filter { it.name.contains(searchText, ignoreCase = true) }
}
if (isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "加载中...",
color = AppColors.secondaryText,
fontSize = 14.sp
)
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(filteredFriends) { friend ->
MemberItem(
member = friend,
isSelected = selectedMemberIds.contains(friend.id),
onSelect = { onMemberSelect(friend) }
)
}
}
}
}

View File

@@ -0,0 +1,65 @@
package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable
fun MemberItem(
member: GroupMember,
isSelected: Boolean = false,
onSelect: () -> Unit
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
Row(
modifier = Modifier
.fillMaxWidth()
.noRippleClickable { onSelect() }
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
CustomAsyncImage(
context = context,
imageUrl = member.avatar,
contentDescription = member.name,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = member.name,
fontSize = 16.sp,
color = AppColors.text,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Checkbox(
checked = isSelected,
onCheckedChange = { onSelect() },
colors = CheckboxDefaults.colors(
checkedColor = AppColors.main,
uncheckedColor = AppColors.secondaryText
)
)
}
}

View File

@@ -52,6 +52,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -259,7 +260,7 @@ fun IndexScreen() {
Scaffold(
bottomBar = {
NavigationBar(
modifier = Modifier.height(56.dp + navigationBarHeight),
modifier = Modifier.height(72.dp + navigationBarHeight),
containerColor = AppColors.background
) {
item.forEachIndexed { idx, it ->
@@ -268,6 +269,7 @@ fun IndexScreen() {
targetValue = if (isSelected) AppColors.brandColorsColor else AppColors.text,
animationSpec = tween(durationMillis = 250), label = ""
)
NavigationBarItem(
modifier = Modifier.padding(top = 6.dp),
selected = isSelected,
@@ -282,58 +284,65 @@ fun IndexScreen() {
}
model.tabIndex = idx
},
interactionSource = remember { MutableInteractionSource() },
colors = NavigationBarItemColors(
selectedTextColor = Color.Red,
selectedIconColor = Color.Transparent,
selectedTextColor = Color.Transparent,
selectedIndicatorColor = Color.Transparent,
unselectedTextColor = Color.Red,
disabledIconColor = Color.Red,
disabledTextColor = Color.Red,
selectedIconColor = iconTint,
unselectedIconColor = iconTint,
unselectedIconColor = Color.Transparent,
unselectedTextColor = Color.Transparent,
disabledIconColor = Color.Transparent,
disabledTextColor = Color.Transparent
),
icon = {
// 特殊处理中间的Add按钮只显示图标并放大
if (it.route == NavigationItem.Add.route) {
Icon(
modifier = Modifier.size(32.dp),
imageVector = if (isSelected) it.selectedIcon() else it.icon(),
contentDescription = null,
tint = AppColors.text
)
} else {
Box(
modifier = Modifier
.width(46.dp)
.height(30.dp)
.background(
color = if (isSelected) AppColors.brandColorsColor.copy(alpha = 0.1f) else Color.Transparent ,
shape = RoundedCornerShape(10.dp)
),
contentAlignment = Alignment.Center
) {
Icon(
modifier = Modifier.size(24.dp),
imageVector = if (isSelected) it.selectedIcon() else it.icon(),
contentDescription = null,
tint = iconTint
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
if (it.route == NavigationItem.Add.route) {
// Add按钮只显示大图标
Image(
painter = painterResource(if (isSelected) it.selectedIcon() else it.icon()),
contentDescription = it.label(),
modifier = Modifier.size(32.dp),
colorFilter = if (!isSelected) ColorFilter.tint(AppColors.text) else null
)
} else {
// 其他按钮:图标+文字
Box(
modifier = Modifier
.width(48.dp)
.height(32.dp)
.background(
color = if (isSelected) AppColors.brandColorsColor.copy(alpha = 0.15f) else Color.Transparent,
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(if (isSelected) it.selectedIcon() else it.icon()),
contentDescription = it.label(),
modifier = Modifier.size(24.dp),
colorFilter = if (!isSelected) ColorFilter.tint(AppColors.text) else null
)
}
// 文字标签,可控制间距
Spacer(modifier = Modifier.height(4.dp))
Text(
text = it.label(),
fontSize = 10.sp,
color = if (isSelected) AppColors.brandColorsColor else AppColors.text,
fontWeight = if (isSelected) FontWeight.W600 else FontWeight.Normal
)
}
}
},
label = {
// Add按钮不显示文字标签
if (it.route != NavigationItem.Add.route) {
Text(
modifier = Modifier.padding(0.dp),
text = it.label(),
fontSize = 9.sp,
color = if (isSelected) AppColors.brandColorsColor else AppColors.text,
)
}
// 不显示默认标签
}
)
}
}
}
@@ -344,7 +353,7 @@ fun IndexScreen() {
modifier = Modifier
.background(AppColors.background)
.padding(0.dp),
beyondBoundsPageCount = 5,
beyondBoundsPageCount = 4,
userScrollEnabled = false
) { page ->
when (page) {

View File

@@ -3,57 +3,46 @@ package com.aiosman.ravenow.ui.index
import android.graphics.Color
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import com.aiosman.ravenow.R
sealed class NavigationItem(
val route: String,
val icon: @Composable () -> ImageVector,
val selectedIcon: @Composable () -> ImageVector = icon,
val icon: @Composable () -> Int,
val selectedIcon: @Composable () -> Int = icon,
val label: @Composable () -> String
) {
data object Home : NavigationItem("Home",
icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_home) },
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_home_hl) },
icon = { R.drawable.rider_pro_nav_home },
selectedIcon = { R.mipmap.rider_pro_nav_home_hl },
label = { stringResource(R.string.main_home) }
)
data object Street : NavigationItem("Street",
icon = { ImageVector.vectorResource(R.drawable.rider_pro_location) },
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_location_filed) },
data object Ai : NavigationItem("Ai",
icon = { R.drawable.rider_pro_nav_ai },
selectedIcon = { R.mipmap.rider_pro_nav_ai_hl },
label = { stringResource(R.string.main_home) }
)
data object Add : NavigationItem("Add",
icon = { ImageVector.vectorResource(R.drawable.ic_nav_add) },
selectedIcon = { ImageVector.vectorResource(R.drawable.ic_nav_add) },
label = { "" }
)
data object Message : NavigationItem("Message",
icon = { ImageVector.vectorResource(R.drawable.rider_pro_video_outline) },
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_video) },
icon = { R.drawable.ic_nav_add },
selectedIcon = { R.drawable.ic_nav_add },
label = { stringResource(R.string.main_home) }
)
data object Notification : NavigationItem("Notification",
icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_notification)},
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_notification) },
icon = { R.drawable.rider_pro_nav_notification },
selectedIcon = { R.mipmap.rider_pro_nav_message_hl },
label = { stringResource(R.string.main_message) }
)
data object Profile : NavigationItem("Profile",
icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_profile) },
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_profile_hl) },
icon = { R.drawable.rider_pro_nav_profile },
selectedIcon = { R.mipmap.rider_pro_nav_profile_hl },
label = { stringResource(R.string.main_profile) }
)
data object Ai : NavigationItem("Ai",
icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_search) },
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_search_hl) },
label = { stringResource(R.string.main_ai) }
)
}

View File

@@ -80,7 +80,7 @@ fun NotificationsScreen() {
val navController = LocalNavController.current
val systemUiController = rememberSystemUiController()
val context = LocalContext.current
var pagerState = rememberPagerState (pageCount = { 4 })
var pagerState = rememberPagerState (pageCount = { 3 })
var scope = rememberCoroutineScope()
val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = {
MessageListViewModel.viewModelScope.launch {
@@ -143,7 +143,7 @@ fun NotificationsScreen() {
modifier = Modifier
.size(24.dp)
.noRippleClickable {
navController.navigate(NavigationRoute.CreateGroupChat.route)
},
colorFilter = ColorFilter.tint(AppColors.text)
)

View File

@@ -1,6 +1,7 @@
package com.aiosman.ravenow.ui.index.tabs.moment
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -31,6 +32,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight
@@ -95,13 +97,16 @@ fun MomentsList() {
fontWeight = FontWeight.W600)
Spacer(modifier = Modifier.height(4.dp))
Box(
modifier = Modifier
.width(34.dp)
.height(4.dp)
.clip(RoundedCornerShape(16.dp))
.background(if (pagerState.currentPage == 0) AppColors.brandColorsColor else AppColors.background)
)
Image(
painter = painterResource(
if (pagerState.currentPage == 0) R.mipmap.tab_indicator_selected
else R.drawable.tab_indicator_unselected
),
contentDescription = "tab indicator",
modifier = Modifier
.width(34.dp)
.height(4.dp)
)
}
Spacer(modifier = Modifier.width(16.dp))
@@ -122,13 +127,16 @@ fun MomentsList() {
fontWeight = FontWeight.W600)
Spacer(modifier = Modifier.height(4.dp))
Box(
modifier = Modifier
.width(34.dp)
.height(4.dp)
.clip(RoundedCornerShape(16.dp))
.background(if (pagerState.currentPage == 1) AppColors.brandColorsColor else AppColors.background)
)
Image(
painter = painterResource(
if (pagerState.currentPage == 1) R.mipmap.tab_indicator_selected
else R.drawable.tab_indicator_unselected
),
contentDescription = "tab indicator",
modifier = Modifier
.width(34.dp)
.height(4.dp)
)
}
//热门tab
@@ -150,12 +158,15 @@ fun MomentsList() {
fontWeight = FontWeight.W600)
Spacer(modifier = Modifier.height(4.dp))
Box(
Image(
painter = painterResource(
if (pagerState.currentPage == 2) R.mipmap.tab_indicator_selected
else R.drawable.tab_indicator_unselected
),
contentDescription = "tab indicator",
modifier = Modifier
.width(34.dp)
.height(4.dp)
.clip(RoundedCornerShape(16.dp))
.background(if (pagerState.currentPage == 2) AppColors.brandColorsColor else AppColors.background)
)
}

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:translateX="-351"
android:translateY="-797">
<group
android:translateY="791">
<group
android:translateX="351"
android:translateY="6">
<path
android:fillType="evenOdd"
android:strokeWidth="1"
android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z" />
<path
android:fillType="evenOdd"
android:strokeColor="#000000"
android:strokeWidth="1.93476923"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:pathData="M 12 3 C 14.2091389993 3 16 4.79086100068 16 7 C 16 9.20913899932 14.2091389993 11 12 11 C 9.79086100068 11 8 9.20913899932 8 7 C 8 4.79086100068 9.79086100068 3 12 3 Z" />
<path
android:fillType="evenOdd"
android:strokeColor="#000000"
android:strokeWidth="1.93476923"
android:pathData="M12,14 C16.418278,14 20,16.0147186 20,18.5 C20,19.3079805 19.6214332,20.0662253 18.9586285,20.7216744 C18.5467837,20.900999 18.0920303,21 17.6141538,21 L6.38584615,21 C5.90796974,21 5.45321625,20.900999 5.04097677,20.7223879 C4.37856678,20.0662253 4,19.3079805 4,18.5 C4,16.0147186 7.581722,14 12,14 Z" />
</group>
</group>
</group>
</vector>

View File

@@ -1,20 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,9m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"
android:strokeWidth="1.93476923"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:fillType="evenOdd"/>
<path
android:pathData="M2.906,20.25C4.782,17.001 8.248,14.999 12,14.999C15.752,14.999 19.218,17.001 21.094,20.25"
android:strokeLineJoin="round"
android:strokeWidth="1.93476923"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:fillType="evenOdd"
android:strokeLineCap="round"/>
<group
android:translateX="-351"
android:translateY="-797">
<group
android:translateY="791">
<group
android:translateX="351"
android:translateY="6">
<path
android:fillType="evenOdd"
android:strokeWidth="1"
android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z" />
<path
android:fillType="evenOdd"
android:strokeColor="#000000"
android:strokeWidth="1.93476923"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:pathData="M 12 3 C 14.2091389993 3 16 4.79086100068 16 7 C 16 9.20913899932 14.2091389993 11 12 11 C 9.79086100068 11 8 9.20913899932 8 7 C 8 4.79086100068 9.79086100068 3 12 3 Z" />
<path
android:fillType="evenOdd"
android:strokeColor="#000000"
android:strokeWidth="1.93476923"
android:pathData="M12,14 C16.418278,14 20,16.0147186 20,18.5 C20,19.3079805 19.6214332,20.0662253 18.9586285,20.7216744 C18.5467837,20.900999 18.0920303,21 17.6141538,21 L6.38584615,21 C5.90796974,21 5.45321625,20.900999 5.04097677,20.7223879 C4.37856678,20.0662253 4,19.3079805 4,18.5 C4,16.0147186 7.581722,14 12,14 Z" />
</group>
</group>
</group>
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="34dp"
android:height="4dp"
android:viewportWidth="34"
android:viewportHeight="4">
<path
android:fillColor="#00000000"
android:pathData="M0,2C0,0.895 0.895,0 2,0L32,0C33.105,0 34,0.895 34,2C34,3.105 33.105,4 32,4L2,4C0.895,4 0,3.105 0,2Z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

View File

@@ -162,5 +162,9 @@
<string name="friend_chat_empty_subtitle">开始与朋友对话吧</string>
<string name="friend_chat_me_prefix">我: </string>
<string name="friend_chat_load_failed">加载失败</string>
<string name="create_group_chat">创建群聊</string>
<string name="quick_create">一键创建</string>
<string name="group_name">群聊名称</string>
<string name="search_placeholder">搜索</string>
</resources>

View File

@@ -158,5 +158,9 @@
<string name="friend_chat_empty_subtitle">开始与朋友对话吧</string>
<string name="friend_chat_me_prefix">我: </string>
<string name="friend_chat_load_failed">加载失败</string>
<string name="create_group_chat">Create Group Chat</string>
<string name="quick_create">Quick Create</string>
<string name="group_name">Group Name</string>
<string name="search_placeholder">Search</string>
</resources>