Создание пользовательского макета в Xamarin.Forms
Xamarin.Forms определяет пять классов макета — StackLayout, AbsoluteLayout, RelativeLayout, Grid и FlexLayout, а каждый — по-другому. Однако иногда необходимо упорядочить содержимое страницы с помощью макета, который не предоставляется Xamarin.Forms. В этой статье объясняется, как написать пользовательский класс макета и продемонстрировать класс WrapLayout с учетом ориентации, который упорядочивает дочерние элементы по горизонтали на странице, а затем преобразует отображение последующих дочерних элементов в дополнительные строки.
Во Xamarin.Formsвсех классах макета наследуют класс Layout<T>
и ограничивают универсальный тип View
и производные типы. В свою очередь, Layout<T>
класс является производным от Layout
класса, который предоставляет механизм для размещения и изменения размера дочерних элементов.
Каждый визуальный элемент отвечает за определение собственного предпочтительного размера, который называется запрошенным размером. Page
, Layout
и Layout<View>
производные типы отвечают за определение расположения и размера своего дочернего или дочернего объекта относительно себя. Таким образом, макет включает отношение "родительский-дочерний", где родитель определяет, какой размер его дочерних элементов должен быть, но будет пытаться вместить запрошенный размер дочернего элемента.
Для создания пользовательского макета требуется тщательное понимание Xamarin.Forms циклов макета и недопустимости. Теперь эти циклы будут обсуждаться.
Макет
Макет начинается в верхней части визуального дерева со страницей, и он проходит через все ветви визуального дерева, чтобы охватывать каждый визуальный элемент на странице. Элементы, которые являются родителями для других элементов, отвечают за изменение размера и размещение своих детей относительно себя.
Класс VisualElement
определяет Measure
метод, который измеряет элемент для операций макета, и Layout
метод, указывающий прямоугольную область, в которую будет отображаться элемент. При запуске приложения и отображении первой страницы цикл макета, состоящий из первых Measure
вызовов, а затем Layout
вызовы начинаются с Page
объекта:
- Во время цикла макета каждый родительский элемент отвечает за вызов
Measure
метода в дочерних элементах. - После измерения дочерних элементов каждый родительский элемент отвечает за вызов
Layout
метода на дочерних элементах.
Этот цикл гарантирует, что каждый визуальный элемент на странице получает вызовы Measure
и Layout
методы. Процесс показан на следующей схеме:
Примечание.
Обратите внимание, что циклы макета также могут возникать в подмножестве визуального дерева, если что-то изменится на макет. К ним относятся элементы, добавляемые или удаленные из коллекции, например в StackLayout
коллекции, изменение IsVisible
свойства элемента или изменение размера элемента.
Каждый Xamarin.Forms класс, имеющий Content
или Children
свойство, имеет переопределимый LayoutChildren
метод. Пользовательские классы макетов, производные от Layout<View>
этого метода, должны переопределить этот метод и убедиться, что Measure
Layout
методы вызываются для всех дочерних элементов, чтобы предоставить нужный пользовательский макет.
Кроме того, каждый класс, производный от Layout
метода или Layout<View>
должен переопределить OnMeasure
метод, который определяет размер класса макета, который он должен быть, вызывая Measure
методы своих дочерних элементов.
Примечание.
Элементы определяют их размер на основе ограничений, указывающих, сколько пространства доступно для элемента в родительском элементе. Ограничения, передаваемые Measure
в методы, OnMeasure
могут варьироваться от 0 до Double.PositiveInfinity
. Элемент ограничен или полностью ограничен, когда он получает вызов метода Measure
с неиграничными аргументами - элемент ограничен определенным размером. Элемент не ограничен или частично ограничен, когда он получает вызов метода Measure
по крайней мере с одним аргументом, равным Double.PositiveInfinity
- бесконечное ограничение может рассматриваться как показывание автосвязи.
Недействительность
Недопустимое — это процесс, с помощью которого изменение элемента на странице активирует новый цикл макета. Элементы считаются недопустимыми, если они больше не имеют правильного размера или положения. Например, если FontSize
свойство Button
изменения, считается недопустимым, Button
так как он больше не будет иметь правильный размер. Изменение Button
размера может иметь эффект рябь изменений в макете через остальную часть страницы.
Элементы недействительна путем вызова InvalidateMeasure
метода, как правило, при изменении свойства элемента, который может привести к новому размеру элемента. Этот метод запускает MeasureInvalidated
событие, которое родительский дескриптор элемента запускает новый цикл макета.
Класс Layout
задает обработчик события для MeasureInvalidated
каждого дочернего элемента, добавленного в его Content
свойство или Children
коллекцию, и отсоединяет обработчик при удалении дочернего элемента. Таким образом, каждый элемент в визуальном дереве с дочерними элементами оповещается всякий раз, когда один из дочерних элементов изменяет размер. На следующей схеме показано, как изменение размера элемента в визуальном дереве может привести к изменениям, которые рябь вверх по дереву:
Layout
Однако класс пытается ограничить влияние изменения размера дочернего элемента на макет страницы. Если макет ограничен, изменение размера дочернего элемента не влияет ни на что большее, чем родительский макет в визуальном дереве. Однако обычно изменение размера макета влияет на то, как макет упорядочивает его дочерние элементы. Таким образом, любое изменение размера макета начнет цикл макета для макета, и макет получит вызовы его OnMeasure
и LayoutChildren
методов.
Класс Layout
также определяет InvalidateLayout
метод, имеющий аналогичную цель InvalidateMeasure
метода. Метод InvalidateLayout
должен вызываться всякий раз, когда изменения влияют на то, как позиции макета и размеры его дочерних элементов. Например, класс вызывает InvalidateLayout
метод всякий раз, Layout
когда дочерний элемент добавляется или удаляется из макета.
Можно InvalidateLayout
переопределить для реализации кэша, чтобы свести к минимуму повторяющиеся вызовы Measure
методов дочерних элементов макета. Переопределение InvalidateLayout
метода предоставит уведомление о добавлении или удалении дочерних элементов из макета. Аналогичным образом OnChildMeasureInvalidated
метод можно переопределить, чтобы предоставить уведомление при изменении одного из дочерних размеров макета. Для переопределения обоих методов настраиваемый макет должен реагировать путем очистки кэша. Дополнительные сведения см. в разделе "Вычисление и кэширование данных макета".
Создание пользовательского макета
Процесс создания пользовательского макета выглядит следующим образом:
Создайте класс, производный от класса
Layout<View>
. Дополнительные сведения см. в разделе "Создание оболочки".[необязательно] Добавьте свойства, поддерживаемые привязываемыми свойствами, для всех параметров, которые должны быть заданы в классе макета. Дополнительные сведения см. в разделе "Добавление свойств, поддерживаемых привязываемыми свойствами".
Переопределите
OnMeasure
метод, чтобы вызватьMeasure
метод на всех дочерних элементах макета и вернуть запрошенный размер макета. Дополнительные сведения см. в разделе "Переопределение метода OnMeasure".Переопределите
LayoutChildren
метод для вызоваLayout
метода во всех дочерних элементах макета. СбойLayout
вызова метода для каждого дочернего элемента в макете приведет к тому, что дочерний объект никогда не получает правильный размер или положение, поэтому дочерний элемент не станет видимым на странице. Дополнительные сведения см. в разделе "Переопределение метода LayoutChildren".Примечание.
При перечислении дочерних элементов в
OnMeasure
иLayoutChildren
переопределении пропустите любой дочерний элемент, для которогоIsVisible
заданоfalse
свойство. Это обеспечит, чтобы пользовательский макет не оставлял место для невидимых дочерних элементов.[необязательно] Переопределите
InvalidateLayout
метод, который следует уведомлять при добавлении или удалении дочерних элементов из макета. Дополнительные сведения см. в разделе "Переопределение метода InvalidateLayout".[необязательно] Переопределите
OnChildMeasureInvalidated
метод, чтобы получать уведомления при изменении размера одного из дочерних элементов макета. Дополнительные сведения см. в разделе "Переопределение метода OnChildMeasureInvalidated".
Примечание.
Обратите внимание, что OnMeasure
переопределение не будет вызываться, если размер макета регулируется родительским элементом, а не его дочерними элементами. Однако переопределение будет вызываться, если одно или оба ограничения являются бесконечными, или если класс макета имеет значения свойств, отличных от по умолчанию HorizontalOptions
или VerticalOptions
свойств. По этой причине переопределение не может полагаться на дочерние размеры, LayoutChildren
полученные во время OnMeasure
вызова метода. Вместо этого LayoutChildren
необходимо вызвать Measure
метод в дочерних элементах макета перед вызовом Layout
метода. Кроме того, размер дочерних элементов, полученных в OnMeasure
переопределении, можно кэшировать, чтобы избежать последующих Measure
вызовов в LayoutChildren
переопределении, но класс макета должен знать, когда размеры должны быть получены снова. Дополнительные сведения см. в разделе "Вычисление и кэширование данных макета".
Затем класс макета можно использовать, добавив его в Page
макет, и добавив дочерние элементы в макет. Дополнительные сведения см. в разделе "Использование WrapLayout".
Создание оболочки
Пример приложения демонстрирует класс с учетом WrapLayout
ориентации, который упорядочивает дочерние элементы по горизонтали на странице, а затем упаковывает отображение последующих дочерних элементов в дополнительные строки.
Класс WrapLayout
выделяет одинаковое количество места для каждого дочернего элемента, известного как размер ячейки, на основе максимального размера дочерних элементов. Дочерние элементы меньше, чем размер ячейки, можно разместить в ячейке на основе их HorizontalOptions
и VerticalOptions
значений свойств.
Определение WrapLayout
класса показано в следующем примере кода:
public class WrapLayout : Layout<View>
{
Dictionary<Size, LayoutData> layoutDataCache = new Dictionary<Size, LayoutData>();
...
}
Вычисление и кэширование данных макета
Структура LayoutData
хранит данные о коллекции дочерних элементов в ряде свойств:
VisibleChildCount
— число дочерних элементов, видимых в макете.CellSize
— максимальный размер всех дочерних элементов, отрегулированных по размеру макета.Rows
— количество строк.Columns
— количество столбцов.
Поле layoutDataCache
используется для хранения нескольких LayoutData
значений. При запуске приложения два LayoutData
объекта будут кэшироваться в layoutDataCache
словарь для текущей ориентации — один для аргументов OnMeasure
ограничения переопределения, а один — для width
переопределения и height
аргументов LayoutChildren
переопределения. При повороте устройства в альбомную ориентацию OnMeasure
переопределение и LayoutChildren
переопределение снова вызывается, что приведет к кэшированию еще двух LayoutData
объектов в словарь. Однако при возвращении устройства в книжную ориентацию дальнейшие вычисления не требуются, так как layoutDataCache
у него уже есть необходимые данные.
В следующем примере кода показан GetLayoutData
метод, который вычисляет свойства структурированного на основе определенного LayoutData
размера:
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;
}
Метод GetLayoutData
выполняет следующие операции:
- Он определяет, уже ли вычисляемое
LayoutData
значение находится в кэше и возвращает его, если оно доступно. - В противном случае он перечисляет все дочерние элементы, вызывая
Measure
метод для каждого дочернего элемента с бесконечной шириной и высотой, и определяет максимальный размер дочернего элемента. - При условии, что есть хотя бы один видимый дочерний элемент, он вычисляет количество строк и столбцов, необходимых, а затем вычисляет размер ячейки для дочерних элементов на основе измерений
WrapLayout
. Обратите внимание, что размер ячейки обычно немного шире, чем максимальный размер ребенка, но он также может быть меньше, еслиWrapLayout
недостаточно широкий для самого широкого ребенка или достаточно высокого для самого высокого ребенка. - Он сохраняет новое
LayoutData
значение в кэше.
Добавление свойств, поддерживаемых привязываемыми свойствами
Класс WrapLayout
определяет ColumnSpacing
и RowSpacing
свойства, значения которых используются для разделения строк и столбцов в макете и которые поддерживаются привязываемыми свойствами. Свойства, привязываемые, показаны в следующем примере кода:
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();
});
Обработчик, измененный свойством каждого привязываемого свойства, вызывает InvalidateLayout
переопределение метода для активации нового прохода макета WrapLayout
. Дополнительные сведения см. в разделе "Переопределение метода InvalidateLayout" и переопределение метода OnChildMeasureInvalidated.
Переопределите метод OnMeasure
Переопределение OnMeasure
показано в следующем примере кода:
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);
}
Переопределение вызывает GetLayoutData
метод и создает SizeRequest
объект из возвращаемых данных, а также учитывает RowSpacing
значения свойств и ColumnSpacing
значений свойств. Дополнительные сведения о методе см. в разделе "Вычисление GetLayoutData
и кэширование данных макета".
Внимание
Методы Measure
никогда не должны запрашивать бесконечное измерение, возвращая SizeRequest
значение с заданным свойствомDouble.PositiveInfinity
.OnMeasure
Однако хотя бы один из аргументов OnMeasure
ограничения может быть Double.PositiveInfinity
.
Переопределите метод LayoutChildren
Переопределение LayoutChildren
показано в следующем примере кода:
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;
}
}
}
Переопределение начинается с вызова GetLayoutData
метода, а затем перечисляет все дочерние элементы для их размера и размещения в ячейке каждого дочернего элемента. Это достигается путем вызова LayoutChildIntoBoundingRegion
метода, который используется для размещения дочернего объекта в прямоугольнике на основе его HorizontalOptions
и VerticalOptions
значений свойств. Это эквивалентно вызову метода дочернего Layout
объекта.
Примечание.
Обратите внимание, что прямоугольник, переданный LayoutChildIntoBoundingRegion
методу, включает всю область, в которой может находиться дочерний элемент.
Дополнительные сведения о методе см. в разделе "Вычисление GetLayoutData
и кэширование данных макета".
Переопределите метод InvalidateLayout
InvalidateLayout
Переопределение вызывается при добавлении или удалении дочерних элементов из макета или при изменении одного из WrapLayout
значений свойств, как показано в следующем примере кода:
protected override void InvalidateLayout()
{
base.InvalidateLayout();
layoutInfoCache.Clear();
}
Переопределение отменяет макет и удаляет все сведения о кэшированном макете.
Примечание.
Чтобы остановить вызов класса InvalidateLayout
при каждом добавлении или удалении дочернего элемента из макета, переопределении ShouldInvalidateOnChildAdded
методов ShouldInvalidateOnChildRemoved
и возвратеfalse
.Layout
Затем класс макета может реализовать пользовательский процесс при добавлении или удалении дочерних элементов.
Переопределите метод OnChildMeasureInvalidated
Переопределение OnChildMeasureInvalidated
вызывается при изменении одного из дочерних размеров макета и отображается в следующем примере кода:
protected override void OnChildMeasureInvalidated()
{
base.OnChildMeasureInvalidated();
layoutInfoCache.Clear();
}
Переопределение отменяет дочерний макет и удаляет все кэшированные сведения о макете.
Использование wrapLayout
Класс WrapLayout
можно использовать, поместив его в производный Page
тип, как показано в следующем примере кода XAML:
<ContentPage ... xmlns:local="clr-namespace:ImageWrapLayout">
<ScrollView Margin="0,20,0,20">
<local:WrapLayout x:Name="wrapLayout" />
</ScrollView>
</ContentPage>
Ниже приведен эквивалентный код на C#:
public class ImageWrapLayoutPageCS : ContentPage
{
WrapLayout wrapLayout;
public ImageWrapLayoutPageCS()
{
wrapLayout = new WrapLayout();
Content = new ScrollView
{
Margin = new Thickness(0, 20, 0, 20),
Content = wrapLayout
};
}
...
}
Затем дочерние элементы можно добавить в необходимые WrapLayout
элементы. В следующем примере кода показаны Image
элементы, добавляемые в 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;
}
Когда откроется страница WrapLayout
, пример приложения асинхронно обращается к удаленному JSON-файлу, содержаму список фотографий, создает Image
элемент для каждой фотографии и добавляет его в него WrapLayout
. Результат показан на следующих снимках экрана:
На следующих снимках WrapLayout
экрана показана после поворота на альбомную ориентацию:
Количество столбцов в каждой строке зависит от размера фотографии, ширины экрана и количества пикселей на устройство независимо от устройства. Элементы Image
асинхронно загружают фотографии, поэтому WrapLayout
класс будет получать частые вызовы к его LayoutChildren
методу, так как каждый Image
элемент получает новый размер на основе загруженной фотографии.