Add a new control AspectRatioLayout

This commit is contained in:
望尘空忧
2024-11-12 23:58:41 +08:00
parent 59378c4b34
commit 1151639635
9 changed files with 338 additions and 1 deletions

View File

@@ -0,0 +1,28 @@
<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"
xmlns:u="https://irihi.tech/ursa"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ursa.Demo.Pages.AspectRatioLayoutDemo">
<Grid
RowDefinitions="Auto,*">
<StackPanel
Grid.Row="0"
Orientation="Horizontal">
<u:NumericDoubleUpDown InnerLeftContent="AspectRatioChangeAmbiguity" Value="{Binding #AspectRatioLayout.AspectRatioChangeAmbiguity}"></u:NumericDoubleUpDown>
</StackPanel>
<u:AspectRatioLayout Name="AspectRatioLayout" Grid.Row="1">
<u:AspectRatioLayoutItem AcceptAspectRatioMode="HorizontalRectangle">
<Button>HorizontalRectangle ControlLayout</Button>
</u:AspectRatioLayoutItem>
<u:AspectRatioLayoutItem AcceptAspectRatioMode="VerticalRectangle">
<Button>VerticalRectangle ControlLayout</Button>
</u:AspectRatioLayoutItem>
<u:AspectRatioLayoutItem AcceptAspectRatioMode="Square">
<Button>Square ControlLayout</Button>
</u:AspectRatioLayoutItem>
</u:AspectRatioLayout>
</Grid>
</UserControl>

View File

@@ -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();
}
}

View File

@@ -0,0 +1,5 @@
namespace Ursa.Demo.ViewModels;
public class AspectRatioLayoutDemoViewModel : ViewModelBase
{
}

View File

@@ -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)
};
}

View File

@@ -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";
}

View File

@@ -2,6 +2,6 @@
"sdk": {
"version": "8.0.0",
"rollForward": "latestMajor",
"allowPrerelease": false
"allowPrerelease": true
}
}

View File

@@ -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<List<AspectRatioLayoutItem>> ItemsProperty =
AvaloniaProperty.Register<AspectRatioLayout, List<AspectRatioLayoutItem>>(
nameof(Items), new List<AspectRatioLayoutItem>());
public static readonly StyledProperty<double> AspectRatioChangeAmbiguityProperty =
AvaloniaProperty.Register<AspectRatioLayout, double>(
nameof(AspectRatioChangeAmbiguity), 0.2);
public static readonly StyledProperty<AspectRatioMode> CurrentAspectRatioModeProperty =
AvaloniaProperty.Register<AspectRatioLayout, AspectRatioMode>(
nameof(CurrentAspectRatioMode));
public AspectRatioMode CurrentAspectRatioMode
{
get => GetValue(CurrentAspectRatioModeProperty);
set => SetValue(CurrentAspectRatioModeProperty, value);
}
private readonly Queue<bool> _history = new();
static AspectRatioLayout()
{
PCrossFade pCrossFade = new()
{
Duration = TimeSpan.FromSeconds(0.55),
FadeInEasing = new QuadraticEaseInOut(),
FadeOutEasing = new QuadraticEaseInOut()
};
PageTransitionProperty.OverrideDefaultValue<AspectRatioLayout>(pCrossFade);
}
protected override Type StyleKeyOverride => typeof(TransitioningContentControl);
[Content]
public List<AspectRatioLayoutItem> 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;
/// <summary>
/// Initializes a new instance of the <see cref="PCrossFade" /> class.
/// </summary>
public PCrossFade()
: this(TimeSpan.Zero)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PCrossFade" /> class.
/// </summary>
/// <param name="duration">The duration of the animation.</param>
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;
}
/// <summary>
/// Gets the duration of the animation.
/// </summary>
public TimeSpan Duration
{
get => _fadeOutAnimation.Duration;
set => _fadeOutAnimation.Duration = _fadeInAnimation.Duration = value;
}
/// <summary>
/// Gets or sets element entrance easing.
/// </summary>
public Easing FadeInEasing
{
get => _fadeInAnimation.Easing;
set => _fadeInAnimation.Easing = value;
}
/// <summary>
/// Gets or sets element exit easing.
/// </summary>
public Easing FadeOutEasing
{
get => _fadeOutAnimation.Easing;
set => _fadeOutAnimation.Easing = value;
}
/// <summary>
/// Starts the animation.
/// </summary>
/// <param name="from">
/// The control that is being transitioned away from. May be null.
/// </param>
/// <param name="to">
/// The control that is being transitioned to. May be null.
/// </param>
/// <param name="forward">
/// Unused for cross-fades.
/// </param>
/// <param name="cancellationToken">allowed cancel transition</param>
/// <returns>
/// A <see cref="Task" /> that tracks the progress of the animation.
/// </returns>
Task IPageTransition.Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
{
return Start(from, to, cancellationToken);
}
/// <inheritdoc cref="Start(Visual, Visual, CancellationToken)" />
public async Task Start(Visual? from, Visual? to, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested) return;
var tasks = new List<Task>();
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;
}
}
}

View File

@@ -0,0 +1,17 @@
using Avalonia;
using Avalonia.Controls;
namespace Ursa.Controls;
public class AspectRatioLayoutItem : ContentControl
{
public static readonly StyledProperty<AspectRatioMode> AcceptScaleModeProperty =
AvaloniaProperty.Register<AspectRatioLayoutItem, AspectRatioMode>(
nameof(AcceptAspectRatioMode));
public AspectRatioMode AcceptAspectRatioMode
{
get => GetValue(AcceptScaleModeProperty);
set => SetValue(AcceptScaleModeProperty, value);
}
}

View File

@@ -0,0 +1,9 @@
namespace Ursa.Controls;
public enum AspectRatioMode
{
None,
Square,
HorizontalRectangle,
VerticalRectangle
}