feat: Add GotoTop control, style and demo.

This commit is contained in:
NaBian committed 2026-06-20 19:40:46 +08:00
1 parent 550b980f54
commit 2c91527181
6 files changed
+283

No files matched your search

@@ -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" ],
@@ -0,0 +1,34 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
x:Class="HandyControlDemo.UserControl.GotoTopDemo"
Background="{DynamicResource RegionBrush}">
<TransitioningContentControl>
<Panel Height="400">
<ScrollViewer x:Name="ScrollViewerDemo"
Height="390"
VerticalAlignment="Top"
Margin="0,10,0,0">
<Border Height="2000"
Margin="8,0">
<Border.Background>
<LinearGradientBrush StartPoint="50%,0%"
EndPoint="50%,100%">
<GradientStop Offset="0"
Color="White" />
<GradientStop Offset="1"
Color="Black" />
</LinearGradientBrush>
</Border.Background>
</Border>
</ScrollViewer>
<hc:GotoTop Animated="True"
AutoHiding="True"
AnimationTime="500"
Target="{Binding #ScrollViewerDemo}"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Margin="0,0,20,20" />
</Panel>
</TransitioningContentControl>
</UserControl>
@@ -0,0 +1,9 @@
namespace HandyControlDemo.UserControl;
public partial class GotoTopDemo : Avalonia.Controls.UserControl
{
public GotoTopDemo()
{
InitializeComponent();
}
}
@@ -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<Control?> TargetProperty =
AvaloniaProperty.Register<GotoTop, Control?>(nameof(Target));
public static readonly StyledProperty<bool> AnimatedProperty =
AvaloniaProperty.Register<GotoTop, bool>(nameof(Animated), true);
public static readonly StyledProperty<double> AnimationTimeProperty =
AvaloniaProperty.Register<GotoTop, double>(nameof(AnimationTime), 200d);
public static readonly StyledProperty<double> HidingHeightProperty =
AvaloniaProperty.Register<GotoTop, double>(nameof(HidingHeight));
public static readonly StyledProperty<bool> AutoHidingProperty =
AvaloniaProperty.Register<GotoTop, bool>(nameof(AutoHiding), true);
private ScrollViewer? _scrollViewer;
private DispatcherTimer? _animationTimer;
private DateTime _animationStartTime;
private double _animationStartOffset;
static GotoTop()
{
TargetProperty.Changed.AddClassHandler<GotoTop>((gotoTop, e) => gotoTop.CreateGotoAction(e.GetNewValue<Control?>()));
AutoHidingProperty.Changed.AddClassHandler<GotoTop>((gotoTop, _) => gotoTop.UpdateVisibility());
HidingHeightProperty.Changed.AddClassHandler<GotoTop>((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<ScrollViewer>().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);
}
}
}
@@ -0,0 +1,45 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal"
xmlns:hc="clr-namespace:HandyControl.Controls">
<ControlTheme x:Key="GotoTopBaseStyle"
BasedOn="{StaticResource ButtonBaseStyle}"
TargetType="hc:GotoTop">
<Setter Property="Width"
Value="50" />
<Setter Property="Height"
Value="50" />
<Setter Property="hc:BorderElement.CornerRadius"
Value="25" />
<Setter Property="CornerRadius"
Value="{Binding $self.(hc:BorderElement.CornerRadius)}" />
<Setter Property="hc:IconElement.Geometry"
Value="{StaticResource UpGeometry}" />
<Setter Property="hc:IconElement.Width"
Value="14" />
<Setter Property="hc:IconElement.Height"
Value="14" />
<Setter Property="Foreground"
Value="{DynamicResource PrimaryTextBrush}" />
<Setter Property="Padding"
Value="16" />
<Setter Property="BorderThickness"
Value="1" />
<Style Selector="^:pointerover">
<Setter Property="Background"
Value="{DynamicResource SecondaryRegionBrush}" />
</Style>
<Style Selector="^:pressed">
<Setter Property="Background"
Value="{DynamicResource BorderBrush}" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type hc:GotoTop}"
BasedOn="{StaticResource GotoTopBaseStyle}"
TargetType="hc:GotoTop" />
</ResourceDictionary>
@@ -75,6 +75,7 @@
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Gravatar.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Tag.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Badge.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/GotoTop.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Shield.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>