Compartilhar via


Criar um layout personalizado em Xamarin.Forms

Xamarin.Forms define cinco classes de layout – StackLayout, AbsoluteLayout, RelativeLayout, Grid e FlexLayout, e cada uma organiza seus filhos de uma maneira diferente. No entanto, às vezes é necessário organizar o conteúdo da página usando um layout não fornecido pelo Xamarin.Forms. Este artigo explica como escrever uma classe de layout personalizada e demonstra uma classe WrapLayout sensível à orientação que organiza seus filhos horizontalmente na página e, em seguida, quebra a exibição de filhos subsequentes em linhas adicionais.

No Xamarin.Forms, todas as classes de layout derivam da Layout<T> classe e restringem o tipo genérico a View e seus tipos derivados. Por sua vez, a Layout<T> classe deriva da Layout classe, que fornece o mecanismo para posicionar e dimensionar elementos filho.

Cada elemento visual é responsável por determinar seu próprio tamanho preferido, que é conhecido como o tamanho solicitado . Page, Layoute Layout<View> tipos derivados são responsáveis por determinar a localização e o tamanho de seus filhos, ou filhos, em relação a si mesmos. Portanto, o layout envolve uma relação pai-filho, onde o pai determina qual deve ser o tamanho de seus filhos, mas tentará acomodar o tamanho solicitado da criança.

Uma compreensão completa do layout e dos Xamarin.Forms ciclos de invalidação é necessária para criar um layout personalizado. Esses ciclos serão agora discutidos.

Layout

O layout começa na parte superior da árvore visual com uma página e prossegue por todos os ramos da árvore visual para abranger todos os elementos visuais em uma página. Elementos que são pais de outros elementos são responsáveis por dimensionar e posicionar seus filhos em relação a si mesmos.

A VisualElement classe define um Measure método que mede um elemento para operações de layout e um Layout método que especifica a área retangular em que o elemento será renderizado. Quando um aplicativo é iniciado e a primeira página é exibida, um ciclo de layout que consiste primeiro em Measure chamadas e, em seguida, em chamadas Layout , começa no Page objeto:

  1. Durante o ciclo de layout, cada elemento pai é responsável por chamar o Measure método em seus filhos.
  2. Depois que os filhos são medidos, cada elemento pai é responsável por chamar o Layout método em seus filhos.

Esse ciclo garante que cada elemento visual na página receba chamadas para os Measure métodos e Layout . O processo é mostrado no diagrama a seguir:

Xamarin.Forms Ciclo de Layout

Observação

Observe que os ciclos de layout também podem ocorrer em um subconjunto da árvore visual se algo for alterado para afetar o layout. Isso inclui itens que estão sendo adicionados ou removidos de uma coleção, como em um StackLayout, uma alteração na IsVisible propriedade de um elemento ou uma alteração no tamanho de um elemento.

Cada Xamarin.Forms classe que tem uma Content ou uma Children propriedade tem um método substituível LayoutChildren . As classes de layout personalizadas derivadas de Layout<View> devem substituir esse método e garantir que os Measure métodos e Layout sejam chamados em todos os filhos do elemento, para fornecer o layout personalizado desejado.

Além disso, cada classe que deriva de Layout ou Layout<View> deve substituir o OnMeasure método, que é onde uma classe de layout determina o tamanho que precisa ser fazendo chamadas para os Measure métodos de seus filhos.

Observação

Os elementos determinam seu tamanho com base em restrições, que indicam quanto espaço está disponível para um elemento dentro do pai do elemento. As restrições passadas para os Measure métodos e OnMeasure podem variar de 0 a Double.PositiveInfinity. Um elemento é restringido, ou totalmente restringido, quando recebe uma chamada para seu Measure método com argumentos não infinitos - o elemento é restrito a um tamanho específico. Um elemento é irrestrito, ou parcialmente constrangido, quando recebe uma chamada para seu Measure método com pelo menos um argumento igual a Double.PositiveInfinity – a restrição infinita pode ser pensada como indicando autodimensionamento.

Invalidação

Invalidação é o processo pelo qual uma alteração em um elemento em uma página dispara um novo ciclo de layout. Os elementos são considerados inválidos quando não têm mais o tamanho ou a posição correta. Por exemplo, se a FontSize propriedade de um Button for alterada, o Button é dito ser inválido porque ele não terá mais o tamanho correto. O redimensionamento do Button pode então ter um efeito cascata de alterações no layout através do resto de uma página.

Os elementos invalidam-se invocando o InvalidateMeasure método, geralmente quando uma propriedade do elemento é alterada que pode resultar em um novo tamanho do elemento. Esse método dispara o MeasureInvalidated evento, que o pai do elemento manipula para disparar um novo ciclo de layout.

A Layout classe define um manipulador para o MeasureInvalidated evento em cada filho adicionado à sua Content propriedade ou Children coleção e desanexa o manipulador quando o filho é removido. Portanto, cada elemento da árvore visual que tem filhos é alertado sempre que um de seus filhos muda de tamanho. O diagrama a seguir ilustra como uma alteração no tamanho de um elemento na árvore visual pode causar alterações que ondulam a árvore:

Invalidação na árvore visual

No entanto, a Layout classe tenta restringir o impacto de uma alteração no tamanho de uma criança no layout de uma página. Se o layout for restrito ao tamanho, uma alteração de tamanho filho não afetará nada maior do que o layout pai na árvore visual. No entanto, geralmente uma alteração no tamanho de um layout afeta como o layout organiza seus filhos. Portanto, qualquer alteração no tamanho de um layout iniciará um ciclo de layout para o layout, e o layout receberá chamadas para seus OnMeasure e LayoutChildren métodos.

A Layout classe também define um InvalidateLayout método que tem uma finalidade semelhante ao InvalidateMeasure método. O InvalidateLayout método deve ser chamado sempre que uma alteração for feita que afete como o layout posiciona e dimensiona seus filhos. Por exemplo, a Layout classe invoca o InvalidateLayout método sempre que um filho é adicionado ou removido de um layout.

O InvalidateLayout pode ser substituído para implementar um cache para minimizar invocações repetitivas dos Measure métodos dos filhos do layout. A substituição do InvalidateLayout método fornecerá uma notificação de quando as crianças são adicionadas ou removidas do layout. Da mesma forma, o OnChildMeasureInvalidated método pode ser substituído para fornecer uma notificação quando um dos filhos do layout muda de tamanho. Para ambas as substituições de método, um layout personalizado deve responder limpando o cache. Para obter mais informações, consulte Calcular e armazenar dados de layout em cache.

Criar um layout personalizado

O processo para criar um layout personalizado é o seguinte:

  1. Crie uma classe que derive da classe Layout<View>. Para obter mais informações, consulte Criar um WrapLayout.

  2. [opcional] Adicione propriedades, com suporte de propriedades vinculáveis, para quaisquer parâmetros que devam ser definidos na classe de layout. Para obter mais informações, consulte Adicionar propriedades com suporte de propriedades vinculáveis.

  3. Substitua o OnMeasure método para invocar o Measure método em todos os filhos do layout e retorne um tamanho solicitado para o layout. Para obter mais informações, consulte Substituir o método OnMeasure.

  4. Substitua o LayoutChildren método para chamar o Layout método em todos os filhos do layout. A falha em invocar o Layout método em cada filho em um layout resultará em que a criança nunca receberá um tamanho ou posição correta e, portanto, a criança não se tornará visível na página. Para obter mais informações, consulte Substituir o método LayoutChildren.

    Observação

    Ao enumerar filhos no OnMeasure e LayoutChildren substitui, ignore qualquer filho cuja IsVisible propriedade esteja definida como false. Isso garantirá que o layout personalizado não deixe espaço para crianças invisíveis.

  5. [opcional] Substitua o InvalidateLayout método a ser notificado quando crianças forem adicionadas ou removidas do layout. Para obter mais informações, consulte Substituir o método InvalidateLayout.

  6. [opcional] Substitua o OnChildMeasureInvalidated método a ser notificado quando um dos filhos do layout mudar de tamanho. Para obter mais informações, consulte Substituir o método OnChildMeasureInvalidated .

Observação

Observe que a OnMeasure substituição não será invocada se o tamanho do layout for governado por seu pai, em vez de seus filhos. No entanto, a substituição será invocada se uma ou ambas as restrições forem infinitas ou se a classe de layout tiver valores de propriedade ou não padrão HorizontalOptionsVerticalOptions . Por esse motivo, a LayoutChildren substituição não pode depender de tamanhos filho obtidos durante a chamada do OnMeasure método. Em vez disso, LayoutChildren deve invocar o Measure método nos filhos do layout, antes de invocar o Layout método. Como alternativa, o tamanho dos filhos obtidos na substituição pode ser armazenado em OnMeasure cache para evitar invocações posteriores Measure na LayoutChildren substituição, mas a classe de layout precisará saber quando os tamanhos precisam ser obtidos novamente. Para obter mais informações, consulte Calcular e armazenar dados de layout em cache.

A classe de layout pode ser consumida adicionando-a a um Page, e adicionando filhos ao layout. Para obter mais informações, consulte Consumir o WrapLayout.

Criar um WrapLayout

O aplicativo de exemplo demonstra uma classe sensível WrapLayout à orientação que organiza seus filhos horizontalmente na página e, em seguida, encapsula a exibição de filhos subsequentes em linhas adicionais.

A WrapLayout classe aloca a mesma quantidade de espaço para cada criança, conhecida como tamanho da célula, com base no tamanho máximo das crianças. Crianças menores que o tamanho da célula podem ser posicionadas dentro da célula com base em seus HorizontalOptions valores e VerticalOptions propriedades.

A WrapLayout definição de classe é mostrada no exemplo de código a seguir:

public class WrapLayout : Layout<View>
{
  Dictionary<Size, LayoutData> layoutDataCache = new Dictionary<Size, LayoutData>();
  ...
}

Calcular e armazenar em cache dados de layout

A LayoutData estrutura armazena dados sobre uma coleção de filhos em várias propriedades:

  • VisibleChildCount – o número de crianças que estão visíveis no layout.
  • CellSize – o tamanho máximo de todas as crianças, ajustado ao tamanho do layout.
  • Rows – o número de linhas.
  • Columns – o número de colunas.

O layoutDataCache campo é usado para armazenar vários LayoutData valores. Quando o aplicativo for iniciado, dois LayoutData objetos serão armazenados em cache no layoutDataCache dicionário para a orientação atual – um para os argumentos de restrição para a OnMeasure substituição e outro para os width argumentos e height para a LayoutChildren substituição. Ao girar o dispositivo para a orientação paisagem, a OnMeasure substituição e a substituição serão novamente invocadas, o LayoutChildren que resultará em outros dois LayoutData objetos sendo armazenados em cache no dicionário. No entanto, ao retornar o dispositivo para a orientação retrato, não são necessários cálculos adicionais porque o layoutDataCache já tem os dados necessários.

O exemplo de código a seguir mostra o GetLayoutData método, que calcula as propriedades do estruturado LayoutData com base em um tamanho específico:

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;
}

O GetLayoutData método executa as seguintes operações:

  • Ele determina se um valor calculado LayoutData já está no cache e o retorna se estiver disponível.
  • Caso contrário, ele enumera todas as crianças, invocando o Measure método em cada criança com uma largura e altura infinitas, e determina o tamanho máximo da criança.
  • Desde que haja pelo menos um filho visível, ele calcula o número de linhas e colunas necessárias e, em seguida, calcula um tamanho de célula para os filhos com base nas dimensões do WrapLayout. Observe que o tamanho da célula geralmente é um pouco maior do que o tamanho máximo da criança, mas que também pode ser menor se a WrapLayout criança não for larga o suficiente para a criança mais larga ou alta o suficiente para a criança mais alta.
  • Ele armazena o novo LayoutData valor no cache.

Adicionar propriedades com suporte de propriedades vinculáveis

A WrapLayout classe define ColumnSpacing e RowSpacing propriedades, cujos valores são usados para separar as linhas e colunas no layout e que são apoiadas por propriedades vinculáveis. As propriedades vinculáveis são mostradas no exemplo de código a seguir:

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();
  });

O manipulador de propriedade alterada de cada propriedade vinculável invoca a InvalidateLayout substituição de método para disparar uma nova passagem de layout no WrapLayout. Para obter mais informações, consulte Substituir o método InvalidateLayout e Substituir o método OnChildMeasureInvalidated .

Substituir o método OnMeasure

A OnMeasure substituição é mostrada no exemplo de código a seguir:

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);
}

A substituição invoca o GetLayoutData método e constrói um SizeRequest objeto a partir dos dados retornados, ao mesmo tempo em que leva em conta os RowSpacing valores e ColumnSpacing propriedade. Para obter mais informações sobre o método, consulte Calcular e armazenar dados de GetLayoutData layout em cache.

Importante

Os Measure métodos e OnMeasure nunca devem solicitar uma dimensão infinita retornando um SizeRequest valor com uma propriedade definida como Double.PositiveInfinity. No entanto, pelo menos um dos argumentos de restrição para OnMeasure pode ser Double.PositiveInfinity.

Substituir o método LayoutChildren

A LayoutChildren substituição é mostrada no exemplo de código a seguir:

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;
    }
  }
}

A substituição começa com uma chamada para o GetLayoutData método e, em seguida, enumera todos os filhos para dimensioná-los e posicioná-los dentro da célula de cada filho. Isso é obtido invocando o LayoutChildIntoBoundingRegion método, que é usado para posicionar uma criança dentro de um retângulo com base em seus HorizontalOptions valores e VerticalOptions propriedade. Isso equivale a fazer uma chamada para o método da Layout criança.

Observação

Observe que o retângulo passado para o LayoutChildIntoBoundingRegion método inclui toda a área em que a criança pode residir.

Para obter mais informações sobre o método, consulte Calcular e armazenar dados de GetLayoutData layout em cache.

Substituir o método InvalidateLayout

A InvalidateLayout substituição é invocada quando os filhos são adicionados ou removidos do layout, ou quando uma das propriedades altera o WrapLayout valor, conforme mostrado no exemplo de código a seguir:

protected override void InvalidateLayout()
{
  base.InvalidateLayout();
  layoutInfoCache.Clear();
}

A substituição invalida o layout e descarta todas as informações de layout armazenadas em cache.

Observação

Para interromper a Layout classe invocando o InvalidateLayout método sempre que um filho for adicionado ou removido de um layout, substitua os ShouldInvalidateOnChildAdded métodos e ShouldInvalidateOnChildRemoved e retorne false. A classe de layout pode implementar um processo personalizado quando os filhos são adicionados ou removidos.

Substitua o método OnChildMeasureInvalidated

A OnChildMeasureInvalidated substituição é invocada quando um dos filhos do layout altera de tamanho e é mostrada no exemplo de código a seguir:

protected override void OnChildMeasureInvalidated()
{
  base.OnChildMeasureInvalidated();
  layoutInfoCache.Clear();
}

A substituição invalida o layout filho e descarta todas as informações de layout armazenadas em cache.

Consumir o WrapLayout

A WrapLayout classe pode ser consumida colocando-a em um Page tipo derivado, conforme demonstrado no exemplo de código XAML a seguir:

<ContentPage ... xmlns:local="clr-namespace:ImageWrapLayout">
    <ScrollView Margin="0,20,0,20">
        <local:WrapLayout x:Name="wrapLayout" />
    </ScrollView>
</ContentPage>

O código C# equivalente é mostrado abaixo:

public class ImageWrapLayoutPageCS : ContentPage
{
  WrapLayout wrapLayout;

  public ImageWrapLayoutPageCS()
  {
    wrapLayout = new WrapLayout();

    Content = new ScrollView
    {
      Margin = new Thickness(0, 20, 0, 20),
      Content = wrapLayout
    };
  }
  ...
}

As crianças podem então ser adicionadas ao conforme WrapLayout necessário. O exemplo de código a WrapLayoutseguir mostra Image elementos que estão sendo adicionados ao :

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;
}

Quando a página que contém o WrapLayout é exibida, o aplicativo de exemplo acessa de forma assíncrona um arquivo JSON remoto que contém uma lista de fotos, cria um Image elemento para cada foto e o adiciona ao WrapLayout. Isso resulta na aparência mostrada nas capturas de tela seguir:

Exemplo de captura de tela retrato do aplicativo

As capturas de tela a seguir mostram o depois que ele foi girado para a WrapLayout orientação paisagem:

Exemplo de captura de tela do cenário do aplicativo iOSExemplo de paisagem do aplicativo Android ScreenshotCaptura de tela de cenário de aplicativo UWP de exemplo

O número de colunas em cada linha depende do tamanho da foto, da largura da tela e do número de pixels por unidade independente do dispositivo. Os Image elementos carregam as fotos de forma assíncrona e, portanto, a WrapLayout classe receberá chamadas frequentes para seu LayoutChildren método à medida que cada Image elemento recebe um novo tamanho com base na foto carregada.