Automatically marshal all AnimationExtensions calls onto UI thread (#48)
authorE.Z. Hart <hartez@users.noreply.github.com>
Fri, 8 Apr 2016 18:26:03 +0000 (12:26 -0600)
committerRui Marinho <me@ruimarinho.net>
Fri, 8 Apr 2016 18:26:03 +0000 (19:26 +0100)
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla39821.cs [new file with mode: 0644]
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
Xamarin.Forms.Controls/App.cs
Xamarin.Forms.Core/AnimationExtensions.cs

diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla39821.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla39821.cs
new file mode 100644 (file)
index 0000000..ab998d0
--- /dev/null
@@ -0,0 +1,118 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using Xamarin.Forms.CustomAttributes;
+
+#if UITEST
+using Xamarin.UITest;
+using NUnit.Framework;
+#endif
+
+namespace Xamarin.Forms.Controls.Issues
+{
+       [Preserve(AllMembers = true)]
+       [Issue(IssueTracker.Bugzilla, 39821, "ViewExtension.TranslateTo cannot be invoked on Main thread")]
+       public class Bugzilla39821 : TestContentPage 
+       {
+               protected override void Init()
+               {
+                       var box = new BoxView { BackgroundColor = Color.Blue, WidthRequest = 50, HeightRequest = 50, HorizontalOptions = LayoutOptions.Center };
+
+                       var instructions = new Label { Text = "Click the 'Animate' button to run animation on the box. If the animations complete without crashing, this test has passed." };
+
+                       var success = new Label { Text = "Success", IsVisible = false };
+
+                       var button = new Button() { Text = "Animate" };
+
+                       Content = new StackLayout
+                       {
+                               VerticalOptions = LayoutOptions.Fill,
+                               HorizontalOptions = LayoutOptions.Fill,
+                               Children =
+                               {
+                                       instructions,
+                                       success,
+                                       button,
+                                       new AbsoluteLayout
+                                       {
+                                               Children = { box },
+                                               HorizontalOptions = LayoutOptions.Fill,
+                                               VerticalOptions = LayoutOptions.Fill
+                                       }
+                               }
+                       };
+
+                       button.Clicked += async (sender, args) => {
+                               // Run a bunch of animations from the thread pool 
+                               await Task.WhenAll(
+                                       Task.Run(async () => await Translate(box)),
+                                       Task.Run(async () => await CheckTranslateRunning(box)),
+                                       Task.Run(async () => await AnimateScale(box)),
+                                       Task.Run(async () => await Rotate(box)),
+                                       Task.Run(async () => await Animate(box)),
+                                       Task.Run(async () => await Kinetic(box)),
+                                       Task.Run(async () => await Cancel(box))
+                                       );
+
+                               success.IsVisible = true;
+                       };
+               }
+
+               async Task CheckTranslateRunning(BoxView box)
+               {
+                       Debug.WriteLine(box.AnimationIsRunning("TranslateTo") ? "Translate is running" : "Translate is not running");
+               }
+
+               static async Task Translate(BoxView box)
+               {
+                       var currentX = box.X;
+                       var currentY = box.Y;
+
+                       await box.TranslateTo(currentX, currentY + 100);
+                       await box.TranslateTo(currentX, currentY);
+               }
+
+               static async Task AnimateScale(BoxView box)
+               {
+                       await box.ScaleTo(2);
+                       await box.ScaleTo(0.5);
+               }
+
+               static async Task Rotate(BoxView box)
+               {
+                       await box.RelRotateTo(360);
+               }
+
+               async Task Cancel(BoxView box)
+               {
+                       box.AbortAnimation("animate");
+                       box.AbortAnimation("kinetic");
+               }
+
+               async Task Animate(BoxView box)
+               {
+                       box.Animate("animate", d => d, d => { }, 100, 1);
+               }
+
+               async Task Kinetic(BoxView box)
+               {
+                       var resultList = new List<Tuple<double, double>>();
+
+                       box.AnimateKinetic("kinetic", (distance, velocity) =>
+                       {
+                               resultList.Add(new Tuple<double, double>(distance, velocity));
+                               return true;
+                       }, 100, 1);
+               }
+
+#if UITEST
+               [Test]
+               public void DoesNotCrash()
+               {
+                       RunningApp.Tap(q => q.Marked("Animate"));
+                       RunningApp.WaitForElement(q => q.Marked("Success"));
+               }
+#endif
+       }
+}
\ No newline at end of file
index e7afb08..c887494 100644 (file)
@@ -96,6 +96,7 @@
     </Compile>
     <Compile Include="$(MSBuildThisFileDirectory)Bugzilla39702.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Bugzilla40173.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)Bugzilla39821.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)CarouselAsync.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Bugzilla34561.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Bugzilla34727.cs" />
index d75b35a..31c356e 100644 (file)
@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Reflection;
 using System.Threading.Tasks;
+using Xamarin.Forms.Controls.Issues;
 
 namespace Xamarin.Forms.Controls
 {
@@ -23,8 +24,9 @@ namespace Xamarin.Forms.Controls
                {
                        _testCloudService = DependencyService.Get<ITestCloudService>();
                        InitInsights();
-                       // MainPage = new MainPageLifeCycleTests ();
-                       MainPage = new MasterDetailPage {
+                       //MainPage = new MainPageLifeCycleTests();
+                       MainPage = new MasterDetailPage
+                       {
                                Master = new ContentPage { Title = "Master", BackgroundColor = Color.Red },
                                Detail = CoreGallery.GetMainPage()
                        };
index efc3e40..957d002 100644 (file)
@@ -42,11 +42,29 @@ namespace Xamarin.Forms
 
                public static bool AbortAnimation(this IAnimatable self, string handle)
                {
-                       CheckAccess();
-
                        var key = new AnimatableKey(self, handle);
 
-                       return AbortAnimation(key) && AbortKinetic(key);
+                       if (!s_animations.ContainsKey(key) && !s_kinetics.ContainsKey(key))
+                       {
+                               return false;
+                       }
+
+                       Action abort = () =>
+                       {
+                               AbortAnimation(key);
+                               AbortKinetic(key);
+                       };
+
+                       if (Device.IsInvokeRequired)
+                       {
+                               Device.BeginInvokeOnMainThread(abort);
+                       }
+                       else
+                       {
+                               abort();
+                       }
+
+                       return true;
                }
 
                public static void Animate(this IAnimatable self, string name, Animation animation, uint rate = 16, uint length = 250, Easing easing = null, Action<double, bool> finished = null,
@@ -67,8 +85,9 @@ namespace Xamarin.Forms
                        self.Animate(name, x => x, callback, rate, length, easing, finished, repeat);
                }
 
-               public static void Animate<T>(this IAnimatable self, string name, Func<double, T> transform, Action<T> callback, uint rate = 16, uint length = 250, Easing easing = null,
-                                                                         Action<T, bool> finished = null, Func<bool> repeat = null)
+               public static void Animate<T>(this IAnimatable self, string name, Func<double, T> transform, Action<T> callback,
+                       uint rate = 16, uint length = 250, Easing easing = null,
+                       Action<T, bool> finished = null, Func<bool> repeat = null)
                {
                        if (transform == null)
                                throw new ArgumentNullException(nameof(transform));
@@ -77,8 +96,75 @@ namespace Xamarin.Forms
                        if (self == null)
                                throw new ArgumentNullException(nameof(self));
 
-                       CheckAccess();
+                       Action animate = () => AnimateInternal(self, name, transform, callback, rate, length, easing, finished, repeat);
+
+                       if (Device.IsInvokeRequired)
+                       {
+                               Device.BeginInvokeOnMainThread(animate);
+                       }
+                       else
+                       {
+                               animate();
+                       }
+               }
+
+
+               public static void AnimateKinetic(this IAnimatable self, string name, Func<double, double, bool> callback, double velocity, double drag, Action finished = null)
+               {
+                       Action animate = () => AnimateKineticInternal(self, name, callback, velocity, drag, finished);
+
+                       if (Device.IsInvokeRequired)
+                       {
+                               Device.BeginInvokeOnMainThread(animate);
+                       }
+                       else
+                       {
+                               animate();
+                       }
+               }
+
+               public static bool AnimationIsRunning(this IAnimatable self, string handle)
+               {
+                       var key = new AnimatableKey(self, handle);
+                       return s_animations.ContainsKey(key);
+               }
+
+               public static Func<double, double> Interpolate(double start, double end = 1.0f, double reverseVal = 0.0f, bool reverse = false)
+               {
+                       double target = reverse ? reverseVal : end;
+                       return x => start + (target - start) * x;
+               }
+
+               static void AbortAnimation(AnimatableKey key)
+               {
+                       if (!s_animations.ContainsKey(key))
+                       {
+                               return;
+                       }
+
+                       Info info = s_animations[key];
+                       info.Tweener.ValueUpdated -= HandleTweenerUpdated;
+                       info.Tweener.Finished -= HandleTweenerFinished;
+                       info.Tweener.Stop();
+                       info.Finished?.Invoke(1.0f, true);
+
+                       s_animations.Remove(key);
+               }
 
+               static void AbortKinetic(AnimatableKey key)
+               {
+                       if (!s_kinetics.ContainsKey(key))
+                       {
+                               return;
+                       }
+
+                       Ticker.Default.Remove(s_kinetics[key]);
+                       s_kinetics.Remove(key);
+               }
+
+               static void AnimateInternal<T>(IAnimatable self, string name, Func<double, T> transform, Action<T> callback,
+                       uint rate, uint length, Easing easing, Action<T, bool> finished, Func<bool> repeat)
+               {
                        var key = new AnimatableKey(self, name);
 
                        AbortAnimation(key);
@@ -107,10 +193,8 @@ namespace Xamarin.Forms
                        tweener.Start();
                }
 
-               public static void AnimateKinetic(this IAnimatable self, string name, Func<double, double, bool> callback, double velocity, double drag, Action finished = null)
+               static void AnimateKineticInternal(IAnimatable self, string name, Func<double, double, bool> callback, double velocity, double drag, Action finished = null)
                {
-                       CheckAccess();
-
                        var key = new AnimatableKey(self, name);
 
                        AbortKinetic(key);
@@ -118,8 +202,7 @@ namespace Xamarin.Forms
                        double sign = velocity / Math.Abs(velocity);
                        velocity = Math.Abs(velocity);
 
-                       int tick = Ticker.Default.Insert(step =>
-                       {
+                       int tick = Ticker.Default.Insert(step => {
                                long ms = step;
 
                                velocity -= drag * ms;
@@ -142,56 +225,6 @@ namespace Xamarin.Forms
                        s_kinetics[key] = tick;
                }
 
-               public static bool AnimationIsRunning(this IAnimatable self, string handle)
-               {
-                       CheckAccess();
-
-                       var key = new AnimatableKey(self, handle);
-
-                       return s_animations.ContainsKey(key);
-               }
-
-               public static Func<double, double> Interpolate(double start, double end = 1.0f, double reverseVal = 0.0f, bool reverse = false)
-               {
-                       double target = reverse ? reverseVal : end;
-                       return x => start + (target - start) * x;
-               }
-
-               static bool AbortAnimation(AnimatableKey key)
-               {
-                       if (!s_animations.ContainsKey(key))
-                       {
-                               return false;
-                       }
-
-                       Info info = s_animations[key];
-                       info.Tweener.ValueUpdated -= HandleTweenerUpdated;
-                       info.Tweener.Finished -= HandleTweenerFinished;
-                       info.Tweener.Stop();
-                       info.Finished?.Invoke(1.0f, true);
-
-                       return s_animations.Remove(key);
-               }
-
-               static bool AbortKinetic(AnimatableKey key)
-               {
-                       if (!s_kinetics.ContainsKey(key))
-                       {
-                               return false;
-                       }
-
-                       Ticker.Default.Remove(s_kinetics[key]);
-                       return s_kinetics.Remove(key);
-               }
-
-               static void CheckAccess()
-               {
-                       if (Device.IsInvokeRequired)
-                       {
-                               throw new InvalidOperationException("Animation operations must be invoked on the UI thread");
-                       }
-               }
-
                static void HandleTweenerFinished(object o, EventArgs args)
                {
                        var tweener = o as Tweener;