csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
274 lines
10 KiB
274 lines
10 KiB
using System;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Rendering.Composition;
|
|
using Avalonia.VisualTree;
|
|
|
|
namespace Avalonia.Controls.PullToRefresh
|
|
{
|
|
internal class ScrollViewerIRefreshInfoProviderAdapter
|
|
{
|
|
private const int MaxSearchDepth = 10;
|
|
private const int InitialOffsetThreshold = 1;
|
|
|
|
private PullDirection _refreshPullDirection;
|
|
private ScrollViewer? _scrollViewer;
|
|
private RefreshInfoProvider? _refreshInfoProvider;
|
|
private PullGestureRecognizer? _pullGestureRecognizer;
|
|
private InputElement? _interactionSource;
|
|
private bool _isVisualizerInteractionSourceAttached;
|
|
|
|
public ScrollViewerIRefreshInfoProviderAdapter(PullDirection pullDirection)
|
|
{
|
|
_refreshPullDirection = pullDirection;
|
|
}
|
|
|
|
public RefreshInfoProvider? AdaptFromTree(IVisual root, Size? refreshVIsualizerSize)
|
|
{
|
|
if (root is ScrollViewer scrollViewer)
|
|
{
|
|
return Adapt(scrollViewer, refreshVIsualizerSize);
|
|
}
|
|
else
|
|
{
|
|
int depth = 0;
|
|
while (depth < MaxSearchDepth)
|
|
{
|
|
var scroll = AdaptFromTreeRecursiveHelper(root, depth);
|
|
|
|
if (scroll != null)
|
|
{
|
|
return Adapt(scroll, refreshVIsualizerSize);
|
|
}
|
|
|
|
depth++;
|
|
}
|
|
}
|
|
|
|
ScrollViewer? AdaptFromTreeRecursiveHelper(IVisual root, int depth)
|
|
{
|
|
if (depth == 0)
|
|
{
|
|
foreach (var child in root.VisualChildren)
|
|
{
|
|
if (child is ScrollViewer viewer)
|
|
{
|
|
return viewer;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (var child in root.VisualChildren)
|
|
{
|
|
var viewer = AdaptFromTreeRecursiveHelper(child, depth - 1);
|
|
if (viewer != null)
|
|
{
|
|
return viewer;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public RefreshInfoProvider Adapt(ScrollViewer adaptee, Size? refreshVIsualizerSize)
|
|
{
|
|
if (adaptee == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(adaptee), "Adaptee cannot be null");
|
|
}
|
|
|
|
if (_scrollViewer != null)
|
|
{
|
|
CleanUpScrollViewer();
|
|
}
|
|
|
|
if (_refreshInfoProvider != null && _interactionSource != null)
|
|
{
|
|
_interactionSource.RemoveHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered);
|
|
_interactionSource.RemoveHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited);
|
|
}
|
|
|
|
_refreshInfoProvider = null;
|
|
_scrollViewer = adaptee;
|
|
|
|
if (_scrollViewer.Content == null)
|
|
{
|
|
throw new ArgumentException(nameof(adaptee), "Adaptee's content property cannot be null.");
|
|
}
|
|
|
|
var content = adaptee.Content as Visual;
|
|
|
|
if (content == null)
|
|
{
|
|
throw new ArgumentException(nameof(adaptee), "Adaptee's content property must be a Visual");
|
|
}
|
|
|
|
if (content.GetVisualParent() == null)
|
|
{
|
|
_scrollViewer.Loaded += ScrollViewer_Loaded;
|
|
}
|
|
else
|
|
{
|
|
ScrollViewer_Loaded(null, null);
|
|
|
|
if (content.Parent is not InputElement)
|
|
{
|
|
throw new ArgumentException(nameof(adaptee), "Adaptee's content's parent must be a InputElement");
|
|
}
|
|
}
|
|
|
|
_refreshInfoProvider = new RefreshInfoProvider(_refreshPullDirection, refreshVIsualizerSize, ElementComposition.GetElementVisual(content));
|
|
|
|
_pullGestureRecognizer = new PullGestureRecognizer(_refreshPullDirection);
|
|
|
|
if (_interactionSource != null)
|
|
{
|
|
_interactionSource.GestureRecognizers.Add(_pullGestureRecognizer);
|
|
_interactionSource.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered);
|
|
_interactionSource.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited);
|
|
_isVisualizerInteractionSourceAttached = true;
|
|
}
|
|
|
|
_scrollViewer.PointerPressed += ScrollViewer_PointerPressed;
|
|
_scrollViewer.PointerReleased += ScrollViewer_PointerReleased;
|
|
_scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
|
|
|
|
return _refreshInfoProvider;
|
|
}
|
|
|
|
private void ScrollViewer_ScrollChanged(object? sender, ScrollChangedEventArgs e)
|
|
{
|
|
if (_isVisualizerInteractionSourceAttached && _refreshInfoProvider != null && _refreshInfoProvider.IsInteractingForRefresh)
|
|
{
|
|
if (!IsWithinOffsetThreashold())
|
|
{
|
|
_refreshInfoProvider.IsInteractingForRefresh = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void SetAnimations(RefreshVisualizer refreshVisualizer)
|
|
{
|
|
var visualizerComposition = ElementComposition.GetElementVisual(refreshVisualizer);
|
|
if (visualizerComposition != null)
|
|
{
|
|
var compositor = visualizerComposition.Compositor;
|
|
|
|
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
|
|
offsetAnimation.Target = "Offset";
|
|
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
|
|
offsetAnimation.Duration = TimeSpan.FromMilliseconds(150);
|
|
|
|
var animation = compositor.CreateImplicitAnimationCollection();
|
|
animation["Offset"] = offsetAnimation;
|
|
visualizerComposition.ImplicitAnimations = animation;
|
|
}
|
|
|
|
if(_scrollViewer != null && _scrollViewer.Content is Visual visual)
|
|
{
|
|
var scollContentComposition = ElementComposition.GetElementVisual(visual);
|
|
|
|
if(scollContentComposition != null)
|
|
{
|
|
var compositor = scollContentComposition.Compositor;
|
|
|
|
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
|
|
offsetAnimation.Target = "Offset";
|
|
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
|
|
offsetAnimation.Duration = TimeSpan.FromMilliseconds(150);
|
|
|
|
var animation = compositor.CreateImplicitAnimationCollection();
|
|
animation["Offset"] = offsetAnimation;
|
|
scollContentComposition.ImplicitAnimations = animation;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ScrollViewer_Loaded(object? sender, RoutedEventArgs? e)
|
|
{
|
|
var content = _scrollViewer?.Content as Visual;
|
|
if (content == null)
|
|
{
|
|
throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content property must be a Visual");
|
|
}
|
|
|
|
if (content.Parent is not InputElement)
|
|
{
|
|
throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content parent must be an InputElement");
|
|
}
|
|
|
|
MakeInteractionSource(content.Parent as InputElement);
|
|
|
|
if (_scrollViewer != null)
|
|
{
|
|
_scrollViewer.Loaded -= ScrollViewer_Loaded;
|
|
}
|
|
}
|
|
|
|
private void MakeInteractionSource(InputElement? element)
|
|
{
|
|
_interactionSource = element;
|
|
|
|
if (_pullGestureRecognizer != null && _refreshInfoProvider != null)
|
|
{
|
|
element?.GestureRecognizers.Add(_pullGestureRecognizer);
|
|
_interactionSource?.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered);
|
|
_interactionSource?.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited);
|
|
_isVisualizerInteractionSourceAttached = true;
|
|
}
|
|
}
|
|
|
|
private void ScrollViewer_PointerReleased(object? sender, PointerReleasedEventArgs e)
|
|
{
|
|
if (_refreshInfoProvider != null)
|
|
{
|
|
_refreshInfoProvider.IsInteractingForRefresh = false;
|
|
}
|
|
}
|
|
|
|
private void ScrollViewer_PointerPressed(object? sender, PointerPressedEventArgs e)
|
|
{
|
|
if (_refreshInfoProvider != null)
|
|
{
|
|
_refreshInfoProvider.PeekingMode = !IsWithinOffsetThreashold();
|
|
}
|
|
}
|
|
|
|
private bool IsWithinOffsetThreashold()
|
|
{
|
|
if (_scrollViewer != null)
|
|
{
|
|
var offset = _scrollViewer.Offset;
|
|
|
|
switch (_refreshPullDirection)
|
|
{
|
|
case PullDirection.TopToBottom:
|
|
return offset.Y < InitialOffsetThreshold;
|
|
case PullDirection.LeftToRight:
|
|
return offset.X < InitialOffsetThreshold;
|
|
case PullDirection.RightToLeft:
|
|
return offset.X > _scrollViewer.Extent.Width - _scrollViewer.Viewport.Width - InitialOffsetThreshold;
|
|
case PullDirection.BottomToTop:
|
|
return offset.Y > _scrollViewer.Extent.Height - _scrollViewer.Viewport.Height - InitialOffsetThreshold;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void CleanUpScrollViewer()
|
|
{
|
|
if (_scrollViewer != null)
|
|
{
|
|
_scrollViewer.PointerPressed -= ScrollViewer_PointerPressed;
|
|
_scrollViewer.PointerReleased -= ScrollViewer_PointerReleased;
|
|
_scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|