Vytvoření vlastního rozložení v aplikaci Xamarin.Forms

Download Sample Stažení ukázky

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, Layouta 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:

  1. 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.
  2. 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:

Xamarin.Forms Layout Cycle

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

Invalidation in the Visual Tree

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

  1. Vytvořte třídu, která je odvozena od třídy Layout<View>. Další informace naleznete v tématu Vytvoření WrapLayout.

  2. [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.

  3. Přepište metodu OnMeasure , která vyvolá metodu Measure 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.

  4. Přepište metodu LayoutChildrenLayout 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 a LayoutChildren přepsání přeskočte všechny podřízené položky, jejichž IsVisible vlastnost je nastavena na falsehodnotu . Tím zajistíte, že vlastní rozložení nezanechá prostor pro neviditelné podřízené položky.

  5. [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.

  6. [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 Pagea 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ých
  • Rows – 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ší, pokud WrapLayout 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 WrapLayouta přidá ji do souboru . Výsledkem je vzhled zobrazený na následujících snímcích obrazovky:

Sample Application Portrait Screenshots

Následující snímky obrazovky znázorňují WrapLayout , jak se otočí na šířku:

Sample iOS Application Landscape ScreenshotSample Android Application Landscape ScreenshotSample UWP Application Landscape Screenshot

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.