If you’re like me, you probably tried to find some decent “Accordion” style view for your Xamarin.Forms app. You probably found some on NuGet that aren’t supported anymore or some old GitHub repositories. Here’s my advice – give up the search and just give in and build your own. But you don’t have to do it alone! Here is a guide to build a simple but flexible and reusable AccordionView
. If enough people like it, maybe we’ll put it up on NuGet for consumption.
Here’s the approach we are taking – We need a list of “sections”. Each Section has a header and the body content. For the sake of the simple example, we’ll do it with just a label body, but this could easily be done with a DataTemplate
or more complicated view (I use the HtmlLabel for this irl https://github.com/matteobortolazzo/HtmlLabelPlugin).
Let’s create a view called AccordionSectionView
:
AccordionSectionView.xaml
<?xml version="1.0" encoding="UTF-8"?> <ContentView xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="YourNamespace.AccordionSectionView"> <ContentView.Content> <StackLayout Orientation="Vertical" Spacing="0"> <Grid x:Name="HeaderView"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="48"/> </Grid.ColumnDefinitions> <Label x:Name="HeaderLabel" VerticalOptions="Center" Margin="16" LineBreakMode="WordWrap" /> <Label x:Name="IndicatorLabel" Style="{DynamicResource BodySecondaryBold}" FontSize="32" VerticalOptions="Center" HorizontalOptions="Center" HorizontalTextAlignment="Center" VerticalTextAlignment="Center" Margin="0,16,16,16" Grid.Column="1"/> <Grid.GestureRecognizers> <TapGestureRecognizer Tapped="Handle_Tapped"/> </Grid.GestureRecognizers> </Grid> <Grid x:Name="BodyView"> <Label x:Name="BodyLabel" Style="{DynamicResource Body}" FontSize="16" Margin="16"/> </Grid> </StackLayout> </ContentView.Content> </ContentView>
So we have a StackLayout
with a container Grid
for the header and our header has a TapGestureRecognizer
.
Now let’s look at how the code behind wires it all up.
AccordionSectionView.xaml.cs
/// <summary> /// Accordion section view. /// </summary> public partial class AccordionSectionView : ContentView { #region Bindable Properties public static BindableProperty HeaderBackgroundColorProperty = BindableProperty.Create(nameof(HeaderBackgroundColor), typeof(Color), typeof(AccordionSectionView), defaultValue: Color.Transparent, propertyChanged: (bindable, oldVal, newVal) => { ((AccordionSectionView)bindable).UpdateHeaderBackgroundColor(); }); public static BindableProperty HeaderOpenedBackgroundColorProperty = BindableProperty.Create(nameof(HeaderOpenedBackgroundColor), typeof(Color), typeof(AccordionSectionView), defaultValue: Color.Transparent, propertyChanged: (bindable, oldVal, newVal) => { ((AccordionSectionView)bindable).UpdateHeaderBackgroundColor(); }); public static BindableProperty HeaderTextColorProperty = BindableProperty.Create(nameof(HeaderTextColor), typeof(Color), typeof(AccordionSectionView), defaultValue: Color.Black, propertyChanged: (bindable, oldVal, newVal) => { ((AccordionSectionView)bindable).UpdateHeaderTextColor((Color)newVal); }); public static BindableProperty HeaderTextProperty = BindableProperty.Create(nameof(HeaderTextProperty), typeof(string), typeof(AccordionSectionView), propertyChanged: (bindable, oldVal, newVal) => { ((AccordionSectionView)bindable).UpdateHeaderText((string)newVal); }); public static BindableProperty BodyTextColorProperty = BindableProperty.Create(nameof(BodyTextColor), typeof(Color), typeof(AccordionSectionView), defaultValue: Color.Black, propertyChanged: (bindable, oldVal, newVal) => { ((AccordionSectionView)bindable).UpdateBodyTextColor((Color)newVal) }); public static BindableProperty BodyTextProperty = BindableProperty.Create(nameof(BodyText), typeof(string), typeof(AccordionSectionView), propertyChanged: (bindable, oldVal, newVal) => { ((AccordionSectionView)bindable).UpdateBodyText((string)newVal); }); public static BindableProperty IsBodyVisibleProperty = BindableProperty.Create(nameof(IsBodyVisible), typeof(bool), typeof(AccordionSectionView), defaultValue: true, propertyChanged: (bindable, oldVal, newVal) => { ((AccordionSectionView)bindable).UpdateBodyVisibility((bool)newVal); }); #endregion #region Public Properties public Color HeaderBackgroundColor { get { return (Color)GetValue(HeaderBackgroundColorProperty); } set { SetValue(HeaderBackgroundColorProperty, value); } } public Color HeaderOpenedBackgroundColor { get { return (Color)GetValue(HeaderOpenedBackgroundColorProperty); } set { SetValue(HeaderOpenedBackgroundColorProperty, value); } } public Color HeaderTextColor { get { return (Color)GetValue(HeaderTextColorProperty); } set { SetValue(HeaderTextColorProperty, value); } } public string HeaderText { get { return (string)GetValue(HeaderTextProperty); } set { SetValue(HeaderTextProperty, value); } } public Color BodyTextColor { get { return (Color)GetValue(BodyTextColorProperty); } set { SetValue(BodyTextColorProperty, value); } } public string BodyText { get { return (string)GetValue(BodyTextProperty); } set { SetValue(BodyTextProperty, value); } } public bool IsBodyVisible { get { return (bool)GetValue(IsBodyVisibleProperty); } set { SetValue(IsBodyVisibleProperty, value); } } #endregion public AccordionSectionView() { InitializeComponent(); IsBodyVisible = false; if (Resources != null) { Resources.MergedWith = typeof(PrimaryTheme); } else { Resources = new ResourceDictionary { MergedWith = typeof(PrimaryTheme) }; } } /// <summary> /// Updates the color of the header background. /// </summary> /// <param name="color">Color.</param> public void UpdateHeaderBackgroundColor(Color color) { HeaderView.BackgroundColor = color; } /// <summary> /// Updates the color of the header background. /// </summary> public void UpdateHeaderBackgroundColor() { if (IsBodyVisible) { HeaderView.BackgroundColor = HeaderOpenedBackgroundColor; BodyView.BackgroundColor = HeaderOpenedBackgroundColor; } else { HeaderView.BackgroundColor = HeaderBackgroundColor; } } /// <summary> /// Updates the color of the header text. /// </summary> /// <param name="color">Color.</param> public void UpdateHeaderTextColor(Color color) { HeaderLabel.TextColor = color; } /// <summary> /// Updates the color of the body text. /// </summary> /// <param name="color">Color.</param> public void UpdateBodyTextColor(Color color) { BodyLabel.TextColor = color; } /// <summary> /// Updates the header text. /// </summary> /// <param name="text">Text.</param> public void UpdateHeaderText(string text) { HeaderLabel.Text = text; } /// <summary> /// Updates the body text. /// </summary> /// <param name="text">Text.</param> public void UpdateBodyText(string text) { BodyLabel.Text = text; } public void UpdateBodyVisibility(bool isVisible) { BodyView.IsVisible = isVisible; IndicatorLabel.Text = "+"; if(isVisible) { IndicatorLabel.RotateTo(45, 100); } else { IndicatorLabel.RotateTo(0, 100); } } private void Handle_Tapped(object sender, System.EventArgs e) { IsBodyVisible = !IsBodyVisible; UpdateHeaderBackgroundColor(); } }
We start by wiring up some bindable properties for the look and feel of the header and body sections, and then handle animations of the IndicatorLabel
when the body is opened and closed. These even lets us update the content when the body is opened or closed.
Now we can use this in our pages. Bonus points when used with me DynamicStackLayout control to create dynamic bindable accordions!
MainPage.xaml
... <suave:DynamicStackLayout x:Name="Accordion" Orientation="Vertical" ItemsSource="{Binding Items}" Spacing="1" BackgroundColor="{DynamicResource AlternateBackground}"> <suave:DynamicStackLayout.ItemTemplate> <DataTemplate> <components:AccordionSectionView HeaderText="{Binding Title}" IsBodyVisible="False" HeaderBackgroundColor="White" HeaderOpenedBackgroundColor="{DynamicResource AlternateBackground}" HeaderTextColor="Black" BodyTextColor="Black" BodyText="{Binding HtmlContent}" /> </DataTemplate> </suave:DynamicStackLayout.ItemTemplate> </suave:DynamicStackLayout> ...
Now all together we have something pretty cool!
And that’s it! Let me know what you’ve done to build your own Accordions and collapsible views!
If you like what you see, don’t forget to follow me on twitter @Suave_Pirate, check out my GitHub, and subscribe to my blog to learn more mobile developer tips and tricks!
Interested in sponsoring developer content? Message @Suave_Pirate on twitter for details.
it gives me error on following lines. it says PrimaryTheme could not be found. what to import for this?
if (Resources != null)
{
Resources.MergedWith = typeof(PrimaryTheme);
}
else
{
Resources = new ResourceDictionary
{
MergedWith = typeof(PrimaryTheme)
};
}
LikeLike
That’s only used if you are merging your resources with a separate resource dictionary. You can remove that if you like
LikeLike
Love your work! Is it possible to make BodyText a Label type rather than a string? Thanks!
LikeLike
I have added the accordion but how do i add another tab
LikeLiked by 1 person
I have added the accordion but how do I add another tab (i haven’t used the dynamic)
LikeLike
Section i mean like you have health and Cuisine
LikeLike
If you’re using the DynamicStackLayout then just add another item to the ItemsSource. If you’re using a regular StackLayout then just add it right below.
LikeLike
When I copy and paste the stack layout version it brings up errors saying mainpage already contains bodyview, bodylabel etc and if i add a 2 on them it does not add them to an drop down
LikeLike
this is the result when I just add it below: https://imgur.com/a/ZZls1wT
LikeLike
Because of the x:Name can not be used twice
LikeLike
I’m not sure what you’re doing with your layout. It should be
ContentPage
ScrollView
StackLayout
AccordionSection
AccordionSection
AccordionSection
/StackLayout
/ScrollView
/ContentPage
It sounds like you’re putting it at the Same level with a new stack layout per section.
LikeLike
LikeLike
Sorry for the replies, This is how xaml page is looking like: https://imgur.com/a/qbcS5uz
its just bringing up errors about already contains a definition for ‘Headerview’ and so on
LikeLike
would I have to edit anything in the xaml.cs because of this?
LikeLike
Oh you didn’t make an AccordionSectionView, that’s why. You can’t have views with the same name because it creates actual properties in the code behind by that name. So take the AccordionSectionView from the post, and then use that like I mentioned in your page. You can set the custom bindable properties to whatever you want. The AccordionSectionView is what handles the custom collapsing and everything itself. You can put that AccordionSectionView wherever you want. Doesn’t have to be the DynamicStackLayout – can be a regular StackLayout, a grid, whatever.
LikeLike
When you say make a view is that just a class? as I want it the finished accordion on my MainPage.xaml
LikeLike
Yes… that’s what the entire blog post is about. There is the AccordionSectionView.xaml and AccordionSectionView.xaml.cs
LikeLike
Right ok, I made a content view called AccordionSectionView and copied it over now in my MainPage.xaml i can do the following:
ContentPage
ScrollView
StackLayout
which is working if that is correct? but it makes all the sections the same body and text:
LikeLike
ContentPage
ScrollView
StackLayout
local:AccordionSectionView /local:AccordionSectionView
local:AccordionSectionView /local:AccordionSectionView
local:AccordionSectionView /local:AccordionSectionView
/StackLayout
/ScrollView
/ContentPage
Sorry my last comment removed the code
LikeLike
Well you need to set the HeaderText property of the accordion section view… Please read the entire post and code. All of this is already explained and there are already examples in the code provided.
LikeLike
Thank you for the clarity on this, big help 🙂
LikeLike
Inside each view I have some pickers and entries with x:names, how would I pull them names over to the mainpage.xaml.cs to use on my button_clicked handler.
I am trying to save all the values to JSON.
LikeLike
Nice tuto. Have you a project about it?
LikeLike
I work well, but I can use it without suave:DynamicStackLayout ? I mean, by using listview and how?
LikeLike
What about closing a panel when another one is tapped to open?
LikeLike