Skip to content

Commit

Permalink
Add MaterialColorUtilities.Maui (#19)
Browse files Browse the repository at this point in the history
* Add basic library code and windows implementation, update playground

* Add Android implementation

* Add seed color guessing on Android 12

* Improve Android code

* Add UseDynamicColor option, refactor initialization

* Add Nuget stuff

* Add Mac implementation

* Update README

* Make everything generic

* Fix trimming errors, follow dark mode preference

* Small changes

* Move shared logic into core file
* Use Application.RequestedTheme instead of AppInfo.RequestedTheme, so the theme can be set using Application.UserAppTheme
* Remove _isInitialized as it is not needed

* Make IsDark public

* Update README
  • Loading branch information
albi005 authored Jul 13, 2022
1 parent 882fc84 commit 63b6f6a
Show file tree
Hide file tree
Showing 22 changed files with 966 additions and 205 deletions.
18 changes: 17 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
[*.cs]
root = true

[*.cs]

# IDE0160: Convert to block scoped namespace
csharp_style_namespace_declarations = file_scoped:silent
Expand Down Expand Up @@ -83,3 +85,17 @@ dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_namespace_match_folder = true:suggestion

# internal and private fields should be _camelCase
dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style

dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal

dotnet_naming_style.camel_case_underscore_style.required_prefix = _
dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case

[*]
charset = utf-8-bom
18 changes: 18 additions & 0 deletions MaterialColorUtilities.Maui/DynamicColorOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace MaterialColorUtilities.Maui;

public class DynamicColorOptions
{
/// <summary>
/// Will be used if a dynamic accent color is not available.
/// </summary>
public int FallbackSeed { get; set; } = unchecked((int)0xff4285F4); // Google Blue

/// <summary>
/// Whether to use wallpaper/accent color based dynamic theming.
/// </summary>
/// <remarks>
/// When set to <see langword="false"/>, <see cref="FallbackSeed"/> will be used as seed,
/// even on platforms that expose an accent color.
/// </remarks>
public bool UseDynamicColor { get; set; } = true;
}
168 changes: 168 additions & 0 deletions MaterialColorUtilities.Maui/DynamicColorService.Android.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
using Android.App;
using Android.Graphics;
using Android.Graphics.Drawables;
using MaterialColorUtilities.ColorAppearance;
using MaterialColorUtilities.Utils;
using Microsoft.Maui.LifecycleEvents;
using System.Runtime.Versioning;

namespace MaterialColorUtilities.Maui;

public partial class DynamicColorService<TCorePalette, TSchemeInt, TSchemeMaui, TLightSchemeMapper, TDarkSchemeMapper>
{
private int _prevSeedSource = -1;
private readonly WallpaperManager _wallpaperManager = WallpaperManager.GetInstance(Platform.AppContext);

partial void PlatformInitialize()
{
if (_wallpaperManager == null) return;
if (OperatingSystem.IsAndroidVersionAtLeast(31))
{
SetFromAndroid12AccentColors();
_lifecycleEventService.AddAndroid(android
=> android.OnResume(_
=> MainThread.BeginInvokeOnMainThread(
#pragma warning disable CA1416
SetFromAndroid12AccentColors
#pragma warning restore CA1416
)));
}
else if (OperatingSystem.IsAndroidVersionAtLeast(27))
{
SetFromAndroid8PrimaryWallpaperColor();
_wallpaperManager.ColorsChanged += (sender, args) =>
{
if (args.Which == (int)WallpaperManagerFlags.Lock) return;

MainThread.BeginInvokeOnMainThread(
#pragma warning disable CA1416
SetFromAndroid8PrimaryWallpaperColor
#pragma warning restore CA1416
);
};
}
else
_ = TrySetFromQuantizedWallpaperColors();
}

[SupportedOSPlatform("android31.0")]
public void SetFromAndroid12AccentColors()
{
// We have access to the basic tones like 0, 10, 20 etc. of every tonal palette,
// but if a different tone is required, we need access to the seed color.
// Android doesn't seem to expose the seed color, so we have to get creative to get it.

// We will use the tone of the primary color with the highest chroma as the seed,
// because it has the same hue as the actual seed and its chroma will be close enough.
int[] primaryIds =
{
Android.Resource.Color.SystemAccent1500,
Android.Resource.Color.SystemAccent110,
Android.Resource.Color.SystemAccent150,
Android.Resource.Color.SystemAccent1100,
Android.Resource.Color.SystemAccent1200,
Android.Resource.Color.SystemAccent1300,
Android.Resource.Color.SystemAccent1400,
Android.Resource.Color.SystemAccent1600,
Android.Resource.Color.SystemAccent1700,
Android.Resource.Color.SystemAccent1800,
Android.Resource.Color.SystemAccent1900,
};
double maxChroma = -1;
int closestColor = 0;
foreach (int id in primaryIds)
{
int color = Platform.AppContext.Resources.GetColor(id, null);

if (id == Android.Resource.Color.SystemAccent1500)
{
// If Primary50 didn't change, return
if (color == _prevSeedSource) return;
_prevSeedSource = color;
}

Hct hct = Hct.FromInt(color);
if (hct.Chroma > maxChroma)
{
maxChroma = hct.Chroma;
closestColor = color;
}
}

SetSeed(closestColor);
}

[SupportedOSPlatform("android27.0")]
public void SetFromAndroid8PrimaryWallpaperColor()
{
int color = _wallpaperManager.GetWallpaperColors((int)WallpaperManagerFlags.System).PrimaryColor.ToArgb();
SetSeed(color);
}

/// <summary>
/// Tries to set colors by using Material Color Utilities to get colors from the system wallpaper.
/// </summary>
/// <returns><see langword="true"/> if the operation completed successfully, <see langword="false"/> otherwise.</returns>
/// <remarks>Requires <see cref="Permissions.StorageRead"/></remarks>
public async Task<bool> TrySetFromQuantizedWallpaperColors()
{
List<int> colors = await Task.Run(async () =>
{
int[] pixels = await GetWallpaperPixels();
if (pixels == null) return null;
return ImageUtils.ColorsFromImage(pixels);
});
if (colors == null) return false;

SetSeed(colors.First());

return true;
}

private async Task<int[]> GetWallpaperPixels()
{
if (_wallpaperManager == null) return null;

if (OperatingSystem.IsAndroidVersionAtLeast(24))
{
int wallpaperId = _wallpaperManager.GetWallpaperId(WallpaperManagerFlags.System);
if (_prevSeedSource == wallpaperId) return null;
_prevSeedSource = wallpaperId;
}

// Need permission to read wallpaper
if ((await Permissions.CheckStatusAsync<Permissions.StorageRead>()) != PermissionStatus.Granted)
return null;

Drawable drawable = _wallpaperManager.Drawable;
if (drawable is not BitmapDrawable bitmapDrawable) return null;
Bitmap bitmap = bitmapDrawable.Bitmap;
if (bitmap.Height * bitmap.Width > 112 * 112)
{
Android.Util.Size optimalSize = CalculateOptimalSize(bitmap.Width, bitmap.Height);
bitmap = Bitmap.CreateScaledBitmap(bitmap, optimalSize.Width, optimalSize.Height, false);
}
int[] pixels = new int[bitmap.ByteCount / 4];
bitmap.GetPixels(pixels, 0, bitmap.Width, 0, 0, bitmap.Width, bitmap.Height);

return pixels;
}

// From https://cs.android.com/android/platform/superproject/+/384d0423f9e93790e76399a5291731f6cfea40e8:frameworks/base/core/java/android/app/WallpaperColors.java
private static Android.Util.Size CalculateOptimalSize(int width, int height)
{
int requestedArea = width * height;
double scale = 1;
if (requestedArea > 112 * 112)
scale = Math.Sqrt(112 * 112 / (double)requestedArea);
int newWidth = (int)(width * scale);
int newHeight = (int)(height * scale);

if (newWidth == 0)
newWidth = 1;
if (newHeight == 0)
newHeight = 1;

return new Android.Util.Size(newWidth, newHeight);
}
}
40 changes: 40 additions & 0 deletions MaterialColorUtilities.Maui/DynamicColorService.Mac.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Foundation;
using MaterialColorUtilities.Utils;
using System.Runtime.InteropServices;
using UIKit;

namespace MaterialColorUtilities.Maui;

// NSColor.controlAccentColor is not available because .NET MAUI uses Mac Catalyst,
// so we have to rely on a workaround to get the accent color and subscribe to its changes.
partial class DynamicColorService<TCorePalette, TSchemeInt, TSchemeMaui, TLightSchemeMapper, TDarkSchemeMapper>
{
private readonly UIButton _dummy = new();

partial void PlatformInitialize()
{
SetFromAccentColor();

// based on https://gist.github.com/JunyuKuang/3ecc7c9374c0ba67438c9a6d06612e36
NSNotificationCenter.DefaultCenter.AddObserver(
(NSString)"NSSystemColorsDidChangeNotification",
_ => MainThread.BeginInvokeOnMainThread(SetFromAccentColor),
null);
}

// https://twitter.com/steipete/status/1186262035543273472
private void SetFromAccentColor()
{
UIColor accentColor = _dummy.TintColor;
accentColor.GetRGBA(
out NFloat r,
out NFloat g,
out NFloat b,
out NFloat _);
int argb = ColorUtils.ArgbFromRgb(
(int)(r * 255),
(int)(g * 255),
(int)(b * 255));
SetSeed(argb);
}
}
23 changes: 23 additions & 0 deletions MaterialColorUtilities.Maui/DynamicColorService.Windows.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using MaterialColorUtilities.Utils;
using Windows.UI.ViewManagement;

namespace MaterialColorUtilities.Maui;

public partial class DynamicColorService<TCorePalette, TSchemeInt, TSchemeMaui, TLightSchemeMapper, TDarkSchemeMapper>
{
private readonly UISettings _uiSettings = new();

partial void PlatformInitialize()
{
SetSeed(GetAccentColor());
_uiSettings.ColorValuesChanged += (_, _)
=> MainThread.BeginInvokeOnMainThread(()
=> SetSeed(GetAccentColor()));
}

private int GetAccentColor()
{
Windows.UI.Color color = _uiSettings.GetColorValue(UIColorType.Accent);
return ColorUtils.ArgbFromRgb(color.R, color.G, color.B);
}
}
Loading

0 comments on commit 63b6f6a

Please sign in to comment.