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

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.csDisplayInformation.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ě:


public static DisplayInformation GetForCurrentView()
{
if (_instance == null)
{
_instance = new DisplayInformation();
}
return _instance;
}

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


private DisplayInformation()
{
Initialize();
}
partial void 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:


private void InitializeOrientation()
{
_didChangeStatusBarOrientationObserver = NSNotificationCenter
.DefaultCenter
.AddObserver(
UIApplication.DidChangeStatusBarOrientationNotification,
n => {
UpdateCurrentOrientation();
OrientationChanged?.Invoke(this, CurrentOrientation);
}
);
UpdateCurrentOrientation();
}
private void UpdateCurrentOrientation()
{
var currentOrientationMask = UIApplication.SharedApplication
.StatusBarOrientation;
switch (currentOrientationMask)
{
case UIInterfaceOrientation.LandscapeLeft:
CurrentOrientation = DisplayOrientations.LandscapeFlipped;
break;
case UIInterfaceOrientation.LandscapeRight:
CurrentOrientation = DisplayOrientations.Landscape;
break;
case UIInterfaceOrientation.Portrait:
CurrentOrientation = DisplayOrientations.Portrait;
break;
case UIInterfaceOrientation.PortraitUpsideDown:
CurrentOrientation = DisplayOrientations.PortraitFlipped;
break;
}
NativeOrientation = 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:


<UserControl
x:Class="UITests.Shared.Windows_Graphics_Display.DisplayInformation.DisplayInformation_Properties"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Button Content="Refresh" Click="Refresh_Click" />
<ListView x:Name="PropertyListView" Grid.Row="1">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Padding="4">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
<TextBlock Text="{Binding Value}" Grid.Row="1" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</UserControl>

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ů.


namespace UITests.Shared.Windows_Graphics_Display.DisplayInformation
{
[SampleControlInfo("DisplayInformation", "DisplayInformation_Properties", description: "Shows the values from DisplayInformation class properties. N/A for not implemented.")]
public sealed partial class DisplayInformation_Properties : UserControl
{
public DisplayInformation_Properties()
{
this.InitializeComponent();
RefreshDisplayInformation();
}
public class PropertyInformation
{
public PropertyInformation( string name, string value)
{
Name = name;
Value = value;
}
public string Name { get; set; }
public string Value { get; set; }
}
private void Refresh_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
RefreshDisplayInformation();
}
...

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


private void RefreshDisplayInformation()
{
var info = Windows.Graphics.Display.DisplayInformation.GetForCurrentView();
var type = info.GetType();
var typeInfo = type.GetTypeInfo();
var propertyInfos = typeInfo.GetProperties();
var properties = propertyInfos.Select(p => new PropertyInformation() { Name = p.Name, Value = SafeGetValue(p, info) }).ToArray();
PropertyListView.ItemsSource = properties;
}

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ě:


private void RefreshDisplayInformation()
{
var info = DisplayInfo.GetForCurrentView();
var properties = new PropertyInformation[]
{
new PropertyInformation(nameof(DisplayInfo.AutoRotationPreferences), SafeGetValue(()=>DisplayInfo.AutoRotationPreferences)),
new PropertyInformation(nameof(info.CurrentOrientation), SafeGetValue(()=>info.CurrentOrientation)),
new PropertyInformation(nameof(info.NativeOrientation), SafeGetValue(()=>info.NativeOrientation)),
new PropertyInformation(nameof(info.ScreenHeightInRawPixels), SafeGetValue(()=>info.ScreenHeightInRawPixels)),
new PropertyInformation(nameof(info.ScreenWidthInRawPixels), SafeGetValue(()=>info.ScreenWidthInRawPixels)),
new PropertyInformation(nameof(info.LogicalDpi), SafeGetValue(()=>info.LogicalDpi)),
new PropertyInformation(nameof(info.DiagonalSizeInInches), SafeGetValue(()=>info.DiagonalSizeInInches)),
new PropertyInformation(nameof(info.RawPixelsPerViewPixel), SafeGetValue(()=>info.RawPixelsPerViewPixel)),
new PropertyInformation(nameof(info.RawDpiX), SafeGetValue(()=>info.RawDpiX)),
new PropertyInformation(nameof(info.RawDpiY), SafeGetValue(()=>info.RawDpiY)),
new PropertyInformation(nameof(info.ResolutionScale), SafeGetValue(()=>info.ResolutionScale)),
};
PropertyListView.ItemsSource = properties;
}

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é:


private string SafeGetValue<T>(Func<T> getter)
{
try
{
var value = getter();
if ( value == null)
{
return "(null)";
}
return Convert.ToString(value);
}
catch (NotImplementedException ex)
{
return "(Not implemented)";
}
}

view raw

SafeGetValue.cs

hosted with ❤ by GitHub

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í:


#pragma warning disable 108 // new keyword hiding
#pragma warning disable 114 // new keyword hiding
namespace Windows.Graphics.Display
{
#if false || false || false || false || false
[global::Uno.NotImplemented]
#endif
public partial class DisplayInformation
{
// Skipping already declared property CurrentOrientation
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public float LogicalDpi
{
get
{
throw new global::System.NotImplementedException("The member float DisplayInformation.LogicalDpi is not implemented in Uno.");
}
}
#endif
// Skipping already declared property NativeOrientation
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public float RawDpiX
{
get
{
throw new global::System.NotImplementedException("The member float DisplayInformation.RawDpiX is not implemented in Uno.");
}
}
#endif
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public float RawDpiY
{
get
{
throw new global::System.NotImplementedException("The member float DisplayInformation.RawDpiY is not implemented in Uno.");
}
}
#endif
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public global::Windows.Graphics.Display.ResolutionScale ResolutionScale
{
get
{
throw new global::System.NotImplementedException("The member ResolutionScale DisplayInformation.ResolutionScale is not implemented in Uno.");
}
}
#endif
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public bool StereoEnabled
{
get
{
throw new global::System.NotImplementedException("The member bool DisplayInformation.StereoEnabled is not implemented in Uno.");
}
}
#endif
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public double RawPixelsPerViewPixel
{
get
{
throw new global::System.NotImplementedException("The member double DisplayInformation.RawPixelsPerViewPixel is not implemented in Uno.");
}
}
#endif
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public double? DiagonalSizeInInches
{
get
{
throw new global::System.NotImplementedException("The member double? DisplayInformation.DiagonalSizeInInches is not implemented in Uno.");
}
}
#endif
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public uint ScreenHeightInRawPixels
{
get
{
throw new global::System.NotImplementedException("The member uint DisplayInformation.ScreenHeightInRawPixels is not implemented in Uno.");
}
}
#endif
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public uint ScreenWidthInRawPixels
{
get
{
throw new global::System.NotImplementedException("The member uint DisplayInformation.ScreenWidthInRawPixels is not implemented in Uno.");
}
}
#endif
// Skipping already declared property AutoRotationPreferences
//… and more!
}
}

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 UnoUno.UI a kontroluje co je již pro které platformy implementované.

Pro příklad přidám první tři vlastnosti DisplayInformation do DisplayInformation.csUno projektu:


public uint ScreenHeightInRawPixels { get; private set; }
public uint ScreenWidthInRawPixels { get; private set; }
public float LogicalDpi { get; private set; }

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


// Skipping already declared property LogicalDpi
// Skipping already declared property ScreenHeightInRawPixels
// Skipping already declared property ScreenWidthInRawPixels

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


#if __ANDROID__
public uint ScreenHeightInRawPixels { get; private set; }
#endif

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


#if false || __IOS__ || NET461 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public uint ScreenHeightInRawPixels
{
get
{
throw new global::System.NotImplementedException("The member uint DisplayInformation.ScreenHeightInRawPixels is not implemented in Uno.");
}
}

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:


public uint ScreenHeightInRawPixels { get; private set; }
public uint ScreenWidthInRawPixels { get; private set; }

view raw

ScreenSize.cs

hosted with ❤ by GitHub

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 InitializeDisplayInformation.iOS.cs:


partial void Initialize()
{
InitializeOrientation();
UpdateProperties();
}

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.


private void UpdateProperties()
{
UpdateRawProperties();
}
private void UpdateRawProperties()
{
}

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í:


private void UpdateRawProperties()
{
var screenSize = UIScreen.MainScreen.Bounds.Size;
var scale = UIScreen.MainScreen.NativeScale;
ScreenHeightInRawPixels = (uint)(screenSize.Height * scale);
ScreenWidthInRawPixels = (uint)(screenSize.Width * scale);
}

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:


partial void Initialize()
{
UpdateProperties();
}
private void UpdateProperties()
{
var realDisplayMetrics = new DisplayMetrics();
var windowManager = ContextHelper.Current.GetSystemService(Context.WindowService).JavaCast<IWindowManager>();
windowManager.DefaultDisplay.GetRealMetrics(realDisplayMetrics);
UpdateRawProperties(realDisplayMetrics);
}

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:


private void UpdateRawProperties(DisplayMetrics realDisplayMetrics)
{
ScreenWidthInRawPixels = (uint)realDisplayMetrics.WidthPixels;
ScreenHeightInRawPixels = (uint)realDisplayMetrics.HeightPixels;
}

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!

Buy me a coffeeBuy me a coffee

2 thoughts on “Vývoj Uno Platform (část 2.)”

  1. Appreciating the hard work you put into your website and detailed information you present.
    It’s good to come across a blog every once in a
    while that isn’t the same old rehashed information. Wonderful read!
    I’ve bookmarked your site and I’m including your RSS feeds to my Google account.

  2. Thank you, I have just been searching for info about this topic for a
    while and yours is the best I have discovered so far.
    But, what about the conclusion? Are you positive concerning the supply?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.