🖼️

Androidの Vector Drawable をGitHubでプレビューできるようにする

2024/11/12に公開

はじめに

こちらの記事は、LUUP のTVCM放映に合わせた一足早い「Luup Developers Advent Calendar 2024」の7日目の記事です。(公開が数日遅れました)

こんにちは、Luup Androidチームの河原です。

GitHubでは、画像のプレビュー機能があり、一般的な画像形式(PNG、JPG、GIF、PSD、SVG) の画像をGitHubのWeb上でプレビューすることができます。(2024/11/07時点の情報)

pngのプレビューの様子
pngファイルの差分の様子

この機能はレビューをする際などにとても便利なのですが、Androidで扱えるVector画像のファイル形式であるVectorDrawableは、上記の形式ではないので、このプレビュー機能の恩恵にあずかることができません。

xmlのプレビューの様子
VectorDrawableファイルの差分の様子

作戦

何か良い方法が無いのかと考えてみたのですが、
VectorDrwableのXML形式から、GitHubのプレビュー機能に対応している画像形式に変換してミラーファイルとして保存する仕組みを整えれば、よいのではないかと考えました。

調べてみるとVectorDrwable形式の変換ツールはAndroidの公式では提供されておらず、今回は有志の方が作成されている vd2svg というツールを使わせていただくことにしました。

概要図

vd2svg の使い方

vd2svgは、下記のようにXMLファイルに対して実行すると、SVG形式で出力してくれます。

動作イメージ

$ vd2svg ic_example.xml
INFO: Processing: ./ic_example.xml
INFO: Save to: ./ic_example.svg

詳しくは、vd2svg のリポジトリーを参照してください。

プレビュー用のミラーファイルの生成スクリプト

vd2svgはコマンド単体では、再帰的にファイルを処理できないので、Androidのres/drawableディレクトリーを再帰的に処理する簡単なスクリプトを書きました。
また、res/drawable 直下に直接SVGファイルを出力してしまうと、元のVectorDrawableとプレビュー用のSVGファイルが混在してしまい、管理が煩雑になるので、res-preview/drawable ディレクトリーに出力するようにしました。

実装例として下記に紹介します。

ミラーファイルの生成スクリプト(実装例)
res-preview.rb

#!/usr/bin/env ruby

require 'find'
require 'fileutils'

# このスクリプトを実行するのに必要な環境
# - vd2svgをインストール (https://github.com/neworld/vd2svg)

# 検索対象のディレクトリパスを指定
SEARCH_PATH = "."  # 現在のディレクトリを検索対象とします。必要に応じて変更してください。

# 正規表現パターンを定義(例: res/drawable, res/drawable-hdpi, etc.)
DRAWABLE_PATTERN = %r{#{Regexp.escape(File::SEPARATOR)}res#{Regexp.escape(File::SEPARATOR)}drawable[^#{Regexp.escape(File::SEPARATOR)}]*$}i

# メイン処理を実行する関数
def main
  search_path = File.expand_path(SEARCH_PATH)

  drawable_dirs = find_drawable_dirs(search_path, DRAWABLE_PATTERN)

  if drawable_dirs.empty?
    puts "指定されたパターンに一致するディレクトリが見つかりませんでした。"
    exit 1
  end

  remove_all_res_preview_dirs(search_path)

  res_preview_dir = File.join(search_path, "res-preview")
  create_directory(res_preview_dir)

  process_drawable_dirs(drawable_dirs, res_preview_dir)

end

# drawable ディレクトリを検索し、配列で返す関数
#
# @param [String] search_path 検索対象のディレクトリパス
# @param [Regexp] pattern 検索に使用する正規表現パターン
# @return [Array<String>] マッチした drawable ディレクトリの絶対パス配列
def find_drawable_dirs(search_path, pattern)
  drawable_dirs = []
  puts "drawable ディレクトリを検索しています: #{search_path}"

  Find.find(search_path) do |path|
    if File.directory?(path) && path =~ pattern
      drawable_dirs << path
    end
  end

  puts "対象のdrawableディレクトリ: #{drawable_dirs.size}件"
  puts drawable_dirs.join("\n")
  drawable_dirs
end

# res-preview ディレクトリを全て検索して削除する関数
#
# @param [String] search_path 検索対象のディレクトリパス
# @return [void]
def remove_all_res_preview_dirs(search_path)
  puts "res-preview ディレクトリを検索して削除しています..."

  Find.find(search_path) do |path|
    if File.directory?(path) && File.basename(path) == "res-preview"
      begin
        FileUtils.rm_rf(path)
        puts "  削除しました: #{path}"
      rescue StandardError => e
        puts "  ディレクトリ #{path} の削除に失敗しました: #{e.message}"
        exit 1
      end
    end
  end

  puts "既存の res-preview ディレクトリを全て削除しました。"
end

# 新しいディレクトリを作成する関数
#
# @param [String] dir_path 作成するディレクトリのパス
# @param [String] error_message エラー時に表示するメッセージ
# @return [void]
def create_directory(dir_path)
  begin
    FileUtils.mkdir_p(dir_path)
    puts "  作成しました: #{dir_path}"
  rescue StandardError => e
    puts "  新しい res-preview ディレクトリの作成に失敗しました: #{e.message}"
    exit 1
  end
end

# drawable ディレクトリに対して vd2svg コマンドを実行する関数
#
# @param [Array<String>] drawable_dirs マッチした drawable ディレクトリの配列
# @param [String] res_preview_dir 作成する res-preview ディレクトリのパス
# @return [void]
def process_drawable_dirs(drawable_dirs, res_preview_dir)
  drawable_dirs.each do |drawable_dir|
    puts "Processing directory: #{drawable_dir}"

    begin
      drawable_name = File.basename(drawable_dir) # drawable-hdpi

      # res-preview/drawable-xxx ディレクトリのパスを設定
      preview_drawable_dir = File.join(res_preview_dir, drawable_name)

      # preview_drawable_dir が存在しない場合は作成
      unless Dir.exist?(preview_drawable_dir)
        create_directory(preview_drawable_dir)
      end

      # drawable_dir 内の *.xml ファイルを取得
      xml_files = Dir.glob(File.join(drawable_dir, "*.xml"))

      if xml_files.empty?
        puts "  No XML files found in #{drawable_dir}. Skipping."
        next
      end

      # drawable_dir から res-preview/drawable-xxx への相対パスを計算
      # drawable_dir = /path/to/project/res/drawable-hdpi
      # preview_drawable_dir = /path/to/project/res-preview/drawable-hdpi
      # 相対パス = ../../res-preview/drawable-hdpi
      relative_output_dir = File.join("../..", "res-preview", drawable_name)

      # ディレクトリに移動してコマンドを実行
      Dir.chdir(drawable_dir) do
        # vd2svg コマンドの構築
        # シェルのワイルドカードを展開するため、ダブルクォートを使用
        # 出力先ディレクトリは相対パスで指定
        cmd = "vd2svg *.xml -o \"#{relative_output_dir}/\""

        puts "  Executing: #{cmd}"
        success = system(cmd)

        # コマンドの実行結果を確認
        if success
          puts "  Successfully executed vd2svg in #{drawable_dir}"
        else
          puts "  Failed to execute vd2svg in #{drawable_dir}"
        end
      end

    rescue StandardError => e
      puts "  Error processing directory #{drawable_dir}: #{e.message}"
    end
  end
end

これを実行することで、drawableのディレクトリーを再帰的に検索し、res/drawable ディレクトリー内のXMLファイルをSVG形式に変換し、res-preview/drawable ディレクトリーにプレビュー用のファイルを保存できます。

CIへの組み込み

現在、Androidチームではこの仕組みをCIに組み込むことで、新しいVectorDrwableが追加されると、自動でミラーファイルが生成されるようにしています。
おかげで、レビューの際にもVectorDrawableの変更がGitHubのWeb上で簡単に確認できるようになり、開発効率が向上しました。

まとめ

今回は、AndroidのVectorDrawableをGitHubでプレビューできるようにする方法をご紹介しました。
vd2svgは公式ツールではないのもあり、一部のファイルの変換がうまくいかないこともありますが、基本的な形状のVectorDrawableであれば問題なく変換できるので、ぜひ試してみてください。

また、同じ作戦でGitHubで非対応のwebp形式のファイルをpngに変換して、GitHubのプレビュー機能を活用できそうと思っていて、今後検討していきたいと思っています。

最後に

Luup では、一緒に開発してくださるソフトウェアエンジニアを積極的に募集しています。
カジュアル面談も実施しておりますのでぜひお気軽にお声掛けください。
https://recruit.luup.sc/

参考リンク

Luup Developers Blog

Discussion