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 AspectRatioChangeAmbiguityProperty = AvaloniaProperty.Register( nameof(AspectRatioChangeAmbiguity), 0.2); public static readonly StyledProperty CurrentAspectRatioModeProperty = AvaloniaProperty.Register( nameof(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 => SetValue(CurrentAspectRatioModeProperty, 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 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 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(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(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; } } }