Carrousel de juguete hecho de papel

CarouselView es un control disponible en Xamarin Forms que se utiliza para mostrar datos en forma de carrusel. Seguramente ya hayas oído hablar de él, pero a partir de Xamarin Forms 5 ya no está en preview y no es necesario que activemos su flag correspondiente para utilizarlo:

//Ya no necesitamos añadirlo :)
Forms.SetFlags("CarouselView_Experimental");

¿Y qué quiere decir esto? Pues que actualmente CarouselView es un control completamente estable y podemos empezar a utilizarlo en nuestras aplicaciones en producción con total seguridad.

Antes de CarouselView, teníamos disponible en Xamarin Forms la opción de utilizar CarouselPage para crear carruseles, con el inconveniente de que era un control más pesado y además se mostraba, como bien indica el nombre, en páginas individuales, por lo que no está destinado a usarse como un control dentro de una página. Otra opción era utilizar alguna librería externa, como CardView o CarouselView, que, aunque son potentes, siempre nos queda la duda del mantenimiento de las librerías a largo plazo. Todo esto queda solucionado con CarouselView, ya que, por una parte nos proporciona la potencia y flexibilidad que teníamos con librerías de terceros, pero asegurándonos un mantenimiento a largo plazo.

CarouselView

Después de esta breve introducción, vamos a dar un repaso a las funcionalidades básicas de CarouselView y cómo podemos utilizarlo. Vamos a partir de una página muy sencilla que contiene un carrusel al que le hemos vinculado una lista de superhéroes:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="CarouselViewSample.MainPage">
    <StackLayout>
        
        <Frame BackgroundColor="#2196F3" Padding="24" CornerRadius="0">
            <Label Text="Welcome to CarouselView example!"
                   HorizontalTextAlignment="Center" TextColor="White" FontSize="36"/>
        </Frame>

        <CarouselView ItemsSource="{Binding Superheroes}"/>

    </StackLayout>
</ContentPage>

Y nuestro modelo Superhero es el siguiente:

public class Superhero
{
    public string Name { get; set; }

    public string AlterEgo { get; set; }

    public string Photo { get; set; }

    public List<string> TeamAffiliations { get; set; }
}

Si ahora ejecutamos nuestra aplicación, veremos algo así:

Captura de pantalla Android con CarouselView pintando texto plano

¿Qué está ocurriendo? Que simplemente nos está pintando el string que se genera por cada elemento de nuestra lista de superhéroes. Vamos a darle aspecto con la propiedad ItemTemplate:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="CarouselViewSample.MainPage">
    <StackLayout>
        
        <Frame BackgroundColor="#2196F3" Padding="24" CornerRadius="0">
            <Label Text="Welcome to CarouselView example!"
                   HorizontalTextAlignment="Center" TextColor="White" FontSize="36"/>
        </Frame>

        <CarouselView ItemsSource="{Binding Superheroes}">
            <CarouselView.ItemTemplate>
                <DataTemplate>
                    <StackLayout>
                        <Frame HasShadow="True"
                               BorderColor="DarkGray"
                               CornerRadius="5"
                               Margin="20"
                               HeightRequest="300" WidthRequest="400"
                               HorizontalOptions="Center"
                               VerticalOptions="CenterAndExpand">
                            <StackLayout>
                                <Label Text="{Binding Name}"
                                       FontAttributes="Bold"
                                       FontSize="Large"
                                       HorizontalOptions="Center"
                                       VerticalOptions="Center" />

                                <Image Source="{Binding Photo}"
                                       Aspect="AspectFit"
                                       HeightRequest="150"
                                       WidthRequest="150"
                                       HorizontalOptions="Center" />

                                <Label Text="{Binding AlterEgo}"
                                       FontSize="Medium"
                                       HorizontalOptions="Center" />

                                <BoxView Color="DarkGray"
                                         HeightRequest="1"
                                         HorizontalOptions="FillAndExpand"/>

                                <StackLayout Orientation="Vertical" BindableLayout.ItemsSource="{Binding TeamAffiliations}">
                                    <Label Text="{Binding .}"
                                           FontSize="Micro"
                                           HorizontalTextAlignment="Center"/>
                                </StackLayout>
                             
                            </StackLayout>
                        </Frame>
                    </StackLayout>
                </DataTemplate>
            </CarouselView.ItemTemplate>
        </CarouselView>

    </StackLayout>
</ContentPage>

Ahora parece que empezamos a ver algo 🙂

Captura de pantalla Android con CarouselView con aspecto visual

Ahora que ya tenemos nuestro aspecto definitivo del carrusel de superhéroes, vamos a empezar a jugar un poco con las propiedades que nos proporciona.

En primer lugar vamos a mostrar unos indicadores para que el usuario sepa en qué posición del carrusel se encuentra y el número total de elementos que contiene. Esto lo vamos a conseguir con un IndicatorView.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="CarouselViewSample.MainPage">
    <StackLayout>
        
        <Frame BackgroundColor="#2196F3" Padding="24" CornerRadius="0">
            <Label Text="Welcome to CarouselView example!"
                   HorizontalTextAlignment="Center" TextColor="White" FontSize="36"/>
        </Frame>

        <CarouselView ItemsSource="{Binding Superheroes}"
                      IndicatorView="CarouselViewIndicator">
            <CarouselView.ItemTemplate>
                <DataTemplate>
                    <StackLayout>
                        <Frame HasShadow="True"
                               BorderColor="DarkGray"
                               CornerRadius="5"
                               Margin="20"
                               HeightRequest="300" WidthRequest="400"
                               HorizontalOptions="Center"
                               VerticalOptions="CenterAndExpand">
                            <StackLayout>
                                <Label Text="{Binding Name}"
                                       FontAttributes="Bold"
                                       FontSize="Large"
                                       HorizontalOptions="Center"
                                       VerticalOptions="Center" />

                                <Image Source="{Binding Photo}"
                                       Aspect="AspectFit"
                                       HeightRequest="150"
                                       WidthRequest="150"
                                       HorizontalOptions="Center" />

                                <Label Text="{Binding AlterEgo}"
                                       FontSize="Medium"
                                       HorizontalOptions="Center" />

                                <BoxView Color="DarkGray"
                                         HeightRequest="1"
                                         HorizontalOptions="FillAndExpand"/>

                                <StackLayout Orientation="Vertical" BindableLayout.ItemsSource="{Binding TeamAffiliations}">
                                    <Label Text="{Binding .}"
                                           FontSize="Micro"
                                           HorizontalTextAlignment="Center"/>
                                </StackLayout>
                             
                            </StackLayout>
                        </Frame>
                    </StackLayout>
                </DataTemplate>
            </CarouselView.ItemTemplate>
        </CarouselView>

        <IndicatorView x:Name="CarouselViewIndicator" 
                       IndicatorColor="LightGray"
                       SelectedIndicatorColor="DarkGray"
                       HorizontalOptions="Center" 
                       Margin="0,20"/>
    </StackLayout>
</ContentPage>

Creamos un IndicatorView básico y, en la propiedad IndicatorView de nuestro CarouselView, le especificamos qué control IndicatorView tiene asociado.

Captura de pantalla Android con CarouselView con indicadores de posición

En la parte inferior ahora se puede observar que nos muestra los indicadores de posición y número de elementos. En este caso nos está pintando los indicadores por defecto, pero estos indicadores se puede configurar a través la propiedad IndicatorTemplate y manejando los estados de los indicadores (estado normal o seleccionado). Si quieres ver cómo hacerlo, puedes verlo en la documentación.

El siguiente cambio que vamos a hacer en nuestro carrusel es poder ver partes de los elementos anteriores y posteriores. Esto lo hacemos de forma muy sencilla con la propiedad PeekAreaInsets.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="CarouselViewSample.MainPage">
    <StackLayout>
        
        <Frame BackgroundColor="#2196F3" Padding="24" CornerRadius="0">
            <Label Text="Welcome to CarouselView example!"
                   HorizontalTextAlignment="Center" TextColor="White" FontSize="36"/>
        </Frame>

        <CarouselView ItemsSource="{Binding Superheroes}"
                      IndicatorView="CarouselViewIndicator"
                      PeekAreaInsets="70">
            <CarouselView.ItemTemplate>
                <DataTemplate>
                    <StackLayout>
                        <Frame HasShadow="True"
                               BorderColor="DarkGray"
                               CornerRadius="5"
                               Margin="20"
                               HeightRequest="300" WidthRequest="400"
                               HorizontalOptions="Center"
                               VerticalOptions="CenterAndExpand">
                            <StackLayout>
                                <Label Text="{Binding Name}"
                                       FontAttributes="Bold"
                                       FontSize="Large"
                                       HorizontalOptions="Center"
                                       VerticalOptions="Center" />

                                <Image Source="{Binding Photo}"
                                       Aspect="AspectFit"
                                       HeightRequest="150"
                                       WidthRequest="150"
                                       HorizontalOptions="Center" />

                                <Label Text="{Binding AlterEgo}"
                                       FontSize="Medium"
                                       HorizontalOptions="Center" />

                                <BoxView Color="DarkGray"
                                         HeightRequest="1"
                                         HorizontalOptions="FillAndExpand"/>

                                <StackLayout Orientation="Vertical" BindableLayout.ItemsSource="{Binding TeamAffiliations}">
                                    <Label Text="{Binding .}"
                                           FontSize="Micro"
                                           HorizontalTextAlignment="Center"/>
                                </StackLayout>
                             
                            </StackLayout>
                        </Frame>
                    </StackLayout>
                </DataTemplate>
            </CarouselView.ItemTemplate>
        </CarouselView>

        <IndicatorView x:Name="CarouselViewIndicator" 
                       IndicatorColor="LightGray"
                       SelectedIndicatorColor="DarkGray"
                       HorizontalOptions="Center" 
                       Margin="0,20"/>
    </StackLayout>
</ContentPage>

En nuestro CarouselView hemos especificado que tenga un PeekAreaInsets de 70 y podemos observar que ahora sí vemos los elementos a los lados.

La siguiente propiedad que nos interesa es ItemsLayout. Con esta propiedad vamos a poder especificar el diseño de nuestro carrusel. En nuestro caso, queremos que nuestro carrusel sea horizontal, que siempre esté anclado algún elemento y que además, el que se ancle lo haga en el centro:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="CarouselViewSample.MainPage">
    <StackLayout>
        
        <Frame BackgroundColor="#2196F3" Padding="24" CornerRadius="0">
            <Label Text="Welcome to CarouselView example!"
                   HorizontalTextAlignment="Center" TextColor="White" FontSize="36"/>
        </Frame>

        <CarouselView ItemsSource="{Binding Superheroes}"
                      IndicatorView="CarouselViewIndicator"
                      PeekAreaInsets="70">
            <CarouselView.ItemsLayout>
                <LinearItemsLayout Orientation="Horizontal" 
                                   SnapPointsAlignment="Center" SnapPointsType="Mandatory"/>
            </CarouselView.ItemsLayout>
            <CarouselView.ItemTemplate>
                <DataTemplate>
                    <StackLayout>
                        <Frame HasShadow="True"
                               BorderColor="DarkGray"
                               CornerRadius="5"
                               Margin="20"
                               HeightRequest="300" WidthRequest="400"
                               HorizontalOptions="Center"
                               VerticalOptions="CenterAndExpand">
                            <StackLayout>
                                <Label Text="{Binding Name}"
                                       FontAttributes="Bold"
                                       FontSize="Large"
                                       HorizontalOptions="Center"
                                       VerticalOptions="Center" />

                                <Image Source="{Binding Photo}"
                                       Aspect="AspectFit"
                                       HeightRequest="150"
                                       WidthRequest="150"
                                       HorizontalOptions="Center" />

                                <Label Text="{Binding AlterEgo}"
                                       FontSize="Medium"
                                       HorizontalOptions="Center" />

                                <BoxView Color="DarkGray"
                                         HeightRequest="1"
                                         HorizontalOptions="FillAndExpand"/>

                                <StackLayout Orientation="Vertical" BindableLayout.ItemsSource="{Binding TeamAffiliations}">
                                    <Label Text="{Binding .}"
                                           FontSize="Micro"
                                           HorizontalTextAlignment="Center"/>
                                </StackLayout>
                             
                            </StackLayout>
                        </Frame>
                    </StackLayout>
                </DataTemplate>
            </CarouselView.ItemTemplate>
        </CarouselView>

        <IndicatorView x:Name="CarouselViewIndicator" 
                       IndicatorColor="LightGray"
                       SelectedIndicatorColor="DarkGray"
                       HorizontalOptions="Center" 
                       Margin="0,20"/>
    </StackLayout>
</ContentPage>

Para entender un poco qué es lo que hemos configurado, vamos a ver el siguiente gif.

Se puede apreciar que, al finalizar el gesto, siempre se ancla un elemento y además se ancla en el centro.

Por último, de la misma forma que otros controles (Entry, CollectionView, etc.), el control CarouselView proporciona diferentes estados a través de VisualStateManager. En concreto tenemos disponibles 4 estados, CurrentItem, PreviousItem, NextItem y DefaultItem.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="CarouselViewSample.MainPage">
    <StackLayout>
        
        <Frame BackgroundColor="#2196F3" Padding="24" CornerRadius="0">
            <Label Text="Welcome to CarouselView example!"
                   HorizontalTextAlignment="Center" TextColor="White" FontSize="36"/>
        </Frame>

        <CarouselView ItemsSource="{Binding Superheroes}"
                      IndicatorView="CarouselViewIndicator"
                      PeekAreaInsets="70">
            <CarouselView.ItemsLayout>
                <LinearItemsLayout Orientation="Horizontal" 
                                   SnapPointsAlignment="Center" SnapPointsType="Mandatory"/>
            </CarouselView.ItemsLayout>
            <CarouselView.ItemTemplate>
                <DataTemplate>
                    <StackLayout>
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualState x:Name="CurrentItem">
                                    <VisualState.Setters>
                                        <Setter Property="TranslationY" Value="-10" />
                                    </VisualState.Setters>
                                </VisualState>
                                <VisualState x:Name="PreviousItem">
                                    <VisualState.Setters>
                                        <Setter Property="Opacity" Value="0.25" />
                                    </VisualState.Setters>
                                </VisualState>
                                <VisualState x:Name="NextItem">
                                    <VisualState.Setters>
                                        <Setter Property="Opacity" Value="0.25" />
                                    </VisualState.Setters>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Frame HasShadow="True"
                               BorderColor="DarkGray"
                               CornerRadius="5"
                               Margin="20"
                               HeightRequest="300" WidthRequest="400"
                               HorizontalOptions="Center"
                               VerticalOptions="CenterAndExpand">
                            <StackLayout>
                                <Label Text="{Binding Name}"
                                       FontAttributes="Bold"
                                       FontSize="Large"
                                       HorizontalOptions="Center"
                                       VerticalOptions="Center" />

                                <Image Source="{Binding Photo}"
                                       Aspect="AspectFit"
                                       HeightRequest="150"
                                       WidthRequest="150"
                                       HorizontalOptions="Center" />

                                <Label Text="{Binding AlterEgo}"
                                       FontSize="Medium"
                                       HorizontalOptions="Center" />

                                <BoxView Color="DarkGray"
                                         HeightRequest="1"
                                         HorizontalOptions="FillAndExpand"/>

                                <StackLayout Orientation="Vertical" BindableLayout.ItemsSource="{Binding TeamAffiliations}">
                                    <Label Text="{Binding .}"
                                           FontSize="Micro"
                                           HorizontalTextAlignment="Center"/>
                                </StackLayout>
                             
                            </StackLayout>
                        </Frame>
                    </StackLayout>
                </DataTemplate>
            </CarouselView.ItemTemplate>
        </CarouselView>

        <IndicatorView x:Name="CarouselViewIndicator" 
                       IndicatorColor="LightGray"
                       SelectedIndicatorColor="DarkGray"
                       HorizontalOptions="Center" 
                       Margin="0,20"/>
    </StackLayout>
</ContentPage>

En nuestro caso hemos utilizado 3 de los estados disponibles, indicando que, para el elemento actual se traslade 10 píxeles hacia arriba, y los elementos anteriores y posteriores tengan un 25% de la opacidad.

uales

Si viésemos el carrusel en movimiento es cierto que el efecto quizá sea un poco brusco, ya que no hay interpolación entre los estados, es decir, que un elemento pasa, por ejemplo, de traslación 0 a traslación -10 sin valores intermedios. Esto lo podríamos solucionar con el evento Scrolled del CarrouselView, es más, puedes ver cómo lo hemos resuelto nosotros en nuestra aplicación (aquí el código específico del carrusel).

Conclusiones

Después de este breve repaso por las propiedades básicas de CarouselView, podemos ver que nos proporciona mucha potencia y flexibilidad. Un ejemplo claro de la flexibilidad de CarouselView la podemos ver en Xamarin Community Toolkit, donde han desarrollado un control basado en pestañas (TabView) cuyo element principal es un CarouselView.

Además de todo lo que hemos visto, hay un montón de cosas más que vas a poder hacer, como seleccionar en runtime el template que se va a pintar para cada elemento del carrusel , incluirlo dentro de un RefreshView para poder hacer pull to refresh, utilizar sus eventos y comandos para realizar acciones y mucho más.

Beatríz Márquez Heredia

Beatríz Márquez Heredia

CEO en DevsDNA

Leave a Reply