Vývoj Uno Platform (část 2.)

Xamarin Uno Platform WinUI XAML

4 years ago

Vítejte v druhé části vývoje našeho prvního pull requestu do Uno Platform. V prvním článkuhe této série jsme se podívali na Uno jako celek a poprvé nahlédli do jeho kódu. Tentokrát se podíváme na existující implementaci třídy DisplayInformation , přidáme do ukázkové aplikace novou stránku na které náš kód budeme testovat, seznámíme se s projektem UWPSyncGenerator a implementujeme ScreenWidthInRawPixels a ScreenHeightInRawPixels.

Uno Platform

Třída DisplayInformation

Již minule jsem zmínil, že issue kterou jsem se rozhodl řešit má číslo #385 - "Add support for DisplayInformation Dimensions". Nejprve se podívejme co UWP třída DisplayInformation umí. Můžeme ji najít v namepsace Windows.Graphics.Display a poskytuje informace o displeji na kterém je aplikace zobrazena. Navíc, tato třída zahrnuje události o změnách displeje jako je OrientationChanged. Abychom API mohli implementovat, je nutné dodžovat co nejpřesněji chování platformy UWP. Naštěstí Microsoft Docs jsou velmi dobře napsané a každá vlastnost je důkladně popsána. Rozhodl jsem se implementovat všechny vlastnosti DisplayInformation na Androidu a iOS a přenechat ostatní platformy a události na další pull requesty. Tím bude pull request výstižný a získám rychlejší zpětnou vazbu. Jakmile bude potvrzeno, že je můj přístup správný, můžu s větší jistotou pokračovat dál.

Existující Uno implementace

Když jsem nahlédl do složky Graphics/Display v projektu Uno , zjistil jsem, že soubory DisplayInformation.cs , DisplayInformation.Android.cs a DisplayInformation.iOS.cs již existují a implementují AutoRotationPreferences , CurrentOrientation a OrientationChanged na iOS a AutoRotationPreferences na Androidu. Pro získání instance DisplayInformation na UWP je třeba zavolat metodu GetForCurrentView z UI vlákna. Na UWP je instance svázaná s vláknem ale Uno nyní používá jednodušší přísutp a vyvtáří singleton instanci nezávislou na vlýkně:

Bezparametrický konstruktor je označen jako private takže nové instance není možné mimo třídu vytvořit. Konstruktor volá partial metodu Initialize.

Metoda Initialize je implementována pro Android a iOS zvlášť v souborech DisplayInformation.Android.cs a DisplayInformation.iOS.cs. Například na iOS Initialize volá InitializeCurrentOrientation, která nastavuje výchozí hodnotu pro CurrentOrientation:

Jak si můžete všimnout, pro určení orientace se používá orientace StatusBaru, protože jde o nejspolehlivější způsob

Úprava ukázkové aplikace

Abych si usnadnil testování implementace, vytvořil jsem v SamplesApp jednoduchý user control:

Control hostuje ListView , který zobrazí seznam vlastností DisplayInformation , jejich hodnoty a tlačítko, které je aktualizuje. UserControl označíme atributem SampleControlInfoAttribute jež se používá pro nalezení a kategorizaci ukázkových příkladů.

Metoda RefreshDisplayInformation získává aktuální hodnoty vlastností ze třídy DisplayInformation . Můj původní plán byl použít reflexi:

To fungovalo poměrně dobře až do chvíle než jsem kód spustil na iOS kde většina vlastností v seznamu chyběla. Důvodem byl fakt, že linker pro zmenšení výsledné assembly odstraňuje kód, na který nic nepřístupuje a nové vlastnosti DisplayInformation byly mezi nimi. Kód jsem opravil tak, že jsem vlastnosti zadal ručně:

Toto samozřejmě pozbývá jednoduchost a obecnost původního řešení, ale na druhou stranu to funguje :-) . Pro úplnost taky uvedu kód metody SafeGetValue , která pomocí delegátu Func<T> získá hodnotu vlastnosti a převede ji na string je-li to možné:

Všimněme si, že kód odcbytává výjimku NotImplementedException , kterou Uno používá pro API která zatím nemají implementaci. Na obrázku níže je ukázková aplikace běžící na Windows proti UWP API. Je před námi hodně práce!

DisplayInformation page in SamplesApp

Stránka DisplayInformation v SamplesApp

Synchronizace s UWP API

Nyní je čas nahlédnout pod pokličku dalšího z UWP kouzel. Jak dokáže tým udržet API plně synchronizované s UWP? Aktualizace Windows 10 přichází pravidelně dvakrát do roka, takže procházet vždy celé API a kontrolovat změny by byl skutečně náročný (a nezáviděníhodný) úkol. Zde ale přichází na scénu projekt Uno.UWPSyncGenerator. Musíme nejprve porozumět tomu, jak Uno pracuje s neimplementovaným API. Uvnitř projektů Uno a Uno.UI můžeme najít složky s názvem Generated . Uvnitř jsou podsložky pro všechna UWP API a každý typ pak má svůj vlastní .cs soubor. Na příklad DisplayInformation.cs je uvnitř podsložky Generated/3.0.0.0/Windows.Graphics.Display a obsahuje následující:

Toto vše je automaticky generovaný kód, který pro všechna neimplementovaná API přidává NotImplementedException wrapper. Také si všimněme, že jsou všechny třídy partial. Skutečné třídy Una pak používají partial také a mohou tak například implementovat jen část dané třídy. Uno.UWPSyncGenerator má na starosti generovat všechen tento kód a jde na to opravdu chytře. Používá balíček Microsoft.CodeAnalysis a prochází projekty Uno a Uno.UI a kontroluje co je již pro které platformy implementované. Pro příklad přidám první tři vlastnosti DisplayInformation do DisplayInformation.cs v Uno projektu:

Po spuštění UWPSyncGenerator deklarované vlastnosti zmizí ze složky Generated a jsou nahrazeny informativním komentářem:

Ale to není vše! Co kdybychom se rozhodli implementovat nějakou z vlastností pouze pro Android?

Nástroj tuto situace zvládne i tak bravurně:

Et voilà, direktiva #if nyní obsahuje namísto symbolu __ANDROID__ neškodné false.

Implementace ScreenWidthInRawPixels a ScreenHeightInRawPixels

Nebudu ve článku do detailů rozebírat implementaci celé třídy DisplayInformation, protože jde z většiny o platformně závislý kód a tato série článků je věnována procesu přispívání do Una jako takévho. Ale pro ukázku můžeme ukázat proces na vlastnostech ScreenWidthInRawPixels a ScreenHeightInRawPixels. Tyto vlastnosti by měly vrátit skutečné rozlišení displeje v pixlech. Ze všeho nejdřív přidám do souboru DisplayInformation.cs nové vlastnosti:

Toto způsobí konflikt s NotImplemented vlastnostmi ve složce Generated , ale spuštěním UWPSyncGenerator konflikt zmizí. Nyní vlastnostem nastavíme skutečnou hodnotu!

iOS

Přidal jsem novou metodu UpdateProperties kterou volá metoda Initialize v DisplayInformation.iOS.cs:

Použiji UpdateProperties pro nastavení hondnot vlastností ve chvíli, kdy se displej změní. Pro tuto chvíli ale události neimplementujeme, takže vlastnosti zatím nastavím při inicializaci. Aby byl kód jasnější, rozdělím metodu UpdateProperties na logické skupiny.

Veĺikost obrazovky na iOS je možné zjistit pomocí vlastnosti UIScreen.MainScreen.Bounds.Size. To však vrací pouze velikost v layout "bodech". Vše se potom renderuje ve zvětšení UIScreen.MainScreen.Scale a pak zase změnší na nativní rozlišení displeje. Tato infografika je pro pochopení tohoto procesu na iOS velmi užitečná. Naštěstí máme i API UIScreen.MainScreen.NativeScale které vrací skutečné "nativní" zvětšení:

A ukázková aplikace nyní ukalzuje:

Raw screen size on iOS

Raw screen size on iOS

Android

Na Androidu jsem postupoval poobně jako na iOS:

Je potřeba trochu kódu navíc pro získání instance DisplayMetrics a IWindowManager, protože tyto budou znovu použity pro implementaci dalších vlastností. Základní implementace UpdateRawProperties vypadá takto:

DisplayMetrics které jsme výše získali přimo obsahují vlastnosti, které vrací šířku a výšku displeje v pixelech, takže je jednoduše přetypujeme a jsme hotovi:

Native screen size running on my OnePlus 6T

Native screen size running on my OnePlus 6T

Shrnutí

Podívali jsme se na existující implementaci DisplayInformation v Unu, pochopili, jak framework synchronizuje své API s UWP a implementovali první dvě vlastnosti. Příště se v krátkosti podíváme na to nejzajímavější ze zbýavjících vlastností a implementujeme událost OrientationChanged na Androidu. Potom konečně vytvoříme svůj pull request a odešleme jej pro review!