Clean-up and simplify Shadow Animations, add support for animating all shadows

This commit is contained in:
michael-hawker 2021-08-24 09:30:03 -07:00
Родитель 864791961c
Коммит ca6f772a99
11 изменённых файлов: 184 добавлений и 56 удалений

Просмотреть файл

@ -13,30 +13,63 @@
<Page.Resources>
<media:AttachedCardShadow x:Key="CommonShadow" Offset="4" CornerRadius="0"/>
<ani:AnimationSet x:Key="ShadowEnterAnimation">
<ani:OffsetDropShadowAnimation To="12"/>
</ani:AnimationSet>
<ani:AnimationSet x:Key="ShadowExitAnimation">
<ani:OffsetDropShadowAnimation To="4"/>
</ani:AnimationSet>
<ani:AnimationSet x:Key="ShadowPopAnimation" IsSequential="True">
<ani:TranslationAnimation To="-8" Duration="0:0:1"/>
<ani:OffsetDropShadowAnimation To="16" Duration="0:0:2" Target="{StaticResource CommonShadow}"/>
<ani:OffsetDropShadowAnimation To="4" Delay="0:0:0.5" Duration="0:0:2" Target="{StaticResource CommonShadow}"/>
<ani:TranslationAnimation To="0" Duration="0:0:1"/>
</ani:AnimationSet>
</Page.Resources>
<Grid>
<Image x:Name="ImageWithShadow"
ui:Effects.Shadow="{StaticResource CommonShadow}"
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image ui:Effects.Shadow="{StaticResource CommonShadow}"
Height="100" Width="100"
Source="ms-appx:///Assets/Photos/Owl.jpg">
<interactivity:Interaction.Behaviors>
<interactions:EventTriggerBehavior EventName="PointerEntered">
<behaviors:StartAnimationAction Animation="{Binding ElementName=ShadowEnterAnimation}"/>
<behaviors:StartAnimationAction Animation="{StaticResource ShadowEnterAnimation}"/>
</interactions:EventTriggerBehavior>
<interactions:EventTriggerBehavior EventName="PointerExited">
<behaviors:StartAnimationAction Animation="{Binding ElementName=ShadowExitAnimation}"/>
<behaviors:StartAnimationAction Animation="{StaticResource ShadowExitAnimation}"/>
</interactions:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
<ani:Explicit.Animations>
<ani:AnimationSet x:Name="ShadowEnterAnimation">
<ani:OffsetDropShadowAnimation From="4" To="12" Target="{Binding ElementName=ImageWithShadow}"/>
</ani:AnimationSet>
<ani:AnimationSet x:Name="ShadowExitAnimation">
<ani:OffsetDropShadowAnimation From="12" To="4" Target="{Binding ElementName=ImageWithShadow}"/>
</ani:AnimationSet>
</ani:Explicit.Animations>
</Image>
<Image ui:Effects.Shadow="{StaticResource CommonShadow}"
Height="100" Width="100"
Grid.Column="1"
Source="ms-appx:///Assets/Photos/Owl.jpg">
<interactivity:Interaction.Behaviors>
<interactions:EventTriggerBehavior EventName="PointerEntered">
<behaviors:StartAnimationAction Animation="{StaticResource ShadowEnterAnimation}"/>
</interactions:EventTriggerBehavior>
<interactions:EventTriggerBehavior EventName="PointerExited">
<behaviors:StartAnimationAction Animation="{StaticResource ShadowExitAnimation}"/>
</interactions:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Image>
<Button Grid.Row="1" Grid.ColumnSpan="2" HorizontalAlignment="Center" VerticalAlignment="Top" Content="Click Me">
<interactivity:Interaction.Behaviors>
<interactions:EventTriggerBehavior EventName="Click">
<behaviors:StartAnimationAction Animation="{StaticResource ShadowPopAnimation}"/>
</interactions:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Button>
</Grid>
</Page>

Просмотреть файл

@ -4,4 +4,5 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.Toolkit.Uwp.UI.Media")]
[assembly: InternalsVisibleTo("Microsoft.Toolkit.Uwp.UI.Behaviors")]
[assembly: InternalsVisibleTo("Microsoft.Toolkit.Uwp.UI.Media")]

Просмотреть файл

@ -21,8 +21,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Animations
/// properties. This can differ from <typeparamref name="TKeyFrame"/> to facilitate XAML parsing.
/// </typeparam>
/// <typeparam name="TKeyFrame">The actual type of keyframe values in use.</typeparam>
public abstract class ShadowAnimation<TShadow, TValue, TKeyFrame> : Animation<TValue, TKeyFrame>
where TShadow : FrameworkElement
public abstract class ShadowAnimation<TShadow, TValue, TKeyFrame> : Animation<TValue, TKeyFrame>, IAttachedTimeline
where TShadow : AttachedShadowBase
where TKeyFrame : unmanaged
{
/// <summary>
@ -46,21 +46,12 @@ namespace Microsoft.Toolkit.Uwp.UI.Animations
/// <inheritdoc/>
public override AnimationBuilder AppendToBuilder(AnimationBuilder builder, TimeSpan? delayHint, TimeSpan? durationHint, EasingType? easingTypeHint, EasingMode? easingModeHint)
{
if (Target is not TShadow target)
{
static AnimationBuilder ThrowTargetNullException() => throw new ArgumentNullException("The target element is null, make sure to set the Target property");
return ThrowTargetNullException();
}
var shadowBase = Effects.GetShadow(Target);
if (shadowBase == null)
{
static AnimationBuilder ThrowArgumentNullException() => throw new ArgumentNullException("The target's shadow is null, make sure to set the Target property to an element with a Shadow");
return ThrowArgumentNullException();
}
throw new NotSupportedException();
}
/// <inheritdoc/>
public AnimationBuilder AppendToBuilder(AnimationBuilder builder, UIElement parent, TimeSpan? delayHint = null, TimeSpan? durationHint = null, EasingType? easingTypeHint = null, EasingMode? easingModeHint = null)
{
if (ExplicitTarget is not string explicitTarget)
{
static AnimationBuilder ThrowArgumentNullException()
@ -72,20 +63,52 @@ namespace Microsoft.Toolkit.Uwp.UI.Animations
return ThrowArgumentNullException();
}
var shadow = shadowBase.GetElementContext(Target).Shadow;
if (Target is TShadow allShadows)
{
// in this case we'll animate all the shadows being used.
foreach (var context in allShadows.GetElementContextEnumerable()) //// TODO: Find better way!!!
{
NormalizedKeyFrameAnimationBuilder<TKeyFrame>.Composition keyFrameBuilder = new(
explicitTarget,
Delay ?? delayHint ?? DefaultDelay,
Duration ?? durationHint ?? DefaultDuration,
Repeat,
DelayBehavior);
NormalizedKeyFrameAnimationBuilder<TKeyFrame>.Composition keyFrameBuilder = new(
explicitTarget,
Delay ?? delayHint ?? DefaultDelay,
Duration ?? durationHint ?? DefaultDuration,
Repeat,
DelayBehavior);
AppendToBuilder(keyFrameBuilder, easingTypeHint, easingModeHint);
AppendToBuilder(keyFrameBuilder, easingTypeHint, easingModeHint);
CompositionAnimation animation = keyFrameBuilder.GetAnimation(context.Shadow, out _);
CompositionAnimation animation = keyFrameBuilder.GetAnimation(shadow, out _);
builder.ExternalAnimation(context.Shadow, animation);
}
return builder.ExternalAnimation(shadow, animation);
return builder;
}
else
{
var shadowBase = Effects.GetShadow(parent as FrameworkElement);
if (shadowBase == null)
{
static AnimationBuilder ThrowArgumentNullException() => throw new ArgumentNullException("The target's shadow is null, make sure to set the Target property to an element with a Shadow");
return ThrowArgumentNullException();
}
var shadow = shadowBase.GetElementContext((FrameworkElement)parent).Shadow;
NormalizedKeyFrameAnimationBuilder<TKeyFrame>.Composition keyFrameBuilder = new(
explicitTarget,
Delay ?? delayHint ?? DefaultDelay,
Duration ?? durationHint ?? DefaultDuration,
Repeat,
DelayBehavior);
AppendToBuilder(keyFrameBuilder, easingTypeHint, easingModeHint);
CompositionAnimation animation = keyFrameBuilder.GetAnimation(shadow, out _);
return builder.ExternalAnimation(shadow, animation);
}
}
}
}

Просмотреть файл

@ -119,7 +119,15 @@ namespace Microsoft.Toolkit.Uwp.UI.Animations
{
foreach (object node in this)
{
if (node is ITimeline timeline)
if (node is IAttachedTimeline attachedTimeline)
{
var builder = AnimationBuilder.Create();
attachedTimeline.AppendToBuilder(builder, element);
await builder.StartAsync(element, token);
}
else if (node is ITimeline timeline)
{
var builder = AnimationBuilder.Create();
@ -166,6 +174,9 @@ namespace Microsoft.Toolkit.Uwp.UI.Animations
{
switch (node)
{
case IAttachedTimeline attachedTimeline:
builder = attachedTimeline.AppendToBuilder(builder, element);
break;
case ITimeline timeline:
builder = timeline.AppendToBuilder(builder);
break;

Просмотреть файл

@ -0,0 +1,35 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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.Xaml;
using Windows.UI.Xaml.Media.Animation;
namespace Microsoft.Toolkit.Uwp.UI.Animations
{
/// <summary>
/// An interface representing a XAML model for a custom animation that requires a specific parent <see cref="UIElement"/> context.
/// </summary>
public interface IAttachedTimeline
{
/// <summary>
/// Appends the current animation to a target <see cref="AnimationBuilder"/> instance.
/// This method is used when the current <see cref="ITimeline"/> instance is explicitly triggered.
/// </summary>
/// <param name="builder">The target <see cref="AnimationBuilder"/> instance to schedule the animation on.</param>
/// <param name="parent">The parent <see cref="UIElement"/> this animation will be started on.</param>
/// <param name="delayHint">A hint for the animation delay, if present.</param>
/// <param name="durationHint">A hint for the animation duration, if present.</param>
/// <param name="easingTypeHint">A hint for the easing type, if present.</param>
/// <param name="easingModeHint">A hint for the easing mode, if present.</param>
/// <returns>The same <see cref="AnimationBuilder"/> instance as <paramref name="builder"/>.</returns>
AnimationBuilder AppendToBuilder(
AnimationBuilder builder,
UIElement parent,
TimeSpan? delayHint = null,
TimeSpan? durationHint = null,
EasingType? easingTypeHint = null,
EasingMode? easingModeHint = null);
}
}

Просмотреть файл

@ -13,7 +13,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Animations
public interface ITimeline
{
/// <summary>
/// Appens the current animation to a target <see cref="AnimationBuilder"/> instance.
/// Appends the current animation to a target <see cref="AnimationBuilder"/> instance.
/// This method is used when the current <see cref="ITimeline"/> instance is explicitly triggered.
/// </summary>
/// <param name="builder">The target <see cref="AnimationBuilder"/> instance to schedule the animation on.</param>

Просмотреть файл

@ -11,7 +11,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Animations
/// <summary>
/// An offset animation working on the composition layer.
/// </summary>
public sealed class OffsetDropShadowAnimation : ShadowAnimation<FrameworkElement, string, Vector3>
public sealed class OffsetDropShadowAnimation : ShadowAnimation<AttachedShadowBase, string, Vector3>
{
/// <inheritdoc/>
protected override string ExplicitTarget => nameof(DropShadow.Offset);

Просмотреть файл

@ -58,13 +58,19 @@ namespace Microsoft.Toolkit.Uwp.UI.Behaviors
ThrowArgumentNullException();
}
UIElement parent = null;
if (TargetObject is not null)
{
Animation.Start(TargetObject);
}
else if (Animation.ParentReference?.TryGetTarget(out parent) == true) //// TODO: Tidy... apply same pattern to Activities?
{
Animation.Start(parent);
}
else
{
Animation.Start();
Animation.Start(sender as UIElement);
}
return null!;

Просмотреть файл

@ -58,13 +58,19 @@ namespace Microsoft.Toolkit.Uwp.UI.Behaviors
ThrowArgumentNullException();
}
UIElement parent = null;
if (TargetObject is not null)
{
Animation.Stop(TargetObject);
}
else if (Animation.ParentReference?.TryGetTarget(out parent) == true) //// TODO: Tidy...
{
Animation.Stop(parent);
}
else
{
Animation.Stop();
Animation.Stop(sender as UIElement);
}
return null!;

Просмотреть файл

@ -123,22 +123,22 @@ namespace Microsoft.Toolkit.Uwp.UI
// Need to remove all old children from previous container if it's changed
if (prevContainer != null && prevContainer != shadow._container)
{
foreach (var context in shadow.ShadowElementContextTable)
foreach (var context in shadow.GetElementContextEnumerable())
{
if (context.Value.IsInitialized &&
prevContainer.Children.Contains(context.Value.SpriteVisual))
if (context.IsInitialized &&
prevContainer.Children.Contains(context.SpriteVisual))
{
prevContainer.Children.Remove(context.Value.SpriteVisual);
prevContainer.Children.Remove(context.SpriteVisual);
}
}
}
// Make sure all child shadows are hooked into container
foreach (var context in shadow.ShadowElementContextTable)
foreach (var context in shadow.GetElementContextEnumerable())
{
if (context.Value.IsInitialized)
if (context.IsInitialized)
{
shadow.SetElementChildVisual(context.Value);
shadow.SetElementChildVisual(context);
}
}
@ -154,12 +154,12 @@ namespace Microsoft.Toolkit.Uwp.UI
{
// Don't use sender or 'e' here as related to container element not
// element for shadow, grab values off context. (Also may be null from internal call.)
foreach (var context in ShadowElementContextTable)
foreach (var context in GetElementContextEnumerable())
{
if (context.Value.IsInitialized)
if (context.IsInitialized)
{
// TODO: Should we use ActualWidth/Height instead of RenderSize?
OnSizeChanged(context.Value, context.Value.Element.RenderSize, context.Value.Element.RenderSize);
OnSizeChanged(context, context.Element.RenderSize, context.Element.RenderSize);
}
}
}

Просмотреть файл

@ -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.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using Windows.Foundation;
@ -64,7 +65,7 @@ namespace Microsoft.Toolkit.Uwp.UI
/// <summary>
/// Gets or sets the collection of <see cref="AttachedShadowElementContext"/> for each element this <see cref="AttachedShadowBase"/> is connected to.
/// </summary>
protected ConditionalWeakTable<FrameworkElement, AttachedShadowElementContext> ShadowElementContextTable { get; set; }
private ConditionalWeakTable<FrameworkElement, AttachedShadowElementContext> ShadowElementContextTable { get; set; }
/// <summary>
/// Gets or sets the blur radius of the shadow.
@ -178,6 +179,18 @@ namespace Microsoft.Toolkit.Uwp.UI
return null;
}
/// <summary>
/// Gets an enumerator over the current list of <see cref="AttachedShadowElementContext"/> of elements using this shared shadow definition.
/// </summary>
/// <returns>Enumeration of <see cref="AttachedShadowElementContext"/> objects.</returns>
public IEnumerable<AttachedShadowElementContext> GetElementContextEnumerable()
{
foreach (var kvp in ShadowElementContextTable)
{
yield return kvp.Value;
}
}
/// <summary>
/// Sets <see cref="AttachedShadowElementContext.SpriteVisual"/> as a child visual on <see cref="AttachedShadowElementContext.Element"/>
/// </summary>