Xamarin.iOS/Android で、文字列と画像をPCLを使って共有させる方法

Xamarin.iOS/Androidアプリで、バンドルリソース処理を完全共通化できそうな仕様 – Qiita
http://qiita.com/kochizufan/items/69d69f37cf991d452226#comment-172bac67ec6257dc81c2
Embedded Resource | Xamarin
http://docs.xamarin.com/content/EmbeddedResources/

多分、PCL を使うと共通できるだろうなぁ、とは思っていたのですが、これといってよい方法が思いつかなかったのです。iOS/Android それぞれのプロジェクトに入れるしかないか(コピーあるいは共有プロジェクト)とは考えていたものの。なるほど、PCL のアセンブリから直接リソースを読みだす技がありましたね。

https://github.com/xamarin/mobile-samples/blob/master/EmbeddedResources/SharedLib/ResourceLoader.cs

のソースをざってと見ていたのですが、iOS/Android 側で GetEmbeddedResourceStream メソッドを呼び出さないといけないのが難点です。と言いますか、PCL の .NET ライブラリには、Assembly クラスの static メソッドが非常に制限されていて、なんともしようがないのです。と思っていたのですが、typeof(ResourceLoader).GetTypeInfo().Assembly という技で、通常の Assembly を取れる技を知りました。
いやいや、よくよくリソースファイル Resources.Designer.cs を覗いてみれば、

global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("XamarinPortableRes.Lib.Properties.Resources", typeof(Resources).GetTypeInfo().Assembly);

なる書き方がされていて、なーんだ。普通に使える方法じゃないですか。

■文字列リソースを追加する

Embedded Resource の例では、文字列リソースをテキストファイルから読み込んでいますが、これは普通のリソースにしたいですよね。普通に「プロジェクト」→「リソース」で文字列を追加できます。

まだ試してはいませんが、国際化による言語切り替えもこれでやると楽かと。

■文字列リソースを使う

使う側も簡単です。string 型で返してくれるので、そのまま Text プロパティなどに設定できます。

Android の場合

FindViewById(Resource.Id.textView1).Text =
    XamarinPortableRes.Lib.Properties.Resources.Message1;

iOS の場合

this.text1.Text = XamarinPortableRes.Lib.Properties.Resources.Message1;

■画像リソースを追加する

PCL プロジェクトのリソース追加は「文字列」のみに制限されています。制限されている理由はわからないのですが、無理やり突っ込んでも *.resx ファイルから *.desinger.cs に変換でダメになるので、そのままでは使い勝手が悪いです。
なので、Resources というフォルダを作って、画像ファイル(pngファイルなど)を入れます。プロパティウィンドウでビルドアクションを「埋め込まれたリソース」にしておきます。こうすると、先の「バンドルリソース処理を完全共通化~」に書いてあるように「XamarinPortableRes.Lib.Resources.MarkBlue.png」のように、ドットつながりでリソースが取れます。

このリソース名をそのまま使ってもいいのですが、文字列指定を間違えると実行エラーになるし、どうせならばインテリセンスが効くようにしたいので、少しだけ工夫します。

こんな風に、リソースを取り出すための ResourceLoader クラスを作ります。ResourceManager みたいなものです。リソース自体は1回しか読み込まないので、static で十分です。名前から検索するのは Embedded Resource の借用ですが、初回のみ GetManifestResourceNames メソッドでリソース名の一覧を取得します。

public static class ResourceLoader
{
    static internal string[] Names { get; set; }
    static internal Assembly Assembly { get; set; }

    public static System.IO.Stream GetObject(string resourceName)
    {
        if (ResourceLoader.Assembly == null)
        {
            ResourceLoader.Assembly = typeof(ResourceLoader).GetTypeInfo().Assembly;
            ResourceLoader.Names = ResourceLoader.Assembly.GetManifestResourceNames();
        }
        try
        {
            string path = ResourceLoader.Names.First(x => x.EndsWith(resourceName, StringComparison.CurrentCultureIgnoreCase));
            return ResourceLoader.Assembly.GetManifestResourceStream(path);
        }
        catch
        {
            return null;
        }
    }
}

インテリセンスが効くように Resources クラスに「手動」でプロパティを作ります。このあたりは T4 で作ってもいいし、まあ色々。これで、Stream までは取得できます。

public class Resources
{
    public static System.IO.Stream MarkBlue
    {
        get { return ResourceLoader.GetObject("MarkBlue.png"); }
    }
    public static System.IO.Stream MarkGreen
    {
        get { return ResourceLoader.GetObject("MarkGreen.png"); }
    }
    public static System.IO.Stream MarkNone
    {
        get { return ResourceLoader.GetObject("MarkNone.png"); }
    }
    public static System.IO.Stream MarkOrange
    {
        get { return ResourceLoader.GetObject("MarkOrange.png"); }
    }
    public static System.IO.Stream MarkPurple
    {
        get { return ResourceLoader.GetObject("MarkPurple.png"); }
    }
    public static System.IO.Stream MarkRed
    {
        get { return ResourceLoader.GetObject("MarkRed.png"); }
    }
}

■画像リソースを使う

画像リソースから取ってくると、System.IO.Stream を取得できますが、そのままだと扱いにくいので System.Drawing.Bitmap にしたいところなのですが、iOS/Android とそれぞれ異なる(UIImage, Android.Graphics.Bitmap)ので、ヘルパーメソッドを作っておきます。

Android の場合は、Stream から Bitmap を返すまで。

global::Android.Graphics.Bitmap ToBitmap(System.IO.Stream st)
{
    using (var mem = new System.IO.MemoryStream())
    {
        st.CopyTo(mem);
        st.Close();
        var data = mem.ToArray();
        var bmp = global::Android.Graphics.BitmapFactory.DecodeByteArray(data, 0, data.Length);
        return bmp;
    }
}

こんな風に画像設定をします。

this.imageBlue = FindViewById(Resource.Id.imageView1);
imageBlue.SetImageBitmap(ToBitmap(XamarinPortableRes.Lib.Resources.MarkBlue));

iOS の場合は、UIImage を作成します。

UIImage ToBitmap(System.IO.Stream st)
{
    var data = NSData.FromStream( st );
    st.Close();
    var bmp = UIImage.LoadFromData(data);
    return bmp;
}

そして、設定

this.imageBlue.Image = ToBitmap(XamarinPortableRes.Lib.Resources.MarkBlue);

本来は ToBitmap まで共通化したいところですが、それぞれ画像の扱いが異なるので、ここぐらいまで。更に Windows Store, Windows Phone を追加すると、より汎用的になるはずですが、これはあとから。

# github で、ToBitmap を Stream クラスの拡張メソッドにする方法に変えたので、説明も後で変更します。

■サンプルコード

https://github.com/moonmile/XamarinPortableRes

■実行結果


カテゴリー: Android, C#, Xamarin, iOS パーマリンク