-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add MaterialColorUtilities.Maui (#19)
* 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
Showing
22 changed files
with
966 additions
and
205 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
168
MaterialColorUtilities.Maui/DynamicColorService.Android.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
23
MaterialColorUtilities.Maui/DynamicColorService.Windows.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.