ぴーさんログ

だいたいXamarin.Formsのブログ

【Xamarin.Forms 2.3.4-pre】ちゃんとBindableになったPicker

Xamarin.Forms 2.3.4-preでPickerがItemsSourceとItemSelectedをサポートするようになります。すっごーい!

さっそくこんな感じのViewModelを用意してContentPageのBindingContextにセットします。

public class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public IEnumerable<AnimalFriend> Friends { get; }

    AnimalFriend _bestFirend;
    public AnimalFriend BestFriend
    {
        get { return _bestFirend; }
        set
        {
            _bestFirend = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BestFriend)));
        }
    }

    public ViewModel()
    {
        Friends = new[]{
            new AnimalFriend { Name = "サーバル" },
            new AnimalFriend { Name = "ジャガー" },
            new AnimalFriend { Name = "トキ"},
            new AnimalFriend { Name = "ツチノコ"},
        };
    }
}

public class AnimalFriend
{
    public string Name { get; set; }
}

ContentPageの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"
             x:Class="XFApp44.BindablePickerPage">
    <ContentPage.Content>
        <StackLayout Padding="15, 40, 15, 20">
            <Picker HeightRequest="40"
                    Title="いちばんのともだちをおしえてね"
                    ItemsSource="{Binding Friends}"
                    ItemDisplayBinding="{Binding Name, StringFormat='{0}ちゃん'}"
                    SelectedItem="{Binding BestFriend, Mode=OneWayToSource}"/>
            <Label Text="{Binding BestFriend.Name, StringFormat='{0}ちゃんととっても仲良しなんだね、すっごーい!'}" />
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

ItemsSourceの各アイテムをPickerに表示する際にはPicker.ItemDisplayBindingを使って適切なプロパティPathを指定することが可能です。 Binding構文で指定するのでStringFormatも使えます。かしこーい。

f:id:ticktack623:20170207011818j:plain

【Xamarin.Forms 2.3.4-pre】新しいOnPlatformメカニズム

Xamarin.Forms 2.3.4-preで新しいOnPlatformの仕組みが導入され、従来の物は非推奨となります。

概要

  • 動作プラットフォームを文字列で判別するアプローチに変更(従来はenum)
  • Xamarin.Formsがサポートしていないプラットフォームへの対応が容易に(Tizenとか)
  • XAMLのOnPlatformで複数のプラットフォームに対してまとめて値を設定可能に
  • Device.OnPlatform(), Device.OnPlatform<T>() は実質廃止

RuntimePlatform

これまで、実行中のプラットフォームを取得するにはenum TargetPlatform型のDevice.OSプロパティを使用して来ました。

これを置き換える形でstring型のDevice.RuntimePlatformプロパティが新しく定義されます。

(Xamarin.Forms本体に定義されていない独自のプラットフォームを追加しやすくする意図があるみたい、Tizenとか?)

Xamarin.FormsがサポートするプラットフォームはDeviceクラスの定数として定義されます。

public const string iOS = "iOS";
public const string Android = "Android";
public const string WinPhone = "WinPhone";
public const string Windows = "Windows";
public const string macOS = "macOS";

つまり動作プラットフォームの判別は以下のように変わります。

// 従来のコード(obsolete)
Deviec.OS == TargetPlatform.Android

// これから推奨されるコード
Device.RuntimePlatform == Device.Android

Device.OnPlatformの廃止

Device.OnPlatform()、Device.OnPlatform<T>()がobsoleteとなり、代わりにswitch文とRuntimePlatformを組み合わせて使う事が推奨されます。

// 従来のコード(obsolete)
Device.OnPlatform(
    () => { /* iOS */ },
    () => { /* Android */ },
    () => { /* WinPhone */ },
    () => { /* Default */ });

label.Text = Device.OnPlatform<string>("iOS", "Android", "WinPhone");

// これから推奨されるコード
switch(Device.RuntimePlatform)
{
    case Device.iOS:
        /* iOS */ break;
    case Device.Android:
        /* Android */ break;
    case Device.WinPhone:
        /* WinPhone */ break;
    case Device.Windows:
        /* Windows */ break;
    case Device.macOS:
        /* macOS */ break;
    case "SomethingNewPlatform":
        /* 独自プラットフォームの場合の処理 */ break;
    default:
        /* default */
}

Xamarin.Formsがサポートしていないプラットフォームへも対応しやすくなっていますね。

OnPlatform<T>クラスの仕様変更

XAMLでプラットフォームごとの値を設定するのに使われていたOnPlatform<T>クラスの使い方が変わります。

これまでのOnPlatform<T>のプロパティとして各プラットフォームごとの値を設定していた代わりに、子要素として持たせる形になります。

<!-- 従来のコード(obsolete) -->
<OnPlatform x:TypeArguments="x:Double">
    <OnPlatform.iOS>20.0</OnPlatform.iOS>
    <OnPlatform.Android>42.0</OnPlatform.Android>
</OnPlatform>

<!-- これから推奨されるコード -->
<OnPlatform x:TypeArguments="x:Double">
    <On Platform="iOS">20.0</On>
    <!-- 複数プラットフォームを指定可能 -->
    <On Platform="Android, Windows">42.0</On>
</OnPlatform>

子要素となるOnクラスのOn.Platformにはカンマ区切りで同時に複数のプラットフォームを指定できます。ここに指定する名前はRuntimePlatformのに使われる文字列と対応します。

Xamarin.Forms.TabbedPageのiOS版でタブを上側に変更するサンプル

teratailで回答したやつ。

Xamarin - Xamarin.FormsのTabbedページのUIをiOSとAndroidで揃えたい(62739)|teratail

スクショ

f:id:ticktack623:20170121111728j:plain

f:id:ticktack623:20170121111738j:plain

stackoverflowの回答を参考にしています。

ios - Positioning UITabBar at the top - Stack Overflow

Xamarin.iOS側のプロジェクトに Xamarin.Forms.Platform.iOS.TabbedRenderer の派生クラスを作ります。

TabbedRendererはUITabBarControllerから派生しているので、ViewWillLayoutSubviews() をoverrideして処理を追加します。

using System;
using UIKit;
using Xamarin.Forms;

[assembly: ExportRenderer(typeof(TabbedPage), typeof(TopTabbarSample.iOS.TopTabbedRenderer))]

namespace TopTabbarSample.iOS
{
    /// <summary>
    /// iOSのTabbedPageのタブバーを上側に表示するためのRenderer
    /// </summary>
    public class TopTabbedRenderer : Xamarin.Forms.Platform.iOS.TabbedRenderer
    {
        // stackoverflowの回答を元にタブバーの位置を上に変更 http://stackoverflow.com/questions/29579992/positioning-uitabbar-at-the-top
        public override void ViewWillLayoutSubviews()
        {
            base.ViewWillLayoutSubviews();

            TabBar.InvalidateIntrinsicContentSize();

            var orientation = UIApplication.SharedApplication.StatusBarOrientation;

            nfloat tabSize = 44.0f;

            if(orientation == UIInterfaceOrientation.LandscapeLeft ||
               orientation == UIInterfaceOrientation.LandscapeRight)
            {
                tabSize = 32.0f;
            }

            var tabFrame = TabBar.Frame;
            tabFrame.Height = tabSize;
            tabFrame.Y = View.Frame.Y;
            TabBar.Frame = tabFrame;

            // 強制的にぼかしを再描画する小技らしい
            TabBar.Translucent = false;
            TabBar.Translucent = true;
        }
    }
}

動作確認用ページ

<?xml version="1.0" encoding="utf-8"?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
            xmlns:local="clr-namespace:TopTabbarSample"
            x:Class="TopTabbarSample.TopTabbarSamplePage">
    <TabbedPage.ItemsSource>
        <x:Array Type="{x:Type x:String}">
            <x:String>First</x:String>
            <x:String>Second</x:String>
            <x:String>Third</x:String>
        </x:Array>
    </TabbedPage.ItemsSource>
</TabbedPage>

GitHubにも置いておきます。

Xamarin.Androidでアプリのビルド時にJavaのヒープがあふれる時の対処

Xamarin.Androidでアプリのビルド時に...

java.lang.OutOfMemoryError. Consider increasing the value of $(JavaMaximumHeapSize). Java ran out of memory while executing 'java.exe -jar ~'

のようにJavaのヒープ領域があふれてエラーになったら

対処

Xamarin.Androidプロジェクトの設定からヒープ領域のサイズを変更できます。

Android Options > Advanced > Advanced Android Build Settings > Java Max Heap Size

f:id:ticktack623:20170113142622j:plain

取り合えず「1G」にして足りなかったら上げればOK(そこまで必要な場合あるのかな?)

【Xamarin.Forms 2.3.3】 Platform Specifics

Xamarin.Forms 2.3.3で Platform Specifics という機能が追加されました。

これはPCLなどの共通コードからプラットフォーム固有の機能を呼び出したりする類のものです。(ただしコードビハインド限定)

Forumでの紹介文によると...

Introducing Platform Specifics! Features or behaviors that apply to one platform only can now be implemented without requiring custom renderers. These new features/behaviors can then be accessed easily via a fluent code API or XAML.

Vendors can easily add their own Platform Specifics by attaching Effects to them (see 63a924d and 1f9482e for complete example).

This feature implements the framework that enables the new API and also includes several examples of platform specific features, which can be previewed using the Platform Specifics gallery page:

  • Blur support for any VisualElement on iOS
  • Translucent navigation bar on iOS
  • Partially collapsed navigation bar (with icons!) on MasterDetailPage on Windows
  • Toolbar placement options on Windows
  • AdjustResize/AdjustPan on Android (known issue: AdjustResize disables status bar color)

とのこと。

Custom RendererやEffectsを使ってやるようなことが簡単にできる風に書かれていますね。

サンプル

さっぱり分からないと思うのでサンプルコードを用意しました。

using System;
using Xamarin.Forms;
using Xamarin.Forms.PlatformConfiguration;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;

namespace PratformSpecificsSample
{
    public class MyNaviPage : Xamarin.Forms.NavigationPage
    {
        public MyNaviPage()
        {
            BackgroundColor = Color.Pink;

            var button = new Button
            {
                Text = "Toggle Translucent",
                BackgroundColor = Color.Silver,
            };

            button.Clicked += (sender, args) => On<iOS>()
                .SetIsNavigationBarTranslucent(!On<iOS>().IsNavigationBarTranslucent());

            var content = new ContentPage
            {
                Title = "iOS Translucent Navigation Bar",
                Content = new StackLayout
                {
                    VerticalOptions = LayoutOptions.Center,
                    HorizontalOptions = LayoutOptions.Center,
                    Children = { button },
                }
            };

            PushAsync(content);
        }
    }
}

このコードを実行するとこんな感じになります。

f:id:ticktack623:20170113004240g:plain

ボタンをタップする度に、ナビゲーションバーの透過を切り替えるというiOS固有の効果が発動しています。

Platform Specifics が使われているのはこの部分、

button.Clicked += (sender, args) => On<iOS>()
    .SetIsNavigationBarTranslucent(!On<iOS>().IsNavigationBarTranslucent());

やっていることは On<iOS>().SetIsNavigationBarTranslucent() が透過ON/OFFのセット、 On<iOS>().IsNavigationBarTranslucent() が現在の透過状態の取得ですね。

Element.On<T>() を入り口として、「対象Elementとプラットフォームの組み合わせという文脈に固有な機能をPCLから呼び出せる」というのが Platform Specifics の意義といえるでしょう。

BoxViewの例

例えば、BoxViewに続けて .On<iOS>(). と入力するとインテリセンスから GetBlurEffect()UseBlurEffect() が呼び出せて...

f:id:ticktack623:20170113004413j:plain

.On<Android>(). には呼び出せるものが無い、といった塩梅です。

f:id:ticktack623:20170113004436j:plain

使い方まとめ

1. usingを追加

using Xamarin.Forms.PlatformConfiguration;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
using Xamarin.Forms.PlatformConfiguration.AndroidSpecific;
using Xamarin.Forms.PlatformConfiguration.WindowsSpecific;

2. だいたいクラス名衝突が発生するので修正する

これはPlatform Specificsの実装が悪い。

なぜNavigationPageに対する拡張メソッドを定義するクラスを「NavigationPage」にしてしまうのか……

3. Element(ApplicationとかPageとかViewのこと)の .On<TIConfigPlatform>() を呼ぶ

Xamarin.Forms 2.3.3時点でTIConfigPlatformに入れられるのは以下の3種類。(GitHubのコード上ではTizenも追加されているので2.3.4には入ってくるかも)

public sealed class Android : IConfigPlatform { }
public sealed class iOS : IConfigPlatform { }
public sealed class Windows : IConfigPlatform { }

4. インテリセンスで使いたい機能を選ぶ

TElement.On<TIConfigPlatform>() の戻り値は IPlatformElementConfiguration<TIConfigPlatform, TElement> のインスタンスとなっており、プラットフォーム固有機能は拡張メソッドで実装されています。つまり後付けなので自分で実装するのもアリ。

ライブラリ開発者向け情報

Xamarin.Formsのカスタムコントロールなどを配布する場合、IElementConfiguration<TElement>を実装してPlatform Specificsのエントリポイント用意してあげるといいかもしれない。

(プラットフォーム固有機能は拡張メソッドで生やせるけども、エントリポイントはコントロールで実装しなければならないため)

Xamarin.Forms.Tizenをハックして地図を表示してみる

この記事は Xamarin(その2) Advent Calendar 2016 20日目の記事です。

Xamarin.FormsにTizenが参戦!?

Connect(); // 2016 のキーノートにて電撃的にXamarin.Forms.Tizenが発表され、一部界隈に衝撃を与えました。

衝撃を受ける様子

当惑ぶりが伺えますね。

さて、TizenでXamarin.Formsアプリを動かしてみよう!的な内容は既になかしょさん(@nakasho_devがやってくれているのでここでは省略します。

nakasho-dev.hatenablog.jp

ここ見ればすんなりいけます。

解析だ!

さあ、TizenでXamarin.Formsアプリが動く事は分かりました。

次はなぜ動くのかが気になるのでソイツを調べてみましょう。

テンプレートから生成されたプロジェクトはどうやら.NET Core Appを吐くようです。(そもそもキーノートの発表的には「Tizenが.NET Coreに対応したよ」という内容なので当然か)

f:id:ticktack623:20161219204517j:plain

projecto.jsonの内容はこんな感じ。

{
    "buildOptions": {
        "emitEntryPoint": true,
        "debugType": "portable",
        "platform": "AnyCPU",
        "preserveCompilationContext": true
    },
    "dependencies": {
        "Microsoft.NETCore.App": {
            "version": "1.0.0"
        },
        "Tizen.Library": "1.0.0-pre1",
        "Xamarin.Forms": "2.3.3.163-pre3",
        "Xamarin.Forms.Platform.Tizen": "2.3.3.163-pre3"
    },
    "runtimes": {
        "win": {},
        "linux": {}
    },
    "frameworks": {
        "netcoreapp1.0": {
            "imports": [
                "portable-net45+wp80+win81+wpa81",
                "netstandard1.6"
            ]
        }
    }
}

使用しているNuGetパッケージ(dependencies辺り)に注目します。

Xamarin.Forms本体はフォークではなく、本家をそのまま参照している模様。

Xamarin.Forms.Platform.TizenTizen.Library が気になるのでNuGet Gallaryを覗きに行きます。

Xamarin.Forms.Platform.Tizen のページ

f:id:ticktack623:20161217231126j:plain

オーナーがtizenプロジェクト、オーサーがSamsungとなっているのでバグ報告などはそちらに投げましょう。次!

Tizen.Library のページ

f:id:ticktack623:20161219204633j:plain

こちらはTizenのAPIを.NET Frameworkから呼ぶためのラッパーの集合体っぽいですね。

Dependenciesに並んでいる名前もTizenアーキテクチャ図に乗っているものとほぼ一致します。

f:id:ticktack623:20161219204647p:plain

ElmSharpだけ違う命名で気になるところ。こいつの正体はElmSharpのページへ跳ぶと説明文で正体が分かります。

ElmSharp is the top-most library based on Tizen Native UI Framwork (EFL). It provides a variety of pre-built UI components, such as layout objects and widgets, that allow you to build rich graphical user interfaces.

つまりTizenのネイティブUIのラッパーです。(なぜTizen.UIにしなかったのか)

Rendererでも覗いてみようか

NuGetパッケージを見る限り、なんとなく動きそうな土台が整っていることが分かりました。

次はRendererとか、どんな感じに実装されてるか気になりますよね。

ソースコード中の適当な所に Xamarin.Forms.Platform.Tizen.ButtonRenderer と書いてF12で定義に跳んでみましょう。

こんな感じになってます。

using System;
using Xamarin.Forms.Platform.Tizen.Native;

namespace Xamarin.Forms.Platform.Tizen
{
    public class ButtonRenderer : ViewRenderer<Button, Native.Button>, IDisposable
    {
        // 省略
    }
}

ViewRenderer<Button, Native.Button> に注目、(Xamarin.Formsから見て)ネイティブ側のコントロールとして Xamarin.Forms.Platform.Tizen.Native.Button を与えていますね。こいつ何者?

TizenのネイティブUIはElmSharpじゃなかったんかい、ということでさらにF12で跳びます。

using ElmSharp;

namespace Xamarin.Forms.Platform.Tizen.Native
{
    public class Button : ElmSharp.Button, IMeasurable
    {
        // 省略
    }
}

はい、ここでElmSharpのButtonが出てきました。

Xamarin.Formsは元来iOSやAndroidのようにリッチなUIフレームワークバックエンドと想定していたため、組み込み寄りなTizenのコントロールではギャップが大きかったのでワンクッション置いているのだと思います。

Button以外でも同様なので、整理すると以下のような関係です。

  • Xamarin.Forms.Core.dll
    • Xamarin.Forms名前空間 (抽象化されたUIコントロール)
  • Xamarin.Forms.Platform.Tizen.dll
    • Xamarin.Forms.Platform.Tizen名前空間 (各種Renderer定義)
    • Xamarin.Forms.Platform.Tizen.Native名前空間 (Rendererのネイティブ側コントロール、ElmSharpコントロールを補強)
  • ElmSharp.dll
    • ElmSharp名前空間 (TizenのネイティブUIフレームワークのラッパー)

せっかくなので地図でも表示してみよう

Xamarin.Forms.MapsのTizen版はまだ無いようです。せっかくなのでここまで調査した内容を駆使して地図を表示してみましょう。モバイルOSなのだから地図ぐらいあるはずです。

ElmSharp名前空間の中にそれっぽいのがあるか探すと ElmSharp.EvasMap が見つかります。残念ながらこれは地図ではありません。(どうやら変形に関するクラスらしい)

仕方ないのでネイティブAPIを探してP/Invokeで何とかします。

それらしいドキュメントによると、 Evas_Object * elm_map_add (Evas_Object *parent) をコールすればMap Widjetのハンドルが得られるようだ。

using System;
using System.Runtime.InteropServices;

internal static class ElementaryMap
{
    [DllImport("libelementary.so.1")]
    internal static extern IntPtr elm_map_add(IntPtr obj);
}

DllImportに指定するライブラリはElmSharp.Buttonを真似する。

using System;
using ElmSharp;
using System.Reflection;

namespace TizenApp1
{
    // ElmSharpのコントロールはLayoutの派生クラスになっていたので真似る
    public class MapView : Layout
    {
        public MapView(EvasObject parent) : base(parent)
        {
        }
        
        // ここでネイティブAPIのハンドルを返せばよさそう
        protected override IntPtr CreateHandle(EvasObject parent)
        {
            IntPtr parentHandle = (IntPtr) typeof(EvasObject).GetTypeInfo().GetProperty("Handle", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).GetMethod.Invoke(parent, new object[] { });
            return ElementaryMap.elm_map_add(parentHandle);
        }
    }
}

新しいWidjetを作るには親要素(コンテナ?)のハンドルが必要らしいのだがinternalだったのでReflectionで頑張る。

これでネイティブ地図コントロールのインスタンスが作れるようになったはずなので、Xamarin.FormsのMapとRendererをこさえる。

using Xamarin.Forms;

namespace TizenApp1
{
    public class Map : View
    {
    }
}
using System;
using Xamarin.Forms.Platform.Tizen;

[assembly: ExportRenderer(typeof(TizenApp1.Map), typeof(TizenApp1.MapRenderer))]

namespace TizenApp1
{
    public class MapRenderer : VisualElementRenderer<Map>
    {
        MapView _control;

        protected override void OnElementChanged(ElementChangedEventArgs<Map> e)
        {
            if(_control == null)
            {
                _control = new MapView(Forms.Context.MainWindow);
                SetNativeControl(_control);
            }

            base.OnElementChanged(e);
        }
    }
}

この辺は普通のXamarin.Formsの世界なので特に変わったことは無し。

public App()
{
    MainPage = new ContentPage
    {
        BackgroundColor = Color.Gray,
        Content = new Map(),
    };
}

ContentPageに組み込んでみる。

f:id:ticktack623:20161218035646j:plain

なんか出た。

Tizen端末を使った事が無いので本当にこれであってるのかイマイチ不安なんですが、ネイティブAPIを叩いて地図Widjetを出現させらるのに成功したようです。 (なおサイズ変更ができない、レイアウト周りのハンドリングが必要だろうがそこまで頑張る気力が無い……)

いずれ地図コントロールも実装されると思うのでその際に答え合わせしたいですね。

更に戦う者達

TizenでXamarin.Forms頑張りたい人向け

Xamarin.Forms.Platform.TizenのGitリポジトリ

ElmSharpのGitリポジトリ

Compliance Specification | Tizen SourceTizen 2.4 Compliance Specification for Mobile Profile (pdf) にDllImportに使いそうなライブラリの名前が載ってる。

Xamarin.Forms.Platform.Tizen最大の謎

個人的に一番気になる点、 Xamarin.Forms.Platform.Tizenはなにがしかのトリックで他assemblyのinternalメンバを参照している のである。

というのも Xamarin.Forms.Platform.XXXX を追加するには、 Xamarin.Forms.Core の internal な interface を実装する必要がある(この話は12/24に投稿されるような気がする)ため、 Xamarin.Forms.Core の AssemblyInfo.cs には各プラットフォームに対する InternalsVisibleToAttribute が記述されているのだが、 Tizen用のInternalsVisibleToは無いのに動いちゃってる不思議。

このトリックが判明すれば本家Xamarin.Formsに手を入れる事無く 対応プラットフォーム勝手に増やし放題 なのである。 (Xamarin.Forms.Platform.WindowsFormsなんてネタも夢じゃない!?)

誰か分かる人いたら教えてください…………。