Added some animations, fixed crash when reopening folder
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}); // TODO: When reopening folder this throws exception
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user