Merge pull request #68 from Kevinlinpr/nagisa

修复动态-短视频界面的各种bug并优化ui
This commit is contained in:
2025-11-12 10:32:54 +08:00
committed by GitHub
18 changed files with 215 additions and 63 deletions

View File

@@ -53,7 +53,8 @@
android:label="@string/app_name"
android:theme="@style/Theme.App.Starting"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustResize"
android:configChanges="fontScale|orientation|screenSize|keyboardHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View File

@@ -7,6 +7,7 @@ import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.os.Bundle
@@ -22,6 +23,8 @@ import androidx.compose.animation.SharedTransitionScope
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.lifecycle.ProcessLifecycleOwner
@@ -57,6 +60,21 @@ class MainActivity : ComponentActivity() {
private val scope = CoroutineScope(Dispatchers.Main)
val context = this
override fun attachBaseContext(newBase: Context) {
// 禁用字体缩放,固定字体大小为系统默认大小
val configuration = Configuration(newBase.resources.configuration)
configuration.fontScale = 1.0f
val context = newBase.createConfigurationContext(configuration)
super.attachBaseContext(context)
}
override fun onConfigurationChanged(newConfig: Configuration) {
// 确保配置变化时字体缩放保持为 1.0
val config = Configuration(newConfig)
config.fontScale = 1.0f
super.onConfigurationChanged(config)
}
// 请求通知权限
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
@@ -128,6 +146,15 @@ class MainActivity : ComponentActivity() {
}
setContent {
// 强制字体缩放为 1.0 - 通过覆盖 Density 来实现
val density = LocalDensity.current
val fixedDensity = remember {
androidx.compose.ui.unit.Density(
density = density.density,
fontScale = 1.0f
)
}
var showSplash by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
@@ -139,7 +166,8 @@ class MainActivity : ComponentActivity() {
SplashScreen()
} else {
CompositionLocalProvider(
LocalAppTheme provides AppState.appTheme
LocalAppTheme provides AppState.appTheme,
LocalDensity provides fixedDensity
) {
CheckUpdateDialog()
// 全局挂载积分底部弹窗 Host

View File

@@ -2,6 +2,7 @@ package com.aiosman.ravenow
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.util.Log
import com.google.firebase.FirebaseApp
import com.google.firebase.perf.FirebasePerformance
@@ -11,6 +12,14 @@ import com.google.firebase.perf.FirebasePerformance
*/
class RaveNowApplication : Application() {
override fun attachBaseContext(base: Context) {
// 禁用字体缩放,固定字体大小为系统默认大小
val configuration = Configuration(base.resources.configuration)
configuration.fontScale = 1.0f
val context = base.createConfigurationContext(configuration)
super.attachBaseContext(context)
}
override fun onCreate() {
super.onCreate()

View File

@@ -40,6 +40,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.CommentEntity
import com.aiosman.ravenow.ui.composables.EditCommentBottomModal
@@ -88,6 +89,7 @@ fun CommentModalContent(
}
)
val commentViewModel = model.commentsViewModel
val AppColors = LocalAppTheme.current
var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
LaunchedEffect(Unit) {
@@ -99,10 +101,24 @@ fun CommentModalContent(
var bottomPadding by remember { mutableStateOf(0.dp) }
var softwareKeyboardController = LocalSoftwareKeyboardController.current
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
var shouldAutoFocus by remember { mutableStateOf(false) }
LaunchedEffect(imePadding) {
bottomPadding = imePadding.dp
}
// 当设置回复评论时,自动聚焦到输入框
LaunchedEffect(replyComment) {
if (replyComment != null) {
// 延迟一下,确保输入框已经渲染
kotlinx.coroutines.delay(100)
shouldAutoFocus = true
// 请求显示键盘
softwareKeyboardController?.show()
} else {
shouldAutoFocus = false
}
}
DisposableEffect(Unit) {
onDispose {
onDismiss()
@@ -113,7 +129,7 @@ fun CommentModalContent(
onDismissRequest = {
showCommentMenu = false
},
containerColor = Color.White,
containerColor = AppColors.background,
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
),
@@ -152,12 +168,13 @@ fun CommentModalContent(
stringResource(R.string.comment),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
modifier = Modifier.align(Alignment.Center)
)
}
HorizontalDivider(
color = Color(0xFFF7F7F7)
color = AppColors.divider
)
Row(
modifier = Modifier
@@ -169,7 +186,7 @@ fun CommentModalContent(
Text(
text = stringResource(id = R.string.comment_count, commentCount),
fontSize = 14.sp,
color = Color(0xff666666)
color = AppColors.secondaryText
)
OrderSelectionComponent {
commentViewModel.order = it
@@ -193,7 +210,9 @@ fun CommentModalContent(
},
onReply = { parentComment, _, _, _ ->
// 设置回复的评论,这样 EditCommentBottomModal 会显示回复输入框
// CommentContent 内部已经处理了游客模式检查,所以这里直接设置即可
replyComment = parentComment
},
)
Spacer(modifier = Modifier.height(72.dp))
@@ -204,9 +223,12 @@ fun CommentModalContent(
Column(
modifier = Modifier
.fillMaxWidth()
.background(Color(0xfff7f7f7))
.background(AppColors.secondaryBackground)
) {
EditCommentBottomModal(replyComment) {
EditCommentBottomModal(
replyComment = replyComment,
autoFocus = shouldAutoFocus
) {
commentViewModel.viewModelScope.launch {
if (replyComment != null) {
if (replyComment?.parentCommentId != null) {
@@ -224,6 +246,13 @@ fun CommentModalContent(
// 顶级评论
commentViewModel.createComment(it)
}
// 评论创建成功后调用回调
onCommentAdded()
// 清空回复状态和自动聚焦状态
replyComment = null
shouldAutoFocus = false
// 隐藏键盘
softwareKeyboardController?.hide()
}
}

View File

@@ -36,6 +36,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
@@ -60,10 +61,15 @@ fun EditCommentBottomModal(
var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
val focusRequester = remember { FocusRequester() }
val context = LocalContext.current
val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(autoFocus) {
if (autoFocus) {
// 延迟一下,确保输入框已经渲染完成
kotlinx.coroutines.delay(150)
focusRequester.requestFocus()
// 显示键盘
keyboardController?.show()
}
}
@@ -83,7 +89,7 @@ fun EditCommentBottomModal(
.fillMaxWidth()
.weight(1f)
.clip(RoundedCornerShape(20.dp))
.background(Color.Gray.copy(alpha = 0.1f))
.background(AppColors.inputBackground)
.padding(horizontal = 16.dp, vertical = 16.dp)
) {
Row(
@@ -100,7 +106,7 @@ fun EditCommentBottomModal(
.weight(1f)
.focusRequester(focusRequester),
textStyle = TextStyle(
color = Color.Black,
color = AppColors.text,
fontWeight = FontWeight.Normal
),
decorationBox = { innerTextField ->

View File

@@ -155,6 +155,17 @@ fun FavouriteListPage() {
) {
items(moments.itemCount) { idx ->
val momentItem = moments[idx] ?: return@items
// 获取缩略图URL优先使用图片如果没有图片则使用视频缩略图
val thumbnailUrl = when {
momentItem.images.isNotEmpty() -> momentItem.images[0].thumbnail
momentItem.videos != null && momentItem.videos.isNotEmpty() -> {
momentItem.videos.first().thumbnailUrl ?: momentItem.videos.first().thumbnailDirectUrl
}
else -> null
}
if (thumbnailUrl == null) return@items
Box(
modifier = Modifier
.fillMaxWidth()
@@ -169,7 +180,7 @@ fun FavouriteListPage() {
}
) {
CustomAsyncImage(
imageUrl = momentItem.images[0].thumbnail,
imageUrl = thumbnailUrl,
contentDescription = "",
modifier = Modifier
.fillMaxSize()

View File

@@ -154,12 +154,12 @@ fun ShortVideoScreen() {
}
},
onCommentClick = { moment ->
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
scope.launch {
viewModel.onAddComment(moment.id)
}
// 点击评论图标只是打开评论弹窗,不应该增加评论数
},
onCommentAdded = { moment ->
// 评论添加后的回调,更新评论数
scope.launch {
viewModel.onAddComment(moment.id)
}
},
onFavoriteClick = { moment ->
@@ -178,6 +178,15 @@ fun ShortVideoScreen() {
onShareClick = { moment ->
// TODO: 实现分享功能
},
onAvatarClick = { moment ->
// 点击头像进入用户界面
navController.navigate(
com.aiosman.ravenow.ui.NavigationRoute.AccountProfile.route.replace(
"{id}",
moment.authorId.toString()
)
)
},
onPageChanged = { idx -> currentIndex.value = idx }
)
}

View File

@@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.RoundedCornerShape
@@ -43,7 +44,9 @@ 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.graphics.toArgb
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.painterResource
@@ -67,6 +70,7 @@ import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.ui.comment.CommentModalContent
@@ -75,6 +79,9 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
// 激活状态的颜色(点赞/收藏时的红色)
private val ActiveIconColor = Color(0xFFD80264)
@Composable
fun ShortViewCompose(
videoItemsUrl: List<String> = emptyList(),
@@ -84,8 +91,10 @@ fun ShortViewCompose(
videoBottom: @Composable ((MomentEntity) -> Unit)? = null,
onLikeClick: ((MomentEntity) -> Unit)? = null,
onCommentClick: ((MomentEntity) -> Unit)? = null,
onCommentAdded: ((MomentEntity) -> Unit)? = null,
onFavoriteClick: ((MomentEntity) -> Unit)? = null,
onShareClick: ((MomentEntity) -> Unit)? = null,
onAvatarClick: ((MomentEntity) -> Unit)? = null,
onPageChanged: ((Int) -> Unit)? = null
) {
// 优先使用 videoMoments如果没有则使用 videoItemsUrl
@@ -159,8 +168,10 @@ fun ShortViewCompose(
VideoBottom = videoBottom,
onLikeClick = onLikeClick,
onCommentClick = onCommentClick,
onCommentAdded = onCommentAdded,
onFavoriteClick = onFavoriteClick,
onShareClick = onShareClick
onShareClick = onShareClick,
onAvatarClick = onAvatarClick
)
}
@@ -183,8 +194,10 @@ private fun SingleVideoItemContent(
VideoBottom: @Composable ((MomentEntity) -> Unit)? = null,
onLikeClick: ((MomentEntity) -> Unit)? = null,
onCommentClick: ((MomentEntity) -> Unit)? = null,
onCommentAdded: ((MomentEntity) -> Unit)? = null,
onFavoriteClick: ((MomentEntity) -> Unit)? = null,
onShareClick: ((MomentEntity) -> Unit)? = null
onShareClick: ((MomentEntity) -> Unit)? = null,
onAvatarClick: ((MomentEntity) -> Unit)? = null
) {
Box(
modifier = Modifier
@@ -199,8 +212,10 @@ private fun SingleVideoItemContent(
pauseIconVisibleState = pauseIconVisibleState,
onLikeClick = onLikeClick,
onCommentClick = onCommentClick,
onCommentAdded = onCommentAdded,
onFavoriteClick = onFavoriteClick,
onShareClick = onShareClick
onShareClick = onShareClick,
onAvatarClick = onAvatarClick
)
VideoHeader.invoke()
if (moment != null && VideoBottom != null) {
@@ -228,12 +243,17 @@ fun VideoPlayer(
pauseIconVisibleState: MutableState<Boolean>,
onLikeClick: ((MomentEntity) -> Unit)? = null,
onCommentClick: ((MomentEntity) -> Unit)? = null,
onCommentAdded: ((MomentEntity) -> Unit)? = null,
onFavoriteClick: ((MomentEntity) -> Unit)? = null,
onShareClick: ((MomentEntity) -> Unit)? = null,
onAvatarClick: ((MomentEntity) -> Unit)? = null,
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val lifecycleOwner = LocalLifecycleOwner.current
val configuration = androidx.compose.ui.platform.LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val sheetHeight = screenHeight * 0.7f // 屏幕的一大半高度
var showCommentModal by remember { mutableStateOf(false) }
var sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
@@ -371,42 +391,51 @@ fun VideoPlayer(
horizontalAlignment = Alignment.CenterHorizontally
) {
if (moment != null) {
UserAvatar(avatarUrl = moment.avatar)
VideoBtn(
icon = R.drawable.rider_pro_video_like,
text = formatCount(moment.likeCount)
) {
moment?.let { onLikeClick?.invoke(it) }
}
VideoBtn(
icon = R.drawable.rider_pro_video_comment,
text = formatCount(moment.commentCount)
) {
moment?.let {
showCommentModal = true
onCommentClick?.invoke(it)
}
}
VideoBtn(
icon = R.drawable.rider_pro_video_favor,
text = formatCount(moment.favoriteCount)
) {
moment?.let { onFavoriteClick?.invoke(it) }
}
VideoBtn(
icon = R.drawable.rider_pro_video_share,
text = formatCount(moment.shareCount)
) {
moment?.let { onShareClick?.invoke(it) }
// 使用 key 确保状态变化时重新组合
androidx.compose.runtime.key(moment.id, moment.isFavorite) {
UserAvatar(
avatarUrl = moment.avatar,
onClick = { onAvatarClick?.invoke(moment) }
)
VideoBtn(
icon = if (moment.liked) R.drawable.rider_pro_moment_liked else R.drawable.rider_pro_moment_like,
text = formatCount(moment.likeCount),
isActive = moment.liked,
onClick = { onLikeClick?.invoke(moment) }
)
VideoBtn(
icon = R.mipmap.icon_comment,
text = formatCount(moment.commentCount),
onClick = {
showCommentModal = true
onCommentClick?.invoke(moment)
}
)
VideoBtn(
icon = if (moment.isFavorite) R.mipmap.icon_variant_2 else R.mipmap.icon_collect,
text = formatCount(moment.favoriteCount),
isActive = false, // 收藏后不使用红色滤镜,保持图标原本颜色
keepOriginalColor = moment.isFavorite, // 收藏后保持原始颜色
onClick = { onFavoriteClick?.invoke(moment) }
)
VideoBtn(
icon = R.mipmap.icon_share,
text = formatCount(moment.shareCount),
onClick = { onShareClick?.invoke(moment) }
)
}
} else {
UserAvatar()
VideoBtn(icon = R.drawable.rider_pro_video_like, text = "0")
VideoBtn(icon = R.drawable.rider_pro_video_comment, text = "0") {
showCommentModal = true
}
VideoBtn(icon = R.drawable.rider_pro_video_favor, text = "0")
VideoBtn(icon = R.drawable.rider_pro_video_share, text = "0")
VideoBtn(icon = R.drawable.rider_pro_moment_like, text = "0")
VideoBtn(
icon = R.mipmap.icon_comment,
text = "0",
onClick = {
showCommentModal = true
}
)
VideoBtn(icon = R.mipmap.icon_collect, text = "0")
VideoBtn(icon = R.mipmap.icon_share, text = "0")
}
}
}
@@ -465,26 +494,44 @@ fun VideoPlayer(
}
if (showCommentModal && moment != null) {
val AppColors = LocalAppTheme.current
ModalBottomSheet(
onDismissRequest = { showCommentModal = false },
containerColor = Color.White,
sheetState = sheetState
containerColor = AppColors.background,
sheetState = sheetState,
modifier = Modifier
.fillMaxWidth()
.height(sheetHeight)
) {
CommentModalContent(postId = moment.id) {
}
CommentModalContent(
postId = moment.id,
commentCount = moment.commentCount,
onCommentAdded = {
onCommentAdded?.invoke(moment)
}
)
}
}
}
@Composable
fun UserAvatar(avatarUrl: String? = null) {
fun UserAvatar(
avatarUrl: String? = null,
onClick: (() -> Unit)? = null
) {
Box(
modifier = Modifier
.padding(bottom = 16.dp)
.size(40.dp)
.border(width = 3.dp, color = Color.White, shape = RoundedCornerShape(40.dp))
.clip(RoundedCornerShape(40.dp))
.then(
if (onClick != null) {
Modifier.noRippleClickable { onClick() }
} else {
Modifier
}
)
) {
if (avatarUrl != null && avatarUrl.isNotEmpty()) {
CustomAsyncImage(
@@ -512,19 +559,31 @@ private fun formatCount(count: Int): String {
}
@Composable
fun VideoBtn(@DrawableRes icon: Int, text: String, onClick: (() -> Unit)? = null) {
fun VideoBtn(
@DrawableRes icon: Int,
text: String,
onClick: (() -> Unit)? = null,
isActive: Boolean = false,
keepOriginalColor: Boolean = false // 是否保持原始颜色(不应用白色滤镜)
) {
Column(
modifier = Modifier
.padding(bottom = 16.dp)
.clickable {
.noRippleClickable {
onClick?.invoke()
},
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
modifier = Modifier.size(36.dp),
modifier = Modifier.size(30.dp),
painter = painterResource(id = icon),
contentDescription = ""
contentDescription = "",
contentScale = ContentScale.FillBounds, // 填满容器,让图标看起来更大
colorFilter = when {
isActive -> ColorFilter.tint(ActiveIconColor)
keepOriginalColor -> null // 保持原始颜色
else -> ColorFilter.tint(Color.White) // 未激活状态时图标为白色
}
)
Text(
text = text,

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B