diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/RefreshVisualizer/IRefreshVisualizerPrivate.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/RefreshVisualizer/IRefreshVisualizerPrivate.cs index 91111a65cced..37f6020d2c26 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/RefreshVisualizer/IRefreshVisualizerPrivate.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/RefreshVisualizer/IRefreshVisualizerPrivate.cs @@ -4,11 +4,11 @@ #nullable enable -using Windows.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls; namespace Microsoft.UI.Private.Controls { - public interface IRefreshVisualizerPrivate + internal partial interface IRefreshVisualizerPrivate { IRefreshInfoProvider InfoProvider { get; set; } diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/RefreshVisualizer/PullToRefreshHelperTestApi.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/RefreshVisualizer/PullToRefreshHelperTestApi.cs index 07ab8825c191..7e5b6cfd31d9 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/RefreshVisualizer/PullToRefreshHelperTestApi.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/RefreshVisualizer/PullToRefreshHelperTestApi.cs @@ -3,7 +3,7 @@ namespace Microsoft.UI.Private.Controls { - public static partial class PullToRefreshHelperTestApi + internal static partial class PullToRefreshHelperTestApi { public static RefreshInteractionRatioChangedEventArgs CreateRefreshInteractionRatioChangedEventArgsInstance(double value) { diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/IAdapterAnimationHandler.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/IAdapterAnimationHandler.cs new file mode 100644 index 000000000000..c2f507eadec4 --- /dev/null +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/IAdapterAnimationHandler.cs @@ -0,0 +1,14 @@ +using Windows.UI.Xaml; +using Windows.UI.Composition.Interactions; + +namespace Microsoft.UI.Private.Controls +{ + internal interface IAdapterAnimationHandler + { + void InteractionTrackerAnimation(UIElement refreshVisualizer, UIElement infoProvider, InteractionTracker interactionTracker); + + void RefreshRequestedAnimation(UIElement refreshVisualizer, UIElement infoProvider, double executionRatio); + + void RefreshCompletedAnimation(UIElement refreshVisualizer, UIElement infoProvider); + } +} diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/IRefreshInfoProviderAdapter.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/IRefreshInfoProviderAdapter.cs new file mode 100644 index 000000000000..cfb911d896ac --- /dev/null +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/IRefreshInfoProviderAdapter.cs @@ -0,0 +1,12 @@ +using Windows.Foundation; +using Windows.UI.Xaml; + +namespace Microsoft.UI.Private.Controls +{ + internal interface IRefreshInfoProviderAdapter + { + IRefreshInfoProvider AdaptFromTree(UIElement root, Size visualizerSize); + + void SetAnimations(UIElement refreshVisualizerAnimatableContainer); + } +} diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/RefreshInfoProviderImpl.Header.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/RefreshInfoProviderImpl.Header.cs new file mode 100644 index 000000000000..99af76a34ca1 --- /dev/null +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/RefreshInfoProviderImpl.Header.cs @@ -0,0 +1,27 @@ +#nullable enable + +using Microsoft.UI.Xaml.Controls; +using Windows.Foundation; +using Windows.UI.Composition; + +namespace Microsoft.UI.Private.Controls +{ + internal partial class RefreshInfoProviderImpl + { + private const double DEFAULT_EXECUTION_RATIO = 0.8; + + private RefreshPullDirection m_refreshPullDirection = RefreshPullDirection.TopToBottom; + private Size m_refreshVisualizerSize = new Size(1.0f, 1.0f); + private bool m_isInteractingForRefresh = false; + private int m_interactionRatioChangedCount = 0; + private CompositionPropertySet? m_compositionProperties = null; + private string m_interactionRatioCompositionProperty = "InteractionRatio"; + private double m_executionRatio = DEFAULT_EXECUTION_RATIO; + private bool m_peeking = false; + + event_source> m_IsInteractingForRefreshChangedEventSource { this }; + event_source> m_InteractionRatioChangedEventSource { this }; + event_source> m_RefreshStartedEventSource { this }; + event_source> m_RefreshCompletedEventSource { this }; + } +} diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/RefreshInfoProviderImpl.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/RefreshInfoProviderImpl.cs new file mode 100644 index 000000000000..95d16f259702 --- /dev/null +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/RefreshInfoProviderImpl.cs @@ -0,0 +1,245 @@ +using Microsoft.UI.Xaml.Controls; +using Windows.Foundation; +using Windows.UI.Composition; + +namespace Microsoft.UI.Private.Controls +{ + internal partial class RefreshInfoProviderImpl : IRefreshInfoProvider + { + // There is not a lot of value in antly firing the interaction ratio changed event as the + // animations which are based off of it use the published composition property set which is + // updated regularly. Instead we fire the event every 5th change to reduce overhead. + private const int RAISE_INTERACTION_RATIO_CHANGED_FREQUENCY = 5; + + // When the user is close to a threshold point we want to make sure that we always raise + // InteractionRatioChanged events so that we don't miss something important. + private const double ALWAYS_RAISE_INTERACTION_RATIO_TOLERANCE = 0.05; + + // This is our private implementation of the IRefreshInfoProvider interface. It is contructed by + // the ScrollViewerAdapter's Adapt method and returned as an instance of an IRefreshInfoProvider. + // It is an InteractionTrackerOwner, the corresponding InteractionTracker is maintained in the Adapter. + + public RefreshInfoProviderImpl() + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + } + + ~RefreshInfoProviderImpl() + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + } + + public RefreshInfoProviderImpl(RefreshPullDirection refreshPullDirection, Size refreshVisualizerSize, Compositor compositor) + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + m_refreshPullDirection = refreshPullDirection; + m_refreshVisualizerSize = refreshVisualizerSize; + m_compositionProperties = compositor.CreatePropertySet(); + } + + private void UpdateIsInteractingForRefresh(bool value) + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + bool isInteractingForRefresh = value && !m_peeking; + if (isInteractingForRefresh != m_isInteractingForRefresh) + { + m_isInteractingForRefresh = isInteractingForRefresh; + RaiseIsInteractingForRefreshChanged(); + } + } + + ///////////////////////////////////////////////////// + /////// IInteractionTrackerOwnerOverrides //////// + ///////////////////////////////////////////////////// + private void ValuesChanged(InteractionTracker sender, InteractionTrackerValuesChangedArgs args) + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH_FLT_FLT_FLT, METH_NAME, this, args.Position.X, args.Position.Y, args.Position.Z); + switch (m_refreshPullDirection) + { + case RefreshPullDirection.TopToBottom: + RaiseInteractionRatioChanged(m_refreshVisualizerSize.Height == 0 ? 1.0 : Math.Min(1.0, (double)-args.Position.Y / m_refreshVisualizerSize.Height)); + break; + case RefreshPullDirection.BottomToTop: + RaiseInteractionRatioChanged(m_refreshVisualizerSize.Height == 0 ? 1.0f : Math.Min(1.0, (double)args.Position.Y / m_refreshVisualizerSize.Height)); + break; + case RefreshPullDirection.LeftToRight: + RaiseInteractionRatioChanged(m_refreshVisualizerSize.Width == 0 ? 1.0f : Math.Min(1.0, (double)-args.Position.X / m_refreshVisualizerSize.Width)); + break; + case RefreshPullDirection.RightToLeft: + RaiseInteractionRatioChanged(m_refreshVisualizerSize.Width == 0 ? 1.0f : Math.Min(1.0, (double)args.Position.X / m_refreshVisualizerSize.Width)); + break; + default: + MUX_ASSERT(false); + break; + } + } + + private void RequestIgnored(InteractionTracker sender, InteractionTrackerRequestIgnoredArgs args) + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH_INT, METH_NAME, this, args.RequestId); + } + + private void InteractingStateEntered(InteractionTracker sender, InteractionTrackerInteractingStateEnteredArgs args) + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH_INT, METH_NAME, this, args.RequestId); + UpdateIsInteractingForRefresh(true); + } + + private void InertiaStateEntered(InteractionTracker sender, InteractionTrackerInertiaStateEnteredArgs args) + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH_INT, METH_NAME, this, args.RequestId()); + UpdateIsInteractingForRefresh(false); + } + + private void IdleStateEntered(InteractionTracker sender, InteractionTrackerIdleStateEnteredArgs args) + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH_INT, METH_NAME, this, args.RequestId()); + } + + private void CustomAnimationStateEntered(InteractionTracker sender, InteractionTrackerCustomAnimationStateEnteredArgs args) + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH_INT, METH_NAME, this, args.RequestId()); + } + + ///////////////////////////////////////////////////// + //////////// IRefreshInfoProvider //////////////// + ///////////////////////////////////////////////////// + private void OnRefreshStarted() + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + RaiseRefreshStarted(); + } + + private void OnRefreshCompleted() + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + RaiseRefreshCompleted(); + } + + event_token InteractionRatioChanged(TypedEventHandler& handler) + { + PTR_TRACE_INFO(null, TRACE_MSG_METH_STR, METH_NAME, this, "Add Handler"); + return m_InteractionRatioChangedEventSource.add(handler); + } + + private void InteractionRatioChanged(event_token& token) + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH_STR, METH_NAME, this, "Remove Handler"); + m_InteractionRatioChangedEventSource.remove(token); + } + + event_token IsInteractingForRefreshChanged(TypedEventHandler& handler) + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH_STR, METH_NAME, this, "Add Handler"); + return m_IsInteractingForRefreshChangedEventSource.add(handler); + } + + void IsInteractingForRefreshChanged(event_token& token) + { + PTR_TRACE_INFO(null, TRACE_MSG_METH_STR, METH_NAME, this, "Remove Handler"); + m_IsInteractingForRefreshChangedEventSource.remove(token); + } + + event_token RefreshStarted(TypedEventHandler& handler) + { + PTR_TRACE_INFO(null, TRACE_MSG_METH_STR, METH_NAME, this, "Add Handler"); + return m_RefreshStartedEventSource.add(handler); + } + + void RefreshStarted(event_token& token) + { + PTR_TRACE_INFO(null, TRACE_MSG_METH_STR, METH_NAME, this, "Remove Handler"); + m_RefreshStartedEventSource.remove(token); + } + + event_token RefreshCompleted(TypedEventHandler& handler) + { + PTR_TRACE_INFO(null, TRACE_MSG_METH_STR, METH_NAME, this, "Add Handler"); + return m_RefreshCompletedEventSource.add(handler); + } + + private void RefreshCompleted(event_token& token) + { + PTR_TRACE_INFO(null, TRACE_MSG_METH_STR, METH_NAME, this, "Remove Handler"); + m_RefreshCompletedEventSource.remove(token); + } + + private double ExecutionRatio() + { + return m_executionRatio; + } + + private string InteractionRatioCompositionProperty() + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + return m_interactionRatioCompositionProperty; + } + + private CompositionPropertySet CompositionProperties() + { + return m_compositionProperties; + } + + private bool IsInteractingForRefresh() + { + return m_isInteractingForRefresh; + } + + ///////////////////////////////////////////////////// + /////////// Private Helpers ///////////// + ///////////////////////////////////////////////////// + private void RaiseInteractionRatioChanged(double interactionRatio) + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH_DBL, METH_NAME, this, interactionRatio); + + m_compositionProperties.InsertScalar(m_interactionRatioCompositionProperty, (float)(interactionRatio)); + + if (m_interactionRatioChangedCount == 0 || AreClose(interactionRatio, 0.0) || AreClose(interactionRatio, m_executionRatio)) + { + if (m_InteractionRatioChangedEventSource) + { + var interactionRatioChangedArgs = new RefreshInteractionRatioChangedEventArgs(interactionRatio); + m_InteractionRatioChangedEventSource(this, interactionRatioChangedArgs); + } + m_interactionRatioChangedCount = 1; + } + else if (m_interactionRatioChangedCount >= RAISE_INTERACTION_RATIO_CHANGED_FREQUENCY) + { + m_interactionRatioChangedCount = 0; + } + else + { + m_interactionRatioChangedCount++; + } + } + + private bool AreClose(double interactionRatio, double target) + { + return Math.Abs(interactionRatio - target) < ALWAYS_RAISE_INTERACTION_RATIO_TOLERANCE; + } + + private void RaiseIsInteractingForRefreshChanged() + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + m_IsInteractingForRefreshChangedEventSource(this, null); + } + + private void RaiseRefreshStarted() + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + m_RefreshStartedEventSource(this, null); + } + + private void RaiseRefreshCompleted() + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + m_RefreshCompletedEventSource(this, null); + } + + private void SetPeekingMode(bool peeking) + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH_INT, METH_NAME, this, peeking); + m_peeking = peeking; + } + } +} diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/ScrollViewerIRefreshInfoProviderAdapter.Header.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/ScrollViewerIRefreshInfoProviderAdapter.Header.cs new file mode 100644 index 000000000000..405dbb6309ff --- /dev/null +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/ScrollViewerIRefreshInfoProviderAdapter.Header.cs @@ -0,0 +1,30 @@ +#nullable enable + +using Uno.Disposables; +using Windows.Foundation; +using Windows.UI.Composition.Interactions; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.UI.Private.Controls +{ + internal partial class ScrollViewerIRefreshInfoProviderAdapter + { + private RefreshInfoProviderImpl? m_infoProvider = null; + private IAdapterAnimationHandler m_animationHandler = null; + private ScrollViewer? m_scrollViewer = null; + private RefreshPullDirection m_refreshPullDirection = RefreshPullDirection.TopToBottom; + private InteractionTracker? m_interactionTracker = null; + private VisualInteractionSource? m_visualInteractionSource = null; + private bool m_visualInteractionSourceIsAttached = false + + private SerialDisposable m_scrollViewer_LoadedToken = new SerialDisposable(); + private SerialDisposable m_scrollViewer_PointerPressedToken = new SerialDisposable(); + private SerialDisposable m_scrollViewer_DirectManipulationCompletedToken = new SerialDisposable(); + private SerialDisposable m_scrollViewer_ViewChangingToken = new SerialDisposable(); + private SerialDisposable m_infoProvider_RefreshStartedToken = new SerialDisposable(); + private SerialDisposable m_infoProvider_RefreshCompletedToken = new SerialDisposable(); + + //tracker_ref m_boxedPointerPressedEventHandler { this }; + } +} diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/ScrollViewerIRefreshInfoProviderAdapter.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/ScrollViewerIRefreshInfoProviderAdapter.cs new file mode 100644 index 000000000000..8b98302f3bdd --- /dev/null +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/ScrollViewerIRefreshInfoProviderAdapter.cs @@ -0,0 +1,412 @@ +using Microsoft.UI.Xaml.Controls; +using Windows.Foundation; +using Windows.UI.Xaml; + +namespace Microsoft.UI.Private.Controls +{ + internal partial class ScrollViewerIRefreshInfoProviderAdapter : IRefreshInfoProviderAdapter + { + // The maximum initial scroll viewer offset allowed before PTR is disabled, and we enter the peeking state + private const float INITIAL_OFFSET_THRESHOLD = 1.0f; + + // The farthest down the tree we are willing to search for a SV in the adapt from tree method + private const int MAX_BFS_DEPTH = 10; + + // In the event that we call Refresh before having an implementation of IRefreshInfoProvider to tell us + // what value to use as the execution ratio, we use this value instead. + private const double FALLBACK_EXECUTION_RATIO = 0.8; + + // The ScrollViewerAdapter is responsible for creating an implementation of the IRefreshInfoProvider interface from a ScrollViewer. This + // Is accomplished by attaching an interaction tracker to the scrollViewer's content presenter and wiring the PTR functionality up to that. + ~ScrollViewerIRefreshInfoProviderAdapter() + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + if (m_scrollViewer != null) + { + CleanupScrollViewer(); + } + + if (m_infoProvider is { } IRefreshInfoProvider) + { + CleanupIRefreshInfoProvider(); + } + } + + public ScrollViewerIRefreshInfoProviderAdapter(RefreshPullDirection refreshPullDirection, IAdapterAnimationHandler animationHandler) + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + + m_refreshPullDirection = refreshPullDirection; + if (animationHandler != null) + { + m_animationHandler = animationHandler; + } + else + { + m_animationHandler = new ScrollViewerIRefreshInfoProviderDefaultAnimationHandler(null, m_refreshPullDirection).as< IAdapterAnimationHandler > (); + } + } + + IRefreshInfoProvider AdaptFromTree(UIElement & root, Size & refreshVisualizerSize) + { + PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + + UIElement & winrtRoot = root; + FxScrollViewer rootAsSV = winrtRoot as FxScrollViewer(); + int depth = 0; + if (rootAsSV) + { + return Adapt(winrtRoot.as< FxScrollViewer > (), refreshVisualizerSize); + } + else + { + while (depth < MAX_BFS_DEPTH) + { + FxScrollViewer helperResult = AdaptFromTreeRecursiveHelper(winrtRoot, depth); + if (helperResult) + { + return Adapt(helperResult, refreshVisualizerSize); + break; + } + depth++; + } + } + + return null; + } + + IRefreshInfoProvider Adapt(FxScrollViewer & adaptee, Size & refreshVisualizerSize) + { + PTR_TRACE_INFO(null, TRACE_MSG_METH_PTR, METH_NAME, this, adaptee); + if (!adaptee) + { + throw hresult_invalid_argument("Adaptee cannot be null."); + } + + if (m_scrollViewer) + { + CleanupScrollViewer(); + } + + if (m_infoProvider) + { + CleanupIRefreshInfoProvider(); + } + + m_infoProvider = null; + m_interactionTracker = null; + m_visualInteractionSource = null; + m_scrollViewer = adaptee; + + + if (!m_scrollViewer.Content()) + { + throw hresult_invalid_argument("Adaptee's content property cannot be null."); + } + + UIElement content = GetScrollContent(); + if (!content) + { + throw hresult_invalid_argument("Adaptee's content property must be a UIElement."); + } + + var contentParent = VisualTreeHelper.GetParent(content); + if (!contentParent) + { + //If the Content property does not have a parent this likely means the OnLoaded event of the SV has not fired yet. + //Attach to this event to finish the adaption. + m_scrollViewer_LoadedToken = m_scrollViewer.Loaded({ this, &ScrollViewerIRefreshInfoProviderAdapter.OnScrollViewerLoaded }); + } + else + { + OnScrollViewerLoaded(null, null); + UIElement contentParentAsUIElement = contentParent as UIElement(); + if (!contentParentAsUIElement) + { + throw hresult_invalid_argument("Adaptee's content's parent must be a UIElement."); + } + } + Visual contentVisual = ElementCompositionPreview.GetElementVisual(content); + Compositor compositor = contentVisual.Compositor(); + + m_infoProvider = make_self(m_refreshPullDirection, refreshVisualizerSize, compositor); + + m_infoProvider_RefreshStartedToken = m_infoProvider.RefreshStarted({ this, &ScrollViewerIRefreshInfoProviderAdapter.OnRefreshStarted }); + m_infoProvider_RefreshCompletedToken = m_infoProvider.RefreshCompleted({ this, &ScrollViewerIRefreshInfoProviderAdapter.OnRefreshCompleted }); + + m_interactionTracker = InteractionTracker.CreateWithOwner(compositor, m_infoProvider.as< IInteractionTrackerOwner > ()); + + m_interactionTracker.MinPosition(float3(0.0f)); + m_interactionTracker.MaxPosition(float3(0.0f)); + m_interactionTracker.MinScale(1.0f); + m_interactionTracker.MaxScale(1.0f); + + if (m_visualInteractionSource) + { + m_interactionTracker.InteractionSources().Add(m_visualInteractionSource); + m_visualInteractionSourceIsAttached = true; + } + + PointerEventHandler myEventHandler = + + [=](var sender, var args) + + { + PTR_TRACE_INFO(null, TRACE_MSG_METH, "ScrollViewer.PointerPressedHandler", this); + if (args.Pointer().PointerDeviceType() == PointerDeviceType.Touch && m_visualInteractionSource) + { + if (m_visualInteractionSourceIsAttached) + { + PointerPoint pp = args.GetCurrentPoint(null); + + if (pp) + { + bool tryRedirectForManipulationSuccessful = true; + + try + { + PTR_TRACE_INFO(null, TRACE_MSG_METH_METH, "ScrollViewer.PointerPressedHandler", this, "TryRedirectForManipulation"); + m_visualInteractionSource.TryRedirectForManipulation(pp); + } + catch (hresult_error&e) + { + // Swallowing Access Denied error because of InteractionTracker bug 17434718 which has been causing crashes at least in RS3, RS4 and RS5. + if (e.to_abi() != E_ACCESSDENIED) + { + throw; + } + + tryRedirectForManipulationSuccessful = false; + } + + if (tryRedirectForManipulationSuccessful) + { + m_infoProvider.SetPeekingMode(!IsWithinOffsetThreshold()); + } + } + } + else + { + throw hresult_invalid_argument("Invalid IRefreshInfoProvider adaptation of scroll viewer, this can occur when calling TryRedirectForManipulation to an unattached visual interaction source."); + } + } + }; + + m_boxedPointerPressedEventHandler = box_value(myEventHandler); + m_scrollViewer.AddHandler(UIElement.PointerPressedEvent(), m_boxedPointerPressedEventHandler, true /* handledEventsToo */); + m_scrollViewer_DirectManipulationCompletedToken = m_scrollViewer.DirectManipulationCompleted({ this, &ScrollViewerIRefreshInfoProviderAdapter.OnScrollViewerDirectManipulationCompleted }); + m_scrollViewer_ViewChangingToken = m_scrollViewer.ViewChanging({ this, &ScrollViewerIRefreshInfoProviderAdapter.OnScrollViewerViewChanging }); + + return m_infoProvider.as< IRefreshInfoProvider > (); + } + + void SetAnimations(UIElement &refreshVisualizerContainer) +{ + PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + if (!refreshVisualizerContainer) + { + throw hresult_invalid_argument("The refreshVisualizerContainer cannot be null."); + } + + m_animationHandler.InteractionTrackerAnimation(refreshVisualizerContainer, GetScrollContent(), m_interactionTracker); + } + + void OnRefreshStarted(object& /*sender*/, object& /*args*/) +{ + PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + var content = GetScrollContent(); + if (content) + { + content.CancelDirectManipulations(); + } + + double executionRatio = FALLBACK_EXECUTION_RATIO; + if (m_infoProvider) + { + executionRatio = m_infoProvider.ExecutionRatio(); + } + + m_animationHandler.RefreshRequestedAnimation(null, content, executionRatio); + } + + void OnRefreshCompleted(object& /*sender*/, object& /*args*/) +{ + PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + m_animationHandler.RefreshCompletedAnimation(null, GetScrollContent()); + } + + void OnScrollViewerLoaded(object& /*sender*/, object& /*args*/) +{ + PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + UIElement content = GetScrollContent(); + if (!content) + { + throw hresult_invalid_argument("Adaptee's content property must be a UIElement."); + } + + var contentParent = VisualTreeHelper.GetParent(content); + if (!contentParent) + { + throw hresult_invalid_argument("Adaptee cannot be null."); + } + + UIElement contentParentAsUIElement = contentParent as UIElement(); + if (!contentParentAsUIElement) + { + throw hresult_invalid_argument("Adaptee's content's parent must be a UIElement."); + } + + MakeInteractionSource(contentParentAsUIElement); + + m_scrollViewer.Loaded(m_scrollViewer_LoadedToken); + } + + void OnScrollViewerDirectManipulationCompleted(object& /*sender*/, object& /*args*/) +{ + PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + if (m_infoProvider) + { + m_infoProvider.UpdateIsInteractingForRefresh(false); + } + } + + void OnScrollViewerViewChanging(object& /*sender*/, Windows.UI.Xaml.Controls.ScrollViewerViewChangingEventArgs & args) +{ + if (m_infoProvider && m_infoProvider.IsInteractingForRefresh()) + { + PTR_TRACE_INFO(null, TRACE_MSG_METH_DBL_DBL, METH_NAME, this, args.FinalView().HorizontalOffset(), args.FinalView().VerticalOffset()); + if (!IsWithinOffsetThreshold()) + { + PTR_TRACE_INFO(null, TRACE_MSG_METH_STR, METH_NAME, this, "No longer interacting for refresh due to ScrollViewer view change."); + m_infoProvider.UpdateIsInteractingForRefresh(false); + } + } + } + + bool IsOrientationVertical() + { + return (m_refreshPullDirection == RefreshPullDirection.TopToBottom || m_refreshPullDirection == RefreshPullDirection.BottomToTop); + } + + UIElement GetScrollContent() + { + if (m_scrollViewer) + { + var content = m_scrollViewer.Content(); + return content as UIElement(); + } + return null; + } + + void MakeInteractionSource(UIElement&contentParent) +{ + PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + Visual contentParentVisual = ElementCompositionPreview.GetElementVisual(contentParent); + + m_visualInteractionSourceIsAttached = false; + m_visualInteractionSource = VisualInteractionSource.Create(contentParentVisual); + m_visualInteractionSource.ManipulationRedirectionMode(VisualInteractionSourceRedirectionMode.CapableTouchpadOnly); + m_visualInteractionSource.ScaleSourceMode(InteractionSourceMode.Disabled); + m_visualInteractionSource.PositionXSourceMode(IsOrientationVertical() ? InteractionSourceMode.Disabled : InteractionSourceMode.EnabledWithInertia); + m_visualInteractionSource.PositionXChainingMode(IsOrientationVertical() ? InteractionChainingMode.Auto : InteractionChainingMode.Never); + m_visualInteractionSource.PositionYSourceMode(IsOrientationVertical() ? InteractionSourceMode.EnabledWithInertia : InteractionSourceMode.Disabled); + m_visualInteractionSource.PositionYChainingMode(IsOrientationVertical() ? InteractionChainingMode.Never : InteractionChainingMode.Auto); + + if (m_interactionTracker) + { + m_interactionTracker.InteractionSources().Add(m_visualInteractionSource); + m_visualInteractionSourceIsAttached = true; + } + } + + FxScrollViewer AdaptFromTreeRecursiveHelper(DependencyObject root, int depth) + { + PTR_TRACE_INFO(null, TRACE_MSG_METH_INT, METH_NAME, this, depth); + int numChildren = VisualTreeHelper.GetChildrenCount(root); + if (depth == 0) + { + for (int i = 0; i < numChildren; i++) + { + DependencyObject childObject = VisualTreeHelper.GetChild(root, i); + FxScrollViewer childObjectAsSV = childObject as FxScrollViewer(); + if (childObjectAsSV) + { + return childObjectAsSV; + } + } + return null; + } + else + { + for (int i = 0; i < numChildren; i++) + { + DependencyObject childObject = VisualTreeHelper.GetChild(root, i); + FxScrollViewer recursiveResult = AdaptFromTreeRecursiveHelper(childObject, depth - 1); + if (recursiveResult) + { + return recursiveResult; + } + } + return null; + } + } + + void CleanupScrollViewer() + { + var sv = m_scrollViewer; + if (m_boxedPointerPressedEventHandler) + { + sv.RemoveHandler(UIElement.PointerPressedEvent(), m_boxedPointerPressedEventHandler); + m_boxedPointerPressedEventHandler = null; + } + if (m_scrollViewer_DirectManipulationCompletedToken.value) + { + sv.DirectManipulationCompleted(m_scrollViewer_DirectManipulationCompletedToken); + m_scrollViewer_DirectManipulationCompletedToken.value = 0; + } + if (m_scrollViewer_ViewChangingToken.value) + { + sv.ViewChanging(m_scrollViewer_ViewChangingToken); + m_scrollViewer_ViewChangingToken.value = 0; + } + if (m_scrollViewer_LoadedToken.value) + { + sv.Loaded(m_scrollViewer_LoadedToken); + m_scrollViewer_LoadedToken.value = 0; + } + } + + void CleanupIRefreshInfoProvider() + { + var provider = m_infoProvider; + if (m_infoProvider_RefreshStartedToken.value) + { + provider.RefreshStarted(m_infoProvider_RefreshStartedToken); + m_infoProvider_RefreshStartedToken.value = 0; + } + if (m_infoProvider_RefreshCompletedToken.value) + { + provider.RefreshCompleted(m_infoProvider_RefreshCompletedToken); + m_infoProvider_RefreshCompletedToken.value = 0; + } + } + + bool IsWithinOffsetThreshold() + { + switch (m_refreshPullDirection) + { + case RefreshPullDirection.TopToBottom: + return m_scrollViewer.VerticalOffset() < INITIAL_OFFSET_THRESHOLD; + case RefreshPullDirection.BottomToTop: + return m_scrollViewer.VerticalOffset() > m_scrollViewer.ScrollableHeight() - INITIAL_OFFSET_THRESHOLD; + case RefreshPullDirection.LeftToRight: + return m_scrollViewer.HorizontalOffset() < INITIAL_OFFSET_THRESHOLD; + case RefreshPullDirection.RightToLeft: + return m_scrollViewer.HorizontalOffset() > m_scrollViewer.ScrollableWidth() - INITIAL_OFFSET_THRESHOLD; + default: + MUX_ASSERT(false); + return false; + } + } + } + } diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/ScrollViewerIRefreshInfoProviderDefaultAnimationHandler.Header.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/ScrollViewerIRefreshInfoProviderDefaultAnimationHandler.Header.cs new file mode 100644 index 000000000000..079103bde1c3 --- /dev/null +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/ScrollViewerIRefreshInfoProviderDefaultAnimationHandler.Header.cs @@ -0,0 +1,38 @@ +#nullable enable + +using Uno.Disposables; +using Windows.UI.Composition; +using Windows.UI.Composition.Interactions; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.UI.Private.Controls +{ + internal partial class ScrollViewerIRefreshInfoProviderDefaultAnimationHandler + { + private readonly RefreshPullDirection m_refreshPullDirection; + + private UIElement? m_refreshVisualizer = null; + private UIElement? m_infoProvider = null; + private Visual? m_refreshVisualizerVisual = null; + private Visual? m_infoProviderVisual = null; + private InteractionTracker? m_interactionTracker = null; + private Compositor? m_compositor = null; + + private bool m_interactionAnimationNeedsUpdating = true; + private bool m_refreshRequestedAnimationNeedsUpdating = true; + private bool m_refreshCompletedAnimationNeedsUpdating = true; + + private ExpressionAnimation? m_refreshVisualizerVisualOffsetAnimation = null; + private ExpressionAnimation? m_infoProviderOffsetAnimation = null; + + private ScalarKeyFrameAnimation? m_refreshVisualizerRefreshRequestedAnimation = null; + private ScalarKeyFrameAnimation? m_infoProviderRefreshRequestedAnimation = null; + + private ScalarKeyFrameAnimation? m_refreshVisualizerRefreshCompletedAnimation = null; + private ScalarKeyFrameAnimation? m_infoProviderRefreshCompletedAnimation = null; + private CompositionScopedBatch? m_refreshCompletedScopedBatch = null; + + private readonly SerialDisposable m_compositionScopedBatchCompletedEventToken = new SerialDisposable(); + } +} diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/ScrollViewerIRefreshInfoProviderDefaultAnimationHandler.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/ScrollViewerIRefreshInfoProviderDefaultAnimationHandler.cs new file mode 100644 index 000000000000..5402da152485 --- /dev/null +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter/ScrollViewerIRefreshInfoProviderDefaultAnimationHandler.cs @@ -0,0 +1,314 @@ +#nullable enable + +using System.Numerics; +using Uno.UI.Helpers.WinUI; +using Windows.UI.Composition.Interactions; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Hosting; + +namespace Microsoft.UI.Private.Controls +{ + internal partial class ScrollViewerIRefreshInfoProviderDefaultAnimationHandler : IAdapterAnimationHandler + { + private static readonly TimeSpan REFRESH_ANIMATION_DURATION = TimeSpan.FromMilliseconds(100); + private const double REFRESH_VISUALIZER_OVERPAN_RATIO = 0.4; + + // Implementors of the IAdapterAnimationHandler interface are responsible for implementing the + // 3 well defined component level animations in a PTR scenario. The three animations involved + // in PTR include the expression animation used to have the RefreshVisualizer and its + // InfoProvider follow the users finger, the animation used to show the RefreshVisualizer + // when a refresh is requested, and the animation used to hide the refreshVisualizer when the + // refresh is completed. + + // The interaction tracker set up by the Adapter has to be assembled in a very particular way. + // Factoring out this functionality is a way to expose the animation for + // Alteration without having to expose the "delicate" interaction tracker. + + public ScrollViewerIRefreshInfoProviderDefaultAnimationHandler(UIElement container, RefreshPullDirection refreshPullDirection) + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + if (container != null) + { + var containerVisual = ElementCompositionPreview.GetElementVisual(container); + m_compositor = containerVisual.Compositor; + containerVisual.Clip = m_compositor.CreateInsetClip(0.0f, 0.0f, 0.0f, 0.0f); + } + m_refreshPullDirection = refreshPullDirection; + } + + ~ScrollViewerIRefreshInfoProviderDefaultAnimationHandler() + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + if (m_refreshCompletedScopedBatch != null) + { + m_compositionScopedBatchCompletedEventToken.Disposable = null; + } + } + + private void InteractionTrackerAnimation(UIElement refreshVisualizer, UIElement infoProvider, InteractionTracker interactionTracker) + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + ValidateAndStoreParameters(refreshVisualizer, infoProvider, interactionTracker); + + if (interactionTracker == null) + { + m_interactionTracker = null; + return; + } + + if ((m_refreshVisualizerVisualOffsetAnimation == null || m_infoProviderOffsetAnimation == null || m_interactionAnimationNeedsUpdating) && m_compositor != null) + { + switch (m_refreshPullDirection) + { + case RefreshPullDirection.TopToBottom: + m_refreshVisualizerVisualOffsetAnimation = m_compositor.CreateExpressionAnimation( + "-refreshVisualizerVisual.Size.Y + min(refreshVisualizerVisual.Size.Y, -interactionTracker.Position.Y)"); + + m_infoProviderOffsetAnimation = m_compositor.CreateExpressionAnimation( + "min(refreshVisualizerVisual.Size.Y, max(-interactionTracker.Position.Y, -refreshVisualizerVisual.Size.Y * 0.4))"); + + break; + case RefreshPullDirection.BottomToTop: + m_refreshVisualizerVisualOffsetAnimation = m_compositor.CreateExpressionAnimation( + "refreshVisualizerVisual.Size.Y - min(refreshVisualizerVisual.Size.Y, interactionTracker.Position.Y)"); + + m_infoProviderOffsetAnimation = m_compositor.CreateExpressionAnimation( + "max(-refreshVisualizerVisual.Size.Y, min(-interactionTracker.Position.Y, refreshVisualizerVisual.Size.Y * 0.4))"); + + break; + case RefreshPullDirection.LeftToRight: + m_refreshVisualizerVisualOffsetAnimation = m_compositor.CreateExpressionAnimation( + "-refreshVisualizerVisual.Size.X + min(refreshVisualizerVisual.Size.X, -interactionTracker.Position.X)"); + + m_infoProviderOffsetAnimation = m_compositor.CreateExpressionAnimation( + "min(refreshVisualizerVisual.Size.X, max(-interactionTracker.Position.X, -refreshVisualizerVisual.Size.X * 0.4))"); + + break; + case RefreshPullDirection.RightToLeft: + m_refreshVisualizerVisualOffsetAnimation = m_compositor.CreateExpressionAnimation( + "refreshVisualizerVisual.Size.X - min(refreshVisualizerVisual.Size.X, interactionTracker.Position.X)"); + + m_infoProviderOffsetAnimation = m_compositor.CreateExpressionAnimation( + "max(-refreshVisualizerVisual.Size.X, min(-interactionTracker.Position.X, refreshVisualizerVisual.Size.X * 0.4))"); + + break; + default: + MUX_ASSERT(false); + } + + m_refreshVisualizerVisualOffsetAnimation.SetReferenceParameter("refreshVisualizerVisual", m_refreshVisualizerVisual); + m_refreshVisualizerVisualOffsetAnimation.SetReferenceParameter("interactionTracker", m_interactionTracker); + + m_infoProviderOffsetAnimation.SetReferenceParameter("refreshVisualizerVisual", m_refreshVisualizerVisual); + m_infoProviderOffsetAnimation.SetReferenceParameter("infoProviderVisual", m_infoProviderVisual); + m_infoProviderOffsetAnimation.SetReferenceParameter("interactionTracker", m_interactionTracker); + + m_interactionAnimationNeedsUpdating = false; + } + + if (m_refreshVisualizerVisualOffsetAnimation && m_infoProviderOffsetAnimation) + { + m_refreshVisualizerVisual.Offset = new Vector3(0.0f); + m_infoProviderVisual.Offset = new Vector3(0.0f); + if (SharedHelpers.IsRS2OrHigher()) + { + m_refreshVisualizerVisual.Properties.InsertVector3("Translation", new Vector3(0.0f, 0.0f, 0.0f)); + m_infoProviderVisual.Properties.InsertVector3("Translation", new Vector3(0.0f, 0.0f, 0.0f)); + } + + string animatedProperty = getAnimatedPropertyName(); + + m_refreshVisualizerVisual.StartAnimation(animatedProperty, m_refreshVisualizerVisualOffsetAnimation); + m_infoProviderVisual.StartAnimation(animatedProperty, m_infoProviderOffsetAnimation); + } + } + + private void RefreshRequestedAnimation(UIElement refreshVisualizer, UIElement infoProvider, double executionRatio) + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + ValidateAndStoreParameters(refreshVisualizer, infoProvider, null); + + if ((m_refreshVisualizerRefreshRequestedAnimation == null || m_infoProviderRefreshRequestedAnimation == null || m_refreshRequestedAnimationNeedsUpdating) && m_compositor != null) + { + m_refreshVisualizerRefreshRequestedAnimation = m_compositor.CreateScalarKeyFrameAnimation(); + m_refreshVisualizerRefreshRequestedAnimation.Duration = REFRESH_ANIMATION_DURATION; + m_infoProviderRefreshRequestedAnimation = m_compositor.CreateScalarKeyFrameAnimation(); + m_infoProviderRefreshRequestedAnimation.Duration = REFRESH_ANIMATION_DURATION; + switch (m_refreshPullDirection) + { + case RefreshPullDirection.TopToBottom: + m_refreshVisualizerRefreshRequestedAnimation.InsertKeyFrame(1.0f, -(m_refreshVisualizerVisual.Size.Y * (1 - (float)executionRatio))); + m_infoProviderRefreshRequestedAnimation.InsertKeyFrame(1.0f, m_refreshVisualizerVisual.Size.Y * (float)executionRatio); + break; + case RefreshPullDirection.BottomToTop: + m_refreshVisualizerRefreshRequestedAnimation.InsertKeyFrame(1.0f, m_refreshVisualizerVisual.Size.Y * (1 - (float)executionRatio)); + m_infoProviderRefreshRequestedAnimation.InsertKeyFrame(1.0f, -m_refreshVisualizerVisual.Size.Y * (float)executionRatio); + break; + case RefreshPullDirection.LeftToRight: + m_refreshVisualizerRefreshRequestedAnimation.InsertKeyFrame(1.0f, -(m_refreshVisualizerVisual.Size().x * (1 - (float)executionRatio))); + m_infoProviderRefreshRequestedAnimation.InsertKeyFrame(1.0f, m_refreshVisualizerVisual.Size().x * (float)executionRatio); + break; + case RefreshPullDirection.RightToLeft: + m_refreshVisualizerRefreshRequestedAnimation.InsertKeyFrame(1.0f, m_refreshVisualizerVisual.Size().x * (1 - (float)executionRatio)); + m_infoProviderRefreshRequestedAnimation.InsertKeyFrame(1.0f, -m_refreshVisualizerVisual.Size().x * (float)executionRatio); + break; + default: + MUX_ASSERT(false); + } + m_refreshRequestedAnimationNeedsUpdating = false; + } + + if (m_refreshVisualizerRefreshRequestedAnimation && m_infoProviderRefreshRequestedAnimation) + { + hstring animatedProperty = getAnimatedPropertyName(); + + m_refreshVisualizerVisual.StartAnimation(animatedProperty, m_refreshVisualizerRefreshRequestedAnimation); + m_infoProviderVisual.StartAnimation(animatedProperty, m_infoProviderRefreshRequestedAnimation); + } + } + + private void RefreshCompletedAnimation(UIElement refreshVisualizer, UIElement infoProvider) + { + //PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + ValidateAndStoreParameters(refreshVisualizer, infoProvider, null); + + if ((m_refreshVisualizerRefreshCompletedAnimation == null || m_infoProviderRefreshCompletedAnimation == null || m_refreshCompletedAnimationNeedsUpdating) && m_compositor != null) + { + m_refreshVisualizerRefreshCompletedAnimation = m_compositor.CreateScalarKeyFrameAnimation(); + m_refreshVisualizerRefreshCompletedAnimation.Duration(REFRESH_ANIMATION_DURATION); + m_infoProviderRefreshCompletedAnimation = m_compositor.CreateScalarKeyFrameAnimation(); + m_infoProviderRefreshCompletedAnimation.Duration(REFRESH_ANIMATION_DURATION); + m_infoProviderRefreshCompletedAnimation.InsertKeyFrame(1.0f, 0.0f); + + switch (m_refreshPullDirection) + { + case RefreshPullDirection.TopToBottom: + m_refreshVisualizerRefreshCompletedAnimation.InsertKeyFrame(1.0f, -(m_refreshVisualizerVisual.Size().y)); + break; + case RefreshPullDirection.BottomToTop: + m_refreshVisualizerRefreshCompletedAnimation.InsertKeyFrame(1.0f, m_refreshVisualizerVisual.Size().y); + break; + case RefreshPullDirection.LeftToRight: + m_refreshVisualizerRefreshCompletedAnimation.InsertKeyFrame(1.0f, -(m_refreshVisualizerVisual.Size().x)); + break; + case RefreshPullDirection.RightToLeft: + m_refreshVisualizerRefreshCompletedAnimation.InsertKeyFrame(1.0f, m_refreshVisualizerVisual.Size().x); + break; + default: + MUX_ASSERT(false); + } + m_refreshCompletedAnimationNeedsUpdating = false; + } + + if (m_compositor) + { + m_refreshCompletedScopedBatch = m_compositor.CreateScopedBatch(CompositionBatchTypes.Animation); + m_compositionScopedBatchCompletedEventToken = m_refreshCompletedScopedBatch.Completed({ this, &ScrollViewerIRefreshInfoProviderDefaultAnimationHandler.RefreshCompletedBatchCompleted }); + } + + if (m_refreshVisualizerRefreshCompletedAnimation && m_infoProviderRefreshCompletedAnimation) + { + hstring animatedProperty = getAnimatedPropertyName(); + + m_refreshVisualizerVisual.StartAnimation(animatedProperty, m_refreshVisualizerRefreshCompletedAnimation); + m_infoProviderVisual.StartAnimation(animatedProperty, m_infoProviderRefreshCompletedAnimation); + } + + if (m_refreshCompletedScopedBatch) + { + m_refreshCompletedScopedBatch.End(); + } + } + + //PrivateHelpers + void ValidateAndStoreParameters(UIElement& refreshVisualizer, UIElement& infoProvider, InteractionTracker& interactionTracker) + { + if (refreshVisualizer && m_refreshVisualizer != refreshVisualizer) + { + + m_refreshVisualizerVisual = ElementCompositionPreview.GetElementVisual(refreshVisualizer); + m_refreshVisualizer = refreshVisualizer; + m_interactionAnimationNeedsUpdating = true; + m_refreshRequestedAnimationNeedsUpdating = true; + m_refreshCompletedAnimationNeedsUpdating = true; + + if (SharedHelpers.IsRS2OrHigher()) + { + ElementCompositionPreview.SetIsTranslationEnabled(refreshVisualizer, true); + m_refreshVisualizerVisual.Properties().InsertVector3("Translation", { 0.0f, 0.0f, 0.0f }); + } + + if (!m_compositor) + { + m_compositor = m_refreshVisualizerVisual.Compositor(); + } + } + + if (infoProvider && m_infoProvider != infoProvider) + { + m_infoProviderVisual = ElementCompositionPreview.GetElementVisual(infoProvider); + m_infoProvider = infoProvider; + m_interactionAnimationNeedsUpdating = true; + m_refreshRequestedAnimationNeedsUpdating = true; + m_refreshCompletedAnimationNeedsUpdating = true; + + if (SharedHelpers.IsRS2OrHigher()) + { + ElementCompositionPreview.SetIsTranslationEnabled(infoProvider, true); + m_infoProviderVisual.Properties().InsertVector3("Translation", { 0.0f, 0.0f, 0.0f }); + } + + if (!m_compositor) + { + m_compositor = m_infoProviderVisual.Compositor(); + } + } + + if (interactionTracker && m_interactionTracker != interactionTracker) + { + m_interactionTracker = interactionTracker; + m_interactionAnimationNeedsUpdating = true; + m_refreshRequestedAnimationNeedsUpdating = true; + m_refreshCompletedAnimationNeedsUpdating = true; + } + } + + void RefreshCompletedBatchCompleted(object& /*sender*/, CompositionBatchCompletedEventArgs& /*args*/) + { + PTR_TRACE_INFO(null, TRACE_MSG_METH, METH_NAME, this); + m_refreshCompletedScopedBatch.Completed(m_compositionScopedBatchCompletedEventToken); + InteractionTrackerAnimation(m_refreshVisualizer, m_infoProvider, m_interactionTracker); + } + + bool IsOrientationVertical() + { + return (m_refreshPullDirection == RefreshPullDirection.TopToBottom || m_refreshPullDirection == RefreshPullDirection.BottomToTop); + } + + hstring getAnimatedPropertyName() + { + hstring animatedProperty = ""; + if (SharedHelpers.IsRS2OrHigher()) + { + animatedProperty = (std.wstring)(animatedProperty) + "Translation"; + } + else + { + animatedProperty = (std.wstring)(animatedProperty) + "Offset"; + } + + if (IsOrientationVertical()) + { + animatedProperty = (std.wstring)(animatedProperty) + ".Y"; + } + else + { + animatedProperty = (std.wstring)(animatedProperty) + ".X"; + } + + return animatedProperty; + } + + } +}