pref: 优化动态列表性能
This commit is contained in:
parent
2ef051f7fa
commit
db88b51b14
|
|
@ -2,6 +2,8 @@ package com.huanchengfly.tieba.post.arch
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Stable
|
||||
class StableHolder<T>(val item: T) {
|
||||
|
|
@ -53,8 +55,8 @@ class ImmutableHolder<T>(val item: T) {
|
|||
return wrapImmutable(getter(item))
|
||||
}
|
||||
|
||||
fun <R> getImmutableList(getter: T.() -> List<R>): List<ImmutableHolder<R>> {
|
||||
return getter(item).map { wrapImmutable(it) }
|
||||
fun <R> getImmutableList(getter: T.() -> List<R>): ImmutableList<ImmutableHolder<R>> {
|
||||
return getter(item).map { wrapImmutable(it) }.toImmutableList()
|
||||
}
|
||||
|
||||
@Stable
|
||||
|
|
@ -68,4 +70,5 @@ fun <T> wrapStable(item: T): StableHolder<T> = StableHolder(item)
|
|||
|
||||
fun <T> wrapImmutable(item: T): ImmutableHolder<T> = ImmutableHolder(item)
|
||||
|
||||
fun <T> List<T>.wrapImmutable(): List<ImmutableHolder<T>> = map { wrapImmutable(it) }
|
||||
fun <T> List<T>.wrapImmutable(): ImmutableList<ImmutableHolder<T>> =
|
||||
map { wrapImmutable(it) }.toImmutableList()
|
||||
|
|
@ -57,6 +57,8 @@ import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
|||
import com.huanchengfly.tieba.post.arch.onEvent
|
||||
import com.huanchengfly.tieba.post.arch.pageViewModel
|
||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
||||
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
|
||||
import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination
|
||||
import com.huanchengfly.tieba.post.ui.page.main.MainUiEvent
|
||||
import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard
|
||||
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
|
||||
|
|
@ -76,6 +78,7 @@ fun PersonalizedPage(
|
|||
viewModel.initialized = true
|
||||
}
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.current
|
||||
val isRefreshing by viewModel.uiState.collectPartialAsState(
|
||||
prop1 = PersonalizedUiState::isRefreshing,
|
||||
initial = false
|
||||
|
|
@ -172,6 +175,9 @@ fun PersonalizedPage(
|
|||
)
|
||||
},
|
||||
onRefresh = { viewModel.send(PersonalizedUiIntent.Refresh) },
|
||||
onOpenForum = {
|
||||
navigator.navigate(ForumPageDestination(it))
|
||||
},
|
||||
state = lazyStaggeredGridState
|
||||
)
|
||||
LaunchedEffect(data.firstOrNull()?.get { id }) {
|
||||
|
|
@ -223,6 +229,7 @@ private fun FeedList(
|
|||
onAgree: (ThreadInfo) -> Unit,
|
||||
onDislike: (ThreadInfo, Long, List<ImmutableHolder<DislikeReason>>) -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
onOpenForum: (forumName: String) -> Unit = {},
|
||||
state: LazyStaggeredGridState,
|
||||
) {
|
||||
val data = dataProvider()
|
||||
|
|
@ -259,6 +266,9 @@ private fun FeedList(
|
|||
onAgree = {
|
||||
onAgree(item.get())
|
||||
},
|
||||
onClickForum = {
|
||||
onOpenForum(item.get { forumInfo?.name ?: "" })
|
||||
}
|
||||
) {
|
||||
Dislike(
|
||||
personalized = threadPersonalizedData[index],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.huanchengfly.tieba.post.ui.utils
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.huanchengfly.tieba.post.api.models.protos.Media
|
||||
import com.huanchengfly.tieba.post.api.models.protos.Post
|
||||
import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo
|
||||
import com.huanchengfly.tieba.post.arch.ImmutableHolder
|
||||
|
|
@ -54,19 +55,35 @@ fun getPhotoViewData(
|
|||
threadInfo: ThreadInfo,
|
||||
index: Int
|
||||
): PhotoViewData {
|
||||
val media = threadInfo.media[index]
|
||||
return getPhotoViewData(
|
||||
medias = threadInfo.media,
|
||||
forumId = threadInfo.forumId,
|
||||
forumName = threadInfo.forumName,
|
||||
threadId = threadInfo.threadId,
|
||||
index = index
|
||||
)
|
||||
}
|
||||
|
||||
fun getPhotoViewData(
|
||||
medias: List<Media>,
|
||||
forumId: Long,
|
||||
forumName: String,
|
||||
threadId: Long,
|
||||
index: Int
|
||||
): PhotoViewData {
|
||||
val media = medias[index]
|
||||
return PhotoViewData(
|
||||
data_ = LoadPicPageData(
|
||||
forumId = threadInfo.forumId,
|
||||
forumName = threadInfo.forumName,
|
||||
threadId = threadInfo.threadId,
|
||||
forumId = forumId,
|
||||
forumName = forumName,
|
||||
threadId = threadId,
|
||||
postId = media.postId,
|
||||
seeLz = false,
|
||||
objType = "index",
|
||||
picId = ImageUtil.getPicId(media.originPic),
|
||||
picIndex = index + 1
|
||||
),
|
||||
picItems = threadInfo.media.mapIndexed { mediaIndex, mediaItem ->
|
||||
picItems = medias.mapIndexed { mediaIndex, mediaItem ->
|
||||
PicItem(
|
||||
picId = ImageUtil.getPicId(mediaItem.originPic),
|
||||
picIndex = mediaIndex + 1,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import androidx.compose.material.icons.rounded.SwapCalls
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -62,8 +63,6 @@ import com.huanchengfly.tieba.post.api.models.protos.User
|
|||
import com.huanchengfly.tieba.post.arch.ImmutableHolder
|
||||
import com.huanchengfly.tieba.post.arch.wrapImmutable
|
||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
||||
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
|
||||
import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination
|
||||
import com.huanchengfly.tieba.post.ui.utils.getImmutablePhotoViewData
|
||||
import com.huanchengfly.tieba.post.ui.widgets.VideoPlayerStandard
|
||||
import com.huanchengfly.tieba.post.utils.DateTimeUtils
|
||||
|
|
@ -306,20 +305,187 @@ private fun ForumInfoChip(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThreadMedia(
|
||||
item: ImmutableHolder<ThreadInfo>
|
||||
) {
|
||||
val isVideo = remember(item) {
|
||||
item.isNotNull { videoInfo }
|
||||
}
|
||||
val medias = remember(item) {
|
||||
item.getImmutableList { media }
|
||||
}
|
||||
|
||||
if (isVideo) {
|
||||
val videoInfo = item.getImmutable { videoInfo!! }
|
||||
VideoPlayer(
|
||||
videoUrl = videoInfo.get { videoUrl },
|
||||
thumbnailUrl = videoInfo.get { thumbnailUrl },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(
|
||||
max(
|
||||
videoInfo
|
||||
.get { thumbnailWidth }
|
||||
.toFloat() / videoInfo.get { thumbnailHeight },
|
||||
16f / 9
|
||||
)
|
||||
)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
)
|
||||
} else if (medias.isNotEmpty()) {
|
||||
Box {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(if (medias.size == 1) 2f else 3f)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
medias.subList(0, min(medias.size, 3))
|
||||
.forEachIndexed { index, media ->
|
||||
val photoViewData = remember(item, index) {
|
||||
getImmutablePhotoViewData(item.get(), index)
|
||||
}
|
||||
NetworkImage(
|
||||
imageUri = media.url,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.weight(1f),
|
||||
photoViewData = photoViewData,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
}
|
||||
if (medias.size > 3) {
|
||||
Badge(
|
||||
icon = Icons.Rounded.PhotoSizeSelectActual,
|
||||
text = "${medias.size}",
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThreadForumInfo(
|
||||
item: ImmutableHolder<ThreadInfo>,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val hasForumInfo = remember(item) { item.isNotNull { forumInfo } }
|
||||
if (hasForumInfo) {
|
||||
val forumInfo = remember(item) { item.getImmutable { forumInfo!! } }
|
||||
if (forumInfo.get { name }.isNotBlank()) {
|
||||
ForumInfoChip(
|
||||
imageUriProvider = { StringUtil.getAvatarUrl(forumInfo.get { avatar }) },
|
||||
nameProvider = { forumInfo.get { name } },
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThreadCommentBtn(
|
||||
commentNum: Int,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
ActionBtn(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_comment_new),
|
||||
contentDescription = stringResource(id = R.string.desc_comment),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = if (commentNum == 0)
|
||||
stringResource(id = R.string.title_reply)
|
||||
else "$commentNum"
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThreadAgreeBtn(
|
||||
hasAgree: Boolean,
|
||||
agreeNum: Int,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val contentColor =
|
||||
if (hasAgree) ExtendedTheme.colors.accent else ExtendedTheme.colors.textSecondary
|
||||
val animatedColor by animateColorAsState(contentColor, label = "agreeBtnContentColor")
|
||||
|
||||
ActionBtn(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = if (hasAgree) Icons.Rounded.Favorite else Icons.Rounded.FavoriteBorder,
|
||||
contentDescription = stringResource(id = R.string.desc_like),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = if (agreeNum == 0)
|
||||
stringResource(id = R.string.title_agree)
|
||||
else "$agreeNum"
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
color = animatedColor,
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThreadShareBtn(
|
||||
shareNum: Long,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
ActionBtn(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.SwapCalls,
|
||||
contentDescription = stringResource(id = R.string.desc_share),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = if (shareNum == 0L)
|
||||
stringResource(id = R.string.title_share)
|
||||
else "$shareNum"
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FeedCard(
|
||||
item: ImmutableHolder<ThreadInfo>,
|
||||
onClick: () -> Unit,
|
||||
onAgree: () -> Unit,
|
||||
onClickForum: () -> Unit = {},
|
||||
dislikeAction: @Composable () -> Unit = {},
|
||||
) {
|
||||
Card(
|
||||
header = {
|
||||
if (item.isNotNull { author }) {
|
||||
val author = item.getImmutable { author!! }
|
||||
val hasAuthor = remember(item) { item.isNotNull { author } }
|
||||
if (hasAuthor) {
|
||||
val author = remember(item) { item.getImmutable { author!! } }
|
||||
val time = remember(item) { item.get { lastTimeInt } }
|
||||
DefaultUserHeader(
|
||||
user = author,
|
||||
time = item.get { lastTimeInt }) { dislikeAction() }
|
||||
time = time
|
||||
) { dislikeAction() }
|
||||
}
|
||||
},
|
||||
content = {
|
||||
|
|
@ -332,130 +498,29 @@ fun FeedCard(
|
|||
isGood = item.get { isGood == 1 }
|
||||
)
|
||||
|
||||
if (item.isNotNull { videoInfo }) {
|
||||
val videoInfo = item.getImmutable { videoInfo!! }
|
||||
VideoPlayer(
|
||||
videoUrl = videoInfo.get { videoUrl },
|
||||
thumbnailUrl = videoInfo.get { thumbnailUrl },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(
|
||||
max(
|
||||
videoInfo
|
||||
.get { thumbnailWidth }
|
||||
.toFloat() / videoInfo.get { thumbnailHeight },
|
||||
16f / 9
|
||||
)
|
||||
)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
)
|
||||
} else if (item.getImmutableList { media }.isNotEmpty()) {
|
||||
val media = item.getImmutableList { media }
|
||||
Box {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(if (media.size == 1) 2f else 3f)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
media.subList(0, min(media.size, 3))
|
||||
.forEachIndexed { index, media ->
|
||||
NetworkImage(
|
||||
imageUri = media.url,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.weight(1f),
|
||||
photoViewData = getImmutablePhotoViewData(item.get(), index),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
}
|
||||
if (media.size > 3) {
|
||||
Badge(
|
||||
icon = Icons.Rounded.PhotoSizeSelectActual,
|
||||
text = "${media.size}",
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
ThreadMedia(item = item)
|
||||
|
||||
if (item.isNotNull { forumInfo }) {
|
||||
val navigator = LocalNavigator.current
|
||||
val forumInfo = item.getImmutable { forumInfo!! }
|
||||
ForumInfoChip(
|
||||
imageUriProvider = { StringUtil.getAvatarUrl(forumInfo.get { avatar }) },
|
||||
nameProvider = { forumInfo.get { name } },
|
||||
onClick = {
|
||||
navigator.navigate(ForumPageDestination(forumInfo.get { name }))
|
||||
}
|
||||
)
|
||||
}
|
||||
ThreadForumInfo(item = item, onClick = onClickForum)
|
||||
},
|
||||
action = {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
ActionBtn(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_comment_new),
|
||||
contentDescription = stringResource(id = R.string.desc_comment)
|
||||
)
|
||||
},
|
||||
text = {
|
||||
val replyNum = item.get { replyNum }
|
||||
|
||||
Text(
|
||||
text = if (replyNum == 0)
|
||||
stringResource(id = R.string.title_reply)
|
||||
else "$replyNum"
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
color = ExtendedTheme.colors.textSecondary,
|
||||
onClick = {},
|
||||
ThreadCommentBtn(
|
||||
commentNum = item.get { commentNum },
|
||||
onClick = onClick,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
val hasAgree = item.get { agree?.hasAgree == 1 }
|
||||
ActionBtn(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = if (hasAgree) Icons.Rounded.Favorite else Icons.Rounded.FavoriteBorder,
|
||||
contentDescription = stringResource(id = R.string.desc_like)
|
||||
)
|
||||
},
|
||||
text = {
|
||||
val agreeNum = item.get { agreeNum }
|
||||
Text(
|
||||
text = if (agreeNum == 0)
|
||||
stringResource(id = R.string.title_agree)
|
||||
else "$agreeNum"
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
color = if (hasAgree) ExtendedTheme.colors.accent else ExtendedTheme.colors.textSecondary,
|
||||
onClick = onAgree
|
||||
ThreadAgreeBtn(
|
||||
hasAgree = item.get { agree?.hasAgree == 1 },
|
||||
agreeNum = item.get { agreeNum },
|
||||
onClick = onAgree,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
ActionBtn(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.SwapCalls,
|
||||
contentDescription = stringResource(id = R.string.desc_share)
|
||||
)
|
||||
},
|
||||
text = {
|
||||
val shareNum = item.get { shareNum }
|
||||
Text(
|
||||
text = if (shareNum == 0L)
|
||||
stringResource(id = R.string.title_share)
|
||||
else shareNum.toString()
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
color = ExtendedTheme.colors.textSecondary,
|
||||
ThreadShareBtn(
|
||||
shareNum = item.get { shareNum },
|
||||
onClick = {},
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
@ -486,35 +551,6 @@ private fun ActionBtnPlaceholder(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionBtn(
|
||||
icon: ImageVector,
|
||||
contentDescription: String?,
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = LocalContentColor.current,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val animatedColor by animateColorAsState(targetValue = color)
|
||||
val clickableModifier = if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier
|
||||
Row(
|
||||
modifier = clickableModifier
|
||||
.padding(vertical = 16.dp)
|
||||
.then(modifier),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = animatedColor,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = text, style = MaterialTheme.typography.caption, color = animatedColor)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionBtn(
|
||||
icon: @Composable () -> Unit,
|
||||
|
|
@ -523,7 +559,6 @@ private fun ActionBtn(
|
|||
color: Color = LocalContentColor.current,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val animatedColor by animateColorAsState(targetValue = color)
|
||||
val clickableModifier = if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier
|
||||
Row(
|
||||
modifier = clickableModifier
|
||||
|
|
@ -532,12 +567,12 @@ private fun ActionBtn(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Box(modifier = Modifier.size(18.dp)) {
|
||||
icon()
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
ProvideTextStyle(value = MaterialTheme.typography.caption) {
|
||||
ProvideContentColor(color = animatedColor) {
|
||||
ProvideContentColor(color = color) {
|
||||
Box(modifier = Modifier.size(18.dp)) {
|
||||
icon()
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
ProvideTextStyle(value = MaterialTheme.typography.caption) {
|
||||
text()
|
||||
}
|
||||
}
|
||||
|
|
@ -548,8 +583,8 @@ private fun ActionBtn(
|
|||
fun VideoPlayer(
|
||||
videoUrl: String,
|
||||
thumbnailUrl: String,
|
||||
title: String = "",
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
title: String = ""
|
||||
) {
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
|
|
|
|||
Loading…
Reference in New Issue