diff --git a/src/Uno.UI/FeatureConfiguration.cs b/src/Uno.UI/FeatureConfiguration.cs index 78bb824b5d20..76fbdf4cd5c2 100644 --- a/src/Uno.UI/FeatureConfiguration.cs +++ b/src/Uno.UI/FeatureConfiguration.cs @@ -493,6 +493,16 @@ public static class UIElement #endif } + public static class VisualState + { + /// + /// When this is set, the will be applied synchronously when changing state, + /// unlike UWP which waits the for the end of the (if any) to apply them. + /// + /// This flag is for backward compatibility with old versions of uno and should not be turned on. + public static bool ApplySettersBeforeTransition { get; set; } = false; + } + public static class WebView { #if __ANDROID__ diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Storyboard.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Storyboard.cs index 479f473a279f..bddfc74bf732 100644 --- a/src/Uno.UI/UI/Xaml/Media/Animation/Storyboard.cs +++ b/src/Uno.UI/UI/Xaml/Media/Animation/Storyboard.cs @@ -287,6 +287,7 @@ internal void TurnOverAnimationsTo(Storyboard storyboard) ((ITimeline)child).Stop(); } } + State = TimelineState.Stopped; } diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.cs index 08ae4a815a63..3de91c724dd1 100644 --- a/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.cs +++ b/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.cs @@ -24,7 +24,7 @@ public Timeline() State = TimelineState.Stopped; } - protected enum TimelineState + protected internal enum TimelineState { Active, Filling, @@ -46,7 +46,7 @@ protected string[] GetTraceProperties() /// An internally-used property which is essentially equivalent to , except that it /// distinguishes from . /// - protected TimelineState State { get; set; } + protected internal TimelineState State { get; set; } public TimeSpan? BeginTime { diff --git a/src/Uno.UI/UI/Xaml/VisualStateGroup.cs b/src/Uno.UI/UI/Xaml/VisualStateGroup.cs index ee06acd3b2ed..4b0266ac803f 100644 --- a/src/Uno.UI/UI/Xaml/VisualStateGroup.cs +++ b/src/Uno.UI/UI/Xaml/VisualStateGroup.cs @@ -30,6 +30,11 @@ public sealed partial class VisualStateGroup : DependencyObject /// The xaml scope in force at the time the VisualStateGroup was created. /// private readonly XamlScope _xamlScope; + private (VisualState state, VisualTransition transition) _current; + + public event VisualStateChangedEventHandler CurrentStateChanging; + + public event VisualStateChangedEventHandler CurrentStateChanged; public VisualStateGroup() { @@ -41,6 +46,8 @@ public VisualStateGroup() this.RegisterParentChangedCallback(this, OnParentChanged); } + public VisualState CurrentState => _current.state; + public string Name { get; set; } #region States Dependency Property @@ -67,7 +74,6 @@ public IList States internal set { this.SetValue(StatesProperty, value); } } - // Using a DependencyProperty as the backing store for States. This enables animation, styling, binding, etc... public static DependencyProperty StatesProperty { get; } = DependencyProperty.Register( "States", @@ -101,16 +107,13 @@ public IList Transitions internal set { this.SetValue(TransitionsProperty, value); } } - // Using a DependencyProperty as the backing store for Transitions. This enables animation, styling, binding, etc... public static DependencyProperty TransitionsProperty { get; } = DependencyProperty.Register( "Transitions", typeof(IList), typeof(VisualStateGroup), new FrameworkPropertyMetadata( - defaultValue: null, - propertyChangedCallback: (s, e) => ((VisualStateGroup)s)?.OnTransitionsChanged(e) - ) + defaultValue: null) ); #endregion @@ -137,17 +140,11 @@ private void VisualStateChanged(object sender, IVectorChangedEventArgs e) RefreshStateTriggers(); } - //Adds Event Handlers when collections changed - private void OnTransitionsChanged(DependencyPropertyChangedEventArgs e) + private void OnParentChanged(object instance, object key, DependencyObjectParentChangedEventArgs args) { + RefreshStateTriggers(force: true); } - public VisualState CurrentState { get; internal set; } - - public event VisualStateChangedEventHandler CurrentStateChanging; - - public event VisualStateChangedEventHandler CurrentStateChanged; - internal void RaiseCurrentStateChanging(VisualState oldState, VisualState newState) { if (this.CurrentStateChanging == null) @@ -175,80 +172,60 @@ private Control FindFirstAncestorControl() return (this.GetParent() as FrameworkElement)?.FindFirstParent(); } - internal void GoToState(IFrameworkElement element, VisualState state, VisualState originalState, bool useTransitions, Action onStateChanged) + internal void GoToState( + IFrameworkElement element, + VisualState state, + bool useTransitions, + Action onStateChanged) { + global::System.Diagnostics.Debug.Assert(state is null || States.Contains(state)); + if (this.Log().IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) { this.Log().DebugFormat("Go to state [{0}/{1}] on [{2}]", Name, state?.Name, element); } - var transition = FindTransition(originalState?.Name, state?.Name); - - EventHandler onComplete = null; - - onComplete = (s, a) => + var current = _current; + var target = (state, transition: FindTransition(current.state?.Name, state?.Name)); + + // Stops running animations (transition or state's storyboard) + // Note about animations (as of 2021-08-16 win 19043): + // Any "running animation", either from the current transition or the current state's storyboard, + // is being "paused" for properties that are going to be animated by the "next animation" (again transition or target state's storyboard), + // and rollbacked for properties that won't be animated anymore. + var runningAnimation = current.transition?.Storyboard is { } currentTransition + && currentTransition.State != Timeline.TimelineState.Stopped + ? currentTransition + : current.state?.Storyboard; + var nextAnimation = target.transition?.Storyboard ?? target.state?.Storyboard; + if (runningAnimation != null) { - onStateChanged(); - - if (state?.Storyboard == null) + if(nextAnimation is null) { - return; + runningAnimation.Stop(); } - - state.Storyboard.Completed -= onComplete; - }; - - EventHandler onTransitionComplete = null; - - onTransitionComplete = (s, a) => - { - if (transition?.Storyboard != null && useTransitions) - { - transition.Storyboard.Completed -= onTransitionComplete; - - if (state?.Storyboard != null) - { - transition.Storyboard.TurnOverAnimationsTo(state.Storyboard); - } - } - - //Starts Storyboard Animation - if (state?.Storyboard == null) - { - onComplete(this, null); - } - else if (state != null) + else { - state.Storyboard.Completed += onComplete; - state.Storyboard.Begin(); + runningAnimation.TurnOverAnimationsTo(nextAnimation); } - }; + } - //Stops Previous Storyboard Animation - if (originalState != null) + // Rollback setters that won't be re-set by the target state setters + // Note about setters and transition (as of 2021-08-16 win 19043): + // * if current and target state have setters for the same property, + // the value of the current is kept as is and updated only once at the end of the transition + // * if current has a setter which is not updated by the target state + // the value is rollbacked before the transition + // * if the target has a setter for a property that was not affected by the current + // the value is applied only at the end of the transition + if (current.state is {} currentState) { - if (originalState.Storyboard != null) - { - if (transition?.Storyboard != null) - { - originalState.Storyboard.TurnOverAnimationsTo(transition.Storyboard); - } - else if (state?.Storyboard != null) - { - originalState.Storyboard.TurnOverAnimationsTo(state.Storyboard); - } - else - { - originalState.Storyboard.Stop(); - } - } - - foreach (var setter in this.CurrentState.Setters.OfType()) + foreach (var setter in currentState.Setters.OfType()) { - if (element != null && (state?.Setters.OfType().Any(o => o.HasSameTarget(setter, DependencyPropertyValuePrecedences.Animations, element)) ?? false)) + if (element != null && (target.state?.Setters.OfType().Any(o => o.HasSameTarget(setter, DependencyPropertyValuePrecedences.Animations, element)) ?? false)) { - // PERF: We clear the value of the current setter only if there isn't any setter in the target state - // which changes the same target property. + // We clear the value of the current setter only if there isn't any setter in the target state + // which changes the same target property (for perf ... and UWP behavior support regarding transition animation). if (this.Log().IsEnabled(LogLevel.Debug)) { @@ -262,78 +239,136 @@ internal void GoToState(IFrameworkElement element, VisualState state, VisualStat } } -#if !HAS_EXPENSIVE_TRYFINALLY - try -#endif + _current = target; + + // For backward compatibility, we may apply the setters before the end of the transition. + if (FeatureConfiguration.VisualState.ApplySettersBeforeTransition) + { + ApplyTargetStateSetters(); + } + + // Finally effectively apply the target state! + if (useTransitions && target.transition?.Storyboard is { } transitionAnimation) { - ResourceResolver.PushNewScope(_xamlScope); + // Note: As of 2021-08-16 win 19043, if the transitionAnimation is Repeat=Forever, we actually never apply the state! - this.CurrentState = state; - if (this.CurrentState != null && element != null) + transitionAnimation.Completed += OnTransitionCompleted; + transitionAnimation.Begin(); + + void OnTransitionCompleted(object s, object a) { - foreach (var setter in this.CurrentState.Setters.OfType()) + transitionAnimation.Completed -= OnTransitionCompleted; + + if (target.state?.Storyboard is { } stateAnimation) { - setter.ApplyValue(DependencyPropertyValuePrecedences.Animations, element); + transitionAnimation.TurnOverAnimationsTo(stateAnimation); } + + ApplyTargetState(); } + } + else + { + ApplyTargetState(); + } - if (transition?.Storyboard == null || !useTransitions) + void ApplyTargetState() + { + // Apply target state setters (the right time to do it!) + if (!FeatureConfiguration.VisualState.ApplySettersBeforeTransition) { - onTransitionComplete(this, null); + ApplyTargetStateSetters(); + } + + // Starts target state animation + if (target.state?.Storyboard is { } stateAnimation) + { + stateAnimation.Completed += OnStateStoryboardCompleted; + stateAnimation.Begin(); + + void OnStateStoryboardCompleted(object s, object a) + { + state.Storyboard.Completed -= OnStateStoryboardCompleted; + onStateChanged(); + } } else { - transition.Storyboard.Completed += onTransitionComplete; - transition.Storyboard.Begin(); + onStateChanged(); } } + + void ApplyTargetStateSetters() + { + if (target.state is null || element is null) + { + return; + } + #if !HAS_EXPENSIVE_TRYFINALLY - finally + try #endif - { - ResourceResolver.PopScope(); + { + ResourceResolver.PushNewScope(_xamlScope); + + foreach (var setter in target.state.Setters.OfType()) + { + setter.ApplyValue(DependencyPropertyValuePrecedences.Animations, element); + } + } +#if !HAS_EXPENSIVE_TRYFINALLY + finally +#endif + { + ResourceResolver.PopScope(); + } } } private VisualTransition FindTransition(string oldStateName, string newStateName) { - if (oldStateName.IsNullOrEmpty() || newStateName.IsNullOrEmpty()) - { - return null; - } - - var perfectMatch = Transitions.FirstOrDefault(vt => - string.Equals(vt.From, oldStateName) && - string.Equals(vt.To, newStateName)); + var hasOld = oldStateName.HasValue(); + var hasNew = newStateName.HasValue(); - if (perfectMatch != null) + if (hasOld && hasNew && Transitions.FirstOrDefault(Match(oldStateName, newStateName)) is { } perfectMatch) { return perfectMatch; } - var fromMatch = Transitions.FirstOrDefault(vt => - string.Equals(vt.From, oldStateName) && - vt.To == null); - - if (fromMatch != null) + if (hasOld && Transitions.FirstOrDefault(Match(oldStateName, null)) is { } fromMatch) { return fromMatch; } - var toMatch = Transitions.FirstOrDefault(vt => - vt.From == null && - string.Equals(vt.To, newStateName)); + if (hasNew && Transitions.FirstOrDefault(Match(null, newStateName)) is { } newMatch) + { + return newMatch; + } + + return default; - return toMatch; + Func Match(string from, string to) + => tr => string.Equals(tr.From, oldStateName) && string.Equals(tr.To, newStateName); } internal void RefreshStateTriggers(bool force = false) { var newState = GetActiveTrigger(); var oldState = CurrentState; - if (!force && newState == oldState) + if (newState == oldState) { - return; + if (!force) + { + return; + } + else if (newState is null) + { + // The 'force' has no effect is both old and new states are 'null' + // (setting the parent for the first time in control's init) + // we however raise the state changed for backward compatibility. + OnStateChanged(); + return; + } } if (this.Log().IsEnabled(LogLevel.Debug)) @@ -347,13 +382,9 @@ void OnStateChanged() } var parent = this.GetParent() as IFrameworkElement; - GoToState(parent, newState, CurrentState, false, OnStateChanged); + GoToState(parent, newState, false, OnStateChanged); } - private void OnParentChanged(object instance, object key, DependencyObjectParentChangedEventArgs args) - { - RefreshStateTriggers(force: true); - } /// /// This method is not using LINQ for performance considerations. @@ -416,6 +447,7 @@ private VisualState GetActiveTrigger() return winnerState; } - public override string ToString() => Name ?? $""; + public override string ToString() + => Name ?? $""; } } diff --git a/src/Uno.UI/UI/Xaml/VisualStateManager.cs b/src/Uno.UI/UI/Xaml/VisualStateManager.cs index be9d4d50c4a4..18ef45317631 100644 --- a/src/Uno.UI/UI/Xaml/VisualStateManager.cs +++ b/src/Uno.UI/UI/Xaml/VisualStateManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using Microsoft.Extensions.Logging; using Uno.Extensions; using Uno.Logging; using Uno.Diagnostics.Eventing; @@ -11,11 +12,12 @@ namespace Windows.UI.Xaml { public partial class VisualStateManager : DependencyObject { - private readonly static IEventProvider _trace = Tracing.Get(TraceProvider.Id); + private static readonly IEventProvider _trace = Tracing.Get(TraceProvider.Id); + private static readonly ILogger _log = typeof(VisualStateManager).Log(); public static class TraceProvider { - public readonly static Guid Id = Guid.Parse("{2F38E5F4-90A2-4872-BD49-3696F897BAD1}"); + public static readonly Guid Id = Guid.Parse("{2F38E5F4-90A2-4872-BD49-3696F897BAD1}"); public const int StoryBoard_GoToState = 1; } @@ -37,8 +39,7 @@ public static void SetVisualStateGroups(FrameworkElement obj, IList), @@ -92,7 +93,7 @@ internal static void SetVisualStateManager(IFrameworkElement obj, VisualStateMan obj.SetValue(VisualStateManagerProperty, value); } - internal static DependencyProperty VisualStateManagerProperty { get ; } = + internal static DependencyProperty VisualStateManagerProperty { get; } = DependencyProperty.RegisterAttached("VisualStateManager", typeof(VisualStateManager), typeof(VisualStateManager), new FrameworkPropertyMetadata(null)); #endregion @@ -100,12 +101,11 @@ internal static void SetVisualStateManager(IFrameworkElement obj, VisualStateMan public static bool GoToState(Control control, string stateName, bool useTransitions) { var templateRoot = control.GetTemplateRoot(); - - if (templateRoot == null) + if (templateRoot is null) { - if (typeof(VisualStateManager).Log().IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + if (_log.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) { - typeof(VisualStateManager).Log().DebugFormat("Failed to set state [{0}], unable to find template root on [{1}]", stateName, control); + _log.DebugFormat("Failed to set state [{0}], unable to find template root on [{1}]", stateName, control); } return false; @@ -115,9 +115,9 @@ public static bool GoToState(Control control, string stateName, bool useTransiti { if (fe.GoToElementState(stateName, useTransitions)) { - if (typeof(VisualStateManager).Log().IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + if (_log.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) { - typeof(VisualStateManager).Log().DebugFormat($"GoToElementStateCore({stateName}) override on [{control}]"); + _log.DebugFormat($"GoToElementStateCore({stateName}) override on [{control}]"); } return true; @@ -125,12 +125,11 @@ public static bool GoToState(Control control, string stateName, bool useTransiti } var groups = GetVisualStateGroups(templateRoot); - - if (groups == null) + if (groups is null) { - if (typeof(VisualStateManager).Log().IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + if (_log.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) { - typeof(VisualStateManager).Log().DebugFormat("Failed to set state [{0}], no visual state group on [{1}]", stateName, control); + _log.DebugFormat("Failed to set state [{0}], no visual state group on [{1}]", stateName, control); } return false; @@ -138,54 +137,42 @@ public static bool GoToState(Control control, string stateName, bool useTransiti // Get all the groups with a state that matches the state name var (group, state) = GetValidGroupAndState(stateName, groups); - - if (group == null) + if (group is null) { - if (typeof(VisualStateManager).Log().IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + if (_log.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) { - typeof(VisualStateManager).Log().DebugFormat("Failed to set state [{0}], there are no matching groups on [{1}]", stateName, control); + _log.DebugFormat("Failed to set state [{0}], there are no matching groups on [{1}]", stateName, control); } return false; } var vsm = GetVisualStateManager(control); - - if (vsm == null) + if (vsm is null) { - if (typeof(VisualStateManager).Log().IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + if (_log.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) { - typeof(VisualStateManager).Log().DebugFormat("Failed to set state [{0}], there is no VisualStateManagr on [{1}]", stateName, control); + _log.DebugFormat("Failed to set state [{0}], there is no VisualStateManagr on [{1}]", stateName, control); } return false; } - var output = vsm.GoToStateCore(control, templateRoot, stateName, group, state, useTransitions); + + var output = templateRoot is FrameworkElement fwRoot + ? vsm.GoToStateCore(control, fwRoot, stateName, group, state, useTransitions) + : vsm.GoToStateCorePrivateBaseImplementation(control, group, state, useTransitions); // For backward compatibility! + #if __WASM__ TryAssignDOMVisualStates(groups, templateRoot); #endif return output; } - private static (VisualStateGroup, VisualState) GetValidGroupAndState(string stateName, IList groups) - { - foreach (var group in groups) - { - foreach (var state in group.States) - { - if (state.Name?.Equals(stateName) ?? false) - { - return (group, state); - } - } - } + protected virtual bool GoToStateCore(Control control, FrameworkElement templateRoot, string stateName, VisualStateGroup group, VisualState state, bool useTransitions) + => GoToStateCorePrivateBaseImplementation(control, group, state, useTransitions); - return (null, null); - } - - protected virtual bool GoToStateCore(Control control, FrameworkElement templateRoot, string stateName, VisualStateGroup group, VisualState state, bool useTransitions) => GoToStateCore(control, (IFrameworkElement) templateRoot, stateName, group, state, useTransitions); - private bool GoToStateCore(Control control, IFrameworkElement templateRoot, string stateName, VisualStateGroup group, VisualState state, bool useTransitions) + private bool GoToStateCorePrivateBaseImplementation(Control control, VisualStateGroup group, VisualState state, bool useTransitions) { #if IS_UNO if (_trace.IsEnabled) @@ -196,7 +183,7 @@ private bool GoToStateCore(Control control, IFrameworkElement templateRoot, stri new[] { control.GetType()?.ToString(), control?.GetDependencyObjectId().ToString(), - stateName, + state.Name, useTransitions ? "UseTransitions" : "NoTransitions" } ); @@ -204,8 +191,7 @@ private bool GoToStateCore(Control control, IFrameworkElement templateRoot, stri #endif var originalState = group.CurrentState; - - if (VisualState.Equals(originalState, state)) + if (object.Equals(originalState, state)) { // Already in the right state return true; @@ -220,13 +206,10 @@ private bool GoToStateCore(Control control, IFrameworkElement templateRoot, stri group.GoToState( control, state, - originalState, useTransitions, () => { - var innerControl = wr?.Target as Control; - - if (innerControl != null) + if (wr?.Target is Control) { RaiseCurrentStateChanged(group, originalState, state); } @@ -270,5 +253,21 @@ internal static VisualState GetCurrentState(Control control, string groupName) return group?.CurrentState; } + + private static (VisualStateGroup, VisualState) GetValidGroupAndState(string stateName, IList groups) + { + foreach (var group in groups) + { + foreach (var state in group.States) + { + if (state.Name?.Equals(stateName) ?? false) + { + return (group, state); + } + } + } + + return (null, null); + } } }