WIP: animation.

This commit is contained in:
rabbitism
2024-02-04 16:06:34 +08:00
parent f3cd4fb74b
commit ff8d4b1dcd
15 changed files with 282 additions and 16 deletions

View File

@@ -8,6 +8,7 @@ public static class MenuKeys
public const string MenuKeyButtonGroup = "ButtonGroup";
public const string MenuKeyDialog = "Dialog";
public const string MenuKeyDivider = "Divider";
public const string MenuKeyDrawer = "Drawer";
public const string MenuKeyDualBadge = "DualBadge";
public const string MenuKeyEnumSelector = "EnumSelector";
public const string MenuKeyImageViewer = "ImageViewer";

View File

@@ -0,0 +1,14 @@
<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"
xmlns:vm="clr-namespace:Ursa.Demo.ViewModels;assembly=Ursa.Demo"
x:DataType="vm:DrawerDemoViewModel"
x:CompileBindings="True"
d:DesignHeight="450"
x:Class="Ursa.Demo.Pages.DrawerDemo">
<StackPanel HorizontalAlignment="Left">
<Button Content="Call Drawer" Command="{Binding OpenDrawerCommand}"></Button>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Ursa.Demo.Pages;
public partial class DrawerDemo : UserControl
{
public DrawerDemo()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,23 @@
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Ursa.Controls;
namespace Ursa.Demo.ViewModels;
public class DrawerDemoViewModel: ObservableObject
{
public ICommand OpenDrawerCommand { get; set; }
public DrawerDemoViewModel()
{
OpenDrawerCommand = new AsyncRelayCommand(OpenDrawer);
}
private async Task OpenDrawer()
{
await Drawer.Show<Calendar, string, bool>("Hello World");
}
}

View File

@@ -30,6 +30,7 @@ public class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeyButtonGroup => new ButtonGroupDemoViewModel(),
MenuKeys.MenuKeyDialog => new DialogDemoViewModel(),
MenuKeys.MenuKeyDivider => new DividerDemoViewModel(),
MenuKeys.MenuKeyDrawer => new DrawerDemoViewModel(),
MenuKeys.MenuKeyDualBadge => new DualBadgeDemoViewModel(),
MenuKeys.MenuKeyEnumSelector => new EnumSelectorDemoViewModel(),
MenuKeys.MenuKeyImageViewer => new ImageViewerDemoViewModel(),

View File

@@ -17,6 +17,7 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "Button Group", Key = MenuKeys.MenuKeyButtonGroup, Status = "Updated"},
new() { MenuHeader = "Dialog", Key = MenuKeys.MenuKeyDialog },
new() { MenuHeader = "Divider", Key = MenuKeys.MenuKeyDivider },
new() { MenuHeader = "Drawer", Key = MenuKeys.MenuKeyDrawer },
new() { MenuHeader = "DualBadge", Key = MenuKeys.MenuKeyDualBadge },
new() { MenuHeader = "Enum Selector", Key = MenuKeys.MenuKeyEnumSelector },
new() { MenuHeader = "Icon Button", Key = MenuKeys.MenuKeyIconButton },

View File

@@ -0,0 +1,44 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="https://irihi.tech/ursa">
<ControlTheme TargetType="u:CustomDrawerControl" x:Key="{x:Type u:CustomDrawerControl}">
<Setter Property="VerticalAlignment" Value="Stretch"></Setter>
<Setter Property="MinWidth" Value="400"></Setter>
<Setter Property="Template">
<ControlTemplate TargetType="u:CustomDrawerControl">
<Border Margin="8 0 0 0"
Padding="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Classes="Shadow"
ClipToBounds="False"
CornerRadius="12 0 0 12"
IsHitTestVisible="True"
Theme="{DynamicResource CardBorder}">
<Border ClipToBounds="True" CornerRadius="12 0 0 12">
<Grid RowDefinitions="Auto, *">
<ContentPresenter
Name="PART_ContentPresenter"
Grid.Row="0"
Grid.RowSpan="2"
Content="{TemplateBinding Content}" />
<Grid Grid.Row="0" ColumnDefinitions="*, Auto">
<Panel
Name="{x:Static u:DialogControl.PART_TitleArea}"
Grid.Column="0"
Grid.ColumnSpan="2"
Background="Transparent" />
<Button
Name="{x:Static u:MessageBoxWindow.PART_CloseButton}"
Grid.Column="1"
Margin="0,24,24,0"
DockPanel.Dock="Right"
Theme="{DynamicResource CloseButton}" />
</Grid>
</Grid>
</Border>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

View File

@@ -7,6 +7,7 @@
<ResourceInclude Source="Dialog.axaml" />
<ResourceInclude Source="DialogShared.axaml" />
<ResourceInclude Source="Divider.axaml" />
<ResourceInclude Source="Drawer.axaml" />
<ResourceInclude Source="DualBadge.axaml" />
<ResourceInclude Source="EnumSelector.axaml" />
<ResourceInclude Source="IconButton.axaml" />

View File

@@ -9,7 +9,6 @@ using Ursa.EventArgs;
namespace Ursa.Controls;
[TemplatePart(PART_OKButton, typeof(Button))]
[TemplatePart(PART_CancelButton, typeof(Button))]
[TemplatePart(PART_YesButton, typeof(Button))]

View File

@@ -1,19 +1,22 @@
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.Utilities;
using Ursa.EventArgs;
namespace Ursa.Controls;
public class OverlayDialogHost : Canvas
{
private readonly List<DialogControl> _dialogs = new();
private readonly List<DialogControl> _modalDialogs = new();
private readonly List<Control> _modalDialogs = new();
private readonly List<Border> _masks = new();
public string? HostId { get; set; }
@@ -57,9 +60,16 @@ public class OverlayDialogHost : Canvas
if (sender is Border border)
{
int i = _masks.IndexOf(border);
DialogControl dialog = _modalDialogs[i];
dialog.CloseDialog();
border.RemoveHandler(PointerReleasedEvent, ClickBorderToCloseDialog);
if (_modalDialogs[i] is DialogControl dialog)
{
dialog?.CloseDialog();
border.RemoveHandler(PointerReleasedEvent, ClickBorderToCloseDialog);
}
else if(_modalDialogs[i] is DrawerControlBase drawer)
{
drawer.CloseDrawer();
border.RemoveHandler(PointerReleasedEvent, ClickBorderToCloseDialog);
}
}
}
@@ -87,7 +97,11 @@ public class OverlayDialogHost : Canvas
foreach (var modalDialog in _modalDialogs)
{
ResetDialogPosition(modalDialog, oldSize, newSize);
if (modalDialog is DialogControl c)
{
ResetDialogPosition(c, oldSize, newSize);
}
}
}
@@ -168,10 +182,7 @@ public class OverlayDialogHost : Canvas
ResetZIndices();
}
internal void AddDrawer(DrawerControlBase control)
{
}
private void OnDialogControlClosing(object sender, object? e)
{
@@ -225,6 +236,84 @@ public class OverlayDialogHost : Canvas
control.AddHandler(DialogControl.ClosedEvent, OnDialogControlClosing);
control.AddHandler(DialogControl.LayerChangedEvent, OnDialogLayerChanged);
}
internal async void AddDrawer(DrawerControlBase control)
{
var mask = CreateOverlayMask(false);
mask.Opacity = 0;
_masks.Add(mask);
_modalDialogs.Add(control);
// control.SetAsModal(true);
for (int i = 0; i < _masks.Count-1; i++)
{
_masks[i].Opacity = 0.5;
}
ResetZIndices();
this.Children.Add(mask);
this.Children.Add(control);
control.Measure(this.Bounds.Size);
control.Arrange(new Rect(control.DesiredSize));
control.Height = this.Bounds.Height;
control.AddHandler(DrawerControlBase.ClosedEvent, OnDrawerControlClosing);
// SetLeft(control, this.Bounds.Width - control.Bounds.Width);
var animation = CreateAnimation(control.Bounds.Width);
var animation2 = CreateOpacityAnimation();
await Task.WhenAll(animation.RunAsync(control), animation2.RunAsync(mask));
}
private Animation CreateAnimation(double width)
{
var animation = new Animation();
animation.Easing = new CubicEaseOut();
animation.FillMode = FillMode.Forward;
var keyFrame1 = new KeyFrame(){ Cue = new Cue(0.0) };
keyFrame1.Setters.Add(new Setter() { Property = Canvas.LeftProperty, Value = Bounds.Width });
var keyFrame2 = new KeyFrame() { Cue = new Cue(1.0) };
keyFrame2.Setters.Add(new Setter() { Property = Canvas.LeftProperty, Value = Bounds.Width - width });
animation.Children.Add(keyFrame1);
animation.Children.Add(keyFrame2);
animation.Duration = TimeSpan.FromSeconds(0.3);
return animation;
}
private Animation CreateOpacityAnimation()
{
var animation = new Animation();
animation.FillMode = FillMode.Forward;
var keyFrame1 = new KeyFrame(){ Cue = new Cue(0.0) };
keyFrame1.Setters.Add(new Setter(){ Property = OpacityProperty, Value = 0.0});
var keyFrame2 = new KeyFrame() { Cue = new Cue(1.0) };
keyFrame2.Setters.Add(new Setter() { Property = OpacityProperty, Value = 1.0 });
animation.Children.Add(keyFrame1);
animation.Children.Add(keyFrame2);
animation.Duration = TimeSpan.FromSeconds(0.3);
return animation;
}
private void OnDrawerControlClosing(object sender, ResultEventArgs e)
{
if (sender is DrawerControlBase control)
{
Children.Remove(control);
control.RemoveHandler(DialogControl.ClosedEvent, OnDialogControlClosing);
control.RemoveHandler(DialogControl.LayerChangedEvent, OnDialogLayerChanged);
if (_modalDialogs.Contains(control))
{
_modalDialogs.Remove(control);
if (_masks.Count > 0)
{
var last = _masks.Last();
this.Children.Remove(last);
_masks.Remove(last);
if (_masks.Count > 0)
{
_masks.Last().IsVisible = true;
}
}
}
ResetZIndices();
}
}
// Handle dialog layer change event
private void OnDialogLayerChanged(object sender, DialogLayerChangeEventArgs e)

View File

@@ -0,0 +1,6 @@
namespace Ursa.Controls;
public class CustomDrawerControl: DrawerControlBase
{
}

View File

@@ -1,6 +1,39 @@
namespace Ursa.Controls;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Ursa.Common;
namespace Ursa.Controls;
[TemplatePart(PART_YesButton, typeof(Button))]
[TemplatePart(PART_NoButton, typeof(Button))]
[TemplatePart(PART_OKButton, typeof(Button))]
[TemplatePart(PART_CancelButton, typeof(Button))]
public class DefaultDrawerControl: DrawerControlBase
{
public const string PART_YesButton = "PART_YesButton";
public const string PART_NoButton = "PART_NoButton";
public const string PART_OKButton = "PART_OKButton";
public const string PART_CancelButton = "PART_CancelButton";
private Button? _yesButton;
private Button? _noButton;
private Button? _okButton;
private Button? _cancelButton;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
EventHelper.UnregisterClickEvent(OnDefaultButtonClick, _yesButton, _noButton, _okButton, _cancelButton);
_yesButton = e.NameScope.Find<Button>(PART_YesButton);
_noButton = e.NameScope.Find<Button>(PART_NoButton);
_okButton = e.NameScope.Find<Button>(PART_OKButton);
_cancelButton = e.NameScope.Find<Button>(PART_CancelButton);
EventHelper.RegisterClickEvent(OnDefaultButtonClick, _yesButton, _noButton, _okButton, _cancelButton);
}
private void OnDefaultButtonClick(object? sender, RoutedEventArgs e)
{
}
}

View File

@@ -1,12 +1,21 @@
namespace Ursa.Controls;
using Avalonia.Controls;
using Ursa.Common;
namespace Ursa.Controls;
public static class Drawer
{
public static Task<TResult?> ShowDialogAsync<TView, TViewModel, TResult>(TViewModel viewModel)
public static Task<TResult?> Show<TView, TViewModel, TResult>(TViewModel vm, Position position = Position.Right)
where TView: Control, new()
{
var host = OverlayDialogManager.GetHost(null);
if (host is null) return Task.FromResult(default(TResult));
var dialog = new DefaultDrawerControl();
var dialog = new CustomDrawerControl()
{
Content = new TView(),
DataContext = vm,
Position = position,
};
host.AddDrawer(dialog);
return dialog.ShowAsync<TResult>();
}

View File

@@ -1,5 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Threading;
using Ursa.Common;
@@ -7,10 +9,15 @@ using Ursa.EventArgs;
namespace Ursa.Controls;
[TemplatePart(PART_CloseButton, typeof(Button))]
public abstract class DrawerControlBase: ContentControl
{
public const string PART_CloseButton = "PART_CloseButton";
internal bool CanClickOnMaskToClose { get; set; }
internal bool ShowCloseButton { get; set; }
protected internal Button? _closeButton;
public static readonly StyledProperty<Position> PositionProperty = AvaloniaProperty.Register<DrawerControlBase, Position>(
nameof(Position));
@@ -43,7 +50,7 @@ public abstract class DrawerControlBase: ContentControl
public static readonly RoutedEvent<ResultEventArgs> ClosedEvent = RoutedEvent.Register<DrawerControlBase, ResultEventArgs>(
nameof(Closed), RoutingStrategies.Bubble);
public event EventHandler<ResultEventArgs> Closed
public event EventHandler<ResultEventArgs>? Closed
{
add => AddHandler(ClosedEvent, value);
remove => RemoveHandler(ClosedEvent, value);
@@ -54,6 +61,14 @@ public abstract class DrawerControlBase: ContentControl
DataContextProperty.Changed.AddClassHandler<DrawerControlBase, object?>((o, e) => o.OnDataContextChange(e));
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
EventHelper.UnregisterClickEvent(OnCloseButtonClick, _closeButton);
_closeButton = e.NameScope.Find<Button>(PART_CloseButton);
EventHelper.RegisterClickEvent(OnCloseButtonClick, _closeButton);
}
private void OnDataContextChange(AvaloniaPropertyChangedEventArgs<object?> args)
{
if(args.OldValue.Value is IDialogContext oldContext)
@@ -71,6 +86,8 @@ public abstract class DrawerControlBase: ContentControl
RaiseEvent(new ResultEventArgs(ClosedEvent, e));
}
private void OnCloseButtonClick(object sender, RoutedEventArgs e) => CloseDrawer();
public Task<T?> ShowAsync<T>(CancellationToken? token = default)
{
var tcs = new TaskCompletionSource<T?>();
@@ -97,6 +114,13 @@ public abstract class DrawerControlBase: ContentControl
internal virtual void CloseDrawer()
{
RaiseEvent(new ResultEventArgs(ClosedEvent, null));
if (DataContext is IDialogContext context)
{
context.Close();
}
else
{
RaiseEvent(new ResultEventArgs(ClosedEvent, null));
}
}
}

View File

@@ -0,0 +1,8 @@
using Avalonia.Controls.Primitives;
namespace Ursa.Controls.Layout;
public class DefaultDialogLayout: TemplatedControl
{
}