Xamarin.Forms の Theme ResourceDictionary を使いこなそう!

  • 17
    Like
  • 0
    Comment

これは Xamarin Advent Calendar 2016 (その1) の 6日目の記事です。

Xamarin.Forms の ResourceDictionary XAML ファイルの記述について説明します。この記事は Xamarin.Forms 2.3.3 時点の情報に基づいて説明しています。

Xamarin.Forms では ResourceDictionary へ色やサイズなどの情報を定義し、アプリ全体の共通スタイル指定をしたり、XAML からスタイルとして参照することができます。スタイル指定を集約することでアプリデザインを統一したり、スタイルの切替ができるようにしたりします。いわゆる Theme と呼ばれるものですが、その機能は XAML と ResourceDictionary クラスを用いて実現しています。ResourceDictionary の使い方や目的は Android の Theme と似ています。

Xamarin.Forms への ResourceDictionary ファイルの導入

まずは ResourceDictionary XAML を使えるようにします。PCL プロジェクトへ Resources/AppTheme.xaml を作成し、ResourceDictionary を継承した AppTheme クラスを用意します。

Xamarin Studio では XAML + CS ファイルを新規作成するメニューがないため、 New File > Forms > Forms ContentView Xaml で ContentView クラスの .xaml と .xaml.cs を作成したあとでそれぞれのファイルを ResourceDictionary クラスへ書き換えます。

creation.png

solution.png

AppTheme.xaml

<?xml version="1.0" encoding="UTF-8"?>
<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    x:Class="XFTheme.Resources.AppTheme">
  <Color x:Key="FontColor1">#ff0000</Color>
  <Color x:Key="FontColor2">#00ff00</Color>
</ResourceDictionary>

AppTheme.xaml.cs

using System;
using System.Collections.Generic;
using Xamarin.Forms;

namespace XFTheme.Resources
{
  public partial class AppTheme : ResourceDictionary
  {
    public AppTheme()
    {
      InitializeComponent();
    }
  }
}

App.xaml から以下のようにリソースファイルを読み込みます。

公式のドキュメント(Resource Dictionaries)では Application.Resources に対して MergedWith でリソース指定する例がありますが、StaticResource では Application クラスの MergedWith を参照できないバグがあるため、 MergedWith の使用は避けています。

※ MergedWith を使用する場合は DynamicResource 指定であれば正常に動作します。

<?xml version="1.0" encoding="utf-8"?>
<Application xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:theme="clr-namespace:XFTheme.Resources;assembly=XFTheme"
    x:Class="XFTheme.App">
  <Application.Resources>
    <!--
    Application.ResourcesでMergedWithは使わない
    See : 42264 – MergedWith doesn't work at application level - https://bugzilla.xamarin.com/show_bug.cgi?id=42264
    -->
    <theme:AppTheme/>
  </Application.Resources>
</Application>

これで、各 View の XAML からスタイルを読み込むことができます。ResourceDictionary 内のスタイルを使用するときには XAML 中から StaticResource または DynamicResource で参照します。

<?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:local="clr-namespace:XFTheme"
    x:Class="XFTheme.XFThemePage">
  <Label Text="FontColor1 Text..." TextColor="{StaticResource FontColor1}" FontSize="20" VerticalOptions="Center" HorizontalOptions="Center" />
</ContentPage>

screen.png

Label に AppTheme.xaml で定義した FontColor1 が設定されています。

Theme ファイルへの値の定義例

Theme (Resource Dictionary) XAML ファイルでのさまざまなクラスの定義例を説明します。

Color 定義

<!-- HEX指定 (Color.FromHex()) -->
<Color x:Key="Color1">#ff00ffff</Color>
<Color x:Key="Color2">#ffff00</Color>
<!-- Color 定義済み色指定 -->
<Color x:Key="Color3">Orange</Color>
<Color x:Key="Color4">Color.Orange</Color>

上記の定義によるパース処理の詳細は ColorTypeConverter クラスと Color クラスの実装を参照してください。

FontAttributes 定義

<FontAttributes x:Key="FontAttr1">None</FontAttributes>
<FontAttributes x:Key="FontAttr2">Bold</FontAttributes>
<FontAttributes x:Key="FontAttr3">Italic</FontAttributes>

FontAttributes はフォントの装飾を指定する列挙体です。列挙体の名称を指定することで FontAttributes を定義できます。

Thickness / Point / Rectangle 定義

<Thickness x:Key="Margin1">30,30,0,0</Thickness>
<Point x:Key="Point1">30,30</Point>
<Rectangle x:Key="Rect1">0,0,30,30</Rectangle>

View の Margin などに使用される Thickness です。上記記述は ThicknessTypeConverter でパースされ、Thickness クラスが生成されます。その他にも PointRectangle も定義できます。パース可能な引数のフォーマットは各 TypeConverter クラスを確認してください。

Double / Int32 / String... 定義

<x:Double x:Key="FontSize1">19</x:Double>
<x:Int32 x:Key="Height1">20</x:Int32>
<x:String x:Key="Value1">Hogehoge</x:String>

XAML に定義がなくても、C# のクラスをリソースとして定義することができます。XAML Markup Extensions にオブジェクト生成の解説があります。

OnPlatform

<OnPlatform x:Key="FontFamily1"
  x:TypeArguments="x:String"
  iOS="HelveticaNeue"
  Android="sans-serif"/>

OnPlatform クラスにより、任意の値をプラットフォーム別に定義することができます。x:TypeArguments によるジェネリッククラス生成は Essential XAML Syntax に記載があります。

Explicit Style (明示的スタイル)

<Style x:Key="LabelStyle1" TargetType="Label">
  <Setter Property="FontAttributes" Value="Bold"/>
  <Setter Property="FontSize" Value="19"/>
  <Setter Property="TextColor" Value="Red"/>
</Style>

Style クラスにより、View のプロパティをまとめて指定することができます。x:Key を指定することで名前付きの(明示的な) Style クラスとなります。TargetType に Style を適用したい対象クラスを指定します。Setter には 対象クラスに存在するどのプロパティでも指定することができます。

上記の LabelStyle1 は View の XAML 中から以下のように使用します。

<Label Text="LabelStyle1 Label..." Style="{StaticResource LabelStyle1}"/>

ResourceDictionary 定義の中でも XML属性(Attributes) には StaticResource または DynamicResource により定義済みのリソースを使用することができます。

<Style x:Key="LabelStyle2" TargetType="Label">
  <Setter Property="FontAttributes" Value="{DynamicResource FontAttr2}"/>
  <Setter Property="FontSize" Value="{DynamicResource FontSize1}"/>
  <Setter Property="TextColor" Value="{DynamicResource Color3}"/>
</Style>

Style クラスは BasedOn 指定により、別の Style クラスを継承することができます。継承については Style Inheritance に記載があります。BasedOn 指定には DynamicResource 指定は使えないようです。

<Style x:Key="LabelStyle3" TargetType="Label" BasedOn="{StaticResource LabelStyle2}">
  <Setter Property="FontAttributes" Value="{DynamicResource FontAttr3}"/>
</Style>

Implicit Style (暗黙的スタイル)

<Style TargetType="Button">
  <Setter Property="BackgroundColor" Value="Blue"/>
  <Setter Property="BorderRadius" Value="2"/>
  <Setter Property="FontAttributes" Value="Bold"/>
  <Setter Property="FontSize" Value="20"/>
</Style>

x:Key 属性を省くと暗黙的なスタイル指定になります。この指定では、View の XAML から Style を指定しなくても、アプリ全体の Button クラスのプロパティへ暗黙的に上記の値が設定されます。

アプリ Theme の差し替えとリアルタイム Theme 切替

ここまで単一の AppTheme.xaml だけを使用してきましたが、複数の Theme ファイルを用意することで、アプリ全体の Theme を複数用意して、それらを切り替えることが可能です。

アプリ全体のテーマを差し替えたいときは、C# コードから (PCL) Application.Resources を差し替えます。

App.xaml.cs

protected override async void OnStart()
{
  // App.xaml により Resources = AppTheme が設定されている状態で開始

  if (OtherThemeを使いたいとき)
  {
    Resources = new OtherTheme(); // リソース差し替え
  }
...
(Theme差し替え後に画面生成処理)
  MainPage = new XFThemePage();
...
}

今回は、OtherTheme クラスは AppTheme クラスを継承して、AppTheme クラスからの差分だけを定義するようにしてみます。MergedWith により OtherTheme クラスへ AppTheme クラスを統合します。このとき、OtherTheme と AppTheme とで同じ x:Key 項目を定義すると、OtherTheme の定義が優先して使用されます。OtherTheme に存在しない x:Key 項目を参照したときは、AppTheme の定義が使われます。OtherTheme クラスで値を上書きする項目について、AppTheme.xaml ファイル内では {DynamicResource ...} の記述により参照する必要があります。StaticResource 指定では OtherTheme.xaml による上書きが反映されません。

上記の実装であれば画面生成前に ResourceDictionary が差し替わるため、View の XAML 内では StaticResource の記述で構わないはずです。しかし前述のとおり StaticResource 指定では MergedWith で統合された項目を参照できないバグがあるため、View の XAML 内でも DynamicResource を使用することになります。

OtherTheme.xaml

<?xml version="1.0" encoding="UTF-8"?>
<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
  xmlns:theme="clr-namespace:XFTheme.Resources;assembly=XFTheme"
  MergedWith="theme:AppTheme"
  x:Class="XFTheme.Resources.OtherTheme">
  <!-- AppThemeから上書きしたい項目だけ記載する -->
  <Color x:Key="Color1">#ff00ff00</Color>
  <Color x:Key="Color2">#ff0000</Color>
  <x:Double x:Key="FontSize1">23</x:Double>
</ResourceDictionary>

AppTheme.xaml では以下のように DynamicResource 指定で参照します。

<Style x:Key="LabelStyle4" TargetType="Label">
  <Setter Property="FontSize" Value="{DynamicResource FontSize1}"/>
  <Setter Property="TextColor" Value="{DynamicResource Color1}"/>
</Style>

StaticResource と DynamicResource

StaticResource は XAML のパース時に値が決定します。DynamicResource は x:Key 参照を保持しており、View 生成時や参照先の ResourceDictionary が変更されたときに値を設定します。

XAML 上ではリソースが変更されないのであれば StaticResource を使用し、変更される場合は DynamicResource を使用するということになります。

(参考) ただし、実際には前述の StaticResource の MergedWith に関するバグがあったり、DynamicResource 指定による変更通知がうまく動かないことはあります。どちらかといえば DynamicResource であればうまく動作することが多いのですが、サードパーティのライブラリでは DynamicResource の変更通知に対応できていないものがあったりします。

リアルタイム Theme 切替

DynamicResource を用いてアプリが動作中であり画面が表示されている状態でも、Theme を切り替えてリアルタイムに表示へ反映することができます。

AppTheme.xaml と OtherTheme.xaml を用意し、アプリ全体で View の XAML 内で DynamicResource 指定を使うようにしてから、(PCL) Application クラスへ以下のメソッドを追加します。

App.xaml.cs

public void ApplyAppTheme() {
  MainPage.Parent = null;
  Resources = new AppTheme();
  MainPage.Parent = this;  // Parent を再設定することにより View 内の DynamicResource プロパティを更新させる
}

public void ApplyOtherTheme() {
  MainPage.Parent = null;
  Resources = new OtherTheme();
  MainPage.Parent = this;  // Parent を再設定することにより View 内の DynamicResource プロパティを更新させる
}

DynamicResource の仕組みであれば Application.Resources を差し替えるだけで View の DynamicResource プロパティが更新されるはずですが、Resources の差し替えだけでは画面へ反映されないことがありました。表示中の View の Parent へ一度 null を代入後に再設定することで強制的に DynamicResource プロパティの更新を発生させています。

これで、画面を表示中に上記メソッドを呼び出すことでリアルタイムに AppTheme と OtherTheme を切り替えることができます。

theme.gif

サンプルプロジェクト

リアルタイム Theme 切替まで対応したアプリのサンプルプロジェクトを GitHub へ用意しています。

本記事の内容と同一ですが、サンプルプロジェクトの AppTheme.xaml は以下のようになりました。

<?xml version="1.0" encoding="UTF-8"?>
<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
  x:Class="XFTheme.Resources.AppTheme">
  <Style TargetType="Label">
    <Setter Property="FontSize" Value="20"/>
  </Style>

  <Style x:Key="LabelStyle1" TargetType="Label">
    <Setter Property="FontAttributes" Value="Bold"/>
    <Setter Property="FontSize" Value="19"/>
    <Setter Property="TextColor" Value="Red"/>
  </Style>

  <Style x:Key="LabelStyle2" TargetType="Label">
    <Setter Property="FontAttributes" Value="{DynamicResource FontAttr2}"/>
    <Setter Property="FontSize" Value="{DynamicResource FontSize1}"/>
    <Setter Property="TextColor" Value="{DynamicResource Color3}"/>
  </Style>

  <Style x:Key="LabelStyle3" TargetType="Label" BasedOn="{StaticResource LabelStyle2}">
    <Setter Property="FontAttributes" Value="{DynamicResource FontAttr3}"/>
    </Style>

  <Style x:Key="LabelStyle4" TargetType="Label">
    <Setter Property="FontSize" Value="{DynamicResource FontSize1}"/>
    <Setter Property="TextColor" Value="{DynamicResource Color1}"/>
  </Style>

  <Style TargetType="Button">
    <Setter Property="BackgroundColor" Value="{DynamicResource Color5}"/>
    <Setter Property="BorderRadius" Value="2"/>
    <Setter Property="FontAttributes" Value="Bold"/>
    <Setter Property="FontSize" Value="20"/>
  </Style>

  <Color x:Key="FontColor1">#ff0000</Color>
  <Color x:Key="FontColor2">#00ff00</Color>
  <!-- HEX指定 (Color.FromHex()) -->
  <Color x:Key="Color1">#ff00ffff</Color>
  <Color x:Key="Color2">#ffff00</Color>
  <!-- Color 定義済み色指定 -->
  <Color x:Key="Color3">Orange</Color>
  <Color x:Key="Color4">Color.Orange</Color>
  <Color x:Key="Color5">Blue</Color>
  <Color x:Key="Color6">#ccffcc</Color>

  <FontAttributes x:Key="FontAttr1">None</FontAttributes>
  <FontAttributes x:Key="FontAttr2">Bold</FontAttributes>
  <FontAttributes x:Key="FontAttr3">Italic</FontAttributes>

  <Thickness x:Key="Margin1">50,0,50,0</Thickness>
  <Point x:Key="Point1">30,30</Point>
  <Rectangle x:Key="Rect1">0,0,30,30</Rectangle>

  <x:Double x:Key="FontSize1">19</x:Double>
  <x:Int32 x:Key="Height1">20</x:Int32>
  <x:String x:Key="Value1">Hogehoge</x:String>

  <OnPlatform x:Key="FontFamily1"
    x:TypeArguments="x:String"
    iOS="HelveticaNeue"
    Android="sans-serif"/>
</ResourceDictionary>

まとめ

Xamarin.Forms の Theme は ResourceDictionary と XAML を中心として StaticResource や DynamicResource や Style クラスなど複数の仕組みを組み合わせて実現しています。

公式のドキュメントはそれぞれの機能ごとに説明が分散しており、なかなか全体像が掴めませんでした。ResourceDictionary を ContentPage.xaml や App.xaml の先頭へ直接埋め込むのではなく、独立した XAML ファイルにまとめる実装についても説明がありません。StaticResource と DynamicResource の実装依存のバグもあり頭を抱えることが多かったのですが、ようやく実践的なノウハウとしてまとめられました。

Xamarin がオープンソースであるおかげで、ドキュメントが分かりにくくても本家のソースコードを読めばなんとかなるし、個々の仕組みの設計思想も理解できるような気がするので非常にありがたいです。

参照

This post is the No.6 article of Xamarin Advent Calendar 2016