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

This commit is contained in:
HapeLee committed 2026-07-01 01:02:56 +08:00
1 parent e9eee6b72c
commit 13685b3364
3 files changed
+117 -71

No files matched your search

@@ -43,12 +43,15 @@ import androidx.compose.material3.WideNavigationRailItem
import androidx.compose.material3.WideNavigationRailValue import androidx.compose.material3.WideNavigationRailValue
import androidx.compose.material3.rememberWideNavigationRailState import androidx.compose.material3.rememberWideNavigationRailState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri 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 androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.kyant.backdrop.backdrops.layerBackdrop import com.kyant.backdrop.backdrops.layerBackdrop
@@ -401,7 +409,11 @@ fun MainScreen(
beyondViewportPageCount = 4 beyondViewportPageCount = 4
) { page -> ) { page ->
val destination = destinations.getOrNull(page) ?: return@HorizontalPager 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( MainDestination.Home -> HomeRouteScreen(
onOpenBook = { book -> onOpenBook = { book ->
context.startActivityForBook(book) context.startActivityForBook(book)
@@ -454,7 +466,6 @@ fun MainScreen(
onOpenExploreShow = onNavigateToExploreShow, onOpenExploreShow = onNavigateToExploreShow,
) )
MainDestination.Rss -> RssScreen( MainDestination.Rss -> RssScreen(
isActive = page == pagerState.currentPage,
onOpenSort = { sourceUrl, sortUrl, key -> onOpenSort = { sourceUrl, sortUrl, key ->
onNavigateToRssSort(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 @Composable
private fun BookshelfRailGroupMenu( private fun BookshelfRailGroupMenu(
expanded: Boolean, expanded: Boolean,
@@ -29,8 +29,6 @@ import androidx.compose.material.icons.filled.Subscriptions
import androidx.compose.material.icons.filled.VerticalAlignTop import androidx.compose.material.icons.filled.VerticalAlignTop
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf 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.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp 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 androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
@@ -79,7 +72,6 @@ import org.koin.androidx.compose.koinViewModel
@Composable @Composable
fun RssScreen( fun RssScreen(
viewModel: RssViewModel = koinViewModel(), viewModel: RssViewModel = koinViewModel(),
isActive: Boolean = true,
onOpenSort: (sourceUrl: String, sortUrl: String?, key: String?) -> Unit, onOpenSort: (sourceUrl: String, sortUrl: String?, key: String?) -> Unit,
onOpenRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit, onOpenRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit,
onOpenRuleSub: () -> Unit, onOpenRuleSub: () -> Unit,
@@ -96,7 +88,6 @@ fun RssScreen(
val currentOnOpenRead by rememberUpdatedState(onOpenRead) val currentOnOpenRead by rememberUpdatedState(onOpenRead)
val currentOnOpenRuleSub by rememberUpdatedState(onOpenRuleSub) val currentOnOpenRuleSub by rememberUpdatedState(onOpenRuleSub)
val currentOnOpenFavorites by rememberUpdatedState(onOpenFavorites) val currentOnOpenFavorites by rememberUpdatedState(onOpenFavorites)
val pageLifecycleOwner = rememberRssPageLifecycleOwner(isActive)
LaunchedEffect(viewModel) { LaunchedEffect(viewModel) {
viewModel.effects.collectLatest { effect -> viewModel.effects.collectLatest { effect ->
@@ -141,8 +132,7 @@ fun RssScreen(
} }
} }
CompositionLocalProvider(LocalLifecycleOwner provides pageLifecycleOwner) { ListScaffold(
ListScaffold(
title = stringResource(R.string.rss), title = stringResource(R.string.rss),
state = uiState, state = uiState,
subtitle = uiState.group.ifEmpty { stringResource(R.string.all) }, subtitle = uiState.group.ifEmpty { stringResource(R.string.all) },
@@ -253,7 +243,6 @@ fun RssScreen(
) )
} }
} }
}
} }
AppAlertDialog( 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) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun RssSourceGridItem( fun RssSourceGridItem(
@@ -341,7 +285,7 @@ fun RssSourceGridItem(
) { ) {
Box { Box {
SourceIcon( SourceIcon(
path = source.sourceIcon.ifEmpty { R.drawable.image_rss }, path = source.sourceIcon,
sourceOrigin = source.sourceUrl, sourceOrigin = source.sourceUrl,
modifier = Modifier.size(48.dp) modifier = Modifier.size(48.dp)
) )
@@ -1,5 +1,6 @@
package io.legado.app.ui.widget.components.image.sourceIcon 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.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size 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.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.DisposableEffect
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
@@ -19,6 +20,9 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest import coil.request.ImageRequest
@@ -48,10 +52,43 @@ fun SourceIcon(
} }
) { ) {
val context = LocalContext.current val context = LocalContext.current
val lifecycle = LocalLifecycleOwner.current.lifecycle
var imageLoaded by remember(path) { mutableStateOf(false) } 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) { DisposableEffect(lifecycle, animatedDrawable) {
imageLoaded = false 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( Box(
@@ -65,18 +102,25 @@ fun SourceIcon(
placeholderIcon() placeholderIcon()
} }
AsyncImage( AsyncImage(
model = ImageRequest.Builder(context) model = imageRequest,
.data(path)
.crossfade(true)
.setParameter("sourceOrigin", sourceOrigin)
.setParameter("loadOnlyWifi", loadOnlyWifi)
.build(),
imageLoader = imageLoader, imageLoader = imageLoader,
contentDescription = null, contentDescription = null,
contentScale = contentScale, // 不裁切 contentScale = contentScale, // 不裁切
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
onSuccess = { imageLoaded = true }, onSuccess = { state ->
onError = { imageLoaded = false } 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
}
) )
} }
} }