From ac5839eb9d769cd8356e248764807d13caadacc3 Mon Sep 17 00:00:00 2001 From: HuanCheng65 <22636177+HuanCheng65@users.noreply.github.com> Date: Sun, 5 Feb 2023 21:08:08 +0800 Subject: [PATCH] =?UTF-8?q?pref:=20=E6=80=A7=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tieba/post/arch/BaseViewModel.kt | 22 +- .../explore/personalized/PersonalizedPage.kt | 220 +++++++++++------- .../tieba/post/ui/widgets/compose/FeedCard.kt | 86 ++++--- 3 files changed, 209 insertions(+), 119 deletions(-) diff --git a/app/src/main/java/com/huanchengfly/tieba/post/arch/BaseViewModel.kt b/app/src/main/java/com/huanchengfly/tieba/post/arch/BaseViewModel.kt index 9baa0934..09a9d6ad 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/arch/BaseViewModel.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/arch/BaseViewModel.kt @@ -1,16 +1,25 @@ package com.huanchengfly.tieba.post.arch import android.util.Log +import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.scan +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch interface PartialChangeProducer, State : UiState> { fun toPartialChangeFlow(intentFlow: Flow): Flow } +@Stable abstract class BaseViewModel< Intent : UiIntent, PC : PartialChange, @@ -58,4 +67,15 @@ abstract class BaseViewModel< _intentFlow.emit(intent) } } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BaseViewModel<*, *, *, *> + + if (initialized != other.initialized) return false + + return true + } } \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt index 0f9166e4..c2d3067b 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.itemsIndexed @@ -33,6 +34,7 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -44,8 +46,12 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.github.panpf.sketch.request.PauseLoadWhenScrollingDrawableDecodeInterceptor import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.activities.ThreadActivity +import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo +import com.huanchengfly.tieba.post.api.models.protos.personalized.DislikeReason +import com.huanchengfly.tieba.post.api.models.protos.personalized.ThreadPersonalized import com.huanchengfly.tieba.post.arch.collectPartialAsState import com.huanchengfly.tieba.post.arch.pageViewModel import com.huanchengfly.tieba.post.arch.wrapImmutable @@ -123,96 +129,50 @@ fun PersonalizedPage( showRefreshTip = false } } - + if (lazyStaggeredGridState.isScrollInProgress) { + DisposableEffect(Unit) { + PauseLoadWhenScrollingDrawableDecodeInterceptor.scrolling = true + onDispose { + PauseLoadWhenScrollingDrawableDecodeInterceptor.scrolling = false + } + } + } Box(modifier = Modifier.pullRefresh(pullRefreshState)) { LoadMoreLayout( isLoading = isLoadingMore, loadEnd = false, onLoadMore = { viewModel.send(PersonalizedUiIntent.LoadMore(currentPage + 1)) }, ) { - LazyVerticalStaggeredGrid( - columns = StaggeredGridCells.Adaptive(240.dp), + FeedList( + dataProvider = { data }, + personalizedDataProvider = { threadPersonalizedData }, + refreshPositionProvider = { refreshPosition }, + hiddenThreadIdsProvider = { hiddenThreadIds }, + onItemClick = { threadInfo -> + ThreadActivity.launch(context, threadInfo.id.toString()) + }, + onAgree = { item -> + viewModel.send( + PersonalizedUiIntent.Agree( + item.threadId, + item.firstPostId, + item.agree?.hasAgree ?: 0 + ) + ) + }, + onDislike = { item, clickTime, reasons -> + viewModel.send( + PersonalizedUiIntent.Dislike( + item.forumInfo?.id ?: 0, + item.threadId, + reasons, + clickTime + ) + ) + }, + onRefresh = { viewModel.send(PersonalizedUiIntent.Refresh) }, state = lazyStaggeredGridState - ) { - itemsIndexed( - items = data, - key = { _, item -> "${item.id}" }, - contentType = { _, item -> - when { - item.videoInfo != null -> "Video" - item.media.size == 1 -> "SingleMedia" - item.media.size > 1 -> "MultiMedia" - else -> "PlainText" - } - } - ) { index, item -> - Column { - AnimatedVisibility( - visible = !hiddenThreadIds.contains(item.threadId), - enter = EnterTransition.None, - exit = shrinkVertically() + fadeOut() - ) { - FeedCard( - info = wrapImmutable(item), - onClick = { - ThreadActivity.launch(context, item.threadId.toString()) - }, - onAgree = { - viewModel.send( - PersonalizedUiIntent.Agree( - item.threadId, - item.firstPostId, - item.agree?.hasAgree ?: 0 - ) - ) - }, - ) { - Dislike( - personalized = threadPersonalizedData[index], - onDislike = { clickTime, reasons -> - viewModel.send( - PersonalizedUiIntent.Dislike( - item.forumInfo?.id ?: 0, - item.threadId, - reasons, - clickTime - ) - ) - } - ) - } - } - if (!hiddenThreadIds.contains(item.threadId)) { - if ((refreshPosition == 0 || index + 1 != refreshPosition) && index < data.size - 1) { - VerticalDivider( - modifier = Modifier.padding(horizontal = 16.dp), - thickness = 2.dp - ) - } - } - if (refreshPosition != 0 && index + 1 == refreshPosition) { - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable { viewModel.send(PersonalizedUiIntent.Refresh) } - .padding(8.dp), - ) { - Icon( - imageVector = Icons.Rounded.Refresh, - contentDescription = null - ) - Spacer(modifier = Modifier.width(16.dp)) - Text( - text = stringResource(id = R.string.tip_refresh), - style = MaterialTheme.typography.subtitle1 - ) - } - } - } - } - } + ) LaunchedEffect(data.firstOrNull()?.id) { //delay(50) lazyStaggeredGridState.scrollToItem(0, 0) @@ -242,7 +202,99 @@ fun PersonalizedPage( .padding(horizontal = 16.dp, vertical = 8.dp) .align(Alignment.TopCenter) ) { - Text(text = stringResource(id = R.string.toast_feed_refresh, refreshCount), color = ExtendedTheme.colors.onAccent) + Text( + text = stringResource(id = R.string.toast_feed_refresh, refreshCount), + color = ExtendedTheme.colors.onAccent + ) + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun FeedList( + dataProvider: () -> List, + personalizedDataProvider: () -> List, + refreshPositionProvider: () -> Int, + hiddenThreadIdsProvider: () -> List, + onItemClick: (ThreadInfo) -> Unit, + onAgree: (ThreadInfo) -> Unit, + onDislike: (ThreadInfo, Long, List) -> Unit, + onRefresh: () -> Unit, + state: LazyStaggeredGridState, +) { + val data = dataProvider() + val threadPersonalizedData = personalizedDataProvider() + val refreshPosition = refreshPositionProvider() + val hiddenThreadIds = hiddenThreadIdsProvider() + LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Adaptive(240.dp), + state = state + ) { + itemsIndexed( + items = data, + key = { _, item -> "${item.id}" }, + contentType = { _, item -> + when { + item.videoInfo != null -> "Video" + item.media.size == 1 -> "SingleMedia" + item.media.size > 1 -> "MultiMedia" + else -> "PlainText" + } + } + ) { index, item -> + Column { + AnimatedVisibility( + visible = !hiddenThreadIds.contains(item.threadId), + enter = EnterTransition.None, + exit = shrinkVertically() + fadeOut() + ) { + FeedCard( + info = wrapImmutable(item), + onClick = { + onItemClick(item) + }, + onAgree = { + onAgree(item) + }, + ) { + Dislike( + personalized = threadPersonalizedData[index], + onDislike = { clickTime, reasons -> + onDislike(item, clickTime, reasons) + } + ) + } + } + if (!hiddenThreadIds.contains(item.threadId)) { + if ((refreshPosition == 0 || index + 1 != refreshPosition) && index < data.size - 1) { + VerticalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + thickness = 2.dp + ) + } + } + if (refreshPosition != 0 && index + 1 == refreshPosition) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onRefresh) + .padding(8.dp), + ) { + Icon( + imageVector = Icons.Rounded.Refresh, + contentDescription = null + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = stringResource(id = R.string.tip_refresh), + style = MaterialTheme.typography.subtitle1 + ) + } + } } } } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt index 10150d79..9ce00063 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt @@ -57,6 +57,7 @@ import com.huanchengfly.tieba.post.activities.UserActivity import com.huanchengfly.tieba.post.api.abstractText import com.huanchengfly.tieba.post.api.hasAbstract import com.huanchengfly.tieba.post.api.models.protos.Media +import com.huanchengfly.tieba.post.api.models.protos.SimpleForum import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo import com.huanchengfly.tieba.post.api.models.protos.User import com.huanchengfly.tieba.post.arch.ImmutableHolder @@ -178,30 +179,35 @@ private fun Badge( } } -private val ThreadContent: @Composable ColumnScope.(ThreadInfo) -> Unit = { - val showTitle = it.isNoTitle != 1 && it.title.isNotBlank() - val showAbstract = it.hasAbstract +@Composable +fun ThreadContent( + info: ImmutableHolder, +) { + val (item) = info + + val showTitle = item.isNoTitle != 1 && item.title.isNotBlank() + val showAbstract = item.hasAbstract val content = buildAnnotatedString { if (showTitle) { withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - if (it.isGood == 1) { + if (item.isGood == 1) { withStyle(style = SpanStyle(color = ExtendedTheme.colors.primary)) { append(stringResource(id = R.string.tip_good)) } append(" ") } - if (it.tabName.isNotBlank()) { - append(it.tabName) + if (item.tabName.isNotBlank()) { + append(item.tabName) append(" | ") } - append(it.title) + append(item.title) } } if (showTitle && showAbstract) append('\n') if (showAbstract) { - append(it.abstractText.emoticonString) + append(item.abstractText.emoticonString) } } @@ -261,6 +267,37 @@ fun FeedCardPlaceholder() { ) } +@Composable +private fun ForumInfoChip( + info: ImmutableHolder, + onClick: () -> Unit +) { + val (forumInfo) = info + Row( + modifier = Modifier + .clip(RoundedCornerShape(4.dp)) + .background(color = ExtendedTheme.colors.chip) + .clickable(onClick = onClick) + .padding(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + NetworkImage( + imageUri = StringUtil.getAvatarUrl(forumInfo.avatar), + contentDescription = null, + modifier = Modifier + .size(12.dp) + .clip(RoundedCornerShape(4.dp)) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(id = R.string.title_forum_name, forumInfo.name), + style = MaterialTheme.typography.body2, + color = ExtendedTheme.colors.onChip, + fontSize = 12.sp, + ) + } +} + @Composable fun FeedCard( info: ImmutableHolder, @@ -276,7 +313,7 @@ fun FeedCard( } }, content = { - ThreadContent(item) + ThreadContent(info) if (item.videoInfo != null) { VideoPlayer( @@ -326,31 +363,12 @@ fun FeedCard( if (item.forumInfo != null) { val navigator = LocalNavigator.current - Row( - modifier = Modifier - .clip(RoundedCornerShape(4.dp)) - .background(color = ExtendedTheme.colors.chip) - .clickable { - navigator.navigate(ForumPageDestination(item.forumInfo.name)) - } - .padding(4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - NetworkImage( - imageUri = StringUtil.getAvatarUrl(item.forumInfo.avatar), - contentDescription = null, - modifier = Modifier - .size(12.dp) - .clip(RoundedCornerShape(4.dp)) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(id = R.string.title_forum_name, item.forumInfo.name), - style = MaterialTheme.typography.body2, - color = ExtendedTheme.colors.onChip, - fontSize = 12.sp, - ) - } + ForumInfoChip( + info = wrapImmutable(item.forumInfo), + onClick = { + navigator.navigate(ForumPageDestination(item.forumInfo.name)) + } + ) } }, action = {