Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
<Label Text="Item 2"/>
</VerticalStackLayout>
</mct:Expander.Content>

<mct:Expander.Behaviors>
<mct:ExpanderAnimationBehavior CollapsingLength="250" ExpandingLength="250"/>
</mct:Expander.Behaviors>
</mct:Expander>

<Label Text="Multi-level expander" FontSize="24" FontAttributes="Bold"/>
Expand All @@ -44,8 +48,14 @@
<mct:Expander.Content>
<Label Text="Item 1" />
</mct:Expander.Content>
<mct:Expander.Behaviors>
<mct:ExpanderAnimationBehavior CollapsingLength="250" ExpandingLength="250"/>
</mct:Expander.Behaviors>
</mct:Expander>
</mct:Expander.Content>
<mct:Expander.Behaviors>
<mct:ExpanderAnimationBehavior CollapsingLength="250" ExpandingLength="250"/>
</mct:Expander.Behaviors>
</mct:Expander>

<Label Text="Expander in ListView" FontSize="24" FontAttributes="Bold"/>
Expand All @@ -68,6 +78,9 @@
HeightRequest="100"/>
</VerticalStackLayout>
</mct:Expander.Content>
<mct:Expander.Behaviors>
<mct:ExpanderAnimationBehavior CollapsingLength="250" ExpandingLength="250"/>
</mct:Expander.Behaviors>
</mct:Expander>
</ViewCell>
</DataTemplate>
Expand All @@ -92,6 +105,9 @@
HeightRequest="100"/>
</VerticalStackLayout>
</mct:Expander.Content>
<mct:Expander.Behaviors>
<mct:ExpanderAnimationBehavior CollapsingLength="250" ExpandingLength="250"/>
</mct:Expander.Behaviors>
</mct:Expander>
</DataTemplate>
</CollectionView.ItemTemplate>
Expand Down Expand Up @@ -121,6 +137,9 @@
HeightRequest="100"/>
</VerticalStackLayout>
</mct:Expander.Content>
<mct:Expander.Behaviors>
<mct:ExpanderAnimationBehavior CollapsingLength="250" ExpandingLength="250"/>
</mct:Expander.Behaviors>
</mct:Expander>
</DataTemplate>
</CollectionView.ItemTemplate>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System.ComponentModel;
using CommunityToolkit.Maui.Views;

namespace CommunityToolkit.Maui.Behaviors;

/// <summary>
/// The <see cref="ExpanderAnimationBehavior"/> is a behavior that animates an <see cref="Expander"/> when it expands or collapses.
/// </summary>
public partial class ExpanderAnimationBehavior : BaseBehavior<Expander>
{
/// <summary>
/// Backing BindableProperty for the <see cref="CollapsingLength"/> property.
/// </summary>
public static readonly BindableProperty CollapsingLengthProperty =
BindableProperty.Create(nameof(CollapsingLength), typeof(uint), typeof(ExpanderAnimationBehavior), 250u);

/// <summary>
/// Backing BindableProperty for the <see cref="CollapsingEasing"/> property.
/// </summary>
public static readonly BindableProperty CollapsingEasingProperty =
BindableProperty.Create(nameof(CollapsingEasing), typeof(Easing), typeof(ExpanderAnimationBehavior), Easing.Linear);

/// <summary>
/// Backing BindableProperty for the <see cref="ExpandingLength"/> property.
/// </summary>
public static readonly BindableProperty ExpandingLengthProperty =
BindableProperty.Create(nameof(ExpandingLength), typeof(uint), typeof(ExpanderAnimationBehavior), 250u);

/// <summary>
/// Backing BindableProperty for the <see cref="ExpandingEasing"/> property.
/// </summary>
public static readonly BindableProperty ExpandingEasingProperty =
BindableProperty.Create(nameof(ExpandingEasing), typeof(Easing), typeof(ExpanderAnimationBehavior), Easing.Linear);

/// <summary>
/// Length in milliseconds of the collapse animation when the <see cref="Expander"/> is collapsing.
/// </summary>
public uint CollapsingLength
{
get => (uint)GetValue(CollapsingLengthProperty);
set => SetValue(CollapsingLengthProperty, value);
}

/// <summary>
/// Easing of the <see cref="Expander"/> collapsing animation.
/// </summary>
public Easing CollapsingEasing
{
get => (Easing)GetValue(CollapsingEasingProperty);
set => SetValue(CollapsingEasingProperty, value);
}

/// <summary>
/// Length in milliseconds of the expand animation when the <see cref="Expander"/> is expanding.
/// </summary>
public uint ExpandingLength
{
get => (uint)GetValue(ExpandingLengthProperty);
set => SetValue(ExpandingLengthProperty, value);
}

/// <summary>
/// Easing of the <see cref="Expander"/> expanding animation.
/// </summary>
public Easing ExpandingEasing
{
get => (Easing)GetValue(ExpandingEasingProperty);
set => SetValue(ExpandingEasingProperty, value);
}

/// <summary>
/// Occurs when the animation for the <see cref="Expander"/> finishes collapsing.
/// </summary>
public event EventHandler? Collapsed;

/// <summary>
/// Occurs when the animation for the <see cref="Expander"/> finishes expanding.
/// </summary>
public event EventHandler? Expanded;

/// <summary>
/// Responds to the <see cref="Expander"/> property changes and triggers the expand/collapse animations.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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<bool> AnimateContentHeight(Expander expander, double fromValue, double toValue, uint length = 250, Easing? easing = null)
{
if (easing == null)
{
easing = Easing.Linear;
}
var tcs = new TaskCompletionSource<bool>();
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;
}
}
105 changes: 65 additions & 40 deletions src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Initialize a new instance of <see cref="Expander"/>.
Expand All @@ -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);
}

/// <summary>
/// Controls the visibility of the content inside the <see cref="Expander"/>.
/// </summary>
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));
}
}

/// <summary>
Expand Down Expand Up @@ -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;
}
}

Expand All @@ -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;
}
}

Expand All @@ -126,35 +154,23 @@ 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:
throw new NotSupportedException($"{nameof(ExpandDirection)} {expandDirection} is not yet supported");
}
}

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;
Expand Down Expand Up @@ -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);
Expand Down
Loading