diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/Expander/ExpanderPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/Expander/ExpanderPage.xaml
index 15ddcb5973..e9bbc597c0 100644
--- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/Expander/ExpanderPage.xaml
+++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/Expander/ExpanderPage.xaml
@@ -28,6 +28,10 @@
+
+
+
+
@@ -44,8 +48,14 @@
+
+
+
+
+
+
@@ -68,6 +78,9 @@
HeightRequest="100"/>
+
+
+
@@ -92,6 +105,9 @@
HeightRequest="100"/>
+
+
+
@@ -121,6 +137,9 @@
HeightRequest="100"/>
+
+
+
diff --git a/src/CommunityToolkit.Maui/Behaviors/ExpanderAnimationBehavior.shared.cs b/src/CommunityToolkit.Maui/Behaviors/ExpanderAnimationBehavior.shared.cs
new file mode 100644
index 0000000000..3ec4ae136b
--- /dev/null
+++ b/src/CommunityToolkit.Maui/Behaviors/ExpanderAnimationBehavior.shared.cs
@@ -0,0 +1,125 @@
+using System.ComponentModel;
+using CommunityToolkit.Maui.Views;
+
+namespace CommunityToolkit.Maui.Behaviors;
+
+///
+/// The is a behavior that animates an when it expands or collapses.
+///
+public partial class ExpanderAnimationBehavior : BaseBehavior
+{
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ public static readonly BindableProperty CollapsingLengthProperty =
+ BindableProperty.Create(nameof(CollapsingLength), typeof(uint), typeof(ExpanderAnimationBehavior), 250u);
+
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ public static readonly BindableProperty CollapsingEasingProperty =
+ BindableProperty.Create(nameof(CollapsingEasing), typeof(Easing), typeof(ExpanderAnimationBehavior), Easing.Linear);
+
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ public static readonly BindableProperty ExpandingLengthProperty =
+ BindableProperty.Create(nameof(ExpandingLength), typeof(uint), typeof(ExpanderAnimationBehavior), 250u);
+
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ public static readonly BindableProperty ExpandingEasingProperty =
+ BindableProperty.Create(nameof(ExpandingEasing), typeof(Easing), typeof(ExpanderAnimationBehavior), Easing.Linear);
+
+ ///
+ /// Length in milliseconds of the collapse animation when the is collapsing.
+ ///
+ public uint CollapsingLength
+ {
+ get => (uint)GetValue(CollapsingLengthProperty);
+ set => SetValue(CollapsingLengthProperty, value);
+ }
+
+ ///
+ /// Easing of the collapsing animation.
+ ///
+ public Easing CollapsingEasing
+ {
+ get => (Easing)GetValue(CollapsingEasingProperty);
+ set => SetValue(CollapsingEasingProperty, value);
+ }
+
+ ///
+ /// Length in milliseconds of the expand animation when the is expanding.
+ ///
+ public uint ExpandingLength
+ {
+ get => (uint)GetValue(ExpandingLengthProperty);
+ set => SetValue(ExpandingLengthProperty, value);
+ }
+
+ ///
+ /// Easing of the expanding animation.
+ ///
+ public Easing ExpandingEasing
+ {
+ get => (Easing)GetValue(ExpandingEasingProperty);
+ set => SetValue(ExpandingEasingProperty, value);
+ }
+
+ ///
+ /// Occurs when the animation for the finishes collapsing.
+ ///
+ public event EventHandler? Collapsed;
+
+ ///
+ /// Occurs when the animation for the finishes expanding.
+ ///
+ public event EventHandler? Expanded;
+
+ ///
+ /// Responds to the property changes and triggers the expand/collapse animations.
+ ///
+ ///
+ ///
+ protected override void OnViewPropertyChanged(Expander sender, PropertyChangedEventArgs e)
+ {
+ base.OnViewPropertyChanged(sender, e);
+
+ switch (e.PropertyName)
+ {
+ case nameof(Expander.IsExpanded):
+ if (sender.IsExpanded)
+ {
+ sender.Dispatcher.Dispatch(async () =>
+ {
+ await AnimateContentHeight(sender, 1.0, sender.BodyContentView.Height, ExpandingLength, ExpandingEasing);
+ Expanded?.Invoke(sender, EventArgs.Empty);
+ });
+ }
+ else
+ {
+ sender.Dispatcher.Dispatch(async () =>
+ {
+ await AnimateContentHeight(sender, sender.BodyContentView.Height, 1.0, CollapsingLength, CollapsingEasing);
+ Collapsed?.Invoke(sender, EventArgs.Empty);
+ });
+ }
+ break;
+ }
+ }
+
+ Task AnimateContentHeight(Expander expander, double fromValue, double toValue, uint length = 250, Easing? easing = null)
+ {
+ if (easing == null)
+ {
+ easing = Easing.Linear;
+ }
+ var tcs = new TaskCompletionSource();
+ expander.ContentHeight = fromValue;
+ var animation = new Animation(v => expander.ContentHeight = v, fromValue, toValue, easing);
+ animation.Commit(expander, nameof(AnimateContentHeight), 16, length, finished: (f, a) => tcs.SetResult(a));
+ return tcs.Task;
+ }
+}
diff --git a/src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs b/src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs
index cad239f1e3..dc07d99e96 100644
--- a/src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs
+++ b/src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs
@@ -23,6 +23,10 @@ public static readonly BindableProperty DirectionProperty
= BindableProperty.Create(nameof(Direction), typeof(ExpandDirection), typeof(Expander), ExpandDirection.Down, propertyChanged: OnDirectionPropertyChanged);
readonly WeakEventManager tappedEventManager = new();
+ readonly Grid contentGrid;
+ readonly ContentView headerContentView;
+ readonly VerticalStackLayout bodyLayout;
+ internal ContentView BodyContentView;
///
/// Initialize a new instance of .
@@ -32,14 +36,59 @@ public Expander()
HandleHeaderTapped = ResizeExpanderInItemsView;
HeaderTapGestureRecognizer.Tapped += OnHeaderTapGestureRecognizerTapped;
- base.Content = new Grid
+ base.Content = contentGrid = new Grid
{
RowDefinitions =
{
new RowDefinition(GridLength.Auto),
new RowDefinition(GridLength.Auto)
+ },
+ Children =
+ {
+ (headerContentView = new ContentView()),
+ (bodyLayout = new VerticalStackLayout()
+ {
+ HeightRequest = 1,
+ Padding = new Thickness(0, 1, 0, 0),
+ Children = { (BodyContentView = new ContentView()) }
+ })
}
};
+
+ contentGrid.SetRow(headerContentView, 0);
+ contentGrid.SetRow(bodyLayout, 1);
+
+ #region Special case for bubbling height from nested Expanders
+ BodyContentView.PropertyChanged += (s, e) =>
+ {
+ if (e.PropertyName == nameof(Height))
+ {
+ if (IsExpanded)
+ {
+ ContentHeight = BodyContentView.Height;
+ }
+ }
+ };
+ #endregion
+
+ headerContentView.GestureRecognizers.Add(HeaderTapGestureRecognizer);
+ }
+
+ ///
+ /// Controls the visibility of the content inside the .
+ ///
+ public double ContentHeight
+ {
+ get => bodyLayout.HeightRequest;
+ set
+ {
+ double newHeight = Math.Max(Math.Min(value, BodyContentView.Height + 1.0), 1.0);
+ if (bodyLayout.Height != newHeight)
+ {
+ bodyLayout.HeightRequest = newHeight;
+ }
+ OnPropertyChanged(nameof(ContentHeight));
+ }
}
///
@@ -77,23 +126,12 @@ public ExpandDirection Direction
}
}
- Grid ContentGrid => (Grid)base.Content;
-
static void OnContentPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var expander = (Expander)bindable;
if (newValue is View view)
{
- view.SetBinding(IsVisibleProperty, new Binding(nameof(IsExpanded), source: expander));
-
- expander.ContentGrid.Remove(oldValue);
- expander.ContentGrid.Add(newValue);
- expander.ContentGrid.SetRow(view, expander.Direction switch
- {
- ExpandDirection.Down => 1,
- ExpandDirection.Up => 0,
- _ => throw new NotSupportedException($"{nameof(ExpandDirection)} {expander.Direction} is not yet supported")
- });
+ expander.BodyContentView.Content = view;
}
}
@@ -102,17 +140,7 @@ static void OnHeaderPropertyChanged(BindableObject bindable, object oldValue, ob
var expander = (Expander)bindable;
if (newValue is View view)
{
- expander.SetHeaderGestures(view);
-
- expander.ContentGrid.Remove(oldValue);
- expander.ContentGrid.Add(newValue);
-
- expander.ContentGrid.SetRow(view, expander.Direction switch
- {
- ExpandDirection.Down => 0,
- ExpandDirection.Up => 1,
- _ => throw new NotSupportedException($"{nameof(ExpandDirection)} {expander.Direction} is not yet supported")
- });
+ expander.headerContentView.Content = view;
}
}
@@ -126,21 +154,16 @@ static void OnDirectionPropertyChanged(BindableObject bindable, object oldValue,
void HandleDirectionChanged(ExpandDirection expandDirection)
{
- if (Header is null || Content is null)
- {
- return;
- }
-
switch (expandDirection)
{
case ExpandDirection.Down:
- ContentGrid.SetRow(Header, 0);
- ContentGrid.SetRow(Content, 1);
+ contentGrid.SetRow(headerContentView, 0);
+ contentGrid.SetRow(bodyLayout, 1);
break;
case ExpandDirection.Up:
- ContentGrid.SetRow(Header, 1);
- ContentGrid.SetRow(Content, 0);
+ contentGrid.SetRow(headerContentView, 1);
+ contentGrid.SetRow(bodyLayout, 0);
break;
default:
@@ -148,13 +171,6 @@ void HandleDirectionChanged(ExpandDirection expandDirection)
}
}
- void SetHeaderGestures(in IView header)
- {
- var headerView = (View)header;
- headerView.GestureRecognizers.Remove(HeaderTapGestureRecognizer);
- headerView.GestureRecognizers.Add(HeaderTapGestureRecognizer);
- }
-
void OnHeaderTapGestureRecognizerTapped(object? sender, TappedEventArgs tappedEventArgs)
{
IsExpanded = !IsExpanded;
@@ -201,6 +217,15 @@ void ResizeExpanderInItemsView(TappedEventArgs tappedEventArgs)
void IExpander.ExpandedChanged(bool isExpanded)
{
+ if (isExpanded)
+ {
+ ContentHeight = BodyContentView.Height + 1.0;
+ }
+ else
+ {
+ ContentHeight = 1.0;
+ }
+
if (Command?.CanExecute(CommandParameter) is true)
{
Command.Execute(CommandParameter);