この記事は Xamarin(その2) Advent Calendar 2016 20日目の記事です。
Xamarin.FormsにTizenが参戦!?
Connect(); // 2016 のキーノートにて電撃的にXamarin.Forms.Tizenが発表され、一部界隈に衝撃を与えました。
衝撃を受ける様子
Tizen!?死んだはずでは? #msftconnect #VSJP
— ざまりん.ふぉーむずマン👀 (@ticktackmobile) 2016年11月16日
ちょっとまって、Xamarin.Forms.Core.Tizenの解説しなきゃダメな流れなん? #msftconnect #VSJP
— ざまりん.ふぉーむずマン👀 (@ticktackmobile) 2016年11月16日
当惑ぶりが伺えますね。
さて、TizenでXamarin.Formsアプリを動かしてみよう!的な内容は既になかしょさん(@nakasho_devがやってくれているのでここでは省略します。
ここ見ればすんなりいけます。
解析だ!
さあ、TizenでXamarin.Formsアプリが動く事は分かりました。
次はなぜ動くのかが気になるのでソイツを調べてみましょう。
テンプレートから生成されたプロジェクトはどうやら.NET Core Appを吐くようです。(そもそもキーノートの発表的には「Tizenが.NET Coreに対応したよ」という内容なので当然か)
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.Tizen と Tizen.Library が気になるのでNuGet Gallaryを覗きに行きます。
Xamarin.Forms.Platform.Tizen のページ
オーナーがtizenプロジェクト、オーサーがSamsungとなっているのでバグ報告などはそちらに投げましょう。次!
Tizen.Library のページ
こちらはTizenのAPIを.NET Frameworkから呼ぶためのラッパーの集合体っぽいですね。
Dependenciesに並んでいる名前もTizenアーキテクチャ図に乗っているものとほぼ一致します。
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に組み込んでみる。
なんか出た。
Tizen端末を使った事が無いので本当にこれであってるのかイマイチ不安なんですが、ネイティブAPIを叩いて地図Widjetを出現させらるのに成功したようです。 (なおサイズ変更ができない、レイアウト周りのハンドリングが必要だろうがそこまで頑張る気力が無い……)
いずれ地図コントロールも実装されると思うのでその際に答え合わせしたいですね。
更に戦う者達
TizenでXamarin.Forms頑張りたい人向け
Xamarin.Forms.Platform.TizenのGitリポジトリ
Compliance Specification | Tizen Source の Tizen 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なんてネタも夢じゃない!?)
誰か分かる人いたら教えてください…………。