Diablo mode

In Android 10, the Dark Theme has system-level support. Dark mode is not only cool, but also has the benefits of reducing screen power consumption and being more comfortable in low-light environments. Today we will take a look at how to adapt the Dark mode. This article will cover the following points:

  • Dynamically enable Dark mode
  • Use DayNight for Dark mode
  • Use Force Dark for Dark mode
  • Force Dark system source code analysis
  • Adaptation Process Suggestions

I believe this article will give you a more complete understanding of the Dark mode.

Dynamic open

The Dark mode switch has been added to Android 10’s OS Settings, but in addition to the system Settings, we can also dynamically enable it ourselves. If we had a button in our project to turn on dark mode, we could do this:

btn.setOnClickListener {
    if (AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES) {
        // Turn off dark mode
        AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
    } else {
        // Start dark mode
        AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
    }
}
Copy the code

Turn off dark mode if it is currently on, and vice versa. You may have seen another way of writing delegate.localNightMode, which also works, but they differ in scope:

// Applies to all components of the current project
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) 
// Applies only to the current component
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES              
Copy the code

Also note that setting dark mode resets the Activity life cycle by default, requiring the entire page to be re-rendered, so don’t set it directly in onCreate. If you don’t want to reset the life cycle, you can configure the Activity android:configChanges=”uiMode”, but this requires manual adaptation in the onConfigurationChanged() method.

NightMode

There are two dark states: YES and NO, but there are more dark states:

  • MODE_NIGHT_FOLLOW_SYSTEM Follows system Settings
  • MODE_NIGHT_NO Turns off dark mode
  • MODE_NIGHT_YES Enables dark mode
  • MODE_NIGHT_AUTO_BATTERY When the system enters power saving mode, enable dark mode
  • MODE_NIGHT_UNSPECIFIED. The default value is unspecified

Using MODE_NIGHT_AUTO_BATTERY doesn’t necessarily work because many custom systems have magic changes to the power-saving mode. In addition, if DefaultNightMode and LocalNightMode are both the default values of MODE_NIGHT_UNSPECIFIED, MODE_NIGHT_FOLLOW_SYSTEM follows the system.

DayNight

Now it’s time to adapt the Dark mode. We used Android Studio’s Basic Activity template to create a project that we modified for dark mode adaptation.

DayNight theme adaptation

The first step is to find the Theme used for the current project and change the default Theme to Theme.AppCompat.DayNight:

<style name="AppTheme" parent="Theme.AppCompat.DayNight">
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
</style>
Copy the code

Step 2, there is no step 2, now the project supports dark mode, turn on dark mode to see the effect:

It’s not that simple, but something tells us it’s not.

Hard coded

If we go into the MainActivity layout file activity_main, we can see that there is no hard coding at all. What is hard coding? It is what we usually call “writing to death”. Will the dark mode still work if we write a dead color? Let’s give it a try. We’ll write a white background for the root layout android:background=”#FFFFFF” and switch to dark mode to look like this:

As you can see, the dark mode fails in the case of dead color values. Let’s take a look at how to fit custom color values.

value-night

Add a configuration color to colors.xml, for example:

<color name="color_bg">#FFFFFF</color>
Copy the code

This is the color value used in normal mode. In order to match dark mode, you need a dark mode color value. Create a new values-night directory and configure the corresponding color value to the color.xml file in this directory.

Change the background color of the root layout to color_bg, so we can use the color we want:

In Dark mode, the system preferentially finds the resource configuration from the night suffix. That’s it for dark mode adaptation using the DayNight theme.

DayNight disadvantages

Some of the articles about Android 10’s dark mode adaptation end there, but the DayNight theme isn’t a new addition to Android 10, it’s been around since Android 6.0. Although it doesn’t cover much, you may have noticed that it’s not very practical in real projects. First of all, using this adaptation requires that all color values of our entire project should not be hard-coded, which is already very difficult to achieve, and many projects are even difficult to achieve uniform design specifications. Taking a step back, even if all of our color values are configured in XML, there are hundreds of color values configured in colors.xml, and we need to configure a corresponding dark color value for all of them and make sure they look good in dark mode. So, unless the project already has a strict design specification and is strictly followed, it’s almost impossible to use the DayNight theme for dark mode. Android 10 is more than just a dark mode switch, but here’s a look at what Android 10 has to offer.

Force Dark

In fact, our requirement is very clear, even hard coding can be adapted to dark mode. Android 10’s new Force Dark enforcement diablo does just that.

forceDarkAllowed

Go back to the previous project, make the background dead white, and go to the theme configuration for styles.xml again. Instead of a DayNight theme, we’ll change the configuration to the following:

<style name="AppTheme" parent="Theme.AppCompat.Light">
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
    <item name="android:forceDarkAllowed">true</item>
</style>
Copy the code

Let’s change the theme back to the Light theme. We’ll talk more about why you want to use Light later in the source code section. In addition, we’ve added a forceDarkAllowed configuration to compileSdkVersion 29. It literally means “turn on mandatory darkness”. Now that the configuration is complete, run it on an Android 10 machine and switch to Dark mode, remembering that this time the background is written dead white:

The background is forced to black, and if you look carefully, the background color of the lower right button is also darker. Force Dark is so violent that even the color value of our writing death has been changed. Although it is convenient, it also gives us a sense of insecurity. What if Force Dark doesn’t match the color we want? Can we also customize the dark value? That’s ok.

Force Dark Custom adaptation

In addition to the theme new forceDarkAllowed configuration, the View also has. If a View needs to use custom color values for Dark mode, we need to add this configuration to the View and have Force Dark exclude it:

android:forceDarkAllowed="false"
Copy the code

Then in the code based on whether the current dark mode, the color value dynamically set. There are a few things to note about View’s forceDarkAllowed:

  • To use this configuration in a View, Force Dark is enabled for the current theme
  • The default value is true, so setting it to true is the same as not setting it
  • Scope is the current View and all of its child Views

In summary, it can be seen that there is no good Force Dark custom scheme at present. Fortunately, the overall effect of Force Dark is not a big problem, and even if we want to customize, we try to customize only the sub-view.

Force Dark source code parsing

Let’s take a look at the source code and see how the system converts colors in dark mode. Just a few key snippets of source code are shown here, but how they are called between them is unnecessary.

updateForceDarkMode

Look at the source code first we need to find the entry, the entry is the theme of the forceDarkAllowed configuration, search to see that this configuration will be used in view of the file. The relevant instructions have been commented in the code.

// android.view.ViewRootImpl.java

private void updateForceDarkMode(a) {
    if (mAttachInfo.mThreadedRenderer == null) return;

    // Check if you are in dark mode
    boolean useAutoDark = getNightMode() == Configuration.UI_MODE_NIGHT_YES;

    if (useAutoDark) {
        // This is used as the default value, but we'll get to that later.
        boolean forceDarkAllowedDefault = SystemProperties.getBoolean(ThreadedRenderer.DEBUG_FORCE_DARK, false);
        TypedArray a = mContext.obtainStyledAttributes(R.styleable.Theme);
        // Determine if the current is a Light theme, which is why we used the Light theme earlier. This makes sense; you only need to be dark if the current theme is a bright color.
        // Determine if forced diablo is currently allowed, which is how we found it.
        useAutoDark = a.getBoolean(R.styleable.Theme_isLightTheme, true)
                && a.getBoolean(R.styleable.Theme_forceDarkAllowed, forceDarkAllowedDefault);
        a.recycle();
    }

    if (mAttachInfo.mThreadedRenderer.setForceDark(useAutoDark)) {
        // TODO: Don't require regenerating all display lists to apply this settinginvalidateWorld(mView); }}Copy the code

To sum up, according to this method we can know that there are three conditions for Force Dark to be effective:

  • In dark mode
  • The Light theme was used
  • Force Dark is allowed

Source code to follow down, found called Native code.

handleForceDark

The next key piece of code is RenderNode’s handleForceDark function. RenderNode is a rendering node. A View can have multiple rendering nodes. For example, the text part of a TextView is a rendering node, and the background it sets is also a rendering node. So let’s see what this function does.

// frameworks/base/libs/hwui/RenderNode.cpp

void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) {
    if(CC_LIKELY(! info || info->disableForceDark)) {return;
    }
    // This function seems a bit complicated, but we only need to focus on the usage parameter.
    // Usage has two values, Foreground and Background.
    auto usage = usageHint();
    const auto& children = mDisplayList->mChildNodes;
    if (mDisplayList->hasText()) {
        // If the current node hasText() contains text, it is a Foreground
        usage = UsageHint::Foreground;
    }
    // The following judgments are set to Background
    if (usage == UsageHint::Unknown) {
        if (children.size() > 1) {
            usage = UsageHint::Background;
        } else if (children.size() == 1 &&
                children.front().getRenderNode()->usageHint() !=
                        UsageHint::Background) {
            usage = UsageHint::Background;
        }
    }
    if (children.size() > 1) {
        // Crude overlap check
        SkRect drawn = SkRect::MakeEmpty();
        for (autoiter = children.rbegin(); iter ! = children.rend(); ++iter) {const auto& child = iter->getRenderNode();
            // We use stagingProperties here because we haven't yet sync'd the children
            SkRect bounds = SkRect::MakeXYWH(child->stagingProperties().getX(), child->stagingProperties().getY(),
                    child->stagingProperties().getWidth(), child->stagingProperties().getHeight());
            if (bounds.contains(drawn)) {
                // This contains everything drawn after it, so make it a backgroundchild->setUsageHint(UsageHint::Background); } drawn.join(bounds); }}// According to the classification, if the background is Dark, otherwise it is Light.
    mDisplayList->mDisplayList.applyColorTransform(
            usage == UsageHint::Background ? ColorTransform::Dark : ColorTransform::Light);
}
Copy the code

This function classifies Foreground or Background of the current drawing node. In order to ensure the visibility of the text, it is necessary to ensure a certain contrast. If the background is switched to a dark color, it is necessary to switch the text to a bright color.

transformColor

Depending on the color type, a CanvasTransform is launched to transform the color. This is also the core of Force Dark.

// frameworks/base/libs/hwui/CanvasTransform.cpp

static SkColor transformColor(ColorTransform transform, SkColor color) {
    switch (transform) {
        case ColorTransform::Light:
            // Switch to a bright color
            return makeLight(color);
        case ColorTransform::Dark:
            // Switch to dark
            return makeDark(color);
        default:
            returncolor; }}Copy the code

The corresponding function is called to convert the color based on the type. Let’s look at makeDark.

static SkColor makeDark(SkColor color) {
    Lab lab = sRGBToLab(color);
    float invertedL = std::min(110 - lab.L, 100.0 f);
    if (invertedL < lab.L) {
        lab.L = invertedL;
        return LabToSRGB(lab, SkColorGetA(color));
    } else {
        returncolor; }}Copy the code

Here the RGB color values are converted to Lab format. Lab format contains L, A and B parameters, ab corresponds to the two dimensions of chromology, do not care about it, we should pay attention to the L inside. L is brightness, and it ranges from 0 to 100. The smaller the number, the darker the color, and vice versa. The color on the right of the Android on the cover of this article is the effect of the reduced brightness. Back in the code, I’m subtracting the current brightness from 110, which is kind of the inverse of the brightness. As for why 110 was used instead of 100, I guess it was to avoid using pure black. As can be seen in the official Dark mode design specification, it is recommended to use a dark grey background rather than plain black.

Finally, compare the brightness of the inverted color value and the primary color value, and return the darker color value. The makeLight function is similar.

static SkColor makeLight(SkColor color) {
    Lab lab = sRGBToLab(color);
    float invertedL = std::min(110 - lab.L, 100.0 f);
    if (invertedL > lab.L) {
        lab.L = invertedL;
        return LabToSRGB(lab, SkColorGetA(color));
    } else {
        returncolor; }}Copy the code

So here we find that Force Dark’s rule for forcing Dark to switch colors, or its essence, is to invert brightness.

Adaptation Process Suggestions

If your project compileSdkVersion has been upgraded to 29, you can now enable Force Dark for Dark mode. However, many projects still have some way to go before they can be upgraded to 29. Is there any way we can adapt in advance?

Debug Force Dark

Back to where we started:

boolean forceDarkAllowedDefault = SystemProperties.getBoolean(ThreadedRenderer.DEBUG_FORCE_DARK, false);
TypedArray a = mContext.obtainStyledAttributes(R.styleable.Theme);
useAutoDark = a.getBoolean(R.styleable.Theme_isLightTheme, true)
        && a.getBoolean(R.styleable.Theme_forceDarkAllowed, forceDarkAllowedDefault);
Copy the code

If Theme_forceDarkAllowed is not available, the DEBUG_FORCE_DARK is used as the default value. Where can I enable the DEBUG_FORCE_DARK? In Android 10’s developer options, you can find an additional option like this:

Here “force enable SmartDark function” is the DEBUG_FORCE_DARK switch, although we see the source code all know that it is not much intelligence. If enabled, it will apply to all projects, so you can use Force Dark for adaptation in advance.

Adaptation process

After Force Dark is enabled, it is likely to find some problematic image resources, such as ICONS with fixed backgrounds. If the project has a plan to adapt to dark mode, I suggest following the following steps:

  1. Developer option to turn on “Force SmartDark”
  2. Replace the faulty resource and perform initial adaptation
  3. Upgrade to compileSdkVersion 29
  4. Open the Force Dark
  5. Communicate with the designer, to part of the control alone adaptation

conclusion

Dark mode can be adapted using DayNight themes, but this approach is not feasible in real projects. The new Dark mode feature for Android 10 is called Force Dark Force-dark, and you just need to add a configuration to your theme that allows you to turn it on. Force Dark is implemented by reducing the background brightness and increasing the font brightness. In essence, the color value is inverted. Finally, on Android 10 devices, you can turn on “Force Enable SmartDark” in the developer option to use Force Dark adaptation in advance.

No problem.