diff --git a/demo/Ursa.Demo/Pages/AspectRatioLayoutDemo.axaml b/demo/Ursa.Demo/Pages/AspectRatioLayoutDemo.axaml
new file mode 100644
index 00000000..c56ec69d
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/AspectRatioLayoutDemo.axaml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 00000000..c7fdad55
--- /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 00000000..bfd2b8ab
--- /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 24a21a67..3fc9e46b 100644
--- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
@@ -77,6 +77,7 @@ private void OnNavigation(MainViewViewModel vm, string s)
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 e894420e..7559eb71 100644
--- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
@@ -58,6 +58,7 @@ public MenuViewModel()
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 b5b37b60..dad2db5e 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 00000000..5608232e
--- /dev/null
+++ b/src/Ursa/Controls/AspectRatioLayout/AspectRatioLayout.cs
@@ -0,0 +1,292 @@
+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));
+
+ public static readonly StyledProperty AspectRatioToleranceProperty =
+ AvaloniaProperty.Register(
+ nameof(AspectRatioTolerance), 0.2);
+
+ private AspectRatioMode _currentAspectRatioMode;
+
+ public static readonly DirectProperty CurrentAspectRatioModeProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(CurrentAspectRatioMode), o => o.CurrentAspectRatioMode);
+
+ private readonly Queue _history = new();
+
+ static AspectRatioLayout()
+ {
+ PCrossFade pCrossFade = new()
+ {
+ Duration = TimeSpan.FromSeconds(0.55),
+ FadeInEasing = new QuadraticEaseInOut(),
+ FadeOutEasing = new QuadraticEaseInOut()
+ };
+ PageTransitionProperty.OverrideDefaultValue(pCrossFade);
+ }
+
+ public AspectRatioLayout()
+ {
+ Items = new List();
+ }
+
+ public AspectRatioMode CurrentAspectRatioMode
+ {
+ get => GetValue(CurrentAspectRatioModeProperty);
+ set => SetAndRaise(CurrentAspectRatioModeProperty, ref _currentAspectRatioMode, value);
+ }
+
+ public static readonly StyledProperty AspectRatioValueProperty =
+ AvaloniaProperty.Register(
+ nameof(AspectRatioValue));
+
+ public double AspectRatioValue
+ {
+ get => GetValue(AspectRatioValueProperty);
+ set => SetValue(AspectRatioValueProperty, value);
+ }
+
+ protected override Type StyleKeyOverride => typeof(TransitioningContentControl);
+
+ [Content]
+ public List Items
+ {
+ get => GetValue(ItemsProperty);
+ set => SetValue(ItemsProperty, value);
+ }
+
+ public double AspectRatioTolerance
+ {
+ get => GetValue(AspectRatioToleranceProperty);
+ set => SetValue(AspectRatioToleranceProperty, value);
+ }
+
+ private void UpdateHistory(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 double GetAspectRatio(Rect rect)
+ {
+ return Math.Round(Math.Truncate(Math.Abs(rect.Width)) / Math.Truncate(Math.Abs(rect.Height)), 3);
+ }
+
+ private AspectRatioMode GetScaleMode(Rect rect)
+ {
+ var scale = GetAspectRatio(rect);
+ var absA = Math.Abs(AspectRatioTolerance);
+ 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 == AspectRatioToleranceProperty ||
+ change.Property == BoundsProperty)
+ {
+ if (change.Property == BoundsProperty)
+ {
+ var o = (Rect)change.OldValue!;
+ var n = (Rect)change.NewValue!;
+ UpdateHistory(GetAspectRatio(o) <= GetAspectRatio(n));
+ if (!IsRightChanges()) return;
+ CurrentAspectRatioMode = GetScaleMode(n);
+ }
+
+ AspectRatioValue = GetAspectRatio(Bounds);
+ var c =
+ Items
+ .Where(x => x.IsUseAspectRatioRange)
+ .FirstOrDefault(x =>
+ x.StartAspectRatioValue <= AspectRatioValue
+ && AspectRatioValue <= x.EndAspectRatioValue);
+
+ 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 00000000..b941fa88
--- /dev/null
+++ b/src/Ursa/Controls/AspectRatioLayout/AspectRatioLayoutItem.cs
@@ -0,0 +1,42 @@
+using Avalonia;
+using Avalonia.Controls;
+
+namespace Ursa.Controls;
+
+public class AspectRatioLayoutItem : ContentControl
+{
+ public static readonly StyledProperty AcceptScaleModeProperty =
+ AvaloniaProperty.Register(
+ nameof(AcceptAspectRatioMode));
+
+ public static readonly StyledProperty StartAspectRatioValueProperty =
+ AvaloniaProperty.Register(
+ nameof(StartAspectRatioValue), defaultValue: double.NaN);
+
+ public double StartAspectRatioValue
+ {
+ get => GetValue(StartAspectRatioValueProperty);
+ set => SetValue(StartAspectRatioValueProperty, value);
+ }
+
+ public static readonly StyledProperty EndAspectRatioValueProperty =
+ AvaloniaProperty.Register(
+ nameof(EndAspectRatioValue), defaultValue: double.NaN);
+
+ public double EndAspectRatioValue
+ {
+ get => GetValue(EndAspectRatioValueProperty);
+ set => SetValue(EndAspectRatioValueProperty, value);
+ }
+
+ public bool IsUseAspectRatioRange =>
+ !double.IsNaN(StartAspectRatioValue)
+ && !double.IsNaN(EndAspectRatioValue)
+ && !(StartAspectRatioValue > EndAspectRatioValue);
+
+ 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 00000000..4617c741
--- /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