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结论。