Compare commits

...

2 Commits

Author SHA1 Message Date
Ano-sys 8735570f35 Removed todo comment beacuse fixed 2026-03-21 17:13:45 +01:00
Ano-sys d286282f71 Added some animations, fixed crash when reopening folder 2026-03-21 17:13:20 +01:00
4 changed files with 221 additions and 29 deletions
@@ -0,0 +1,159 @@
using System;
using System.Numerics;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Rendering.Composition;
namespace Photos.Animations;
public static class CardHoverEffectBehavior
{
private static readonly TimeSpan _animationDuration = TimeSpan.FromMilliseconds(150);
public static readonly AttachedProperty<bool> IsEnabledProperty =
AvaloniaProperty.RegisterAttached<Control, bool>(
"IsEnabled",
typeof(CardHoverEffectBehavior),
false);
public static readonly AttachedProperty<double> MaxRotationAngleProperty =
AvaloniaProperty.RegisterAttached<Control, double>(
"MaxRotationAngle",
typeof(CardHoverEffectBehavior),
5.5);
public static readonly AttachedProperty<double> ScaleFactorProperty =
AvaloniaProperty.RegisterAttached<Control, double>(
"ScaleFactor",
typeof(CardHoverEffectBehavior),
0.99);
static CardHoverEffectBehavior()
{
IsEnabledProperty.Changed.AddClassHandler<Control>(OnIsEnabledChanged);
}
public static bool GetIsEnabled(AvaloniaObject obj) => obj.GetValue(IsEnabledProperty);
public static void SetIsEnabled(AvaloniaObject obj, bool value) => obj.SetValue(IsEnabledProperty, value);
public static double GetMaxRotationAngle(AvaloniaObject obj) => obj.GetValue(MaxRotationAngleProperty);
public static void SetMaxRotationAngle(AvaloniaObject obj, double value) => obj.SetValue(MaxRotationAngleProperty, value);
public static double GetScaleFactor(AvaloniaObject obj) => obj.GetValue(ScaleFactorProperty);
public static void SetScaleFactor(AvaloniaObject obj, double value) => obj.SetValue(ScaleFactorProperty, value);
private static void OnIsEnabledChanged(Control c, AvaloniaPropertyChangedEventArgs e)
{
if (e.NewValue is not bool enabled)
return;
if (enabled)
{
c.PointerEntered += Element_PointerEntered;
c.PointerMoved += Element_PointerMoved;
c.PointerExited += Element_PointerExited;
}
else
{
c.PointerEntered -= Element_PointerEntered;
c.PointerMoved -= Element_PointerMoved;
c.PointerExited -= Element_PointerExited;
}
}
private static void Element_PointerEntered(object? sender, PointerEventArgs e)
{
if (sender is not Control element)
return;
element.ZIndex = 1;
var visual = ElementComposition.GetElementVisual(element);
var compositor = visual?.Compositor;
if (visual is null || compositor is null)
return;
var scaleFactor = (float)GetScaleFactor(element);
var width = (float)element.Bounds.Width;
var height = (float)element.Bounds.Height;
if (width <= 0 || height <= 0)
return;
visual.CenterPoint = new Vector3(width / 2f, height / 2f, 0f);
var scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
scaleAnimation.InsertKeyFrame(1.0f, new Vector3(scaleFactor, scaleFactor, 1f));
scaleAnimation.Duration = _animationDuration;
var orientationAnimation = compositor.CreateQuaternionKeyFrameAnimation();
orientationAnimation.InsertKeyFrame(1.0f, Quaternion.Identity);
orientationAnimation.Duration = _animationDuration;
visual.StartAnimation("Scale", scaleAnimation);
visual.StartAnimation(nameof(visual.Orientation), orientationAnimation);
}
private static void Element_PointerMoved(object? sender, PointerEventArgs e)
{
if (sender is not Control element)
return;
var visual = ElementComposition.GetElementVisual(element);
if (visual is null)
return;
var maxRotationAngle = (float)GetMaxRotationAngle(element);
var width = element.Bounds.Width;
var height = element.Bounds.Height;
if (width <= 0 || height <= 0)
return;
var position = e.GetPosition(element);
var centerX = width / 2.0;
var centerY = height / 2.0;
var normalizedX = (position.X - centerX) / centerX;
var normalizedY = (position.Y - centerY) / centerY;
var rotationY_deg = (float)(normalizedX * maxRotationAngle);
var rotationX_deg = (float)(-normalizedY * maxRotationAngle);
var rotationX_rad = (float)(Math.PI / 180.0 * rotationX_deg);
var rotationY_rad = (float)(Math.PI / 180.0 * rotationY_deg);
var qx = Quaternion.CreateFromAxisAngle(Vector3.UnitX, rotationX_rad);
var qy = Quaternion.CreateFromAxisAngle(Vector3.UnitY, rotationY_rad);
visual.Orientation = qy * qx;
}
private static void Element_PointerExited(object? sender, PointerEventArgs e)
{
if (sender is not Control element)
return;
element.ZIndex = 0;
var visual = ElementComposition.GetElementVisual(element);
var compositor = visual?.Compositor;
if (visual is null || compositor is null)
return;
var scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
scaleAnimation.InsertKeyFrame(1.0f, new Vector3(1f, 1f, 1f));
scaleAnimation.Duration = _animationDuration;
var orientationAnimation = compositor.CreateQuaternionKeyFrameAnimation();
orientationAnimation.InsertKeyFrame(1.0f, Quaternion.Identity);
orientationAnimation.Duration = _animationDuration;
visual.StartAnimation("Scale", scaleAnimation);
visual.StartAnimation(nameof(visual.Orientation), orientationAnimation);
}
}
+4 -3
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
@@ -96,7 +97,7 @@ public static partial class PhotoExtension
.ToList();
}
public static void CreateAndAddPhotosToList(List<string> photos, string rootPath, IList<Photo> toAddTo)
public static void CreateAndAddPhotosToList(List<string> photos, string rootPath, ObservableCollection<Photo> toAddTo)
{
var bag = new ConcurrentBag<Photo>();
@@ -105,12 +106,12 @@ public static partial class PhotoExtension
bag.Add(new Photo(p, rootPath));
});
Dispatcher.UIThread.InvokeAsync(() =>
Dispatcher.UIThread.Invoke(() =>
{
foreach (var photo in bag)
{
toAddTo.Add(photo);
}
}).GetAwaiter().GetResult(); // TODO: When reopening folder this throws exception
});
}
}
+2 -1
View File
@@ -35,7 +35,8 @@ public partial class MainWindowViewModel : ViewModelBase
var folder = await _folderPickerService.PickFolderAsync();
if (folder is null) return;
foreach(var p in Photos) p.Dispose();
// foreach(var p in Photos) p.Dispose();
Photos.Clear();
LibraryPath = folder;
await AddPhotosAsync(folder);
+38 -7
View File
@@ -6,6 +6,7 @@
xmlns:i="using:Avalonia.Xaml.Interactivity"
xmlns:ic="using:Avalonia.Xaml.Interactions.Core"
xmlns:models="using:Photos.Models"
xmlns:anim="using:Photos.Animations"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Photos.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
@@ -31,16 +32,30 @@
<Button Content="Open Directory" Command="{Binding OpenDirectory}"/>
</Menu>
<ScrollViewer Grid.Column="1">
<ItemsRepeater ItemsSource="{Binding Photos}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ItemsRepeater ItemsSource="{Binding Photos}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Margin="5"
>
<ItemsRepeater.Layout>
<UniformGridLayout MinItemWidth="190"
MinItemHeight="240"
MinRowSpacing="12"
MinColumnSpacing="12" />
MinColumnSpacing="12"
>
</UniformGridLayout>
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="models:Photo">
<Border BorderBrush="Black"
Background="#aa000000"
BorderThickness="1"
CornerRadius="7.5"
anim:CardHoverEffectBehavior.IsEnabled="True"
anim:CardHoverEffectBehavior.MaxRotationAngle="3.5"
anim:CardHoverEffectBehavior.ScaleFactor="1.5"
>
<StackPanel Orientation="Vertical" Margin="12">
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="PointerReleased">
@@ -49,24 +64,40 @@
</i:Interaction.Behaviors>
<Image Source="{Binding PreviewImage}"
Width="150"
Height="150" />
Height="150"/>
<TextBlock Text="{Binding DisplayName}"
HorizontalAlignment="Center"
Width="150" />
<TextBlock Text="{Binding DisplayPath}" HorizontalAlignment="Center" Width="150"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
<Grid Grid.Column="0" Grid.ColumnSpan="2" IsEnabled="{Binding SelectedPhoto}">
<Image Source="{Binding SelectedPhoto.FullImage}" Margin="50">
<TransitioningContentControl Grid.Column="0"
Grid.ColumnSpan="2"
Content="{Binding SelectedPhoto}">
<TransitioningContentControl.PageTransition>
<CrossFade Duration="0:0:0.18" />
</TransitioningContentControl.PageTransition>
<TransitioningContentControl.ContentTemplate>
<DataTemplate DataType="models:Photo">
<Border Background="#AA000000">
<Image Source="{Binding FullImage}"
Margin="50"
Stretch="Uniform"
>
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="PointerReleased">
<ic:InvokeCommandAction Command="{Binding Path=SelectedPhotoClicked}"/>
<ic:InvokeCommandAction Command="{Binding Path=DataContext.SelectedPhotoClicked, ElementName=Root}"/>
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
</Image>
</Grid>
</Border>
</DataTemplate>
</TransitioningContentControl.ContentTemplate>
</TransitioningContentControl>
</Grid>
</Window>