using System; using Avalonia.Controls.Primitives; using Avalonia.Controls.PullToRefresh; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Reactive; namespace Avalonia.Controls { /// /// Represents a container control that provides a and pull-to-refresh functionality for scrollable content. /// public class RefreshContainer : ContentControl { internal const int DefaultPullDimensionSize = 100; private bool _hasDefaultRefreshInfoProviderAdapter; private ScrollViewerIRefreshInfoProviderAdapter? _refreshInfoProviderAdapter; private RefreshInfoProvider? _refreshInfoProvider; private IDisposable? _visualizerSizeSubscription; private Grid? _visualizerPresenter; private RefreshVisualizer? _refreshVisualizer; private bool _hasDefaultRefreshVisualizer; /// /// Defines the event. /// public static readonly RoutedEvent RefreshRequestedEvent = RoutedEvent.Register(nameof(RefreshRequested), RoutingStrategies.Bubble); internal static readonly DirectProperty RefreshInfoProviderAdapterProperty = AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProviderAdapter), (s) => s.RefreshInfoProviderAdapter, (s, o) => s.RefreshInfoProviderAdapter = o); /// /// Defines the event. /// public static readonly DirectProperty VisualizerProperty = AvaloniaProperty.RegisterDirect(nameof(Visualizer), s => s.Visualizer, (s, o) => s.Visualizer = o); /// /// Defines the property. /// public static readonly StyledProperty PullDirectionProperty = AvaloniaProperty.Register(nameof(PullDirection), PullDirection.TopToBottom); internal ScrollViewerIRefreshInfoProviderAdapter? RefreshInfoProviderAdapter { get => _refreshInfoProviderAdapter; set { _hasDefaultRefreshInfoProviderAdapter = false; SetAndRaise(RefreshInfoProviderAdapterProperty, ref _refreshInfoProviderAdapter, value); } } /// /// Defines the property. /// /// /// Allows to enable the pull 2 refresh gesture for devices using a mouse. Disabled by default /// public static readonly StyledProperty IsMouseEnabledProperty = AvaloniaProperty.Register(nameof(IsMouseEnabled), false); /// /// Gets or sets a value that indicates whether the pull-to-refresh gesture is enabled for desktop devices. /// Allows to enable the pull 2 refresh gesture for devices using a mouse. Disabled by default /// public bool IsMouseEnabled { get => GetValue(IsMouseEnabledProperty); set => SetValue(IsMouseEnabledProperty, value); } /// /// Gets or sets the for this container. /// public RefreshVisualizer? Visualizer { get => _refreshVisualizer; set { if (_refreshVisualizer != null) { _visualizerSizeSubscription?.Dispose(); _refreshVisualizer.RefreshRequested -= Visualizer_RefreshRequested; } SetAndRaise(VisualizerProperty, ref _refreshVisualizer, value); } } /// /// Gets or sets a value that specifies the direction to pull to initiate a refresh. /// public PullDirection PullDirection { get => GetValue(PullDirectionProperty); set => SetValue(PullDirectionProperty, value); } /// /// Occurs when an update of the content has been initiated. /// public event EventHandler? RefreshRequested { add => AddHandler(RefreshRequestedEvent, value); remove => RemoveHandler(RefreshRequestedEvent, value); } public RefreshContainer() { _hasDefaultRefreshInfoProviderAdapter = true; _refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection, IsMouseEnabled); RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); _visualizerPresenter = e.NameScope.Find("PART_RefreshVisualizerPresenter"); if (_refreshVisualizer == null) { _hasDefaultRefreshVisualizer = true; Visualizer = new RefreshVisualizer(); } else { _hasDefaultRefreshVisualizer = false; RaisePropertyChanged(VisualizerProperty, default, _refreshVisualizer); } OnPullDirectionChanged(); } private void OnVisualizerSizeChanged(Rect obj) { if (_hasDefaultRefreshInfoProviderAdapter && _refreshInfoProviderAdapter != null) { _refreshInfoProviderAdapter.UpdateVisualizerSize(obj.Size); RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter); } } private void Visualizer_RefreshRequested(object? sender, RefreshRequestedEventArgs e) { var ev = new RefreshRequestedEventArgs(e.GetDeferral(), RefreshRequestedEvent); RaiseEvent(ev); ev.DecrementCount(); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == RefreshInfoProviderAdapterProperty) { if (_refreshVisualizer != null) { if (_refreshInfoProvider != null) { _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; } else { if (RefreshInfoProviderAdapter != null && _refreshVisualizer != null) { _refreshInfoProvider = RefreshInfoProviderAdapter?.AdaptFromTree(this, _refreshVisualizer.Bounds.Size); if (_refreshInfoProvider != null) { _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; RefreshInfoProviderAdapter?.SetAnimations(_refreshVisualizer); } } } } } else if (change.Property == VisualizerProperty) { if (_visualizerPresenter != null) { _visualizerPresenter.Children.Clear(); if (_refreshVisualizer != null) { _visualizerPresenter.Children.Add(_refreshVisualizer); } } if (_refreshVisualizer != null) { _refreshVisualizer.RefreshRequested += Visualizer_RefreshRequested; _visualizerSizeSubscription = _refreshVisualizer.GetObservable(Control.BoundsProperty).Subscribe(OnVisualizerSizeChanged); } } else if (change.Property == PullDirectionProperty) { OnPullDirectionChanged(); } else if (change.Property == IsMouseEnabledProperty) { OnIsMouseEnabledChanged(); } } private void OnPullDirectionChanged() { if (_visualizerPresenter != null && _refreshVisualizer != null) { switch (PullDirection) { case PullDirection.TopToBottom: _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Top; _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Stretch; if (_hasDefaultRefreshVisualizer) { _refreshVisualizer.PullDirection = PullDirection.TopToBottom; _refreshVisualizer.Height = DefaultPullDimensionSize; _refreshVisualizer.Width = double.NaN; } break; case PullDirection.BottomToTop: _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Bottom; _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Stretch; if (_hasDefaultRefreshVisualizer) { _refreshVisualizer.PullDirection = PullDirection.BottomToTop; _refreshVisualizer.Height = DefaultPullDimensionSize; _refreshVisualizer.Width = double.NaN; } break; case PullDirection.LeftToRight: _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Stretch; _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Left; if (_hasDefaultRefreshVisualizer) { _refreshVisualizer.PullDirection = PullDirection.LeftToRight; _refreshVisualizer.Width = DefaultPullDimensionSize; _refreshVisualizer.Height = double.NaN; } break; case PullDirection.RightToLeft: _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Stretch; _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Right; if (_hasDefaultRefreshVisualizer) { _refreshVisualizer.PullDirection = PullDirection.RightToLeft; _refreshVisualizer.Width = DefaultPullDimensionSize; _refreshVisualizer.Height = double.NaN; } break; } if (_hasDefaultRefreshInfoProviderAdapter) { if (_hasDefaultRefreshVisualizer) { var size = new Size(_refreshVisualizer.Width, _refreshVisualizer.Height); _refreshInfoProviderAdapter?.UpdateVisualizerSize(size); } _refreshInfoProviderAdapter?.UpdatePullDirection(PullDirection); RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter); } } } private void OnIsMouseEnabledChanged() { _refreshInfoProviderAdapter?.UpdateIsMouseEnabled(IsMouseEnabled); } /// /// Initiates an update of the content. /// public void RequestRefresh() { _refreshVisualizer?.RequestRefresh(); } } }