From 13685b33646c8998babb1310715a3830068e2219 Mon Sep 17 00:00:00 2001 From: HapeLee <63206378+HapeLee@users.noreply.github.com> Date: Wed, 1 Jul 2026 00:42:39 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=9A=82=E5=81=9C=E5=B1=8F=E5=B9=95?= =?UTF-8?q?=E5=A4=96=E4=B9=A6=E6=BA=90=20GIF=20=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/io/legado/app/ui/main/MainScreen.kt | 62 ++++++++++++++++- .../io/legado/app/ui/main/rss/RssScreen.kt | 60 +---------------- .../components/image/sourceIcon/SourceIcon.kt | 66 +++++++++++++++---- 3 files changed, 117 insertions(+), 71 deletions(-) diff --git a/app/src/main/java/io/legado/app/ui/main/MainScreen.kt b/app/src/main/java/io/legado/app/ui/main/MainScreen.kt index 5aff2dfaa..b6d4e2d4f 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainScreen.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainScreen.kt @@ -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, diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt index 9e4a62940..a06506104 100644 --- a/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt +++ b/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt @@ -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) ) diff --git a/app/src/main/java/io/legado/app/ui/widget/components/image/sourceIcon/SourceIcon.kt b/app/src/main/java/io/legado/app/ui/widget/components/image/sourceIcon/SourceIcon.kt index 790fe5e44..616ca14d0 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/image/sourceIcon/SourceIcon.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/image/sourceIcon/SourceIcon.kt @@ -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(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 + } ) } }