fix: 暂停屏幕外书源 GIF 动画

This commit is contained in:
HapeLee
2026-07-01 01:02:56 +08:00
parent e9eee6b72c
commit 13685b3364
3 changed files with 117 additions and 71 deletions
@@ -43,12 +43,15 @@ import androidx.compose.material3.WideNavigationRailItem
import androidx.compose.material3.WideNavigationRailValue
import androidx.compose.material3.rememberWideNavigationRailState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -62,6 +65,11 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
import com.kyant.backdrop.backdrops.layerBackdrop
@@ -401,7 +409,11 @@ fun MainScreen(
beyondViewportPageCount = 4
) { page ->
val destination = destinations.getOrNull(page) ?: return@HorizontalPager
when (destination) {
val pageLifecycleOwner = rememberMainPageLifecycleOwner(
isActive = page == pagerState.currentPage
)
CompositionLocalProvider(LocalLifecycleOwner provides pageLifecycleOwner) {
when (destination) {
MainDestination.Home -> HomeRouteScreen(
onOpenBook = { book ->
context.startActivityForBook(book)
@@ -454,7 +466,6 @@ fun MainScreen(
onOpenExploreShow = onNavigateToExploreShow,
)
MainDestination.Rss -> RssScreen(
isActive = page == pagerState.currentPage,
onOpenSort = { sourceUrl, sortUrl, key ->
onNavigateToRssSort(sourceUrl, sortUrl, key)
},
@@ -476,6 +487,7 @@ fun MainScreen(
}
)
}
}
}
}
@@ -552,6 +564,52 @@ fun MainScreen(
}
}
@Composable
private fun rememberMainPageLifecycleOwner(isActive: Boolean): LifecycleOwner {
val parentLifecycle = LocalLifecycleOwner.current.lifecycle
val currentActive by rememberUpdatedState(isActive)
val owner = remember(parentLifecycle) { MainPageLifecycleOwner() }
DisposableEffect(parentLifecycle) {
val observer = LifecycleEventObserver { _, _ ->
owner.update(parentLifecycle.currentState, currentActive)
}
parentLifecycle.addObserver(observer)
owner.update(parentLifecycle.currentState, currentActive)
onDispose {
parentLifecycle.removeObserver(observer)
owner.destroy()
}
}
LaunchedEffect(isActive, parentLifecycle) {
owner.update(parentLifecycle.currentState, isActive)
}
return owner
}
private class MainPageLifecycleOwner : LifecycleOwner {
private val registry = LifecycleRegistry(this)
override val lifecycle: Lifecycle = registry
fun update(parentState: Lifecycle.State, isActive: Boolean) {
registry.currentState = when {
parentState == Lifecycle.State.DESTROYED -> Lifecycle.State.DESTROYED
parentState == Lifecycle.State.INITIALIZED -> Lifecycle.State.INITIALIZED
parentState == Lifecycle.State.CREATED -> Lifecycle.State.CREATED
isActive -> parentState
else -> Lifecycle.State.STARTED
}
}
fun destroy() {
registry.currentState = Lifecycle.State.DESTROYED
}
}
@Composable
private fun BookshelfRailGroupMenu(
expanded: Boolean,
@@ -29,8 +29,6 @@ import androidx.compose.material.icons.filled.Subscriptions
import androidx.compose.material.icons.filled.VerticalAlignTop
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -46,11 +44,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.legado.app.R
import io.legado.app.data.entities.RssSource
@@ -79,7 +72,6 @@ import org.koin.androidx.compose.koinViewModel
@Composable
fun RssScreen(
viewModel: RssViewModel = koinViewModel(),
isActive: Boolean = true,
onOpenSort: (sourceUrl: String, sortUrl: String?, key: String?) -> Unit,
onOpenRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit,
onOpenRuleSub: () -> Unit,
@@ -96,7 +88,6 @@ fun RssScreen(
val currentOnOpenRead by rememberUpdatedState(onOpenRead)
val currentOnOpenRuleSub by rememberUpdatedState(onOpenRuleSub)
val currentOnOpenFavorites by rememberUpdatedState(onOpenFavorites)
val pageLifecycleOwner = rememberRssPageLifecycleOwner(isActive)
LaunchedEffect(viewModel) {
viewModel.effects.collectLatest { effect ->
@@ -141,8 +132,7 @@ fun RssScreen(
}
}
CompositionLocalProvider(LocalLifecycleOwner provides pageLifecycleOwner) {
ListScaffold(
ListScaffold(
title = stringResource(R.string.rss),
state = uiState,
subtitle = uiState.group.ifEmpty { stringResource(R.string.all) },
@@ -253,7 +243,6 @@ fun RssScreen(
)
}
}
}
}
AppAlertDialog(
@@ -270,51 +259,6 @@ fun RssScreen(
)
}
@Composable
private fun rememberRssPageLifecycleOwner(isActive: Boolean): LifecycleOwner {
val parentLifecycle = LocalLifecycleOwner.current.lifecycle
val currentActive by rememberUpdatedState(isActive)
val owner = remember(parentLifecycle) { RssPageLifecycleOwner() }
DisposableEffect(parentLifecycle) {
val observer = LifecycleEventObserver { _, _ ->
owner.update(parentLifecycle.currentState, currentActive)
}
parentLifecycle.addObserver(observer)
owner.update(parentLifecycle.currentState, currentActive)
onDispose {
parentLifecycle.removeObserver(observer)
owner.destroy()
}
}
LaunchedEffect(isActive, parentLifecycle) {
owner.update(parentLifecycle.currentState, isActive)
}
return owner
}
private class RssPageLifecycleOwner : LifecycleOwner {
private val registry = LifecycleRegistry(this)
override val lifecycle: Lifecycle = registry
fun update(parentState: Lifecycle.State, isActive: Boolean) {
registry.currentState = when {
parentState == Lifecycle.State.DESTROYED -> Lifecycle.State.DESTROYED
!parentState.isAtLeast(Lifecycle.State.STARTED) -> Lifecycle.State.CREATED
isActive -> parentState
else -> Lifecycle.State.STARTED
}
}
fun destroy() {
registry.currentState = Lifecycle.State.DESTROYED
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RssSourceGridItem(
@@ -341,7 +285,7 @@ fun RssSourceGridItem(
) {
Box {
SourceIcon(
path = source.sourceIcon.ifEmpty { R.drawable.image_rss },
path = source.sourceIcon,
sourceOrigin = source.sourceUrl,
modifier = Modifier.size(48.dp)
)
@@ -1,5 +1,6 @@
package io.legado.app.ui.widget.components.image.sourceIcon
import android.graphics.drawable.Animatable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
@@ -8,7 +9,7 @@ import androidx.compose.material.icons.filled.RssFeed
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -19,6 +20,9 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import coil.ImageLoader
import coil.compose.AsyncImage
import coil.request.ImageRequest
@@ -48,10 +52,43 @@ fun SourceIcon(
}
) {
val context = LocalContext.current
val lifecycle = LocalLifecycleOwner.current.lifecycle
var imageLoaded by remember(path) { mutableStateOf(false) }
var animatedDrawable by remember(path) { mutableStateOf<Animatable?>(null) }
val imageRequest = remember(context, path, sourceOrigin, loadOnlyWifi) {
ImageRequest.Builder(context)
.data(path)
.crossfade(true)
.setParameter("sourceOrigin", sourceOrigin)
.setParameter("loadOnlyWifi", loadOnlyWifi)
.build()
}
LaunchedEffect(path) {
imageLoaded = false
DisposableEffect(lifecycle, animatedDrawable) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
if (animatedDrawable?.isRunning == false) {
animatedDrawable?.start()
}
}
Lifecycle.Event.ON_PAUSE -> animatedDrawable?.stop()
else -> Unit
}
}
lifecycle.addObserver(observer)
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
if (animatedDrawable?.isRunning == false) {
animatedDrawable?.start()
}
} else {
animatedDrawable?.stop()
}
onDispose {
lifecycle.removeObserver(observer)
animatedDrawable?.stop()
}
}
Box(
@@ -65,18 +102,25 @@ fun SourceIcon(
placeholderIcon()
}
AsyncImage(
model = ImageRequest.Builder(context)
.data(path)
.crossfade(true)
.setParameter("sourceOrigin", sourceOrigin)
.setParameter("loadOnlyWifi", loadOnlyWifi)
.build(),
model = imageRequest,
imageLoader = imageLoader,
contentDescription = null,
contentScale = contentScale, // 不裁切
modifier = Modifier.fillMaxSize(),
onSuccess = { imageLoaded = true },
onError = { imageLoaded = false }
onSuccess = { state ->
imageLoaded = true
animatedDrawable = (state.result.drawable as? Animatable)?.also { drawable ->
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
if (!drawable.isRunning) drawable.start()
} else {
drawable.stop()
}
}
},
onError = {
imageLoaded = false
animatedDrawable = null
}
)
}
}