🐙

ユーザーの選んだ画像からFlutterアプリのテーマカラーを選ぶ

2023/03/26に公開1

はじめに

Dynamic Colorを知っていますか?
Dynamic Color(ダイナミックカラー)は、Googleが提案するデザインフレームワークであるMaterial Designの一部です。ダイナミックカラーは、アプリケーションのカラースキームをユーザーの好みやデバイスの設定に合わせて動的に変化させる機能です。これにより、ユーザーが個別の好みや条件に応じて最適なカラースキームを選択できるようになります。by GPT4

この記事ではDynamic Colorの仕組みを使って、画像から色を抽出しFlutterアプリのテーマカラーに反映する方法を紹介します。
最終的に以下のようなアプリが作れます。

material_color_utilitiesパッケージ

Dynamic Colorの仕組みは複雑ですが、material_color_utilitiesパッケージにアルゴリズムの実装があるため、こちらを使うことで簡単に画像から色の抽出ができます。

ImageUtilsを実装

まずはmaterial_color_utilitiesを使って画像から色を抽出する実装をします。

image_utils.dart
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色を返すメソッドです。
以下の処理をしています。

  1. copyResizeは結構処理が重かったので、画像サイズを小さくしています。
  2. 画像をピクセルごとにargb形式のintにしたListを作る
  3. QuantizerCelebiクラスのquantizeを使って色数を絞る
  4. Scoreクラスのscoreを使って上位を色を決める
  5. 上位4つの色を返す

補足

material_color_utilitiesのReadmeにも書いてあるのですが、端末で設定された色を使いたいだけなら以下のパッケージがあります。
https://pub.dev/packages/dynamic_color

補足2

material_color_utilitiesはflutterの以下の部分で使われてたりもします。SeedColorからSchemeを作るところですね👀
https://github.com/flutter/flutter/blob/4c25587b71aa91e27c115893f59b48b8b4a22ca9/packages/flutter/lib/src/material/color_scheme.dart#L201-L209

実際のアプリに反映する

先程作った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の一部をコンテンツに合った色に変えたり出来るので、気に入ったら是非試してみたください。

以下は今回のサンプルアプリの実装です。⭐してもらえると嬉しいです🥳

https://github.com/K9i-0/flutter_mcu_sample

おすすめ

Dynamic Colorの仕組みを知りたい人向け
https://blog.smartbank.co.jp/entry/2022/10/06/dynamic-color

デザイン視点の話
https://note.com/ritar/n/n0f6aad6c2560

GitHubで編集を提案

Discussion