Skip to content

Feature/implicit animation binding #3843

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
38 changes: 19 additions & 19 deletions Microsoft.Toolkit.Uwp.UI.Animations/Implicit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Windows.Foundation.Collections;
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Hosting;

Expand Down Expand Up @@ -130,30 +130,30 @@ public static void SetAnimations(UIElement element, ImplicitAnimationSet value)
/// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance for the current event.</param>
private static void OnShowAnimationsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
static void OnAnimationsChanged(IObservableVector<DependencyObject> sender, IVectorChangedEventArgs args)
static void OnAnimationsChanged(object sender, EventArgs e)
{
var collection = (ImplicitAnimationSet)sender;

if (collection.ParentReference!.TryGetTarget(out UIElement element))
{
ElementCompositionPreview.SetImplicitShowAnimation(element, collection.GetCompositionAnimationGroup());
ElementCompositionPreview.SetImplicitShowAnimation(element, collection.GetCompositionAnimationGroup(element));
}
}

if (e.OldValue is ImplicitAnimationSet oldCollection)
{
oldCollection.VectorChanged -= OnAnimationsChanged;
oldCollection.AnimationsChanged -= OnAnimationsChanged;
}

if (d is UIElement element &&
e.NewValue is ImplicitAnimationSet collection)
{
collection.ParentReference = new(element);
collection.VectorChanged -= OnAnimationsChanged;
collection.VectorChanged += OnAnimationsChanged;
collection.AnimationsChanged -= OnAnimationsChanged;
collection.AnimationsChanged += OnAnimationsChanged;

ElementCompositionPreview.SetIsTranslationEnabled(element, true);
ElementCompositionPreview.SetImplicitShowAnimation(element, collection.GetCompositionAnimationGroup());
ElementCompositionPreview.SetImplicitShowAnimation(element, collection.GetCompositionAnimationGroup(element));
}
}

Expand All @@ -164,30 +164,30 @@ static void OnAnimationsChanged(IObservableVector<DependencyObject> sender, IVec
/// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance for the current event.</param>
private static void OnHideAnimationsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
static void OnAnimationsChanged(IObservableVector<DependencyObject> sender, IVectorChangedEventArgs args)
static void OnAnimationsChanged(object sender, EventArgs e)
{
var collection = (ImplicitAnimationSet)sender;

if (collection.ParentReference!.TryGetTarget(out UIElement element))
{
ElementCompositionPreview.SetImplicitHideAnimation(element, collection.GetCompositionAnimationGroup());
ElementCompositionPreview.SetImplicitHideAnimation(element, collection.GetCompositionAnimationGroup(element));
}
}

if (e.OldValue is ImplicitAnimationSet oldCollection)
{
oldCollection.VectorChanged -= OnAnimationsChanged;
oldCollection.AnimationsChanged -= OnAnimationsChanged;
}

if (d is UIElement element &&
e.NewValue is ImplicitAnimationSet collection)
{
collection.ParentReference = new(element);
collection.VectorChanged -= OnAnimationsChanged;
collection.VectorChanged += OnAnimationsChanged;
collection.AnimationsChanged -= OnAnimationsChanged;
collection.AnimationsChanged += OnAnimationsChanged;

ElementCompositionPreview.SetIsTranslationEnabled(element, true);
ElementCompositionPreview.SetImplicitHideAnimation(element, collection.GetCompositionAnimationGroup());
ElementCompositionPreview.SetImplicitHideAnimation(element, collection.GetCompositionAnimationGroup(element));
}
}

Expand All @@ -198,30 +198,30 @@ static void OnAnimationsChanged(IObservableVector<DependencyObject> sender, IVec
/// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance for the current event.</param>
private static void OnAnimationsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
static void OnAnimationsChanged(IObservableVector<DependencyObject> sender, IVectorChangedEventArgs args)
static void OnAnimationsChanged(object sender, EventArgs e)
{
var collection = (ImplicitAnimationSet)sender;

if (collection.ParentReference!.TryGetTarget(out UIElement element))
{
ElementCompositionPreview.GetElementVisual(element).ImplicitAnimations = collection.GetImplicitAnimationCollection();
ElementCompositionPreview.GetElementVisual(element).ImplicitAnimations = collection.GetImplicitAnimationCollection(element);
}
}

if (e.OldValue is ImplicitAnimationSet oldCollection)
{
oldCollection.VectorChanged -= OnAnimationsChanged;
oldCollection.AnimationsChanged -= OnAnimationsChanged;
}

if (d is UIElement element &&
e.NewValue is ImplicitAnimationSet collection)
{
collection.ParentReference = new(element);
collection.VectorChanged -= OnAnimationsChanged;
collection.VectorChanged += OnAnimationsChanged;
collection.AnimationsChanged -= OnAnimationsChanged;
collection.AnimationsChanged += OnAnimationsChanged;

ElementCompositionPreview.SetIsTranslationEnabled(element, true);
ElementCompositionPreview.GetElementVisual(element).ImplicitAnimations = collection.GetImplicitAnimationCollection();
ElementCompositionPreview.GetElementVisual(element).ImplicitAnimations = collection.GetImplicitAnimationCollection(element);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,14 @@ public EasingMode? EasingMode
/// </summary>
public RepeatOption Repeat
{
get => (RepeatOption)GetValue(RepeatOptionProperty);
set => SetValue(RepeatOptionProperty, value);
get => (RepeatOption)GetValue(RepeatProperty);
set => SetValue(RepeatProperty, value);
}

/// <summary>
/// Identifies the <seealso cref="Repeat"/> dependency property.
/// </summary>
public static readonly DependencyProperty RepeatOptionProperty = DependencyProperty.Register(
public static readonly DependencyProperty RepeatProperty = DependencyProperty.Register(
nameof(Repeat),
typeof(RepeatOption),
typeof(Animation),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#nullable enable

using System;
using Windows.UI.Composition;
using Windows.UI.Xaml;
using static Microsoft.Toolkit.Uwp.UI.Animations.AnimationExtensions;
Expand All @@ -17,6 +18,25 @@ namespace Microsoft.Toolkit.Uwp.UI.Animations
public abstract class ImplicitAnimation<TValue, TKeyFrame> : Animation<TValue, TKeyFrame>, IImplicitTimeline
where TKeyFrame : unmanaged
{
/// <inheritdoc/>
public event EventHandler? AnimationPropertyChanged;

/// <summary>
/// Initializes a new instance of the <see cref="ImplicitAnimation{TValue, TKeyFrame}"/> class.
/// </summary>
protected ImplicitAnimation()
{
RegisterPropertyChangedCallback(DelayProperty, RaiseAnimationPropertyChanged);
RegisterPropertyChangedCallback(DurationProperty, RaiseAnimationPropertyChanged);
RegisterPropertyChangedCallback(EasingTypeProperty, RaiseAnimationPropertyChanged);
RegisterPropertyChangedCallback(EasingModeProperty, RaiseAnimationPropertyChanged);
RegisterPropertyChangedCallback(RepeatProperty, RaiseAnimationPropertyChanged);
RegisterPropertyChangedCallback(DelayBehaviorProperty, RaiseAnimationPropertyChanged);
RegisterPropertyChangedCallback(ToProperty, RaiseAnimationPropertyChanged);
RegisterPropertyChangedCallback(FromProperty, RaiseAnimationPropertyChanged);
RegisterPropertyChangedCallback(KeyFramesProperty, RaiseAnimationPropertyChanged);
}

/// <summary>
/// Gets or sets the optional implicit target for the animation. This can act as a trigger property for the animation.
/// </summary>
Expand Down Expand Up @@ -67,5 +87,15 @@ public CompositionAnimation GetAnimation(UIElement element, out string? target)

return builder.GetAnimation(element.GetVisual(), out _);
}

/// <summary>
/// Raises the <see cref="AnimationPropertyChanged"/> event.
/// </summary>
/// <param name="sender">The instance raising the event.</param>
/// <param name="property">The <see cref="DependencyProperty"/> that was changed.</param>
private void RaiseAnimationPropertyChanged(DependencyObject sender, DependencyProperty property)
{
AnimationPropertyChanged?.Invoke(this, EventArgs.Empty);
}
}
}
95 changes: 69 additions & 26 deletions Microsoft.Toolkit.Uwp.UI.Animations/Xaml/ImplicitAnimationSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,81 @@

using System;
using System.Diagnostics.Contracts;
using Windows.Foundation.Collections;
using Windows.UI.Composition;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Hosting;

namespace Microsoft.Toolkit.Uwp.UI.Animations
{
/// <summary>
/// A collection of implicit animations that can be grouped together. This type represents a composite animation
/// (such as <see cref="Windows.UI.Xaml.Media.Animation.Storyboard"/>) that is executed on a given element.
/// A collection of implicit animations that can be assigned to a <see cref="UIElement"/> and configured to be run automatically
/// when the element is either shown or hidden (through <see cref="Implicit.ShowAnimationsProperty"/> and <see cref="Implicit.HideAnimationsProperty"/>),
/// or whenever one of the targeted properties on the underlying <see cref="Visual"/> element changes (through <see cref="Implicit.AnimationsProperty"/>).
/// <para>
/// Animations within an <see cref="ImplicitAnimationSet"/> should be objects implementing the <see cref="IImplicitTimeline"/> interface, such as
/// types inheriting from <see cref="ImplicitAnimation{TValue, TKeyFrame}"/> (eg. <see cref="OpacityAnimation"/>, <see cref="TranslationAnimation"/>,
/// <see cref="OffsetAnimation"/> and <see cref="ScaleAnimation"/>, or custom ones such as <see cref="ScalarAnimation"/> and <see cref="Vector3Animation"/>).
/// Adding incompatible elements cannot be validated at build-time, but will result in a runtime crash.
/// </para>
/// <para>
/// Animations will monitor for changes in real-time to any of their public properties. For instance, if a binding is used to dynamically update the
/// <see cref="Animation{TValue, TKeyFrame}.To"/> or <see cref="Animation{TValue, TKeyFrame}.From"/> properties, the entire animation set will be
/// initialized again and assigned to the underlying <see cref="Visual"/> object for the targeted <see cref="UIElement"/>. This does not currently apply
/// to changes to the <see cref="Animation{TValue, TKeyFrame}.KeyFrames"/> property though (other than the entire property being reassigned). To achieve
/// dynamic updates to animation sets in that case, either leverage expression keyframes or just use code-behind to manually reinitialize the animations.
/// </para>
/// </summary>
/// <remarks>
/// An <see cref="ImplicitAnimationSet"/> instance can only be used on a single <see cref="UIElement"/> target, and it cannot be shared across multiple
/// elements. Attempting to do so will result in a runtime crash. Furthermore, it is recommended not to move <see cref="IImplicitTimeline"/> instances from
/// one <see cref="ImplicitAnimationSet"/> to another, and doing so will add unnecessary runtime overhead over time. If you want to apply the same animations
/// to multiple elements, simply create another <see cref="ImplicitAnimationSet"/> instance and another set of animations with the same properties within it.
/// </remarks>
public sealed class ImplicitAnimationSet : DependencyObjectCollection
{
/// <summary>
/// Raised whenever any configuration change occurrs within the current <see cref="ImplicitAnimationSet"/> instance.
/// </summary>
internal event EventHandler? AnimationsChanged;

/// <summary>
/// Initializes a new instance of the <see cref="ImplicitAnimationSet"/> class.
/// </summary>
public ImplicitAnimationSet()
{
VectorChanged += ImplicitAnimationSetVectorChanged;
}

/// <summary>
/// Registers <see cref="RaiseAnimationsChanged(object, EventArgs)"/> for every added animation.
/// </summary>
/// <param name="sender">The current vector of animations.</param>
/// <param name="event">The <see cref="IVectorChangedEventArgs"/> instance for the current event.</param>
private void ImplicitAnimationSetVectorChanged(IObservableVector<DependencyObject> sender, IVectorChangedEventArgs @event)
{
if (@event.CollectionChange == CollectionChange.ItemInserted ||
@event.CollectionChange == CollectionChange.ItemChanged)
{
IImplicitTimeline item = (IImplicitTimeline)sender[(int)@event.Index];

item.AnimationPropertyChanged -= RaiseAnimationsChanged;
item.AnimationPropertyChanged += RaiseAnimationsChanged;
}

AnimationsChanged?.Invoke(this, EventArgs.Empty);
}

/// <summary>
/// Raises the <see cref="AnimationsChanged"/> event.
/// </summary>
/// <param name="sender">The instance raising the event.</param>
/// <param name="e">The empty <see cref="EventArgs"/> for the event.</param>
private void RaiseAnimationsChanged(object sender, EventArgs e)
{
AnimationsChanged?.Invoke(this, e);
}

/// <summary>
/// Gets or sets the weak reference to the parent that owns the current implicit animation collection.
/// </summary>
Expand All @@ -27,11 +90,11 @@ public sealed class ImplicitAnimationSet : DependencyObjectCollection
/// Creates a <see cref="CompositionAnimationGroup"/> for the current collection.
/// This can be used to be assigned to show/hide implicit composition animations.
/// </summary>
/// <param name="parent">The target <see cref="UIElement"/> to which the animations are being applied to.</param>
/// <returns>The <see cref="CompositionAnimationGroup"/> instance to use.</returns>
[Pure]
internal CompositionAnimationGroup GetCompositionAnimationGroup()
internal CompositionAnimationGroup GetCompositionAnimationGroup(UIElement parent)
{
UIElement parent = GetParent();
Compositor compositor = ElementCompositionPreview.GetElementVisual(parent).Compositor;
CompositionAnimationGroup animations = compositor.CreateAnimationGroup();

Expand All @@ -47,11 +110,11 @@ internal CompositionAnimationGroup GetCompositionAnimationGroup()
/// Creates an <see cref="ImplicitAnimationCollection"/> for the current collection.
/// This can be used to be assigned to implicit composition animations.
/// </summary>
/// <param name="parent">The target <see cref="UIElement"/> to which the animations are being applied to.</param>
/// <returns>The <see cref="ImplicitAnimationCollection"/> instance to use.</returns>
[Pure]
internal ImplicitAnimationCollection GetImplicitAnimationCollection()
internal ImplicitAnimationCollection GetImplicitAnimationCollection(UIElement parent)
{
UIElement parent = GetParent();
Compositor compositor = ElementCompositionPreview.GetElementVisual(parent).Compositor;
ImplicitAnimationCollection animations = compositor.CreateImplicitAnimationCollection();

Expand All @@ -71,25 +134,5 @@ internal ImplicitAnimationCollection GetImplicitAnimationCollection()

return animations;
}

/// <summary>
/// Gets the current parent <see cref="UIElement"/> instance.
/// </summary>
/// <returns>The <see cref="UIElement"/> reference from <see cref="ParentReference"/>.</returns>
/// <exception cref="InvalidOperationException">Thrown if there is no parent available.</exception>
[Pure]
private UIElement GetParent()
{
UIElement? parent = null;

if (ParentReference?.TryGetTarget(out parent) != true)
{
ThrowInvalidOperationException();
}

return parent!;

static void ThrowInvalidOperationException() => throw new InvalidOperationException("The current ImplicitAnimationSet object isn't bound to a parent UIElement instance.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using Windows.UI.Composition;
using Windows.UI.Xaml;

Expand All @@ -14,6 +15,13 @@ namespace Microsoft.Toolkit.Uwp.UI.Animations
/// </summary>
public interface IImplicitTimeline
{
/// <summary>
/// Raised whenever a property that influences the animation changes.
/// This event is used by <see cref="ImplicitAnimationSet"/> to update the animations collection
/// assigned to a target <see cref="UIElement"/> when any of the individual animations is modified.
/// </summary>
event EventHandler? AnimationPropertyChanged;

/// <summary>
/// Gets a <see cref="CompositionAnimation"/> from the current node. This animation might
/// be used either as an implicit show/hide animation, or as a direct implicit animation.
Expand Down