Automatically marshal all AnimationExtensions calls onto UI thread (#48)
This commit is contained in:
Родитель
e9f6556885
Коммит
d8ed9630bd
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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" />
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
Загрузка…
Ссылка в новой задаче