界面调整、以及修复bug等

-收藏界面和动态界面添加了多图角标和视频角标
-短视频新增双击点赞和双击取消点赞功能
-修复帖子详情页的多图内容不能左右滑动图片,去掉帖子详情页多图下通过Next和Previous按钮来切换图片
-评论框界面调整
This commit is contained in:
2025-11-24 18:35:51 +08:00
parent 6d18e13826
commit c94fcd493e
6 changed files with 215 additions and 88 deletions

View File

@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -31,6 +32,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@@ -79,8 +81,9 @@ class CommentModalViewModel(
fun CommentModalContent( fun CommentModalContent(
postId: Int? = null, postId: Int? = null,
commentCount: Int = 0, commentCount: Int = 0,
onCommentAdded: () -> Unit = {}, onDismiss: () -> Unit = {},
onDismiss: () -> Unit = {} showTitle: Boolean = true,
onCommentAdded: () -> Unit = {}
) { ) {
val model = viewModel<CommentModalViewModel>( val model = viewModel<CommentModalViewModel>(
key = "CommentModalViewModel_$postId", key = "CommentModalViewModel_$postId",
@@ -161,28 +164,42 @@ fun CommentModalContent(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
) { ) {
// 拖动手柄
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 16.dp, bottom = 16.dp, end = 16.dp) .padding(top = 8.dp, bottom = 12.dp),
contentAlignment = Alignment.Center
) { ) {
Text( Box(
stringResource(R.string.comment), modifier = Modifier
fontSize = 18.sp, .width(40.dp)
fontWeight = FontWeight.Bold, .height(4.dp)
color = AppColors.text, .clip(RoundedCornerShape(50))
modifier = Modifier.align(Alignment.Center) .background(AppColors.divider)
) )
} }
HorizontalDivider( if (showTitle) {
color = AppColors.divider Box(
) modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, bottom = 16.dp, end = 16.dp)
) {
Text(
stringResource(R.string.comment),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
modifier = Modifier.align(Alignment.Center)
)
}
}
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp), .padding(horizontal = 20.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {

View File

@@ -17,15 +17,19 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -155,7 +159,10 @@ fun FavouriteListPage() {
.clip(RoundedCornerShape(8.dp)), .clip(RoundedCornerShape(8.dp)),
context = context context = context
) )
if (momentItem.images.size > 1) {
val isVideoMoment = momentItem.images.isEmpty() && !momentItem.videos.isNullOrEmpty()
if (momentItem.images.size > 1 || (momentItem.videos?.size ?: 0) > 1) {
Box( Box(
modifier = Modifier modifier = Modifier
.padding(top = 8.dp, end = 8.dp) .padding(top = 8.dp, end = 8.dp)
@@ -168,6 +175,31 @@ fun FavouriteListPage() {
) )
} }
} }
if (isVideoMoment) {
Box(
modifier = Modifier
.padding(top = 8.dp, end = 8.dp)
.align(Alignment.TopEnd)
) {
Box(
modifier = Modifier
.size(24.dp)
.background(
color = Color.Black.copy(alpha = 0.4f),
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = "",
tint = Color.White,
modifier = Modifier.size(16.dp)
)
}
}
}
} }
} }
} }

View File

@@ -33,8 +33,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
@@ -97,6 +99,9 @@ fun VideoRecommendationItem(
skipPartiallyExpanded = true skipPartiallyExpanded = true
) )
var pauseIconVisibleState by remember { mutableStateOf(false) } var pauseIconVisibleState by remember { mutableStateOf(false) }
// 防抖:记录上次双击时间,防止快速重复双击
val lastDoubleTapTime = remember { mutableStateOf(0L) }
val doubleTapDebounceTime = 500L // 500ms 防抖时间
val exoPlayer = remember(videoUrl) { val exoPlayer = remember(videoUrl) {
ExoPlayer.Builder(context) ExoPlayer.Builder(context)
@@ -167,18 +172,32 @@ fun VideoRecommendationItem(
}, },
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.noRippleClickable { .pointerInput(videoUrl, moment.liked) {
pauseIconVisibleState = true detectTapGestures(
exoPlayer.pause() onDoubleTap = { offset ->
scope.launch { // 双击点赞/取消点赞
delay(100) val currentTime = System.currentTimeMillis()
if (exoPlayer.isPlaying) { if (currentTime - lastDoubleTapTime.value > doubleTapDebounceTime) {
lastDoubleTapTime.value = currentTime
// 检查当前点赞状态,如果已点赞则取消点赞,如果未点赞则点赞
onLikeClick?.invoke(moment)
}
},
onTap = {
// 单击播放/暂停
pauseIconVisibleState = true
exoPlayer.pause() exoPlayer.pause()
} else { scope.launch {
pauseIconVisibleState = false delay(100)
exoPlayer.play() if (exoPlayer.isPlaying) {
exoPlayer.pause()
} else {
pauseIconVisibleState = false
exoPlayer.play()
}
}
} }
} )
} }
) )
@@ -300,7 +319,9 @@ fun VideoRecommendationItem(
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { showCommentModal = false }, onDismissRequest = { showCommentModal = false },
containerColor = Color.White, containerColor = Color.White,
sheetState = sheetState sheetState = sheetState,
dragHandle = {},
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
) { ) {
CommentModalContent(postId = moment.id) { CommentModalContent(postId = moment.id) {
// 评论添加后的回调 // 评论添加后的回调

View File

@@ -18,6 +18,9 @@ import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -273,6 +276,45 @@ fun GalleryGrid(
) )
) )
} }
if (momentItem.images.size > 1 || (momentItem.videos?.size ?: 0) > 1) {
Box(
modifier = Modifier
.padding(top = 8.dp, end = 8.dp)
.align(Alignment.TopEnd)
) {
Image(
modifier = Modifier.size(24.dp),
painter = painterResource(R.drawable.rider_pro_picture_more),
contentDescription = "",
)
}
}
if (isVideoMoment) {
Box(
modifier = Modifier
.padding(top = 8.dp, end = 8.dp)
.align(Alignment.TopEnd)
) {
Box(
modifier = Modifier
.size(24.dp)
.background(
color = Color.Black.copy(alpha = 0.4f),
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = "",
tint = Color.White,
modifier = Modifier.size(16.dp)
)
}
}
}
} }
} }
} }

View File

@@ -13,6 +13,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -56,6 +57,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
@@ -467,6 +469,9 @@ fun VideoPlayer(
.clip(RectangleShape) .clip(RectangleShape)
) { ) {
var playerView by remember { mutableStateOf<PlayerView?>(null) } var playerView by remember { mutableStateOf<PlayerView?>(null) }
// 防抖:记录上次双击时间,防止快速重复双击
val lastDoubleTapTime = remember { mutableStateOf(0L) }
val doubleTapDebounceTime = 500L // 500ms 防抖时间
// 使用 key 强制每个视频的 PlayerView 完全独立,避免布局状态残留 // 使用 key 强制每个视频的 PlayerView 完全独立,避免布局状态残留
androidx.compose.runtime.key(videoUrl) { androidx.compose.runtime.key(videoUrl) {
@@ -479,8 +484,24 @@ fun VideoPlayer(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(RectangleShape) .clip(RectangleShape)
.noRippleClickable { .pointerInput(videoUrl, moment?.liked) {
handleVideoClick(pauseIconVisibleState, exoPlayer, scope) detectTapGestures(
onDoubleTap = { offset ->
// 双击点赞/取消点赞
val currentTime = System.currentTimeMillis()
if (currentTime - lastDoubleTapTime.value > doubleTapDebounceTime) {
lastDoubleTapTime.value = currentTime
moment?.let {
// 检查当前点赞状态,如果已点赞则取消点赞,如果未点赞则点赞
onLikeClick?.invoke(it)
}
}
},
onTap = {
// 单击播放/暂停
handleVideoClick(pauseIconVisibleState, exoPlayer, scope)
}
)
} }
) )
} }
@@ -660,7 +681,8 @@ fun VideoPlayer(
}, },
containerColor = AppColors.background, containerColor = AppColors.background,
sheetState = sheetState, sheetState = sheetState,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) dragHandle = {},
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -670,6 +692,7 @@ fun VideoPlayer(
CommentModalContent( CommentModalContent(
postId = moment.id, postId = moment.id,
commentCount = moment.commentCount, commentCount = moment.commentCount,
showTitle = false,
onCommentAdded = { onCommentAdded = {
onCommentAdded?.invoke(moment) onCommentAdded?.invoke(moment)
} }

View File

@@ -12,6 +12,8 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
@@ -1159,14 +1161,26 @@ fun ImageViewerDialog(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun PostImageView( fun PostImageView(
images: List<MomentImageEntity>, images: List<MomentImageEntity>,
initialPage: Int? = 0 initialPage: Int? = 0
) { ) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope()
var isImageViewerDialog by remember { mutableStateOf(false) } var isImageViewerDialog by remember { mutableStateOf(false) }
var currentImageIndex by remember { mutableStateOf(initialPage ?: 0) } val initialPageIndex = initialPage ?: 0
val pagerState = rememberPagerState(
pageCount = { images.size },
initialPage = initialPageIndex.coerceIn(0, maxOf(0, images.size - 1))
)
var currentImageIndex by remember { mutableStateOf(pagerState.currentPage) }
// 同步 pagerState 的当前页面到 currentImageIndex
LaunchedEffect(pagerState.currentPage) {
currentImageIndex = pagerState.currentPage
}
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
@@ -1187,23 +1201,31 @@ fun PostImageView(
modifier = Modifier modifier = Modifier
) { ) {
if (images.isNotEmpty()) { if (images.isNotEmpty()) {
CustomAsyncImage( HorizontalPager(
context, state = pagerState,
images[currentImageIndex].thumbnail,
contentDescription = "Image",
contentScale = ContentScale.Crop,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.fillMaxWidth() .fillMaxWidth()
.pointerInput(Unit) { ) { page ->
detectTapGestures( val image = images[page]
onTap = { CustomAsyncImage(
isImageViewerDialog = true context,
} image.thumbnail,
) contentDescription = "Image",
} blurHash = image.blurHash,
.background(Color.Gray.copy(alpha = 0.1f)) contentScale = ContentScale.Crop,
) modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures(
onTap = {
isImageViewerDialog = true
}
)
}
.background(Color.Gray.copy(alpha = 0.1f))
)
}
} }
// 图片导航控件 // 图片导航控件
@@ -1212,56 +1234,26 @@ fun PostImageView(
modifier = Modifier modifier = Modifier
.padding(8.dp) .padding(8.dp)
.fillMaxWidth(), .fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Previous button
Text(
text = "Previous",
modifier = Modifier
.padding(8.dp)
.noRippleClickable {
if (currentImageIndex > 0) {
currentImageIndex--
}
},
color = if (currentImageIndex > 0) Color.Blue else Color.Gray
)
// Indicators // Indicators
Row( images.forEachIndexed { index, _ ->
horizontalArrangement = Arrangement.Center Box(
) { modifier = Modifier
images.forEachIndexed { index, _ -> .size(4.dp)
Box( .clip(CircleShape)
modifier = Modifier .background(
.size(4.dp) if (pagerState.currentPage == index) Color.Red else Color.Gray.copy(
.clip(CircleShape) alpha = 0.5f
.background(
if (currentImageIndex == index) Color.Red else Color.Gray.copy(
alpha = 0.5f
)
) )
.padding(4.dp) )
) .padding(4.dp)
if (index < images.size - 1) { )
Spacer(modifier = Modifier.width(8.dp)) if (index < images.size - 1) {
} Spacer(modifier = Modifier.width(8.dp))
} }
} }
// Next button
Text(
text = "Next",
modifier = Modifier
.padding(8.dp)
.noRippleClickable {
if (currentImageIndex < images.size - 1) {
currentImageIndex++
}
},
color = if (currentImageIndex < images.size - 1) Color.Blue else Color.Gray
)
} }
} }
} }