diff --git a/src/Avalonia/HandyControlDemo_Avalonia/Data/DemoInfo.json b/src/Avalonia/HandyControlDemo_Avalonia/Data/DemoInfo.json index d2c0eb21..cc757c13 100644 --- a/src/Avalonia/HandyControlDemo_Avalonia/Data/DemoInfo.json +++ b/src/Avalonia/HandyControlDemo_Avalonia/Data/DemoInfo.json @@ -46,6 +46,7 @@ [ "ButtonGroup", "ButtonGroupDemo", "Brush.ButtonGroup", "", "Button" ], [ "ProgressButton", "ProgressButtonDemo", "Brush.ProgressBarStatusBar8", "", "Button" ], [ "Card", "CardDemo", "Brush.ContactCard", "", "DataDisplay" ], + [ "GotoTop", "GotoTopDemo", "Brush.GoToTop", "", "Interacting" ], [ "PinBox", "PinBoxDemo", "Brush.Ellipsis", "", "DataEntry" ], [ "SearchBar", "SearchBarDemo", "Brush.Search", "", "DataEntry" ], [ "ElementGroup", "ElementGroupDemo", "Brush.ButtonGroup", "", "Layout" ], diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/GotoTopDemo.axaml b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/GotoTopDemo.axaml new file mode 100644 index 00000000..5af2e959 --- /dev/null +++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/GotoTopDemo.axaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/GotoTopDemo.axaml.cs b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/GotoTopDemo.axaml.cs new file mode 100644 index 00000000..bbeb8ebd --- /dev/null +++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/GotoTopDemo.axaml.cs @@ -0,0 +1,9 @@ +namespace HandyControlDemo.UserControl; + +public partial class GotoTopDemo : Avalonia.Controls.UserControl +{ + public GotoTopDemo() + { + InitializeComponent(); + } +} diff --git a/src/Avalonia/HandyControl_Avalonia/Controls/Other/GotoTop.cs b/src/Avalonia/HandyControl_Avalonia/Controls/Other/GotoTop.cs new file mode 100644 index 00000000..41d82af8 --- /dev/null +++ b/src/Avalonia/HandyControl_Avalonia/Controls/Other/GotoTop.cs @@ -0,0 +1,193 @@ +using System; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace HandyControl.Controls; + +public class GotoTop : Button +{ + public static readonly StyledProperty TargetProperty = + AvaloniaProperty.Register(nameof(Target)); + + public static readonly StyledProperty AnimatedProperty = + AvaloniaProperty.Register(nameof(Animated), true); + + public static readonly StyledProperty AnimationTimeProperty = + AvaloniaProperty.Register(nameof(AnimationTime), 200d); + + public static readonly StyledProperty HidingHeightProperty = + AvaloniaProperty.Register(nameof(HidingHeight)); + + public static readonly StyledProperty AutoHidingProperty = + AvaloniaProperty.Register(nameof(AutoHiding), true); + + private ScrollViewer? _scrollViewer; + private DispatcherTimer? _animationTimer; + private DateTime _animationStartTime; + private double _animationStartOffset; + + static GotoTop() + { + TargetProperty.Changed.AddClassHandler((gotoTop, e) => gotoTop.CreateGotoAction(e.GetNewValue())); + AutoHidingProperty.Changed.AddClassHandler((gotoTop, _) => gotoTop.UpdateVisibility()); + HidingHeightProperty.Changed.AddClassHandler((gotoTop, _) => gotoTop.UpdateVisibility()); + } + + public GotoTop() + { + Loaded += (_, _) => CreateGotoAction(Target); + } + + public Control? Target + { + get => GetValue(TargetProperty); + set => SetValue(TargetProperty, value); + } + + public bool Animated + { + get => GetValue(AnimatedProperty); + set => SetValue(AnimatedProperty, value); + } + + public double AnimationTime + { + get => GetValue(AnimationTimeProperty); + set => SetValue(AnimationTimeProperty, value); + } + + public double HidingHeight + { + get => GetValue(HidingHeightProperty); + set => SetValue(HidingHeightProperty, value); + } + + public bool AutoHiding + { + get => GetValue(AutoHidingProperty); + set => SetValue(AutoHidingProperty, value); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + if (_scrollViewer != null) + { + _scrollViewer.ScrollChanged -= ScrollViewerOnScrollChanged; + } + + if (_animationTimer != null) + { + _animationTimer.Stop(); + } + } + + public virtual void CreateGotoAction(Control? obj) + { + if (_scrollViewer != null) + { + _scrollViewer.ScrollChanged -= ScrollViewerOnScrollChanged; + } + + _scrollViewer = null; + var target = obj ?? this; + + if (target is ScrollViewer directScrollViewer) + { + _scrollViewer = directScrollViewer; + } + else + { + _scrollViewer = target.GetVisualDescendants().OfType().FirstOrDefault(); + } + + if (_scrollViewer != null) + { + _scrollViewer.ScrollChanged += ScrollViewerOnScrollChanged; + } + + UpdateVisibility(); + } + + private void ScrollViewerOnScrollChanged(object? sender, ScrollChangedEventArgs e) + { + UpdateVisibility(); + } + + private void UpdateVisibility() + { + if (!AutoHiding) + { + IsVisible = true; + return; + } + + IsVisible = (_scrollViewer?.Offset.Y ?? 0d) >= HidingHeight; + } + + protected override void OnClick() + { + base.OnClick(); + + if (_scrollViewer == null) + { + return; + } + + if (!Animated || AnimationTime <= 0) + { + _scrollViewer.Offset = new Vector(_scrollViewer.Offset.X, 0); + return; + } + + StartAnimatedScroll(); + } + + private void StartAnimatedScroll() + { + if (_scrollViewer == null) + { + return; + } + + _animationStartOffset = _scrollViewer.Offset.Y; + if (_animationStartOffset <= 0) + { + return; + } + + _animationStartTime = DateTime.Now; + + _animationTimer ??= new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(16) }; + _animationTimer.Tick -= AnimationTimerOnTick; + _animationTimer.Tick += AnimationTimerOnTick; + _animationTimer.Start(); + } + + private void AnimationTimerOnTick(object? sender, EventArgs e) + { + if (_scrollViewer == null) + { + _animationTimer?.Stop(); + return; + } + + var total = Math.Max(AnimationTime, 1d); + var elapsed = (DateTime.Now - _animationStartTime).TotalMilliseconds; + var progress = Math.Clamp(elapsed / total, 0d, 1d); + + var current = _animationStartOffset * (1d - progress); + _scrollViewer.Offset = new Vector(_scrollViewer.Offset.X, current); + + if (progress >= 1d) + { + _animationTimer?.Stop(); + _scrollViewer.Offset = new Vector(_scrollViewer.Offset.X, 0d); + } + } +} diff --git a/src/Avalonia/HandyControl_Avalonia/Themes/Styles/GotoTop.axaml b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/GotoTop.axaml new file mode 100644 index 00000000..b040f557 --- /dev/null +++ b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/GotoTop.axaml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia/HandyControl_Avalonia/Themes/Styles/MergedStyles.axaml b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/MergedStyles.axaml index 1dab63d2..478293bd 100644 --- a/src/Avalonia/HandyControl_Avalonia/Themes/Styles/MergedStyles.axaml +++ b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/MergedStyles.axaml @@ -75,6 +75,7 @@ +