アプリのダークモード(ダークテーマ)対応にて、WebViewの中身についてはCSSの prefers-color-scheme メディア特性を使って対応しておくだけでOKだろうと思っていました。しかし、簡単にはいかず、調べてもなかなか解決方法が見つからなかったので書きます。
結論だけ先に書くと androidx.webkit を使い、WebコンテンツのCSSを使うダークモードのON/OFFを設定します。これでAndroid 5(API Level 21)以上でダークモード対応を行うことができます。
※Android的にはダークテーマ・ライトテーマですね。書いた後に気づきました。m(__)m
https://developer.android.com/guide/topics/ui/look-and-feel/darktheme
Webコンテンツのダークモード対応はCSSで行う
Webコンテンツのダークモード対応はCSSの prefers-color-scheme というメディア特性を使って対応するのが正攻法のようですね。
@media (prefers-color-scheme: dark) {
// ダークモード用のCSS
}
@media (prefers-color-scheme: light) {
// ライトモード用のCSS
}
https://developer.mozilla.org/ja/docs/Web/CSS/@media/prefers-color-scheme
こちらによると、AndroidのWebViewは76以降で対応しているようなのでWebViewやChromeをアップデートしていれば大体のシステムで使えそうです。
しかし、CSSでダークモード対応したHTMLをWebViewに読み込ませて見ても、何もしないとシステム設定やアプリ設定に連動して変わってはくれません。
検証アプリ
まずは検証アプリを作りましょう。以下のようなテーマ切替がわかりやすいHTMLを作ります。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<style>
@media (prefers-color-scheme: dark) {
body { background:black; }
}
@media (prefers-color-scheme: light) {
body { background:white; }
}
</style>
</head>
<body>
<h1 style="color:white">dark theme</h1>
<h1 style="color:black">light theme</h1>
</body>
</html>
ライトモードとダークモードで以下のように表示が変わることを期待しています。
light | dark |
---|---|
アプリの方はダークモードの切替ができるように作っておきます。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<WebView
android:id="@+id/web_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/radio_group"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<RadioGroup
android:id="@+id/radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
>
<RadioButton
android:id="@+id/radio_system"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="System"
/>
<RadioButton
android:id="@+id/radio_light"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="Light"
/>
<RadioButton
android:id="@+id/radio_dark"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="Dark"
/>
</RadioGroup>
</androidx.constraintlayout.widget.ConstraintLayout>
アプリのダークモードは AppCompatDelegate#setDefaultNightMode で切り替えます。
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
when (AppCompatDelegate.getDefaultNightMode()) {
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM ->
binding.radioSystem.isChecked = true
AppCompatDelegate.MODE_NIGHT_NO ->
binding.radioLight.isChecked = true
AppCompatDelegate.MODE_NIGHT_YES ->
binding.radioDark.isChecked = true
else -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
binding.radioSystem.isChecked = true
}
}
binding.radioGroup.setOnCheckedChangeListener { _, checkedId ->
when (checkedId) {
R.id.radio_system ->
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
R.id.radio_light ->
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
R.id.radio_dark ->
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
}
binding.webView.loadUrl("file:///android_asset/dark.html")
}
}
これで切り替えてみても、ネイティブ部分はテーマが切り替わりますが、WebViewの中身は変わりません。
ただし、prefers-color-scheme が解釈されていない訳ではなく、どちらもlightテーマとして振る舞っているようです。
light | dark |
---|---|
アプリのテーマをWebViewに反映し、Webコンテンツのダーク設定を使わせる
WebViewのコンテンツのテーマ切替を行うには、アプリネイティブ部分のテーマ設定と、それをどのようにコンテンツに反映するかをWebViewに伝えてあげる必要があります。ただし、通常のWebViewやWebSettingsのインターフェースではかなり制約があるので、androidx.webkitを利用します。
Android 5以上であればWebView(Chrome)はアップデート可能なので、機能が追加されていきますが、WebViewのインターフェースはシステム組み込みなので古いSDKバージョンでは新機能が使えません。androidx.webkit を使うと、この部分を補ってくれるため、古いSDKバージョンでも最新のWebViewの機能が使えるようになります。ダークモード対応もAndroid5(API Level 21)以上で利用可能です。
implementation("androidx.webkit:webkit:1.3.0")
WebSettingsCompat
のsetForceDark
と setForceDarkStrategy
を利用します。
ForceDarkというと、ライトモードの色を元にいい感じに反転してくれるダークモード、のイメージがあります。そのため、このメソッドじゃないなと思ってしまいがちですが、**CSSの設定を使うダークモードもForceDarkを使って設定します。**ちょっとわかりにくいですね。
ダークモードのON/OFFをsetForceDark
で設定し、どのようにダークモードを反映するかを setForceDarkStrategy
で設定します。
WebSettingsCompat
で設定を行う前に WebViewFeature.isFeatureSupported
でその機能が使えるかを調べてから実行します。
使えない環境でコールするとExceptionが発生してしまうので注意しましょう。
WebViewやChromeのアップデートを行っていない環境では使えません。なので使えない環境についても考慮は必要です。
setForceDark
の設定値は以下のようになっています。FORCE_DARK_AUTO
を設定すれば良さそう(というかデフォルト値ですが)、どうもうまく動いてくれなかったので、直接ON/OFFを設定しています。
値 | 意味 |
---|---|
FORCE_DARK_AUTO | 親のViewに連動してダークモードのON/OFFを切り替えます |
FORCE_DARK_OFF | ダークモードを無効にします、ネイティブUIに関係なく非ダークモードでの表示になります |
FORCE_DARK_ON | ダークモードを有効にします、ネイティブUIに関係なくダークモードでの表示になります |
setForceDarkStrategy
の設定値は以下のようになっています。
値 | 意味 |
---|---|
DARK_STRATEGY_WEB_THEME_DARKENING_ONLY | Webコンテンツがダークモードに対応していれば、それを使用してダークモード表示を行います。Webコンテンツが対応していなければ何もしません |
DARK_STRATEGY_USER_AGENT_DARKENING_ONLY | Webコンテンツのダークモード設定を無視して強制ダークモード表示を行います |
DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING | Webコンテンツが対応していればそれを利用し、対応していなければ強制ダークモード表示を行います |
強制ダークモードもそれなりにいい感じのダークモード表示をしてくれますが、コンテンツによってはおかしな表示になってしまったりするので、通常はDARK_STRATEGY_WEB_THEME_DARKENING_ONLY
を指定することになると思います。なお、なにも設定しないで getForceDarkStrategy
で調べると DARK_STRATEGY_WEB_THEME_DARKENING_ONLY
になっていますが、setForceDark
でFORCE_DARK_ON
などを設定すると、DARK_STRATEGY_USER_AGENT_DARKENING_ONLY
に変わりますので、やはりどちらも設定が必要です。
現在のUIの設定は resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
で調べることができますので、それを元にWebViewの設定を行います。
val webSettings = binding.webView.settings
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
val nightMode = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
val forceDarkMode = if (nightMode) WebSettingsCompat.FORCE_DARK_ON else WebSettingsCompat.FORCE_DARK_OFF
WebSettingsCompat.setForceDark(webSettings, forceDarkMode)
}
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) {
WebSettingsCompat.setForceDarkStrategy(
webSettings, WebSettingsCompat.DARK_STRATEGY_WEB_THEME_DARKENING_ONLY
)
}
これで以下のように、WebViewにUIのテーマが伝わり、CSSのprefers-color-schemeに基づいてダークモードとライトモードが切り替わるようになります。
light | dark |
---|---|
以上です。