ユーザーの選んだ画像からFlutterアプリのテーマカラーを選ぶ
はじめに
Dynamic Colorを知っていますか?
Dynamic Color(ダイナミックカラー)は、Googleが提案するデザインフレームワークであるMaterial Designの一部です。ダイナミックカラーは、アプリケーションのカラースキームをユーザーの好みやデバイスの設定に合わせて動的に変化させる機能です。これにより、ユーザーが個別の好みや条件に応じて最適なカラースキームを選択できるようになります。by GPT4
この記事ではDynamic Colorの仕組みを使って、画像から色を抽出しFlutterアプリのテーマカラーに反映する方法を紹介します。
最終的に以下のようなアプリが作れます。
material_color_utilitiesパッケージ
Dynamic Colorの仕組みは複雑ですが、material_color_utilitiesパッケージにアルゴリズムの実装があるため、こちらを使うことで簡単に画像から色の抽出ができます。
ImageUtilsを実装
まずはmaterial_color_utilitiesを使って画像から色を抽出する実装をします。
import 'package:flutter/material.dart' as material;
import 'package:image/image.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
import 'package:material_color_utilities/utils/color_utils.dart';
class ImageUtils {
/// 上位4つの色を返す
static Future<List<material.Color>> sourceColorsFromImage(Image image) async {
// 処理を軽くするためにリサイズ(当然結果が変わるので注意)
final resizedImage = copyResize(image, width: 512, height: 512);
// ピクセルごとのargb形式のintのリスト
List<int> pixels = [];
for (int y = 0; y < resizedImage.height; y++) {
for (int x = 0; x < resizedImage.width; x++) {
// 画像のピクセルの色を取得
final pixel = resizedImage.getPixel(x, y);
pixels.add(
ColorUtils.argbFromRgb(
pixel.r.toInt(),
pixel.g.toInt(),
pixel.b.toInt(),
),
);
}
}
// セレビィさん考案のアルゴリズムで量子化
final quantizerResult = await QuantizerCelebi().quantize(pixels, 128);
// 量子化した色のリストをスコアリング
final ranked = Score.score(quantizerResult.colorToCount);
// スコアの高い4色を返す
return ranked
.take(4)
.map(
(colorInt) => material.Color(colorInt),
)
.toList();
}
}
簡単な解説
やってることはコメントの通りなのですが、一応簡単な解説です。
sourceColorsFromImageは画像を引数にとって、抽出した上位4色を返すメソッドです。
以下の処理をしています。
- copyResizeは結構処理が重かったので、画像サイズを小さくしています。
- 画像をピクセルごとにargb形式のintにしたListを作る
- QuantizerCelebiクラスのquantizeを使って色数を絞る
- Scoreクラスのscoreを使って上位を色を決める
- 上位4つの色を返す
補足
material_color_utilitiesのReadmeにも書いてあるのですが、端末で設定された色を使いたいだけなら以下のパッケージがあります。
補足2
material_color_utilitiesはflutterの以下の部分で使われてたりもします。SeedColorからSchemeを作るところですね👀
実際のアプリに反映する
先程作ったImageUtilsを実際のアプリで使ってみます。冒頭の動画のアプリです。
すべてのコードを貼ると長いので一部を抜粋します。
Notifierの実装
画像と色をまとめてモデルのリストを持つNotifierProviderの実装です。
色抽出処理は重いので、computeを使って別スレッドで行っています。
class ImageAndColorsList extends _$ImageAndColorsList {
Future<List<ImageAndColors>> build() {
final defaultImageAssets = [
'assets/image_1.jpg',
'assets/image_2.jpg',
'assets/image_3.jpg',
'assets/image_4.jpg',
];
final defaultImages = Future.wait(
defaultImageAssets.map((asset) async {
final bytes = await rootBundle.load(asset);
final decoded = image.decodeImage(bytes.buffer.asUint8List());
// 処理が重いので別スレッドで実行
final colors =
await compute(ImageUtils.sourceColorsFromImage, decoded!);
return ImageAndColors(
image: AssetImage(asset),
colors: colors,
);
}),
);
return defaultImages;
}
Future<void> addImage(XFile imageFile) async {
final file = File(imageFile.path);
final bytes = await file.readAsBytes();
final decoded = image.decodeImage(bytes);
// 処理が重いので別スレッドで実行
final colors = await compute(ImageUtils.sourceColorsFromImage, decoded!);
update(
(current) => [
...current,
ImageAndColors(
image: FileImage(file),
colors: colors,
),
],
);
}
}
MaterialAppの実装
MaterialAppの実装です。
colorSchemeSeedをNotifierのStateをwatchするようにすることで、アプリのテーマカラーを動的に変更可能にしています。
class ColorSchemeSeed extends _$ColorSchemeSeed {
Color build() {
return Colors.blue;
}
void changeColor(Color color) {
state = color;
}
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
title: 'Material Color Utilities Demo',
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: ref.watch(colorSchemeSeedProvider),
),
darkTheme: ThemeData(
brightness: Brightness.dark,
useMaterial3: true,
colorSchemeSeed: ref.watch(colorSchemeSeedProvider),
),
themeMode: ref.watch(themeModeStateProvider),
home: const GalleryPage(),
);
}
}
まとめ
画像の色を抽出してアプリのテーマカラーを変える方法の紹介でした。
テーマカラー以外にもUIの一部をコンテンツに合った色に変えたり出来るので、気に入ったら是非試してみたください。
以下は今回のサンプルアプリの実装です。⭐してもらえると嬉しいです🥳
おすすめ
Dynamic Colorの仕組みを知りたい人向け
デザイン視点の話
Discussion
Flutter本体に画像から色を抽出する処理が実装されたので、今後はこちらをつかうのがおすすめです。