47

Background

On recent Android versions, ever since Android 8.1, the OS got more and more support for themes. More specifically dark theme.

The problem

Even though there is a lot of talk about dark mode in the point-of-view for users, there is almost nothing that's written for developers.

What I've found

Starting from Android 8.1, Google provided some sort of dark theme . If the user chooses to have a dark wallpaper, some UI components of the OS would turn black (article here).

In addition, if you developed a live wallpaper app, you could tell the OS which colors it has (3 types of colors), which affected the OS colors too (at least on Vanilla based ROMs and on Google devices). That's why I even made an app that lets you have any wallpaper while still be able to choose the colors (here). This is done by calling notifyColorsChanged and then provide them using onComputeColors

Starting from Android 9.0, it is now possible to choose which theme to have: light, dark or automatic (based on wallpaper) :

enter image description here

And now on the near Android Q , it seems it went further, yet it's still unclear to what extent. Somehow a launcher called "Smart Launcher" has ridden on it, offering to use the theme right on itself (article here). So, if you enable dark mode (manually, as written here) , you get the app's settings screen as such:

enter image description here

The only thing I've found so far is the above articles, and that I'm following this kind of topic.

I also know how to request the OS to change color using the live wallpaper, but this seems to be changing on Android Q, at least according to what I've seen when trying it (I think it's more based on time-of-day, but not sure).

The questions

  1. Is there an API to get which colors the OS is set to use ?

  2. Is there any kind of API to get the theme of the OS ? From which version?

  3. Is the new API somehow related to night mode too? How do those work together?

  4. Is there a nice API for apps to handle the chosen theme? Meaning that if the OS is in certain theme, so will the current app?

android developer
  • 114,585
  • 152
  • 739
  • 1,270

6 Answers6

48

Google has just published the documentation on the dark theme at the end of I/O 2019, here.

In order to manage the dark theme, you must first use the latest version of the Material Components library: "com.google.android.material:material:1.1.0-alpha06".

Change the application theme according to the system theme

For the application to switch to the dark theme depending on the system, only one theme is required. To do this, the theme must have Theme.MaterialComponents.DayNight as a parent.

<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">
    ...
</style>

Determine the current system theme

To know if the system is currently in dark theme or not, you can implement the following code:

switch (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) {
    case Configuration.UI_MODE_NIGHT_YES:
        …
        break;
    case Configuration.UI_MODE_NIGHT_NO:
        …
        break; 
}

Be notified of a change in the theme

I don't think it's possible to implement a callback to be notified whenever the theme changes, but that's not a problem. Indeed, when the system changes theme, the activity is automatically recreated. Placing the previous code at the beginning of the activity is then sufficient.

From which version of the Android SDK does it work?

I couldn't get this to work on Android Pie with version 28 of the Android SDK. So I assume that this only works from the next version of the SDK, which will be launched with Q, version 29.

Result

result

android developer
  • 114,585
  • 152
  • 739
  • 1,270
Charles Annic
  • 865
  • 1
  • 8
  • 20
  • Why is it called "DayNight" ? Have they returned the feature that changes the theme according to time ? Have you tried this API ? Does it work ? – android developer May 08 '19 at 17:37
  • I have no idea about the name, I find it weird too. I didn't find the setting to change the theme over time in the Android Q settings, but this is independent of your implementation in your application. If this feature is present in Q, your application will adapt to it with the solution I just gave. I have tried this API in the application I am currently developing and it works perfectly! – Charles Annic May 08 '19 at 18:22
  • Edit : I have just added an example of the result obtained. – Charles Annic May 08 '19 at 18:30
  • From which device did you take it? I never saw quick settings tiles being square shaped... – android developer May 08 '19 at 23:17
  • And how come you said it will restart, but I see no transition of restarting an Activity? It changed color in an instant... – android developer May 08 '19 at 23:27
  • I've found this one too: https://youtu.be/l1e1gHhci70?t=1214 . Is this what you meant to use? – android developer May 09 '19 at 00:25
  • 1
    I took it from the Android Studio emulator with the Android Q beta 3. Since the first beta of Android Q, it is possible to modify some aesthetic aspects of the system, such as the shape of the tiles. See here: https://www.androidpolice.com/2019/04/04/android-q-developer-options-include-accent-color-font-and-icon-shape-customization/ – Charles Annic May 09 '19 at 07:10
  • 1
    In the documentation I gave previously, Google provides the following link for the AppCompat DayNight Documentation: https://medium.com/androiddevelopers/appcompat-v23-2-daynight-d10f90c83e94. This is where it is said that the activity is recreated with each change. I tested it, the activity is being recreated. Indeed, there is no animation because the transition is probably overriding by the system when changing themes. And finally, yes, that's what I am using. Have you tried my solution? Doesn't that work for you? – Charles Annic May 09 '19 at 07:10
  • About the icons shape, I thought it's just for the launcher. Cool (but a bit weird, haha) . About the theme check, works well. About the theme in styles file, it works, but what should be done for `AppTheme.AppBarOverlay` and `AppTheme.PopupOverlay` ? What should be their parents? And is there a way to get which colors the wallpaper or live wallpaper sends to the OS to be used, on Android 8.1 ? – android developer May 10 '19 at 21:50
  • I agree, haha, I'll probably stick to the round icons! `AppTheme.AppBarOverlay` inherits `ThemeOverlay.MaterialComponents.Dark.ActionBar` and `AppTheme.PopupOverlay` inherits `ThemeOverlay.MaterialComponents.Light`. Is the WallpaperColors API what you are looking for? It allows you to get the primary, secondary and tertiary colors of the wallpaper. https://developer.android.com/reference/android/app/WallpaperColors – Charles Annic May 11 '19 at 09:22
  • This doesn't look well. When I click on the overflow menu item, I see the popup text to be black on dark background, so it's barely readable. See: https://imgur.com/a/kz914YM . About WallpaperColors, how do I get an instance of it? As far as I know, this class is used by the live wallpaper app. My question was if it's possible to get those colors that are set to be used by the OS (which are requested either by the live wallpaper or the normal one). I don't know if this API even exists. – android developer May 12 '19 at 18:59
31

A simpler Kotlin approach to Charles Annic's answer:

fun Context.isDarkThemeOn(): Boolean {
    return resources.configuration.uiMode and 
            Configuration.UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
}
Vitor Hugo Schwaab
  • 1,545
  • 20
  • 31
9

OK so I got to know how this usually works, on both newest version of Android (Q) and before.

It seems that when the OS creates the WallpaperColors , it also generates color-hints. In the function WallpaperColors.fromBitmap , there is a call to int hints = calculateDarkHints(bitmap); , and this is the code of calculateDarkHints :

/**
 * Checks if image is bright and clean enough to support light text.
 *
 * @param source What to read.
 * @return Whether image supports dark text or not.
 */
private static int calculateDarkHints(Bitmap source) {
    if (source == null) {
        return 0;
    }

    int[] pixels = new int[source.getWidth() * source.getHeight()];
    double totalLuminance = 0;
    final int maxDarkPixels = (int) (pixels.length * MAX_DARK_AREA);
    int darkPixels = 0;
    source.getPixels(pixels, 0 /* offset */, source.getWidth(), 0 /* x */, 0 /* y */,
            source.getWidth(), source.getHeight());

    // This bitmap was already resized to fit the maximum allowed area.
    // Let's just loop through the pixels, no sweat!
    float[] tmpHsl = new float[3];
    for (int i = 0; i < pixels.length; i++) {
        ColorUtils.colorToHSL(pixels[i], tmpHsl);
        final float luminance = tmpHsl[2];
        final int alpha = Color.alpha(pixels[i]);
        // Make sure we don't have a dark pixel mass that will
        // make text illegible.
        if (luminance < DARK_PIXEL_LUMINANCE && alpha != 0) {
            darkPixels++;
        }
        totalLuminance += luminance;
    }

    int hints = 0;
    double meanLuminance = totalLuminance / pixels.length;
    if (meanLuminance > BRIGHT_IMAGE_MEAN_LUMINANCE && darkPixels < maxDarkPixels) {
        hints |= HINT_SUPPORTS_DARK_TEXT;
    }
    if (meanLuminance < DARK_THEME_MEAN_LUMINANCE) {
        hints |= HINT_SUPPORTS_DARK_THEME;
    }

    return hints;
}

Then searching for getColorHints that the WallpaperColors.java has, I've found updateTheme function in StatusBar.java :

    WallpaperColors systemColors = mColorExtractor
            .getWallpaperColors(WallpaperManager.FLAG_SYSTEM);
    final boolean useDarkTheme = systemColors != null
            && (systemColors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_THEME) != 0;

This would work only on Android 8.1 , because then the theme was based on the colors of the wallpaper alone. On Android 9.0 , the user can set it without any connection to the wallpaper.

Here's what I've made, according to what I've seen on Android :

enum class DarkThemeCheckResult {
    DEFAULT_BEFORE_THEMES, LIGHT, DARK, PROBABLY_DARK, PROBABLY_LIGHT, USER_CHOSEN
}

@JvmStatic
fun getIsOsDarkTheme(context: Context): DarkThemeCheckResult {
    when {
        Build.VERSION.SDK_INT <= Build.VERSION_CODES.O -> return DarkThemeCheckResult.DEFAULT_BEFORE_THEMES
        Build.VERSION.SDK_INT <= Build.VERSION_CODES.P -> {
            val wallpaperManager = WallpaperManager.getInstance(context)
            val wallpaperColors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM)
                    ?: return DarkThemeCheckResult.UNKNOWN
            val primaryColor = wallpaperColors.primaryColor.toArgb()
            val secondaryColor = wallpaperColors.secondaryColor?.toArgb() ?: primaryColor
            val tertiaryColor = wallpaperColors.tertiaryColor?.toArgb() ?: secondaryColor
            val bitmap = generateBitmapFromColors(primaryColor, secondaryColor, tertiaryColor)
            val darkHints = calculateDarkHints(bitmap)
            //taken from StatusBar.java , in updateTheme :
            val HINT_SUPPORTS_DARK_THEME = 1 shl 1
            val useDarkTheme = darkHints and HINT_SUPPORTS_DARK_THEME != 0
            if (Build.VERSION.SDK_INT == VERSION_CODES.O_MR1)
                return if (useDarkTheme)
                    DarkThemeCheckResult.UNKNOWN_MAYBE_DARK
                else DarkThemeCheckResult.UNKNOWN_MAYBE_LIGHT
            return if (useDarkTheme)
                DarkThemeCheckResult.MOST_PROBABLY_DARK
            else DarkThemeCheckResult.MOST_PROBABLY_LIGHT
        }
        else -> {
            return when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
                Configuration.UI_MODE_NIGHT_YES -> DarkThemeCheckResult.DARK
                Configuration.UI_MODE_NIGHT_NO -> DarkThemeCheckResult.LIGHT
                else -> DarkThemeCheckResult.MOST_PROBABLY_LIGHT
            }
        }
    }
}

fun generateBitmapFromColors(@ColorInt primaryColor: Int, @ColorInt secondaryColor: Int, @ColorInt tertiaryColor: Int): Bitmap {
    val colors = intArrayOf(primaryColor, secondaryColor, tertiaryColor)
    val imageSize = 6
    val bitmap = Bitmap.createBitmap(imageSize, 1, Bitmap.Config.ARGB_8888)
    for (i in 0 until imageSize / 2)
        bitmap.setPixel(i, 0, colors[0])
    for (i in imageSize / 2 until imageSize / 2 + imageSize / 3)
        bitmap.setPixel(i, 0, colors[1])
    for (i in imageSize / 2 + imageSize / 3 until imageSize)
        bitmap.setPixel(i, 0, colors[2])
    return bitmap
}

I've set the various possible values, because in most of those cases nothing is guaranteed.

android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • what is the MAX_DARK_AREA,DARK_PIXEL_LUMINANCE, BRIGHT_IMAGE_MEAN_LUMINANCE, HINT_SUPPORTS_DARK_TEXT HINT_SUPPORTS_DARK_THEME values ? – Yusuf Eka Sayogana Apr 20 '21 at 09:01
  • @YusufEkaSayogana I didn't copy all the code. It's not mine and it's irrelevant . If you wish you can read the entire code online somewhere, just as I did. It's also in the source code of Android, available on the SDK. Search for the code of "calculateDarkHints". I think it also changed over Android versions. – android developer Apr 20 '21 at 09:18
  • why this answer is accepted since it is not complete – erik.aortiz Feb 24 '22 at 01:22
  • @eriknyk What's missing? – android developer Feb 25 '22 at 07:59
  • @androiddeveloper inside calculateDarkHints() methods there are many known constants like: MAX_DARK_AREA, DARK_PIXEL_LUMINANCE, BRIGHT_IMAGE_MEAN_LUMINANCE, DARK_THEME_MEAN_LUMINANCE. And other uknown props in getIsOsDarkTheme() are: DarkThemeCheckResult.UNKNOWN, DarkThemeCheckResult.UNKNOWN_MAYBE_DARK, DarkThemeCheckResult.UNKNOWN_MAYBE_LIGHT, DarkThemeCheckResult.MOST_PROBABLY_DARK, else DarkThemeCheckResult.MOST_PROBABLY_LIGHT. in this secodn case we can just add them however not sure why we need all those variants, but in the first case I have not idea. – erik.aortiz Mar 02 '22 at 21:44
  • @eriknyk But what would you do with these, that you think they are missing and important? – android developer Mar 03 '22 at 19:49
  • @androiddeveloper think like you're another developer with another knowlege level than you and asnwer your question yourself – erik.aortiz Mar 09 '22 at 20:46
  • @eriknyk Why would I talk about myself? I'm not the topic. I'm talking about what you wrote... – android developer Mar 09 '22 at 23:48
5

I think Google is basing at the battery level for applying dark and light themes in Android Q.

Maybe DayNight theme?

You then need to enable the feature in your app. You do that by calling AppCompatDelegate.setDefaultNightMode(), which takes one of the follow values:

  • MODE_NIGHT_NO. Always use the day (light) theme.
  • MODE_NIGHT_YES. Always use the night (dark) theme.
  • MODE_NIGHT_FOLLOW_SYSTEM (default). This setting follows the system’s setting, which on Android Pie and above is a system setting (more on this below).
  • MODE_NIGHT_AUTO_BATTERY. Changes to dark when the device has its ‘Battery Saver’ feature enabled, light otherwise. ✨New in v1.1.0-alpha03.
  • MODE_NIGHT_AUTO_TIME & MODE_NIGHT_AUTO. Changes between day/night based on the time of day.
JavierSegoviaCordoba
  • 6,531
  • 9
  • 37
  • 51
  • What about getting the colors? Is it possible? And why does the article say about Android Q? What's special about it? I've tested the code to check on which mode I am, and even though now it's night, it says "Night mode is not active, we're in day time" . Tested on Android 9 . How come ? Or maybe it's for Android Q ? – android developer Apr 21 '19 at 23:06
  • @androiddeveloper the article really said nothing about Android Q (2016). It only helps to let you change the theme based on the device settings (like the behavior of Smart Launcher). – JavierSegoviaCordoba Apr 21 '19 at 23:06
  • Tomorrow I will try personally with multiple API versions. – JavierSegoviaCordoba Apr 21 '19 at 23:09
  • Actually something is weird on my device. The settings of the OS lets me to choose which theme to use, but all of them let it stay on dark theme, including when I choose light theme. I remember I've reported about this issue, but I thought it was fixed... Anyway, is there a way to also check the colors and not just dark vs light ? – android developer Apr 21 '19 at 23:46
  • I am testing it and on my Android PIE emulator the daynight theme doesn't work (even trying to force to use it everytime) and Android Studio doesn't let me download AndroidQ. I am going to try Android Oreo... – JavierSegoviaCordoba Apr 22 '19 at 11:46
  • Really the emulator doesn't change the theme even in the android settings screen... I am going to try with my OnePlus 6 – JavierSegoviaCordoba Apr 22 '19 at 11:53
  • I can't get it working. I tried both (App class and for each Activity). I tried to force the Dark mode with AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) and delegate.setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES). I tried AppCompat and Material Components too. – JavierSegoviaCordoba Apr 22 '19 at 12:21
  • Maybe it works only on Android Q, then? Can you please share me the project you've tried? I have Android Q emulator working. I can also try on my Pixel 2 which currently has Android 9 (had Q on it before, but went back because of annoyances). – android developer Apr 22 '19 at 17:08
  • Really I got nothing working. But it should works on Android Pie at least. I will try to download Android Q tomorrow. The project has really nothing interesting, I just followed the steps from the Chris Banes post. – JavierSegoviaCordoba Apr 23 '19 at 02:01
  • `AppCompatDelegate.MODE_NIGHT_YES` is working on my One Plus 3T running Android 9 but `AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM` is not even with the night mode enabled. – Pedro Paulo Amorim Sep 04 '19 at 10:17
4

I wanted to add to Vitor Hugo Schwaab answer, you could break the code down further and use isNightModeActive.

resources.configuration.isNightModeActive

resources
configuration
isNightModeActive

Shawn
  • 1,222
  • 1
  • 18
  • 41
0

I made this code based on the available information from all the possible sources and it worked for me!!! Hope it helps others too. The app for which I created this code is intended for API level 21 (Android Lollipop 5.0), so use it accordingly.

public class MainActivity extends AppCompatActivity{
    public final String[] themes = {"System Default","Light","Dark"};
    public static int checkedTheme;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        loadAppTheme(); //always put this before setContentView();
        setContentView(R.layout.activity_main);
        //your other code
    }

    private void showChangeThemeAlertDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("Change Theme");
        builder.setSingleChoiceItems(themes, checkedTheme, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                checkedTheme = which;
                switch (which) {
                    case 0:
                        setAppTheme(0);
                        break;
                    case 1:
                        setAppTheme(1);
                        break;
                    case 2:
                        setAppTheme(2);
                        break;
                }
                dialog.dismiss();
            }
        });
        AlertDialog alertDialog = builder.create();
        alertDialog.show();

    }

    private void setAppTheme(int themeNo) {
        switch (themeNo){
            case 0:
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
                break;
            case 1:
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
                break;
            case 2:
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
                break;
        }
        SharedPreferences.Editor editor = getSharedPreferences("Themes",MODE_PRIVATE).edit();
        editor.putInt("ThemeNo",checkedTheme);
        editor.apply();
    }


    private void loadAppTheme() {
        SharedPreferences themePreference = getSharedPreferences("Themes",Activity.MODE_PRIVATE);
        checkedTheme = themePreference.getInt("ThemeNo",0);
        setAppTheme(checkedTheme);
    }
}
Shubham Nanche
  • 130
  • 2
  • 11