diff --git a/Perspex.Animation/Animate.cs b/Perspex.Animation/Animate.cs
index 1deb8e3061..ecf8f4477f 100644
--- a/Perspex.Animation/Animate.cs
+++ b/Perspex.Animation/Animate.cs
@@ -96,8 +96,8 @@ namespace Perspex.Animation
/// The value of the property at the end of the animation.
/// The easing function to use.
/// The duration of the animation.
- /// An that can be used to stop the animation.
- public static IDisposable Property(
+ /// An that can be used to track or stop the animation.
+ public static Animation Property(
PerspexObject target,
PerspexProperty property,
object start,
@@ -106,7 +106,7 @@ namespace Perspex.Animation
TimeSpan duration)
{
var o = GetTimer(duration).Select(progress => easing.Ease(progress, start, finish));
- return target.Bind(property, o, BindingPriority.Animation);
+ return new Animation(o, target.Bind(property, o, BindingPriority.Animation));
}
///
@@ -119,8 +119,8 @@ namespace Perspex.Animation
/// The value of the property at the end of the animation.
/// The easing function to use.
/// The duration of the animation.
- /// An that can be used to stop the animation.
- public static IDisposable Property(
+ /// An that can be used to track or stop the animation.
+ public static Animation Property(
PerspexObject target,
PerspexProperty property,
T start,
@@ -129,7 +129,7 @@ namespace Perspex.Animation
TimeSpan duration)
{
var o = GetTimer(duration).Select(progress => easing.Ease(progress, start, finish));
- return target.Bind(property, o, BindingPriority.Animation);
+ return new Animation(o, target.Bind(property, o, BindingPriority.Animation));
}
}
}
diff --git a/Perspex.Animation/Animation.cs b/Perspex.Animation/Animation.cs
new file mode 100644
index 0000000000..2f4d8ffdc3
--- /dev/null
+++ b/Perspex.Animation/Animation.cs
@@ -0,0 +1,62 @@
+// -----------------------------------------------------------------------
+//
+// Copyright 2015 MIT Licence. See licence.md for more information.
+//
+// -----------------------------------------------------------------------
+
+namespace Perspex.Animation
+{
+ using System;
+
+ ///
+ /// Tracks the progress of an animation.
+ ///
+ public class Animation : IObservable, IDisposable
+ {
+ private IObservable inner;
+
+ private IDisposable subscription;
+
+ public Animation(IObservable inner, IDisposable subscription)
+ {
+ this.inner = inner;
+ this.subscription = subscription;
+ }
+
+ public void Dispose()
+ {
+ this.subscription.Dispose();
+ }
+
+ public IDisposable Subscribe(IObserver observer)
+ {
+ return this.inner.Subscribe(observer);
+ }
+ }
+
+ ///
+ /// Tracks the progress of an animation.
+ ///
+ public class Animation : IObservable, IDisposable
+ {
+ private IObservable inner;
+
+ private IDisposable subscription;
+
+ public Animation(IObservable inner, IDisposable subscription)
+ {
+ this.inner = inner;
+ this.subscription = subscription;
+ }
+
+ public void Dispose()
+ {
+ this.subscription.Dispose();
+ }
+
+ public IDisposable Subscribe(IObserver observer)
+ {
+ return this.inner.Subscribe(observer);
+ }
+ }
+}
diff --git a/Perspex.Animation/Perspex.Animation.csproj b/Perspex.Animation/Perspex.Animation.csproj
index a4f59c5059..d00dc42aa3 100644
--- a/Perspex.Animation/Perspex.Animation.csproj
+++ b/Perspex.Animation/Perspex.Animation.csproj
@@ -41,6 +41,7 @@
+
diff --git a/Perspex.Controls/Deck.cs b/Perspex.Controls/Deck.cs
index d6123ca32c..b40f7ba2c7 100644
--- a/Perspex.Controls/Deck.cs
+++ b/Perspex.Controls/Deck.cs
@@ -11,12 +11,16 @@ namespace Perspex.Controls
using Perspex.Controls.Primitives;
using Perspex.Controls.Utils;
using Perspex.Input;
+ using Perspex.Animation;
///
/// A selecting items control that displays a single item that fills the control.
///
public class Deck : SelectingItemsControl
{
+ public static readonly PerspexProperty TransitionProperty =
+ PerspexProperty.Register("Transition");
+
private static readonly ItemsPanelTemplate PanelTemplate =
new ItemsPanelTemplate(() => new Panel());
@@ -25,6 +29,12 @@ namespace Perspex.Controls
ItemsPanelProperty.OverrideDefaultValue(typeof(Deck), PanelTemplate);
}
+ public IVisibilityTransition Transition
+ {
+ get { return this.GetValue(TransitionProperty); }
+ set { this.SetValue(TransitionProperty, value); }
+ }
+
protected override ItemContainerGenerator CreateItemContainerGenerator()
{
return new TypedItemContainerGenerator(this);
diff --git a/Perspex.Controls/Presenters/DeckPresenter.cs b/Perspex.Controls/Presenters/DeckPresenter.cs
index c3fbe01544..71af424279 100644
--- a/Perspex.Controls/Presenters/DeckPresenter.cs
+++ b/Perspex.Controls/Presenters/DeckPresenter.cs
@@ -6,6 +6,7 @@
namespace Perspex.Controls.Presenters
{
+ using Perspex.Animation;
using Perspex.Controls.Generators;
using Perspex.Controls.Primitives;
using Perspex.Input;
@@ -13,7 +14,9 @@ namespace Perspex.Controls.Presenters
using System;
using System.Collections;
using System.Collections.Specialized;
+ using System.Linq;
using System.Reactive.Linq;
+ using System.Threading.Tasks;
public class DeckPresenter : Control, IVisual, IPresenter, ITemplatedControl
{
@@ -26,6 +29,9 @@ namespace Perspex.Controls.Presenters
public static readonly PerspexProperty SelectedItemProperty =
SelectingItemsControl.SelectedItemProperty.AddOwner();
+ public static readonly PerspexProperty TransitionProperty =
+ Deck.TransitionProperty.AddOwner();
+
private bool createdPanel;
public DeckPresenter()
@@ -63,6 +69,12 @@ namespace Perspex.Controls.Presenters
private set;
}
+ public IVisibilityTransition Transition
+ {
+ get { return this.GetValue(TransitionProperty); }
+ set { this.SetValue(TransitionProperty, value); }
+ }
+
public override sealed void ApplyTemplate()
{
if (!this.createdPanel)
@@ -106,21 +118,34 @@ namespace Perspex.Controls.Presenters
return this.ItemContainerGenerator;
}
- private void SelectedItemChanged(Tuple value)
+ private async void SelectedItemChanged(Tuple value)
{
if (this.createdPanel)
{
var generator = this.GetGenerator();
+ Control from = null;
+ Control to = null;
if (value.Item1 != null)
{
- generator.RemoveAll();
- this.Panel.Children.Clear();
+ from = generator.GetContainerForItem(value.Item1);
}
if (value.Item2 != null)
{
- this.Panel.Children.AddRange(generator.Generate(new[] { value.Item2 }));
+ to = generator.Generate(new[] { value.Item2 }).Single();
+ this.Panel.Children.Add(to);
+ }
+
+ if (this.Transition != null)
+ {
+ await this.Transition.Start(from, to);
+ }
+
+ if (from != null)
+ {
+ this.Panel.Children.Remove(from);
+ generator.Remove(new[] { value.Item1 });
}
}
}
diff --git a/Perspex.Controls/TabControl.cs b/Perspex.Controls/TabControl.cs
index c06ab9b037..8b39cd501a 100644
--- a/Perspex.Controls/TabControl.cs
+++ b/Perspex.Controls/TabControl.cs
@@ -15,6 +15,7 @@ namespace Perspex.Controls
using Perspex.Controls.Primitives;
using Perspex.Controls.Templates;
using Perspex.Input;
+ using Perspex.Animation;
public class TabControl : SelectingItemsControl, ILogical
{
@@ -24,6 +25,9 @@ namespace Perspex.Controls
public static readonly PerspexProperty SelectedTabProperty =
PerspexProperty.Register("SelectedTab");
+ public static readonly PerspexProperty TransitionProperty =
+ Deck.TransitionProperty.AddOwner();
+
private PerspexReadOnlyListView logicalChildren =
new PerspexReadOnlyListView();
@@ -56,6 +60,12 @@ namespace Perspex.Controls
set { this.SetValue(SelectedTabProperty, value); }
}
+ public IVisibilityTransition Transition
+ {
+ get { return this.GetValue(TransitionProperty); }
+ set { this.SetValue(TransitionProperty, value); }
+ }
+
IPerspexReadOnlyList ILogical.LogicalChildren
{
get { return this.logicalChildren; }
diff --git a/Perspex.SceneGraph/Animation/CrossFadeTransition.cs b/Perspex.SceneGraph/Animation/CrossFadeTransition.cs
new file mode 100644
index 0000000000..f61145da67
--- /dev/null
+++ b/Perspex.SceneGraph/Animation/CrossFadeTransition.cs
@@ -0,0 +1,70 @@
+// -----------------------------------------------------------------------
+//
+// Copyright 2015 MIT Licence. See licence.md for more information.
+//
+// -----------------------------------------------------------------------
+
+namespace Perspex.Animation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Reactive.Linq;
+ using System.Reactive.Threading.Tasks;
+ using System.Threading.Tasks;
+
+ public class CrossFadeTransition : IVisibilityTransition
+ {
+ public CrossFadeTransition(TimeSpan duration)
+ {
+ this.Duration = duration;
+ }
+
+ public TimeSpan Duration { get; }
+
+ public async Task Start(Visual from, Visual to)
+ {
+ var tasks = new List();
+
+ if (to != null)
+ {
+ to.Opacity = 0;
+ }
+
+ if (from != null)
+ {
+ tasks.Add(Animate.Property(
+ from,
+ Visual.OpacityProperty,
+ from.Opacity,
+ 0,
+ LinearEasing.For(),
+ this.Duration).ToTask());
+ }
+
+ if (to != null)
+ {
+ to.Opacity = 0;
+ to.IsVisible = true;
+
+ tasks.Add(Animate.Property(
+ to,
+ Visual.OpacityProperty,
+ 0,
+ 1,
+ LinearEasing.For(),
+ this.Duration).ToTask());
+
+ }
+
+ await Task.WhenAll(tasks.ToArray());
+
+ if (from != null)
+ {
+ from.IsVisible = false;
+ from.Opacity = 1;
+ }
+
+ to.Opacity = 1;
+ }
+ }
+}
diff --git a/Perspex.SceneGraph/Animation/IVisibilityTransition.cs b/Perspex.SceneGraph/Animation/IVisibilityTransition.cs
new file mode 100644
index 0000000000..7e7e0a46af
--- /dev/null
+++ b/Perspex.SceneGraph/Animation/IVisibilityTransition.cs
@@ -0,0 +1,15 @@
+// -----------------------------------------------------------------------
+//
+// Copyright 2015 MIT Licence. See licence.md for more information.
+//
+// -----------------------------------------------------------------------
+
+namespace Perspex.Animation
+{
+ using System.Threading.Tasks;
+
+ public interface IVisibilityTransition
+ {
+ Task Start(Visual from, Visual to);
+ }
+}
diff --git a/Perspex.SceneGraph/Media/IDrawingContext.cs b/Perspex.SceneGraph/Media/IDrawingContext.cs
index 018812d7c1..03be931167 100644
--- a/Perspex.SceneGraph/Media/IDrawingContext.cs
+++ b/Perspex.SceneGraph/Media/IDrawingContext.cs
@@ -63,6 +63,13 @@ namespace Perspex.Media
/// A disposable used to undo the clip rectangle.
IDisposable PushClip(Rect clip);
+ ///
+ /// Pushes an opacity value.
+ ///
+ /// The opacity.
+ /// A disposable used to undo the opacity.
+ IDisposable PushOpacity(double opacity);
+
///
/// Pushes a matrix transformation.
///
diff --git a/Perspex.SceneGraph/Perspex.SceneGraph.csproj b/Perspex.SceneGraph/Perspex.SceneGraph.csproj
index e2699ea80f..bc0c481581 100644
--- a/Perspex.SceneGraph/Perspex.SceneGraph.csproj
+++ b/Perspex.SceneGraph/Perspex.SceneGraph.csproj
@@ -45,6 +45,8 @@
+
+
diff --git a/Perspex.SceneGraph/Rendering/RendererBase.cs b/Perspex.SceneGraph/Rendering/RendererBase.cs
index 923f6d57fb..e032f61826 100644
--- a/Perspex.SceneGraph/Rendering/RendererBase.cs
+++ b/Perspex.SceneGraph/Rendering/RendererBase.cs
@@ -64,7 +64,9 @@ namespace Perspex.Rendering
/// The drawing context.
protected virtual void Render(IVisual visual, IDrawingContext context, Matrix translation, Matrix transform)
{
- if (visual.IsVisible && visual.Opacity > 0)
+ var opacity = visual.Opacity;
+
+ if (visual.IsVisible && opacity > 0)
{
// Translate any existing transform into this controls coordinate system.
Matrix offset = Matrix.Translation(visual.Bounds.Position);
@@ -84,6 +86,7 @@ namespace Perspex.Rendering
var m = transform * translation;
var d = context.PushTransform(m);
+ using (context.PushOpacity(opacity))
using (visual.ClipToBounds ? context.PushClip(visual.Bounds) : null)
{
visual.Render(context);
diff --git a/Perspex.SceneGraph/Visual.cs b/Perspex.SceneGraph/Visual.cs
index c67698fdb9..c70b1b5d73 100644
--- a/Perspex.SceneGraph/Visual.cs
+++ b/Perspex.SceneGraph/Visual.cs
@@ -49,6 +49,7 @@ namespace Perspex
static Visual()
{
AffectsRender(IsVisibleProperty);
+ AffectsRender(OpacityProperty);
RenderTransformProperty.Changed.Subscribe(RenderTransformChanged);
}
diff --git a/Perspex.Themes.Default/DeckStyle.cs b/Perspex.Themes.Default/DeckStyle.cs
index b932aeb72f..8be76a1a14 100644
--- a/Perspex.Themes.Default/DeckStyle.cs
+++ b/Perspex.Themes.Default/DeckStyle.cs
@@ -35,6 +35,7 @@ namespace Perspex.Themes.Default
[~ItemsPresenter.ItemsProperty] = control[~Deck.ItemsProperty],
[~ItemsPresenter.ItemsPanelProperty] = control[~Deck.ItemsPanelProperty],
[~DeckPresenter.SelectedItemProperty] = control[~Deck.SelectedItemProperty],
+ [~DeckPresenter.TransitionProperty] = control[~Deck.TransitionProperty],
};
}
}
diff --git a/Perspex.Themes.Default/TabControlStyle.cs b/Perspex.Themes.Default/TabControlStyle.cs
index 7e4b8edb95..098ecc7771 100644
--- a/Perspex.Themes.Default/TabControlStyle.cs
+++ b/Perspex.Themes.Default/TabControlStyle.cs
@@ -13,6 +13,8 @@ namespace Perspex.Themes.Default
using Perspex.Controls.Primitives;
using Perspex.Styling;
using Perspex.Controls.Templates;
+ using Perspex.Animation;
+ using System;
public class TabControlStyle : Styles
{
@@ -56,6 +58,7 @@ namespace Perspex.Themes.Default
},
[~Deck.ItemsProperty] = control[~TabControl.ItemsProperty],
[!Deck.SelectedItemProperty] = control[!TabControl.SelectedItemProperty],
+ [~Deck.TransitionProperty] = control[~TabControl.TransitionProperty],
[Grid.RowProperty] = 1,
}
}
diff --git a/TestApplication/Program.cs b/TestApplication/Program.cs
index 33c98a9b77..06993479ff 100644
--- a/TestApplication/Program.cs
+++ b/TestApplication/Program.cs
@@ -155,6 +155,7 @@ namespace TestApplication
LayoutTab(),
AnimationsTab(),
},
+ Transition = new CrossFadeTransition(TimeSpan.FromSeconds(0.25)),
[Grid.ColumnSpanProperty] = 2,
},
(fps = new TextBlock
diff --git a/Windows/Perspex.Direct2D1/Media/DrawingContext.cs b/Windows/Perspex.Direct2D1/Media/DrawingContext.cs
index bbbc81266c..98fc237d1f 100644
--- a/Windows/Perspex.Direct2D1/Media/DrawingContext.cs
+++ b/Windows/Perspex.Direct2D1/Media/DrawingContext.cs
@@ -191,6 +191,37 @@ namespace Perspex.Direct2D1.Media
});
}
+ ///
+ /// Pushes an opacity value.
+ ///
+ /// The opacity.
+ /// A disposable used to undo the opacity.
+ public IDisposable PushOpacity(double opacity)
+ {
+ if (opacity < 1)
+ {
+ var parameters = new LayerParameters
+ {
+ ContentBounds = RectangleF.Infinite,
+ MaskTransform = Matrix3x2.Identity,
+ Opacity = (float)opacity,
+ };
+
+ var layer = new Layer(this.renderTarget);
+
+ this.renderTarget.PushLayer(ref parameters, layer);
+
+ return Disposable.Create(() =>
+ {
+ this.renderTarget.PopLayer();
+ });
+ }
+ else
+ {
+ return Disposable.Empty;
+ }
+ }
+
///
/// Pushes a matrix transformation.
///