Tworzenie niestandardowego układu w programie Xamarin.Forms
Xamarin.Forms Definiuje pięć klas układu — StackLayout, AbsoluteLayout, RelativeLayout, Grid i FlexLayout, a każda z nich rozmieszcza swoje elementy podrzędne w inny sposób. Czasami jednak konieczne jest organizowanie zawartości strony przy użyciu układu, który nie jest dostarczany przez Xamarin.Formsprogram . W tym artykule wyjaśniono, jak napisać niestandardową klasę układu i przedstawiono klasę WrapLayout z uwzględnieniem orientacji, która rozmieszcza swoje elementy podrzędne w poziomie na stronie, a następnie opakowuje wyświetlanie kolejnych elementów podrzędnych do dodatkowych wierszy.
W Xamarin.Formssystemie wszystkie klasy układu pochodzą z Layout<T>
klasy i ogranicz typ ogólny do View
i jego typów pochodnych. Z kolei Layout<T>
klasa pochodzi z Layout
klasy, która zapewnia mechanizm pozycjonowania i określania rozmiaru elementów podrzędnych.
Każdy element wizualizacji jest odpowiedzialny za określenie własnego preferowanego rozmiaru, który jest nazywany żądanym rozmiarem. Page
, Layout
i Layout<View>
typy pochodne są odpowiedzialne za określenie lokalizacji i rozmiaru ich dziecka lub dzieci względem siebie. W związku z tym układ obejmuje relację nadrzędny-podrzędny, w której element nadrzędny określa, jaki powinien być rozmiar jego elementów podrzędnych, ale podejmie próbę dostosowania żądanego rozmiaru elementu podrzędnego.
Do utworzenia układu niestandardowego wymagane jest dokładne zrozumienie Xamarin.Forms układu i cykli unieważniania. Te cykle zostaną teraz omówione.
Układ
Układ zaczyna się w górnej części drzewa wizualizacji ze stroną i przechodzi przez wszystkie gałęzie drzewa wizualnego, aby objąć każdy element wizualizacji na stronie. Elementy, które są rodzicami innych elementów, są odpowiedzialne za ustalanie rozmiaru i pozycjonowanie swoich dzieci względem siebie.
Klasa VisualElement
definiuje metodę Measure
, która mierzy element dla operacji układu, a Layout
metoda określająca prostokątny obszar, w ramach którego element zostanie renderowany. Po uruchomieniu aplikacji i wyświetleniu pierwszej strony cykl układu składający się najpierw z Measure
wywołań, a następnie Layout
wywołania uruchamia się na Page
obiekcie:
- Podczas cyklu układu każdy element nadrzędny jest odpowiedzialny za wywoływanie
Measure
metody dla jej elementów podrzędnych. - Po zmierzeniu elementów podrzędnych każdy element nadrzędny jest odpowiedzialny za wywoływanie
Layout
metody dla jej elementów podrzędnych.
Ten cykl gwarantuje, że każdy element wizualizacji na stronie odbiera wywołania Measure
metod i Layout
. Proces pokazano na poniższym diagramie:
Uwaga
Należy pamiętać, że cykle układu mogą również występować w podzestawie drzewa wizualnego, jeśli coś się zmieni, aby wpłynąć na układ. Obejmuje to dodawanie lub usuwanie elementów z kolekcji, takich jak w StackLayout
obiekcie , zmiana IsVisible
właściwości elementu lub zmiana rozmiaru elementu.
Każda Xamarin.Forms klasa, która ma Content
właściwość lub Children
, ma metodę przesłoniętą LayoutChildren
. Niestandardowe klasy układu pochodzące z Layout<View>
klasy muszą zastąpić tę metodę i upewnić się, że Measure
metody i Layout
są wywoływane dla wszystkich elementów podrzędnych elementu, aby zapewnić żądany układ niestandardowy.
Ponadto każda klasa, która pochodzi z Layout
metody lub Layout<View>
musi przesłonić OnMeasure
metodę, w której klasa układu określa rozmiar, który musi być przez wywołania Measure
metod jej elementów podrzędnych.
Uwaga
Elementy określają ich rozmiar na podstawie ograniczeń, które wskazują, ile miejsca jest dostępne dla elementu nadrzędnego elementu. Ograniczenia przekazywane do Measure
metod i OnMeasure
mogą wahać się od 0 do Double.PositiveInfinity
. Element jest ograniczony lub w pełni ograniczony, gdy odbiera wywołanie Measure
metody z argumentami nieskończenie nieskończonymi — element jest ograniczony do określonego rozmiaru. Element nie jest ograniczony lub częściowo ograniczony, gdy odbiera wywołanie Measure
metody z co najmniej jednym argumentem równym Double.PositiveInfinity
— nieskończone ograniczenie można traktować jako wskazujące autoskalowanie.
Unieważnienie
Unieważnienie to proces, przez który zmiana elementu na stronie wyzwala nowy cykl układu. Elementy są uznawane za nieprawidłowe, gdy nie mają już poprawnego rozmiaru ani położenia. Na przykład, jeśli FontSize
właściwość Button
zmian, mówi się, że jest nieprawidłowa, Button
ponieważ nie będzie już mieć poprawnego rozmiaru. Zmiana rozmiaru Button
może mieć wpływ na zmiany układu w pozostałej części strony.
Elementy unieważniają się przez wywołanie InvalidateMeasure
metody, zazwyczaj gdy właściwość elementu ulegnie zmianie, co może spowodować nowy rozmiar elementu. Ta metoda uruchamia MeasureInvalidated
zdarzenie, które element nadrzędny obsługuje w celu wyzwolenia nowego cyklu układu.
Klasa Layout
ustawia procedurę obsługi dla zdarzenia dla MeasureInvalidated
każdego elementu podrzędnego dodanego do jego Content
właściwości lub Children
kolekcji i odłącza program obsługi po usunięciu elementu podrzędnego. W związku z tym każdy element w drzewie wizualnym, który zawiera elementy podrzędne, jest powiadamiany za każdym razem, gdy jeden z jego elementów podrzędnych zmienia rozmiar. Na poniższym diagramie pokazano, jak zmiana rozmiaru elementu w drzewie wizualnym może spowodować zmiany, które falują drzewo:
Layout
Jednak klasa próbuje ograniczyć wpływ zmiany rozmiaru elementu podrzędnego na układ strony. Jeśli układ ma ograniczenie rozmiaru, zmiana rozmiaru podrzędnego nie ma wpływu na coś wyższego niż układ nadrzędny w drzewie wizualizacji. Jednak zazwyczaj zmiana rozmiaru układu wpływa na sposób rozmieszczania elementów podrzędnych w układzie. W związku z tym każda zmiana rozmiaru układu rozpocznie cykl układu dla układu, a układ będzie otrzymywać wywołania do jego OnMeasure
metod i LayoutChildren
.
Klasa Layout
definiuje również metodę InvalidateLayout
podobną do InvalidateMeasure
metody . Metoda InvalidateLayout
powinna być wywoływana za każdym razem, gdy zostanie wprowadzona zmiana, która wpływa na położenie układu i rozmiar jego elementów podrzędnych. Na przykład klasa wywołuje metodę InvalidateLayout
za każdym razem, Layout
gdy element podrzędny zostanie dodany do lub usunięty z układu.
Element InvalidateLayout
może zostać zastąpiony w celu zaimplementowania pamięci podręcznej w celu zminimalizowania powtarzających się wywołań Measure
metod elementów podrzędnych układu. InvalidateLayout
Zastąpienie metody spowoduje powiadomienie o dodaniu lub usunięciu elementów podrzędnych z układu. Podobnie można zastąpić metodę, OnChildMeasureInvalidated
aby podać powiadomienie, gdy jeden z elementów podrzędnych układu zmienia rozmiar. W przypadku obu przesłonięć metod układ niestandardowy powinien odpowiadać przez wyczyszczenie pamięci podręcznej. Aby uzyskać więcej informacji, zobacz Calculate and Cache Layout Data (Obliczanie i buforowanie danych układu).
Tworzenie układu niestandardowego
Proces tworzenia układu niestandardowego jest następujący:
Utwórz klasę pochodną od klasy
Layout<View>
. Aby uzyskać więcej informacji, zobacz Create a WrapLayout (Tworzenie elementu WrapLayout).[opcjonalnie] Dodaj właściwości oparte na właściwościach możliwych do powiązania dla wszystkich parametrów, które powinny być ustawione w klasie układu. Aby uzyskać więcej informacji, zobacz Dodawanie właściwości wspieranych przez właściwości możliwe do powiązania.
Zastąpi metodę
OnMeasure
, aby wywołać metodęMeasure
dla wszystkich elementów podrzędnych układu i zwrócić żądany rozmiar układu. Aby uzyskać więcej informacji, zobacz Zastępowanie metody OnMeasure.Zastąpi metodę
LayoutChildren
, aby wywołać metodęLayout
we wszystkich elementach podrzędnych układu. Niepowodzenie wywołaniaLayout
metody dla każdego elementu podrzędnego w układzie spowoduje, że element podrzędny nigdy nie otrzymuje poprawnego rozmiaru lub położenia, dlatego element podrzędny nie będzie widoczny na stronie. Aby uzyskać więcej informacji, zobacz Zastępowanie metody LayoutChildren.Uwaga
Podczas wyliczania elementów podrzędnych w przesłonięciach
OnMeasure
iLayoutChildren
pomiń wszystkie elementy podrzędne, którychIsVisible
właściwość jest ustawiona nafalse
. Dzięki temu układ niestandardowy nie pozostawi miejsca dla niewidocznych elementów podrzędnych.[opcjonalnie] Zastąpij metodę
InvalidateLayout
, aby otrzymywać powiadomienia, gdy elementy podrzędne zostaną dodane do układu lub usunięte z niego. Aby uzyskać więcej informacji, zobacz Zastępowanie metody InvalidateLayout.[opcjonalnie] Zastąpij metodę
OnChildMeasureInvalidated
, aby otrzymywać powiadomienia, gdy jeden z elementów podrzędnych układu zmienia rozmiar. Aby uzyskać więcej informacji, zobacz Zastępowanie metody OnChildMeasureInvalidated.
Uwaga
Należy pamiętać, że OnMeasure
przesłonięcia nie będą wywoływane, jeśli rozmiar układu jest zarządzany przez jego element nadrzędny, a nie jego elementy podrzędne. Jednak przesłonięcia będą wywoływane, jeśli jedno lub oba ograniczenia są nieskończone lub jeśli klasa układu ma wartości inne niż domyślne HorizontalOptions
lub VerticalOptions
właściwości. Z tego powodu LayoutChildren
przesłonięcia nie mogą polegać na rozmiarach podrzędnych uzyskanych podczas wywołania OnMeasure
metody. LayoutChildren
Zamiast tego należy wywołać metodę Measure
dla elementów podrzędnych układu przed wywołaniem Layout
metody . Alternatywnie rozmiar elementów podrzędnych uzyskanych w OnMeasure
przesłonięciu może być buforowany, aby uniknąć późniejszego Measure
wywołania w LayoutChildren
przesłonięciu, ale klasa układu będzie musiała wiedzieć, kiedy należy ponownie uzyskać rozmiary. Aby uzyskać więcej informacji, zobacz Calculate and Cache Layout Data (Obliczanie i buforowanie danych układu).
Następnie można użyć klasy układu, dodając ją do Page
elementu , i dodając elementy podrzędne do układu. Aby uzyskać więcej informacji, zobacz Korzystanie z elementu WrapLayout.
Tworzenie obiektu WrapLayout
Przykładowa aplikacja demonstruje klasę wrażliwą na WrapLayout
orientację, która rozmieszcza swoje elementy podrzędne w poziomie na stronie, a następnie opakowuje wyświetlanie kolejnych elementów podrzędnych do dodatkowych wierszy.
Klasa WrapLayout
przydziela taką samą ilość miejsca dla każdego elementu podrzędnego, znanego jako rozmiar komórki, na podstawie maksymalnego rozmiaru elementów podrzędnych. Elementy podrzędne mniejsze niż rozmiar komórki można umieścić w komórce na podstawie ich HorizontalOptions
wartości właściwości i VerticalOptions
.
Definicja klasy jest wyświetlana WrapLayout
w poniższym przykładzie kodu:
public class WrapLayout : Layout<View>
{
Dictionary<Size, LayoutData> layoutDataCache = new Dictionary<Size, LayoutData>();
...
}
Obliczanie i buforowanie danych układu
Struktura LayoutData
przechowuje dane dotyczące kolekcji elementów podrzędnych w wielu właściwościach:
VisibleChildCount
— liczba elementów podrzędnych widocznych w układzie.CellSize
– maksymalny rozmiar wszystkich elementów podrzędnych, dostosowany do rozmiaru układu.Rows
— liczba wierszy.Columns
— liczba kolumn.
Pole layoutDataCache
jest używane do przechowywania wielu LayoutData
wartości. Po uruchomieniu aplikacji dwa LayoutData
obiekty będą buforowane w słowniku layoutDataCache
dla bieżącej orientacji — jeden dla argumentów ograniczeń do przesłonięcia, a jeden dla width
argumentów OnMeasure
i height
do LayoutChildren
przesłonięcia. Podczas rotacji urządzenia w orientację OnMeasure
poziomą przesłonięcia i LayoutChildren
przesłonięcia zostaną ponownie wywołane, co spowoduje buforowanie kolejnych dwóch LayoutData
obiektów w słowniku. Jednak podczas zwracania urządzenia do orientacji pionowej nie są wymagane żadne dalsze obliczenia, ponieważ layoutDataCache
te dane są już wymagane.
Poniższy przykład kodu przedstawia metodę GetLayoutData
, która oblicza właściwości LayoutData
struktury na podstawie określonego rozmiaru:
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
wykonuje następujące operacje:
- Określa, czy wartość obliczeniowa
LayoutData
znajduje się już w pamięci podręcznej i zwraca ją, jeśli jest dostępna. - W przeciwnym razie wylicza wszystkie elementy podrzędne, wywołując
Measure
metodę dla każdego elementu podrzędnego o nieskończonej szerokości i wysokości, i określa maksymalny rozmiar elementu podrzędnego. - Pod warunkiem, że istnieje co najmniej jeden widoczny element podrzędny, oblicza liczbę wymaganych wierszy i kolumn, a następnie oblicza rozmiar komórki dla elementów podrzędnych na podstawie wymiarów
WrapLayout
elementu . Należy pamiętać, że rozmiar komórki jest zwykle nieco szerszy niż maksymalny rozmiar dziecka, ale może być również mniejszy, jeśliWrapLayout
nie jest wystarczająco szeroki dla najszerszego dziecka lub wystarczająco wysoki dla najwyższego dziecka. - Przechowuje nową
LayoutData
wartość w pamięci podręcznej.
Dodawanie właściwości wspieranych przez właściwości możliwe do powiązania
Klasa WrapLayout
definiuje ColumnSpacing
i RowSpacing
właściwości, których wartości są używane do oddzielania wierszy i kolumn w układzie oraz które są wspierane przez powiązane właściwości. Właściwości możliwe do powiązania są wyświetlane w poniższym przykładzie kodu:
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();
});
Program obsługi zmienionych właściwości każdej właściwości możliwej do powiązania wywołuje InvalidateLayout
zastąpienie metody w celu wyzwolenia nowego układu przekazywanego w WrapLayout
obiekcie . Aby uzyskać więcej informacji, zobacz Zastąp metodę InvalidateLayout i zastąp metodę OnChildMeasureInvalidated.
Zastąpij metodę OnMeasure
Przesłonięcia OnMeasure
pokazano w poniższym przykładzie kodu:
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);
}
Przesłonięcia wywołuje metodę GetLayoutData
i konstruuje SizeRequest
obiekt z zwracanych danych, a także uwzględnia RowSpacing
wartości właściwości i ColumnSpacing
. Aby uzyskać więcej informacji na temat GetLayoutData
metody, zobacz Calculate and Cache Layout Data (Obliczanie i buforowanie danych układu).
Ważne
Measure
Metody i OnMeasure
nigdy nie powinny żądać nieskończonego wymiaru, zwracając SizeRequest
wartość z właściwością ustawioną na Double.PositiveInfinity
. Jednak co najmniej jeden z argumentów OnMeasure
ograniczeń może mieć wartość Double.PositiveInfinity
.
Zastąpij metodę LayoutChildren
Przesłonięcia LayoutChildren
pokazano w poniższym przykładzie kodu:
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;
}
}
}
Przesłonięcia zaczynają się od wywołania GetLayoutData
metody, a następnie wylicza wszystkie elementy podrzędne do rozmiaru i umieszczania ich w komórce każdego elementu podrzędnego. Jest to osiągane przez wywołanie LayoutChildIntoBoundingRegion
metody , która służy do umieszczania elementu podrzędnego w prostokątze na podstawie jej HorizontalOptions
wartości właściwości i VerticalOptions
. Jest to równoważne wywołaniu metody podrzędnej Layout
.
Uwaga
Należy pamiętać, że prostokąt przekazany do LayoutChildIntoBoundingRegion
metody zawiera cały obszar, w którym może znajdować się element podrzędny.
Aby uzyskać więcej informacji na temat GetLayoutData
metody, zobacz Calculate and Cache Layout Data (Obliczanie i buforowanie danych układu).
Zastąpij metodę InvalidateLayout
Przesłonięcia InvalidateLayout
są wywoływane, gdy elementy podrzędne są dodawane do układu lub usuwane z układu albo gdy jedna z WrapLayout
właściwości zmienia wartość, jak pokazano w poniższym przykładzie kodu:
protected override void InvalidateLayout()
{
base.InvalidateLayout();
layoutInfoCache.Clear();
}
Zastąpić unieważnia układ i odrzuca wszystkie buforowane informacje o układzie.
Uwaga
Aby zatrzymać Layout
wywołanie InvalidateLayout
klasy za każdym razem, gdy element podrzędny zostanie dodany do lub usunięty z układu, przesłonięć ShouldInvalidateOnChildAdded
metody i ShouldInvalidateOnChildRemoved
i zwrócić wartość false
. Klasa układu może następnie zaimplementować proces niestandardowy po dodaniu lub usunięciu elementów podrzędnych.
Zastąp metodę OnChildMeasureInvalidated
Przesłonięcia OnChildMeasureInvalidated
jest wywoływane, gdy jeden z elementów podrzędnych układu zmienia rozmiar i jest wyświetlany w poniższym przykładzie kodu:
protected override void OnChildMeasureInvalidated()
{
base.OnChildMeasureInvalidated();
layoutInfoCache.Clear();
}
Zastąpić unieważnia układ podrzędny i odrzuca wszystkie buforowane informacje o układzie.
Korzystanie z obiektu WrapLayout
Klasę WrapLayout
można użyć, umieszczając ją na typie pochodnym Page
, jak pokazano w poniższym przykładzie kodu XAML:
<ContentPage ... xmlns:local="clr-namespace:ImageWrapLayout">
<ScrollView Margin="0,20,0,20">
<local:WrapLayout x:Name="wrapLayout" />
</ScrollView>
</ContentPage>
Równoważny kod języka C# jest pokazany poniżej:
public class ImageWrapLayoutPageCS : ContentPage
{
WrapLayout wrapLayout;
public ImageWrapLayoutPageCS()
{
wrapLayout = new WrapLayout();
Content = new ScrollView
{
Margin = new Thickness(0, 20, 0, 20),
Content = wrapLayout
};
}
...
}
Następnie można dodać elementy podrzędne do elementu zgodnie z WrapLayout
wymaganiami. Poniższy przykład kodu przedstawia Image
elementy dodawane do elementu 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;
}
Gdy zostanie wyświetlona strona zawierająca WrapLayout
element, przykładowa aplikacja asynchronicznie uzyskuje dostęp do zdalnego pliku JSON zawierającego listę zdjęć, tworzy Image
element dla każdego zdjęcia i dodaje go do pliku WrapLayout
. Spowoduje to wyświetlenie wyglądu pokazanego na poniższych zrzutach ekranu:
Na poniższych zrzutach ekranu przedstawiono WrapLayout
po obróceniu do orientacji poziomej:
Liczba kolumn w każdym wierszu zależy od rozmiaru zdjęcia, szerokości ekranu i liczby pikseli na jednostkę niezależną od urządzenia. Image
Elementy asynchronicznie ładują zdjęcia, a zatem WrapLayout
klasa będzie otrzymywać częste wywołania metodyLayoutChildren
, ponieważ każdy Image
element otrzymuje nowy rozmiar na podstawie załadowanego zdjęcia.