- You could make all your images color-theme agnostic, but having to compromise on your app's UI is far from ideal.
-
Throw it over the wall to a designer, get them to create all image assets in every required color, and then use some naming convention to select the correct images at runtime? What if you want to remain on good terms with the designer, or you're not fully set on the color themes themselves, or want to enable users to pick their own colors?
-
You could use a custom font file for iconography, and then each device renders an image in the correct color on the fly and uses the resulting image file. This is a decent option and there's plenty of guides on doing this out there, but it can get complicated with regards to getting the right resolutions for the device's pixel density. Composite images like a calendar with a day number become a royal pain because you need to render multiple images and overlay them.
-
Use one set of image assets, and recolor the images on the fly, while letting the underlying SDK pick the correct image size for the device. Viable if your images are a single color on a transparent background.
-
Give up and reconsider your life choices.

Starting off
interface IColorTransformService { FileImageSource TransformFileImageSource(FileImageSource source, Color outputColor); } public interface IPlatformResourceImageResolver { Stream GetImageData(FileImageSource source); }
Getting the image data
class DroidImageResolver : IPlatformResourceImageResolver { private readonly Context _context; public DroidImageResolver(Context context) { _context = context; } public Stream GetImageData(FileImageSource source) { string file = source?.File; if (string.IsNullOrWhiteSpace(file)) throw new ArgumentException($"Expected a file image source, but no file was specified"); var imageId = _context.Resources.GetIdentifier(file, "drawable", _context.PackageName); using (var drawable = _context.GetDrawable(imageId)) { Bitmap bitmap = ((BitmapDrawable) drawable).Bitmap; Stream memoryStream = new MemoryStream(); bitmap.Compress(Bitmap.CompressFormat.Png, quality: 100, stream: memoryStream); memoryStream.Seek(0, SeekOrigin.Begin); return memoryStream; } } }
class iOSImageResolver : IPlatformResourceImageResolver { public Stream GetImageData(FileImageSource source) { var filesource = source; var file = filesource?.File; if (!string.IsNullOrEmpty(file)) { var image = File.Exists(file) ? new UIImage(file) : UIImage.FromBundle(file); return image.AsPNG().AsStream(); } throw new ArgumentException("Image file did not exist"); } }
Converting the image colors
using (Stream sourceImageStream = _platformImageResolver.GetImageData(fileImageSource)) using (SKBitmap sourceBitmap = SKBitmap.Decode(sourceImageStream)) { SKImageInfo info = new SKImageInfo( sourceBitmap.Width, sourceBitmap.Height, sourceBitmap.ColorType, sourceBitmap.AlphaType, sourceBitmap.ColorSpace); using (SKBitmap outputBitmap = new SKBitmap(info)) using (SKCanvas canvas = new SKCanvas(outputBitmap)) { // Draw to the canvas } }
using (Stream sourceImageStream = _platformImageResolver.GetImageData(fileImageSource)) using (SKBitmap sourceBitmap = SKBitmap.Decode(sourceImageStream)) { SKImageInfo info = new SKImageInfo( sourceBitmap.Width, sourceBitmap.Height, sourceBitmap.ColorType, sourceBitmap.AlphaType, sourceBitmap.ColorSpace); using (SKBitmap outputBitmap = new SKBitmap(info)) using (SKCanvas canvas = new SKCanvas(outputBitmap)) using (SKPaint transformationBrush = new SKPaint()) { canvas.DrawColor(SKColors.Transparent); SKColor targetColor = outputColor.ToSKColor(); var tableRed = new byte[256]; var tableGreen = new byte[256]; var tableBlue = new byte[256]; // We expect the icon to be a single color on a transparent background for (int i = 0; i < 256; i++) { tableRed[i] = targetColor.Red; tableGreen[i] = targetColor.Green; tableBlue[i] = targetColor.Blue; } transformationBrush.ColorFilter = SKColorFilter.CreateTable(null, tableRed, tableGreen, tableBlue); canvas.DrawBitmap( sourceBitmap, info.Rect /* Draw the whole image, not a subset */, transformationBrush); using (var image = SKImage.FromBitmap(outputBitmap)) using (var data = image.Encode(SKEncodedImageFormat.Png, 100)) using (var stream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write)) data.SaveTo(stream); } }
class ColorTransformService : IColorTransformService { private readonly IFileSystem _filesystem; private readonly IPlatformResourceImageResolver _resolver; private readonly TransformedImageCache _cache = new TransformedImageCache(); public ColorTransformService(IFileSystem filesystem, IPlatformResourceImageResolver resolver) { _filesystem = filesystem ?? throw new ArgumentNullException(nameof(filesystem)); _resolver = resolver ?? throw new ArgumentNullException(nameof(resolver)); } public FileImageSource TransformFileImageSource(FileImageSource source, Color outputColor) { string transformedFile = null; if (_cache.TryGetValue(source.File, outputColor, out transformedFile)) { return (FileImageSource)ImageSource.FromFile(transformedFile); } transformedFile = _filesystem.GetTempImageFilePathForCurrentPixelDensity(); using (Stream sourceImageStream = _resolver.GetImageData(source)) using (SKBitmap sourceBitmap = SKBitmap.Decode(sourceImageStream)) { SKImageInfo info = new SKImageInfo( sourceBitmap.Width, sourceBitmap.Height, sourceBitmap.ColorType, sourceBitmap.AlphaType, sourceBitmap.ColorSpace); using (SKBitmap outputBitmap = new SKBitmap(info)) using (SKCanvas canvas = new SKCanvas(outputBitmap)) using (SKPaint transformationBrush = new SKPaint()) { canvas.DrawColor(SKColors.Transparent); var targetColor = outputColor.ToSKColor(); var tableRed = new byte[256]; var tableGreen = new byte[256]; var tableBlue = new byte[256]; for (int i = 0; i < 256; i++) { tableRed[i] = targetColor.Red; tableGreen[i] = targetColor.Green; tableBlue[i] = targetColor.Blue; } // Alpha channel remains unchanged transformationBrush.ColorFilter = SKColorFilter.CreateTable(null, tableRed, tableGreen, tableBlue); canvas.DrawBitmap(sourceBitmap, info.Rect, transformationBrush); using (var image = SKImage.FromBitmap(outputBitmap)) using (SKData data = image.Encode(SKEncodedImageFormat.Png, 100)) using (var stream = new FileStream(transformedFile, FileMode.Create, FileAccess.Write)) data.SaveTo(stream); } } _cache.Add(source.File, outputColor, transformedFile); return (FileImageSource) ImageSource.FromFile(transformedFile); } private class TransformedImageCache { private readonly Dictionary<CacheKey, string> _cachedImagesFiles = new Dictionary<CacheKey, string>(); private class CacheKey : IEquatable { public CacheKey(string sourceImageName, Color outputColor) { SourceImageName = sourceImageName; OutputColor = outputColor; } public string SourceImageName { get; private set; } public Color OutputColor { get; private set; } public bool Equals(CacheKey other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return string.Equals(SourceImageName, other.SourceImageName) && OutputColor.Equals(other.OutputColor); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != this.GetType()) return false; return Equals((CacheKey) obj); } public override int GetHashCode() { unchecked { return ((SourceImageName != null ? SourceImageName.GetHashCode() : 0) * 397) ^ OutputColor.GetHashCode(); } } } public bool TryGetValue(string sourceFile, Color outputColor, out string transformedFile) { var key = new CacheKey(sourceFile, outputColor); return _cachedImagesFiles.TryGetValue(key, out transformedFile); } public void Add(string sourceFile, Color outputColor, string transformedFile) { var key = new CacheKey(sourceFile, outputColor); _cachedImagesFiles[key] = transformedFile; } } }
Putting it to use
class ColorTransformImage : Image { private IColorTransformService _transformer; private IColorTransformService Transformer { get { return _transformer ?? (_transformer = YourServiceLocator.Resolve()); } } public static readonly BindableProperty SourceImageProperty = BindableProperty.Create(nameof(SourceImage), typeof(FileImageSource), typeof(ColorTransformImage), null, propertyChanged: OnSourceImagePropertyChanged); public static readonly BindableProperty SourceImageColorProperty = BindableProperty.Create(nameof(SourceImageColor), typeof(Color), typeof(ColorTransformImage), Color.Default, propertyChanged: OnSourceImageColorPropertyChanged); public static readonly BindableProperty TargetTintColorProperty = BindableProperty.Create(nameof(TargetTintColor), typeof(Color), typeof(ColorTransformImage), Color.Default, propertyChanged: OnTargetTintColorPropertyChanged); public ImageSource SourceImage { get => (ImageSource)GetValue(SourceImageProperty); set => SetValue(SourceImageProperty, value); } public Color SourceImageColor { get => (Color)GetValue(SourceImageColorProperty); set => SetValue(SourceImageColorProperty, value); } public Color TargetTintColor { get => (Color)GetValue(TargetTintColorProperty); set => SetValue(TargetTintColorProperty, value); } private static void OnSourceImagePropertyChanged(BindableObject bindable, object oldvalue, object newvalue) { ColorTransformImage button = (ColorTransformImage)bindable; if (CanTransformSourceImage(button)) button.TransformSourceImage(); } private static void OnSourceImageColorPropertyChanged(BindableObject bindable, object oldvalue, object newvalue) { ColorTransformImage button = (ColorTransformImage)bindable; if (CanTransformSourceImage(button)) button.TransformSourceImage(); } private static void OnTargetTintColorPropertyChanged(BindableObject bindable, object oldvalue, object newvalue) { ColorTransformImage button = (ColorTransformImage)bindable; if (CanTransformSourceImage(button)) button.TransformSourceImage(); } private static bool CanTransformSourceImage(ColorTransformImage button) { return button.SourceImage != null; } private void TransformSourceImage() { var imageSource = (FileImageSource)SourceImage; if (SourceImageColor == TargetTintColor || !IsValidForTransformation(TargetTintColor)) { Source = imageSource; return; } this.Source = Transformer.TransformFileImageSource(imageSource, this.TargetTintColor); } public bool IsValidForTransformation(Color color) { // Color.Default has negative values for R,G,B,A return color.R >= 0 && color.G >= 0 && color.B >= 0; } }
- Platform-specific implementations for loading image assets
- Some platform specifics for file system access
- Cross-platform image processing with SkiaSharp
- Cross-platform control which leverages the default image renderers
- Simple caching to minimize the performance hit