feat: 看图加载更多/前一页
This commit is contained in:
parent
3928df1132
commit
ab7e442ea8
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
Loading…
Reference in New Issue