Android WebView暗夜模式适配

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

WebView 的行为可与 prefers-color-schemecolor-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结论。

相关推荐
studyForMokey8 小时前
【Android Activity】生命周期深入理解
android·kotlin
浅影歌年8 小时前
Android 嵌入h5顶部状态栏空白
android
来来走走11 小时前
kotlin学习 lambda编程
android·学习·kotlin
无知的前端11 小时前
一文精通-Kotlin中双冒号:: 语法使用
android·kotlin
Andy12 小时前
Mysql基础2
android·数据库·mysql
下位子13 小时前
『OpenGL学习滤镜相机』- Day1: OpenGL ES 入门与环境搭建
android·opengl
正经教主13 小时前
【问题】Android Studio专用C盘空间过大问题:迁移相关程序文件
android·android studio
下位子13 小时前
『OpenGL学习』 从零打造 Android 滤镜相机
android·opengl
●VON13 小时前
双非大学生自学鸿蒙5.0零基础入门到项目实战 - 歌曲列表
android·华为·harmonyos