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