Mu-Tsun Tsai
Mu-Tsun Tsai

Reputation: 2534

Letting WebView on Android work with prefers-color-scheme: dark

I have an Android App that uses webview, and lately I'm trying to figure out how to add a dark theme by using the new @media (prefers-color-scheme: dark) CSS syntax. I have the correct CSS written on my page, and if I open it in Chrome with the dark mode of Chrome turning on, it works. I also have my AppTheme inheriting Theme.AppCompat.DayNight, and my app shows dark loading dialog etc. when I turn on dark mode for the entire OS on my device. Even the auto-complete options for the <input> elements become dark. But still, the webpage loaded with my webview doesn't turn dark. According to this page, webviews should support this feature, but I just can't get it to work. What am I missing here?

I also just found out that in API 29 there's this WebSettings.setForceDark() method; could it be the thing I'm looking for? I hope to find a solution that works with lower API level though.

By the way, my current workaround is to inject a JavaScript interface like this:

webView.addJavascriptInterface(new JSInterface(), "jsInterface");

...

public class JSInterface {
    @JavascriptInterface
    public boolean isNightMode() {
        int nightModeFlags = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
        return nightModeFlags == Configuration.UI_MODE_NIGHT_YES;
    }
}

And then in my webpage, call the jsInterface.isNightMode() method and dynamically load different CSS file based on the result. It certainly works and responses to the global dark mode setting as desired, but I still wonder if I can make prefers-color-scheme work.

Upvotes: 43

Views: 36276

Answers (6)

Leon Lucardie
Leon Lucardie

Reputation: 9730

UPDATE 2022: AndroidX Webkit 1.5.0 fixed a lot of the quirkyness of how webview handles dark mode.

For this to work you need to:

  • Add the latest version of webkit to your project (at least 1.5.0):
    • implementation 'androidx.webkit:webkit:1.5.0'
  • Set the target version of your application to 33 or up
    • targetSdkVersion 33
  • Make sure Android System Webview v100 or up is installed on the device

With this setup the webview will properly adjust based on the app theme without the need to adjust any further settings. The prefers-color-scheme CSS query value will automatically adjust to light or dark based on the type of theme (light/dark) the current activity/fragment is running. (based on the isLightTheme attribute of the theme or its parent themes)

The pre-API 33 methods to adjust dark mode (WebViewSettingsCompat.setForceDark and WebViewSettingsCompat.setForceDarkStrategy etc.) are no-ops if your app targetSdkVersion is set to 33 or up, and can be removed from your code.

NOTE: This new method requires that your app targets API 33 or up, but is backwards compatible back to at least API 29 (the first Android version to support dark mode)

NOTE 2: User-agent darkening (automatic dark mode for web pages that do not support it) is now disabled by default in most cases, but can be enabled using the following method:

if(WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
    WebSettingsCompat.setAlgorithmicDarkeningAllowed(myWebView.getSettings(), true);
}

Full description of this method's behaviour can be found here




LEGACY METHOD (Apps targeting API 32 or below)

Android Webview on apps targeting API 32 and below handles day/night mode a bit differently from the rest of the views. Setting your theme to dark will change the WebView components (scrollbar, zoom buttons etc.) to a dark mode version, but will not change the content it loaded.

To change the content you need to use the setForceDark method of the webview settings to make it change its contents as well. A compatibility version of this method can be found in the AndroidX webkit package.

Add the following dependency to your gradle build:

implementation 'androidx.webkit:webkit:1.3.0'

(1.3.0 is the minimum required version of this package. But higher versions should work as well.)

And add the following lines of code to your webview intitialization:

if(WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
    WebSettingsCompat.setForceDark(myWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
}

The isFeatureSupported check is there to make sure the Android System WebView version the user has installed on their device supports dark mode (since this can be updated or downgraded independently from the Android version through Google Play).

Note: The setForceDark feature requires Android System WebView v76 or up to be installed on the running device.

The force dark feature for webview content has two so-called strategies:

  • User agent darkening: The webview will set its content to dark mode by automatically inverting or darkening colors of its content.

  • Theme based darkening: The webview will change to dark mode according to the theme of the content (which includes the @media (prefers-color-scheme: dark) query).

To set which strategy the webview should use to apply force dark you can use the following code:

if(WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) {
    WebSettingsCompat.setForceDarkStrategy(myWebView.getSettings(), WebSettingsCompat.DARK_STRATEGY_WEB_THEME_DARKENING_ONLY);
}

Note: Strategy selection requires Android System WebView v83 or up to be installed on the running device. WebView versions that support setForceDark but do not support strategy selections (v76 to v81) will use user agent darkening

The supported strategy options are:

  • DARK_STRATEGY_USER_AGENT_DARKENING_ONLY: Only use user agent darkening and ignore any themes in the content (default)
  • DARK_STRATEGY_WEB_THEME_DARKENING_ONLY: Only use the dark theme of the content itself to set the page to dark mode. If the content doesn't have a dark theme, the webview won't apply any darkening and show it "as is".
  • DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING: Use the dark theme of the content itself to set the page to dark mode. If content doesn't have a dark theme, use user agent darkening.

How do Javascript checks work for darkened webviews?

The JavaScript call window.matchMedia('(prefers-color-scheme: dark)') will match in both the user agent darkening and web theme darkening strategy.

I have my webview set to FORCE_DARK_AUTO and my app is running in a daynight theme, but somehow my webview doesn't apply dark mode automatically based on my app theme. Why does this happen?

It's because the FORCE_DARK_AUTO setting value of the webview doesn't work based on themes (as noted in the documentation). It checks for the Android 10 Force Dark feature (a "quick-fix" dark mode feature for apps. It's similarly named, but not directly related to the WebView force dark).

If you aren't using force dark but a app theme to handle dark mode (as recommended), you have to implement your own check for when to apply the webview's force dark feature. An example when using a DayNight theme:

int nightModeFlags = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
if (nightModeFlags == Configuration.UI_MODE_NIGHT_YES) {
   //Code to enable force dark using FORCE_DARK_ON and select force dark strategy 
}

Upvotes: 66

Xavier Vega
Xavier Vega

Reputation: 731

for Webview up to v93 (implementation 'androidx.webkit:webkit:1.4.0') the flag DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING wasn't working for me untill I used this code:

if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
            if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)){            
            WebSettingsCompat.setForceDarkStrategy(
                webView.settings,
                WebSettingsCompat.DARK_STRATEGY_WEB_THEME_DARKENING_ONLY
            )
        }
        WebSettingsCompat.setForceDark(webView.settings, WebSettingsCompat.FORCE_DARK_ON)
    }    
webView.evaluateJavascript("document.documentElement.setAttribute('data-color-mode', 'dark');", null)

which requests the site's dark theme, but does not force the color inversion.

Upvotes: 0

Ahamadullah Saikat
Ahamadullah Saikat

Reputation: 4644

implementation 'androidx.webkit:webkit:1.4.0'


WebSettings settings = webView.getSettings();

if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
   if (isNightMode) {
      WebSettingsCompat.setForceDark(settings, WebSettingsCompat.FORCE_DARK_ON);
   } else {
      WebSettingsCompat.setForceDark(settings, WebSettingsCompat.FORCE_DARK_OFF);
   }
}

Upvotes: 2

Tobias Sargeant
Tobias Sargeant

Reputation: 41

There are two relevant pieces of documentation here:

https://developer.android.com/guide/topics/ui/look-and-feel/darktheme#force_dark

https://developer.android.com/reference/android/view/View.html#setForceDarkAllowed(boolean)

The key points are

  1. The WebView must allow force dark, and all its parents must too.
  2. The theme must be marked as light.

This means that getting FORCE_DARK_AUTO behaviour to work in WebView is a little complex. Adding the following to an AppCompat.DayNight theme does work, but is maybe not the best solution as it may apply Android force dark to views other than the WebView.

<item name="android:forceDarkAllowed">true</item>
<item name="android:isLightTheme">true</item>

The other option is to handle this manually, by setting FORCE_DARK_ON/OFF based upon the relevant configuration change.

Currentlly WebView does not support prefers-color-scheme partly because force dark was implemented before prefers-color-scheme standardisation, and partly because there's no way to integrate that sensibly (without style flashes) with force dark. The intent is to provide the ability to choose either force dark or prefers-color-scheme through androidx.webkit.

Upvotes: 4

jcesarmobile
jcesarmobile

Reputation: 53301

What I do, based on your question code, is to force based on current status:

int nightModeFlags = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
if (nightModeFlags == Configuration.UI_MODE_NIGHT_YES) {
  webSettings.setForceDark(WebSettings.FORCE_DARK_ON);
}

This way @media (prefers-color-scheme: dark) works, but @media (prefers-color-scheme: light) still doesn't work (tried using FORCE_DARK_OFF and FORCE_DARK_AUTO in an else)

Upvotes: 9

cmd
cmd

Reputation: 81

I don't think WebView honours the prefers-color-scheme CSS media query yet.

The new API for setForceDark has three states: on, off or auto.

ON - your content will always be rendered darker everytime.

OFF - your content will always be rendered light everytime.

AUTO - the content will be rendered darker if your app's theme is darker OR the device OS is in dark mode because the user toggles the OS level switch or battery saver mode turned on.

I believe support for older versions of Android and also control over whether to use prefers-color-scheme instead of WebView's force dark is coming soon via AndroidX. Due date is unknown at present.

For now I would recommend setting the WebView to setForceDark Auto. This will work on Android Q and above.

I would keep an eye on AndroidX release notes for the rest of the support you require for devices on Android P and below.

Upvotes: 1

Related Questions