feat: 看图加载更多/前一页

This commit is contained in:
HuanCheng65 2023-07-13 23:53:22 +08:00
parent 3928df1132
commit ab7e442ea8
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
2 changed files with 194 additions and 20 deletions

View File

@ -26,6 +26,7 @@ import androidx.compose.material.icons.rounded.Download
import androidx.compose.material.icons.rounded.Share import androidx.compose.material.icons.rounded.Share
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -46,6 +47,7 @@ import com.github.panpf.sketch.zoom.SketchZoomImageView
import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.arch.BaseComposeActivityWithParcelable import com.huanchengfly.tieba.post.arch.BaseComposeActivityWithParcelable
import com.huanchengfly.tieba.post.arch.collectPartialAsState import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.models.protos.LoadPicPageData
import com.huanchengfly.tieba.post.models.protos.PhotoViewData import com.huanchengfly.tieba.post.models.protos.PhotoViewData
import com.huanchengfly.tieba.post.toastShort import com.huanchengfly.tieba.post.toastShort
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
@ -165,7 +167,11 @@ class PhotoViewActivity : BaseComposeActivityWithParcelable<PhotoViewData>() {
prop1 = PhotoViewUiState::hasNext, prop1 = PhotoViewUiState::hasNext,
initial = false initial = false
) )
val pageCount = items.size val loadPicPageData by viewModel.uiState.collectPartialAsState(
prop1 = PhotoViewUiState::loadPicPageData,
initial = LoadPicPageData()
)
val pageCount by remember { derivedStateOf { items.size } }
Surface(color = Color.Black) { Surface(color = Color.Black) {
if (items.isNotEmpty()) { if (items.isNotEmpty()) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@ -173,12 +179,34 @@ class PhotoViewActivity : BaseComposeActivityWithParcelable<PhotoViewData>() {
LaunchedEffect(initialIndex) { LaunchedEffect(initialIndex) {
if (pagerState.currentPage != initialIndex) pagerState.scrollToPage(initialIndex) if (pagerState.currentPage != initialIndex) pagerState.scrollToPage(initialIndex)
} }
LaunchedEffect(pagerState.currentPage, pageCount, loadPicPageData) {
loadPicPageData?.let {
val item = items[pagerState.currentPage]
if (pagerState.currentPage == 0 && hasPrev) {
viewModel.send(
PhotoViewUiIntent.LoadPrev(
item.picId,
item.overallIndex,
it
)
)
} else if (pagerState.currentPage == pageCount - 1 && hasNext) {
viewModel.send(
PhotoViewUiIntent.LoadMore(
item.picId,
item.overallIndex,
it
)
)
}
}
}
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
HorizontalPager( HorizontalPager(
pageCount = pageCount, pageCount = pageCount,
state = pagerState, state = pagerState,
key = { key = {
"${items[it].originUrl}_${items[it].overallIndex}" "${items[it].overallIndex}"
} }
) { ) {
val item = items[it] val item = items[it]
@ -186,9 +214,10 @@ class PhotoViewActivity : BaseComposeActivityWithParcelable<PhotoViewData>() {
imageUri = item.originUrl, imageUri = item.originUrl,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
onDrag = { dx, dy, isAtEdge -> onDrag = { dx, dy, isAtEdge ->
val currentPage = pagerState.currentPage
if (abs(dy) < 15 && abs(dx) > 25 && isAtEdge) { if (abs(dy) < 15 && abs(dx) > 25 && isAtEdge) {
val prevPage = it - 1 val prevPage = currentPage - 1
val nextPage = it + 1 val nextPage = currentPage + 1
if (dx > 0 && prevPage >= 0) { if (dx > 0 && prevPage >= 0) {
coroutineScope.launch { coroutineScope.launch {
pagerState.animateScrollToPage(prevPage) pagerState.animateScrollToPage(prevPage)
@ -225,7 +254,7 @@ class PhotoViewActivity : BaseComposeActivityWithParcelable<PhotoViewData>() {
) { ) {
val index = pagerState.currentPage val index = pagerState.currentPage
if (totalAmount > 1) { if (totalAmount > 1) {
val picIndex = items[index].overallIndex ?: (index + 1) val picIndex = items[index].overallIndex
Text( Text(
text = "$picIndex / $totalAmount", text = "$picIndex / $totalAmount",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)

View File

@ -8,6 +8,7 @@ import com.huanchengfly.tieba.post.arch.PartialChangeProducer
import com.huanchengfly.tieba.post.arch.UiEvent import com.huanchengfly.tieba.post.arch.UiEvent
import com.huanchengfly.tieba.post.arch.UiIntent import com.huanchengfly.tieba.post.arch.UiIntent
import com.huanchengfly.tieba.post.arch.UiState import com.huanchengfly.tieba.post.arch.UiState
import com.huanchengfly.tieba.post.models.protos.LoadPicPageData
import com.huanchengfly.tieba.post.models.protos.PhotoViewData import com.huanchengfly.tieba.post.models.protos.PhotoViewData
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -17,6 +18,7 @@ import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
class PhotoViewViewModel : class PhotoViewViewModel :
BaseViewModel<PhotoViewUiIntent, PhotoViewPartialChange, PhotoViewUiState, PhotoViewUiEvent>() { BaseViewModel<PhotoViewUiIntent, PhotoViewPartialChange, PhotoViewUiState, PhotoViewUiEvent>() {
@ -28,24 +30,95 @@ class PhotoViewViewModel :
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
override fun toPartialChangeFlow(intentFlow: Flow<PhotoViewUiIntent>): Flow<PhotoViewPartialChange> = override fun toPartialChangeFlow(intentFlow: Flow<PhotoViewUiIntent>): Flow<PhotoViewPartialChange> =
merge( merge(
intentFlow.filterIsInstance<PhotoViewUiIntent.Init>().flatMapConcat { it.producePartialChange() } intentFlow.filterIsInstance<PhotoViewUiIntent.Init>()
.flatMapConcat { it.producePartialChange() },
intentFlow.filterIsInstance<PhotoViewUiIntent.LoadPrev>()
.flatMapConcat { it.producePartialChange() },
intentFlow.filterIsInstance<PhotoViewUiIntent.LoadMore>()
.flatMapConcat { it.producePartialChange() },
) )
private fun PhotoViewUiIntent.LoadPrev.producePartialChange(): Flow<PhotoViewPartialChange.LoadPrev> =
TiebaApi.getInstance()
.picPageFlow(
forumId = data.forumId.toString(),
forumName = data.forumName,
threadId = data.threadId.toString(),
seeLz = data.seeLz,
picId = picId,
picIndex = overallIndex.toString(),
objType = data.objType,
prev = true
)
.map<PicPageBean, PhotoViewPartialChange.LoadPrev> { picPageBean ->
val items = picPageBean.picList.map {
PhotoViewItem(
picId = it.img.original.id,
originUrl = it.img.original.originalSrc,
url = if (it.showOriginalBtn) it.img.original.bigCdnSrc else null,
overallIndex = it.overAllIndex.toInt()
)
}
val hasPrev = items.first().overallIndex > 1
PhotoViewPartialChange.LoadPrev.Success(
hasPrev = hasPrev,
items = items
)
}
.onStart { emit(PhotoViewPartialChange.LoadPrev.Start) }
.catch {
emit(PhotoViewPartialChange.LoadPrev.Failure(it))
}
private fun PhotoViewUiIntent.LoadMore.producePartialChange(): Flow<PhotoViewPartialChange.LoadMore> =
TiebaApi.getInstance()
.picPageFlow(
forumId = data.forumId.toString(),
forumName = data.forumName,
threadId = data.threadId.toString(),
seeLz = data.seeLz,
picId = picId,
picIndex = overallIndex.toString(),
objType = data.objType,
prev = false
)
.map<PicPageBean, PhotoViewPartialChange.LoadMore> { picPageBean ->
val items = picPageBean.picList.map {
PhotoViewItem(
picId = it.img.original.id,
originUrl = it.img.original.originalSrc,
url = if (it.showOriginalBtn) it.img.original.bigCdnSrc else null,
overallIndex = it.overAllIndex.toInt()
)
}
val hasNext = items.last().overallIndex < picPageBean.picAmount.toInt()
PhotoViewPartialChange.LoadMore.Success(
hasNext = hasNext,
items = items
)
}
.onStart { emit(PhotoViewPartialChange.LoadMore.Start) }
.catch {
emit(PhotoViewPartialChange.LoadMore.Failure(it))
}
private fun PhotoViewUiIntent.Init.producePartialChange(): Flow<PhotoViewPartialChange.Init> { private fun PhotoViewUiIntent.Init.producePartialChange(): Flow<PhotoViewPartialChange.Init> {
val flow = if (data.data_ == null) { val flow = if (data.data_ == null) {
flowOf( flowOf(
PhotoViewPartialChange.Init.Success( PhotoViewPartialChange.Init.Success(
items = data.picItems.map { items = data.picItems.mapIndexed { index, item ->
PhotoViewItem( PhotoViewItem(
originUrl = it.originUrl, picId = item.picId,
url = if (it.showOriginBtn) it.url else null, originUrl = item.originUrl,
overallIndex = null url = if (item.showOriginBtn) item.url else null,
overallIndex = index + 1
) )
}, },
hasNext = false, hasNext = false,
hasPrev = false, hasPrev = false,
totalAmount = data.picItems.size, totalAmount = data.picItems.size,
initialIndex = data.index initialIndex = data.index,
loadPicPageData = null
) )
) )
} else { } else {
@ -64,32 +137,35 @@ class PhotoViewViewModel :
val picAmount = picPageBean.picAmount.toInt() val picAmount = picPageBean.picAmount.toInt()
val fetchedItems = picPageBean.picList.map { val fetchedItems = picPageBean.picList.map {
PhotoViewItem( PhotoViewItem(
picId = it.img.original.id,
originUrl = it.img.original.originalSrc, originUrl = it.img.original.originalSrc,
url = if (it.showOriginalBtn) it.img.original.bigCdnSrc else null, url = if (it.showOriginalBtn) it.img.original.bigCdnSrc else null,
overallIndex = it.overAllIndex.toInt() overallIndex = it.overAllIndex.toInt()
) )
} }
val firstItemIndex = fetchedItems.first().overallIndex!! val firstItemIndex = fetchedItems.first().overallIndex
val localItems = val localItems =
if (data.data_.picIndex == 1) emptyList() else data.picItems.subList( if (data.data_.picIndex == 1) emptyList() else data.picItems.subList(
0, 0,
data.data_.picIndex - 1 data.data_.picIndex - 1
).mapIndexed { index, item -> ).mapIndexed { index, item ->
PhotoViewItem( PhotoViewItem(
picId = item.picId,
originUrl = item.originUrl, originUrl = item.originUrl,
url = if (item.showOriginBtn) item.url else null, url = if (item.showOriginBtn) item.url else null,
overallIndex = firstItemIndex - (data.data_.picIndex - 1 - index) overallIndex = firstItemIndex - (data.data_.picIndex - 1 - index)
) )
} }
val items = localItems + fetchedItems val items = localItems + fetchedItems
val hasNext = items.last().overallIndex!! < picAmount val hasNext = items.last().overallIndex < picAmount
val hasPrev = items.first().overallIndex!! > 1 val hasPrev = items.first().overallIndex > 1
PhotoViewPartialChange.Init.Success( PhotoViewPartialChange.Init.Success(
hasPrev = hasPrev, hasPrev = hasPrev,
hasNext = hasNext, hasNext = hasNext,
totalAmount = picAmount, totalAmount = picAmount,
items = items, items = items,
initialIndex = data.data_.picIndex - 1 initialIndex = data.data_.picIndex - 1,
loadPicPageData = data.data_
) )
} }
.catch { .catch {
@ -103,6 +179,18 @@ class PhotoViewViewModel :
sealed interface PhotoViewUiIntent : UiIntent { sealed interface PhotoViewUiIntent : UiIntent {
data class Init(val data: PhotoViewData) : PhotoViewUiIntent data class Init(val data: PhotoViewData) : PhotoViewUiIntent
data class LoadMore(
val picId: String,
val overallIndex: Int,
val data: LoadPicPageData
) : PhotoViewUiIntent
data class LoadPrev(
val picId: String,
val overallIndex: Int,
val data: LoadPicPageData
) : PhotoViewUiIntent
} }
sealed interface PhotoViewPartialChange : PartialChange<PhotoViewUiState> { sealed interface PhotoViewPartialChange : PartialChange<PhotoViewUiState> {
@ -115,16 +203,18 @@ sealed interface PhotoViewPartialChange : PartialChange<PhotoViewUiState> {
hasPrev = hasPrev, hasPrev = hasPrev,
totalAmount = totalAmount, totalAmount = totalAmount,
initialIndex = initialIndex, initialIndex = initialIndex,
loadPicPageData = loadPicPageData,
isLoading = false isLoading = false
) )
is Failure -> { is Failure -> {
oldState.copy( oldState.copy(
data = data.picItems.map { data = data.picItems.mapIndexed { index, item ->
PhotoViewItem( PhotoViewItem(
originUrl = it.originUrl, picId = item.picId,
url = if (it.showOriginBtn) it.url else null, originUrl = item.originUrl,
overallIndex = null url = if (item.showOriginBtn) item.url else null,
overallIndex = index + 1
) )
}, },
hasNext = false, hasNext = false,
@ -142,6 +232,7 @@ sealed interface PhotoViewPartialChange : PartialChange<PhotoViewUiState> {
val hasPrev: Boolean, val hasPrev: Boolean,
val totalAmount: Int, val totalAmount: Int,
val initialIndex: Int, val initialIndex: Int,
val loadPicPageData: LoadPicPageData?,
) : Init() ) : Init()
data class Failure( data class Failure(
@ -149,6 +240,58 @@ sealed interface PhotoViewPartialChange : PartialChange<PhotoViewUiState> {
val error: Throwable val error: Throwable
) : Init() ) : Init()
} }
sealed class LoadPrev : PhotoViewPartialChange {
override fun reduce(oldState: PhotoViewUiState): PhotoViewUiState =
when (this) {
Start -> oldState.copy(isLoading = true)
is Success -> oldState.copy(
data = items.filterNot { item -> oldState.data.any { item.picId == it.picId } } + oldState.data,
hasPrev = hasPrev,
isLoading = false
)
is Failure -> oldState.copy(isLoading = false)
}
object Start : LoadPrev()
data class Success(
val items: List<PhotoViewItem>,
val hasPrev: Boolean,
) : LoadPrev()
data class Failure(
val error: Throwable
) : LoadPrev()
}
sealed class LoadMore : PhotoViewPartialChange {
override fun reduce(oldState: PhotoViewUiState): PhotoViewUiState =
when (this) {
Start -> oldState.copy(isLoading = true)
is Success -> oldState.copy(
data = oldState.data + items.filterNot { item -> oldState.data.any { item.picId == it.picId } },
hasNext = hasNext,
isLoading = false
)
is Failure -> oldState.copy(isLoading = false)
}
object Start : LoadMore()
data class Success(
val items: List<PhotoViewItem>,
val hasNext: Boolean,
) : LoadMore()
data class Failure(
val error: Throwable
) : LoadMore()
}
} }
data class PhotoViewUiState( data class PhotoViewUiState(
@ -158,12 +301,14 @@ data class PhotoViewUiState(
val hasNext: Boolean = false, val hasNext: Boolean = false,
val hasPrev: Boolean = false, val hasPrev: Boolean = false,
val initialIndex: Int = 0, val initialIndex: Int = 0,
val loadPicPageData: LoadPicPageData? = null,
) : UiState ) : UiState
sealed interface PhotoViewUiEvent : UiEvent sealed interface PhotoViewUiEvent : UiEvent
data class PhotoViewItem( data class PhotoViewItem(
val picId: String,
val originUrl: String, val originUrl: String,
val url: String?, val url: String?,
val overallIndex: Int? val overallIndex: Int
) )