Vytvoření vlastního rozložení v aplikaci Xamarin.Forms
Xamarin.Forms definuje pět tříd rozložení – StackLayout, AbsoluteLayout, RelativeLayout, Grid a FlexLayout a každý z nich uspořádá podřízené položky jiným způsobem. Někdy je však nutné uspořádat obsah stránky pomocí rozložení, které Xamarin.Formsneposkytuje . Tento článek vysvětluje, jak napsat vlastní třídu rozložení a demonstruje třídu WrapLayout citlivou na orientaci, která uspořádá podřízené položky vodorovně přes stránku, a potom zabalí zobrazení následných podřízených položek do dalších řádků.
Všechny Xamarin.Formstřídy rozložení jsou odvozeny od Layout<T>
třídy a omezují obecný typ na View
a jeho odvozené typy. Třída je odvozena Layout<T>
od Layout
třídy, která poskytuje mechanismus pro umístění a změnu velikosti podřízených prvků.
Každý vizuální prvek zodpovídá za určení vlastní upřednostňované velikosti, která se označuje jako požadovaná velikost. Page
, Layout
a Layout<View>
odvozené typy jsou zodpovědné za určení umístění a velikosti jejich dítěte nebo dětí vzhledem k sobě. Rozložení proto zahrnuje vztah nadřazený-podřízený, kde nadřazený objekt určuje, jakou velikost podřízených položek má být, ale pokusí se přizpůsobit požadovanou velikost podřízeného objektu.
K vytvoření vlastního rozložení se vyžaduje důkladné porozumění Xamarin.Forms cyklům rozložení a zneplatnění. Tyto cykly budou nyní popsány.
Rozložení
Rozložení začíná v horní části vizuálního stromu stránkou a pokračuje všemi větvemi vizuálního stromu, aby zahrnovalo všechny vizuální prvky na stránce. Prvky, které jsou nadřazené jiným prvkům, jsou zodpovědné za určení velikosti a umístění jejich dětí vzhledem k sobě.
VisualElement
Třída definuje metoduMeasure
, která měří prvek pro operace rozložení, a Layout
metoda, která určuje obdélníkovou oblast prvek bude vykreslen uvnitř. Když se aplikace spustí a zobrazí se první stránka, spustí se cyklus rozložení, který se skládá z prvních Measure
volání, a potom Layout
volání, začne na objektuPage
:
- Během cyklu rozložení je každý nadřazený prvek zodpovědný za volání
Measure
metody ve svých podřízených objektech. - Po měření podřízených položek je každý nadřazený prvek zodpovědný za volání
Layout
metody pro své podřízené položky.
Tento cyklus zajišťuje, že každý vizuální prvek na stránce přijímá volání metod Measure
a Layout
metod. Proces se zobrazí v následujícím diagramu:
Poznámka:
Všimněte si, že cykly rozložení mohou nastat také na podmnožině vizuálního stromu, pokud se něco změní, aby ovlivnilo rozložení. To zahrnuje přidávání nebo odebírání položek z kolekce, jako StackLayout
je například změna IsVisible
vlastnosti elementu nebo změna velikosti prvku.
Každá Xamarin.Forms třída, která má Content
nebo Children
vlastnost má přepsánou LayoutChildren
metodu. Vlastní třídy rozložení, které jsou odvozeny od Layout<View>
musí přepsat tuto metodu a zajistit, aby Measure
byly volána u Layout
všech podřízených prvků elementu, aby bylo zajištěno požadované vlastní rozložení.
Kromě toho každá třída, která je odvozena nebo Layout<View>
Layout
musí přepsat metoduOnMeasure
, což je místo, kde třída rozložení určuje velikost, kterou musí být provedením volání Measure
metod svých podřízených položek.
Poznámka:
Prvky určují jejich velikost na základě omezení, která označují, kolik místa je k dispozici pro prvek v nadřazené části elementu. Omezení předávaná metodám a OnMeasure
mohou být v rozsahu Measure
od 0 do Double.PositiveInfinity
. Prvek je omezen nebo plně omezen, když obdrží volání své Measure
metody s nenekonečnou argumenty - prvek je omezen na určitou velikost. Prvek je unconstrained nebo částečně omezené, když obdrží volání metody Measure
s alespoň jedním argumentem rovnou Double.PositiveInfinity
– nekonečné omezení lze považovat za indikující automatické velikosti.
Neplatnost
Neplatným procesem je proces, kterým změna prvku na stránce aktivuje nový cyklus rozložení. Prvky jsou považovány za neplatné, pokud již nemají správnou velikost nebo pozici. Pokud FontSize
se například vlastnost Button
změny změní, znamená to, že je neplatná, Button
protože už nebude mít správnou velikost. Změna velikosti Button
poté může mít efekt zvlnění změn v rozložení přes zbytek stránky.
Elementy se zneplatňují vyvoláním InvalidateMeasure
metody, obecně když vlastnost elementu změní, což může mít za následek novou velikost elementu. Tato metoda aktivuje MeasureInvalidated
událost, kterou nadřazená rutina elementu aktivuje nový cyklus rozložení.
Třída Layout
nastaví obslužnou rutinu pro událost u každého podřízeného objektu MeasureInvalidated
přidaného do jeho Content
vlastnosti nebo Children
kolekce a odpojte obslužnou rutinu při odebrání podřízeného objektu. Každý prvek ve vizuálním stromu, který má podřízené položky, je proto upozorňován vždy, když se změní velikost jednoho z podřízených položek. Následující diagram znázorňuje, jak změna velikosti prvku ve vizuálním stromu může způsobit změny, které strom zvlní:
Layout
Třída se však pokusí omezit dopad změny velikosti dítěte na rozložení stránky. Pokud je rozložení omezené, změna podřízené velikosti neovlivní nic vyššího než nadřazené rozložení ve stromu vizuálu. Změna velikosti rozložení ale obvykle ovlivňuje, jak rozložení uspořádá podřízené položky. Proto jakákoli změna velikosti rozložení začne cyklus rozložení rozložení a rozložení bude přijímat volání jeho OnMeasure
a LayoutChildren
metod.
Třída Layout
také definuje metodu InvalidateLayout
, která má podobný účel jako metoda InvalidateMeasure
. Metoda InvalidateLayout
by se měla vyvolat při každé změně, která má vliv na to, jak pozice rozložení a velikosti podřízených položek. Třída například Layout
vyvolá metodu InvalidateLayout
při každém přidání nebo odebrání podřízeného objektu z rozložení.
Lze InvalidateLayout
přepsat, aby se implementovaly mezipaměti, aby se minimalizovaly opakované vyvolání Measure
metod podřízených objektů rozložení. InvalidateLayout
Přepsání metody poskytne oznámení o tom, kdy jsou podřízené položky přidány nebo odebrány z rozložení. Podobně lze metodu OnChildMeasureInvalidated
přepsat tak, aby poskytovala oznámení, když se změní velikost jedné z podřízených položek rozložení. U přepsání obou metod by vlastní rozložení mělo reagovat zrušením mezipaměti. Další informace najdete v tématu Výpočty a data rozložení mezipaměti.
Vytvoření vlastního rozložení
Proces vytvoření vlastního rozložení je následující:
Vytvořte třídu, která je odvozena od třídy
Layout<View>
. Další informace naleznete v tématu Vytvoření WrapLayout.[volitelné] Přidejte vlastnosti zálohované vlastnostmi vázání pro všechny parametry, které by měly být nastaveny ve třídě rozložení. Další informace naleznete v tématu Přidání vlastností zálohovaných vlastnostmi bindable.
Přepište metodu
OnMeasure
, která vyvolá metoduMeasure
u všech podřízených položek rozložení, a vrátí požadovanou velikost rozložení. Další informace naleznete v tématu Přepsání OnMeasure Metoda.Přepište metodu
LayoutChildren
Layout
pro vyvolání metody u všech podřízených položek rozložení. Selhání vyvoláníLayout
metody u každého podřízeného objektu v rozložení způsobí, že dítě nikdy neobdrží správnou velikost nebo pozici, a proto se podřízená položka na stránce nezobrazí. Další informace naleznete v tématu Přepsání LayoutChildren Metoda.Poznámka:
Při vytváření výčtu podřízených položek v objektu
OnMeasure
aLayoutChildren
přepsání přeskočte všechny podřízené položky, jejichžIsVisible
vlastnost je nastavena nafalse
hodnotu . Tím zajistíte, že vlastní rozložení nezanechá prostor pro neviditelné podřízené položky.[volitelné] Přepište metodu
InvalidateLayout
, která má být upozorněna při přidání podřízených položek do nebo odebrání z rozložení. Další informace naleznete v tématu Přepsání InvalidateLayout Metoda.[volitelné] Přepište metodu
OnChildMeasureInvalidated
, která má být upozorněna, když se změní velikost jedné z podřízených položek rozložení. Další informace naleznete v tématu Přepsání OnChildMeasureInvalidated – metoda.
Poznámka:
Všimněte si, že OnMeasure
přepsání se nevyvolá, pokud se velikost rozložení řídí nadřazeným objektem, nikoli podřízenými položkami. Přepsání se však vyvolá, pokud je jedno nebo obě omezení nekonečné nebo pokud třída rozložení má jiné než výchozí HorizontalOptions
hodnoty nebo VerticalOptions
vlastnosti. Z tohoto důvodu LayoutChildren
se přepsání nemůže spoléhat na podřízené velikosti získané během OnMeasure
volání metody. LayoutChildren
Místo toho musí vyvolat metodu Measure
u podřízených položek rozložení před vyvoláním Layout
metody. Případně lze velikost podřízených objektů získaných v OnMeasure
přepsání uložit do mezipaměti, aby se zabránilo pozdějšímu Measure
vyvolání přepsání LayoutChildren
, ale třída rozložení bude muset vědět, kdy je potřeba velikost získat znovu. Další informace najdete v tématu Výpočty a data rozložení mezipaměti.
Třídu rozložení pak můžete využít tak, že ji přidáte do objektu Page
a přidáte do rozložení podřízené položky. Další informace naleznete v tématu Využití WrapLayout.
Vytvoření WrapLayoutu
Ukázková aplikace ukazuje třídu citlivou na WrapLayout
orientaci, která uspořádá podřízené položky vodorovně přes stránku a pak zabalí zobrazení následných podřízených položek do dalších řádků.
Třída WrapLayout
přiděluje každému podřízení stejné místo, označované jako velikost buňky, na základě maximální velikosti podřízených položek. Podřízené položky menší než velikost buňky mohou být umístěny v buňce na základě hodnot jejich HorizontalOptions
a VerticalOptions
vlastností.
Definice WrapLayout
třídy je uvedena v následujícím příkladu kódu:
public class WrapLayout : Layout<View>
{
Dictionary<Size, LayoutData> layoutDataCache = new Dictionary<Size, LayoutData>();
...
}
Výpočet a ukládání dat rozložení mezipaměti
Struktura LayoutData
ukládá data o kolekci podřízených položek v řadě vlastností:
VisibleChildCount
– počet podřízených položek, které jsou viditelné v rozložení.CellSize
– maximální velikost všechpodřízenýchRows
– počet řádků.Columns
– počet sloupců.
Pole layoutDataCache
slouží k ukládání více LayoutData
hodnot. Při spuštění aplikace budou dva LayoutData
objekty uloženy do layoutDataCache
slovníku pro aktuální orientaci – jeden pro argumenty omezení přepsání OnMeasure
a jeden pro width
argumenty a height
argumenty přepsání LayoutChildren
. Při otáčení zařízení do orientace OnMeasure
na šířku se znovu vyvolá přepsání a LayoutChildren
přepsání, což způsobí, že se do slovníku uloží další dva LayoutData
objekty do mezipaměti. Při návratu zařízení na výšku se ale nevyžadují žádné další výpočty, protože layoutDataCache
už požadovaná data mají.
Následující příklad kódu ukazuje metodu GetLayoutData
, která vypočítá vlastnosti LayoutData
strukturované na základě konkrétní velikosti:
LayoutData GetLayoutData(double width, double height)
{
Size size = new Size(width, height);
// Check if cached information is available.
if (layoutDataCache.ContainsKey(size))
{
return layoutDataCache[size];
}
int visibleChildCount = 0;
Size maxChildSize = new Size();
int rows = 0;
int columns = 0;
LayoutData layoutData = new LayoutData();
// Enumerate through all the children.
foreach (View child in Children)
{
// Skip invisible children.
if (!child.IsVisible)
continue;
// Count the visible children.
visibleChildCount++;
// Get the child's requested size.
SizeRequest childSizeRequest = child.Measure(Double.PositiveInfinity, Double.PositiveInfinity);
// Accumulate the maximum child size.
maxChildSize.Width = Math.Max(maxChildSize.Width, childSizeRequest.Request.Width);
maxChildSize.Height = Math.Max(maxChildSize.Height, childSizeRequest.Request.Height);
}
if (visibleChildCount != 0)
{
// Calculate the number of rows and columns.
if (Double.IsPositiveInfinity(width))
{
columns = visibleChildCount;
rows = 1;
}
else
{
columns = (int)((width + ColumnSpacing) / (maxChildSize.Width + ColumnSpacing));
columns = Math.Max(1, columns);
rows = (visibleChildCount + columns - 1) / columns;
}
// Now maximize the cell size based on the layout size.
Size cellSize = new Size();
if (Double.IsPositiveInfinity(width))
cellSize.Width = maxChildSize.Width;
else
cellSize.Width = (width - ColumnSpacing * (columns - 1)) / columns;
if (Double.IsPositiveInfinity(height))
cellSize.Height = maxChildSize.Height;
else
cellSize.Height = (height - RowSpacing * (rows - 1)) / rows;
layoutData = new LayoutData(visibleChildCount, cellSize, rows, columns);
}
layoutDataCache.Add(size, layoutData);
return layoutData;
}
Metoda GetLayoutData
provádí následující operace:
- Určuje, jestli je počítaná
LayoutData
hodnota již v mezipaměti, a vrátí ji, pokud je k dispozici. - V opačném případě provede výčet všech podřízených položek, vyvolá metodu
Measure
u každého podřízeného objektu s neomezenou šířkou a výškou a určí maximální velikost podřízené položky. - Za předpokladu, že existuje alespoň jedno viditelné dítě, vypočítá požadovaný počet řádků a sloupců a potom vypočítá velikost buňky pro podřízené položky na základě rozměrů
WrapLayout
. Všimněte si, že velikost buňky je obvykle o něco širší než maximální podřízená velikost, ale že může být také menší, pokudWrapLayout
není dostatečně široká pro nejširší dítě nebo dostatečně vysoké pro nejvyšší dítě. - Uloží novou
LayoutData
hodnotu do mezipaměti.
Přidání vlastností zálohovaných vlastnostmi s možností vazby
Třída WrapLayout
definuje ColumnSpacing
a RowSpacing
vlastnosti, jejichž hodnoty se používají k oddělení řádků a sloupců v rozložení a které jsou podporovány vlastnostmi s možností vazby. Vlastnosti s možností vytvoření vazby jsou uvedeny v následujícím příkladu kódu:
public static readonly BindableProperty ColumnSpacingProperty = BindableProperty.Create(
"ColumnSpacing",
typeof(double),
typeof(WrapLayout),
5.0,
propertyChanged: (bindable, oldvalue, newvalue) =>
{
((WrapLayout)bindable).InvalidateLayout();
});
public static readonly BindableProperty RowSpacingProperty = BindableProperty.Create(
"RowSpacing",
typeof(double),
typeof(WrapLayout),
5.0,
propertyChanged: (bindable, oldvalue, newvalue) =>
{
((WrapLayout)bindable).InvalidateLayout();
});
Obslužná rutina změněná vlastností každé bindable vlastnosti vyvolá InvalidateLayout
přepsání metody pro aktivaci nového předání rozložení na WrapLayout
. Další informace naleznete v tématu Přepsání InvalidateLayout Metoda a Override OnChildMeasureInvalidated Metoda.
Override OnMeasure – metoda
Přepsání OnMeasure
je znázorněno v následujícím příkladu kódu:
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
LayoutData layoutData = GetLayoutData(widthConstraint, heightConstraint);
if (layoutData.VisibleChildCount == 0)
{
return new SizeRequest();
}
Size totalSize = new Size(layoutData.CellSize.Width * layoutData.Columns + ColumnSpacing * (layoutData.Columns - 1),
layoutData.CellSize.Height * layoutData.Rows + RowSpacing * (layoutData.Rows - 1));
return new SizeRequest(totalSize);
}
Přepsání vyvolá metodu GetLayoutData
a vytvoří SizeRequest
objekt z vrácených dat, a zároveň bere v úvahu RowSpacing
hodnoty a ColumnSpacing
vlastnosti. Další informace o metodě naleznete v GetLayoutData
tématu Calculate and Cache Layout Data.
Důležité
Metody Measure
by OnMeasure
nikdy neměly požadovat nekonečné dimenze vrácením SizeRequest
hodnoty s vlastností nastavenou na Double.PositiveInfinity
. Alespoň jeden z argumentů OnMeasure
omezení však může být Double.PositiveInfinity
.
Override the LayoutChildren – metoda
Přepsání LayoutChildren
je znázorněno v následujícím příkladu kódu:
protected override void LayoutChildren(double x, double y, double width, double height)
{
LayoutData layoutData = GetLayoutData(width, height);
if (layoutData.VisibleChildCount == 0)
{
return;
}
double xChild = x;
double yChild = y;
int row = 0;
int column = 0;
foreach (View child in Children)
{
if (!child.IsVisible)
{
continue;
}
LayoutChildIntoBoundingRegion(child, new Rectangle(new Point(xChild, yChild), layoutData.CellSize));
if (++column == layoutData.Columns)
{
column = 0;
row++;
xChild = x;
yChild += RowSpacing + layoutData.CellSize.Height;
}
else
{
xChild += ColumnSpacing + layoutData.CellSize.Width;
}
}
}
Přepsání začíná voláním GetLayoutData
metody a potom vyčíslí všechny podřízené položky tak, aby velikost a umístění v každé podřízené buňce. Toho dosáhnete vyvoláním LayoutChildIntoBoundingRegion
metody, která slouží k umístění podřízeného objektu do obdélníku na základě hodnot jeho HorizontalOptions
a VerticalOptions
vlastností. To je ekvivalentem volání metody dítěte Layout
.
Poznámka:
Všimněte si, že obdélník předaný LayoutChildIntoBoundingRegion
metodě zahrnuje celou oblast, ve které se podřízený objekt může nacházet.
Další informace o metodě naleznete v GetLayoutData
tématu Calculate and Cache Layout Data.
Přepsání invalidateLayout – metoda
Přepsání InvalidateLayout
se vyvolá, když jsou podřízené položky přidány nebo odebrány z rozložení, nebo když některá z WrapLayout
vlastností změní hodnotu, jak je znázorněno v následujícím příkladu kódu:
protected override void InvalidateLayout()
{
base.InvalidateLayout();
layoutInfoCache.Clear();
}
Přepsání zruší platnost rozložení a zahodí všechny informace o rozložení v mezipaměti.
Poznámka:
Chcete-li zastavit Layout
třídu vyvolání InvalidateLayout
metody při každém přidání nebo odebrání podřízené položky z rozložení, přepsání ShouldInvalidateOnChildAdded
a ShouldInvalidateOnChildRemoved
metody a vrácení false
. Třída rozložení pak může implementovat vlastní proces při přidání nebo odebrání podřízených položek.
Override OnChildMeasureInvalidated – metoda
Přepsání OnChildMeasureInvalidated
se vyvolá, když jedna z podřízených položek rozložení změní velikost a zobrazí se v následujícím příkladu kódu:
protected override void OnChildMeasureInvalidated()
{
base.OnChildMeasureInvalidated();
layoutInfoCache.Clear();
}
Přepsání zruší platnost podřízeného rozložení a zahodí všechny informace o rozložení v mezipaměti.
Využití WrapLayoutu
Třídu WrapLayout
lze použít tak, že ji umístíte na odvozený Page
typ, jak je znázorněno v následujícím příkladu kódu XAML:
<ContentPage ... xmlns:local="clr-namespace:ImageWrapLayout">
<ScrollView Margin="0,20,0,20">
<local:WrapLayout x:Name="wrapLayout" />
</ScrollView>
</ContentPage>
Ekvivalentní kód jazyka C# je uvedený níže:
public class ImageWrapLayoutPageCS : ContentPage
{
WrapLayout wrapLayout;
public ImageWrapLayoutPageCS()
{
wrapLayout = new WrapLayout();
Content = new ScrollView
{
Margin = new Thickness(0, 20, 0, 20),
Content = wrapLayout
};
}
...
}
Podřízené položky je pak možné přidat podle WrapLayout
potřeby. Následující příklad kódu ukazuje Image
prvky, které se přidávají do WrapLayout
:
protected override async void OnAppearing()
{
base.OnAppearing();
var images = await GetImageListAsync();
if (images != null)
{
foreach (var photo in images.Photos)
{
var image = new Image
{
Source = ImageSource.FromUri(new Uri(photo))
};
wrapLayout.Children.Add(image);
}
}
}
async Task<ImageList> GetImageListAsync()
{
try
{
string requestUri = "https://raw.githubusercontent.com/xamarin/docs-archive/master/Images/stock/small/stock.json";
string result = await _client.GetStringAsync(requestUri);
return JsonConvert.DeserializeObject<ImageList>(result);
}
catch (Exception ex)
{
Debug.WriteLine($"\tERROR: {ex.Message}");
}
return null;
}
Když se zobrazí stránka obsahující danou WrapLayout
stránku, ukázková aplikace asynchronně přistupuje ke vzdálenému souboru JSON obsahujícímu seznam fotek, vytvoří Image
prvek pro každou fotku WrapLayout
a přidá ji do souboru . Výsledkem je vzhled zobrazený na následujících snímcích obrazovky:
Následující snímky obrazovky znázorňují WrapLayout
, jak se otočí na šířku:
Počet sloupců v každém řádku závisí na velikosti fotky, šířce obrazovky a počtu pixelů na jednotku nezávislou na zařízení. Prvky Image
asynchronně načítají fotky, a proto třída obdrží časté volání své LayoutChildren
metody, protože WrapLayout
každý Image
prvek obdrží novou velikost na základě načtené fotografie.