From 08aa54e16fe728e1d553bbe1f4930faab2801bc7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 May 2015 19:20:55 +0200 Subject: [PATCH] Added Transitions to TabControl. Currently only one transition available: CrossFadeTransition, which is demonstrated in the test app. --- Perspex.Animation/Animate.cs | 12 ++-- Perspex.Animation/Animation.cs | 62 ++++++++++++++++ Perspex.Animation/Perspex.Animation.csproj | 1 + Perspex.Controls/Deck.cs | 10 +++ Perspex.Controls/Presenters/DeckPresenter.cs | 33 +++++++-- Perspex.Controls/TabControl.cs | 10 +++ .../Animation/CrossFadeTransition.cs | 70 +++++++++++++++++++ .../Animation/IVisibilityTransition.cs | 15 ++++ Perspex.SceneGraph/Media/IDrawingContext.cs | 7 ++ Perspex.SceneGraph/Perspex.SceneGraph.csproj | 2 + Perspex.SceneGraph/Rendering/RendererBase.cs | 5 +- Perspex.SceneGraph/Visual.cs | 1 + Perspex.Themes.Default/DeckStyle.cs | 1 + Perspex.Themes.Default/TabControlStyle.cs | 3 + TestApplication/Program.cs | 1 + .../Perspex.Direct2D1/Media/DrawingContext.cs | 31 ++++++++ 16 files changed, 253 insertions(+), 11 deletions(-) create mode 100644 Perspex.Animation/Animation.cs create mode 100644 Perspex.SceneGraph/Animation/CrossFadeTransition.cs create mode 100644 Perspex.SceneGraph/Animation/IVisibilityTransition.cs 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. ///