Android WebView暗夜模式适配

Android WebView暗夜模式适配是一项出海应用的基本开发需求,这篇文章主要介绍如何开发适配。

WebView 的行为可与 prefers-color-scheme 和 color-scheme 网络标准互操作。根据Android官方的资料介绍,WebView的暗夜适配大致可分为使用 prefers-color-scheme 的 Web 内容和不使用 prefers-color-scheme 的网页内容,下面分别讲解如何适配。

一、使用 prefers-color-scheme 的 Web 内容

暗夜模式的开发初期很容易进入一个误区,平时获取APP是否为暗夜模式,一般会通过下面的函数:

kotlin 复制代码
 /** 检测系统是否为暗夜模式 */
    private fun isSystemInDarkMode(context: Context): Boolean {
        val uiMode = context.resources.configuration.uiMode
        return (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
    }

但使用 prefers-color-scheme 的 Web 内容与主题有关,与APP是否为暗夜模式无关。

WebView 的暗夜模式是依赖系统或应用主题(Theme)状态变化触发的,而不是直接根据 context.resources.configuration.uiMode 判定的。 因为 WebView 是一个「独立进程 + 独立渲染引擎」组件,它的暗色算法是在 WebView 内核侧控制的,而不是依赖宿主 Context 的配置状态。

OK,修改主题,尝试下面的方式修改为暗色主题:

kotlin 复制代码
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)

结果WebView内的内容依然没有变为暗夜模式,原因是:

AppCompatDelegate.setDefaultNightMode() 只影响 AppCompat 的 UI(即 View 层、主题资源加载), 而 WebView 的暗夜模式属于 Web 内核(Chromium)侧的行为, 它不会自动跟随 AppCompat 的模式切换,除非你在 Activity 重新创建后手动通知 WebView。

这条主题设置只能修改AppCompat资源的切换而不能影响WebView内部。真正影响WebView获取isLightTheme主题的是Activity的Theme,下面是一个可以参考的主题:

kotlin 复制代码
//给Activity设置日间主题
Theme.AppCompat.Light.NoActionBar

//给Activity设置夜间主题
Theme.AppCompat.DayNight.NoActionBar

我们可以通过下面的示例来测试:

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <meta name="color-scheme" content="light dark">
    <title>Dark Mode Test</title>
    <style>
        :root {
          --bg: #ffffff;
          --text: #111111;
        }
        :root.dark {
          --bg: #121212;
          --text: #eeeeee;
        }
        body {
          margin: 0;
          padding: 20px;
          font-family: sans-serif;
          background-color: var(--bg);
          color: var(--text);
          transition: all 0.3s;
        }
        .status {
          margin-top: 20px;
          padding: 10px;
          border-radius: 8px;
          background: rgba(128,128,128,0.2);
        }
    </style>
</head>
<body>
<h2>🌗 WebView 暗夜模式测试</h2>
<div class="status" id="status">加载中...</div>

<script>
    function applyTheme(isDark) {
      document.documentElement.classList.toggle('dark', !!isDark);
      document.getElementById('status').innerText =
        '当前主题: ' + (isDark ? '🌙 暗夜模式' : '☀️ 日间模式');
    }

    window.onAndroidThemeChanged = function(isDark) {
      applyTheme(isDark);
      console.log('onAndroidThemeChanged', isDark);
    };

    window.addEventListener('androidThemeChanged', function(e){
      applyTheme(e.detail.isDark);
    });

    document.addEventListener('DOMContentLoaded', function() {
      if (typeof window.__ANDROID_DARK_MODE__ !== 'undefined') {
        applyTheme(window.__ANDROID_DARK_MODE__);
      } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
        applyTheme(true);
      } else {
        applyTheme(false);
      }
    });
</script>
</body>
</html>

在assets文件夹创建测试文件test.html,放入上面的内容,用webView加载就能测试效果:

kotlin 复制代码
val testUrl = "file:///android_asset/test.html"
//用WebView加载这个Url
loadUrl(testUrl)

二、不使用 prefers-color-scheme 的网页内容

假如用WebView加载掘金的官网:juejin.cn

会发现暗夜模式并没有起作用,这就需要使用其他实现。

需要导入WebKit:

kotlin 复制代码
implementation "androidx.webkit:webkit:1.14.0"

1、允许算法调暗

对于以 Android 13(API 级别 33)或更高版本为目标平台的应用,请使用 AndroidX setAlgorithmicDarkeningAllowed() 方法并传入 true,以指定 WebView 应允许通过算法加深。此方法向后兼容之前的 Android 版本。

kotlin 复制代码
if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
    WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, true)
}

2、允许算法加深

对于未使用应用级强制深色模式且以 Android 12(API 级别 32)或更低版本为目标平台的应用,请使用 FORCE_DARK_ON 来允许算法调暗。

如果您的应用提供了在浅色和深色主题之间切换的自己的方法(例如界面中的可切换元素或基于时间的自动选择),请将 FORCE_DARK_ON 与 FORCE_DARK_OFF 搭配使用。

kotlin 复制代码
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
    //向下兼容旧版本
    WebSettingsCompat.setForceDark(
        settings,
        if (isDarkMode) WebSettingsCompat.FORCE_DARK_ON
        else WebSettingsCompat.FORCE_DARK_OFF
    )
} 

3、注入暗夜CSS

对于更老的版本则可以注入暗夜css来实现暗夜模式,后面会给出未经完全测试的代码(没足够场景测试)。

三、代码封装

封装一个WebView的暗夜模式辅助类:

kotlin 复制代码
import android.content.Context
import android.content.res.Configuration
import android.webkit.WebSettings
import android.webkit.WebView
import androidx.appcompat.app.AppCompatDelegate
import androidx.webkit.WebSettingsCompat
import androidx.webkit.WebViewFeature

/**
 * WebView 暗夜模式辅助类
 * - 支持强制日间 / 夜间 / 跟随系统
 * - 自动同步 WebView prefers-color-scheme
 * - Android 13+ 自动生效,无需 JS 注入
 * - 旧版 WebView 自动注入暗夜 CSS
 */
object WebViewDarkModeHelper {

    enum class Mode {
        /** 跟随系统 */
        FOLLOW_SYSTEM,

        /** 强制日间 */
        LIGHT,

        /** 强制夜间 */
        DARK
    }

    /**
     * 应用全局主题(AppCompat + WebView)
     */
    fun apply(context: Context, webView: WebView, mode: Mode = Mode.FOLLOW_SYSTEM) {
        val settings = webView.settings
        settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
        settings.javaScriptEnabled = true

        // 设置全局 AppCompatDelegate 模式
        when (mode) {
            Mode.LIGHT -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
            Mode.DARK -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
            Mode.FOLLOW_SYSTEM -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
        }

        // 当前是否暗色模式
        val isDarkMode = when (mode) {
            Mode.DARK -> true
            Mode.LIGHT -> false
            Mode.FOLLOW_SYSTEM -> {
                val uiMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
                uiMode == Configuration.UI_MODE_NIGHT_YES
            }
        }

        // 优先使用新特性 AlgorithmicDarkening(Android 13+)
        if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
            WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, true)
        } else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
            //向下兼容旧版本
            WebSettingsCompat.setForceDark(
                settings,
                if (isDarkMode) WebSettingsCompat.FORCE_DARK_ON
                else WebSettingsCompat.FORCE_DARK_OFF
            )
        } else {
            //最老的 WebView,不支持暗化:注入 CSS
            injectDarkModeCSS(webView, isDarkMode)
        }
    }

    /**
     * 兼容旧 WebView 注入暗夜 CSS
     */
    private fun injectDarkModeCSS(webView: WebView, enable: Boolean) {
        if (!enable) {
            val removeJs = """
                var existingStyle = document.getElementById('dark-mode-style');
                if (existingStyle) existingStyle.remove();
            """.trimIndent()
            webView.evaluateJavascript(removeJs, null)
            return
        }

        val css = """
            html, body {
                background-color: #121212 !important;
                color: #E0E0E0 !important;
            }
            a { color: #BB86FC !important; }
            img, video { filter: brightness(0.8) contrast(1.1); }
        """.trimIndent()

        val js = """
            var style = document.createElement('style');
            style.id = 'dark-mode-style';
            style.type = 'text/css';
            style.appendChild(document.createTextNode(`$css`));
            document.head.appendChild(style);
        """.trimIndent()

        webView.evaluateJavascript(js, null)
    }

    /**
     * 当系统主题变化时调用(例如配置变化).
     */
    fun onSystemThemeChanged(context: Context, webView: WebView) {
        apply(context, webView, Mode.FOLLOW_SYSTEM)
    }
}

使用:

kotlin 复制代码
//跟随系统
WebViewDarkModeHelper.apply(this, webView, WebViewDarkModeHelper.Mode.FOLLOW_SYSTEM)

//强制暗夜
WebViewDarkModeHelper.apply(this, webView, WebViewDarkModeHelper.Mode.DARK)

//强制日间
WebViewDarkModeHelper.apply(this, webView, WebViewDarkModeHelper.Mode.LIGHT)

//监听系统变化
override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    WebViewDarkModeHelper.onSystemThemeChanged(this, webView)
}

Android官方文档-在 WebView 中调暗 Web 内容

部分内容借助AI结论。

相关推荐
千里马学框架5 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台5 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone5 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc5 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo5 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077005 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼5 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone5 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen5 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone5 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui