Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

Commit

Permalink
Automatically marshal all AnimationExtensions calls onto UI thread (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
hartez authored and Jason Smith committed Apr 8, 2016
1 parent ad1c053 commit 3b63b22
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 63 deletions.
@@ -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
}
}
Expand Up @@ -91,6 +91,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" />
Expand Down
6 changes: 4 additions & 2 deletions Xamarin.Forms.Controls/App.cs
Expand Up @@ -3,6 +3,7 @@
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Xamarin.Forms.Controls.Issues;

namespace Xamarin.Forms.Controls
{
Expand All @@ -23,8 +24,9 @@ public App()
{
_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()
};
Expand Down
155 changes: 94 additions & 61 deletions Xamarin.Forms.Core/AnimationExtensions.cs
Expand Up @@ -42,11 +42,29 @@ static AnimationExtensions()

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,
Expand All @@ -67,8 +85,9 @@ public static bool AbortAnimation(this IAnimatable self, string handle)
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));
Expand All @@ -77,8 +96,75 @@ public static bool AbortAnimation(this IAnimatable self, string handle)
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);
Expand Down Expand Up @@ -107,19 +193,16 @@ public static bool AbortAnimation(this IAnimatable self, string handle)
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);

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;
Expand All @@ -142,56 +225,6 @@ public static void AnimateKinetic(this IAnimatable self, string name, Func<doubl
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;
Expand Down

0 comments on commit 3b63b22

Please sign in to comment.