HapeLee 2 месяцев назад
Родитель
Сommit
b463c5ee83

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/audio/AudioPlayScreen.kt

@@ -148,7 +148,7 @@ fun AudioPlayScreenContent(
     AppScaffold(
         modifier = Modifier.fillMaxSize(),
         alwaysDrawBehindBars = true,
-        disableHazeSource = true,
+        disableContentSampling = true,
         contentWindowInsets = WindowInsets(0),
         topBar = {
             val hazeModifier = if (hazeEnabled) {

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/readaloud/player/ReadAloudPlayerScreen.kt

@@ -159,7 +159,7 @@ fun ReadAloudPlayerScreenContent(
     AppScaffold(
         modifier = Modifier.fillMaxSize(),
         alwaysDrawBehindBars = true,
-        disableHazeSource = true,
+        disableContentSampling = true,
         contentWindowInsets = WindowInsets(0),
         topBar = {
             val hazeModifier = if (hazeEnabled) {

+ 3 - 6
app/src/main/java/io/legado/app/ui/browser/WebViewRouteScreen.kt

@@ -106,8 +106,9 @@ fun WebViewRouteScreen(
     }
 
     AppScaffold(
-        // WebView 内容不能作为 haze 模糊采样源,否则会被反复重采样导致闪烁(与 RSS 页一致)
-        disableHazeSource = true,
+        // WebView 是 interop view:内容不能作为 haze / 液态玻璃的采样源,否则网页会被反复录进
+        // 离屏 GraphicsLayer 而闪烁(与 RSS 阅读页一致)
+        disableContentSampling = true,
         snackbarHost = { SnackbarHost(snackbarHostState) },
         topBar = {
             GlassSmallTopAppBar(
@@ -159,10 +160,6 @@ fun WebViewRouteScreen(
                     modifier = Modifier.fillMaxSize(),
                     onCreated = { createdWebView ->
                         createdWebView.apply {
-                            // Keep the verification page in an opaque WebView layer. A transparent WebView is
-                            // appropriate for RSS text, but Chromium repeatedly composites it during challenge
-                            // animations and causes the unstable flashing reported for manga sources.
-                            setBackgroundColor(android.graphics.Color.WHITE)
                             settings.apply {
                                 mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
                                 domStorageEnabled = true

+ 0 - 1
app/src/main/java/io/legado/app/ui/dict/DictSheet.kt

@@ -156,7 +156,6 @@ private fun DictWebSheet(
                 modifier = Modifier.fillMaxSize(),
                 onCreated = { webView ->
                     webView.apply {
-                        setBackgroundColor(android.graphics.Color.WHITE)
                         settings.apply {
                             mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
                             domStorageEnabled = true

+ 0 - 3
app/src/main/java/io/legado/app/ui/login/SourceLoginSheets.kt

@@ -326,9 +326,6 @@ private fun SourceLoginWebView(
                         displayZoomControls = false
                         state.headers[AppConst.UA_NAME]?.let { userAgentString = it }
                     }
-                    // 不透明 WebView:透明层在 Chromium 反复合成时会导致网页内 fixed 悬浮表单
-                    // (如登录弹层)显示异常,参考 WebViewRouteScreen 的处理
-                    setBackgroundColor(android.graphics.Color.WHITE)
                     webViewClient = object : WebViewClient() {
                         override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
                             url?.let { currentIntent(SourceLoginIntent.WebPageStarted(it)) }

+ 55 - 2
app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt

@@ -6,8 +6,10 @@ import android.net.Uri
 import androidx.activity.compose.BackHandler
 import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.AnimatedContentTransitionScope
 import androidx.compose.animation.ExperimentalSharedTransitionApi
 import androidx.compose.animation.SharedTransitionScope
+import androidx.compose.animation.core.FastOutSlowInEasing
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
@@ -157,6 +159,53 @@ import org.koin.androidx.compose.koinViewModel
 import org.koin.compose.koinInject
 import org.koin.core.parameter.parametersOf
 
+/**
+ * WebView 类页面(内置浏览器、订阅阅读)只做位移转场。
+ *
+ * WebView 是 AndroidView interop view:所在子树一旦被加上 graphicsLayer(fade 的 alpha、
+ * scaleOut 的缩放),Compose 会把网页一并画进离屏 RenderNode
+ * (`AndroidViewHolder.draw` → `AndroidComposeView.drawAndroidView`),Chromium 在这条绘制路径上
+ * 不稳定,部分设备会表现为网页闪烁。`slideIntoContainer` / `slideOutOfContainer` 只改 layout
+ * offset、不产生图层,所以这里保留默认的位移与时长,去掉 fade 与 scale。
+ */
+private fun webViewEntryMetadata(predictiveBackEnabled: Boolean) = metadata {
+    put(NavDisplay.TransitionKey) {
+        slideIntoContainer(
+            towards = AnimatedContentTransitionScope.SlideDirection.Start,
+            animationSpec = tween(durationMillis = 480, easing = FastOutSlowInEasing),
+            initialOffset = { fullWidth -> fullWidth }
+        ) togetherWith slideOutOfContainer(
+            towards = AnimatedContentTransitionScope.SlideDirection.Start,
+            animationSpec = tween(durationMillis = 480, easing = FastOutSlowInEasing),
+            targetOffset = { fullWidth -> fullWidth / 4 }
+        )
+    }
+    put(NavDisplay.PopTransitionKey) {
+        slideIntoContainer(
+            towards = AnimatedContentTransitionScope.SlideDirection.Start,
+            animationSpec = tween(durationMillis = 480, easing = FastOutSlowInEasing),
+            initialOffset = { fullWidth -> -fullWidth / 4 }
+        ) togetherWith slideOutOfContainer(
+            towards = AnimatedContentTransitionScope.SlideDirection.Start,
+            animationSpec = tween(durationMillis = 480, easing = FastOutSlowInEasing),
+            targetOffset = { fullWidth -> fullWidth }
+        )
+    }
+    if (predictiveBackEnabled) {
+        put(NavDisplay.PredictivePopTransitionKey) { _ ->
+            slideIntoContainer(
+                towards = AnimatedContentTransitionScope.SlideDirection.Start,
+                animationSpec = tween(easing = FastOutSlowInEasing),
+                initialOffset = { fullWidth -> -fullWidth / 4 }
+            ) togetherWith slideOutOfContainer(
+                towards = AnimatedContentTransitionScope.SlideDirection.Start,
+                animationSpec = tween(easing = FastOutSlowInEasing),
+                targetOffset = { fullWidth -> fullWidth }
+            )
+        }
+    }
+}
+
 @OptIn(ExperimentalSharedTransitionApi::class)
 fun MainActivity.mainEntryProvider(
     backStack: MutableList<NavKey>,
@@ -167,7 +216,9 @@ fun MainActivity.mainEntryProvider(
     onNavigateToRoute: (NavKey) -> Unit,
     onNavigateBack: () -> Unit,
 ) = entryProvider {
-    entry<MainRouteWebView> { route ->
+    entry<MainRouteWebView>(
+        metadata = webViewEntryMetadata(configuration.appShell.predictiveBackEnabled)
+    ) { route ->
         val viewModel = koinViewModel<WebViewModel>(
             key = "WebView:${route.url}:${route.sourceOrigin}:${route.sourceVerificationEnable}",
         )
@@ -994,7 +1045,9 @@ fun MainActivity.mainEntryProvider(
         )
     }
 
-    entry<MainRouteRssRead> { route ->
+    entry<MainRouteRssRead>(
+        metadata = webViewEntryMetadata(configuration.appShell.predictiveBackEnabled)
+    ) { route ->
         RssReadRouteScreen(
             title = route.title,
             origin = route.origin,

+ 2 - 2
app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt

@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.Login
+import androidx.compose.material.icons.automirrored.filled.VolumeUp
 import androidx.compose.material.icons.filled.Check
 import androidx.compose.material.icons.filled.CleaningServices
 import androidx.compose.material.icons.filled.OpenInBrowser
@@ -27,7 +28,6 @@ import androidx.compose.material.icons.filled.Refresh
 import androidx.compose.material.icons.filled.Share
 import androidx.compose.material.icons.filled.Star
 import androidx.compose.material.icons.filled.Stop
-import androidx.compose.material.icons.automirrored.filled.VolumeUp
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
 import androidx.compose.runtime.Composable
@@ -289,7 +289,7 @@ fun RssReadRouteScreen(
 
     Box(modifier = Modifier.fillMaxSize()) {
         AppScaffold(
-            disableHazeSource = true,
+            disableContentSampling = true,
             topBar = {
                 GlassSmallTopAppBar(
                     title = pageTitle.ifBlank { defaultTopBarTitle },

+ 18 - 5
app/src/main/java/io/legado/app/ui/widget/components/AppScaffold.kt

@@ -48,7 +48,16 @@ fun AppScaffold(
     contentColor: Color = contentColorFor(MiuixTheme.colorScheme.surface),
     contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
     alwaysDrawBehindBars: Boolean = false,
-    disableHazeSource: Boolean = false,
+    /**
+     * 内容不作为模糊 / 液态玻璃的采样源。
+     *
+     * haze 的 `hazeSource` 与液态玻璃的 `layerBackdrop` 都会把整棵内容录进 GraphicsLayer;
+     * 内容里若有 AndroidView(WebView 等 interop view),Compose 会把它一并画进那张离屏
+     * RenderNode(`AndroidViewHolder.draw` → `AndroidComposeView.drawAndroidView`)。
+     * Chromium 在「把网页画进别人的图层」这条路径上不稳定,部分设备会表现为网页闪烁,
+     * 因此 WebView 类页面必须把两个采样源一起关掉。
+     */
+    disableContentSampling: Boolean = false,
     content: @Composable (PaddingValues) -> Unit
 ) {
     val isDark = LegadoTheme.isDark
@@ -129,14 +138,16 @@ fun AppScaffold(
                             modifier = Modifier
                                 .fillMaxSize()
                                 .then(
-                                    if (liquidGlassEnabled) {
+                                    if (liquidGlassEnabled && !disableContentSampling) {
                                         Modifier.layerBackdrop(topBarContentBackdrop)
                                     } else {
                                         Modifier
                                     }
                                 )
                                 .then(
-                                    if (!disableHazeSource) Modifier.responsiveHazeSource(hazeState)
+                                    if (!disableContentSampling) Modifier.responsiveHazeSource(
+                                        hazeState
+                                    )
                                     else Modifier
                                 )
                                 .then(
@@ -192,14 +203,16 @@ fun AppScaffold(
                             modifier = Modifier
                                 .fillMaxSize()
                                 .then(
-                                    if (liquidGlassEnabled) {
+                                    if (liquidGlassEnabled && !disableContentSampling) {
                                         Modifier.layerBackdrop(topBarContentBackdrop)
                                     } else {
                                         Modifier
                                     }
                                 )
                                 .then(
-                                    if (!disableHazeSource) Modifier.responsiveHazeSource(hazeState)
+                                    if (!disableContentSampling) Modifier.responsiveHazeSource(
+                                        hazeState
+                                    )
                                     else Modifier
                                 )
                                 .then(