Using a Dynamic System Accent Color in UWP

This article describes an algorithm that allows individual pages in a XAML UWP app to override the system accent color. By default, the system accent color is the Windows theme color chosen by end user. When a UWP app is running, this color is applied to lot of controls (checkbox, slider, focused text box) and hence this color could possibly collide with the app’s own theme colors. Therefor, a lot of apps statically override this accent color.

This article shows how this accent color can be overridden when navigating to or from individual pages, so that each page can have its own theme. Let’s say you’re building an app around the elements of nature. Wouldn’t it be nice to give all the pages on Water blue controls, and all pages on Fire red controls without creating different style for each control and each theme color?

Here’s a screen capture from the sample app, which is derived from my SplitView Navigation project. As you see, every page has its own accent color applied to all controls:
DynamicAccentColor

Static Change

Let’s start with explaining the correct way to statically override the system accent color. In your app.xaml, add a ThemeDictionary and override the SystemAccentColor for each theme (Default, Dark, and Light):

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.ThemeDictionaries>
            <ResourceDictionary x:Key="Default">
                <Color x:Key="SystemAccentColor">OrangeRed</Color>
            </ResourceDictionary>
            <ResourceDictionary x:Key="Dark">
                <Color x:Key="SystemAccentColor">DeepPink</Color>
            </ResourceDictionary>
            <ResourceDictionary x:Key="Light">
                <Color x:Key="SystemAccentColor">OrangeRed</Color>
            </ResourceDictionary>
        </ResourceDictionary.ThemeDictionaries>
        <ResourceDictionary.MergedDictionaries>
            <!-- Your resources -->
            <!-- ... -->
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

This will impact the following default brushes (the list depends on your Windows 10 version), in order of appearance in generic.xaml:

  • SystemControlBackgroundAccentBrush
  • SystemControlDisabledAccentBrush
  • SystemControlForegroundAccentBrush
  • SystemControlHighlightAccentBrush
  • SystemControlHighlightAltAccentBrush
  • SystemControlHighlightAltListAccentHighBrush
  • SystemControlHighlightAltListAccentLowBrush
  • SystemControlHighlightAltListAccentMediumBrush
  • SystemControlHighlightListAccentHighBrush
  • SystemControlHighlightListAccentLowBrush
  • SystemControlHighlightListAccentMediumBrush
  • SystemControlHyperlinkTextBrush
  • ContentDialogBorderThemeBrush
  • JumpListDefaultEnabledBackground
  • InkToolbarAccentColorThemeBrush

Dynamic Change

The basics

To change the accent color from C#, it suffices to override the SystemAccentColor in the Resources of the current application. At least that was my theory:

Application.Current.Resources["SystemAccentColor"] = accentColor;

Respect the High Contrast theme

Before you override the accent color -or any other theme color- you have to make sure that the user did not select a high contrast theme. I quote from the documentation: “High Contrast Themes in Windows are useful for those computer users who have an eye-sight disability, since they heighten the color contrast of text, windows borders and images on your screen, in order to make them more visible and easier to read and identify”. Fortunately UWP has an AccessibilitySettings class that allows you to verify if a high contrast theme is active.

So here’s the –theoretical- full method to apply a new accent color:

public static void ApplyAccentColor(Color accentColor)
{
    if (!new AccessibilitySettings().HighContrast)
    {
        Application.Current.Resources["SystemAccentColor"] = accentColor;
    }
}

Caveat: the page should request a Theme

Changing the system accent color at runtime only has effect when the page that you’re navigating to has declaratively set its RequestedTheme:

<Page x:Class="XamlBrewer.Uwp.DynamicAccentColor.HorsePage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      ...
      RequestedTheme="Light">

If not, the new accent color is simply ignored.

Caveat: controls are cached

The method can be called from a page’s code. You have to do this before any control starts to render, so preferably in the constructor. The call even be done before InitializeComponent  (which is actually a weird place to write code). But even there it is already too late for some controls. Apparently UWP creates a cache of the most common control types. Sometimes you’re served with a control from this cache and you’ll end up with a recycled UI element that still uses a previous accent color, like this:
Oops_2

Whilst thoroughly testing the app I noticed that this phenomenon never occurred when navigating from one of the empty pages (Settings and About). So I added an new empty page to the solution, moved call to switch the accent color out of the Page and into the Navigation Service, and added an extra visit to the new empty page to the navigation logic. Hosting the logic in the Navigation Service instead of in the Page also allows to set back the default color when the user navigates away from the page. Finally, to remove all accent color responsibilities from the page, I decorated the Theming service with a dictionary to host the preferred accent color for each color page type:

private static Dictionary<Type, Color> AccentColors = new Dictionary<Type, Color>();
public static void RegisterAccentColor(Type pageType, Color accentColor)
{
    if (AccentColors.ContainsKey(pageType))
    {
        AccentColors.Add(pageType, accentColor);
    }
    else
    {
        AccentColors[pageType] = accentColor;
    }
}

The dictionary is populated by the Shell when the app starts up:

Theme.RegisterAccentColor(typeof(BirdPage), 
	(Color)Application.Current.Resources["BirdAccentColor"]);
Theme.RegisterAccentColor(typeof(DonkeyPage), 
	(Color)Application.Current.Resources["DonkeyAccentColor"]);
Theme.RegisterAccentColor(typeof(HorsePage), 
	(Color)Application.Current.Resources["HorseAccentColor"]);
Theme.RegisterAccentColor(typeof(RabbitPage), 
	(Color)Application.Current.Resources["RabbitAccentColor"]);

Note: these dictionary entries can be changed at runtime.

The Theming service was also extended with a method that sets the accent color based on the page type, with a fallback to the default color:

public static void ApplyAccentColor(Type pageType)
{
    if (AccentColors.ContainsKey(pageType))
    {
        ApplyAccentColor(AccentColors[pageType]);
    }
    else
    {
        ApplyAccentColor((Color)Application.Current.Resources["DefaultAccentColor"]);
    }
}

Apparently just hitting an empty page before navigating to a page with controls isn’t sufficient to clear the cache. You also need a short ‘asynchronous pause’ over there, with a Task.Delay() call with a timespan of over 100 milliseconds (250 seems long enough for most devices). I realize that this is the kind of call that you only do when you painted yourself into a corner, and it brings back memories to that guilty feeling when we needed to call old VB’s DoEvents function. But it does the trick, and I must admit that the short visit to the empty page actually enhances the transition animation.

Here’s the call that is done before navigating to a new destination:

private static async Task InitiateNavigation(Type sourcePageType)
{
    lock (typeof(Navigation))
    {
        // Apply the page's accent color (or the default)
        Theme.ApplyAccentColor(sourcePageType);

        // Clear native control and page caches so that they accept the new SystemAccentColor.
        // Navigate to an empty page.
        _frame.Navigate(typeof(BackgroundPage));
    }

    // Ye olde VB6 DoEvents.
    await Task.Delay(250);  // Put it to 100 to see the slider's delay.
}

The lock statement is there to serialize navigation steps and make the app behave properly when the user is wildly clicking around in the splitview’s menu (been there, done that).

There’s also a post-navigation step: to prevent the user from ending up in the empty page when pressing the back button we remove the last entry in the host frame’s navigation BackStack:

private static void CompleteNavigation()
{
    lock (typeof(Navigation))
    {
        if (_frame.BackStackDepth > 0)
        {
            _frame.BackStack.RemoveAt(_frame.BackStackDepth - 1);
        }
    }
}

Here’s the full Navigate method:

public static async Task<bool> Navigate(Type sourcePageType)
{
    if (_frame.CurrentSourcePageType == sourcePageType)
    {
        return true;
    }

    await InitiateNavigation(sourcePageType);
    var result = _frame.Navigate(sourcePageType);
    CompleteNavigation();

    return result;
}

Back button logic

The algorithm for navigating backwards is similar: we double check that the eventual target is not that empty page, pay a visit to the empty page and wait a while, and serialize the steps with a lock statement to prevent chaos when the user accidentally double clicks the back button:

public static async Task GoBack()
{
    try
    {
        if (!_frame.CanGoBack)
        {
            return;
        }

        // Just in case there's still a BackGroundPage hanging around.
        lock (typeof(Navigation))
        {
            if (_frame.BackStack[_frame.BackStackDepth - 1].SourcePageType == typeof(BackgroundPage))
            {
                _frame.GoBack();
            }
        }

        if (_frame.CanGoBack)
        {
            var type = _frame.BackStack[_frame.BackStackDepth - 1].SourcePageType;
            await InitiateNavigation(type);
            CompleteNavigation();

            _frame.GoBack();
        }
    }
    catch (Exception ex)
    {
        // Ignore, wild clicking going on.
        Debugger.Break();
    }
}

On top of that, the entire call is wrapped inside a Pokémon exception handler (to catch ‘m all). In a worst case scenario we end up with the wrong color on a page, but at least we never crash the app on a back button click:
Oops_1

Hardware back button processing

The same logic can be reused for processing hardware back button clicks. Just make sure to set the Handled property of the BackPressEventArgs immediately to true, or you just navigate straight out of the app:

public static async Task GoBack(BackPressedEventArgs e)
{
    if (!_frame.CanGoBack)
    {
        // Bail out.
        return;
    }

    // Stay in the app.
    e.Handled = true;
    await GoBack();
}

Source

The sample project lives here on GitHub.

I know that there are some code smells in the implementation and there’s no guarantee that this algorithm will work on all devices. However, “it works on my machine(s)”: I have tested it successfully on all of my devices and it runs it on the simulators and emulators that I have on my development boxes.

I just love how this effect improves the UI of some of my apps and that’s why I’m sharing it.

I may have painted myself in the corner, but at least I did it with an accent color of my choice. 🙂

Enjoy!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s