🕌

FlutterでSVGの特定要素の色を動的に変更する方法

2024/11/28に公開

FlutterでSVGの色を動的に変更する実装方法

はじめに

SVGファイルは拡大縮小しても品質が劣化しない優れたベクター画像形式です。アプリ内で色を動的に変更したい場合、単純な画像置き換えでは対応できません。

今回は、SVGファイル内の特定の要素の色を自由に変更できるウィジェットの実装方法をご紹介します。

SVG の色を動的に変更するとできること

百聞は一見に如かず。という事で下記をご覧ください。

change_svg_color.gif

私が現在開発中のアプリではテーマカラー選択ができるのですが、
ワンちゃんの画像のスカーフの部分の色だけが
テーマカラーに合わせて変わっていると思います。
こんなふうに、動的に色を変えられるようになるのがメリットです。

SVG ファイルの準備

まず、動的に色を変更させるsvgファイルを用意します。
svg形式で取得可能なフリー素材を用いればOKですが、迷ったらこのサイトから探してみてください。

unDraw

色を変更したい要素に id= で適当な文字列を渡します。
例では id="change_color_target" を追加しています。

<svg xmlns="http://www.w3.org/2000/svg" ...>
  <!-- 色を変更したい要素にIDを追加 -->
  <path id="change_color_target" 
        d="..." 
        fill="#000000"/>
  <!-- 他の要素 -->
  <path d="..." fill="#3f3d56"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" ...><path id="change_color_target" d="..." fill="#000000"/><path d="..." fill="#3f3d56"/></svg>

改行を避けるべき理由

XMLパーサーの挙動
FlutterのSVGパーサーは、XMLの構文解析を厳密に行います
改行やインデントが入ることで、予期せぬ場所で要素が分割される可能性があります
これにより XmlParserException: ">" expected at XX:XX のようなエラーが発生することがあります

pubspec.yaml の準備

まず、必要なパッケージをpubspec.yamlに追加します:

dependencies:
  flutter_svg: ^2.0.14

次に、SVGファイルをアセットとして登録します:

flutter:
  assets:
    - assets/images/example.svg

パッケージのインストールとアセットの登録が完了したら、以下のコマンドを実行します:

flutter pub get

実装

ColorChangeSvgクラスを作成します:

import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';

/// SVGファイル内の特定の要素の色を変更するウィジェット
class ColorChangeSvg extends StatelessWidget {
  final String assetName;
  final Map<String, Color> colorMapping;
  final double width;
  final double height;

  const ColorChangeSvg({
    required this.assetName,
    required this.colorMapping,
    required this.width,
    required this.height,
    super.key,
  });

  
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: DefaultAssetBundle.of(context).loadString(assetName),
      builder: (context, snapshot) {
        if (!snapshot.hasData) {
          return SizedBox(width: width, height: height);
        }

        String svgContent = snapshot.data!;

        // 各ターゲットに対して色を変更
        colorMapping.forEach((targetId, color) {
          final hexColor = '#${color.value.toRadixString(16).padLeft(8, '0').substring(2)}';
          final pattern = RegExp('(id="$targetId"[^>]*?)(fill="[^"]*")', multiLine: true);
          
          svgContent = svgContent.replaceAllMapped(pattern, (match) {
            final prefix = match.group(1) ?? '';
            return '$prefix fill="$hexColor"';
          });
        });

        return SvgPicture.string(
          svgContent,
          width: width,
          height: height,
          fit: BoxFit.contain,
        );
      },
    );
  }
}

使用方法

このウィジェットは以下のように使用できます:

// 単一の色を変更する場合
ColorChangeSvg(
  assetName: 'assets/images/example.svg',
  colorMapping: {'change_color_target': Colors.blue},
  width: 150,
  height: 150,
)

// 複数の色を変更する場合
ColorChangeSvg(
  assetName: 'assets/images/example.svg',
  colorMapping: {
    'target_1': Colors.yellow,
    'target_2': Colors.red,
    'target_3': Colors.green,
  },
  width: 150,
  height: 150,
)

デモページの実装例

下記のsvgファイルを使用します。

<svg xmlns="http://www.w3.org/2000/svg" width="888" height="483.61099" viewBox="0 0 888 483.61099" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="m1,421.53481h887v-2H1c-.55228,0-1,.44772-1,1h0c0,.55228.44771,1,1,1Z" fill="#3f3d56"/><path d="m406,438.53481c5,24,33.5-5.5,33.5-5.5l34-4s21.5.5,35.5,11.5,23.5-3.5,23.5-3.5c15.77283,5.2576,35.09692,5.23627,51.83264,3.55252,24.11597-2.42627,43.44604-21.04041,46.76044-45.05038,18.69678-135.44147-84.09314-202.00211-84.09314-202.00211l-30-81s9.5-23.5,12.5-44.5-19-41-46-61c-11.8125-8.75-25.34766-8.12109-36.83716-4.64502-12.88892,3.89946-24.13214,11.9183-32.4397,22.51616-6.88251,8.77995-18.68735,21.21468-31.66315,23.36885-4.48999.73999-8.91998,2.52002-13.06,5.76001-4.28003,3.34998-6.16998,8.94-6.22998,15.89001-.32001,30.35999,24.75,66.90998,46.72998,72.60999,27,7,12.5,73.5,12.5,73.5-44,62-13,131-13,131l-8,56s-.5,11.5,4.5,35.5l.00006-.00003Z" fill="#3f3d56"/><path d="m553,309.53481l-11,137s15,38-36,36-21-80-21-80l16-102" fill="#3f3d56"/><path d="m509.79492,483.61099c-1.24707,0-2.52344-.02539-3.83398-.07715-11.29004-.44238-19.60352-4.60059-24.71094-12.3584-14.60254-22.18164,1.63867-65.90625,2.77832-68.89941l15.9834-101.89648c.08594-.5459.59375-.91309,1.14355-.83301.54492.08594.91797.59766.83301,1.14355l-16,102c-.01172.06934-.0293.1377-.05469.2041-.1748.45215-17.25195,45.55566-3.01172,67.18359,4.73242,7.18652,12.50977,11.04199,23.11719,11.45801,16.65234.64941,27.85254-2.90332,33.29785-10.56836,7.12402-10.02832,1.78711-23.92676,1.73242-24.06641-.05566-.1416-.07812-.29492-.06641-.44629l11-137c.04395-.5498.5166-.95117,1.07715-.91699.55078.04492.96094.52637.91699,1.07715l-10.98242,136.77539c.72656,1.96094,5.28223,15.4043-2.03906,25.72363-5.41699,7.63574-15.89844,11.49707-31.18066,11.49707Z" fill="#2f2e41"/><path d="m462,337.53481s3,127-60,124-6-43-6-43l5.5-15.5" fill="#3f3d56"/><path d="m404.16211,462.58657c-.73145,0-1.4668-.01758-2.20996-.05273-17.98828-.85645-28.15186-4.88672-30.20801-11.97852-3.7041-12.7793,20.43945-30.52148,23.41895-32.65332l5.39453-15.20117c.18457-.52148.75586-.79297,1.27637-.6084s.79297.75586.6084,1.27637l-5.5,15.5c-.06934.19629-.19824.36523-.36816.48438-.26367.18555-26.37744,18.68457-22.90869,30.64551,1.79248,6.18066,11.34131,9.72559,28.38232,10.53711,11.34863.5459,21.29688-3.27051,29.55078-11.3291,30.95215-30.21582,29.4209-110.83789,29.40137-111.64844-.0127-.55176.42383-1.00977.97656-1.02344.52246.00098,1.00977.4248,1.02344.97656.0791,3.3457,1.57227,82.29785-30.00293,113.125-8.12793,7.93555-17.82031,11.9502-28.83496,11.9502Z" fill="#2f2e41"/><path d="m363.27002,69.92484c7.17999.85999,18.41998.81995,27.72998-5.39001,9.96997-6.65002-.38-12.85004-8.44-16.26001-4.48999.73999-8.91998,2.52002-13.06,5.76001-4.28003,3.34998-6.16998,8.94-6.22998,15.89001Z" fill="#2f2e41"/><path id="target_2" d="m479.2984,51.94388s-17.44968-44.81639,7.54245-45.4435,44.73257,38.20994,44.73257,38.20994c0,0,62.72797,114.34248-23.21378,108.17637-85.94171-6.16615-44.87375-51.81067-44.87375-51.81067,0,0,19.60165-24.09928,15.81253-49.13211h-.00003v-.00002h0s0-.00002,0-.00002Z" fill="#2f2e41"/><path id="target_3" d="m575.03857,386.85064c11.06787,4.92365,22.63171,9.948,34.73578,9.46738s24.84155-8.27557,26.72772-20.24146c.97363-6.17697-.95746-12.80396,1.40759-18.59268,3.18213-7.78867,13.44672-10.79233,21.42578-8.12323s13.84076,9.55188,18.04358,16.84058c7.86328,13.63672,11.0141,31.53461,2.58307,44.82779-7.30878,11.52368-21.17084,16.73105-34.06519,21.19748-17.17535,5.94928-36.35114,11.854-52.99396,4.54617-16.7381-7.34964-25.85565-28.58435-19.65753-45.7821" fill="#3f3d56"/><path id="target_1" d="m538.1142,170.92143s22,11,11,23-79,46-104,41-31-20-31-24,1.88583-52.38661,9.88583-50.38661,58.11417,47.38661,114.11417,10.38661Z" fill="#00bfa6"/></svg>

実際の使用例はこちら:

import 'package:flutter/material.dart';
import 'package:knocqnow/presentation/components/common/color_change_svg.dart';

/// SVGの色変更機能のデモページ
class SvgMockPage extends StatelessWidget {
  const SvgMockPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('SVG Color Change Demo'),
      ),
      body: const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Single Color Change'),
            SizedBox(height: 16),
            ColorChangeSvg(
              assetName: 'assets/images/example.svg',
              colorMapping: {'target_1': Colors.blue},
              width: 200,
              height: 200,
            ),
            SizedBox(height: 32),
            Text('Multi Color Change'),
            SizedBox(height: 16),
            ColorChangeSvg(
              assetName: 'assets/images/example.svg',
              colorMapping: {
                'target_1': Colors.yellow,
                'target_2': Colors.red,
                'target_3': Colors.green,
              },
              width: 200,
              height: 200,
            ),
          ],
        ),
      ),
    );
  }
}

完成イメージはこちら

Screenshot 2024-11-28 at 10.24.03 PM.png

トラブルシューティング

  1. SVGファイルがpubspec.yamlassetsに正しく登録されているか
  2. SVGファイル内の要素に適切なid属性が設定されているか
  3. colorMappingで指定したIDがSVGファイル内のIDと完全に一致しているか

問題が解決しない場合は、以下のコマンドを実行してキャッシュをクリアしてください。
一度でうまくいかない場合は何回か試してみてください:

flutter clean
flutter pub get

まとめ

このウィジェットを使用することで、SVGファイルの色を動的に変更できるようになります。
是非お試しください。

参考資料

Discussion