添加业务逻辑

This commit is contained in:
2024-07-28 15:07:08 +08:00
parent dfbc151e4e
commit f35bde65a2
8 changed files with 355 additions and 260 deletions

View File

@@ -0,0 +1,98 @@
package com.aiosman.riderpro.data.comment
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.aiosman.riderpro.data.ListContainer
import java.io.IOException
import kotlin.random.Random
data class Comment(
val name: String,
val comment: String,
val date: String,
val likes: Int,
val replies: List<Comment>
)
class CommentPagingSource(
private val remoteDataSource: CommentRemoteDataSource,
) : PagingSource<Int, Comment>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Comment> {
return try {
val currentPage = params.key ?: 1
val comments = remoteDataSource.getComments(
pageNumber = currentPage
)
LoadResult.Page(
data = comments.list,
prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = if (comments.list.isEmpty()) null else comments.page + 1
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
}
}
override fun getRefreshKey(state: PagingState<Int, Comment>): Int? {
return state.anchorPosition
}
}
class CommentRemoteDataSource(
private val commentService: CommentService,
) {
suspend fun getComments(pageNumber: Int): ListContainer<Comment> {
return commentService.getComments(pageNumber)
}
}
interface CommentService {
suspend fun getComments(pageNumber: Int): ListContainer<Comment>
}
class TestCommentServiceImpl : CommentService {
private val mockData = generateMockComments(100)
override suspend fun getComments(pageNumber: Int): ListContainer<Comment> {
val from = pageNumber * DataBatchSize
val to = (pageNumber + 1) * DataBatchSize
val currentSublist = mockData.subList(from, to)
return ListContainer(
total = mockData.size,
page = pageNumber,
pageSize = DataBatchSize,
list = currentSublist
)
}
private fun generateMockComments(count: Int): List<Comment> {
return (0 until count).map {
Comment(
name = "User $it",
comment = "This is comment $it",
date = "2023-02-02 11:23",
likes = Random.nextInt(0, 100),
replies = generateMockReplies()
)
}
}
private fun generateMockReplies(): List<Comment> {
val replyCount = Random.nextInt(0, 6)
return (0 until replyCount).map {
Comment(
name = "Reply User $it",
comment = "This is reply $it",
date = "2023-02-02 11:23",
likes = Random.nextInt(0, 100),
replies = emptyList()
)
}
}
companion object {
const val DataBatchSize = 5
}
}

View File

@@ -1,12 +1,12 @@
package com.aiosman.riderpro.data.moment
import android.net.http.HttpException
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.ListContainer
import com.aiosman.riderpro.model.MomentItem
import java.io.IOException
import kotlin.random.Random
class MomentPagingSource(
private val remoteDataSource: MomentRemoteDataSource,
@@ -45,9 +45,15 @@ class MomentRemoteDataSource(
interface MomentService {
suspend fun getMoments(pageNumber: Int): ListContainer<MomentItem>
suspend fun getMomentById(id: Int): MomentItem
}
class TestMomentServiceImpl() : MomentService {
var imageList = listOf(
"https://img.freepik.com/free-photo/white-billboard-template_23-2147726635.jpg?t=st=1722150015~exp=1722153615~hmac=5540620196d7898215d822be26353c87a63d51bbfb2b814e032626e1948a1583&w=740",
"https://img.freepik.com/free-photo/minimal-clothing-label-fashion-brands_53876-111053.jpg?w=1060&t=st=1722150122~exp=1722150722~hmac=67f8a2b6abfe3d08714cf0cc0085485c3221e1ba00dda14378b03753dce39153",
"https://img.freepik.com/free-photo/marketing-strategy-planning-strategy-concept_53876-42950.jpg"
)
val mockData = (0..300).toList().mapIndexed { idx, _ ->
MomentItem(
id = idx,
@@ -61,13 +67,18 @@ class TestMomentServiceImpl() : MomentService {
likeCount = 21,
commentCount = 43,
shareCount = 33,
favoriteCount = 211
favoriteCount = 211,
images = imageList.shuffled().take(3),
)
}
val testMomentBackend = TestMomentBackend(mockData)
override suspend fun getMoments(pageNumber: Int): ListContainer<MomentItem> {
return testMomentBackend.fetchMomentItems(pageNumber)
}
override suspend fun getMomentById(id: Int): MomentItem {
return mockData[id]
}
}
class TestMomentBackend(

View File

@@ -15,7 +15,8 @@ data class MomentItem(
val likeCount: Int,
val commentCount: Int,
val shareCount: Int,
val favoriteCount: Int
val favoriteCount: Int,
val images: List<String> = emptyList()
)
val momentTestItem = MomentItem(

View File

@@ -35,13 +35,32 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.ui.post.CommentsSection
import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.comment.Comment
import com.aiosman.riderpro.data.comment.CommentPagingSource
import com.aiosman.riderpro.data.comment.CommentRemoteDataSource
import com.aiosman.riderpro.data.comment.TestCommentServiceImpl
import com.aiosman.riderpro.model.MomentItem
import com.aiosman.riderpro.ui.index.tabs.moment.MomentViewModel.momentListPagingSource
import kotlinx.coroutines.flow.Flow
@Preview
@Composable
fun CommentModalContent(onDismiss: () -> Unit = {}) {
var commentSource = CommentPagingSource(
CommentRemoteDataSource(TestCommentServiceImpl())
)
val commentsFlow: Flow<PagingData<Comment>> = Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
pagingSourceFactory = { commentSource }
).flow
val comments = commentsFlow.collectAsLazyPagingItems()
val insets = WindowInsets
val imePadding = insets.ime.getBottom(density = LocalDensity.current)
var bottomPadding by remember { mutableStateOf(0.dp) }
@@ -80,7 +99,11 @@ fun CommentModalContent(onDismiss: () -> Unit = {}) {
.padding(horizontal = 16.dp)
.weight(1f)
) {
CommentsSection{}
CommentsSection(lazyPagingItems = comments) {
}
}
Box(
modifier = Modifier
@@ -131,7 +154,6 @@ fun CommentModalContent(onDismiss: () -> Unit = {}) {
}
}
}

View File

@@ -0,0 +1,76 @@
package com.aiosman.riderpro.ui.composables
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
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.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.aiosman.riderpro.R
@Composable
fun EditCommentBottomModal() {
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color(0xfff7f7f7))
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Center),
verticalAlignment = Alignment.CenterVertically
) {
// rounded
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.clip(RoundedCornerShape(20.dp))
.background(Color(0xffe5e5e5))
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
BasicTextField(
value = "",
onValueChange = { },
modifier = Modifier
.fillMaxWidth(),
textStyle = TextStyle(
color = Color.Black,
fontWeight = FontWeight.Normal
),
minLines = 5
)
}
Spacer(modifier = Modifier.width(16.dp))
Image(
painter = painterResource(id = R.drawable.rider_pro_send),
contentDescription = "Send",
modifier = Modifier
.size(32.dp)
)
}
}
}

View File

@@ -10,6 +10,7 @@ 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.aspectRatio
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
@@ -36,6 +37,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
@@ -43,6 +45,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.paging.compose.collectAsLazyPagingItems
import coil.compose.AsyncImage
import com.aiosman.riderpro.LocalAnimatedContentScope
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.LocalSharedTransitionScope
@@ -251,34 +254,30 @@ fun MomentTopRowGroup(momentItem: MomentItem) {
fun MomentContentGroup(
momentItem: MomentItem,
) {
val sharedTransitionScope = LocalSharedTransitionScope.current
val animatedContentScope = LocalAnimatedContentScope.current
with(sharedTransitionScope) {
Text(
text = momentItem.momentTextContent,
// val sharedTransitionScope = LocalSharedTransitionScope.current
// val animatedContentScope = LocalAnimatedContentScope.current
val displayImageUrl = momentItem.images.firstOrNull()
Text(
text = momentItem.momentTextContent,
modifier = Modifier
.fillMaxWidth()
.padding(top = 22.dp, bottom = 16.dp, start = 24.dp, end = 24.dp),
fontSize = 16.sp
)
displayImageUrl?.let {
AsyncImage(
it,
modifier = Modifier
.sharedElement(
sharedTransitionScope.rememberSharedContentState(key = "text-${momentItem.id}"),
animatedVisibilityScope = animatedContentScope
)
.fillMaxWidth()
.padding(top = 22.dp, bottom = 16.dp, start = 24.dp, end = 24.dp),
fontSize = 16.sp
)
Image(
modifier = Modifier
.sharedElement(
sharedTransitionScope.rememberSharedContentState(key = "image-${momentItem.id}"),
animatedVisibilityScope = animatedContentScope
)
.fillMaxWidth(),
painter = painterResource(id = momentItem.momentPicture),
.aspectRatio(1f),
contentScale = ContentScale.Crop,
contentDescription = ""
)
}
}
@Composable
fun MomentOperateBtn(@DrawableRes icon: Int, count: String) {
Row(verticalAlignment = Alignment.CenterVertically) {

View File

@@ -36,8 +36,10 @@ import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -56,36 +58,66 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import coil.compose.AsyncImage
import com.aiosman.riderpro.LocalAnimatedContentScope
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.LocalSharedTransitionScope
import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.comment.Comment
import com.aiosman.riderpro.data.comment.CommentPagingSource
import com.aiosman.riderpro.data.comment.CommentRemoteDataSource
import com.aiosman.riderpro.data.comment.TestCommentServiceImpl
import com.aiosman.riderpro.data.moment.MomentService
import com.aiosman.riderpro.data.moment.TestMomentServiceImpl
import com.aiosman.riderpro.model.MomentItem
import com.aiosman.riderpro.ui.comment.CommentModalContent
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
import com.aiosman.riderpro.ui.composables.EditCommentBottomModal
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
fun makeMockImages(): List<PostImage> {
return listOf(
PostImage(R.drawable.default_moment_img, "Image 1"),
PostImage(R.drawable.default_avatar, "Image 2"),
PostImage(R.drawable.rider_pro_moment_demo_1, "Image 3")
)
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun PostScreen(
id: String,
) {
var service: MomentService = TestMomentServiceImpl()
var commentSource = CommentPagingSource(
CommentRemoteDataSource(TestCommentServiceImpl())
)
val commentsFlow: Flow<PagingData<Comment>> = Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
pagingSourceFactory = { commentSource }
).flow
val lazyPagingItems = commentsFlow.collectAsLazyPagingItems()
var showCollapseContent by remember { mutableStateOf(true) }
val scrollState = rememberLazyListState()
val uiController = rememberSystemUiController()
var moment by remember { mutableStateOf<MomentItem?>(null) }
LaunchedEffect(Unit) {
uiController.setNavigationBarColor(Color.White)
moment = service.getMomentById(id.toInt())
}
StatusBarMaskLayout {
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = { BottomNavigationBar() }
) {
it
Column(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
) {
Header()
Column(modifier = Modifier.animateContentSize()) {
AnimatedVisibility(visible = showCollapseContent) {
@@ -99,12 +131,13 @@ fun PostScreen(
) {
PostImageView(
id,
makeMockImages(),
moment?.images ?: emptyList()
)
}
PostDetails(
id,
moment
)
}
}
@@ -114,18 +147,18 @@ fun PostScreen(
.fillMaxWidth()
) {
CommentsSection(scrollState) {
CommentsSection(lazyPagingItems = lazyPagingItems, scrollState) {
showCollapseContent = it
}
}
}
}
}
}
@Composable
fun Header() {
val navController = LocalNavController.current
Row(
modifier = Modifier
.fillMaxWidth()
@@ -136,6 +169,9 @@ fun Header() {
painter = painterResource(id = R.drawable.rider_pro_nav_back), // Replace with your image resource
contentDescription = "Back",
modifier = Modifier
.noRippleClickable {
navController.popBackStack()
}
.size(32.dp)
)
@@ -171,257 +207,97 @@ fun Header() {
}
}
data class PostImage(
val imgRes: Int,
val description: String
)
@OptIn(ExperimentalFoundationApi::class, ExperimentalSharedTransitionApi::class)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PostImageView(
postId: String,
images: List<PostImage>,
images: List<String>,
) {
val sharedTransitionScope = LocalSharedTransitionScope.current
val animatedContentScope = LocalAnimatedContentScope.current
val pagerState = rememberPagerState(pageCount = { images.size })
with(sharedTransitionScope) {
Column {
HorizontalPager(
state = pagerState,
Column {
HorizontalPager(
state = pagerState,
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
) { page ->
val image = images[page]
AsyncImage(
image,
contentDescription = "Image",
contentScale = ContentScale.Crop,
modifier = Modifier
.weight(1f)
.fillMaxWidth()
) { page ->
Image(
painter = painterResource(id = images[page].imgRes),
contentDescription = images[page].description,
contentScale = ContentScale.Crop,
modifier = Modifier.Companion
.sharedElement(
sharedTransitionScope.rememberSharedContentState(key = "image-$postId"),
animatedVisibilityScope = animatedContentScope
)
.fillMaxSize()
)
}
.fillMaxSize()
)
}
// Indicator container
Row(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
images.forEachIndexed { index, _ ->
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
// Indicator container
Row(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
images.forEachIndexed { index, _ ->
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(
if (pagerState.currentPage == index) Color.Red else Color.Gray.copy(
alpha = 0.5f
)
.background(
if (pagerState.currentPage == index) Color.Red else Color.Gray.copy(
alpha = 0.5f
)
.padding(4.dp)
)
.padding(4.dp)
)
Spacer(modifier = Modifier.width(8.dp))
}
)
Spacer(modifier = Modifier.width(8.dp))
}
}
}
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun PostDetails(
postId: String,
momentItem: MomentItem?
) {
val sharedTransitionScope = LocalSharedTransitionScope.current
val animatedContentScope = LocalAnimatedContentScope.current
with(sharedTransitionScope) {
Column(modifier = Modifier.padding(16.dp)) {
momentItem?.let {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = "By strongarming Ducati into giving him the factory seat.Marquez effectively …", fontSize = 16.sp, fontWeight = FontWeight.Bold, modifier = Modifier.Companion.sharedElement(
sharedTransitionScope.rememberSharedContentState(key = "text-$postId"),
animatedVisibilityScope = animatedContentScope
))
Text(text = "12-11 发布")
Spacer(modifier = Modifier.height(8.dp))
Text(text = "共231条评论")
}
}
}
fun MakeMockComments(): List<Comment> {
return listOf(
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = listOf(
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
)
Text(
text = momentItem.momentTextContent,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
)
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
),
Comment(
name = "Diego Morata",
comment = "这里太适合骑行了",
date = "3 days ago",
likes = 200,
replies = emptyList()
)
)
Text(text = "12-11 发布")
Spacer(modifier = Modifier.height(8.dp))
Text(text = "共231条评论")
}
}
}
@Composable
fun CommentsSection(
lazyPagingItems: LazyPagingItems<Comment>,
scrollState: LazyListState = rememberLazyListState(),
onWillCollapse: (Boolean) -> Unit
) {
val items = MakeMockComments()
LazyColumn(
state = scrollState, modifier = Modifier
.padding(start = 16.dp, end = 16.dp)
.fillMaxHeight()
) {
items(items) { comment ->
CommentItem(comment)
items(lazyPagingItems.itemCount) { idx ->
val item = lazyPagingItems[idx] ?: return@items
CommentItem(item)
}
}
@@ -437,14 +313,6 @@ fun CommentsSection(
}
}
data class Comment(
val name: String,
val comment: String,
val date: String,
val likes: Int,
val replies: List<Comment>
)
@Composable
fun CommentItem(comment: Comment) {
@@ -485,6 +353,21 @@ fun CommentItem(comment: Comment) {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomNavigationBar() {
val systemUiController = rememberSystemUiController()
var showCommentModal by remember { mutableStateOf(false) }
if (showCommentModal) {
ModalBottomSheet(
onDismissRequest = { showCommentModal = false },
containerColor = Color.White,
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
),
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
) {
EditCommentBottomModal()
}
}
Column(
modifier = Modifier.background(Color.White)
) {
@@ -503,6 +386,9 @@ fun BottomNavigationBar() {
.weight(1f)
.height(31.dp)
.padding(8.dp)
.noRippleClickable {
showCommentModal = true
}
) {
Row(
verticalAlignment = Alignment.CenterVertically