committed by
GitHub
130 changed files with 4668 additions and 1377 deletions
@ -0,0 +1,5 @@ |
|||
<ProjectConfiguration> |
|||
<Settings> |
|||
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
|||
</Settings> |
|||
</ProjectConfiguration> |
|||
@ -0,0 +1,27 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
xmlns:viewModels="using:ControlCatalog.ViewModels" |
|||
mc:Ignorable="d" |
|||
d:DesignWidth="800" |
|||
d:DesignHeight="450" |
|||
x:DataType="viewModels:RefreshContainerViewModel" |
|||
x:Class="ControlCatalog.Pages.RefreshContainerPage"> |
|||
<DockPanel HorizontalAlignment="Stretch" |
|||
Height="600" |
|||
VerticalAlignment="Top"> |
|||
<Label DockPanel.Dock="Top">A control that supports pull to refresh</Label> |
|||
<RefreshContainer Name="Refresh" |
|||
DockPanel.Dock="Bottom" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
PullDirection="TopToBottom" |
|||
RefreshRequested="RefreshContainerPage_RefreshRequested" |
|||
Margin="5"> |
|||
<ListBox HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Top" |
|||
Items="{Binding Items}"/> |
|||
</RefreshContainer> |
|||
</DockPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,36 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
using ControlCatalog.ViewModels; |
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public class RefreshContainerPage : UserControl |
|||
{ |
|||
private RefreshContainerViewModel _viewModel; |
|||
|
|||
public RefreshContainerPage() |
|||
{ |
|||
this.InitializeComponent(); |
|||
|
|||
_viewModel = new RefreshContainerViewModel(); |
|||
|
|||
DataContext = _viewModel; |
|||
} |
|||
|
|||
private async void RefreshContainerPage_RefreshRequested(object? sender, RefreshRequestedEventArgs e) |
|||
{ |
|||
var deferral = e.GetDeferral(); |
|||
|
|||
await _viewModel.AddToTop(); |
|||
|
|||
deferral.Complete(); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using System.Collections.ObjectModel; |
|||
using System.Linq; |
|||
using System.Reactive; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls.Notifications; |
|||
using ControlCatalog.Pages; |
|||
using MiniMvvm; |
|||
|
|||
namespace ControlCatalog.ViewModels |
|||
{ |
|||
public class RefreshContainerViewModel : ViewModelBase |
|||
{ |
|||
public ObservableCollection<string> Items { get; } |
|||
|
|||
public RefreshContainerViewModel() |
|||
{ |
|||
Items = new ObservableCollection<string>(Enumerable.Range(1, 200).Select(i => $"Item {i}")); |
|||
} |
|||
|
|||
public async Task AddToTop() |
|||
{ |
|||
await Task.Delay(3000); |
|||
Items.Insert(0, $"Item {200 - Items.Count}"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
<Application |
|||
xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="ReactiveUIDemo.App"> |
|||
<Application.Styles> |
|||
<FluentTheme /> |
|||
</Application.Styles> |
|||
</Application> |
|||
@ -0,0 +1,37 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.ReactiveUI; |
|||
using ReactiveUI; |
|||
using ReactiveUIDemo.ViewModels; |
|||
using ReactiveUIDemo.Views; |
|||
using Splat; |
|||
|
|||
namespace ReactiveUIDemo |
|||
{ |
|||
public class App : Application |
|||
{ |
|||
public override void Initialize() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
Locator.CurrentMutable.Register(() => new FooView(), typeof(IViewFor<FooViewModel>)); |
|||
Locator.CurrentMutable.Register(() => new BarView(), typeof(IViewFor<BarViewModel>)); |
|||
} |
|||
|
|||
public override void OnFrameworkInitializationCompleted() |
|||
{ |
|||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) |
|||
desktop.MainWindow = new MainWindow(); |
|||
base.OnFrameworkInitializationCompleted(); |
|||
} |
|||
|
|||
public static int Main(string[] args) |
|||
=> BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); |
|||
|
|||
public static AppBuilder BuildAvaloniaApp() |
|||
=> AppBuilder.Configure<App>() |
|||
.UsePlatformDetect() |
|||
.UseReactiveUI() |
|||
.LogToTrace(); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' |
|||
x:Class="ReactiveUIDemo.MainWindow" |
|||
xmlns:vm="using:ReactiveUIDemo.ViewModels" |
|||
xmlns:rxui="using:Avalonia.ReactiveUI" |
|||
Title="AvaloniaUI ReactiveUI Demo" |
|||
x:DataType="vm:MainWindowViewModel"> |
|||
<TabControl TabStripPlacement="Left"> |
|||
<TabItem Header="RoutedViewHost"> |
|||
<DockPanel DataContext="{Binding RoutedViewHost}"> |
|||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="8"> |
|||
<Button Command="{Binding ShowFoo}">Foo</Button> |
|||
<Button Command="{Binding ShowBar}">Bar</Button> |
|||
</StackPanel> |
|||
<rxui:RoutedViewHost Router="{Binding Router}"/> |
|||
</DockPanel> |
|||
</TabItem> |
|||
</TabControl> |
|||
</Window> |
|||
@ -0,0 +1,22 @@ |
|||
using ReactiveUIDemo.ViewModels; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace ReactiveUIDemo |
|||
{ |
|||
public class MainWindow : Window |
|||
{ |
|||
public MainWindow() |
|||
{ |
|||
this.InitializeComponent(); |
|||
this.DataContext = new MainWindowViewModel(); |
|||
this.AttachDevTools(); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>Exe</OutputType> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<Nullable>enable</Nullable> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Compile Update="Views\BarView.axaml.cs"> |
|||
<DependentUpon>BarView.axaml</DependentUpon> |
|||
</Compile> |
|||
<Compile Update="Views\FooView.axaml.cs"> |
|||
<DependentUpon>FooView.axaml</DependentUpon> |
|||
</Compile> |
|||
</ItemGroup> |
|||
|
|||
<Import Project="..\..\build\SampleApp.props" /> |
|||
<Import Project="..\..\build\ReferenceCoreLibraries.props" /> |
|||
<Import Project="..\..\build\BuildTargets.targets" /> |
|||
<Import Project="..\..\build\Rx.props" /> |
|||
<Import Project="..\..\build\ReactiveUI.props" /> |
|||
</Project> |
|||
@ -0,0 +1,11 @@ |
|||
using ReactiveUI; |
|||
|
|||
namespace ReactiveUIDemo.ViewModels |
|||
{ |
|||
internal class BarViewModel : ReactiveObject, IRoutableViewModel |
|||
{ |
|||
public BarViewModel(IScreen screen) => HostScreen = screen; |
|||
public string UrlPathSegment => "Bar"; |
|||
public IScreen HostScreen { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using ReactiveUI; |
|||
|
|||
namespace ReactiveUIDemo.ViewModels |
|||
{ |
|||
internal class FooViewModel : ReactiveObject, IRoutableViewModel |
|||
{ |
|||
public FooViewModel(IScreen screen) => HostScreen = screen; |
|||
public string UrlPathSegment => "Foo"; |
|||
public IScreen HostScreen { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using ReactiveUI; |
|||
|
|||
namespace ReactiveUIDemo.ViewModels |
|||
{ |
|||
internal class MainWindowViewModel : ReactiveObject |
|||
{ |
|||
public RoutedViewHostPageViewModel RoutedViewHost { get; } = new(); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using ReactiveUI; |
|||
|
|||
namespace ReactiveUIDemo.ViewModels |
|||
{ |
|||
internal class RoutedViewHostPageViewModel : ReactiveObject, IScreen |
|||
{ |
|||
public RoutedViewHostPageViewModel() |
|||
{ |
|||
Foo = new(this); |
|||
Bar = new(this); |
|||
Router.Navigate.Execute(Foo); |
|||
} |
|||
|
|||
public RoutingState Router { get; } = new(); |
|||
public FooViewModel Foo { get; } |
|||
public BarViewModel Bar { get; } |
|||
|
|||
public void ShowFoo() => Router.Navigate.Execute(Foo); |
|||
public void ShowBar() => Router.Navigate.Execute(Bar); |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="ReactiveUIDemo.Views.BarView" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch"> |
|||
<Border Background="Blue"> |
|||
<TextBlock HorizontalAlignment="Center" |
|||
VerticalAlignment="Center" |
|||
FontSize="48"> |
|||
Bar! |
|||
</TextBlock> |
|||
</Border> |
|||
</UserControl> |
|||
@ -0,0 +1,28 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
using ReactiveUI; |
|||
using ReactiveUIDemo.ViewModels; |
|||
|
|||
namespace ReactiveUIDemo.Views |
|||
{ |
|||
internal partial class BarView : UserControl, IViewFor<BarViewModel> |
|||
{ |
|||
public BarView() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
|
|||
public BarViewModel? ViewModel { get; set; } |
|||
|
|||
object? IViewFor.ViewModel |
|||
{ |
|||
get => ViewModel; |
|||
set => ViewModel = (BarViewModel?)value; |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="ReactiveUIDemo.Views.FooView" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch"> |
|||
<Border Background="Red"> |
|||
<TextBlock HorizontalAlignment="Center" |
|||
VerticalAlignment="Center" |
|||
FontSize="48"> |
|||
Foo! |
|||
</TextBlock> |
|||
</Border> |
|||
</UserControl> |
|||
@ -0,0 +1,28 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
using ReactiveUI; |
|||
using ReactiveUIDemo.ViewModels; |
|||
|
|||
namespace ReactiveUIDemo.Views |
|||
{ |
|||
internal partial class FooView : UserControl, IViewFor<FooViewModel> |
|||
{ |
|||
public FooView() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
|
|||
public FooViewModel? ViewModel { get; set; } |
|||
|
|||
object? IViewFor.ViewModel |
|||
{ |
|||
get => ViewModel; |
|||
set => ViewModel = (FooViewModel?)value; |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,152 @@ |
|||
using Avalonia.Input.GestureRecognizers; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
public class PullGestureRecognizer : StyledElement, IGestureRecognizer |
|||
{ |
|||
private IInputElement? _target; |
|||
private IGestureRecognizerActionsDispatcher? _actions; |
|||
private Point _initialPosition; |
|||
private int _gestureId; |
|||
private IPointer? _tracking; |
|||
private PullDirection _pullDirection; |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="PullDirection"/> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<PullGestureRecognizer, PullDirection> PullDirectionProperty = |
|||
AvaloniaProperty.RegisterDirect<PullGestureRecognizer, PullDirection>( |
|||
nameof(PullDirection), |
|||
o => o.PullDirection, |
|||
(o, v) => o.PullDirection = v); |
|||
|
|||
public PullDirection PullDirection |
|||
{ |
|||
get => _pullDirection; |
|||
set => SetAndRaise(PullDirectionProperty, ref _pullDirection, value); |
|||
} |
|||
|
|||
public PullGestureRecognizer(PullDirection pullDirection) |
|||
{ |
|||
PullDirection = pullDirection; |
|||
} |
|||
|
|||
public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) |
|||
{ |
|||
_target = target; |
|||
_actions = actions; |
|||
|
|||
_target?.AddHandler(InputElement.PointerPressedEvent, OnPointerPressed, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble); |
|||
_target?.AddHandler(InputElement.PointerReleasedEvent, OnPointerReleased, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble); |
|||
} |
|||
|
|||
private void OnPointerPressed(object? sender, PointerPressedEventArgs e) |
|||
{ |
|||
PointerPressed(e); |
|||
} |
|||
|
|||
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) |
|||
{ |
|||
PointerReleased(e); |
|||
} |
|||
|
|||
public void PointerCaptureLost(IPointer pointer) |
|||
{ |
|||
if (_tracking == pointer) |
|||
{ |
|||
EndPull(); |
|||
} |
|||
} |
|||
|
|||
public void PointerMoved(PointerEventArgs e) |
|||
{ |
|||
if (_tracking == e.Pointer && _target is Visual visual) |
|||
{ |
|||
var currentPosition = e.GetPosition(visual); |
|||
_actions!.Capture(e.Pointer, this); |
|||
|
|||
Vector delta = default; |
|||
switch (PullDirection) |
|||
{ |
|||
case PullDirection.TopToBottom: |
|||
if (currentPosition.Y > _initialPosition.Y) |
|||
{ |
|||
delta = new Vector(0, currentPosition.Y - _initialPosition.Y); |
|||
} |
|||
break; |
|||
case PullDirection.BottomToTop: |
|||
if (currentPosition.Y < _initialPosition.Y) |
|||
{ |
|||
delta = new Vector(0, _initialPosition.Y - currentPosition.Y); |
|||
} |
|||
break; |
|||
case PullDirection.LeftToRight: |
|||
if (currentPosition.X > _initialPosition.X) |
|||
{ |
|||
delta = new Vector(currentPosition.X - _initialPosition.X, 0); |
|||
} |
|||
break; |
|||
case PullDirection.RightToLeft: |
|||
if (currentPosition.X < _initialPosition.X) |
|||
{ |
|||
delta = new Vector(_initialPosition.X - currentPosition.X, 0); |
|||
} |
|||
break; |
|||
} |
|||
|
|||
_target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection)); |
|||
} |
|||
} |
|||
|
|||
public void PointerPressed(PointerPressedEventArgs e) |
|||
{ |
|||
if (_target != null && _target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) |
|||
{ |
|||
var position = e.GetPosition(visual); |
|||
|
|||
var canPull = false; |
|||
|
|||
var bounds = visual.Bounds; |
|||
|
|||
switch (PullDirection) |
|||
{ |
|||
case PullDirection.TopToBottom: |
|||
canPull = position.Y < bounds.Height * 0.1; |
|||
break; |
|||
case PullDirection.BottomToTop: |
|||
canPull = position.Y > bounds.Height - (bounds.Height * 0.1); |
|||
break; |
|||
case PullDirection.LeftToRight: |
|||
canPull = position.X < bounds.Width * 0.1; |
|||
break; |
|||
case PullDirection.RightToLeft: |
|||
canPull = position.X > bounds.Width - (bounds.Width * 0.1); |
|||
break; |
|||
} |
|||
|
|||
if (canPull) |
|||
{ |
|||
_gestureId = PullGestureEventArgs.GetNextFreeId(); |
|||
_tracking = e.Pointer; |
|||
_initialPosition = position; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void PointerReleased(PointerReleasedEventArgs e) |
|||
{ |
|||
if (_tracking == e.Pointer) |
|||
{ |
|||
EndPull(); |
|||
} |
|||
} |
|||
|
|||
private void EndPull() |
|||
{ |
|||
_tracking = null; |
|||
_initialPosition = default; |
|||
|
|||
_target?.RaiseEvent(new PullGestureEndedEventArgs(_gestureId, PullDirection)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using System; |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
public class PullGestureEventArgs : RoutedEventArgs |
|||
{ |
|||
public int Id { get; } |
|||
public Vector Delta { get; } |
|||
public PullDirection PullDirection { get; } |
|||
|
|||
private static int _nextId = 1; |
|||
|
|||
internal static int GetNextFreeId() => _nextId++; |
|||
|
|||
public PullGestureEventArgs(int id, Vector delta, PullDirection pullDirection) : base(Gestures.PullGestureEvent) |
|||
{ |
|||
Id = id; |
|||
Delta = delta; |
|||
PullDirection = pullDirection; |
|||
} |
|||
} |
|||
|
|||
public class PullGestureEndedEventArgs : RoutedEventArgs |
|||
{ |
|||
public int Id { get; } |
|||
public PullDirection PullDirection { get; } |
|||
|
|||
public PullGestureEndedEventArgs(int id, PullDirection pullDirection) : base(Gestures.PullGestureEndedEvent) |
|||
{ |
|||
Id = id; |
|||
PullDirection = pullDirection; |
|||
} |
|||
} |
|||
|
|||
public enum PullDirection |
|||
{ |
|||
TopToBottom, |
|||
BottomToTop, |
|||
LeftToRight, |
|||
RightToLeft |
|||
} |
|||
} |
|||
@ -0,0 +1,293 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Runtime.CompilerServices; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Media.TextFormatting |
|||
{ |
|||
public readonly struct CharacterBufferRange : IReadOnlyList<char> |
|||
{ |
|||
/// <summary>
|
|||
/// Getting an empty character string
|
|||
/// </summary>
|
|||
public static CharacterBufferRange Empty => new CharacterBufferRange(); |
|||
|
|||
/// <summary>
|
|||
/// Construct <see cref="CharacterBufferRange"/> from character array
|
|||
/// </summary>
|
|||
/// <param name="characterArray">character array</param>
|
|||
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
|
|||
/// <param name="characterLength">character length</param>
|
|||
public CharacterBufferRange( |
|||
char[] characterArray, |
|||
int offsetToFirstChar, |
|||
int characterLength |
|||
) |
|||
: this( |
|||
new CharacterBufferReference(characterArray, offsetToFirstChar), |
|||
characterLength |
|||
) |
|||
{ } |
|||
|
|||
/// <summary>
|
|||
/// Construct <see cref="CharacterBufferRange"/> from string
|
|||
/// </summary>
|
|||
/// <param name="characterString">character string</param>
|
|||
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
|
|||
/// <param name="characterLength">character length</param>
|
|||
public CharacterBufferRange( |
|||
string characterString, |
|||
int offsetToFirstChar, |
|||
int characterLength |
|||
) |
|||
: this( |
|||
new CharacterBufferReference(characterString, offsetToFirstChar), |
|||
characterLength |
|||
) |
|||
{ } |
|||
|
|||
/// <summary>
|
|||
/// Construct a <see cref="CharacterBufferRange"/> from <see cref="CharacterBufferReference"/>
|
|||
/// </summary>
|
|||
/// <param name="characterBufferReference">character buffer reference</param>
|
|||
/// <param name="characterLength">number of characters</param>
|
|||
public CharacterBufferRange( |
|||
CharacterBufferReference characterBufferReference, |
|||
int characterLength |
|||
) |
|||
{ |
|||
if (characterLength < 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException("characterLength", "ParameterCannotBeNegative"); |
|||
} |
|||
|
|||
int maxLength = characterBufferReference.CharacterBuffer.Length > 0 ? |
|||
characterBufferReference.CharacterBuffer.Length - characterBufferReference.OffsetToFirstChar : |
|||
0; |
|||
|
|||
if (characterLength > maxLength) |
|||
{ |
|||
throw new ArgumentOutOfRangeException("characterLength", $"ParameterCannotBeGreaterThan {maxLength}"); |
|||
} |
|||
|
|||
CharacterBufferReference = characterBufferReference; |
|||
Length = characterLength; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Construct a <see cref="CharacterBufferRange"/> from part of another <see cref="CharacterBufferRange"/>
|
|||
/// </summary>
|
|||
internal CharacterBufferRange( |
|||
CharacterBufferRange characterBufferRange, |
|||
int offsetToFirstChar, |
|||
int characterLength |
|||
) : |
|||
this( |
|||
characterBufferRange.CharacterBuffer, |
|||
characterBufferRange.OffsetToFirstChar + offsetToFirstChar, |
|||
characterLength |
|||
) |
|||
{ } |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Construct a <see cref="CharacterBufferRange"/> from string
|
|||
/// </summary>
|
|||
internal CharacterBufferRange( |
|||
string charString |
|||
) : |
|||
this( |
|||
charString, |
|||
0, |
|||
charString.Length |
|||
) |
|||
{ } |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Construct <see cref="CharacterBufferRange"/> from memory buffer
|
|||
/// </summary>
|
|||
internal CharacterBufferRange( |
|||
ReadOnlyMemory<char> charBuffer, |
|||
int offsetToFirstChar, |
|||
int characterLength |
|||
) : |
|||
this( |
|||
new CharacterBufferReference(charBuffer, offsetToFirstChar), |
|||
characterLength |
|||
) |
|||
{ } |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Construct a <see cref="CharacterBufferRange"/> by extracting text info from a text run
|
|||
/// </summary>
|
|||
internal CharacterBufferRange(TextRun textRun) |
|||
{ |
|||
CharacterBufferReference = textRun.CharacterBufferReference; |
|||
Length = textRun.Length; |
|||
} |
|||
|
|||
public char this[int index] |
|||
{ |
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
get |
|||
{ |
|||
#if DEBUG
|
|||
if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(index)); |
|||
} |
|||
#endif
|
|||
return Span[index]; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a reference to the character buffer
|
|||
/// </summary>
|
|||
public CharacterBufferReference CharacterBufferReference { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of characters in text source character store
|
|||
/// </summary>
|
|||
public int Length { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a span from the character buffer range
|
|||
/// </summary>
|
|||
public ReadOnlySpan<char> Span => |
|||
CharacterBufferReference.CharacterBuffer.Span.Slice(CharacterBufferReference.OffsetToFirstChar, Length); |
|||
|
|||
/// <summary>
|
|||
/// Gets the character memory buffer
|
|||
/// </summary>
|
|||
internal ReadOnlyMemory<char> CharacterBuffer |
|||
{ |
|||
get { return CharacterBufferReference.CharacterBuffer; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the character offset relative to the beginning of buffer to
|
|||
/// the first character of the run
|
|||
/// </summary>
|
|||
internal int OffsetToFirstChar |
|||
{ |
|||
get { return CharacterBufferReference.OffsetToFirstChar; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Indicate whether the character buffer range is empty
|
|||
/// </summary>
|
|||
internal bool IsEmpty |
|||
{ |
|||
get { return CharacterBufferReference.CharacterBuffer.Length == 0 || Length <= 0; } |
|||
} |
|||
|
|||
internal CharacterBufferRange Take(int length) |
|||
{ |
|||
if (IsEmpty) |
|||
{ |
|||
return this; |
|||
} |
|||
|
|||
if (length > Length) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(length)); |
|||
} |
|||
|
|||
return new CharacterBufferRange(CharacterBufferReference, length); |
|||
} |
|||
|
|||
internal CharacterBufferRange Skip(int length) |
|||
{ |
|||
if (IsEmpty) |
|||
{ |
|||
return this; |
|||
} |
|||
|
|||
if (length > Length) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(length)); |
|||
} |
|||
|
|||
if (length == Length) |
|||
{ |
|||
return new CharacterBufferRange(new CharacterBufferReference(), 0); |
|||
} |
|||
|
|||
var characterBufferReference = new CharacterBufferReference( |
|||
CharacterBufferReference.CharacterBuffer, |
|||
CharacterBufferReference.OffsetToFirstChar + length); |
|||
|
|||
return new CharacterBufferRange(characterBufferReference, Length - length); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compute hash code
|
|||
/// </summary>
|
|||
public override int GetHashCode() |
|||
{ |
|||
return CharacterBufferReference.GetHashCode() ^ Length; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Test equality with the input object
|
|||
/// </summary>
|
|||
/// <param name="obj"> The object to test </param>
|
|||
public override bool Equals(object? obj) |
|||
{ |
|||
if (obj is CharacterBufferRange range) |
|||
{ |
|||
return Equals(range); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Test equality with the input CharacterBufferRange
|
|||
/// </summary>
|
|||
/// <param name="value"> The CharacterBufferRange value to test </param>
|
|||
public bool Equals(CharacterBufferRange value) |
|||
{ |
|||
return CharacterBufferReference.Equals(value.CharacterBufferReference) |
|||
&& Length == value.Length; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compare two CharacterBufferRange for equality
|
|||
/// </summary>
|
|||
/// <param name="left">left operand</param>
|
|||
/// <param name="right">right operand</param>
|
|||
/// <returns>whether or not two operands are equal</returns>
|
|||
public static bool operator ==(CharacterBufferRange left, CharacterBufferRange right) |
|||
{ |
|||
return left.Equals(right); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compare two CharacterBufferRange for inequality
|
|||
/// </summary>
|
|||
/// <param name="left">left operand</param>
|
|||
/// <param name="right">right operand</param>
|
|||
/// <returns>whether or not two operands are equal</returns>
|
|||
public static bool operator !=(CharacterBufferRange left, CharacterBufferRange right) |
|||
{ |
|||
return !(left == right); |
|||
} |
|||
|
|||
int IReadOnlyCollection<char>.Count => Length; |
|||
|
|||
public IEnumerator<char> GetEnumerator() |
|||
{ |
|||
return new ImmutableReadOnlyListStructEnumerator<char>(this); |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() |
|||
{ |
|||
return GetEnumerator(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,115 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Media.TextFormatting |
|||
{ |
|||
/// <summary>
|
|||
/// Text character buffer reference
|
|||
/// </summary>
|
|||
public readonly struct CharacterBufferReference : IEquatable<CharacterBufferReference> |
|||
{ |
|||
/// <summary>
|
|||
/// Construct character buffer reference from character array
|
|||
/// </summary>
|
|||
/// <param name="characterArray">character array</param>
|
|||
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
|
|||
public CharacterBufferReference(char[] characterArray, int offsetToFirstChar = 0) |
|||
: this(characterArray.AsMemory(), offsetToFirstChar) |
|||
{ } |
|||
|
|||
/// <summary>
|
|||
/// Construct character buffer reference from string
|
|||
/// </summary>
|
|||
/// <param name="characterString">character string</param>
|
|||
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
|
|||
public CharacterBufferReference(string characterString, int offsetToFirstChar = 0) |
|||
: this(characterString.AsMemory(), offsetToFirstChar) |
|||
{ } |
|||
|
|||
/// <summary>
|
|||
/// Construct character buffer reference from memory buffer
|
|||
/// </summary>
|
|||
internal CharacterBufferReference(ReadOnlyMemory<char> characterBuffer, int offsetToFirstChar = 0) |
|||
{ |
|||
if (offsetToFirstChar < 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException("offsetToFirstChar", "ParameterCannotBeNegative"); |
|||
} |
|||
|
|||
// maximum offset is one less than CharacterBuffer.Count, except that zero is always a valid offset
|
|||
// even in the case of an empty or null character buffer
|
|||
var maxOffset = characterBuffer.Length == 0 ? 0 : Math.Max(0, characterBuffer.Length - 1); |
|||
if (offsetToFirstChar > maxOffset) |
|||
{ |
|||
throw new ArgumentOutOfRangeException("offsetToFirstChar", $"ParameterCannotBeGreaterThan, {maxOffset}"); |
|||
} |
|||
|
|||
CharacterBuffer = characterBuffer; |
|||
OffsetToFirstChar = offsetToFirstChar; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the character memory buffer
|
|||
/// </summary>
|
|||
public ReadOnlyMemory<char> CharacterBuffer { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the character offset relative to the beginning of buffer to
|
|||
/// the first character of the run
|
|||
/// </summary>
|
|||
public int OffsetToFirstChar { get; } |
|||
|
|||
/// <summary>
|
|||
/// Compute hash code
|
|||
/// </summary>
|
|||
public override int GetHashCode() |
|||
{ |
|||
return CharacterBuffer.IsEmpty ? 0 : CharacterBuffer.GetHashCode(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Test equality with the input object
|
|||
/// </summary>
|
|||
/// <param name="obj"> The object to test. </param>
|
|||
public override bool Equals(object? obj) |
|||
{ |
|||
if (obj is CharacterBufferReference reference) |
|||
{ |
|||
return Equals(reference); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Test equality with the input CharacterBufferReference
|
|||
/// </summary>
|
|||
/// <param name="value"> The characterBufferReference value to test </param>
|
|||
public bool Equals(CharacterBufferReference value) |
|||
{ |
|||
return CharacterBuffer.Equals(value.CharacterBuffer); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compare two CharacterBufferReference for equality
|
|||
/// </summary>
|
|||
/// <param name="left">left operand</param>
|
|||
/// <param name="right">right operand</param>
|
|||
/// <returns>whether or not two operands are equal</returns>
|
|||
public static bool operator ==(CharacterBufferReference left, CharacterBufferReference right) |
|||
{ |
|||
return left.Equals(right); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compare two CharacterBufferReference for inequality
|
|||
/// </summary>
|
|||
/// <param name="left">left operand</param>
|
|||
/// <param name="right">right operand</param>
|
|||
/// <returns>whether or not two operands are equal</returns>
|
|||
public static bool operator !=(CharacterBufferReference left, CharacterBufferReference right) |
|||
{ |
|||
return !(left == right); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,239 +0,0 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The type of elements in the slice.</typeparam>
|
|||
[DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))] |
|||
public readonly struct ReadOnlySlice<T> : IReadOnlyList<T> where T : struct |
|||
{ |
|||
private readonly int _bufferOffset; |
|||
|
|||
/// <summary>
|
|||
/// Gets an empty <see cref="ReadOnlySlice{T}"/>
|
|||
/// </summary>
|
|||
public static ReadOnlySlice<T> Empty => new ReadOnlySlice<T>(Array.Empty<T>()); |
|||
|
|||
private readonly ReadOnlyMemory<T> _buffer; |
|||
|
|||
public ReadOnlySlice(ReadOnlyMemory<T> buffer) : this(buffer, 0, buffer.Length) { } |
|||
|
|||
public ReadOnlySlice(ReadOnlyMemory<T> buffer, int start, int length, int bufferOffset = 0) |
|||
{ |
|||
#if DEBUG
|
|||
if (start.CompareTo(0) < 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof (start)); |
|||
} |
|||
|
|||
if (length.CompareTo(buffer.Length) > 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof (length)); |
|||
} |
|||
#endif
|
|||
|
|||
_buffer = buffer; |
|||
Start = start; |
|||
Length = length; |
|||
_bufferOffset = bufferOffset; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the start.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The start.
|
|||
/// </value>
|
|||
public int Start { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the end.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The end.
|
|||
/// </value>
|
|||
public int End => Start + Length - 1; |
|||
|
|||
/// <summary>
|
|||
/// Gets the length.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The length.
|
|||
/// </value>
|
|||
public int Length { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value that indicates whether this instance of <see cref="ReadOnlySlice{T}"/> is Empty.
|
|||
/// </summary>
|
|||
public bool IsEmpty => Length == 0; |
|||
|
|||
/// <summary>
|
|||
/// Get the underlying span.
|
|||
/// </summary>
|
|||
public ReadOnlySpan<T> Span => _buffer.Span.Slice(_bufferOffset, Length); |
|||
|
|||
/// <summary>
|
|||
/// Get the buffer offset.
|
|||
/// </summary>
|
|||
public int BufferOffset => _bufferOffset; |
|||
|
|||
/// <summary>
|
|||
/// Get the underlying buffer.
|
|||
/// </summary>
|
|||
public ReadOnlyMemory<T> Buffer => _buffer; |
|||
|
|||
/// <summary>
|
|||
/// Returns a value to specified element of the slice.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the element to return.</param>
|
|||
/// <returns>The <typeparamref name="T"/>.</returns>
|
|||
/// <exception cref="IndexOutOfRangeException">
|
|||
/// Thrown when index less than 0 or index greater than or equal to <see cref="Length"/>.
|
|||
/// </exception>
|
|||
public T this[int index] |
|||
{ |
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
get |
|||
{ |
|||
#if DEBUG
|
|||
if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof (index)); |
|||
} |
|||
#endif
|
|||
return Span[index]; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a sub slice of elements that start at the specified index and has the specified number of elements.
|
|||
/// </summary>
|
|||
/// <param name="start">The start of the sub slice.</param>
|
|||
/// <param name="length">The length of the sub slice.</param>
|
|||
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the specified start.</returns>
|
|||
public ReadOnlySlice<T> AsSlice(int start, int length) |
|||
{ |
|||
if (IsEmpty) |
|||
{ |
|||
return this; |
|||
} |
|||
|
|||
if (length == 0) |
|||
{ |
|||
return Empty; |
|||
} |
|||
|
|||
if (start < 0 || _bufferOffset + start > _buffer.Length - 1) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(start)); |
|||
} |
|||
|
|||
if (_bufferOffset + start + length > _buffer.Length) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(length)); |
|||
} |
|||
|
|||
return new ReadOnlySlice<T>(_buffer, start, length, _bufferOffset); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a specified number of contiguous elements from the start of the slice.
|
|||
/// </summary>
|
|||
/// <param name="length">The number of elements to return.</param>
|
|||
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the start of this slice.</returns>
|
|||
public ReadOnlySlice<T> Take(int length) |
|||
{ |
|||
if (IsEmpty) |
|||
{ |
|||
return this; |
|||
} |
|||
|
|||
if (length > Length) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(length)); |
|||
} |
|||
|
|||
return new ReadOnlySlice<T>(_buffer, Start, length, _bufferOffset); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Bypasses a specified number of elements in the slice and then returns the remaining elements.
|
|||
/// </summary>
|
|||
/// <param name="length">The number of elements to skip before returning the remaining elements.</param>
|
|||
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the elements that occur after the specified index in this slice.</returns>
|
|||
public ReadOnlySlice<T> Skip(int length) |
|||
{ |
|||
if (IsEmpty) |
|||
{ |
|||
return this; |
|||
} |
|||
|
|||
if (length > Length) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(length)); |
|||
} |
|||
|
|||
return new ReadOnlySlice<T>(_buffer, Start + length, Length - length, _bufferOffset + length); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns an enumerator for the slice.
|
|||
/// </summary>
|
|||
public ImmutableReadOnlyListStructEnumerator<T> GetEnumerator() |
|||
{ |
|||
return new ImmutableReadOnlyListStructEnumerator<T>(this); |
|||
} |
|||
|
|||
IEnumerator<T> IEnumerable<T>.GetEnumerator() |
|||
{ |
|||
return GetEnumerator(); |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() |
|||
{ |
|||
return GetEnumerator(); |
|||
} |
|||
|
|||
int IReadOnlyCollection<T>.Count => Length; |
|||
|
|||
T IReadOnlyList<T>.this[int index] => this[index]; |
|||
|
|||
public static implicit operator ReadOnlySlice<T>(T[] array) |
|||
{ |
|||
return new ReadOnlySlice<T>(array); |
|||
} |
|||
|
|||
public static implicit operator ReadOnlySlice<T>(ReadOnlyMemory<T> memory) |
|||
{ |
|||
return new ReadOnlySlice<T>(memory); |
|||
} |
|||
|
|||
public static implicit operator ReadOnlySpan<T>(ReadOnlySlice<T> slice) => slice.Span; |
|||
|
|||
internal class ReadOnlySliceDebugView |
|||
{ |
|||
private readonly ReadOnlySlice<T> _readOnlySlice; |
|||
|
|||
public ReadOnlySliceDebugView(ReadOnlySlice<T> readOnlySlice) |
|||
{ |
|||
_readOnlySlice = readOnlySlice; |
|||
} |
|||
|
|||
public int Start => _readOnlySlice.Start; |
|||
|
|||
public int End => _readOnlySlice.End; |
|||
|
|||
public int Length => _readOnlySlice.Length; |
|||
|
|||
public bool IsEmpty => _readOnlySlice.IsEmpty; |
|||
|
|||
public ReadOnlySpan<T> Items => _readOnlySlice.Span; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
using System; |
|||
using System.Threading; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Deferral class for notify that a work done in RefreshRequested event is done.
|
|||
/// </summary>
|
|||
public class RefreshCompletionDeferral |
|||
{ |
|||
private Action _deferredAction; |
|||
private int _deferCount; |
|||
|
|||
public RefreshCompletionDeferral(Action deferredAction) |
|||
{ |
|||
_deferredAction = deferredAction; |
|||
} |
|||
|
|||
public void Complete() |
|||
{ |
|||
Interlocked.Decrement(ref _deferCount); |
|||
|
|||
if (_deferCount == 0) |
|||
{ |
|||
_deferredAction?.Invoke(); |
|||
} |
|||
} |
|||
|
|||
public RefreshCompletionDeferral Get() |
|||
{ |
|||
Interlocked.Increment(ref _deferCount); |
|||
|
|||
return this; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,252 @@ |
|||
using System; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Controls.PullToRefresh; |
|||
using Avalonia.Input; |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a container control that provides a <see cref="RefreshVisualizer"/> and pull-to-refresh functionality for scrollable content.
|
|||
/// </summary>
|
|||
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; |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="RefreshRequested"/> event.
|
|||
/// </summary>
|
|||
public static readonly RoutedEvent<RefreshRequestedEventArgs> RefreshRequestedEvent = |
|||
RoutedEvent.Register<RefreshContainer, RefreshRequestedEventArgs>(nameof(RefreshRequested), RoutingStrategies.Bubble); |
|||
|
|||
internal static readonly DirectProperty<RefreshContainer, ScrollViewerIRefreshInfoProviderAdapter?> RefreshInfoProviderAdapterProperty = |
|||
AvaloniaProperty.RegisterDirect<RefreshContainer, ScrollViewerIRefreshInfoProviderAdapter?>(nameof(RefreshInfoProviderAdapter), |
|||
(s) => s.RefreshInfoProviderAdapter, (s, o) => s.RefreshInfoProviderAdapter = o); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Visualizer"/> event.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<RefreshContainer, RefreshVisualizer?> VisualizerProperty = |
|||
AvaloniaProperty.RegisterDirect<RefreshContainer, RefreshVisualizer?>(nameof(Visualizer), |
|||
s => s.Visualizer, (s, o) => s.Visualizer = o); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="PullDirection"/> event.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<PullDirection> PullDirectionProperty = |
|||
AvaloniaProperty.Register<RefreshContainer, PullDirection>(nameof(PullDirection), PullDirection.TopToBottom); |
|||
|
|||
internal ScrollViewerIRefreshInfoProviderAdapter? RefreshInfoProviderAdapter |
|||
{ |
|||
get => _refreshInfoProviderAdapter; set |
|||
{ |
|||
_hasDefaultRefreshInfoProviderAdapter = false; |
|||
SetAndRaise(RefreshInfoProviderAdapterProperty, ref _refreshInfoProviderAdapter, value); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the <see cref="RefreshVisualizer"/> for this container.
|
|||
/// </summary>
|
|||
public RefreshVisualizer? Visualizer |
|||
{ |
|||
get => _refreshVisualizer; set |
|||
{ |
|||
if (_refreshVisualizer != null) |
|||
{ |
|||
_visualizerSizeSubscription?.Dispose(); |
|||
_refreshVisualizer.RefreshRequested -= Visualizer_RefreshRequested; |
|||
} |
|||
|
|||
SetAndRaise(VisualizerProperty, ref _refreshVisualizer, value); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value that specifies the direction to pull to initiate a refresh.
|
|||
/// </summary>
|
|||
public PullDirection PullDirection |
|||
{ |
|||
get => GetValue(PullDirectionProperty); |
|||
set => SetValue(PullDirectionProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Occurs when an update of the content has been initiated.
|
|||
/// </summary>
|
|||
public event EventHandler<RefreshRequestedEventArgs>? RefreshRequested |
|||
{ |
|||
add => AddHandler(RefreshRequestedEvent, value); |
|||
remove => RemoveHandler(RefreshRequestedEvent, value); |
|||
} |
|||
|
|||
public RefreshContainer() |
|||
{ |
|||
_hasDefaultRefreshInfoProviderAdapter = true; |
|||
_refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); |
|||
RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter); |
|||
} |
|||
|
|||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) |
|||
{ |
|||
base.OnApplyTemplate(e); |
|||
|
|||
_visualizerPresenter = e.NameScope.Find<Grid>("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 = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); |
|||
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(); |
|||
} |
|||
} |
|||
|
|||
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 && |
|||
_hasDefaultRefreshVisualizer && |
|||
_refreshVisualizer.Bounds.Height == DefaultPullDimensionSize && |
|||
_refreshVisualizer.Bounds.Width == DefaultPullDimensionSize) |
|||
{ |
|||
_refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); |
|||
RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initiates an update of the content.
|
|||
/// </summary>
|
|||
public void RequestRefresh() |
|||
{ |
|||
_refreshVisualizer?.RequestRefresh(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,141 @@ |
|||
using System; |
|||
using Avalonia.Input; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Rendering.Composition; |
|||
|
|||
namespace Avalonia.Controls.PullToRefresh |
|||
{ |
|||
internal class RefreshInfoProvider : Interactive |
|||
{ |
|||
internal const double DefaultExecutionRatio = 0.8; |
|||
|
|||
private readonly PullDirection _refreshPullDirection; |
|||
private readonly Size _refreshVisualizerSize; |
|||
|
|||
private readonly CompositionVisual? _visual; |
|||
private bool _isInteractingForRefresh; |
|||
private double _interactionRatio; |
|||
private bool _entered; |
|||
|
|||
public DirectProperty<RefreshInfoProvider, bool> IsInteractingForRefreshProperty = |
|||
AvaloniaProperty.RegisterDirect<RefreshInfoProvider, bool>(nameof(IsInteractingForRefresh), |
|||
s => s.IsInteractingForRefresh, (s, o) => s.IsInteractingForRefresh = o); |
|||
|
|||
|
|||
public DirectProperty<RefreshInfoProvider, double> ExecutionRatioProperty = |
|||
AvaloniaProperty.RegisterDirect<RefreshInfoProvider, double>(nameof(ExecutionRatio), |
|||
s => s.ExecutionRatio); |
|||
|
|||
public DirectProperty<RefreshInfoProvider, double> InteractionRatioProperty = |
|||
AvaloniaProperty.RegisterDirect<RefreshInfoProvider, double>(nameof(InteractionRatio), |
|||
s => s.InteractionRatio, (s, o) => s.InteractionRatio = o); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="RefreshStarted"/> event.
|
|||
/// </summary>
|
|||
public static readonly RoutedEvent<RoutedEventArgs> RefreshStartedEvent = |
|||
RoutedEvent.Register<RefreshInfoProvider, RoutedEventArgs>(nameof(RefreshStarted), RoutingStrategies.Bubble); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="RefreshCompleted"/> event.
|
|||
/// </summary>
|
|||
public static readonly RoutedEvent<RoutedEventArgs> RefreshCompletedEvent = |
|||
RoutedEvent.Register<RefreshInfoProvider, RoutedEventArgs>(nameof(RefreshCompleted), RoutingStrategies.Bubble); |
|||
|
|||
public bool PeekingMode { get; internal set; } |
|||
|
|||
public bool IsInteractingForRefresh |
|||
{ |
|||
get => _isInteractingForRefresh; |
|||
internal set |
|||
{ |
|||
var isInteractingForRefresh = value && !PeekingMode; |
|||
|
|||
if (isInteractingForRefresh != _isInteractingForRefresh) |
|||
{ |
|||
SetAndRaise(IsInteractingForRefreshProperty, ref _isInteractingForRefresh, isInteractingForRefresh); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public double InteractionRatio |
|||
{ |
|||
get => _interactionRatio; |
|||
set |
|||
{ |
|||
SetAndRaise(InteractionRatioProperty, ref _interactionRatio, value); |
|||
} |
|||
} |
|||
|
|||
public double ExecutionRatio |
|||
{ |
|||
get => DefaultExecutionRatio; |
|||
} |
|||
|
|||
internal CompositionVisual? Visual => _visual; |
|||
|
|||
public event EventHandler<RoutedEventArgs> RefreshStarted |
|||
{ |
|||
add => AddHandler(RefreshStartedEvent, value); |
|||
remove => RemoveHandler(RefreshStartedEvent, value); |
|||
} |
|||
|
|||
public event EventHandler<RoutedEventArgs> RefreshCompleted |
|||
{ |
|||
add => AddHandler(RefreshCompletedEvent, value); |
|||
remove => RemoveHandler(RefreshCompletedEvent, value); |
|||
} |
|||
|
|||
internal void InteractingStateEntered(object? sender, PullGestureEventArgs e) |
|||
{ |
|||
if (!_entered) |
|||
{ |
|||
IsInteractingForRefresh = true; |
|||
_entered = true; |
|||
} |
|||
|
|||
ValuesChanged(e.Delta); |
|||
} |
|||
|
|||
internal void InteractingStateExited(object? sender, PullGestureEndedEventArgs e) |
|||
{ |
|||
IsInteractingForRefresh = false; |
|||
_entered = false; |
|||
|
|||
ValuesChanged(default); |
|||
} |
|||
|
|||
|
|||
public RefreshInfoProvider(PullDirection refreshPullDirection, Size? refreshVIsualizerSize, CompositionVisual? visual) |
|||
{ |
|||
_refreshPullDirection = refreshPullDirection; |
|||
_refreshVisualizerSize = refreshVIsualizerSize ?? default; |
|||
_visual = visual; |
|||
} |
|||
|
|||
public void OnRefreshStarted() |
|||
{ |
|||
RaiseEvent(new RoutedEventArgs(RefreshStartedEvent)); |
|||
} |
|||
|
|||
public void OnRefreshCompleted() |
|||
{ |
|||
RaiseEvent(new RoutedEventArgs(RefreshCompletedEvent)); |
|||
} |
|||
|
|||
internal void ValuesChanged(Vector value) |
|||
{ |
|||
switch (_refreshPullDirection) |
|||
{ |
|||
case PullDirection.TopToBottom: |
|||
case PullDirection.BottomToTop: |
|||
InteractionRatio = _refreshVisualizerSize.Height == 0 ? 1 : Math.Min(1, value.Y / _refreshVisualizerSize.Height); |
|||
break; |
|||
case PullDirection.LeftToRight: |
|||
case PullDirection.RightToLeft: |
|||
InteractionRatio = _refreshVisualizerSize.Height == 0 ? 1 : Math.Min(1, value.X / _refreshVisualizerSize.Width); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
using System; |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Provides event data for RefreshRequested events.
|
|||
/// </summary>
|
|||
public class RefreshRequestedEventArgs : RoutedEventArgs |
|||
{ |
|||
private RefreshCompletionDeferral _refreshCompletionDeferral; |
|||
|
|||
/// <summary>
|
|||
/// Gets a deferral object for managing the work done in the RefreshRequested event handler.
|
|||
/// </summary>
|
|||
/// <returns>A <see cref="RefreshCompletionDeferral"/> object</returns>
|
|||
public RefreshCompletionDeferral GetDeferral() |
|||
{ |
|||
return _refreshCompletionDeferral.Get(); |
|||
} |
|||
|
|||
public RefreshRequestedEventArgs(Action deferredAction, RoutedEvent? routedEvent) : base(routedEvent) |
|||
{ |
|||
_refreshCompletionDeferral = new RefreshCompletionDeferral(deferredAction); |
|||
} |
|||
|
|||
public RefreshRequestedEventArgs(RefreshCompletionDeferral completionDeferral, RoutedEvent? routedEvent) : base(routedEvent) |
|||
{ |
|||
_refreshCompletionDeferral = completionDeferral; |
|||
} |
|||
|
|||
internal void IncrementCount() |
|||
{ |
|||
_refreshCompletionDeferral?.Get(); |
|||
} |
|||
|
|||
internal void DecrementCount() |
|||
{ |
|||
_refreshCompletionDeferral?.Complete(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,553 @@ |
|||
using System; |
|||
using System.Numerics; |
|||
using System.Reactive.Linq; |
|||
using Avalonia.Animation.Easings; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Controls.PullToRefresh; |
|||
using Avalonia.Input; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Media; |
|||
using Avalonia.Rendering.Composition; |
|||
using Avalonia.Rendering.Composition.Animations; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class RefreshVisualizer : ContentControl |
|||
{ |
|||
private const int DefaultIndicatorSize = 24; |
|||
private const float MinimumIndicatorOpacity = 0.4f; |
|||
private const float ParallaxPositionRatio = 0.5f; |
|||
private double _executingRatio = 0.8; |
|||
|
|||
private RefreshVisualizerState _refreshVisualizerState; |
|||
private RefreshInfoProvider? _refreshInfoProvider; |
|||
private IDisposable? _isInteractingSubscription; |
|||
private IDisposable? _interactionRatioSubscription; |
|||
private bool _isInteractingForRefresh; |
|||
private Grid? _root; |
|||
private Control? _content; |
|||
private RefreshVisualizerOrientation _orientation; |
|||
private float _startingRotationAngle; |
|||
private double _interactionRatio; |
|||
private bool _played; |
|||
private ScalarKeyFrameAnimation? _rotateAnimation; |
|||
|
|||
private bool IsPullDirectionVertical => PullDirection == PullDirection.TopToBottom || PullDirection == PullDirection.BottomToTop; |
|||
private bool IsPullDirectionFar => PullDirection == PullDirection.BottomToTop || PullDirection == PullDirection.RightToLeft; |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="PullDirection"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<PullDirection> PullDirectionProperty = |
|||
AvaloniaProperty.Register<RefreshVisualizer, PullDirection>(nameof(PullDirection), PullDirection.TopToBottom); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="RefreshRequested"/> event.
|
|||
/// </summary>
|
|||
public static readonly RoutedEvent<RefreshRequestedEventArgs> RefreshRequestedEvent = |
|||
RoutedEvent.Register<RefreshVisualizer, RefreshRequestedEventArgs>(nameof(RefreshRequested), RoutingStrategies.Bubble); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="RefreshVisualizerState"/> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<RefreshVisualizer, RefreshVisualizerState> RefreshVisualizerStateProperty = |
|||
AvaloniaProperty.RegisterDirect<RefreshVisualizer, RefreshVisualizerState>(nameof(RefreshVisualizerState), |
|||
s => s.RefreshVisualizerState); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Orientation"/> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<RefreshVisualizer, RefreshVisualizerOrientation> OrientationProperty = |
|||
AvaloniaProperty.RegisterDirect<RefreshVisualizer, RefreshVisualizerOrientation>(nameof(Orientation), |
|||
s => s.Orientation, (s, o) => s.Orientation = o); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="RefreshInfoProvider"/> property.
|
|||
/// </summary>
|
|||
internal DirectProperty<RefreshVisualizer, RefreshInfoProvider?> RefreshInfoProviderProperty = |
|||
AvaloniaProperty.RegisterDirect<RefreshVisualizer, RefreshInfoProvider?>(nameof(RefreshInfoProvider), |
|||
s => s.RefreshInfoProvider, (s, o) => s.RefreshInfoProvider = o); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value that indicates the refresh state of the visualizer.
|
|||
/// </summary>
|
|||
protected RefreshVisualizerState RefreshVisualizerState |
|||
{ |
|||
get |
|||
{ |
|||
return _refreshVisualizerState; |
|||
} |
|||
private set |
|||
{ |
|||
SetAndRaise(RefreshVisualizerStateProperty, ref _refreshVisualizerState, value); |
|||
UpdateContent(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value that indicates the orientation of the visualizer.
|
|||
/// </summary>
|
|||
public RefreshVisualizerOrientation Orientation |
|||
{ |
|||
get |
|||
{ |
|||
return _orientation; |
|||
} |
|||
set |
|||
{ |
|||
SetAndRaise(OrientationProperty, ref _orientation, value); |
|||
} |
|||
} |
|||
|
|||
internal PullDirection PullDirection |
|||
{ |
|||
get => GetValue(PullDirectionProperty); |
|||
set => SetValue(PullDirectionProperty, value); |
|||
} |
|||
|
|||
internal RefreshInfoProvider? RefreshInfoProvider |
|||
{ |
|||
get => _refreshInfoProvider; |
|||
set |
|||
{ |
|||
if (_refreshInfoProvider != null) |
|||
{ |
|||
_refreshInfoProvider.RenderTransform = null; |
|||
} |
|||
SetAndRaise(RefreshInfoProviderProperty, ref _refreshInfoProvider, value); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Occurs when an update of the content has been initiated.
|
|||
/// </summary>
|
|||
public event EventHandler<RefreshRequestedEventArgs>? RefreshRequested |
|||
{ |
|||
add => AddHandler(RefreshRequestedEvent, value); |
|||
remove => RemoveHandler(RefreshRequestedEvent, value); |
|||
} |
|||
|
|||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) |
|||
{ |
|||
base.OnApplyTemplate(e); |
|||
|
|||
this.ClipToBounds = false; |
|||
|
|||
_root = e.NameScope.Find<Grid>("PART_Root"); |
|||
|
|||
if (_root != null) |
|||
{ |
|||
_content = Content as Control; |
|||
|
|||
if (_content == null) |
|||
{ |
|||
_content = new PathIcon() |
|||
{ |
|||
Height = DefaultIndicatorSize, |
|||
Width = DefaultIndicatorSize, |
|||
Name = "PART_Icon" |
|||
}; |
|||
|
|||
_content.Loaded += (s, e) => |
|||
{ |
|||
var composition = ElementComposition.GetElementVisual(_content); |
|||
var compositor = composition!.Compositor; |
|||
composition.Opacity = 0; |
|||
|
|||
var smoothRotationAnimation |
|||
= compositor.CreateScalarKeyFrameAnimation(); |
|||
smoothRotationAnimation.Target = "RotationAngle"; |
|||
smoothRotationAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing()); |
|||
smoothRotationAnimation.Duration = TimeSpan.FromMilliseconds(100); |
|||
|
|||
var opacityAnimation |
|||
= compositor.CreateScalarKeyFrameAnimation(); |
|||
opacityAnimation.Target = "Opacity"; |
|||
opacityAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing()); |
|||
opacityAnimation.Duration = TimeSpan.FromMilliseconds(100); |
|||
|
|||
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); |
|||
offsetAnimation.Target = "Offset"; |
|||
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing()); |
|||
offsetAnimation.Duration = TimeSpan.FromMilliseconds(150); |
|||
|
|||
var scaleAnimation |
|||
= compositor.CreateVector3KeyFrameAnimation(); |
|||
scaleAnimation.Target = "Scale"; |
|||
scaleAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing()); |
|||
scaleAnimation.Duration = TimeSpan.FromMilliseconds(100); |
|||
|
|||
var animation = compositor.CreateImplicitAnimationCollection(); |
|||
animation["RotationAngle"] = smoothRotationAnimation; |
|||
animation["Offset"] = offsetAnimation; |
|||
animation["Scale"] = scaleAnimation; |
|||
animation["Opacity"] = opacityAnimation; |
|||
|
|||
composition.ImplicitAnimations = animation; |
|||
|
|||
UpdateContent(); |
|||
}; |
|||
|
|||
Content = _content; |
|||
} |
|||
else |
|||
{ |
|||
RaisePropertyChanged(ContentProperty, null, Content, Data.BindingPriority.Style, false); |
|||
} |
|||
} |
|||
|
|||
OnOrientationChanged(); |
|||
|
|||
UpdateContent(); |
|||
} |
|||
|
|||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
base.OnAttachedToVisualTree(e); |
|||
|
|||
UpdateContent(); |
|||
} |
|||
|
|||
private void UpdateContent() |
|||
{ |
|||
if (_content != null && _root != null) |
|||
{ |
|||
var root = _root; |
|||
var visual = _refreshInfoProvider?.Visual; |
|||
var contentVisual = ElementComposition.GetElementVisual(_content); |
|||
var visualizerVisual = ElementComposition.GetElementVisual(this); |
|||
if (visual != null && contentVisual != null && visualizerVisual != null) |
|||
{ |
|||
contentVisual.CenterPoint = new Vector3((float)(_content.Bounds.Width / 2), (float)(_content.Bounds.Height / 2), 0); |
|||
switch (RefreshVisualizerState) |
|||
{ |
|||
case RefreshVisualizerState.Idle: |
|||
_played = false; |
|||
if(_rotateAnimation != null) |
|||
{ |
|||
_rotateAnimation.IterationBehavior = AnimationIterationBehavior.Count; |
|||
_rotateAnimation = null; |
|||
} |
|||
contentVisual.Opacity = MinimumIndicatorOpacity; |
|||
contentVisual.RotationAngle = _startingRotationAngle; |
|||
visualizerVisual.Offset = IsPullDirectionVertical ? |
|||
new Vector3(visualizerVisual.Offset.X, 0, 0) : |
|||
new Vector3(0, visualizerVisual.Offset.Y, 0); |
|||
visual.Offset = default; |
|||
_content.InvalidateMeasure(); |
|||
break; |
|||
case RefreshVisualizerState.Interacting: |
|||
_played = false; |
|||
contentVisual.Opacity = MinimumIndicatorOpacity; |
|||
contentVisual.RotationAngle = (float)(_startingRotationAngle + _interactionRatio * 2 * Math.PI); |
|||
Vector3 offset = default; |
|||
if (IsPullDirectionVertical) |
|||
{ |
|||
offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); |
|||
} |
|||
else |
|||
{ |
|||
offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); |
|||
} |
|||
visual.Offset = offset; |
|||
visualizerVisual.Offset = IsPullDirectionVertical ? |
|||
new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : |
|||
new Vector3(offset.X, visualizerVisual.Offset.Y, 0); |
|||
break; |
|||
case RefreshVisualizerState.Pending: |
|||
contentVisual.Opacity = 1; |
|||
contentVisual.RotationAngle = _startingRotationAngle + (float)(2 * Math.PI); |
|||
if (IsPullDirectionVertical) |
|||
{ |
|||
offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); |
|||
} |
|||
else |
|||
{ |
|||
offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); |
|||
} |
|||
visual.Offset = offset; |
|||
visualizerVisual.Offset = IsPullDirectionVertical ? |
|||
new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : |
|||
new Vector3(offset.X, visualizerVisual.Offset.Y, 0); |
|||
|
|||
if (!_played) |
|||
{ |
|||
_played = true; |
|||
var scaleAnimation = contentVisual.Compositor!.CreateVector3KeyFrameAnimation(); |
|||
scaleAnimation.Target = "Scale"; |
|||
scaleAnimation.InsertKeyFrame(0.5f, new Vector3(1.5f, 1.5f, 1)); |
|||
scaleAnimation.InsertKeyFrame(1f, new Vector3(1f, 1f, 1)); |
|||
scaleAnimation.Duration = TimeSpan.FromSeconds(0.3); |
|||
|
|||
contentVisual.StartAnimation("Scale", scaleAnimation); |
|||
} |
|||
break; |
|||
case RefreshVisualizerState.Refreshing: |
|||
_rotateAnimation = contentVisual.Compositor!.CreateScalarKeyFrameAnimation(); |
|||
_rotateAnimation.Target = "RotationAngle"; |
|||
_rotateAnimation.InsertKeyFrame(0, _startingRotationAngle, new LinearEasing()); |
|||
_rotateAnimation.InsertKeyFrame(1, _startingRotationAngle + (float)(2 * Math.PI), new LinearEasing()); |
|||
_rotateAnimation.IterationBehavior = AnimationIterationBehavior.Forever; |
|||
_rotateAnimation.StopBehavior = AnimationStopBehavior.LeaveCurrentValue; |
|||
_rotateAnimation.Duration = TimeSpan.FromSeconds(0.5); |
|||
|
|||
contentVisual.StartAnimation("RotationAngle", _rotateAnimation); |
|||
contentVisual.Opacity = 1; |
|||
float translationRatio = (float)(_refreshInfoProvider != null ? (1.0f - _refreshInfoProvider.ExecutionRatio) * ParallaxPositionRatio : 1.0f) |
|||
* (IsPullDirectionFar ? -1f : 1f); |
|||
if (IsPullDirectionVertical) |
|||
{ |
|||
offset = new Vector3(0, (float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); |
|||
} |
|||
else |
|||
{ |
|||
offset = new Vector3((float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); |
|||
} |
|||
visual.Offset = offset; |
|||
contentVisual.Offset += IsPullDirectionVertical ? new Vector3(0, (float)(translationRatio * root.Bounds.Height), 0) : |
|||
new Vector3((float)(translationRatio * root.Bounds.Width), 0, 0); |
|||
visualizerVisual.Offset = IsPullDirectionVertical ? |
|||
new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : |
|||
new Vector3(offset.X, visualizerVisual.Offset.Y, 0); |
|||
break; |
|||
case RefreshVisualizerState.Peeking: |
|||
contentVisual.Opacity = 1; |
|||
contentVisual.RotationAngle = _startingRotationAngle; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initiates an update of the content.
|
|||
/// </summary>
|
|||
public void RequestRefresh() |
|||
{ |
|||
RefreshVisualizerState = RefreshVisualizerState.Refreshing; |
|||
RefreshInfoProvider?.OnRefreshStarted(); |
|||
|
|||
RaiseRefreshRequested(); |
|||
} |
|||
|
|||
private void RefreshCompleted() |
|||
{ |
|||
RefreshVisualizerState = RefreshVisualizerState.Idle; |
|||
|
|||
RefreshInfoProvider?.OnRefreshCompleted(); |
|||
} |
|||
|
|||
private void RaiseRefreshRequested() |
|||
{ |
|||
var refreshArgs = new RefreshRequestedEventArgs(RefreshCompleted, RefreshRequestedEvent); |
|||
|
|||
refreshArgs.IncrementCount(); |
|||
|
|||
RaiseEvent(refreshArgs); |
|||
|
|||
refreshArgs.DecrementCount(); |
|||
} |
|||
|
|||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) |
|||
{ |
|||
base.OnPropertyChanged(change); |
|||
|
|||
if (change.Property == RefreshInfoProviderProperty) |
|||
{ |
|||
OnRefreshInfoProviderChanged(); |
|||
} |
|||
else if (change.Property == ContentProperty) |
|||
{ |
|||
if (_root != null && _content != null) |
|||
{ |
|||
_root.Children.Insert(0, _content); |
|||
_content.VerticalAlignment = Layout.VerticalAlignment.Center; |
|||
_content.HorizontalAlignment = Layout.HorizontalAlignment.Center; |
|||
} |
|||
|
|||
UpdateContent(); |
|||
} |
|||
else if (change.Property == OrientationProperty) |
|||
{ |
|||
OnOrientationChanged(); |
|||
|
|||
UpdateContent(); |
|||
} |
|||
else if (change.Property == BoundsProperty) |
|||
{ |
|||
switch (PullDirection) |
|||
{ |
|||
case PullDirection.TopToBottom: |
|||
RenderTransform = new TranslateTransform(0, -Bounds.Height); |
|||
break; |
|||
case PullDirection.BottomToTop: |
|||
RenderTransform = new TranslateTransform(0, Bounds.Height); |
|||
break; |
|||
case PullDirection.LeftToRight: |
|||
RenderTransform = new TranslateTransform(-Bounds.Width, 0); |
|||
break; |
|||
case PullDirection.RightToLeft: |
|||
RenderTransform = new TranslateTransform(Bounds.Width, 0); |
|||
break; |
|||
} |
|||
|
|||
UpdateContent(); |
|||
} |
|||
else if(change.Property == PullDirectionProperty) |
|||
{ |
|||
OnOrientationChanged(); |
|||
|
|||
UpdateContent(); |
|||
} |
|||
} |
|||
|
|||
private void OnOrientationChanged() |
|||
{ |
|||
switch (_orientation) |
|||
{ |
|||
case RefreshVisualizerOrientation.Auto: |
|||
switch (PullDirection) |
|||
{ |
|||
case PullDirection.TopToBottom: |
|||
case PullDirection.BottomToTop: |
|||
_startingRotationAngle = 0.0f; |
|||
break; |
|||
case PullDirection.LeftToRight: |
|||
_startingRotationAngle = (float)(-Math.PI / 2); |
|||
break; |
|||
case PullDirection.RightToLeft: |
|||
_startingRotationAngle = (float)(Math.PI / 2); |
|||
break; |
|||
} |
|||
break; |
|||
case RefreshVisualizerOrientation.Normal: |
|||
_startingRotationAngle = 0.0f; |
|||
break; |
|||
case RefreshVisualizerOrientation.Rotate90DegreesCounterclockwise: |
|||
_startingRotationAngle = (float)(Math.PI / 2); |
|||
break; |
|||
case RefreshVisualizerOrientation.Rotate270DegreesCounterclockwise: |
|||
_startingRotationAngle = (float)(-Math.PI / 2); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
private void OnRefreshInfoProviderChanged() |
|||
{ |
|||
_isInteractingSubscription?.Dispose(); |
|||
_isInteractingSubscription = null; |
|||
_interactionRatioSubscription?.Dispose(); |
|||
_interactionRatioSubscription = null; |
|||
|
|||
if (RefreshInfoProvider != null) |
|||
{ |
|||
_isInteractingSubscription = RefreshInfoProvider.GetObservable(RefreshInfoProvider.IsInteractingForRefreshProperty) |
|||
.Subscribe(InteractingForRefreshObserver); |
|||
|
|||
_interactionRatioSubscription = RefreshInfoProvider.GetObservable(RefreshInfoProvider.InteractionRatioProperty) |
|||
.Subscribe(InteractionRatioObserver); |
|||
|
|||
var visual = RefreshInfoProvider.Visual; |
|||
|
|||
_executingRatio = RefreshInfoProvider.ExecutionRatio; |
|||
} |
|||
else |
|||
{ |
|||
_executingRatio = 1; |
|||
} |
|||
} |
|||
|
|||
private void InteractionRatioObserver(double obj) |
|||
{ |
|||
var wasAtZero = _interactionRatio == 0.0; |
|||
_interactionRatio = obj; |
|||
|
|||
if (_isInteractingForRefresh) |
|||
{ |
|||
if (RefreshVisualizerState == RefreshVisualizerState.Idle) |
|||
{ |
|||
if (wasAtZero) |
|||
{ |
|||
if (_interactionRatio > _executingRatio) |
|||
{ |
|||
RefreshVisualizerState = RefreshVisualizerState.Pending; |
|||
} |
|||
else if (_interactionRatio > 0) |
|||
{ |
|||
RefreshVisualizerState = RefreshVisualizerState.Interacting; |
|||
} |
|||
} |
|||
else if (_interactionRatio > 0) |
|||
{ |
|||
RefreshVisualizerState = RefreshVisualizerState.Peeking; |
|||
} |
|||
} |
|||
else if (RefreshVisualizerState == RefreshVisualizerState.Interacting) |
|||
{ |
|||
if (_interactionRatio <= 0) |
|||
{ |
|||
RefreshVisualizerState = RefreshVisualizerState.Idle; |
|||
} |
|||
else if (_interactionRatio > _executingRatio) |
|||
{ |
|||
RefreshVisualizerState = RefreshVisualizerState.Pending; |
|||
} |
|||
else |
|||
{ |
|||
UpdateContent(); |
|||
} |
|||
} |
|||
else if (RefreshVisualizerState == RefreshVisualizerState.Pending) |
|||
{ |
|||
if (_interactionRatio <= _executingRatio) |
|||
{ |
|||
RefreshVisualizerState = RefreshVisualizerState.Interacting; |
|||
} |
|||
else if (_interactionRatio <= 0) |
|||
{ |
|||
RefreshVisualizerState = RefreshVisualizerState.Idle; |
|||
} |
|||
else |
|||
{ |
|||
UpdateContent(); |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (RefreshVisualizerState != RefreshVisualizerState.Refreshing) |
|||
{ |
|||
if (_interactionRatio > 0) |
|||
{ |
|||
RefreshVisualizerState = RefreshVisualizerState.Peeking; |
|||
} |
|||
else |
|||
{ |
|||
RefreshVisualizerState = RefreshVisualizerState.Idle; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void InteractingForRefreshObserver(bool obj) |
|||
{ |
|||
_isInteractingForRefresh = obj; |
|||
|
|||
if (!_isInteractingForRefresh) |
|||
{ |
|||
switch (_refreshVisualizerState) |
|||
{ |
|||
case RefreshVisualizerState.Pending: |
|||
RequestRefresh(); |
|||
break; |
|||
case RefreshVisualizerState.Refreshing: |
|||
// We don't want to interrupt a currently executing refresh.
|
|||
break; |
|||
default: |
|||
RefreshVisualizerState = RefreshVisualizerState.Idle; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Defines constants that specify the orientation of a RefreshVisualizer.
|
|||
/// </summary>
|
|||
public enum RefreshVisualizerOrientation |
|||
{ |
|||
Auto, |
|||
Normal, |
|||
Rotate90DegreesCounterclockwise, |
|||
Rotate270DegreesCounterclockwise |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Defines constants that specify the state of a RefreshVisualizer
|
|||
/// </summary>
|
|||
public enum RefreshVisualizerState |
|||
{ |
|||
Idle, |
|||
Peeking, |
|||
Interacting, |
|||
Pending, |
|||
Refreshing |
|||
} |
|||
} |
|||
@ -0,0 +1,274 @@ |
|||
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(Visual 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(Visual 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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
<ResourceDictionary xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
|||
<ControlTheme x:Key="{x:Type RefreshContainer}" |
|||
TargetType="RefreshContainer"> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Grid> |
|||
<ContentPresenter Name="PART_ContentPresenter" |
|||
Background="{TemplateBinding Background}" |
|||
BorderBrush="{TemplateBinding BorderBrush}" |
|||
BorderThickness="{TemplateBinding BorderThickness}" |
|||
CornerRadius="{TemplateBinding CornerRadius}" |
|||
ContentTemplate="{TemplateBinding ContentTemplate}" |
|||
Content="{TemplateBinding Content}" |
|||
Padding="{TemplateBinding Padding}" |
|||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" |
|||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"> |
|||
</ContentPresenter> |
|||
<Grid Name="PART_RefreshVisualizerPresenter"/> |
|||
</Grid> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</ControlTheme> |
|||
</ResourceDictionary> |
|||
@ -0,0 +1,29 @@ |
|||
<ResourceDictionary xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
|||
<ControlTheme x:Key="{x:Type RefreshVisualizer}" |
|||
TargetType="RefreshVisualizer"> |
|||
<Setter Property="IsTabStop" Value="False"/> |
|||
<Setter Property="IsHitTestVisible" Value="False"/> |
|||
<Setter Property="Height" |
|||
Value="100"/> |
|||
<Setter Property="Background" |
|||
Value="{DynamicResource RefreshVisualizerBackground}"/> |
|||
<Setter Property="Foreground" |
|||
Value="{DynamicResource RefreshVisualizerForeground}"/> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Grid Name="PART_Root" |
|||
MinHeight="80" |
|||
Background="{TemplateBinding Background}"> |
|||
<Grid.Styles> |
|||
<Style Selector="PathIcon#PART_Icon"> |
|||
<Setter Property="Data" |
|||
Value="M18.6195264,3.31842271 C19.0080059,3.31842271 19.3290603,3.60710385 19.3798716,3.9816481 L19.3868766,4.08577298 L19.3868766,6.97963208 C19.3868766,7.36811161 19.0981955,7.68916605 18.7236513,7.73997735 L18.6195264,7.74698235 L15.7256673,7.74698235 C15.3018714,7.74698235 14.958317,7.40342793 14.958317,6.97963208 C14.958317,6.59115255 15.2469981,6.27009811 15.6215424,6.21928681 L15.7256673,6.21228181 L16.7044011,6.21182461 C13.7917384,3.87107476 9.52212532,4.05209336 6.81933829,6.75488039 C3.92253872,9.65167996 3.92253872,14.34832 6.81933829,17.2451196 C9.71613786,20.1419192 14.4127779,20.1419192 17.3095775,17.2451196 C19.0725398,15.4821573 19.8106555,12.9925923 19.3476248,10.58925 C19.2674502,10.173107 19.5398064,9.77076216 19.9559494,9.69058758 C20.3720923,9.610413 20.7744372,9.88276918 20.8546118,10.2989121 C21.4129973,13.1971899 20.5217103,16.2033812 18.3947747,18.3303168 C14.8986373,21.8264542 9.23027854,21.8264542 5.73414113,18.3303168 C2.23800371,14.8341794 2.23800371,9.16582064 5.73414113,5.66968323 C9.05475132,2.34907304 14.3349409,2.18235834 17.8523166,5.16953912 L17.8521761,4.08577298 C17.8521761,3.66197713 18.1957305,3.31842271 18.6195264,3.31842271 Z"> |
|||
</Setter> |
|||
</Style> |
|||
</Grid.Styles> |
|||
</Grid> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</ControlTheme> |
|||
</ResourceDictionary> |
|||
@ -0,0 +1,24 @@ |
|||
<ResourceDictionary xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
|||
<ControlTheme x:Key="{x:Type RefreshContainer}" |
|||
TargetType="RefreshContainer"> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Grid> |
|||
<ContentPresenter Name="PART_ContentPresenter" |
|||
Background="{TemplateBinding Background}" |
|||
BorderBrush="{TemplateBinding BorderBrush}" |
|||
BorderThickness="{TemplateBinding BorderThickness}" |
|||
CornerRadius="{TemplateBinding CornerRadius}" |
|||
ContentTemplate="{TemplateBinding ContentTemplate}" |
|||
Content="{TemplateBinding Content}" |
|||
Padding="{TemplateBinding Padding}" |
|||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" |
|||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"> |
|||
</ContentPresenter> |
|||
<Grid Name="PART_RefreshVisualizerPresenter"/> |
|||
</Grid> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</ControlTheme> |
|||
</ResourceDictionary> |
|||
@ -0,0 +1,31 @@ |
|||
<ResourceDictionary xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
|||
<ControlTheme x:Key="{x:Type RefreshVisualizer}" |
|||
TargetType="RefreshVisualizer"> |
|||
<Setter Property="IsTabStop" |
|||
Value="False"/> |
|||
<Setter Property="IsHitTestVisible" |
|||
Value="False"/> |
|||
<Setter Property="Height" |
|||
Value="100"/> |
|||
<Setter Property="Background" |
|||
Value="{DynamicResource RefreshVisualizerBackground}"/> |
|||
<Setter Property="Foreground" |
|||
Value="{DynamicResource RefreshVisualizerForeground}"/> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Grid Name="PART_Root" |
|||
MinHeight="80" |
|||
Background="{TemplateBinding Background}"> |
|||
<Grid.Styles> |
|||
<Style Selector="PathIcon#PART_Icon"> |
|||
<Setter Property="Data" |
|||
Value="M18.6195264,3.31842271 C19.0080059,3.31842271 19.3290603,3.60710385 19.3798716,3.9816481 L19.3868766,4.08577298 L19.3868766,6.97963208 C19.3868766,7.36811161 19.0981955,7.68916605 18.7236513,7.73997735 L18.6195264,7.74698235 L15.7256673,7.74698235 C15.3018714,7.74698235 14.958317,7.40342793 14.958317,6.97963208 C14.958317,6.59115255 15.2469981,6.27009811 15.6215424,6.21928681 L15.7256673,6.21228181 L16.7044011,6.21182461 C13.7917384,3.87107476 9.52212532,4.05209336 6.81933829,6.75488039 C3.92253872,9.65167996 3.92253872,14.34832 6.81933829,17.2451196 C9.71613786,20.1419192 14.4127779,20.1419192 17.3095775,17.2451196 C19.0725398,15.4821573 19.8106555,12.9925923 19.3476248,10.58925 C19.2674502,10.173107 19.5398064,9.77076216 19.9559494,9.69058758 C20.3720923,9.610413 20.7744372,9.88276918 20.8546118,10.2989121 C21.4129973,13.1971899 20.5217103,16.2033812 18.3947747,18.3303168 C14.8986373,21.8264542 9.23027854,21.8264542 5.73414113,18.3303168 C2.23800371,14.8341794 2.23800371,9.16582064 5.73414113,5.66968323 C9.05475132,2.34907304 14.3349409,2.18235834 17.8523166,5.16953912 L17.8521761,4.08577298 C17.8521761,3.66197713 18.1957305,3.31842271 18.6195264,3.31842271 Z"> |
|||
</Setter> |
|||
</Style> |
|||
</Grid.Styles> |
|||
</Grid> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</ControlTheme> |
|||
</ResourceDictionary> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue