しっぽを追いかけて

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

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

Xamarin.Forms で Microsoft Band のテーマカラー設定を取得する

今回からは着せ替え機能を攻めます!

Xamarin.Forms で Microsoft Band の下記のようなテーマカラーの設定を取得してみます

仕様などはこちらを参照ください

f:id:matatabi_ux:20150502083420p:plain

ソースコードの一式は下記にあります!

細かい実装などはこちらを参照ください

github.com

※ 順次改修していく予定なので、この記事の内容が現時点のソースより古い可能性があります


ソリューションで追加したものはこんな感じ

AndroidiOS とほぼ同じ構成なので割愛してます

共通プロジェクトに IBandPersonalizationImageManager を追加しているのは、着せ替え機能でやりとりする BandImage というクラスが直接インスタンス化できないため、別のクラスをやり取りに使いたいからです *1

このせいで Windows Phone 用の IBandPersonalizationImageManager 実装クラスも必要になりました;

そんなこんなでまずは Xamarin.Forms と各プラットフォーム間の変換ヘルパークラスを作ります

iOS 用の BandTheme、BandColor と共通プロジェクト用の BandTheme、BandColor を変換する処理を NativeBandThemeConvert に書きました

/// <summary>
/// iOS 用テーマカラーコンバーター
/// </summary>
public static class NativeBandThemeConvert
{
    /// <summary>
    /// 配色情報の変換
    /// </summary>
    /// <param name="color">ネイティブ配色情報</param>
    /// <returns>共通配色情報</returns>
    public static BandColor FromNative(Native.Personalization.BandColor color)
    {
        nfloat red, green, blue, alpha;
        color.UIColor.GetRGBA(out red, out green, out blue, out alpha);
        return new BandColor
        {
            R = (byte)(red * 255f),
            G = (byte)(green * 255f),
            B = (byte)(blue * 255f),
        };
    }

    /// <summary>
    /// 配色情報の変換
    /// </summary>
    /// <param name="color">共通配色情報</param>
    /// <returns>ネイティブ配色情報</returns>
    public static Native.Personalization.BandColor ToNative(BandColor color)
    {
        return Native.Personalization.BandColor.FromRgb(color.R, color.G, color.B);
    }
        
    /// <summary>
    /// テーマカラーの変換
    /// </summary>
    /// <param name="nativeColor">ネイティブテーマカラー情報</param>
    /// <returns>共通テーマカラー情報</returns>
    public static BandTheme FromNative(Native.Personalization.BandTheme theme)
    {
        return new BandTheme
        {
            Base = FromNative(theme.Base),
            HighContrast = FromNative(theme.HighContrast),
            Highlight = FromNative(theme.Highlight),
            Lowlight = FromNative(theme.Lowlight),
            Muted = FromNative(theme.Muted),
            SecondaryText = FromNative(theme.SecondaryText),
        };
    }

    /// <summary>
    /// テーマカラーの変換
    /// </summary>
    /// <param name="color">共通テーマカラー情報</param>
    /// <returns>ネイティブテーマカラー情報</returns>
    public static Native.Personalization.BandTheme ToNative(BandTheme theme)
    {
        return new Native.Personalization.BandTheme
        {
            Base = ToNative(theme.Base),
            HighContrast = ToNative(theme.HighContrast),
            Highlight = ToNative(theme.Highlight),
            Lowlight = ToNative(theme.Lowlight),
            Muted = ToNative(theme.Muted),
            SecondaryText = ToNative(theme.SecondaryText),
        };
    }
}

iOS 用の UIColor は RGB を 0~1 の float で表現するので、0~255 の byte に変換するのに (byte) (red * 255f) という計算をしているのがポイントです

これが Android だと

/// <summary>
/// Android 用テーマカラーコンバーター
/// </summary>
public static class NativeBandThemeConvert
{
    /// <summary>
    /// 配色情報の変換
    /// </summary>
    /// <param name="color">ネイティブ配色情報</param>
    /// <returns>共通配色情報</returns>
    public static BandColor FromNative(Color color)
    {
        return new BandColor
        {
            R = color.R,
            G = color.G,
            B = color.B,
        };
    }

    /// <summary>
    /// 配色情報の変換
    /// </summary>
    /// <param name="color">共通配色情報</param>
    /// <returns>ネイティブ配色情報</returns>
    public static Color ToNative(BandColor color)
    {
        return new Color(color.R, color.G, color.B);
    }

    /// <summary>
    /// テーマカラーの変換
    /// </summary>
    /// <param name="nativeColor">ネイティブテーマカラー情報</param>
    /// <returns>共通テーマカラー情報</returns>
    public static BandTheme FromNative(Native.Tiles.BandTheme theme)
    {
        return new BandTheme
        {
            Base = FromNative(theme.BaseColor),
            HighContrast = FromNative(theme.HighContrastColor),
            Highlight = FromNative(theme.HighContrastColor),
            Lowlight = FromNative(theme.LowlightColor),
            Muted = FromNative(theme.MutedColor),
            SecondaryText = FromNative(theme.SecondaryTextColor),
        };
    }

    /// <summary>
    /// テーマカラーの変換
    /// </summary>
    /// <param name="color">共通テーマカラー情報</param>
    /// <returns>ネイティブテーマカラー情報</returns>
    public static Native.Tiles.BandTheme ToNative(BandTheme theme)
    {
        return new Native.Tiles.BandTheme
        {
            BaseColor = ToNative(theme.Base),
            HighContrastColor = ToNative(theme.HighContrast),
            HighlightColor = ToNative(theme.Highlight),
            LowlightColor = ToNative(theme.Lowlight),
            MutedColor = ToNative(theme.Muted),
            SecondaryTextColor = ToNative(theme.SecondaryText),
        };
    }
}

かんたん!

次に IBandPersonalizationImageManager を実装した NativeBandPersonalizationManager

/// <summary>
/// iOS 用着せ替え管理クラス
/// </summary>
public class NativeBandPersonalizationManager : IBandPersonalizationImageManager
{
    /// <summary>
    /// 着せ替え管理クラス
    /// </summary>
    private Native.Personalization.IBandPersonalizationManager manager = null;

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public NativeBandPersonalizationManager(Native.BandClient client)
    {
        this.manager = client.PersonalizationManager;
    }

    ~ 中略 ~

    /// <summary>
    /// テーマカラーの取得
    /// </summary>
    /// <param name="cancel">中断トークン</param>
    /// <returns>テーマ情報</returns>
    [Obsolete("CancellationToken is not supported for iOS.")]
    public Task<BandTheme> GetThemeAsync(CancellationToken cancel)
    {
        return this.GetThemeAsync();
    }

    /// <summary>
    /// テーマカラーの取得
    /// </summary>
    /// <returns>テーマ情報</returns>
    public async Task<BandTheme> GetThemeAsync()
    {
        return NativeBandThemeConvert.FromNative(
            await Native.Personalization.BandPersonalizationManagerExtensions.GetThemeTaskAsync(this.manager));
    }

    ~ 中略 ~
}

NativeBandThemeConvert のおかげですっきりしました・・・AndroidWindows Phone の場合もほぼ同じです

さらに動作確認用に ViewModel を作ります

/// <summary>
/// 配色 ViewModel
/// </summary>
public class ColorViewModel : BindableBase
{
    /// <summary>
    /// 配色名選択肢
    /// </summary>
    public List<string> ColorLabelSelection { get; private set; }

    /// <summary>
    /// 配色選択肢
    /// </summary>
    public List<string> ColorSelection { get; private set; }

    /// <summary>
    /// 配色
    /// </summary>
    private string color = "Transparent";

    /// <summary>
    /// 配色
    /// </summary>
    public string Color
    {
        get { return this.color; }
        set { this.SetProperty<string>(ref this.color, value); }
    }

    /// <summary>
    /// 選択肢連番
    /// </summary>
    private int selecedIndex = 0;

    /// <summary>
    /// 選択肢連番
    /// </summary>
    public int SelecedIndex
    {
        get { return this.selecedIndex; }
        set
        {
            this.SetProperty<int>(ref this.selecedIndex, value);
            if (this.selecedIndex >= 0 && this.selecedIndex < this.ColorSelection.Count)
            {
                this.Color = this.ColorSelection[this.selecedIndex];
            }
        }
    }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public ColorViewModel()
    {
        this.ColorLabelSelection = new List<string>
        {
            "Strong blue",
            "Strong violet",
            "Strong pink",
            "Lime green",
            "Pure orange",
            "Dark gray",
            "Pure cyan",
            "Strong orange",
            "Strong magenta",
            "Strong green",
        };
        this.ColorSelection = new List<string>
        {
            "#FF3366cc",
            "#FF6633cc",
            "#FFcc3366",
            "#FF33cc66",
            "#FFff9900",
            "#FF999999",
            "#FF00ccff",
            "#FFff6600",
            "#FFcc33cc",
            "#FF99cc00",
        };
        this.Color = this.ColorSelection[this.selecedIndex];
    }
}

配色関連のあれこれをやってくれる ColorViewModel と

/// <summary>
/// 着せ替え設定 ViewModel
/// </summary>
public class PersonalizeViewModel : BindableBase
{
    /// <summary>
    /// 接続クライアント
    /// </summary>
    private IBandClient client = null;

    /// <summary>
    /// 着せ替え管理クラス
    /// </summary>
    private IBandPersonalizationImageManager manager = null;

    /// <summary>
    /// 設定状況取得コマンド
    /// </summary>
    public ICommand PullCommand { get; private set; }

    /// <summary>
    /// 設定適用コマンド
    /// </summary>
    public ICommand ApplyCommand { get; private set; }

    /// <summary>
    /// 処理中フラグ
    /// </summary>
    private bool isBusy = false;

    /// <summary>
    /// 処理中フラグ
    /// </summary>
    public bool IsBusy
    {
        get { return this.isBusy; }
        set { this.SetProperty<bool>(ref this.isBusy, value); }
    }

    #region ThemeColors

    /// <summary>
    /// 基本色
    /// </summary>
    private ColorViewModel baseColor = new ColorViewModel();

    /// <summary>
    /// 基本色
    /// </summary>
    public ColorViewModel BaseColor
    {
        get { return this.baseColor; }
        set { this.SetProperty<ColorViewModel>(ref this.baseColor, value); }
    }

    ~ 中略 ~

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="client">接続クライアント</param>
    [InjectionConstructor]
    public PersonalizeViewModel(IBandClient client, IBandPersonalizationImageManager manager)
    {
        this.IsBusy = true;
        this.client = client;
        this.manager = manager;

        this.PullCommand = DelegateCommand.FromAsyncHandler(this.Pull);
        this.ApplyCommand = DelegateCommand.FromAsyncHandler(this.Apply);

        this.BaseColor.SelecedIndex = 1;
        this.HighContrastColor.Color = "#FF9966ff";
        this.HighlightColor.Color = "#FF9966ff";
        this.LowlightColor.Color = "#FF6633cc";
        this.MutedColor.Color = "#FF663399";

        this.SecondaryTextColor.SelecedIndex = 5;

        this.IsBusy = false;
    }

    /// <summary>
    /// 設定状況取得
    /// </summary>
    /// <returns>Task</returns>
    private async Task Pull()
    {
        this.IsBusy = true;
        var theme = await this.manager.GetThemeAsync();

        this.BaseColor.Color = ColorToString(theme.Base);
        this.BaseColor.SelecedIndex = -1;
        this.HighContrastColor.Color = ColorToString(theme.HighContrast);
        this.HighlightColor.Color = ColorToString(theme.Highlight);
        this.LowlightColor.Color = ColorToString(theme.Lowlight);
        this.MutedColor.Color = ColorToString(theme.Muted);
        this.SecondaryTextColor.Color = ColorToString(theme.SecondaryText);
        this.SecondaryTextColor.SelecedIndex = -1;

        this.IsBusy = false;
    }

    /// <summary>
    /// BandColor をカラーコード文字列に変換する
    /// </summary>
    /// <param name="color">BandColor</param>
    /// <returns>カラーコード</returns>
    private static string ColorToString(BandColor color)
    {
        return string.Format("#FF{0}{1}{2}", color.R.ToString("X2"), color.G.ToString("X2"), color.B.ToString("X2"));
    }

    ~ 中略 ~
}

テーマカラーの情報を表示するための PersonalizeViewModel を作りました

情報取得中にボタンを抑止するための処理中フラグもつけています

最後に XAML

<?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:cv="clr-namespace:XamarinBandSample.Converters;assembly=XamarinBandSample"
             xmlns:c="clr-namespace:XamarinBandSample.Controls;assembly=XamarinBandSample"
             xmlns:t="clr-namespace:XamarinBandSample.Triggers;assembly=XamarinBandSample"
             xmlns:prismmvvm="clr-namespace:Prism.Mvvm;assembly=XamarinBandSample"
             prismmvvm:ViewModelLocator.AutoWireViewModel="true"
             x:Class="XamarinBandSample.Views.TopPage">
  <ContentPage.Resources>
    <ResourceDictionary>
      <cv:NegativeConverter x:Key="NegativeConverter"/>
      <cv:ColorConverter x:Key="ColorConverter"/>
    </ResourceDictionary>
  </ContentPage.Resources>

  <Grid>

  ~ 中略 ~

    <!-- Persomalize Info Pain-->
    <ScrollView IsVisible="{Binding ShowPersonalize}"
                Orientation="Vertical">
      <StackLayout Orientation="Vertical"
                   BindingContext="{Binding Personalize}"
                   Padding="10"
                   Spacing="10"
                   VerticalOptions="StartAndExpand">
        <StackLayout Orientation="Horizontal"
                     BindingContext="{Binding BaseColor}"
                     Spacing="10">
          <Label Text="Base" 
                 WidthRequest="100"
                 FontSize="Small"/>
          <BoxView WidthRequest="30"
                   HeightRequest="30"
                   BackgroundColor="{Binding Color, Converter={StaticResource ColorConverter}}"/>
          <c:BindablePicker ItemsSource="{Binding ColorLabelSelection}"
                            SelectedIndex="{Binding SelecedIndex, Mode=TwoWay}"/>
        </StackLayout>
        <StackLayout Orientation="Horizontal"
                     BindingContext="{Binding HighContrastColor}"
                     Spacing="10">
          <Label Text="HighContrast"
                 WidthRequest="100"
                 FontSize="Small"/>
          <BoxView WidthRequest="30"
                   HeightRequest="30"
                   BackgroundColor="{Binding Color, Converter={StaticResource ColorConverter}}"/>
        </StackLayout>
        <StackLayout Orientation="Horizontal"
                     BindingContext="{Binding HighlightColor}"
                     Spacing="10">
          <Label Text="Highlight"
                 WidthRequest="100"
                 FontSize="Small"/>
          <BoxView WidthRequest="30"
                   HeightRequest="30"
                   BackgroundColor="{Binding Color, Converter={StaticResource ColorConverter}}"/>
        </StackLayout>
        <StackLayout Orientation="Horizontal"
                     BindingContext="{Binding LowlightColor}"
                     Spacing="10">
          <Label Text="Lowlight"
                 WidthRequest="100"
                 FontSize="Small"/>
          <BoxView WidthRequest="30"
                   HeightRequest="30"
                   BackgroundColor="{Binding Color, Converter={StaticResource ColorConverter}}"/>
        </StackLayout>
        <StackLayout Orientation="Horizontal"
                     BindingContext="{Binding MutedColor}"
                     Spacing="10">
          <Label Text="Muted"
                 WidthRequest="100"
                 FontSize="Small"/>
          <BoxView WidthRequest="30"
                   HeightRequest="30"
                   BackgroundColor="{Binding Color, Converter={StaticResource ColorConverter}}"/>
        </StackLayout>
        <StackLayout Orientation="Horizontal"
                     BindingContext="{Binding SecondaryTextColor}"
                     Spacing="10">
          <Label Text="SecondaryText"
                 WidthRequest="100"
                 FontSize="Small"/>
          <BoxView WidthRequest="30"
                   HeightRequest="30"
                   BackgroundColor="{Binding Color, Converter={StaticResource ColorConverter}}"/>
          <c:BindablePicker ItemsSource="{Binding ColorLabelSelection}"
                            SelectedIndex="{Binding SelecedIndex, Mode=TwoWay}"/>
        </StackLayout>

        <StackLayout Orientation="Horizontal"
                       Padding="10"
                       Spacing="10"
                       HorizontalOptions="End">
          <Button Text="Pull"
                  Command="{Binding PullCommand}"
                  VerticalOptions="Center"
                  FontSize="Medium"/>
          <Button Text=" Apply"
                  Command="{Binding ApplyCommand}"
                  VerticalOptions="Center"
                  FontSize="Medium"/>
        </StackLayout>
      </StackLayout>

    </ScrollView>
  </Grid>

</ContentPage>

BoxView の BackgroundColor に取得した色をバインドするようにしています・・・とりあえず実行!

f:id:matatabi_ux:20150501204437p:plain f:id:matatabi_ux:20150503160442p:plain

配色の設定がとれました!

*1:もともとは IBandPersonalizationManager というインターフェースが IBandPersonalizationManager.SetMeTileImageAsync(BandImage image) みたいなメソッドを強制するのですが、IBandImage ではなく BandImage になっているので手出しできなかったという・・・