Partilhar 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, encapsula a exibição de filhos subsequentes em linhas adicionais.

Em 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 tamanho solicitado . Page, Layoute Layout<View> os tipos derivados são responsáveis por determinar a localização e o tamanho de seu filho, ou filhos, em relação a si mesmos. Portanto, o layout envolve uma relação pai-filho, em que o pai determina qual deve ser o tamanho de seus filhos, mas tentará acomodar o tamanho solicitado do filho.

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 discutidos agora.

Layout

O layout começa na parte superior da árvore visual com uma página e prossegue por todas as ramificações 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 Layout , chamadas, é iniciado 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 forem medidos, cada elemento pai será 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 and 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 mudar 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 propriedade ou tem um Children método substituível LayoutChildren . As classes de layout personalizado derivadas de Layout<View> devem substituir esse método e garantir que os Measure métodos and Layout sejam chamados em todos os filhos do elemento, para fornecer o layout personalizado desejado.

Além disso, cada classe que deriva ou Layout Layout<View> deve substituir o OnMeasure método, que é onde uma classe de layout determina o tamanho que ela precisa ter 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 é restrito, ou totalmente restrito, 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 restrito, quando recebe uma chamada para seu Measure método com pelo menos um argumento igual a Double.PositiveInfinity – a restrição infinita pode ser considerada como indicando autossínculo.

Invalidação

A invalidação é o processo pelo qual uma alteração em um elemento em uma página aciona um novo ciclo de layout. Os elementos são considerados inválidos quando não têm mais o tamanho ou a posição corretos. Por exemplo, se a FontSize propriedade de um Button for alterada, o Button será considerado inválido porque não terá mais o tamanho correto. Redimensionar o Button pode ter um efeito cascata de mudanças no layout no restante de uma página.

Os elementos se invalidam invocando o InvalidateMeasure método, geralmente quando uma propriedade do elemento é alterada, o que pode resultar em um novo tamanho do elemento. Esse método aciona o MeasureInvalidated evento, que o pai do elemento manipula para acionar 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 na á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 se propagam na á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 tiver restrição de 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 a forma 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 métodos and LayoutChildren .

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 invocado sempre que for feita uma alteração que afete a forma 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. Substituir o InvalidateLayout método fornecerá uma notificação de quando os filhos forem adicionados ou removidos 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 em cache dados de layout.

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, apoiadas por propriedades associáveis, para quaisquer parâmetros que devam ser definidos na classe de layout. Para obter mais informações, consulte Adicionar propriedades apoiadas por propriedades associá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 chamá-lo Layout em todos os filhos do layout. A falha em invocar o Layout método em cada filho em um layout resultará no filho nunca recebendo um tamanho ou posição corretos e, portanto, o filho não ficará visível na página. Para obter mais informações, consulte Substituir o método LayoutChildren.

    Observação

    Ao enumerar filhos nas OnMeasure substituições e LayoutChildren , 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 as 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 OnChildMeasureInvalided.

Observação

Observe que a OnMeasure substituição não será invocada se o tamanho do layout for regido 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 não padrão HorizontalOptions ou VerticalOptions de propriedade. Por esse motivo, a LayoutChildren substituição não pode depender de tamanhos filho obtidos durante a chamada de 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 em cache dados de layout.

A classe de layout pode ser consumida adicionando-a a um Pagee 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 filho, conhecida como tamanho da célula, com base no tamanho máximo dos filhos. Crianças menores que o tamanho da célula podem ser posicionadas dentro da célula com base em seus HorizontalOptions valores de propriedade e VerticalOptions .

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

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 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 LayoutChildren substituição serão invocadas novamente, o 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, nenhum cálculo adicional é necessário porque o já layoutDataCache possui os dados necessários.

O exemplo de código a seguir mostra o GetLayoutData método, que calcula LayoutData as propriedades da estrutura 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 todos os filhos, invocando o Measure método em cada filho com largura e altura infinitas e determina o tamanho máximo do filho.
  • 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 também pode ser menor se não WrapLayout for largo o suficiente para a criança mais larga ou alto o suficiente para a criança mais alta.
  • Ele armazena o novo LayoutData valor no cache.

Adicionar propriedades apoiadas por propriedades associá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 associáveis. As propriedades associáveis são mostradas no seguinte exemplo de código:

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 associável invoca a substituição de InvalidateLayout 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 seguinte exemplo de código:

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 valores de RowSpacing propriedade and ColumnSpacing . Para obter mais informações sobre o método, consulte Calcular e armazenar em cache dados de GetLayoutData layout.

Importante

Os Measure métodos and 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 seguinte exemplo de código:

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 um filho dentro de um retângulo com base em seus HorizontalOptions valores de propriedade e VerticalOptions . Isso é equivalente 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 em cache dados de GetLayoutData layout.

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 impedir que a Layout classe invoque o InvalidateLayout método sempre que um filho for adicionado ou removido de um layout, substitua os ShouldInvalidateOnChildAdded métodos and ShouldInvalidateOnChildRemoved e retorne false. A classe de layout pode implementar um processo personalizado quando os filhos são adicionados ou removidos.

Substituir o método OnChildMeasureInvalidated

A OnChildMeasureInvalidated substituição é invocada quando um dos filhos do layout muda 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 WrapLayout conforme necessário. O exemplo de código a seguir mostra Image elementos sendo adicionados ao 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;
}

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:

Capturas de tela de retrato de aplicativo de amostra

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

Exemplo de captura de tela do cenário de aplicativos iOSExemplo de captura de tela do Android Application LandscapeExemplo de captura de tela do cenário de aplicativos UWP

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 classe receberá chamadas frequentes para seu LayoutChildren método à medida que WrapLayout cada Image elemento recebe um novo tamanho com base na foto carregada.