Merge pull request #2546 from unoplatform/dev/dr/AnimsRollback

Fix animations are not reaching their final state on iOS
This commit is contained in:
Jérôme Laban 2020-01-29 06:25:55 -05:00 коммит произвёл GitHub
Родитель 504997ff3d bda897f889
Коммит 01ac836b10
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
14 изменённых файлов: 494 добавлений и 93 удалений

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

@ -41,7 +41,7 @@ jobs:
- job: Android_Tests
dependsOn: Android_Build_For_Tests
timeoutInMinutes: 90
variables:
CI_Build: true
SourceLinkEnabled: false

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

@ -55,7 +55,7 @@ jobs:
ArtifactType: Container
- job: iOS_Tests
timeoutInMinutes: 90
dependsOn: iOS_Build
pool:

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

@ -40,7 +40,8 @@ else
namespace = 'SamplesApp.UITests.Windows_UI_Xaml_Controls.TextBlockTests' or \
namespace = 'SamplesApp.UITests.Microsoft_UI_Xaml_Controls.NumberBoxTests' or \
namespace = 'SamplesApp.UITests.Windows_UI_Xaml_Controls.ImageTests' or \
namespace = 'SamplesApp.UITests.Windows_UI_Xaml_Controls.TextBoxTests'
namespace = 'SamplesApp.UITests.Windows_UI_Xaml_Controls.TextBoxTests' or \
namespace = 'SamplesApp.UITests.Windows_UI_Xaml_Media_Animation.DoubleAnimation_Tests'
"
fi

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

@ -69,7 +69,7 @@ namespace Uno.Samples.UITest.Generator
}
private object GetAttributePropertyValue(AttributeData attr, string name)
=> attr.NamedArguments.FirstOrDefault(kvp => kvp.Key == name).Value;
=> attr.NamedArguments.FirstOrDefault(kvp => kvp.Key == name).Value.Value;
private object GetConstructorParameterValue(AttributeData info, string name)
=> info.ConstructorArguments.IsDefaultOrEmpty

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

@ -212,7 +212,7 @@ namespace SamplesApp.UITests.TestFramework
}
}
public static void AssertDoesNotHaveColorAt(FileInfo screenshot, float x, float y, Color excludedColor, byte tolerance = 0)
public static void DoesNotHaveColorAt(FileInfo screenshot, float x, float y, Color excludedColor, byte tolerance = 0)
{
using (var bitmap = new Bitmap(screenshot.FullName))
{

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

@ -119,7 +119,7 @@ namespace SamplesApp.UITests.Windows_UI_Xaml_Controls.PopupTests
var during = TakeScreenshot("During", ignoreInSnapshotCompare: AppInitializer.GetLocalPlatform() == Platform.Android /*Status bar appears with clock*/);
ImageAssert.AssertDoesNotHaveColorAt(during, rect.CenterX, rect.CenterY, Color.Blue);
ImageAssert.DoesNotHaveColorAt(during, rect.CenterX, rect.CenterY, Color.Blue);
// Dismiss popup
var screenRect = _app.Marked("sampleContent").FirstResult().Rect;

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

@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NUnit.Framework;
using SamplesApp.UITests.TestFramework;
using Uno.UITest.Helpers;
using Uno.UITest.Helpers.Queries;
namespace SamplesApp.UITests.Windows_UI_Xaml_Media_Animation
{
[TestFixture]
public partial class DoubleAnimation_Tests : SampleControlUITestBase
{
private const string _finalStateOpacityTestControl = "UITests.Windows_UI_Xaml_Media_Animation.DoubleAnimation_FinalState_Opacity";
[Test] [AutoRetry] public void When_Opacity_Completed_With_FillBehaviorStop_Then_Rollback() => TestOpacityFinalState();
[Test] [AutoRetry] public void When_Opacity_Completed_With_FillBehaviorHold_Then_Hold() => TestOpacityFinalState();
[Test] [AutoRetry] public void When_Opacity_Paused_With_FillBehaviorStop_Then_Hold() => TestOpacityFinalState();
[Test] [AutoRetry] public void When_Opacity_Paused_With_FillBehaviorHold_Then_Hold() => TestOpacityFinalState();
[Test] [AutoRetry] public void When_Opacity_Canceled_With_FillBehaviorStop_Then_Rollback() => TestOpacityFinalState();
[Test] [AutoRetry] public void When_Opacity_Canceled_With_FillBehaviorHold_Then_Rollback() => TestOpacityFinalState();
private void TestOpacityFinalState([CallerMemberName] string testName = null)
{
var match = Regex.Match(testName, @"When_Opacity_(?<type>\w+)_With_FillBehavior(?<fill>\w+)_Then_(?<expected>\w+)");
if (!match.Success)
{
throw new InvalidOperationException("Invalid test name.");
}
var type = match.Groups["type"].Value;
var fill = match.Groups["fill"].Value;
var expected = match.Groups["expected"].Value;
bool isSame = false, isGray = false, isDifferent = false;
switch (type)
{
case "Completed" when expected == "Hold":
isGray = true;
break;
case "Completed" when expected == "Rollback":
isSame = true;
break;
case "Paused":
isDifferent = true;
break;
case "Canceled":
isSame = true;
break;
default:
throw new InvalidOperationException("Invalid test name.");
}
Run(_finalStateOpacityTestControl, skipInitialScreenshot: true);
var initial = TakeScreenshot("Initial", ignoreInSnapshotCompare: true);
var element = _app.WaitForElement($"{type}AnimationHost_{fill}").Single().Rect;
_app.Marked("StartButton").Tap();
_app.WaitForDependencyPropertyValue(_app.Marked("Status"), "Text", "Completed");
// Assert
var final = TakeScreenshot("Final", ignoreInSnapshotCompare: true);
if (isSame)
{
ImageAssert.AreEqual(initial, final, element);
}
else if (isGray)
{
ImageAssert.HasColorAt(final, element.CenterX, element.CenterY, Color.LightGray);
}
else if (isDifferent)
{
ImageAssert.AreNotEqual(initial, final, element);
ImageAssert.DoesNotHaveColorAt(final, element.CenterX, element.CenterY, Color.LightGray);
}
}
}
}

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

@ -15,18 +15,18 @@ namespace SamplesApp.UITests.Windows_UI_Xaml_Media_Animation
[TestFixture]
public partial class DoubleAnimation_Tests : SampleControlUITestBase
{
private const string _finalStateTestControl = "UITests.Windows_UI_Xaml_Media_Animation.DoubleAnimation_FinalState";
private const string _finalStateTransformsTestControl = "UITests.Windows_UI_Xaml_Media_Animation.DoubleAnimation_FinalState_Transforms";
[Test] [AutoRetry] public void When_Completed_With_FillBehaviorStop_Then_Rollback() => TestFinalState();
[Test] [AutoRetry] public void When_Completed_With_FillBehaviorHold_Then_Hold() => TestFinalState();
[Test] [AutoRetry] public void When_Paused_With_FillBehaviorStop_Then_Hold() => TestFinalState();
[Test] [AutoRetry] public void When_Paused_With_FillBehaviorHold_Then_Hold() => TestFinalState();
[Test] [AutoRetry] public void When_Canceled_With_FillBehaviorStop_Then_Rollback() => TestFinalState();
[Test] [AutoRetry] public void When_Canceled_With_FillBehaviorHold_Then_Rollback() => TestFinalState();
[Test] [AutoRetry] public void When_Transforms_Completed_With_FillBehaviorStop_Then_Rollback() => TestTransformsFinalState();
[Test] [AutoRetry] public void When_Transforms_Completed_With_FillBehaviorHold_Then_Hold() => TestTransformsFinalState();
[Test] [AutoRetry] public void When_Transforms_Paused_With_FillBehaviorStop_Then_Hold() => TestTransformsFinalState();
[Test] [AutoRetry] public void When_Transforms_Paused_With_FillBehaviorHold_Then_Hold() => TestTransformsFinalState();
[Test] [AutoRetry] public void When_Transforms_Canceled_With_FillBehaviorStop_Then_Rollback() => TestTransformsFinalState();
[Test] [AutoRetry] public void When_Transforms_Canceled_With_FillBehaviorHold_Then_Rollback() => TestTransformsFinalState();
private void TestFinalState([CallerMemberName] string testName = null)
private void TestTransformsFinalState([CallerMemberName] string testName = null)
{
var match = Regex.Match(testName, @"When_(?<type>\w+)_With_FillBehavior(?<fill>\w+)_Then_(?<expected>\w+)");
var match = Regex.Match(testName, @"When_Transforms_(?<type>\w+)_With_FillBehavior(?<fill>\w+)_Then_(?<expected>\w+)");
if (!match.Success)
{
throw new InvalidOperationException("Invalid test name.");
@ -60,7 +60,7 @@ namespace SamplesApp.UITests.Windows_UI_Xaml_Media_Animation
throw new InvalidOperationException("Invalid test name.");
}
Run(_finalStateTestControl);
Run(_finalStateTransformsTestControl, skipInitialScreenshot: true);
var initial = TakeScreenshot("Initial", ignoreInSnapshotCompare: true);
var initialLocation = _app.WaitForElement($"{type}AnimationHost_{fill}").Single().Rect;
@ -73,7 +73,7 @@ namespace SamplesApp.UITests.Windows_UI_Xaml_Media_Animation
_app.WaitForDependencyPropertyValue(_app.Marked("Status"), "Text", "Completed");
// Assert
var final = TakeScreenshot("Final", ignoreInSnapshotCompare: false);
var final = TakeScreenshot("Final", ignoreInSnapshotCompare: true);
var finalLocation = _app.WaitForElement($"{type}AnimationHost_{fill}").Single().Rect;
var actualDelta = finalLocation.Y - initialLocation.Y;

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

@ -2401,7 +2401,11 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Media_Animation\DoubleAnimation_FinalState.xaml">
<Page Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Media_Animation\DoubleAnimation_FinalState_Opacity.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Media_Animation\DoubleAnimation_FinalState_Transforms.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
@ -4110,8 +4114,11 @@
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Input\RoutedEvents\RoutedEvent_TappedControl.xaml.cs">
<DependentUpon>RoutedEvent_TappedControl.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Media_Animation\DoubleAnimation_FinalState.xaml.cs">
<DependentUpon>DoubleAnimation_FinalState.xaml</DependentUpon>
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Media_Animation\DoubleAnimation_FinalState_Opacity.xaml.cs">
<DependentUpon>DoubleAnimation_FinalState_Opacity.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Media_Animation\DoubleAnimation_FinalState_Transforms.xaml.cs">
<DependentUpon>DoubleAnimation_FinalState_Transforms.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Media_Animation\SequentialAnimationsPage.xaml.cs">
<DependentUpon>SequentialAnimationsPage.xaml</DependentUpon>

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

@ -0,0 +1,176 @@
<Page
x:Class="UITests.Windows_UI_Xaml_Media_Animation.DoubleAnimation_FinalState_Opacity"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UITests.Shared.Windows_UI_Xaml_Media_Animation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Page.Resources>
<Style TargetType="Border" x:Key="Marker">
<Setter Property="Width" Value="50" />
<Setter Property="Height" Value="50" />
<Setter Property="Background" Value="LightGray" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Top" />
</Style>
</Page.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button
x:Name="StartButton"
Grid.Row="0"
Grid.Column="0"
Content="Start animations"
Click="StartAnimations"/>
<CheckBox
x:Name="UseFromValue"
Grid.Row="0"
Grid.Column="1"
Content="Set 'From = .75' " />
<TextBlock
x:Name="Status"
Grid.Row="0"
Grid.Column="2"
VerticalAlignment="Center"
FontSize="16"
Text="Waiting" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Center"
Text="Complete" />
<TextBlock
Grid.Row="1"
Grid.Column="1"
HorizontalAlignment="Center"
Text="Paused" />
<TextBlock
Grid.Row="1"
Grid.Column="2"
HorizontalAlignment="Center"
Text="Canceled" />
<TextBlock
Grid.Row="2"
Grid.ColumnSpan="3"
HorizontalAlignment="Center"
Text="FillBehavior: HOLD END" />
<Border Style="{StaticResource Marker}" Grid.Row="3" Grid.Column="0" />
<Border Style="{StaticResource Marker}" Grid.Row="3" Grid.Column="1" />
<Border Style="{StaticResource Marker}" Grid.Row="3" Grid.Column="2" />
<Border Style="{StaticResource Marker}" Grid.Row="5" Grid.Column="0" />
<Border Style="{StaticResource Marker}" Grid.Row="5" Grid.Column="1" />
<Border Style="{StaticResource Marker}" Grid.Row="5" Grid.Column="2" />
<Border
x:Name="CompletedAnimationHost_Hold"
Grid.Row="3"
Grid.Column="0"
Background="#FF0000"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Width="50"
Height="50">
<Border.RenderTransform>
<TranslateTransform />
</Border.RenderTransform>
</Border>
<Border
x:Name="PausedAnimationHost_Hold"
Grid.Row="3"
Grid.Column="1"
Background="#FF8000"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Width="50"
Height="50">
<Border.RenderTransform>
<TranslateTransform />
</Border.RenderTransform>
</Border>
<Border
x:Name="CanceledAnimationHost_Hold"
Grid.Row="3"
Grid.Column="2"
Background="#FFFF00"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Width="50"
Height="50">
<Border.RenderTransform>
<TranslateTransform />
</Border.RenderTransform>
</Border>
<TextBlock
Grid.Row="4"
Grid.ColumnSpan="3"
HorizontalAlignment="Center"
Text="FillBehavior: STOP" />
<Border
x:Name="CompletedAnimationHost_Stop"
Grid.Row="5"
Grid.Column="0"
Background="#008000"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Width="50"
Height="50">
<Border.RenderTransform>
<TranslateTransform />
</Border.RenderTransform>
</Border>
<Border
x:Name="PausedAnimationHost_Stop"
Grid.Row="5"
Grid.Column="1"
Background="#0000FF"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Width="50"
Height="50">
<Border.RenderTransform>
<TranslateTransform />
</Border.RenderTransform>
</Border>
<Border
x:Name="CanceledAnimationHost_Stop"
Grid.Row="6"
Grid.Column="2"
Background="#A000C0"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Width="50"
Height="50">
<Border.RenderTransform>
<TranslateTransform />
</Border.RenderTransform>
</Border>
</Grid>
</Page>

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

@ -0,0 +1,95 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Animation;
using Uno.Extensions;
using Uno.UI.Samples.Controls;
namespace UITests.Windows_UI_Xaml_Media_Animation
{
[Sample("Animations", Name = "DoubleAnimation opacity final state", Description = _description)]
public sealed partial class DoubleAnimation_FinalState_Opacity : Page
{
private const string _description = @"This (automated) test validate the final state of when animating opacity using a double animation.
Expected result:
* Completed: stays at 0 if HOLD, goes back to 1 if STOP
* Paused: stays half visible no matter the fill behavior
* Canceled: goes back to 1 no matter the fill behavior
If 'Set From' was selected, then rollback means back to value before animation, not the 'From' value!";
private TimeSpan _duration;
public DoubleAnimation_FinalState_Opacity()
{
this.InitializeComponent();
#if DEBUG
_duration = TimeSpan.FromSeconds(2);
#else
_duration = TimeSpan.FromMilliseconds(400);
#endif
}
private async void StartAnimations(object sender, RoutedEventArgs e)
{
var toLetComplete = new[]
{
CreateAnimation(CompletedAnimationHost_Hold, FillBehavior.HoldEnd),
CreateAnimation(CompletedAnimationHost_Stop, FillBehavior.Stop)
};
var toPause = new[]
{
CreateAnimation(PausedAnimationHost_Hold, FillBehavior.HoldEnd),
CreateAnimation(PausedAnimationHost_Stop, FillBehavior.Stop)
};
var toCancel = new[]
{
CreateAnimation(CanceledAnimationHost_Hold, FillBehavior.HoldEnd),
CreateAnimation(CanceledAnimationHost_Stop, FillBehavior.Stop)
};
Status.Text = "Animating";
await Dispatcher.RunAsync(
CoreDispatcherPriority.Normal,
async () =>
{
var halfAnimation = (int)_duration.TotalMilliseconds / 2;
await Task.Delay(halfAnimation);
toPause.ForEach(s => s.Pause());
toCancel.ForEach(s => s.Stop());
await Task.Delay((int)(halfAnimation * 1.2));
Status.Text = "Completed";
});
toLetComplete.Concat(toPause).Concat(toCancel).ForEach(s => s.Begin());
}
private Storyboard CreateAnimation(UIElement target, FillBehavior fill)
{
var animation = new DoubleAnimation
{
To = 0,
Duration = _duration,
FillBehavior = fill
};
if (UseFromValue.IsChecked.GetValueOrDefault())
{
animation.From = .75;
}
Storyboard.SetTarget(animation, target);
Storyboard.SetTargetProperty(animation, nameof(UIElement.Opacity));
return new Storyboard
{
Children = { animation }
};
}
}
}

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

@ -1,5 +1,5 @@
<Page
x:Class="UITests.Windows_UI_Xaml_Media_Animation.DoubleAnimation_FinalState"
x:Class="UITests.Windows_UI_Xaml_Media_Animation.DoubleAnimation_FinalState_Transforms"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UITests.Shared.Windows_UI_Xaml_Media_Animation"
@ -12,7 +12,7 @@
<Style TargetType="Border" x:Key="Marker">
<Setter Property="Width" Value="50" />
<Setter Property="Height" Value="50" />
<Setter Property="Background" Value="#33000000" />
<Setter Property="Background" Value="LightGray" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Top" />
</Style>

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

@ -11,12 +11,21 @@ using Uno.UI.Samples.Controls;
namespace UITests.Windows_UI_Xaml_Media_Animation
{
[Sample("Animations")]
public sealed partial class DoubleAnimation_FinalState : Page
[Sample("Animations", "Transform", Name="DoubleAnimation transforms final state", Description = _description)]
public sealed partial class DoubleAnimation_FinalState_Transforms : Page
{
private const string _description = @"This (automated) test validate the final state of when animating transformation using a double animation.
Expected result:
* Completed: stays at bottom if HOLD, goes back to top if STOP
* Paused: stays in the middle no matter the fill behavior
* Canceled: goes back to top no matter the fille behavior
If 'Set From' was selected, then rollback means back to value before animation, not the 'From' value!";
private TimeSpan _duration;
public DoubleAnimation_FinalState()
public DoubleAnimation_FinalState_Transforms()
{
this.InitializeComponent();
@ -34,12 +43,12 @@ namespace UITests.Windows_UI_Xaml_Media_Animation
CreateAnimation(CompletedAnimationHost_Hold, FillBehavior.HoldEnd),
CreateAnimation(CompletedAnimationHost_Stop, FillBehavior.Stop)
};
var toPause = new []
var toPause = new[]
{
CreateAnimation(PausedAnimationHost_Hold, FillBehavior.HoldEnd),
CreateAnimation(PausedAnimationHost_Stop, FillBehavior.Stop)
};
var toCancel = new []
var toCancel = new[]
{
CreateAnimation(CanceledAnimationHost_Hold, FillBehavior.HoldEnd),
CreateAnimation(CanceledAnimationHost_Stop, FillBehavior.Stop)

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

@ -103,6 +103,7 @@ namespace Windows.UI.Composition
duration = _durationMilliseconds;
}
}
if (_stop.value.HasValue)
{
from = _stop.value.Value;
@ -133,8 +134,8 @@ namespace Windows.UI.Composition
if (_isDiscrete)
{
var discreteAnim = CAKeyFrameAnimation.FromKeyPath(_property);
discreteAnim.KeyTimes = new NSNumber[] { new NSNumber(0.0), new NSNumber(1.0) };
discreteAnim.Values = new NSObject[] { _nsValueConversion(to) };
discreteAnim.KeyTimes = new NSNumber[] {new NSNumber(0.0), new NSNumber(1.0)};
discreteAnim.Values = new NSObject[] {_nsValueConversion(to)};
discreteAnim.CalculationMode = CAKeyFrameAnimation.AnimationDescrete;
animation = discreteAnim;
@ -148,17 +149,19 @@ namespace Windows.UI.Composition
animation = continuousAnim;
}
if (delayMilliseconds > 0)
{
// Note: We must make sure to use the time relative to the 'layer', otherwise we might introduce a random delay and the animations
// will run twice (once "managed" while updating the DP, and a second "native" using this animator)
animation.BeginTime = layer.ConvertTimeFromLayer(CAAnimation.CurrentMediaTime() + delayMilliseconds / __millisecondsPerSecond, null);
}
animation.Duration = durationMilliseconds / __millisecondsPerSecond;
animation.FillMode = CAFillMode.Forwards;
animation.RemovedOnCompletion = true;
animation.AnimationStarted += OnAnimationStarted;
animation.AnimationStopped += OnAnimationStopped;
animation.AnimationStarted += OnAnimationStarted(animation);
animation.AnimationStopped += OnAnimationStopped(animation);
// Start the animation
_stop = default; // Cleanup stop reason
@ -182,87 +185,108 @@ namespace Windows.UI.Composition
layer.RemoveAnimation(_key); // This will effectively stop the animation (and invoke OnAnimationStopped)
}
private void OnAnimationStarted(object sender, EventArgs _)
private EventHandler OnAnimationStarted(CAAnimation animation)
{
if (sender is CAAnimation animation)
{
animation.AnimationStarted -= OnAnimationStarted; // Prevent leak
}
EventHandler handler = default;
handler= Handler;
if (_current.animation != sender)
{
return; // We are no longer the current animation, do not interfere with the current
}
return handler;
if (this.Log().IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
void Handler(object sender, EventArgs _)
{
this.Log().DebugFormat("CoreAnimation '{0}' has started.", _property);
}
// Note: The sender is usually not the same managed instance of the started animation
// (even the sender.Handle is not the same). So we cannot rely on it to determine
// if we are still the '_current'. Instead we have to create a new handler
// for each started animation which captures its target 'animation' instance.
// This will disable the transform while the native animation handles it
// It must be the first thing we do when the animation starts
// (However we have to wait for the first frame in order to not remove the transform while waiting for the BeginTime)
_prepare?.Invoke();
animation.AnimationStarted -= handler; // Prevent leak
if (_current.animation != animation)
{
return; // We are no longer the current animation, do not interfere with the current
}
if (this.Log().IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
this.Log().DebugFormat("CoreAnimation '{0}' has started.", _property);
}
// This will disable the transform while the native animation handles it
// It must be the first thing we do when the animation starts
// (However we have to wait for the first frame in order to not remove the transform while waiting for the BeginTime)
_prepare?.Invoke();
}
}
private void OnAnimationStopped(object sender, CAAnimationStateEventArgs args)
private EventHandler<CAAnimationStateEventArgs> OnAnimationStopped(CAAnimation animation)
{
// This callback will be invoked when the animation is stopped, no matter the reason (completed, paused or canceled)
if (sender is CAAnimation animation)
{
animation.AnimationStopped -= OnAnimationStopped; // Prevent leak
}
EventHandler<CAAnimationStateEventArgs> handler = default;
handler = Handler;
var (currentAnim, from, to) = _current;
if (currentAnim != sender)
{
return; // We are no longer the current animation, do not interfere with the current
}
_current = default;
return handler;
if (this.Log().IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
void Handler(object sender, CAAnimationStateEventArgs args)
{
this.Log().DebugFormat("CoreAnimation on property {0} has been {1}.", _property, _stop.reason);
}
// Note: The sender is usually not the same managed instance of the started animation
// (even the sender.Handle is not the same). So we cannot rely on it to determine
// if we are still the '_current'. Instead we have to create a new handler
// for each started animation which captures its target 'animation' instance.
// First commit the expected final (end, current or initial) value.
if (_layer.TryGetTarget(out var layer))
{
var keyPath = new NSString(_property);
NSObject finalValue;
switch (_stop.reason)
animation.AnimationStopped -= handler; // Prevent leak
var (currentAnim, from, to) = _current;
if (currentAnim != animation)
{
case StopReason.Paused:
finalValue = _stop.value.HasValue
? _nsValueConversion(_stop.value.Value)
: layer.ValueForKeyPath(keyPath);
break;
case StopReason.Canceled:
finalValue = _nsValueConversion(from);
break;
default:
case StopReason.Completed:
finalValue = _nsValueConversion(to);
break;
return; // We are no longer the current animation, do not interfere with the current
}
CATransaction.Begin();
CATransaction.DisableActions = true;
layer.SetValueForKeyPath(finalValue, keyPath);
CATransaction.Commit();
}
_current = default;
// Then reactivate the managed code handling of transforms that was disabled by the _prepare.
_cleanup?.Invoke();
if (this.Log().IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
this.Log().DebugFormat("CoreAnimation on property {0} has been {1}.", _property, _stop.reason);
}
// Finally raise callbacks
if (_stop.reason == StopReason.Completed)
{
Debug.Assert(args.Finished);
_onCompleted();
// First commit the expected final (end, current or initial) value.
if (_layer.TryGetTarget(out var layer))
{
var keyPath = new NSString(_property);
NSObject finalValue;
switch (_stop.reason)
{
case StopReason.Paused:
finalValue = _stop.value.HasValue
? _nsValueConversion(_stop.value.Value)
: layer.ValueForKeyPath(keyPath);
break;
case StopReason.Canceled:
finalValue = _nsValueConversion(from);
break;
default:
case StopReason.Completed:
finalValue = _nsValueConversion(to);
break;
}
CATransaction.Begin();
CATransaction.DisableActions = true;
layer.SetValueForKeyPath(finalValue, keyPath);
CATransaction.Commit();
}
// Then reactivate the managed code handling of transforms that was disabled by the _prepare.
_cleanup?.Invoke();
// Finally raise callbacks
if (_stop.reason == StopReason.Completed)
{
Debug.Assert(args.Finished);
_onCompleted();
}
}
}