├── icon.png
├── screenshot.png
├── TestAccordion
├── App.axaml
├── MainWindow.axaml.cs
├── TestAccordion.csproj
├── App.axaml.cs
├── Program.cs
└── MainWindow.axaml
├── AvaloniaAccordion
├── AvaloniaAccordion.csproj
├── Converters.cs
├── Accordion.axaml
├── Accordion.axaml.cs
└── Accordion.properties.cs
├── AvaloniaAccordion.sln
├── Readme.md
├── icon.svg
└── LICENSE
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arklumpus/AvaloniaAccordion/HEAD/icon.png
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arklumpus/AvaloniaAccordion/HEAD/screenshot.png
--------------------------------------------------------------------------------
/TestAccordion/App.axaml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/TestAccordion/MainWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls;
3 | using Avalonia.Markup.Xaml;
4 |
5 | namespace TestAccordion
6 | {
7 | public partial class MainWindow : Window
8 | {
9 | public MainWindow()
10 | {
11 | InitializeComponent();
12 | #if DEBUG
13 | this.AttachDevTools();
14 | #endif
15 | }
16 |
17 | private void InitializeComponent()
18 | {
19 | AvaloniaXamlLoader.Load(this);
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/TestAccordion/TestAccordion.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | net5.0
5 | enable
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/TestAccordion/App.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls.ApplicationLifetimes;
3 | using Avalonia.Markup.Xaml;
4 |
5 | namespace TestAccordion
6 | {
7 | public class App : Application
8 | {
9 | public override void Initialize()
10 | {
11 | AvaloniaXamlLoader.Load(this);
12 | }
13 |
14 | public override void OnFrameworkInitializationCompleted()
15 | {
16 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
17 | {
18 | desktop.MainWindow = new MainWindow();
19 | }
20 |
21 | base.OnFrameworkInitializationCompleted();
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/TestAccordion/Program.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls;
3 | using Avalonia.Controls.ApplicationLifetimes;
4 | using System;
5 |
6 | namespace TestAccordion
7 | {
8 | class Program
9 | {
10 | // Initialization code. Don't use any Avalonia, third-party APIs or any
11 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
12 | // yet and stuff might break.
13 | public static void Main(string[] args) => BuildAvaloniaApp()
14 | .StartWithClassicDesktopLifetime(args);
15 |
16 | // Avalonia configuration, don't remove; also used by visual designer.
17 | public static AppBuilder BuildAvaloniaApp()
18 | => AppBuilder.Configure()
19 | .UsePlatformDetect()
20 | .LogToTrace();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/AvaloniaAccordion/AvaloniaAccordion.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | 1.0.0
6 |
7 | Giorgio Bianchini
8 | true
9 | University of Bristol
10 | An accordion-like control for Avalonia.
11 | GPL-3.0-only
12 | https://github.com/arklumpus/AvaloniaAccordion
13 | icon.png
14 |
15 |
16 |
17 |
18 | true
19 | .
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/AvaloniaAccordion.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.31005.135
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestAccordion", "TestAccordion\TestAccordion.csproj", "{B008BCFF-0915-4EFA-AAD6-A69F8AC4E08D}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AvaloniaAccordion", "AvaloniaAccordion\AvaloniaAccordion.csproj", "{A3FB540B-FF11-4C47-8AB6-1582FAE90A0E}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {B008BCFF-0915-4EFA-AAD6-A69F8AC4E08D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {B008BCFF-0915-4EFA-AAD6-A69F8AC4E08D}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {B008BCFF-0915-4EFA-AAD6-A69F8AC4E08D}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {B008BCFF-0915-4EFA-AAD6-A69F8AC4E08D}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {A3FB540B-FF11-4C47-8AB6-1582FAE90A0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {A3FB540B-FF11-4C47-8AB6-1582FAE90A0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {A3FB540B-FF11-4C47-8AB6-1582FAE90A0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {A3FB540B-FF11-4C47-8AB6-1582FAE90A0E}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {7C612F9E-A21E-4798-874F-4A119ADF7D5B}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/TestAccordion/MainWindow.axaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 | This is an accordion control
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | CheckBox
23 | RadioButton
24 |
25 | TextBox
26 |
27 |
28 |
29 |
30 |
31 |
32 | This is a nested accordion control
33 |
34 |
35 |
36 |
37 |
38 | CheckBox 2
39 | RadioButton 2
40 |
41 | TextBox 2
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # AvaloniaAccordion: an accordion-like expandable control for Avalonia
2 |
3 |
4 |
5 | **AvaloniaAccordion** is an "accordion" control for Avalonia. This control is similar to an `Expander`, in the sense that it can be in an "open" state, in which its contents are shown, or in a "closed" state, in which the contents are hidden and only the header is shown.
6 |
7 | The library contains a control, defined in the `AvaloniaAccordion.Accordion` class. It is released under the [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html) licence.
8 |
9 |
10 |
11 | ## Getting started
12 |
13 | The library targets .NET Standard 2.0, thus it can be used in projects that target .NET Standard 2.0+ and .NET Core 2.0+.
14 |
15 | To use the library in your project, you should install the [AvaloniaAccordion Nuget package](https://www.nuget.org/packages/AvaloniaAccordion/).
16 |
17 | This repository also contains a very simple demo project using the control.
18 |
19 | ## Usage
20 |
21 | You will need to add the relevant `using` directive (in C# code) or the XML namespace (in the XAML code). You can then add the control from the AvaloniaAccordion namespace. For example
22 |
23 | ```XAML
24 |
26 | ...
27 |
28 | ...
29 |
30 | ```
31 |
32 | Look at the source code in the [MainWindow.xaml](https://github.com/arklumpus/AvaloniaAccordion/blob/main/TestAccordion/MainWindow.axaml) file to see more examples.
33 |
34 | ### `Accordion` control
35 |
36 | The main properties of the `Accordion` control are:
37 |
38 | * The `IsOpen` property, which (unsurprisingly) is `true` when the accordion is open and `false` when it is closed. This can be manually set to open/close the accordion from code.
39 |
40 | * The `TransitionDuration` property, which determines the duration of the animations that plays when the accordion is open. Set to `0` to have the accordion open/close instantly.
41 |
42 | * The `AccordionContent` property, which is used to specify the content of the accordion. Set this to an avalonia control (e.g. a `StackPanel` or a `Grid`).
43 |
44 | * The `AccordionHeader` property, which specifies the header of the accordion. Set this to an avalonia control (e.g. a `StackPanel` or a `Grid`).
45 |
46 | * `HeaderForegeround` specifies the colour of the arrows in the header.
47 |
48 | * `HeaderBackground` specifies the background brush of the header (at rest).
49 |
50 | * `HeaderHoverBackground` specifies the background brush of the header (when the mouse is hovering it).
51 |
52 | * `HeaderPressedBackground` specifies the background colour of the header (when the user is clicking on it).
53 |
54 | * `ContentBackground` determines the background colour of the part of the accordion that contains the `AccordionContent`.
55 |
56 | * `BottomBarBrush` specifies the brush used for the bar at the bottom of the accordion.
57 |
58 | * `BottomBarThickness` specifies the thickness of the bar at the bottom of the accordion. Set to `0` to hide the bar.
59 |
60 | * The `ArrowSize` property determines the size of the arrows in the header.
61 |
62 | * The `ArrowPosition` property determines the positon of the arrows in the header (i.e. to the left, to the right, neither or both).
63 |
64 | The control also has a public `InvalidateHeight` method; this method should be called every time that the height of the content of the accordion changes, so that the accordion is notified of this and can update its height to match the content. If you are using nested `Accordion`s, each one will automatically notify its first `Accordion` ancestor; otherwise, if you are using other kinds of controls that can change size, you need to manually invoke the method whenever the height changes.
65 |
66 | ## Source code
67 |
68 | The source code for the library is available in this repository. The repository contains code for the `AvaloniaAccordion` library project and the demo application.
--------------------------------------------------------------------------------
/AvaloniaAccordion/Converters.cs:
--------------------------------------------------------------------------------
1 | /*
2 | AvaloniaAccordion - An accordion-like expandable control for Avalonia.
3 | Copyright (C) 2021 Giorgio Bianchini
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU General Public License as published by
7 | the Free Software Foundation, version 3.
8 | This program is distributed in the hope that it will be useful,
9 | but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | GNU General Public License for more details.
12 | You should have received a copy of the GNU General Public License
13 | along with this program. If not, see .
14 | */
15 |
16 | using Avalonia.Data.Converters;
17 | using System;
18 | using System.Globalization;
19 | using Avalonia.Controls;
20 |
21 | namespace AvaloniaAccordion
22 | {
23 | ///
24 | /// Converts into s.
25 | ///
26 | public class AccordionArrowPositionConverter : IValueConverter
27 | {
28 | ///
29 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
30 | {
31 | if (value is Accordion.ArrowPositions pos && parameter is string s && int.TryParse(s, out int i))
32 | {
33 | return (((int)pos) & i) != 0;
34 | }
35 | else
36 | {
37 | return false;
38 | }
39 | }
40 |
41 | ///
42 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
43 | {
44 | if (value is bool b && b)
45 | {
46 | return Accordion.ArrowPositions.Both;
47 | }
48 | else
49 | {
50 | return Accordion.ArrowPositions.Neither;
51 | }
52 | }
53 | }
54 |
55 | ///
56 | /// Converts the accordion status into an angle for the accordion arrows.
57 | ///
58 | public class AccordionAngleConverter : IValueConverter
59 | {
60 | internal bool ReturnLastValue { private get; set; } = false;
61 |
62 | private double LastValue = 0;
63 |
64 | ///
65 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
66 | {
67 | if (ReturnLastValue)
68 | {
69 | return LastValue;
70 | }
71 |
72 | if (value is bool b && b)
73 | {
74 | LastValue = 180;
75 | return 180;
76 | }
77 | else
78 | {
79 | LastValue = 0;
80 | return 0;
81 | }
82 | }
83 |
84 | ///
85 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
86 | {
87 | if (value is double d && d != 0)
88 | {
89 | return true;
90 | }
91 | else
92 | {
93 | return false;
94 | }
95 | }
96 | }
97 |
98 |
99 | ///
100 | /// Converts the accordion status into the height of the container.
101 | ///
102 | public class AccordionHeightConverter : IValueConverter
103 | {
104 | internal Grid ContentGrid { private get; set; } = null;
105 | internal bool ReturnLastValue { private get; set; } = false;
106 |
107 | private double LastValue = 0;
108 |
109 | ///
110 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
111 | {
112 | if (ReturnLastValue)
113 | {
114 | return LastValue;
115 | }
116 |
117 | if (value is bool b && b)
118 | {
119 | if (ContentGrid != null)
120 | {
121 | ContentGrid.Measure(new Avalonia.Size(ContentGrid.Bounds.Width, double.PositiveInfinity));
122 |
123 | LastValue = ContentGrid.DesiredSize.Height;
124 | return LastValue;
125 | }
126 | else
127 | {
128 | LastValue = 0;
129 | return 0;
130 | }
131 | }
132 | else
133 | {
134 | LastValue = 0;
135 | return 0;
136 | }
137 | }
138 |
139 | ///
140 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
141 | {
142 | if (value is double d && d != 0)
143 | {
144 | return true;
145 | }
146 | else
147 | {
148 | return false;
149 | }
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
124 |
--------------------------------------------------------------------------------
/AvaloniaAccordion/Accordion.axaml:
--------------------------------------------------------------------------------
1 |
15 |
16 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
32 |
33 |
36 |
37 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
62 |
63 |
64 |
65 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/AvaloniaAccordion/Accordion.axaml.cs:
--------------------------------------------------------------------------------
1 | /*
2 | AvaloniaAccordion - An accordion-like expandable control for Avalonia.
3 | Copyright (C) 2021 Giorgio Bianchini
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU General Public License as published by
7 | the Free Software Foundation, version 3.
8 | This program is distributed in the hope that it will be useful,
9 | but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | GNU General Public License for more details.
12 | You should have received a copy of the GNU General Public License
13 | along with this program. If not, see .
14 | */
15 |
16 | using Avalonia;
17 | using Avalonia.Controls;
18 | using Avalonia.Controls.Shapes;
19 | using Avalonia.Markup.Xaml;
20 | using Avalonia.Media;
21 | using Avalonia.VisualTree;
22 | using System;
23 |
24 | namespace AvaloniaAccordion
25 | {
26 | public partial class Accordion : UserControl
27 | {
28 | ///
29 | /// If the accordion is open, forces a recomputation of the height to make sure that all the contents fit.
30 | ///
31 | public void InvalidateHeight()
32 | {
33 | if (this.IsOpen)
34 | {
35 | AccordionHeightConverter.ReturnLastValue = true;
36 | AccordionAngleConverter.ReturnLastValue = true;
37 | this.IsOpen = false;
38 |
39 | _ = Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(async () =>
40 | {
41 | AccordionHeightConverter.ReturnLastValue = false;
42 | AccordionAngleConverter.ReturnLastValue = false;
43 | this.IsOpen = true;
44 |
45 | Accordion parentAccordion = this.FindAncestorOfType();
46 |
47 | if (parentAccordion != null)
48 | {
49 | await System.Threading.Tasks.Task.Delay(TimeSpan.FromMilliseconds(Math.Max(10, this.TransitionDuration.TotalMilliseconds * 2)));
50 | parentAccordion.InvalidateHeight();
51 | await System.Threading.Tasks.Task.Delay(TimeSpan.FromMilliseconds(Math.Max(10, this.TransitionDuration.TotalMilliseconds)));
52 | parentAccordion.InvalidateHeight();
53 | }
54 |
55 | }, Avalonia.Threading.DispatcherPriority.MinValue);
56 | }
57 | }
58 |
59 |
60 | private Grid ContentGridParent;
61 | private AccordionHeightConverter AccordionHeightConverter;
62 | private AccordionAngleConverter AccordionAngleConverter;
63 |
64 | ///
65 | protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
66 | {
67 | base.OnPropertyChanged(change);
68 |
69 | if (change.Property == Accordion.TransitionDurationProperty)
70 | {
71 | if (ContentGridParent != null)
72 | {
73 | ((Avalonia.Animation.DoubleTransition)ContentGridParent.Transitions[0]).Duration = change.NewValue.GetValueOrDefault();
74 |
75 | ((Avalonia.Animation.DoubleTransition)((RotateTransform)this.FindControl("ArrowPathLeft").RenderTransform).Transitions[0]).Duration = change.NewValue.GetValueOrDefault();
76 | ((Avalonia.Animation.DoubleTransition)((RotateTransform)this.FindControl("ArrowPathRight").RenderTransform).Transitions[0]).Duration = change.NewValue.GetValueOrDefault();
77 | }
78 | }
79 | else if (change.Property == Accordion.AccordionContentProperty)
80 | {
81 | this.InvalidateHeight();
82 | }
83 | else if (change.Property == Accordion.BottomBarThicknessProperty)
84 | {
85 | if (this.IsOpen)
86 | {
87 | this.FindControl