Xamarin.Formsでダークモード対応やってみた (Android/iOS)

Xamarin.FormsでAndroidとiOSのダークモード対応をやってみました。
2020.05.25

AndroidとiOSについて、それぞれAndroid 10とiOS 13からダークモード対応ができるようになりました。 特にiOSアプリはいつ必須になるか分かりませんので、早めの準備や対応をしておくと安心です。 というわけで、Xamarin.FormsでAndroidとiOSのダークモード対応をやってみました。

環境

  • Xamarin.Forms: 4.4.0.991265
  • Xamarin.Essentials: 1.5.3.2
  • Android: 10
  • iOS: 13.4.1

まずはアプリを作る

プロジェクトを新規作成する

プロジェクトを新規作成します。

Xamarin.Formsプロジェクトを作成する

シンプルにするめテンプレートは「空白」を選択します。

空白テンプレートを選択する

画面を作る

MainPage.xamlにSearchBarとButtonを追加します。あとiOS用にSaseArea設定も行います。

MainPage.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:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d"
             xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
             ios:Page.UseSafeArea="True"
             x:Class="DarkModeSample.MainPage">

    <StackLayout>
        <Label Text="Welcome to Xamarin.Forms!"
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand" />
        <Button Text="Apple"
                HorizontalOptions="Center"
                VerticalOptions="CenterAndExpand" />
    </StackLayout>
</ContentPage>

画面の様子

左側がライトモードで右側がダークモードの様子です。

Android

変化ありません。

対応前(Android)の様子

iOS

ダークモード時(右側)では、時刻表示が消えちゃいました(文字色が白色になっている)。

対応前(iOS)の様子

ダークモード対応やってみる

Xamarin.Essentialsの更新

Xamarin.Essentialsを最低でも1.4.0以上に更新します。

Xamairn.Essentialsを最新にする

リソースを作成する

Resourcesフォルダを作成

Resourcesフォルダを新規作成します。

Resourcesフォルダを作成する

Light用のテーマを作成

先ほど作成したResourceフォルダの配下にLightTheme.xamlファイルを新規作成します。 作りたいものはResourceDictionaryですが、直接作成できないため「コンテンツページ」を選択しています。 (xamlファイルとxaml.csファイルを作りたい)

LightTemeを作成する

ファイルの作成後、それぞれをResourceDictionaryに書き換えます。

LightTheme.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="DarkModeSample.Resources.LightTheme">
    <Color x:Key="PageBackgroundColor">White</Color>
    <Color x:Key="PrimaryTextColor">Black</Color>
    <Color x:Key="ButtonBackgroundColor">LightBlue</Color>
</ResourceDictionary>

LightTheme.xaml.cs

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace DarkModeSample.Resources
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class LightTheme : ResourceDictionary
    {
        public LightTheme()
        {
            InitializeComponent();
        }
    }
}

Dark用のテーマを作成

同様にDarkTheme.xamlを作成します。

DarkTheme.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="DarkModeSample.Resources.DarkTheme">
    <Color x:Key="PageBackgroundColor">Black</Color>
    <Color x:Key="PrimaryTextColor">White</Color>
    <Color x:Key="ButtonBackgroundColor">LightPink</Color>
</ResourceDictionary>

DarkTheme.xaml.cs

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace DarkModeSample.Resources
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class DarkTheme : ResourceDictionary
    {
        public DarkTheme()
        {
            InitializeComponent();
        }
    }
}

テーマの変更に追従する

テーマ変更の処理

App.xaml.csを下記にします。

App.xaml.cs

using DarkModeSample.Resources;
using Xamarin.Essentials;
using Xamarin.Forms;

namespace DarkModeSample
{
    public partial class App : Application
    {
        public App()
        {
            InitializeComponent();

            ApplyTheme();

            MainPage = new MainPage();
        }

        protected override void OnStart()
        {
        }

        protected override void OnSleep()
        {
        }

        protected override void OnResume()
        {
        }

        public static void ApplyTheme()
        {
            if (AppInfo.RequestedTheme == AppTheme.Dark)
            {
                App.Current.Resources = new DarkTheme();
            }
            else
            {
                App.Current.Resources = new LightTheme();
            }
        }
    }
}

テーマ変更を伝える (iOS)

iOSプロジェクトにPageRenderer.csを新規作成します。

PageRenderer.cs

using System;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ExportRenderer(typeof(ContentPage), typeof(DarkModeSample.iOS.PageRenderer))]
namespace DarkModeSample.iOS
{
    public class PageRenderer : Xamarin.Forms.Platform.iOS.PageRenderer
    {
        protected override void OnElementChanged(VisualElementChangedEventArgs e)
        {
            base.OnElementChanged(e);

            if (e.OldElement != null || Element == null) return;

            try
            {
                App.ApplyTheme();
            }
            catch (Exception)
            {
                // ignored
            }
        }

        public override void TraitCollectionDidChange(UITraitCollection previousTraitCollection)
        {
            base.TraitCollectionDidChange(previousTraitCollection);

            App.ApplyTheme();
        }
    }
}

ライトモードとダークモードが変化したとき、App.ApplyTheme()を呼んで反映させています。 Androidはこのような処理は不要でした。

動的リソースに対応する

画面の修正

MainPage.xamlについて、動的リソースを使うように修正します。

MainPage.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:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d"
             xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
             ios:Page.UseSafeArea="True"
             x:Class="DarkModeSample.MainPage"
             BackgroundColor="{DynamicResource PageBackgroundColor}">

    <StackLayout>
        <Label Text="Welcome to Xamarin.Forms!"
               TextColor="{DynamicResource PrimaryTextColor}"
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand" />
        <Button Text="Apple"
                BackgroundColor="{DynamicResource ButtonBackgroundColor}"
                HorizontalOptions="Center"
                VerticalOptions="CenterAndExpand" />
    </StackLayout>
</ContentPage>

画面の様子

左側がライトモードで右側がダークモードの様子です。イイカンジですね!

Android

対応後(Android)の様子

iOS

対応後(iOS)の様子

補足

Styleを使うと書き方を少し変えることができます。UIパーツとデザインの分離ができます。HTMLとCSSみたいですね。

MainPage.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:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d"
             xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
             ios:Page.UseSafeArea="True"
             x:Class="DarkModeSample.MainPage"
             BackgroundColor="{DynamicResource PageBackgroundColor}">

    <ContentPage.Resources>
        <ResourceDictionary>
            <Style TargetType="Label">
                <Setter Property="TextColor" Value="{DynamicResource PrimaryTextColor}" />
                <Setter Property="HorizontalOptions" Value="Center" />
                <Setter Property="VerticalOptions" Value="CenterAndExpand" />
            </Style>
            <Style x:Key="FooButtonStyle" TargetType="Button">
                <Setter Property="BackgroundColor" Value="{DynamicResource ButtonBackgroundColor}" />
                <Setter Property="HorizontalOptions" Value="Center" />
                <Setter Property="VerticalOptions" Value="CenterAndExpand" />
            </Style>
        </ResourceDictionary>
    </ContentPage.Resources>

    <StackLayout>
        <Label Text="Welcome to Xamarin.Forms!" />
        <Button Text="Apple"
                Style="{StaticResource FooButtonStyle}" />
    </StackLayout>
</ContentPage>

Stylex:Keyが未指定の場合はTargetTypeで指定した全ての要素に反映され、x:Keyを指定した場合は任意箇所のみ反映できます。

さいごに

Xamarin.Formsを使ってダークモード対応をしてみました。まだ試験実装中の機能を使えば、さらにシンプルにできそうですね。他に良い方法があれば教えていただけると助かります。

参考