読者です 読者をやめる 読者になる 読者になる

しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

Unity のプロパティ注入だけでプラットフォームごとに Xamarin アプリの配色を切り替える

Xamarin でプラットフォームごとに配色を切り替える場合、一般には下記のような XAML を記述すると思います

<OnPlatform x:Key="SecondHandColor"
            x:TypeArguments="Color"
            iOS="Red"
            Android="Aqua"
            WinPhone="#80FF80" />

ただ、こうしてしまうと共通コードに各プラットフォームごとの分岐処理を記述するのと同様なので、後で特定のプラットフォームだけの修正をしようとすると共通コードに影響したり、修正箇所が広範囲にわたってしまい修正もれを起こしやすくなったりするおそれがあります

上記を防ぐために Unity のプロパティ注入を利用してみます

      <!-- 秒針描画 -->
      <BoxView BindingContext="{Binding SecondHand}"
               Color="{Binding Color, Converter={StaticResource ColorConverter}}"
               Opacity="0.5"
               AbsoluteLayout.LayoutBounds="{Binding LayoutBounds, Converter={StaticResource RectangleConverter}}"
               AbsoluteLayout.LayoutFlags="None"
               AnchorX="0.5"
               AnchorY="0.5"
               Rotation="{Binding Rotation}"/>

XAML 側はこんな感じに Color プロパティに ViewModel のプロパティをバインドするようにします

ViewModel のプロパティは文字列にしたいので、ColorConverter を作成して string から Color に変換するようにします

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prismmvvm="clr-namespace:Prism.Mvvm;assembly=Prism.Mvvm.Xamarin"
             xmlns:local="clr-namespace:XamarinUnityInjection.Views;assembly=XamarinUnityInjection"
             xmlns:cv="clr-namespace:XamarinUnityInjection.Converters;assembly=XamarinUnityInjection"
             xmlns:vm="clr-namespace:XamarinUnityInjection.ViewModels;assembly=XamarinUnityInjection"
             prismmvvm:ViewModelLocator.AutoWireViewModel="true"
             local:PageStateDetect.DetectState="true"
             x:Class="XamarinUnityInjection.Views.TopPage">
  <ContentPage.Resources>
    <ResourceDictionary>
      <cv:RectangleConverter x:Key="RectangleConverter"/>
      <cv:ColorConverter x:Key="ColorConverter"/>
    </ResourceDictionary>
  </ContentPage.Resources>
  
    <AbsoluteLayout HorizontalOptions="FillAndExpand"
                    VerticalOptions="FillAndExpand">
      
      <!-- 目盛り描画 -->
      <local:AbsoluteItemsControl
        HorizontalOptions="FillAndExpand"
        VerticalOptions="FillAndExpand"
        x:TypeArguments="vm:TickItemViewModel"
        ItemsSource="{Binding TickItems}">
        <local:AbsoluteItemsControl.ItemTemplate>
          <DataTemplate>
            <BoxView Color="{Binding Color, Converter={StaticResource ColorConverter}}"
                     AbsoluteLayout.LayoutBounds="{Binding LayoutBounds, Converter={StaticResource RectangleConverter}}"
                     AbsoluteLayout.LayoutFlags="None"
                     AnchorX="0.5"
                     AnchorY="0.5"
                     Rotation="{Binding Rotation}"/>
          </DataTemplate>
        </local:AbsoluteItemsControl.ItemTemplate>
      </local:AbsoluteItemsControl>

      <!-- 数字描画 -->
      <local:AbsoluteItemsControl  HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
        x:TypeArguments="vm:NumberItemViewModel"
        ItemsSource="{Binding NumberItems}">
        <local:AbsoluteItemsControl.ItemTemplate>
          <DataTemplate>
            <Label TextColor="{Binding Color, Converter={StaticResource ColorConverter}}"
                   HorizontalOptions="FillAndExpand"
                   VerticalOptions="CenterAndExpand"
                   Font="20"
                   Text="{Binding NumberString}"
                   AbsoluteLayout.LayoutBounds="{Binding LayoutBounds, Converter={StaticResource RectangleConverter}}"
                   AbsoluteLayout.LayoutFlags="None"
                   AnchorX="0.5"
                   AnchorY="0.5"
                   Rotation="{Binding Rotation}"/>
          </DataTemplate>
        </local:AbsoluteItemsControl.ItemTemplate>
      </local:AbsoluteItemsControl>

      <!-- 時針描画 -->
      <BoxView BindingContext="{Binding HourHand}"
               Color="{Binding Color, Converter={StaticResource ColorConverter}}"
               Opacity="0.5"
               AbsoluteLayout.LayoutBounds="{Binding LayoutBounds, Converter={StaticResource RectangleConverter}}"
               AbsoluteLayout.LayoutFlags="None"
               AnchorX="0.5"
               AnchorY="0.5"
               Rotation="{Binding Rotation}"/>
      
      <!-- 分針描画 -->
      <BoxView BindingContext="{Binding MinuteHand}"
               Color="{Binding Color, Converter={StaticResource ColorConverter}}"
               Opacity="0.5"
               AbsoluteLayout.LayoutBounds="{Binding LayoutBounds, Converter={StaticResource RectangleConverter}}"
               AbsoluteLayout.LayoutFlags="None"
               AnchorX="0.5"
               AnchorY="0.5"
               Rotation="{Binding Rotation}"/>

      <!-- 秒針描画 -->
      <BoxView BindingContext="{Binding SecondHand}"
               Color="{Binding Color, Converter={StaticResource ColorConverter}}"
               Opacity="0.5"
               AbsoluteLayout.LayoutBounds="{Binding LayoutBounds, Converter={StaticResource RectangleConverter}}"
               AbsoluteLayout.LayoutFlags="None"
               AnchorX="0.5"
               AnchorY="0.5"
               Rotation="{Binding Rotation}"/>
    </AbsoluteLayout>
</ContentPage>

XAML 全体だとこんな感じ

/// <summary>
/// string を Color に変換する Converter
/// </summary>
public class ColorConverter : IValueConverter
{
    /// <summary>
    /// string を Color に変換する Converter
    /// </summary>
    private static readonly ColorTypeConverter Converter = new ColorTypeConverter(); 

    #region IValueConverter

    /// <summary>
    /// 文字列を Color に変換します
    /// </summary>
    /// <param name="value">文字列</param>
    /// <param name="targetType">変換後の型</param>
    /// <param name="parameter">パラメータ</param>
    /// <param name="culture">対象カルチャ</param>
    /// <returns>Color</returns>
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (!(value is string))
        {
            return default(Color);
        }

        return Converter.ConvertFrom(CultureInfo.CurrentCulture, value);
    }

    /// <summary>
    /// Color を文字列に変換します
    /// </summary>
    /// <param name="value">Color</param>
    /// <param name="targetType">変換後の型</param>
    /// <param name="parameter">パラメータ</param>
    /// <param name="culture">対象カルチャ</param>
    /// <returns>文字列</returns>
    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (!(value is Color))
        {
            return default(string);
        }

        var color = ((Color)value);
        return color.ToString();
    }

    #endregion //IValueConverter
}

ちなみに ColorConverter はこんな感じ

あとは iOS の Main.cs、Windows Phone の App.xaml.cs にそれぞれ下記の記述を追加します

public class Application
{
    // This is the main entry point of the application.
    static void Main(string[] args)
    {
        // プラットフォーム依存のプロパティを注入
        App.Container
            .RegisterInstance<string>("HourHandColor", "#FF8469")    // 時針の色 
            .RegisterInstance<string>("MinuteHandColor", "#FF461C")  // 分針の色
            .RegisterInstance<string>("SecondHandColor", "#CC3816")  // 秒針の色
            .RegisterType<HandItemViewModel>(new InjectionProperty("Color", "#7F4234"))
            .RegisterType<TickItemViewModel>(new InjectionProperty("Color", "#7F4234")) // 目盛りの色
            .RegisterType<NumberItemViewModel>(new InjectionProperty("Color", "#7F230E"), new InjectionProperty("Number", 0)); // 数字の色

        // アプリケーション起動処理
        App.Current.OnLaunchApplication();

        // if you want to use a different Application Delegate class from "AppDelegate"
        // you can specify it here.
        UIApplication.Main(args, null, "AppDelegate");
    }
}

これは iOS

/// <summary>
/// Constructor for the Application object.
/// </summary>
public App()
{
    // Global handler for uncaught exceptions.
    UnhandledException += Application_UnhandledException;

    // プラットフォーム依存のプロパティを注入
    XamarinUnityInjection.App.Container
        .RegisterInstance<string>("HourHandColor", "#08007F")    // 時針の色 
        .RegisterInstance<string>("MinuteHandColor", "#0D00CC")  // 分針の色
        .RegisterInstance<string>("SecondHandColor", "#1000FF")  // 秒針の色
        .RegisterType<HandItemViewModel>(new InjectionProperty("Color", "#2C267F"))
        .RegisterType<TickItemViewModel>(new InjectionProperty("Color", "#2C267F")) // 目盛りの色
        .RegisterType<NumberItemViewModel>(new InjectionProperty("Color", "#584CFF"), new InjectionProperty("Number", 0)); // 数字の色

    // アプリケーション起動処理
    XamarinUnityInjection.App.Current.OnLaunchApplication();

    DisplayProperties.AutoRotationPreferences = DisplayOrientations.Landscape | DisplayOrientations.LandscapeFlipped | DisplayOrientations.Portrait | DisplayOrientations.PortraitFlipped;
    
    ~ 中略 ~
}

こちらは Windows Phone

Unity のプロパティ注入のカラーコードだけを変えています

/// <summary>
/// 最初の画面の ViewModel
/// </summary>
public class TopPageViewModel : BindableBase
{
    ~ 中略 ~

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="repository">リポジトリ(DI コンテナにより自動注入される)</param>
    public TopPageViewModel(IPageStateDetectService pageStateDetectService)
    {
        this.pageStateDetectService = pageStateDetectService;

        // Unity を利用して ViewModel のインスタンスを生成
        this.hourHand = App.Container.Resolve<HandItemViewModel>(
            new PropertyOverride("Color", new ResolvedParameter<string>("HourHandColor")));

        this.minuteHand = App.Container.Resolve<HandItemViewModel>(
            new PropertyOverride("Color", new ResolvedParameter<string>("MinuteHandColor")));

        this.secondHand = App.Container.Resolve<HandItemViewModel>(
            new PropertyOverride("Color", new ResolvedParameter<string>("SecondHandColor")));

        this.tickItems.Clear();
        for (var i = 0; i < 60; i++)
        {
            this.tickItems.Add(App.Container.Resolve<TickItemViewModel>());
        }

        this.numberItems.Clear();
        for (var i = 1; i < 13; i++)
        {
            this.numberItems.Add(App.Container.Resolve<NumberItemViewModel>(new PropertyOverride("Number", i)));
        }

        this.pageStateDetectService.PageAppearing += this.OnPageAppearing;
    }
}

最後に PageViewModel の ViewModel をインスタンス化している箇所

ここで Unity を利用してプロパティ注入しています

さてアプリを実行すると・・・

f:id:matatabi_ux:20141205080512p:plain

プラットフォームごとに配色が変わりました!