Animando la cabecera de un CollectionView en Xamarin.Forms

  • tips
  • xamarin-forms

Sigo con mi saga de post sobre animaciones en Xamarin Forms. En esta ocasión quiero mostrar cómo conseguir animar la cabecera de un CollectionView, cambiando el tamaño de ésta y moviendo los elementos que contiene. De esta manera, podemos tener información detallada, o con un mayor tamaño al iniciar nuestra página y ver los primeros elementos de la colección, pero reducir su tamaño y su información a medida que se hace scroll y aparecen más elementos en pantalla, tal y como se muestra a continuación.

Tal y como he comentado en otras ocasiones, aquí os muestro solamente una manera de hacer este tipo de animaciones. No es la única y no tiene porque ser la mejor.

Lo primero que podemos pensar es utilizar el header del CollectionView para conseguir el efecto mostrado anteriormente. ERROR. Si utilizamos el header, éste se mantendrá en la parte superior de la colección, desapareciendo a medida que hacemos scroll para mostrar el resto de elementos.

La solución que os voy a dar aquí es algo más compleja. Lo primero que voy a hacer es desplazar el CollectionView para que el primer elemento aparezca justo debajo de la cabecera que fabricaremos más adelante. Para ello, sí que he utilizado el header, pero solamente para dejar un espacio en blanco. No he usado un margin, ni un padding porque quiero, que al hacer scroll, los elementos desaparezca por la parte más elevada de la pantalla. Con un margin/padding no lo conseguiría, porque el header se movería junto con el resto de elementos, por lo que conseguiría el efecto deseado.

<Grid x:Name="gridContent"
      RowDefinitions="15,100,35,35,15,*"
      RowSpacing="0" ColumnSpacing="0"
      Padding="0" Margin="0">

    <CollectionView x:Name="cvItems" Grid.RowSpan="6">
        <CollectionView.Header>
            <BoxView HeightRequest="200"
                     BackgroundColor="Transparent" />
        </CollectionView.Header>
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <Label Text="{Binding .}"
                        FontAttributes="Bold"
                        Margin="20"/>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>

</Grid>

Los RowDefinitions del código anterior los utilizaré más adelante para crear los elementos de la cabecera, así que no tienen demasiado importancia, de momento. Lo interesante en este punto es que el tamaño del header de la colección es el tamaño que va a tener la cabecera en su estado inicial, que no es otro que la suma de las filas que usare en la cabecera (15+100+35+35+15).

En este punto, ya estamos en condiciones de crear la cabecera. La primera y la última fila, con una altura de 15, solo las uso a modo de margen, por lo que el contenido estará alojado en las filas 1, 2 y 3, albergando la imagen y los dos label, respectivamente. Además, usaré un BoxView gris para dar color al fondo de la cabecera.

<BoxView x:Name="bvHeaderBackground" Grid.RowSpan="5"
         BackgroundColor="LightGray"
         HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" />

<Frame x:Name="frImage" Grid.Row="1"
       HasShadow="False"
       IsClippedToBounds="True"
       Padding="0"
       CornerRadius="50" HeightRequest="100" WidthRequest="100"
       HorizontalOptions="Center" VerticalOptions="Center">
    <Image Source="https://jorgediegocrespo.wordpress.com/wp-content/uploads/2019/11/iphonesafearea-e1574276852279.jpg"
           Aspect="AspectFill"
           HeightRequest="100" WidthRequest="100"
           HorizontalOptions="Center" VerticalOptions="Center" />
</Frame>

<Label x:Name="lbName" Grid.Row="2"
       Text="JORGE DIEGO CRESPO"
       FontAttributes="Bold" FontSize="20"
       Margin="0,10,0,0"
       VerticalTextAlignment="Center" HorizontalOptions="Center"/>

<Label x:Name="lbJob" Grid.Row="3"
       Text="Xamarin developer"
       FontSize="16" TextColor="Gray"
       Margin="0,10,0,0"
       VerticalTextAlignment="Start" HorizontalOptions="Center"/>

Ya tenemos la colección y tenemos la cabecera, solo faltaría hacer la animación, para lo que he utilizado el evento Scrolled de la colección.

...
<CollectionView x:Name="cvItems" Grid.RowSpan="6"
                Scrolled="cvItems_Scrolled">
...

Este evento nos ofrece el offset que se ha desplazado la colección, y lo podemos aprovechar para mover los elementos de la cabecera.

Lo primero que he hecho es limitar los valores del offset con los que voy a trabajar, esto son, valores positivos menores que 100. 100 porque es el desplazamiento máximo que quiero tener, ya que el tamaño mínimo de la cabecera quiero que sea 100 (200 del tamaño inicial - 100 de tamaño mínimo).

Una vez que tengo el valor del offset, puedo realizar las animaciones, que son las siguientes:

  • Escalado y translación de la imagen.
  • Translación de las dos label.
private async void cvItems_Scrolled(Object sender, ItemsViewScrolledEventArgs e)
{
    if (gridContent.Width <= 0)
        return;

    double maxOffset = 100;
    double offset = 0;
    if (e.VerticalOffset > maxOffset)
        offset = maxOffset;
    else if (e.VerticalOffset < 0)
        offset = 0;
    else
        offset = e.VerticalOffset;

    double imageXTranslation = -offset *  (gridContent.Width - 140) / (2 * maxOffset);
    double imageScale = 1 - (offset * 0.5 / maxOffset);
    double imageYTranslation = ((frImage.HeightRequest * imageScale) - frImage.HeightRequest) / 2;
    uint animationLenght = 50;

    await Task.WhenAll(frImage.TranslateTo(imageXTranslation, imageYTranslation, animationLenght),
                        frImage.ScaleTo(imageScale, animationLenght),
                        lbName.TranslateTo(offset / 2, -offset, animationLenght),
                        lbJob.TranslateTo(offset / 2, -offset, animationLenght),
                        bvHeaderBackground.TranslateTo(0, -offset, animationLenght));
}

Antes de terminar el post, quiero hacer un par de apuntes sobre la animación.

  • Para mover los elementos he tenido en cuenta el ancho del grid, así puedo pegar la imagen a la izquierda, independientemente del tamaño de la pantalla que tenga el dispositivo.
  • Las translaciones y el escalado las hago en una animación, ya que si se asignan los valores de translación y escalado directamente, se produce un efecto de flicker muy poco elegante. La duración de cada animación es de 50 ms, lo cual es suficiente para eliminar ese efecto.

Como siempre, aquí os dejo un enlace al repositorio del proyecto de ejemplo que he creado, para que podáis descargarlo y jugar con el ejemplo que he creado.