FlutterでSVG画像の特定箇所だけ色を変更できるCustomWidgetを作った話

image

この記事は、【 可茂IT塾 Advent Calendar 2025 】の14日目の記事です。

はじめに

特定の箇所だけのSVG画像のカラーを変更したいときありませんか?

既存パッケージ(flutter_svgなど)でもカラー変更は可能ですが、画像全体のカラーが変更されてしまいますよね...

例えば:

  • アイコンの一部だけテーマカラーに変更したい
  • ダークモード対応で特定の要素だけ色を変えたい
  • グラデーションは保ったまま、一部の色だけ変更したい!

そんなSVG問題を解決するためにCustomWidgetを作成しました!

実装したWidget: UniversalSvg

できること

✅ 特定の色コードを指定して置換
✅ SVG要素のIDを指定して色変更
✅ グラデーションの色も個別に変更可能
✅ fill/stroke/style属性など様々な形式に対応

ID指定での色変更

UniversalSvg(
  assetPath: 'assets/icons/sample_icon.svg',
  colorRules: [
    SvgColorRule.byId(
      targetId: 'background-circle',  // この要素だけ
      changedColor: '#00FF00',         // 緑色に変更
    ),
  ],
)

グラデーションの色変更

UniversalSvg(
  assetPath: 'assets/icons/gradient_icon.svg',
  gradientRules: [
    SvgGradientRule(
      targetGradientId: 'gradient1',
      newColors: [
        Color(0xFF6366F1), // 開始色
        Color(0xFF8B5CF6), // 終了色
      ],
    ),
  ],
)

複数ルールの組み合わせ

UniversalSvg(
  assetPath: 'assets/icons/complex_icon.svg',
  colorRules: [
    SvgColorRule.byColor(
      targetColor: '#000000',
      changedColor: Theme.of(context).primaryColor.toHex(),
    ),
    SvgColorRule.byId(
      targetId: 'accent-part',
      changedColor: '#FF6B6B',
    ),
  ],
  gradientRules: [
    SvgGradientRule(
      targetGradientId: 'bg-gradient',
      newColors: [Colors.purple, Colors.pink],
    ),
  ],
)

コードの解説

仕組み:どうやって色を変更しているのか?

このWidgetの核心は「SVGをStringとして扱い、該当箇所を文字列置換する」というシンプルな仕組みです。

Future<String> _loadAndProcessSvg() async {
  // 1. SVGファイルを文字列として読み込み
  final svgContent = await rootBundle.loadString(assetPath);
  
  // 2. 色の置換ルールを適用
  String result = _applySvgColorRules(svgContent, colorRules);
  
  // 3. グラデーションの置換ルールを適用
  result = _applySvgGradientRules(result, gradientRules);
  
  // 4. 処理済みSVG文字列を返す
  return result;
}

1. 色指定での置換(_replaceColorInAllFormats

SVGファイル内では色が様々な形式で定義されています:

<!-- fill属性 -->
<path fill="#FF0000" ... />

<!-- stroke属性 -->
<circle stroke="#FF0000" ... />

<!-- style属性(スペースなし) -->
<rect style="fill:#FF0000" ... />

<!-- style属性(スペースあり) -->
<polygon style="fill: #FF0000" ... />

これら全てのパターンに対応するため、複数の置換処理を実行しています:

String _replaceColorInAllFormats(String svgContent, String targetColor, String changedColor) {
  String result = svgContent;
  
  // fill属性
  result = _replaceIgnoreCase(result, 'fill="$targetColor"', 'fill="$changedColor"');
  
  // stroke属性
  result = _replaceIgnoreCase(result, 'stroke="$targetColor"', 'stroke="$changedColor"');
  
  // style内のfill(スペースなし/あり両対応)
  result = _replaceIgnoreCase(result, 'fill:$targetColor', 'fill:$changedColor');
  result = _replaceIgnoreCase(result, 'fill: $targetColor', 'fill: $changedColor');
  
  // 以下同様...
  return result;
}

ポイント: 大文字小文字を区別しない置換(caseSensitive: false)で、#ff0000#FF0000も確実に置換できます。

2. ID指定での置換(_replaceColorById

正規表現を使って、特定のIDを持つ要素を探して色を置換します:

// id="targetId"を含む要素のfill属性を置換
final fillPattern = RegExp(
  r'(id="' + RegExp.escape(targetId) + r'"[^>]*?)fill="[^"]*"',
  caseSensitive: false,
);

工夫したポイント:

  • fill属性が既にある場合は置換
  • fill属性がない場合は新規追加
  • stroke属性も同様に処理

これにより、以下のようなSVGでも確実に色変更できます:

<!-- 既存のfillを置換 -->
<circle id="target" fill="#000000" /><circle id="target" fill="#FF0000" />

<!-- fillがない場合は追加 -->
<rect id="target" /><rect id="target" fill="#FF0000" />

3. グラデーション置換(_replaceGradientColors

グラデーションは少し複雑で、<linearGradient>内の複数の<stop>要素を順次置換します:

<linearGradient id="gradient1">
  <stop offset="0%" stop-color="#FF0000" />
  <stop offset="100%" stop-color="#00FF00" />
</linearGradient>

処理の流れ:

String _replaceGradientColors(String svgContent, String gradientId, List<Color> newColors) {
  // 1. 対象のlinearGradientブロック全体を検索
  final gradientPattern = RegExp(
    r'<linearGradient[^>]*?id="' + RegExp.escape(gradientId) + r'"[^>]*?>[\s\S]*?</linearGradient>'
  );
  
  return svgContent.replaceAllMapped(gradientPattern, (match) {
    String gradientBlock = match.group(0) ?? '';
    
    // 2. stop要素を検索
    final stopMatches = stopPattern.allMatches(gradientBlock).toList();
    
    // 3. 各stop要素の色を順次置換
    for (int i = 0; i < stopMatches.length && i < newColors.length; i++) {
      String updatedStop = _replaceStopColor(stopElement, newColor);
      gradientBlock = gradientBlock.replaceFirst(stopElement, updatedStop);
    }
    
    return gradientBlock;
  });
}

結果:

<linearGradient id="gradient1">
  <stop offset="0%" stop-color="#6366F1" />  <!-- 新しい色1 -->
  <stop offset="100%" stop-color="#8B5CF6" /> <!-- 新しい色2 -->
</linearGradient>

メリット・デメリット

メリット ✅

  • 柔軟性が高い:色指定・ID指定・グラデーションすべてに対応
  • シンプルな実装:SVGパーサー不要、文字列置換のみ
  • 軽量:追加の依存関係なし(flutter_svgのみ)
  • 理解しやすい:コードが読みやすく、カスタマイズしやすい

デメリット ⚠️

  • 文字列ベース:複雑な正規表現が必要な場合がある
  • パフォーマンス:大きなSVGや大量の置換では若干のオーバーヘッド
  • エッジケース:稀なSVG記法には対応していない可能性

コード全体

githubにサンプルを掲載しているので詳細はこちらからチェックしてもらえると!

cloneしてサンプルも動かすことができます

まとめ

SVGの特定箇所だけ色を変更したいという要望に応えるため、UniversalSvgウィジェットを作成しました。

文字列置換というシンプルな方法ながら、以下の機能を実現:

  • ✅ 色コード指定での置換
  • ✅ ID指定での置換
  • ✅ グラデーションの色変更

実装も比較的シンプルなので、プロジェクトに合わせてカスタマイズしやすいのも特徴です。

同じような課題を抱えている方の参考になれば幸いです!

お知らせ

可茂IT塾ではFlutter/Reactのインターンを募集しています!(募集停止中)

可茂IT塾ではFlutter/Reactのインターンを募集しています!(募集停止中)

可茂IT塾ではFlutter/Reactのインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。

Read More
U30可茂ITインターンハッカソン

U30可茂ITインターンハッカソン

12月28,29日開催。2日間でアプリ開発の企画から完成までを目指す!U30可茂ITインターンハッカソンを開催します。

Read More

タグ

Flutter (126)初心者向け (32)イベント (19)Google Apps Script (17)Nextjs (13)可茂IT塾 (12)AI (8)React (8)riverpod (7)デザイン (7)Firebase (7)Figma (6)VSCode (6)JavaScript (6)ChatGPT (5)Slack (5)TypeScript (5)新卒 (4)就活 (4)Prisma (4)Dart (4)アプリ開発 (4)お知らせ (4)FlutterWeb (3)経験談 (3)NestJS (3)tailwindcss (3)ワーケーション (3)インターン (3)Web (2)Obsidian (2)Supabase (2)設計 (2)線型計画法 (2)事例 (2)Git (2)CSS (2)Freezed (2)Image (2)File (2)GitHub Actions (2)Material Design (2) (2)会社員 (2)画像 (2)Mac (2)iOS (2)React Hooks (2)社会人 (2)大学生 (2)RSS (1)Google (1)CodeRunner (1)vibe-kanban (1)NotebookLM (1)個人開発 (1)SVG (1)Android (1)Unity (1)WebView (1)Twitter (1)フルリモート (1)TextScaler (1)textScaleFactor (1)学生向け (1)Java (1)Spring Boot (1)shell script (1)正規表現 (1)table (1)テーブル (1)hooks (1)パワーポイント (1)ブックマーク (1)Pocket (1)ブクマク (1)MCPサーバー (1)OpenAI (1)ベクトル検索 (1)趣味 (1)モンスターボール (1)SCSS (1)Swift (1)MapBox (1)Cupertino (1)gpt-oss (1)生成AI (1)llama.cpp (1)LLM (1)ListView (1)postgresql (1)cloudrun (1)gcp (1)就活浪人 (1)既卒 (1)保守性 (1)iPad (1)シェアハウス (1)スクレイピング (1)PageView (1)画面遷移 (1)dotenvx (1)dotenv (1)Python (1)flutter_hooks (1)Gmail (1)GoogleWorkspace (1)ShaderMask (1)google map (1)Google Places API (1)GCPコンソール (1)Google_ML_Kit (1)Vercel (1)Google Domains (1)DeepLeaning (1)深層学習 (1)Google Colab (1)Firebase Analytics (1)Gemini AI (1)コード生成 (1)GitHub Copilot (1)gemini (1)オンラインオフィス (1)html (1)オブジェクト指向 (1)クラスの継承 (1)ポリモーフィズム (1)LINE Messaging API (1)LINE Notify (1)LINE (1)Bitcoin (1)bitFlyer (1)コミュニティー (1)文系エンジニア (1)build_runner (1)ヒーター (1)作業効率 (1) (1)Flutter実践開発 (1)permission_handler (1)flutter_local_notifications (1)markdown (1)GlobalKey (1)ValueKey (1)Key (1)アイコン (1)go_router (1)FireStorage (1)debug (1)datetime_picker (1)Apple Store Connect (1)FlutterGen (1)デバッグ (1)Widget Inspector (1)VRChat (1)API (1)検索機能 (1)ローディング (1)Skeletonizer (1)Simmer (1)Shader (1)SharedPreferences (1)オフラインサポート (1)Navigator (1)メール送信 (1)FlutterFlow (1)Firebase App Distribution (1)Fastlane (1)Dio (1)CustomClipper (1)ClipPath (1)video_player (1)IMA (1)カスタム認証 (1)アニメーション (1)Arduino (1)ESP32 (1)フリーランス (1)会社設立 (1)csv (1)docker (1)GithubActions (1)Dialog (1)BI (1)sora2 (1)iPhone (1)Gemini CLI (1)Claude Code (1)LifeHack (1)ショートカット (1)Chrome (1)高校生 (1)キャリア教育 (1)非同期処理 (1)生体認証 (1)BackdropFilter (1)レビュー (1)Antigravity (1)getAuth (1)クローズドテスト (1)PlayConsole (1)Algolia (1)コンサルティング (1)Symbol (1)

お知らせ

可茂IT塾ではFlutter/Reactのインターンを募集しています!(募集停止中)

可茂IT塾ではFlutter/Reactのインターンを募集しています!(募集停止中)

可茂IT塾ではFlutter/Reactのインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。

Read More
U30可茂ITインターンハッカソン

U30可茂ITインターンハッカソン

12月28,29日開催。2日間でアプリ開発の企画から完成までを目指す!U30可茂ITインターンハッカソンを開催します。

Read More