From 115163963599c519f3d0ba2376f229538fb17d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=9B=E5=B0=98=E7=A9=BA=E5=BF=A7?= Date: Tue, 12 Nov 2024 23:58:41 +0800 Subject: [PATCH] Add a new control AspectRatioLayout --- .../Pages/AspectRatioLayoutDemo.axaml | 28 ++ .../Pages/AspectRatioLayoutDemo.axaml.cs | 13 + .../AspectRatioLayoutDemoViewModel.cs | 5 + .../Ursa.Demo/ViewModels/MainViewViewModel.cs | 1 + demo/Ursa.Demo/ViewModels/MenuViewModel.cs | 2 + global.json | 2 +- .../AspectRatioLayout/AspectRatioLayout.cs | 262 ++++++++++++++++++ .../AspectRatioLayoutItem.cs | 17 ++ .../AspectRatioLayout/AspectRatioMode.cs | 9 + 9 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 demo/Ursa.Demo/Pages/AspectRatioLayoutDemo.axaml create mode 100644 demo/Ursa.Demo/Pages/AspectRatioLayoutDemo.axaml.cs create mode 100644 demo/Ursa.Demo/ViewModels/AspectRatioLayoutDemoViewModel.cs create mode 100644 src/Ursa/Controls/AspectRatioLayout/AspectRatioLayout.cs create mode 100644 src/Ursa/Controls/AspectRatioLayout/AspectRatioLayoutItem.cs create mode 100644 src/Ursa/Controls/AspectRatioLayout/AspectRatioMode.cs diff --git a/demo/Ursa.Demo/Pages/AspectRatioLayoutDemo.axaml b/demo/Ursa.Demo/Pages/AspectRatioLayoutDemo.axaml new file mode 100644 index 0000000..7d66922 --- /dev/null +++ b/demo/Ursa.Demo/Pages/AspectRatioLayoutDemo.axaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/demo/Ursa.Demo/Pages/AspectRatioLayoutDemo.axaml.cs b/demo/Ursa.Demo/Pages/AspectRatioLayoutDemo.axaml.cs new file mode 100644 index 0000000..c7fdad5 --- /dev/null +++ b/demo/Ursa.Demo/Pages/AspectRatioLayoutDemo.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Ursa.Demo.Pages; + +public partial class AspectRatioLayoutDemo : UserControl +{ + public AspectRatioLayoutDemo() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/AspectRatioLayoutDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/AspectRatioLayoutDemoViewModel.cs new file mode 100644 index 0000000..bfd2b8a --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/AspectRatioLayoutDemoViewModel.cs @@ -0,0 +1,5 @@ +namespace Ursa.Demo.ViewModels; + +public class AspectRatioLayoutDemoViewModel : ViewModelBase +{ +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index 24a21a6..3fc9e46 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -77,6 +77,7 @@ public partial class MainViewViewModel : ViewModelBase MenuKeys.MenuKeyToolBar => new ToolBarDemoViewModel(), MenuKeys.MenuKeyTreeComboBox => new TreeComboBoxDemoViewModel(), MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(), + MenuKeys.AspectRatioLayout => new AspectRatioLayoutDemoViewModel(), _ => throw new ArgumentOutOfRangeException(nameof(s), s, null) }; } diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index e894420..7559eb7 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -58,6 +58,7 @@ public class MenuViewModel : ViewModelBase new() { MenuHeader = "ToolBar", Key = MenuKeys.MenuKeyToolBar }, new() { MenuHeader = "TreeComboBox", Key = MenuKeys.MenuKeyTreeComboBox }, new() { MenuHeader = "TwoTonePathIcon", Key = MenuKeys.MenuKeyTwoTonePathIcon }, + new() { MenuHeader = "AspectRatioLayout", Key = MenuKeys.AspectRatioLayout ,Status = "WIP"}, }; } } @@ -111,4 +112,5 @@ public static class MenuKeys public const string MenuKeyToolBar = "ToolBar"; public const string MenuKeyTreeComboBox = "TreeComboBox"; public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon"; + public const string AspectRatioLayout = "AspectRatioLayout"; } \ No newline at end of file diff --git a/global.json b/global.json index b5b37b6..dad2db5 100644 --- a/global.json +++ b/global.json @@ -2,6 +2,6 @@ "sdk": { "version": "8.0.0", "rollForward": "latestMajor", - "allowPrerelease": false + "allowPrerelease": true } } \ No newline at end of file diff --git a/src/Ursa/Controls/AspectRatioLayout/AspectRatioLayout.cs b/src/Ursa/Controls/AspectRatioLayout/AspectRatioLayout.cs new file mode 100644 index 0000000..e1f7da5 --- /dev/null +++ b/src/Ursa/Controls/AspectRatioLayout/AspectRatioLayout.cs @@ -0,0 +1,262 @@ +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Controls; +using Avalonia.Metadata; +using Avalonia.Styling; + +namespace Ursa.Controls; + +public class AspectRatioLayout : TransitioningContentControl +{ + public static readonly StyledProperty> ItemsProperty = + AvaloniaProperty.Register>( + nameof(Items), new List()); + + public static readonly StyledProperty AspectRatioChangeAmbiguityProperty = + AvaloniaProperty.Register( + nameof(AspectRatioChangeAmbiguity), 0.2); + + public static readonly StyledProperty CurrentAspectRatioModeProperty = + AvaloniaProperty.Register( + nameof(CurrentAspectRatioMode)); + + public AspectRatioMode CurrentAspectRatioMode + { + get => GetValue(CurrentAspectRatioModeProperty); + set => SetValue(CurrentAspectRatioModeProperty, value); + } + + private readonly Queue _history = new(); + + static AspectRatioLayout() + { + PCrossFade pCrossFade = new() + { + Duration = TimeSpan.FromSeconds(0.55), + FadeInEasing = new QuadraticEaseInOut(), + FadeOutEasing = new QuadraticEaseInOut() + }; + PageTransitionProperty.OverrideDefaultValue(pCrossFade); + } + + protected override Type StyleKeyOverride => typeof(TransitioningContentControl); + + [Content] + public List Items + { + get => GetValue(ItemsProperty); + set => SetValue(ItemsProperty, value); + } + + public double AspectRatioChangeAmbiguity + { + get => GetValue(AspectRatioChangeAmbiguityProperty); + set => SetValue(AspectRatioChangeAmbiguityProperty, value); + } + + private void UpdataHistory(bool value) + { + _history.Enqueue(value); + while (_history.Count > 3) + _history.Dequeue(); + } + + private bool IsRightChanges() + { + //if (_history.Count < 3) return false; + return _history.All(x => x) || _history.All(x => !x); + } + + private AspectRatioMode GetScaleMode(Rect rect) + { + var scale = Math.Round(Math.Truncate(Math.Abs(rect.Width)) / Math.Truncate(Math.Abs(rect.Height)), 3); + var absA = Math.Abs(AspectRatioChangeAmbiguity); + var h = 1d + absA; + var v = 1d - absA; + if (scale >= h) return AspectRatioMode.HorizontalRectangle; + if (v < scale && scale < h) return AspectRatioMode.Square; + if (scale <= v) return AspectRatioMode.VerticalRectangle; + return AspectRatioMode.None; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == ItemsProperty || + change.Property == AspectRatioChangeAmbiguityProperty || + change.Property == BoundsProperty) + { + if (change.Property == BoundsProperty) + { + var o = (Rect)change.OldValue!; + var n = (Rect)change.NewValue!; + UpdataHistory(GetScaleMode(o) == GetScaleMode(n)); + if (!IsRightChanges()) return; + CurrentAspectRatioMode = GetScaleMode(n); + } + + var c = Items.FirstOrDefault(x => x.AcceptAspectRatioMode == GetScaleMode(Bounds)); + if (c == null) + { + if (Items.Count == 0) return; + c = Items.First(); + } + + Content = c; + } + } + + private class PCrossFade : IPageTransition + { + private readonly Animation _fadeInAnimation; + private readonly Animation _fadeOutAnimation; + + /// + /// Initializes a new instance of the class. + /// + public PCrossFade() + : this(TimeSpan.Zero) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public PCrossFade(TimeSpan duration) + { + _fadeOutAnimation = new Animation + { + Children = + { + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = 1d + } + }, + Cue = new Cue(0d) + }, + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = 0d + } + }, + Cue = new Cue(1d) + } + } + }; + _fadeInAnimation = new Animation + { + Children = + { + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = 0d + } + }, + Cue = new Cue(0d) + }, + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = 1d + } + }, + Cue = new Cue(1d) + } + } + }; + _fadeInAnimation.FillMode = FillMode.Both; + _fadeOutAnimation.FillMode = FillMode.Both; + _fadeOutAnimation.Duration = _fadeInAnimation.Duration = duration; + } + + /// + /// Gets the duration of the animation. + /// + public TimeSpan Duration + { + get => _fadeOutAnimation.Duration; + set => _fadeOutAnimation.Duration = _fadeInAnimation.Duration = value; + } + + /// + /// Gets or sets element entrance easing. + /// + public Easing FadeInEasing + { + get => _fadeInAnimation.Easing; + set => _fadeInAnimation.Easing = value; + } + + /// + /// Gets or sets element exit easing. + /// + public Easing FadeOutEasing + { + get => _fadeOutAnimation.Easing; + set => _fadeOutAnimation.Easing = value; + } + + /// + /// Starts the animation. + /// + /// + /// The control that is being transitioned away from. May be null. + /// + /// + /// The control that is being transitioned to. May be null. + /// + /// + /// Unused for cross-fades. + /// + /// allowed cancel transition + /// + /// A that tracks the progress of the animation. + /// + Task IPageTransition.Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + { + return Start(from, to, cancellationToken); + } + + /// + public async Task Start(Visual? from, Visual? to, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) return; + + var tasks = new List(); + + if (from != null) tasks.Add(_fadeOutAnimation.RunAsync(from, cancellationToken)); + + if (to != null) + { + to.IsVisible = true; + tasks.Add(_fadeInAnimation.RunAsync(to, cancellationToken)); + } + + await Task.WhenAll(tasks); + + if (from != null && !cancellationToken.IsCancellationRequested) from.IsVisible = false; + } + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/AspectRatioLayout/AspectRatioLayoutItem.cs b/src/Ursa/Controls/AspectRatioLayout/AspectRatioLayoutItem.cs new file mode 100644 index 0000000..e4cf639 --- /dev/null +++ b/src/Ursa/Controls/AspectRatioLayout/AspectRatioLayoutItem.cs @@ -0,0 +1,17 @@ +using Avalonia; +using Avalonia.Controls; + +namespace Ursa.Controls; + +public class AspectRatioLayoutItem : ContentControl +{ + public static readonly StyledProperty AcceptScaleModeProperty = + AvaloniaProperty.Register( + nameof(AcceptAspectRatioMode)); + + public AspectRatioMode AcceptAspectRatioMode + { + get => GetValue(AcceptScaleModeProperty); + set => SetValue(AcceptScaleModeProperty, value); + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/AspectRatioLayout/AspectRatioMode.cs b/src/Ursa/Controls/AspectRatioLayout/AspectRatioMode.cs new file mode 100644 index 0000000..4617c74 --- /dev/null +++ b/src/Ursa/Controls/AspectRatioLayout/AspectRatioMode.cs @@ -0,0 +1,9 @@ +namespace Ursa.Controls; + +public enum AspectRatioMode +{ + None, + Square, + HorizontalRectangle, + VerticalRectangle +} \ No newline at end of file