Unverified Commit 87ac905a authored by Eilidh Southren's avatar Eilidh Southren Committed by GitHub

Add content-based colorScheme functionality (#122919)

* add shadowColor property

* add to bottom app bar

* basic MCU connection demo

* more image tests

* demo update

* remove branch changes

* demo cleanup

* cleanup

* update algo

* working consistent example

* demo cleanup

* Added tests

* fix merge

* rebase

* basic MCU connection demo

* more image tests

* demo update

* remove branch changes

* demo cleanup

* cleanup

* update algo

* working consistent example

* demo cleanup

* Added tests

* lint fixes

* fix theme error

* whitespace fixes

* revert old commit

* update test formatting

* modify image source to external repo

* remove pngs

* add blank line

* fix web tests

* comment responses

* remove whitespace

* whitespace

* whitespace
parent 9fc1fd15
......@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
......@@ -988,4 +991,198 @@ class ColorScheme with Diagnosticable {
properties.add(ColorProperty('secondaryVariant', secondaryVariant, defaultValue: defaultScheme.secondaryVariant));
properties.add(ColorProperty('surfaceTint', surfaceTint, defaultValue: defaultScheme.surfaceTint));
}
/// Generate a [ColorScheme] derived from the given `imageProvider`.
///
/// Material Color Utilities extracts the dominant color from the
/// supplied [ImageProvider]. Using this color, a set of tonal palettes are
/// constructed. These tonal palettes are based on the Material 3 Color
/// system and provide all the needed colors for a [ColorScheme]. These
/// colors are designed to work well together and meet contrast
/// requirements for accessibility.
///
/// If any of the optional color parameters are non-null, they will be
/// used in place of the generated colors for that field in the resulting
/// color scheme. This allows apps to override specific colors for their
/// needs.
///
/// Given the nature of the algorithm, the most dominant color of the
/// `imageProvider` may not wind up as one of the ColorScheme colors.
///
/// The provided image will be scaled down to a maximum size of 112x112 pixels
/// during color extraction.
///
/// See also:
///
/// * <https://m3.material.io/styles/color/the-color-system/color-roles>, the
/// Material 3 Color system specification.
/// * <https://pub.dev/packages/material_color_utilities>, the package
/// used to generate the base color and tonal palettes needed for the scheme.
static Future<ColorScheme> fromImageProvider({
required ImageProvider provider,
Brightness brightness = Brightness.light,
Color? primary,
Color? onPrimary,
Color? primaryContainer,
Color? onPrimaryContainer,
Color? secondary,
Color? onSecondary,
Color? secondaryContainer,
Color? onSecondaryContainer,
Color? tertiary,
Color? onTertiary,
Color? tertiaryContainer,
Color? onTertiaryContainer,
Color? error,
Color? onError,
Color? errorContainer,
Color? onErrorContainer,
Color? outline,
Color? outlineVariant,
Color? background,
Color? onBackground,
Color? surface,
Color? onSurface,
Color? surfaceVariant,
Color? onSurfaceVariant,
Color? inverseSurface,
Color? onInverseSurface,
Color? inversePrimary,
Color? shadow,
Color? scrim,
Color? surfaceTint,
}) async {
// Extract dominant colors from image.
final QuantizerResult quantizerResult =
await _extractColorsFromImageProvider(provider);
final Map<int, int> colorToCount = quantizerResult.colorToCount.map(
(int key, int value) => MapEntry<int, int>(_getArgbFromAbgr(key), value),
);
// Score colors for color scheme suitability.
final List<int> scoredResults = Score.score(colorToCount, desired: 1);
final ui.Color baseColor = Color(scoredResults.first);
final Scheme scheme;
switch (brightness) {
case Brightness.light:
scheme = Scheme.light(baseColor.value);
break;
case Brightness.dark:
scheme = Scheme.dark(baseColor.value);
break;
}
return ColorScheme(primary: primary ?? Color(scheme.primary),
onPrimary: onPrimary ?? Color(scheme.onPrimary),
primaryContainer: primaryContainer ?? Color(scheme.primaryContainer),
onPrimaryContainer: onPrimaryContainer ?? Color(scheme.onPrimaryContainer),
secondary: secondary ?? Color(scheme.secondary),
onSecondary: onSecondary ?? Color(scheme.onSecondary),
secondaryContainer: secondaryContainer ?? Color(scheme.secondaryContainer),
onSecondaryContainer: onSecondaryContainer ?? Color(scheme.onSecondaryContainer),
tertiary: tertiary ?? Color(scheme.tertiary),
onTertiary: onTertiary ?? Color(scheme.onTertiary),
tertiaryContainer: tertiaryContainer ?? Color(scheme.tertiaryContainer),
onTertiaryContainer: onTertiaryContainer ?? Color(scheme.onTertiaryContainer),
error: error ?? Color(scheme.error),
onError: onError ?? Color(scheme.onError),
errorContainer: errorContainer ?? Color(scheme.errorContainer),
onErrorContainer: onErrorContainer ?? Color(scheme.onErrorContainer),
outline: outline ?? Color(scheme.outline),
outlineVariant: outlineVariant ?? Color(scheme.outlineVariant),
background: background ?? Color(scheme.background),
onBackground: onBackground ?? Color(scheme.onBackground),
surface: surface ?? Color(scheme.surface),
onSurface: onSurface ?? Color(scheme.onSurface),
surfaceVariant: surfaceVariant ?? Color(scheme.surfaceVariant),
onSurfaceVariant: onSurfaceVariant ?? Color(scheme.onSurfaceVariant),
inverseSurface: inverseSurface ?? Color(scheme.inverseSurface),
onInverseSurface: onInverseSurface ?? Color(scheme.inverseOnSurface),
inversePrimary: inversePrimary ?? Color(scheme.inversePrimary),
shadow: shadow ?? Color(scheme.shadow),
scrim: scrim ?? Color(scheme.scrim),
surfaceTint: surfaceTint ?? Color(scheme.primary),
brightness: brightness,
);
}
// ColorScheme.fromImageProvider() utilities.
// Extracts bytes from an [ImageProvider] and returns a [QuantizerResult]
// containing the most dominant colors.
static Future<QuantizerResult> _extractColorsFromImageProvider(ImageProvider imageProvider) async {
final ui.Image scaledImage = await _imageProviderToScaled(imageProvider);
final ByteData? imageBytes = await scaledImage.toByteData();
final QuantizerResult quantizerResult = await QuantizerCelebi().quantize(
imageBytes!.buffer.asUint32List(),
128,
returnInputPixelToClusterPixel: true,
);
return quantizerResult;
}
// Scale image size down to reduce computation time of color extraction.
static Future<ui.Image> _imageProviderToScaled(ImageProvider imageProvider) async {
const double maxDimension = 112.0;
final ImageStream stream = imageProvider.resolve(
const ImageConfiguration(size: Size(maxDimension, maxDimension)));
final Completer<ui.Image> imageCompleter = Completer<ui.Image>();
late ImageStreamListener listener;
late ui.Image scaledImage;
Timer? loadFailureTimeout;
listener = ImageStreamListener((ImageInfo info, bool sync) async {
loadFailureTimeout?.cancel();
stream.removeListener(listener);
final ui.Image image = info.image;
final int width = image.width;
final int height = image.height;
double paintWidth = width.toDouble();
double paintHeight = height.toDouble();
assert(width > 0 && height > 0);
final bool rescale = width > maxDimension || height > maxDimension;
if (rescale) {
paintWidth = (width > height) ? maxDimension : (maxDimension / height) * width;
paintHeight = (height > width) ? maxDimension : (maxDimension / width) * height;
}
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
final Canvas canvas = Canvas(pictureRecorder);
paintImage(
canvas: canvas,
rect: Rect.fromLTRB(0, 0, paintWidth, paintHeight),
image: image,
filterQuality: FilterQuality.none);
final ui.Picture picture = pictureRecorder.endRecording();
scaledImage = await picture.toImage(paintWidth.toInt(), paintHeight.toInt());
imageCompleter.complete(info.image);
}, onError: (Object exception, StackTrace? stackTrace) {
stream.removeListener(listener);
throw Exception('Failed to render image: $exception');
});
loadFailureTimeout = Timer(const Duration(seconds: 5), () {
stream.removeListener(listener);
imageCompleter.completeError(
TimeoutException('Timeout occurred trying to load image'));
});
stream.addListener(listener);
await imageCompleter.future;
return scaledImage;
}
// Converts AABBGGRR color int to AARRGGBB format.
static int _getArgbFromAbgr(int abgr) {
const int exceptRMask = 0xFF00FFFF;
const int onlyRMask = ~exceptRMask;
const int exceptBMask = 0xFFFFFF00;
const int onlyBMask = ~exceptBMask;
final int r = (abgr & onlyRMask) >> 16;
final int b = abgr & onlyBMask;
return (abgr & exceptRMask & exceptBMask) | (b << 16) | r;
}
}
......@@ -2,9 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../image_data.dart';
void main() {
test('ColorScheme lerp special cases', () {
const ColorScheme scheme = ColorScheme.light();
......@@ -364,6 +368,104 @@ void main() {
expect(scheme.brightness, baseScheme.brightness);
});
test('can generate a light scheme from an imageProvider', () async {
final Uint8List blueSquareBytes = Uint8List.fromList(kBlueSquarePng);
final ImageProvider image = MemoryImage(blueSquareBytes);
final ColorScheme scheme =
await ColorScheme.fromImageProvider(provider: image);
expect(scheme.brightness, Brightness.light);
expect(scheme.primary, const Color(0xff4040f3));
expect(scheme.onPrimary, const Color(0xffffffff));
expect(scheme.primaryContainer, const Color(0xffe1e0ff));
expect(scheme.onPrimaryContainer, const Color(0xff06006c));
expect(scheme.secondary, const Color(0xff5d5c72));
expect(scheme.onSecondary, const Color(0xffffffff));
expect(scheme.secondaryContainer, const Color(0xffe2e0f9));
expect(scheme.onSecondaryContainer, const Color(0xff191a2c));
expect(scheme.tertiary, const Color(0xff79536a));
expect(scheme.onTertiary, const Color(0xffffffff));
expect(scheme.tertiaryContainer, const Color(0xffffd8ec));
expect(scheme.onTertiaryContainer, const Color(0xff2e1125));
expect(scheme.error, const Color(0xffba1a1a));
expect(scheme.onError, const Color(0xffffffff));
expect(scheme.errorContainer, const Color(0xffffdad6));
expect(scheme.onErrorContainer, const Color(0xff410002));
expect(scheme.background, const Color(0xfffffbff));
expect(scheme.onBackground, const Color(0xff1c1b1f));
expect(scheme.surface, const Color(0xfffffbff));
expect(scheme.onSurface, const Color(0xff1c1b1f));
expect(scheme.surfaceVariant, const Color(0xffe4e1ec));
expect(scheme.onSurfaceVariant, const Color(0xff46464f));
expect(scheme.outline, const Color(0xff777680));
expect(scheme.outlineVariant, const Color(0xffc8c5d0));
expect(scheme.shadow, const Color(0xff000000));
expect(scheme.scrim, const Color(0xff000000));
expect(scheme.inverseSurface, const Color(0xff313034));
expect(scheme.onInverseSurface, const Color(0xfff3eff4));
expect(scheme.inversePrimary, const Color(0xffc0c1ff));
expect(scheme.surfaceTint, const Color(0xff4040f3));
expect(scheme.primaryVariant, const Color(0xff4040f3));
expect(scheme.secondaryVariant, const Color(0xff5d5c72));
}, skip: isBrowser, // [intended] uses dart:typed_data.
);
test('can generate a dark scheme from an imageProvider', () async {
final Uint8List blueSquareBytes = Uint8List.fromList(kBlueSquarePng);
final ImageProvider image = MemoryImage(blueSquareBytes);
final ColorScheme scheme = await ColorScheme.fromImageProvider(
provider: image, brightness: Brightness.dark);
expect(scheme.primary, const Color(0xffc0c1ff));
expect(scheme.onPrimary, const Color(0xff0f00aa));
expect(scheme.primaryContainer, const Color(0xff2218dd));
expect(scheme.onPrimaryContainer, const Color(0xffe1e0ff));
expect(scheme.secondary, const Color(0xffc6c4dd));
expect(scheme.onSecondary, const Color(0xff2e2f42));
expect(scheme.secondaryContainer, const Color(0xff454559));
expect(scheme.onSecondaryContainer, const Color(0xffe2e0f9));
expect(scheme.tertiary, const Color(0xffe9b9d3));
expect(scheme.onTertiary, const Color(0xff46263a));
expect(scheme.tertiaryContainer, const Color(0xff5f3c51));
expect(scheme.onTertiaryContainer, const Color(0xffffd8ec));
expect(scheme.error, const Color(0xffffb4ab));
expect(scheme.onError, const Color(0xff690005));
expect(scheme.errorContainer, const Color(0xff93000a));
expect(scheme.onErrorContainer, const Color(0xffffb4ab));
expect(scheme.background, const Color(0xff1c1b1f));
expect(scheme.onBackground, const Color(0xffe5e1e6));
expect(scheme.surface, const Color(0xff1c1b1f));
expect(scheme.onSurface, const Color(0xffe5e1e6));
expect(scheme.surfaceVariant, const Color(0xff46464f));
expect(scheme.onSurfaceVariant, const Color(0xffc8c5d0));
expect(scheme.outline, const Color(0xff918f9a));
expect(scheme.outlineVariant, const Color(0xff46464f));
expect(scheme.inverseSurface, const Color(0xffe5e1e6));
expect(scheme.onInverseSurface, const Color(0xff313034));
expect(scheme.inversePrimary, const Color(0xff4040f3));
expect(scheme.primaryVariant, const Color(0xffc0c1ff));
expect(scheme.secondaryVariant, const Color(0xffc6c4dd));
expect(scheme.surfaceTint, const Color(0xffc0c1ff));
}, skip: isBrowser, // [intended] uses dart:isolate and io.
);
test('fromImageProvider() propogates TimeoutException when image cannot be rendered', () async {
final Uint8List blueSquareBytes = Uint8List.fromList(kBlueSquarePng);
// Corrupt the image's bytelist so it cannot be read.
final Uint8List corruptImage = blueSquareBytes.sublist(5);
final ImageProvider image = MemoryImage(corruptImage);
expect(() async => ColorScheme.fromImageProvider(provider: image), throwsA(
isA<Exception>().having((Exception e) => e.toString(),
'Timeout occurred trying to load image', contains('TimeoutException')),
),
);
});
testWidgets('generated scheme "on" colors meet a11y contrast guidelines', (WidgetTester tester) async {
final ColorScheme colors = ColorScheme.fromSeed(seedColor: Colors.teal);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment