Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
7 changes: 7 additions & 0 deletions src/SamplesApp/UITests.Shared/UITests.Shared.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -5290,6 +5290,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Media_Animation\DoubleAnimation_AutoReverse.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Media_Animation\DoubleAnimation_RenderTransformOrigin.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
Expand Down Expand Up @@ -8932,6 +8936,9 @@
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Media_Animation\DoubleAnimation_Opacity_TextBlock.xaml.cs">
<DependentUpon>DoubleAnimation_Opacity_TextBlock.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Media_Animation\DoubleAnimation_AutoReverse.xaml.cs">
<DependentUpon>DoubleAnimation_AutoReverse.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Media_Animation\DoubleAnimation_RenderTransformOrigin.xaml.cs">
<DependentUpon>DoubleAnimation_RenderTransformOrigin.xaml</DependentUpon>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<UserControl
x:Class="GenericApp.Views.Content.UITests.Animations.DoubleAnimation_AutoReverse"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="600"
d:DesignWidth="400">

<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>

<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="10" Margin="10">
<Button x:Name="StartButton" Content="Start" Click="StartAnimation" />
<Button x:Name="StopButton" Content="Stop" Click="StopAnimation" />
</StackPanel>

<TextBlock Grid.Row="1" x:Name="StatusText" Text="Click Start to begin animation" Margin="10" />

<Grid Grid.Row="2" Margin="20">
<Rectangle x:Name="AnimatedRectangle"
Width="50"
Height="50"
Fill="{ThemeResource SystemAccentColorBrush}"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<Rectangle.RenderTransform>
<TranslateTransform x:Name="RectTransform" />
</Rectangle.RenderTransform>
</Rectangle>

<StackPanel VerticalAlignment="Bottom" Margin="10">
<TextBlock Text="AutoReverse=True" FontWeight="Bold" Margin="0,0,0,10" />
<TextBlock Text="Duration: 2 seconds" />
<TextBlock Text="From: 0, To: 300" />
<TextBlock Text="Expected: Move right, then back to start" TextWrapping="Wrap" />
</StackPanel>
</Grid>

<Grid.Resources>
<Storyboard x:Key="MoveStoryboard">
<DoubleAnimation
Storyboard.TargetName="RectTransform"
Storyboard.TargetProperty="X"
From="0"
To="300"
Duration="0:0:2"
AutoReverse="True"
FillBehavior="HoldEnd" />
</Storyboard>
</Grid.Resources>
</Grid>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
using Uno.UI.Samples.Controls;

namespace GenericApp.Views.Content.UITests.Animations
{
[Sample("Animations", Description = "Demonstrates AutoReverse functionality. The rectangle should move right then back to the start position.")]
public sealed partial class DoubleAnimation_AutoReverse : UserControl
{
private Storyboard _storyboard;

public DoubleAnimation_AutoReverse()
{
this.InitializeComponent();
}

private void StartAnimation(object sender, RoutedEventArgs e)
{
if (_storyboard == null)
{
_storyboard = (Storyboard)Resources["MoveStoryboard"];
_storyboard.Completed += OnStoryboardCompleted;
}

StatusText.Text = "Animation started...";
_storyboard.Begin();
}

private void StopAnimation(object sender, RoutedEventArgs e)
{
_storyboard?.Stop();
StatusText.Text = "Animation stopped";
}

private void OnStoryboardCompleted(object sender, object e)
{
StatusText.Text = "Animation completed - rectangle should be back at start position";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -391,5 +391,146 @@ private static double GetTranslateY(TranslateTransform translate, bool isStillAn
// And, when the animation is completed, this native value is reset even for HoldEnd animation.
: translate.Y;
#endif

[TestMethod]
[PlatformCondition(ConditionMode.Exclude, RuntimeTestPlatforms.NativeAndroid | RuntimeTestPlatforms.NativeIOS)]
public async Task When_AutoReverse_True()
{
var target = new TextBlock() { Text = "Test AutoReverse" };
WindowHelper.WindowContent = target;
await WindowHelper.WaitForIdle();
await WindowHelper.WaitForLoaded(target);

var transform = new TranslateTransform();
target.RenderTransform = transform;

var animation = new DoubleAnimation()
{
From = 0,
To = 100,
Duration = TimeSpan.FromMilliseconds(500),
AutoReverse = true,
FillBehavior = FillBehavior.HoldEnd,
};
Storyboard.SetTarget(animation, transform);
Storyboard.SetTargetProperty(animation, nameof(TranslateTransform.X));

var storyboard = new Storyboard();
storyboard.Children.Add(animation);

bool completed = false;
storyboard.Completed += (s, e) => completed = true;

storyboard.Begin();

// Wait for quarter of the animation (forward phase)
await Task.Delay(250);
var valueAt25Percent = transform.X;

// Wait for just past halfway (should be near the end of forward phase)
await Task.Delay(300);
var valueAt55Percent = transform.X;

// Wait for 75% of total duration (should be in reverse phase)
await Task.Delay(250);
var valueAt80Percent = transform.X;

// Wait for completion
await WindowHelper.WaitFor(() => completed, timeoutMS: 2000);

// Final value should be back at start (0) with HoldEnd
var finalValue = transform.X;

// Verify animation went forward then backward
Assert.IsTrue(valueAt25Percent > 0 && valueAt25Percent < 100,
$"At 25%, value should be between 0 and 100, got {valueAt25Percent}");
Assert.IsTrue(valueAt55Percent > valueAt25Percent,
$"At 55%, value should be greater than at 25%, got {valueAt55Percent} vs {valueAt25Percent}");
Assert.IsTrue(valueAt80Percent < valueAt55Percent,
$"At 80% (reverse phase), value should be less than at 55%, got {valueAt80Percent} vs {valueAt55Percent}");
Assert.IsTrue(Math.Abs(finalValue) < 10,
$"Final value should be close to 0, got {finalValue}");
}

[TestMethod]
[PlatformCondition(ConditionMode.Exclude, RuntimeTestPlatforms.NativeAndroid | RuntimeTestPlatforms.NativeIOS)]
public async Task When_AutoReverse_False()
{
var target = new TextBlock() { Text = "Test No AutoReverse" };
WindowHelper.WindowContent = target;
await WindowHelper.WaitForIdle();
await WindowHelper.WaitForLoaded(target);

var transform = new TranslateTransform();
target.RenderTransform = transform;

var animation = new DoubleAnimation()
{
From = 0,
To = 100,
Duration = TimeSpan.FromMilliseconds(500),
AutoReverse = false,
FillBehavior = FillBehavior.HoldEnd,
};
Storyboard.SetTarget(animation, transform);
Storyboard.SetTargetProperty(animation, nameof(TranslateTransform.X));

var storyboard = new Storyboard();
storyboard.Children.Add(animation);

bool completed = false;
storyboard.Completed += (s, e) => completed = true;

storyboard.Begin();

// Wait for completion
await WindowHelper.WaitFor(() => completed, timeoutMS: 2000);

// Final value should stay at end (100) with HoldEnd and no AutoReverse
var finalValue = transform.X;
Assert.IsTrue(Math.Abs(finalValue - 100) < 10,
$"Final value should be close to 100, got {finalValue}");
}

[TestMethod]
[PlatformCondition(ConditionMode.Exclude, RuntimeTestPlatforms.NativeAndroid | RuntimeTestPlatforms.NativeIOS)]
public async Task When_AutoReverse_WithRepeat()
{
var target = new TextBlock() { Text = "Test AutoReverse with Repeat" };
WindowHelper.WindowContent = target;
await WindowHelper.WaitForIdle();
await WindowHelper.WaitForLoaded(target);

var transform = new TranslateTransform();
target.RenderTransform = transform;

var animation = new DoubleAnimation()
{
From = 0,
To = 50,
Duration = TimeSpan.FromMilliseconds(250),
AutoReverse = true,
RepeatBehavior = new RepeatBehavior(2), // Repeat twice (4 total half-cycles)
FillBehavior = FillBehavior.HoldEnd,
};
Storyboard.SetTarget(animation, transform);
Storyboard.SetTargetProperty(animation, nameof(TranslateTransform.X));

var storyboard = new Storyboard();
storyboard.Children.Add(animation);

bool completed = false;
storyboard.Completed += (s, e) => completed = true;

storyboard.Begin();

// Wait for completion (2 repeats * 2 phases * 250ms = 1000ms)
await WindowHelper.WaitFor(() => completed, timeoutMS: 2500);

// Final value should be back at start (0)
var finalValue = transform.X;
Assert.IsTrue(Math.Abs(finalValue) < 10,
$"Final value should be close to 0 after repeating with AutoReverse, got {finalValue}");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,7 @@ public double SpeedRatio
// Skipping already declared property FillBehavior
// Skipping already declared property Duration
// Skipping already declared property BeginTime
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
public bool AutoReverse
{
get
{
return (bool)this.GetValue(AutoReverseProperty);
}
set
{
this.SetValue(AutoReverseProperty, value);
}
}
#endif
// Skipping already declared property AutoReverse
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
public static bool AllowDependentAnimations
Expand All @@ -54,14 +41,7 @@ public static bool AllowDependentAnimations
}
}
#endif
#if __ANDROID__ || __IOS__ || __TVOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "__TVOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")]
public static global::Microsoft.UI.Xaml.DependencyProperty AutoReverseProperty { get; } =
Microsoft.UI.Xaml.DependencyProperty.Register(
nameof(AutoReverse), typeof(bool),
typeof(global::Microsoft.UI.Xaml.Media.Animation.Timeline),
new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(bool)));
#endif
// Skipping already declared property AutoReverseProperty
// Skipping already declared property BeginTimeProperty
// Skipping already declared property DurationProperty
// Skipping already declared property FillBehaviorProperty
Expand Down
28 changes: 26 additions & 2 deletions src/Uno.UI/UI/Xaml/Media/Animation/Timeline.animation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public static class TraceProvider
private int _replayCount = 1;
private T? _startingValue;
private T? _endValue;
private bool _isReversing = false; // Track if we're in the reverse phase of AutoReverse

// Initialize the field with zero capacity, as it may stay empty more often than it is being used.
private readonly CompositeDisposable _subscriptions = new CompositeDisposable(0);
Expand All @@ -61,6 +62,7 @@ private TimelineState State
private TimeSpan? BeginTime => _owner?.BeginTime;
private Duration Duration => _owner?.Duration ?? default(Duration);
private FillBehavior FillBehavior => _owner?.FillBehavior ?? default(FillBehavior);
private bool AutoReverse => _owner?.AutoReverse ?? false;

private T? From => AnimationOwner?.From;
private T? To => AnimationOwner?.To;
Expand Down Expand Up @@ -94,6 +96,7 @@ public void Begin()

_activeDuration.Restart();
_replayCount = 1;
_isReversing = false; // Reset reversing state when beginning

//Start the animation
Play();
Expand Down Expand Up @@ -192,7 +195,8 @@ public void SkipToFill()
}

// Set property to its final value
var value = ComputeToValue();
// With AutoReverse, the final value is the starting value (after reversing back)
var value = AutoReverse ? ComputeFromValue() : ComputeToValue();
SetValue(value);

OnEnd();
Expand Down Expand Up @@ -225,7 +229,11 @@ private void InitializeAnimator()

_endValue = ComputeToValue();

_animator = AnimatorFactory.Create(_owner, _startingValue.Value, _endValue.Value);
// If we're in the reverse phase, swap the from and to values
var fromValue = _isReversing ? _endValue.Value : _startingValue.Value;
var toValue = _isReversing ? _startingValue.Value : _endValue.Value;

_animator = AnimatorFactory.Create(_owner, fromValue, toValue);

_animator.SetEasingFunction(this.EasingFunction); //Set the Easing Function of the animator

Expand Down Expand Up @@ -337,6 +345,22 @@ private void OnEnd()
{
_animator?.Dispose();

// Handle AutoReverse: if enabled and we just finished the forward animation, reverse it
if (AutoReverse && !_isReversing)
{
_isReversing = true;
// Use Play() instead of Replay() to avoid incrementing _replayCount during the reverse phase.
// This ensures RepeatBehavior counts complete cycles (forward + reverse) as single iterations.
Play();
return;
}

// If we were reversing, we've now completed both forward and reverse
if (_isReversing)
{
_isReversing = false;
}

// If the animation was GPU based, remove the animated value

if (NeedsRepeat(_activeDuration, _replayCount))
Expand Down
9 changes: 9 additions & 0 deletions src/Uno.UI/UI/Xaml/Media/Animation/Timeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ public event EventHandler<object> Completed
remove => _completedHandlers?.Remove(value);
}

public bool AutoReverse
{
get => (bool)GetValue(AutoReverseProperty);
set => SetValue(AutoReverseProperty, value);
}

public static DependencyProperty AutoReverseProperty { get; } =
DependencyProperty.Register("AutoReverse", typeof(bool), typeof(Timeline), new FrameworkPropertyMetadata(false));

public Timeline()
{
IsAutoPropertyInheritanceEnabled = false;
Expand Down
Loading