From d8ed9630bda0a22ad60389aaf083e7273917474c Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Fri, 8 Apr 2016 12:26:03 -0600 Subject: [PATCH] Automatically marshal all AnimationExtensions calls onto UI thread (#48) --- .../Bugzilla39821.cs | 118 ++++++++++++++++ .../Xamarin.Forms.Controls.Issues.Shared.projitems | 1 + Xamarin.Forms.Controls/App.cs | 6 +- Xamarin.Forms.Core/AnimationExtensions.cs | 155 +++++++++++++-------- 4 files changed, 217 insertions(+), 63 deletions(-) create mode 100644 Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla39821.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 index 0000000..ab998d0 --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla39821.cs @@ -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>(); + + box.AnimateKinetic("kinetic", (distance, velocity) => + { + resultList.Add(new Tuple(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 diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems index e7afb08..c887494 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems @@ -96,6 +96,7 @@ + diff --git a/Xamarin.Forms.Controls/App.cs b/Xamarin.Forms.Controls/App.cs index d75b35a..31c356e 100644 --- a/Xamarin.Forms.Controls/App.cs +++ b/Xamarin.Forms.Controls/App.cs @@ -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(); InitInsights(); - // MainPage = new MainPageLifeCycleTests (); - MainPage = new MasterDetailPage { + //MainPage = new MainPageLifeCycleTests(); + MainPage = new MasterDetailPage + { Master = new ContentPage { Title = "Master", BackgroundColor = Color.Red }, Detail = CoreGallery.GetMainPage() }; diff --git a/Xamarin.Forms.Core/AnimationExtensions.cs b/Xamarin.Forms.Core/AnimationExtensions.cs index efc3e40..957d002 100644 --- a/Xamarin.Forms.Core/AnimationExtensions.cs +++ b/Xamarin.Forms.Core/AnimationExtensions.cs @@ -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 finished = null, @@ -67,8 +85,9 @@ namespace Xamarin.Forms self.Animate(name, x => x, callback, rate, length, easing, finished, repeat); } - public static void Animate(this IAnimatable self, string name, Func transform, Action callback, uint rate = 16, uint length = 250, Easing easing = null, - Action finished = null, Func repeat = null) + public static void Animate(this IAnimatable self, string name, Func transform, Action callback, + uint rate = 16, uint length = 250, Easing easing = null, + Action finished = null, Func 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 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 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(IAnimatable self, string name, Func transform, Action callback, + uint rate, uint length, Easing easing, Action finished, Func 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 callback, double velocity, double drag, Action finished = null) + static void AnimateKineticInternal(IAnimatable self, string name, Func 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 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; -- 2.7.4