Анімації — це те, що робить інтерфейс живим. Плавне відкриття бічної панелі, fade-in при завантаженні сторінки, пульсуючий індикатор завантаження, зміна кольору кнопки при наведенні — все це анімації. Вони покращують user experience, роблять інтерфейс зрозумілішим та приємнішим у використанні.
WPF має потужну систему анімацій через Storyboard — контейнер для анімацій, що може змінювати будь-які властивості контролів у часі. Можна анімувати числа (Width, Opacity), кольори (Background), товщину (Margin), та навіть складні трансформації (обертання, масштабування).
У цій статті ми детально розберемо всі аспекти анімацій у WPF: від базового синтаксису до складних easing functions та code-behind контролю. Ви навчитесь створювати професійні анімації, що роблять ваш інтерфейс сучасним та інтерактивним.
Storyboard — це клас WPF, що координує виконання кількох анімацій одночасно або послідовно. Він визначає, які властивості яких елементів анімувати та як.
<Window.Resources>
<Storyboard x:Key="FadeInStoryboard">
<DoubleAnimation Storyboard.TargetName="MyButton"
Storyboard.TargetProperty="Opacity"
From="0"
To="1"
Duration="0:0:0.5"/>
</Storyboard>
</Window.Resources>
<Button x:Name="MyButton" Content="Fade In" Opacity="0"/>
Ключові властивості:
Storyboard.TargetName — ім'я елемента для анімації (x:Name)Storyboard.TargetProperty — властивість для анімаціїFrom — початкове значенняTo — кінцеве значенняDuration — тривалість у форматі hours:minutes:seconds.milliseconds<Button Content="Fade In" Opacity="0">
<Button.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="0"
To="1"
Duration="0:0:0.5"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
Доступні події для EventTrigger:
Loaded — елемент завантаженоMouseEnter — курсор над елементомMouseLeave — курсор покинув елементGotFocus — елемент отримав фокусLostFocus — елемент втратив фокусLoading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Border Background="#f8fafc" Padding="40" CornerRadius="8">
<StackPanel Spacing="16" HorizontalAlignment="Center">
<TextBlock Text="🎬 Анімація Fade-in"
FontSize="24" FontWeight="Bold"
Foreground="#1e293b"
HorizontalAlignment="Center"/>
<TextBlock Text="При завантаженні Opacity змінюється з 0 до 1 за 0.5 секунди"
FontSize="13" Foreground="#64748b"
HorizontalAlignment="Center"
TextWrapping="Wrap"
MaxWidth="300"/>
<Border Background="#3b82f6"
CornerRadius="8"
Padding="20"
HorizontalAlignment="Center">
<TextBlock Text="Я з'являюсь плавно!"
Foreground="White"
FontSize="16"
FontWeight="SemiBold"/>
</Border>
</StackPanel>
</Border>
WPF підтримує різні типи анімацій для різних типів властивостей.
Для властивостей типу double: Width, Height, Opacity, FontSize, Margin (окремі значення).
<Storyboard x:Key="ExpandWidth">
<DoubleAnimation Storyboard.TargetName="Sidebar"
Storyboard.TargetProperty="Width"
From="0"
To="250"
Duration="0:0:0.3"/>
</Storyboard>
From/To/By варіанти:
<!-- From + To: з 0 до 250 -->
<DoubleAnimation From="0" To="250" Duration="0:0:0.3"/>
<!-- Лише To: з поточного значення до 250 -->
<DoubleAnimation To="250" Duration="0:0:0.3"/>
<!-- By: збільшити на 50 від поточного -->
<DoubleAnimation By="50" Duration="0:0:0.3"/>
<!-- From + By: з 0 збільшити на 250 -->
<DoubleAnimation From="0" By="250" Duration="0:0:0.3"/>
Для властивостей типу Color (не Brush!). Потрібен Property Path до Color всередині Brush.
<Button x:Name="MyButton" Content="Hover Me">
<Button.Background>
<SolidColorBrush Color="#3b82f6"/>
</Button.Background>
<Button.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Button.Background).(SolidColorBrush.Color)"
To="#2563eb"
Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Button.Background).(SolidColorBrush.Color)"
To="#3b82f6"
Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
Важливо: (Button.Background).(SolidColorBrush.Color) — це Property Path до Color всередині SolidColorBrush.
Для властивостей типу Thickness: Margin, Padding, BorderThickness.
<Storyboard x:Key="SlideIn">
<ThicknessAnimation Storyboard.TargetName="Panel"
Storyboard.TargetProperty="Margin"
From="-250,0,0,0"
To="0,0,0,0"
Duration="0:0:0.3"/>
</Storyboard>
Для властивостей будь-якого типу, що не підтримують інтерполяцію (наприклад, Visibility, string).
<Storyboard x:Key="BlinkAnimation">
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Indicator"
Storyboard.TargetProperty="Visibility"
RepeatBehavior="Forever">
<DiscreteObjectKeyFrame KeyTime="0:0:0" Value="{x:Static Visibility.Visible}"/>
<DiscreteObjectKeyFrame KeyTime="0:0:0.5" Value="{x:Static Visibility.Collapsed}"/>
<DiscreteObjectKeyFrame KeyTime="0:0:1" Value="{x:Static Visibility.Visible}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
| Тип анімації | Для властивостей | Приклад |
|---|---|---|
DoubleAnimation | double (Width, Opacity) | Fade-in, розширення |
ColorAnimation | Color (всередині Brush) | Зміна кольору фону |
ThicknessAnimation | Thickness (Margin, Padding) | Slide-in панель |
PointAnimation | Point | Переміщення по координатах |
ObjectAnimationUsingKeyFrames | Будь-який тип | Мигання, дискретні зміни |
Формат: hours:minutes:seconds.milliseconds
<!-- 0.5 секунди -->
<DoubleAnimation Duration="0:0:0.5"/>
<!-- 300 мілісекунд -->
<DoubleAnimation Duration="0:0:0.3"/>
<!-- 1 секунда -->
<DoubleAnimation Duration="0:0:1"/>
<!-- 2 секунди -->
<DoubleAnimation Duration="0:0:2"/>
<!-- Виконати 1 раз (за замовчуванням) -->
<DoubleAnimation RepeatBehavior="1x"/>
<!-- Виконати 3 рази -->
<DoubleAnimation RepeatBehavior="3x"/>
<!-- Повторювати нескінченно -->
<DoubleAnimation RepeatBehavior="Forever"/>
<!-- Повторювати протягом 5 секунд -->
<DoubleAnimation RepeatBehavior="0:0:5"/>
<!-- Анімація виконується туди-назад -->
<DoubleAnimation From="0" To="1" Duration="0:0:0.5" AutoReverse="True"/>
Приклад пульсуючого індикатора:
<Ellipse x:Name="Indicator" Width="20" Height="20" Fill="#3b82f6">
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="1"
To="0.3"
Duration="0:0:0.8"
AutoReverse="True"
RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
Easing Functions роблять анімації природнішими, додаючи прискорення та уповільнення. Замість лінійної зміни (постійна швидкість), значення змінюється за кривою.
QuadraticEase — квадратична крива (найпопулярніша):
<DoubleAnimation From="0" To="250" Duration="0:0:0.3">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
EasingMode:
EaseIn — прискорення на початкуEaseOut — уповільнення в кінці (найприродніше)EaseInOut — прискорення на початку + уповільнення в кінці| Функція | Опис | Використання |
|---|---|---|
QuadraticEase | Квадратична крива | Універсальна, природна |
CubicEase | Кубічна крива | Більш виражена |
QuarticEase | Крива 4-го степеня | Дуже виражена |
QuinticEase | Крива 5-го степеня | Екстремальна |
SineEase | Синусоїдальна | Плавна, м'яка |
ExponentialEase | Експоненційна | Різке прискорення |
CircleEase | Кругова | Плавна, природна |
BackEase | З "відскоком" назад | Ефект пружини |
ElasticEase | Еластична | Ефект гумки |
BounceEase | З відскоком | Ефект м'яча |
BackEase — відскок назад:
<DoubleAnimation From="0" To="250" Duration="0:0:0.5">
<DoubleAnimation.EasingFunction>
<BackEase EasingMode="EaseOut" Amplitude="0.5"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
ElasticEase — еластичний ефект:
<DoubleAnimation From="0" To="250" Duration="0:0:0.8">
<DoubleAnimation.EasingFunction>
<ElasticEase EasingMode="EaseOut" Oscillations="3" Springiness="5"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
BounceEase — відскок:
<DoubleAnimation From="0" To="250" Duration="0:0:0.6">
<DoubleAnimation.EasingFunction>
<BounceEase EasingMode="EaseOut" Bounces="3" Bounciness="2"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
Щоб зрозуміти різницю між EasingMode, уявіть автомобіль, що рухається від точки A до точки B:
<StackPanel>
<!-- EaseIn — прискорення на початку -->
<Border x:Name="EaseInBox" Width="50" Height="50" Background="#3b82f6" Margin="0,0,0,8">
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)"
From="0" To="300" Duration="0:0:1"
RepeatBehavior="Forever" AutoReverse="True">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseIn"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
<!-- EaseOut — уповільнення в кінці -->
<Border Width="50" Height="50" Background="#22c55e" Margin="0,0,0,8">
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)"
From="0" To="300" Duration="0:0:1"
RepeatBehavior="Forever" AutoReverse="True">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
<!-- EaseInOut — прискорення + уповільнення -->
<Border Width="50" Height="50" Background="#f59e0b">
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)"
From="0" To="300" Duration="0:0:1"
RepeatBehavior="Forever" AutoReverse="True">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseInOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
</StackPanel>
QuadraticEase або CubicEase з EasingMode="EaseOut". Це найприродніший вигляд для відкриття панелей, fade-in/out та інших стандартних анімацій. Дослідження показують, що користувачі сприймають EaseOut як найбільш "природний" рух, схожий на фізичні об'єкти у реальному світі.Кожна Easing Function створює унікальну криву руху. Ось як вони відрізняються:
QuadraticEase vs CubicEase vs QuarticEase:
Ці функції відрізняються "силою" прискорення/уповільнення:
<StackPanel Spacing="16">
<TextBlock Text="Порівняння сили Easing Functions" FontWeight="Bold"/>
<!-- QuadraticEase — м'яке -->
<Border Width="60" Height="40" Background="#3b82f6" CornerRadius="4">
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)"
From="0" To="300" Duration="0:0:1"
RepeatBehavior="Forever" AutoReverse="True">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<TextBlock Text="Quadratic" Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<!-- CubicEase — середнє -->
<Border Width="60" Height="40" Background="#22c55e" CornerRadius="4">
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)"
From="0" To="300" Duration="0:0:1"
RepeatBehavior="Forever" AutoReverse="True">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<TextBlock Text="Cubic" Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<!-- QuarticEase — сильне -->
<Border Width="60" Height="40" Background="#f59e0b" CornerRadius="4">
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)"
From="0" To="300" Duration="0:0:1"
RepeatBehavior="Forever" AutoReverse="True">
<DoubleAnimation.EasingFunction>
<QuarticEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<TextBlock Text="Quartic" Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</StackPanel>
BackEase — ефект "відскоку":
BackEase створює ефект, коли анімація "відскакує" назад перед рухом вперед (EaseIn) або після досягнення цілі (EaseOut). Це нагадує натягування пружини перед стрибком.
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Border Background="#f8fafc" Padding="40" CornerRadius="8">
<StackPanel Spacing="20" HorizontalAlignment="Center">
<TextBlock Text="🎯 BackEase Animation"
FontSize="24" FontWeight="Bold"
Foreground="#1e293b"
HorizontalAlignment="Center"/>
<TextBlock Text="Елемент 'відскакує' назад перед рухом вперед"
FontSize="13" Foreground="#64748b"
HorizontalAlignment="Center"/>
<Border Width="100" Height="100"
Background="#3b82f6"
CornerRadius="12">
<Border.RenderTransform>
<ScaleTransform ScaleX="1" ScaleY="1"/>
</Border.RenderTransform>
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever">
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleX)"
From="1" To="1.3" Duration="0:0:1"
AutoReverse="True">
<DoubleAnimation.EasingFunction>
<BackEase EasingMode="EaseInOut" Amplitude="0.5"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleY)"
From="1" To="1.3" Duration="0:0:1"
AutoReverse="True">
<DoubleAnimation.EasingFunction>
<BackEase EasingMode="EaseInOut" Amplitude="0.5"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<TextBlock Text="Scale"
Foreground="White"
FontSize="18"
FontWeight="Bold"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</StackPanel>
</Border>
ElasticEase — еластичний ефект:
ElasticEase створює ефект гумки або пружини — елемент "коливається" навколо кінцевої позиції перед зупинкою. Параметри Oscillations (кількість коливань) та Springiness (жорсткість пружини) контролюють поведінку.
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Border Background="#f8fafc" Padding="40" CornerRadius="8">
<StackPanel Spacing="20" HorizontalAlignment="Center">
<TextBlock Text="🎪 ElasticEase Animation"
FontSize="24" FontWeight="Bold"
Foreground="#1e293b"
HorizontalAlignment="Center"/>
<TextBlock Text="Елемент коливається як пружина перед зупинкою"
FontSize="13" Foreground="#64748b"
HorizontalAlignment="Center"/>
<Ellipse Width="80" Height="80"
Fill="#22c55e">
<Ellipse.RenderTransform>
<TranslateTransform X="0" Y="0"/>
</Ellipse.RenderTransform>
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever">
<DoubleAnimation Storyboard.TargetProperty="(Ellipse.RenderTransform).(TranslateTransform.Y)"
From="0" To="100" Duration="0:0:1.5"
AutoReverse="True">
<DoubleAnimation.EasingFunction>
<ElasticEase EasingMode="EaseOut" Oscillations="3" Springiness="5"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
</StackPanel>
</Border>
BounceEase — ефект відскоку:
BounceEase імітує фізичний відскок м'яча від поверхні. Параметри Bounces (кількість відскоків) та Bounciness (висота відскоку) контролюють поведінку.
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Border Background="#f8fafc" Padding="40" CornerRadius="8">
<StackPanel Spacing="20" HorizontalAlignment="Center">
<TextBlock Text="⚽ BounceEase Animation"
FontSize="24" FontWeight="Bold"
Foreground="#1e293b"
HorizontalAlignment="Center"/>
<TextBlock Text="М'яч відскакує від поверхні"
FontSize="13" Foreground="#64748b"
HorizontalAlignment="Center"/>
<Canvas Width="300" Height="200">
<Ellipse Width="60" Height="60"
Fill="#f59e0b"
Canvas.Left="120">
<Ellipse.RenderTransform>
<TranslateTransform X="0" Y="0"/>
</Ellipse.RenderTransform>
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever">
<DoubleAnimation Storyboard.TargetProperty="(Ellipse.RenderTransform).(TranslateTransform.Y)"
From="0" To="140" Duration="0:0:1.2"
AutoReverse="True">
<DoubleAnimation.EasingFunction>
<BounceEase EasingMode="EaseOut" Bounces="3" Bounciness="2"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
<!-- Підлога -->
<Rectangle Width="300" Height="4" Fill="#1e293b" Canvas.Top="196"/>
</Canvas>
</StackPanel>
</Border>
Для анімації властивостей всередині інших об'єктів використовується Property Path.
Формат: (Type.Property).(NestedType.NestedProperty)
Приклади:
<!-- Анімація Color всередині SolidColorBrush у Background -->
<ColorAnimation Storyboard.TargetProperty="(Button.Background).(SolidColorBrush.Color)"
To="#2563eb" Duration="0:0:0.2"/>
<!-- Анімація Color всередині BorderBrush -->
<ColorAnimation Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
To="#3b82f6" Duration="0:0:0.2"/>
<!-- Анімація Foreground -->
<ColorAnimation Storyboard.TargetProperty="(TextBlock.Foreground).(SolidColorBrush.Color)"
To="#1e293b" Duration="0:0:0.2"/>
<Button Content="Rotate Me">
<Button.RenderTransform>
<RotateTransform Angle="0"/>
</Button.RenderTransform>
<Button.Triggers>
<EventTrigger RoutedEvent="Click">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Button.RenderTransform).(RotateTransform.Angle)"
From="0" To="360" Duration="0:0:0.5"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
<!-- Анімація лише Left у Margin -->
<ThicknessAnimation Storyboard.TargetProperty="Margin"
From="0,0,0,0" To="20,0,0,0" Duration="0:0:0.3"/>
<!-- Або через Property Path до конкретного значення -->
<DoubleAnimation Storyboard.TargetProperty="(FrameworkElement.Margin).(Thickness.Left)"
From="0" To="20" Duration="0:0:0.3"/>
Transform — це потужний механізм WPF для зміни візуального представлення елементів без зміни їх логічного розташування. Анімація Transform дозволяє створювати ефекти обертання, масштабування, нахилу та переміщення.
WPF має два типи трансформацій:
Для анімацій завжди використовуйте RenderTransform!
TranslateTransform переміщує елемент по осях X та Y без зміни його логічного розташування.
<Button Content="Slide In">
<Button.RenderTransform>
<TranslateTransform X="-200" Y="0"/>
</Button.RenderTransform>
<Button.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Button.RenderTransform).(TranslateTransform.X)"
From="-200" To="0" Duration="0:0:0.5">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
Приклад: Slide-in панель зліва:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Border Background="#f8fafc" Padding="40" CornerRadius="8">
<Grid>
<Border Width="200" Height="300"
Background="#3b82f6"
CornerRadius="8"
HorizontalAlignment="Left">
<Border.RenderTransform>
<TranslateTransform X="-200" Y="0"/>
</Border.RenderTransform>
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(TranslateTransform.X)"
From="-200" To="0" Duration="0:0:0.6">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<StackPanel Margin="20" VerticalAlignment="Center">
<TextBlock Text="📱 Menu"
Foreground="White"
FontSize="24"
FontWeight="Bold"
Margin="0,0,0,20"/>
<TextBlock Text="• Home" Foreground="White" FontSize="16" Margin="0,0,0,8"/>
<TextBlock Text="• Profile" Foreground="White" FontSize="16" Margin="0,0,0,8"/>
<TextBlock Text="• Settings" Foreground="White" FontSize="16" Margin="0,0,0,8"/>
<TextBlock Text="• Logout" Foreground="White" FontSize="16"/>
</StackPanel>
</Border>
</Grid>
</Border>
ScaleTransform змінює розмір елемента. Властивості ScaleX та ScaleY контролюють масштаб по осях.
<Button Content="Zoom In">
<Button.RenderTransform>
<ScaleTransform ScaleX="1" ScaleY="1"/>
</Button.RenderTransform>
<Button.RenderTransformOrigin>0.5,0.5</Button.RenderTransformOrigin>
<Button.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Button.RenderTransform).(ScaleTransform.ScaleX)"
To="1.1" Duration="0:0:0.2"/>
<DoubleAnimation Storyboard.TargetProperty="(Button.RenderTransform).(ScaleTransform.ScaleY)"
To="1.1" Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Button.RenderTransform).(ScaleTransform.ScaleX)"
To="1" Duration="0:0:0.2"/>
<DoubleAnimation Storyboard.TargetProperty="(Button.RenderTransform).(ScaleTransform.ScaleY)"
To="1" Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
Важливо: RenderTransformOrigin="0.5,0.5" встановлює центр трансформації в центр елемента. За замовчуванням це 0,0 (верхній лівий кут).
Приклад: Hover ефект для карток:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Border Background="#f8fafc" Padding="40" CornerRadius="8">
<StackPanel Orientation="Horizontal" Spacing="20" HorizontalAlignment="Center">
<Border Width="120" Height="160"
Background="White"
BorderBrush="#e2e8f0"
BorderThickness="1"
CornerRadius="8"
Padding="16">
<Border.RenderTransform>
<ScaleTransform ScaleX="1" ScaleY="1"/>
</Border.RenderTransform>
<Border.RenderTransformOrigin>0.5,0.5</Border.RenderTransformOrigin>
<Border.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleX)"
To="1.05" Duration="0:0:0.2">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleY)"
To="1.05" Duration="0:0:0.2">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleX)"
To="1" Duration="0:0:0.2"/>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleY)"
To="1" Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="📦" FontSize="48" HorizontalAlignment="Center" Margin="0,0,0,12"/>
<TextBlock Text="Product" FontSize="16" FontWeight="SemiBold" Foreground="#1e293b" HorizontalAlignment="Center"/>
<TextBlock Text="$99" FontSize="14" Foreground="#64748b" HorizontalAlignment="Center" Margin="0,4,0,0"/>
</StackPanel>
</Border>
<Border Width="120" Height="160"
Background="White"
BorderBrush="#e2e8f0"
BorderThickness="1"
CornerRadius="8"
Padding="16">
<Border.RenderTransform>
<ScaleTransform ScaleX="1" ScaleY="1"/>
</Border.RenderTransform>
<Border.RenderTransformOrigin>0.5,0.5</Border.RenderTransformOrigin>
<Border.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleX)"
To="1.05" Duration="0:0:0.2">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleY)"
To="1.05" Duration="0:0:0.2">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleX)"
To="1" Duration="0:0:0.2"/>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleY)"
To="1" Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="🎨" FontSize="48" HorizontalAlignment="Center" Margin="0,0,0,12"/>
<TextBlock Text="Design" FontSize="16" FontWeight="SemiBold" Foreground="#1e293b" HorizontalAlignment="Center"/>
<TextBlock Text="$149" FontSize="14" Foreground="#64748b" HorizontalAlignment="Center" Margin="0,4,0,0"/>
</StackPanel>
</Border>
</StackPanel>
</Border>
RotateTransform обертає елемент навколо точки. Властивість Angle контролює кут обертання в градусах.
<Button Content="Rotate">
<Button.RenderTransform>
<RotateTransform Angle="0"/>
</Button.RenderTransform>
<Button.RenderTransformOrigin>0.5,0.5</Button.RenderTransformOrigin>
<Button.Triggers>
<EventTrigger RoutedEvent="Click">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Button.RenderTransform).(RotateTransform.Angle)"
From="0" To="360" Duration="0:0:0.6">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseInOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
Приклад: Обертання іконки завантаження:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Border Background="#f8fafc" Padding="40" CornerRadius="8">
<StackPanel Spacing="20" HorizontalAlignment="Center">
<TextBlock Text="⚙️ Loading Spinner"
FontSize="24" FontWeight="Bold"
Foreground="#1e293b"
HorizontalAlignment="Center"/>
<Border Width="60" Height="60"
BorderBrush="#3b82f6"
BorderThickness="4"
CornerRadius="30">
<Border.RenderTransform>
<RotateTransform Angle="0"/>
</Border.RenderTransform>
<Border.RenderTransformOrigin>0.5,0.5</Border.RenderTransformOrigin>
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(RotateTransform.Angle)"
From="0" To="360" Duration="0:0:1"
RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<!-- Частина кола для візуального ефекту -->
<Border Width="50" Height="50"
Background="#3b82f6"
CornerRadius="25"
Margin="0,0,25,25"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"/>
</Border>
</StackPanel>
</Border>
SkewTransform нахиляє елемент по осях X та Y. Рідко використовується, але корисний для креативних ефектів.
<Border Background="#3b82f6" Width="100" Height="100">
<Border.RenderTransform>
<SkewTransform AngleX="0" AngleY="0"/>
</Border.RenderTransform>
<Border.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(SkewTransform.AngleX)"
To="10" Duration="0:0:0.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(SkewTransform.AngleX)"
To="0" Duration="0:0:0.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
Для одночасного застосування кількох трансформацій використовується TransformGroup.
<Border Background="#3b82f6" Width="100" Height="100">
<Border.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<RotateTransform Angle="0"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Border.RenderTransform>
<Border.RenderTransformOrigin>0.5,0.5</Border.RenderTransformOrigin>
<Border.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<!-- Масштабування -->
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)"
To="1.2" Duration="0:0:0.3"/>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)"
To="1.2" Duration="0:0:0.3"/>
<!-- Обертання -->
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(TransformGroup.Children)[1].(RotateTransform.Angle)"
To="15" Duration="0:0:0.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
Приклад: Card flip ефект:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Border Background="#f8fafc" Padding="40" CornerRadius="8">
<Grid HorizontalAlignment="Center">
<Border Width="200" Height="280"
Background="#3b82f6"
CornerRadius="12"
Padding="20">
<Border.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<RotateTransform Angle="0"/>
</TransformGroup>
</Border.RenderTransform>
<Border.RenderTransformOrigin>0.5,0.5</Border.RenderTransformOrigin>
<Border.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(TransformGroup.Children)[1].(RotateTransform.Angle)"
To="5" Duration="0:0:0.3">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)"
To="1.05" Duration="0:0:0.3">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)"
To="1.05" Duration="0:0:0.3">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(TransformGroup.Children)[1].(RotateTransform.Angle)"
To="0" Duration="0:0:0.3"/>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)"
To="1" Duration="0:0:0.3"/>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)"
To="1" Duration="0:0:0.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="💳" FontSize="64" HorizontalAlignment="Center" Margin="0,0,0,20"/>
<TextBlock Text="Premium Card"
Foreground="White"
FontSize="20"
FontWeight="Bold"
HorizontalAlignment="Center"
Margin="0,0,0,8"/>
<TextBlock Text="**** **** **** 1234"
Foreground="White"
FontSize="16"
HorizontalAlignment="Center"
Margin="0,0,0,20"/>
<TextBlock Text="Hover to interact"
Foreground="#93c5fd"
FontSize="12"
HorizontalAlignment="Center"/>
</StackPanel>
</Border>
</Grid>
</Border>
(Border.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX). Індекс [0], [1], [2] відповідає порядку трансформацій у TransformGroup.KeyFrame анімації дозволяють створювати складні анімації з кількома проміжними точками. Замість простого руху від A до B, ви можете визначити точки C, D, E з різними швидкостями та easing functions.
Найпопулярніший тип KeyFrame анімації — для числових значень.
<Border x:Name="AnimatedBox" Width="60" Height="60" Background="#3b82f6">
<Border.RenderTransform>
<TranslateTransform X="0" Y="0"/>
</Border.RenderTransform>
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.RenderTransform).(TranslateTransform.X)">
<!-- Початок: X = 0 -->
<LinearDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
<!-- Через 1 секунду: X = 200 -->
<EasingDoubleKeyFrame KeyTime="0:0:1" Value="200">
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase EasingMode="EaseOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<!-- Через 2 секунди: X = 200 (пауза) -->
<LinearDoubleKeyFrame KeyTime="0:0:2" Value="200"/>
<!-- Через 3 секунди: X = 0 (повернення) -->
<EasingDoubleKeyFrame KeyTime="0:0:3" Value="0">
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase EasingMode="EaseIn"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
WPF підтримує три типи KeyFrame:
| Тип | Опис | Використання |
|---|---|---|
LinearDoubleKeyFrame | Лінійна інтерполяція | Постійна швидкість між точками |
EasingDoubleKeyFrame | З Easing Function | Природні анімації з прискоренням/уповільненням |
DiscreteDoubleKeyFrame | Миттєва зміна | Стрибок без інтерполяції |
SplineDoubleKeyFrame | Кубічна крива Безьє | Максимальний контроль над кривою |
SplineDoubleKeyFrame використовує кубічну криву Безьє для контролю швидкості. Властивість KeySpline визначає дві контрольні точки кривої.
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="Opacity">
<SplineDoubleKeyFrame KeyTime="0:0:0" Value="0" KeySpline="0.1,0.9 0.2,1"/>
<SplineDoubleKeyFrame KeyTime="0:0:1" Value="1" KeySpline="0.3,0.1 0.9,0.2"/>
</DoubleAnimationUsingKeyFrames>
KeySpline формат: "x1,y1 x2,y2" — дві контрольні точки кривої Безьє.
Для анімації кольорів з кількома проміжними точками:
<Border Background="#3b82f6">
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever" AutoReverse="True">
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)">
<!-- Синій -->
<LinearColorKeyFrame KeyTime="0:0:0" Value="#3b82f6"/>
<!-- Зелений -->
<LinearColorKeyFrame KeyTime="0:0:2" Value="#22c55e"/>
<!-- Помаранчевий -->
<LinearColorKeyFrame KeyTime="0:0:4" Value="#f59e0b"/>
<!-- Червоний -->
<LinearColorKeyFrame KeyTime="0:0:6" Value="#ef4444"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
Приклад: Складна траєкторія руху:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Border Background="#f8fafc" Padding="40" CornerRadius="8">
<Canvas Width="400" Height="300">
<Ellipse Width="40" Height="40" Fill="#3b82f6">
<Ellipse.RenderTransform>
<TranslateTransform X="0" Y="0"/>
</Ellipse.RenderTransform>
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever">
<!-- Рух по X -->
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(Ellipse.RenderTransform).(TranslateTransform.X)">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
<EasingDoubleKeyFrame KeyTime="0:0:1" Value="180">
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase EasingMode="EaseInOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime="0:0:2" Value="360">
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase EasingMode="EaseInOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime="0:0:3" Value="180">
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase EasingMode="EaseInOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime="0:0:4" Value="0">
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase EasingMode="EaseInOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<!-- Рух по Y (синусоїда) -->
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(Ellipse.RenderTransform).(TranslateTransform.Y)">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="130"/>
<EasingDoubleKeyFrame KeyTime="0:0:1" Value="30">
<EasingDoubleKeyFrame.EasingFunction>
<SineEase EasingMode="EaseInOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime="0:0:2" Value="130">
<EasingDoubleKeyFrame.EasingFunction>
<SineEase EasingMode="EaseInOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime="0:0:3" Value="230">
<EasingDoubleKeyFrame.EasingFunction>
<SineEase EasingMode="EaseInOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime="0:0:4" Value="130">
<EasingDoubleKeyFrame.EasingFunction>
<SineEase EasingMode="EaseInOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
<!-- Траєкторія (для візуалізації) -->
<Path Stroke="#cbd5e1" StrokeThickness="2" StrokeDashArray="4,4">
<Path.Data>
<PathGeometry>
<PathFigure StartPoint="20,150">
<BezierSegment Point1="100,50" Point2="150,50" Point3="200,150"/>
<BezierSegment Point1="250,250" Point2="300,250" Point3="380,150"/>
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
</Canvas>
</Border>
Для властивостей, що не підтримують інтерполяцію (Visibility, string, enum), використовується ObjectAnimationUsingKeyFrames.
<TextBlock x:Name="StatusText" Text="Idle" FontSize="24">
<TextBlock.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever">
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Text">
<DiscreteObjectKeyFrame KeyTime="0:0:0" Value="Loading"/>
<DiscreteObjectKeyFrame KeyTime="0:0:1" Value="Loading."/>
<DiscreteObjectKeyFrame KeyTime="0:0:2" Value="Loading.."/>
<DiscreteObjectKeyFrame KeyTime="0:0:3" Value="Loading..."/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
Приклад: Мигаючий індикатор через Visibility:
<Ellipse Width="20" Height="20" Fill="#ef4444">
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever">
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0" Value="{x:Static Visibility.Visible}"/>
<DiscreteObjectKeyFrame KeyTime="0:0:0.5" Value="{x:Static Visibility.Collapsed}"/>
<DiscreteObjectKeyFrame KeyTime="0:0:1" Value="{x:Static Visibility.Visible}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
Типова анімація для сповіщень — з'являється зверху, затримується, зникає.
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Border Background="#f8fafc" Padding="40" CornerRadius="8">
<Grid>
<Border Width="320" Height="80"
Background="White"
BorderBrush="#22c55e"
BorderThickness="2"
CornerRadius="8"
Padding="16"
VerticalAlignment="Top"
HorizontalAlignment="Center">
<Border.RenderTransform>
<TranslateTransform X="0" Y="-100"/>
</Border.RenderTransform>
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<!-- Slide-in зверху -->
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.RenderTransform).(TranslateTransform.Y)">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="-100"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="0">
<EasingDoubleKeyFrame.EasingFunction>
<BackEase EasingMode="EaseOut" Amplitude="0.3"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<!-- Затримка 2 секунди -->
<EasingDoubleKeyFrame KeyTime="0:0:2.5" Value="0"/>
<!-- Slide-out вгору -->
<EasingDoubleKeyFrame KeyTime="0:0:3" Value="-100">
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase EasingMode="EaseIn"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<!-- Fade-out в кінці -->
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0:0:0" Value="1"/>
<LinearDoubleKeyFrame KeyTime="0:0:2.5" Value="1"/>
<LinearDoubleKeyFrame KeyTime="0:0:3" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Width="48" Height="48"
Background="#22c55e"
CornerRadius="24"
Margin="0,0,12,0">
<TextBlock Text="✓"
Foreground="White"
FontSize="28"
FontWeight="Bold"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="Success!"
FontSize="16"
FontWeight="Bold"
Foreground="#1e293b"
Margin="0,0,0,4"/>
<TextBlock Text="Your changes have been saved."
FontSize="13"
Foreground="#64748b"/>
</StackPanel>
</Grid>
</Border>
</Grid>
</Border>
Типова анімація для модальних вікон — затемнення фону + масштабування діалогу.
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Border Background="#f8fafc" Padding="40" CornerRadius="8">
<Grid>
<!-- Overlay -->
<Border Background="#000000" Opacity="0">
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="0" To="0.5" Duration="0:0:0.3">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
<!-- Dialog -->
<Border Width="300" Height="200"
Background="White"
CornerRadius="12"
Padding="24"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Border.RenderTransform>
<ScaleTransform ScaleX="0.8" ScaleY="0.8"/>
</Border.RenderTransform>
<Border.RenderTransformOrigin>0.5,0.5</Border.RenderTransformOrigin>
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleX)"
From="0.8" To="1" Duration="0:0:0.3">
<DoubleAnimation.EasingFunction>
<BackEase EasingMode="EaseOut" Amplitude="0.3"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleY)"
From="0.8" To="1" Duration="0:0:0.3">
<DoubleAnimation.EasingFunction>
<BackEase EasingMode="EaseOut" Amplitude="0.3"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="0" To="1" Duration="0:0:0.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="⚠️" FontSize="48" HorizontalAlignment="Center" Margin="0,0,0,16"/>
<TextBlock Text="Confirm Action"
FontSize="20"
FontWeight="Bold"
Foreground="#1e293b"
HorizontalAlignment="Center"
Margin="0,0,0,8"/>
<TextBlock Text="Are you sure you want to proceed?"
FontSize="14"
Foreground="#64748b"
HorizontalAlignment="Center"
TextWrapping="Wrap"
TextAlignment="Center"/>
</StackPanel>
</Border>
</Grid>
</Border>
Анімація прогрес-бару з плавним заповненням.
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Border Background="#f8fafc" Padding="40" CornerRadius="8">
<StackPanel Spacing="16" HorizontalAlignment="Center">
<TextBlock Text="📊 Loading Progress"
FontSize="20" FontWeight="Bold"
Foreground="#1e293b"
HorizontalAlignment="Center"/>
<!-- Progress Bar Container -->
<Border Width="300" Height="8"
Background="#e2e8f0"
CornerRadius="4">
<!-- Progress Fill -->
<Border Background="#3b82f6"
CornerRadius="4"
HorizontalAlignment="Left"
Width="0">
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Width"
From="0" To="300" Duration="0:0:3">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseInOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
</Border>
<TextBlock Text="Loading assets..."
FontSize="13"
Foreground="#64748b"
HorizontalAlignment="Center"/>
</StackPanel>
</Border>
Сучасний підхід до індикації завантаження — skeleton screens.
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Border Background="#f8fafc" Padding="40" CornerRadius="8">
<StackPanel Spacing="12" Width="300">
<!-- Avatar Skeleton -->
<Border Width="60" Height="60"
Background="#e2e8f0"
CornerRadius="30"
HorizontalAlignment="Left">
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="1" To="0.5" Duration="0:0:1"
AutoReverse="True"
RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
<!-- Title Skeleton -->
<Border Width="200" Height="20"
Background="#e2e8f0"
CornerRadius="4"
HorizontalAlignment="Left">
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="1" To="0.5" Duration="0:0:1"
AutoReverse="True"
RepeatBehavior="Forever"
BeginTime="0:0:0.1"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
<!-- Subtitle Skeleton -->
<Border Width="150" Height="16"
Background="#e2e8f0"
CornerRadius="4"
HorizontalAlignment="Left">
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="1" To="0.5" Duration="0:0:1"
AutoReverse="True"
RepeatBehavior="Forever"
BeginTime="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
<!-- Content Lines -->
<Border Width="300" Height="12"
Background="#e2e8f0"
CornerRadius="4"
Margin="0,8,0,0">
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="1" To="0.5" Duration="0:0:1"
AutoReverse="True"
RepeatBehavior="Forever"
BeginTime="0:0:0.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
<Border Width="280" Height="12"
Background="#e2e8f0"
CornerRadius="4">
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="1" To="0.5" Duration="0:0:1"
AutoReverse="True"
RepeatBehavior="Forever"
BeginTime="0:0:0.4"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
</StackPanel>
</Border>
Іноді потрібно запускати анімації з коду, а не через EventTrigger.
private void AnimateButton_Click(object sender, RoutedEventArgs e)
{
// Створюємо анімацію
var animation = new DoubleAnimation
{
From = 0,
To = 1,
Duration = TimeSpan.FromMilliseconds(500),
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }
};
// Створюємо Storyboard
var storyboard = new Storyboard();
storyboard.Children.Add(animation);
// Встановлюємо Target та Property
Storyboard.SetTarget(animation, MyButton);
Storyboard.SetTargetProperty(animation, new PropertyPath("Opacity"));
// Запускаємо
storyboard.Begin();
}
private Storyboard? _currentStoryboard;
private void StartAnimation_Click(object sender, RoutedEventArgs e)
{
_currentStoryboard = CreateFadeInStoryboard();
_currentStoryboard.Begin();
}
private void PauseAnimation_Click(object sender, RoutedEventArgs e)
{
_currentStoryboard?.Pause();
}
private void ResumeAnimation_Click(object sender, RoutedEventArgs e)
{
_currentStoryboard?.Resume();
}
private void StopAnimation_Click(object sender, RoutedEventArgs e)
{
_currentStoryboard?.Stop();
}
private Storyboard CreateFadeInStoryboard()
{
var animation = new DoubleAnimation
{
From = 0,
To = 1,
Duration = TimeSpan.FromSeconds(2)
};
var storyboard = new Storyboard();
storyboard.Children.Add(animation);
Storyboard.SetTarget(animation, MyButton);
Storyboard.SetTargetProperty(animation, new PropertyPath("Opacity"));
return storyboard;
}
private void StartAnimation_Click(object sender, RoutedEventArgs e)
{
var storyboard = CreateStoryboard();
// Підписка на події
storyboard.Completed += (s, args) =>
{
MessageBox.Show("Анімація завершена!");
};
storyboard.CurrentTimeInvalidated += (s, args) =>
{
// Викликається при кожному кадрі
var progress = storyboard.GetCurrentTime().TotalSeconds / storyboard.Duration.TimeSpan.TotalSeconds;
ProgressTextBlock.Text = $"Прогрес: {progress:P0}";
};
storyboard.Begin();
}
Анімації можуть суттєво вплинути на продуктивність додатку, особливо на слабких пристроях. Розуміння того, як WPF рендерить анімації, допоможе створювати плавні та ефективні інтерфейси.
WPF підтримує апаратне прискорення через DirectX. Деякі анімації виконуються на GPU (швидко), інші на CPU (повільно).
Анімації на GPU (швидкі):
Opacity — найшвидша анімаціяRenderTransform (TranslateTransform, ScaleTransform, RotateTransform, SkewTransform)Clip (RectangleGeometry)Анімації на CPU (повільні):
Width, Height — викликають перерахунок layoutMargin, Padding — викликають перерахунок layoutLayoutTransform — викликає перерахунок layoutPath геометріїRenderTransform замість Width/Height/Margin для анімацій переміщення та масштабування. RenderTransform виконується на GPU і не викликає перерахунок layout.❌ Неправильно (повільно):
<!-- Анімація Width викликає перерахунок layout на кожному кадрі -->
<Border x:Name="Sidebar" Width="0">
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Width"
From="0" To="250" Duration="0:0:0.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
✅ Правильно (швидко):
<!-- Анімація ScaleTransform виконується на GPU -->
<Border Width="250">
<Border.RenderTransform>
<ScaleTransform ScaleX="0" ScaleY="1"/>
</Border.RenderTransform>
<Border.RenderTransformOrigin>0,0.5</Border.RenderTransformOrigin>
<Border.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleX)"
From="0" To="1" Duration="0:0:0.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
Для складних елементів з багатьма дочірніми елементами використовуйте CacheMode="BitmapCache". Це кешує візуальне представлення як bitmap і анімує його на GPU.
<Border CacheMode="BitmapCache">
<Border.RenderTransform>
<TranslateTransform X="0" Y="0"/>
</Border.RenderTransform>
<!-- Складний вміст з багатьма елементами -->
<StackPanel>
<TextBlock Text="Title"/>
<TextBlock Text="Subtitle"/>
<!-- ... багато інших елементів ... -->
</StackPanel>
</Border>
Коли використовувати BitmapCache:
Коли НЕ використовувати BitmapCache:
За замовчуванням WPF намагається досягти 60 FPS. Для деяких анімацій це надмірно і можна знизити FPS для економії ресурсів.
<Storyboard Timeline.DesiredFrameRate="30">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="0" To="1" Duration="0:0:2"/>
</Storyboard>
Рекомендації:
Об'єкти, що не змінюються, можна "заморозити" через Freeze(). Це дозволяє WPF оптимізувати їх використання.
// Створюємо Brush і заморожуємо його
var brush = new SolidColorBrush(Colors.Blue);
brush.Freeze(); // Тепер brush незмінний і оптимізований
// Використовуємо в анімації
var animation = new ColorAnimation
{
To = Colors.Red,
Duration = TimeSpan.FromSeconds(1)
};
animation.Freeze(); // Заморожуємо анімацію
// Frozen об'єкти можна використовувати в кількох потоках
Переваги Freeze:
Для виявлення проблем з продуктивністю використовуйте Visual Studio Profiler або WPF Performance Suite.
Включення FPS counter в WPF:
// В App.xaml.cs або MainWindow.xaml.cs
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Показати FPS counter
System.Diagnostics.PresentationTraceSources.SetTraceLevel(
this,
System.Diagnostics.PresentationTraceLevel.High
);
}
✅ Використовуйте RenderTransform
✅ Opacity для fade-in/out
✅ BitmapCache для складних елементів
✅ Freeze незмінні об'єкти
❌ Уникайте Width/Height анімацій
❌ Уникайте LayoutTransform
⚡ Знижуйте FPS для фонових анімацій
🔍 Профілюйте продуктивність
| Анімація | Продуктивність | GPU/CPU | Layout Recalc | Рекомендація |
|---|---|---|---|---|
Opacity | ⚡⚡⚡ Відмінна | GPU | Ні | Завжди використовуйте |
RenderTransform | ⚡⚡⚡ Відмінна | GPU | Ні | Завжди використовуйте |
Clip | ⚡⚡ Добра | GPU | Ні | Можна використовувати |
Width/Height | ⚠️ Погана | CPU | Так | Уникайте |
Margin/Padding | ⚠️ Погана | CPU | Так | Уникайте |
LayoutTransform | ⚠️ Погана | CPU | Так | Уникайте |
Background Color | ⚡ Середня | GPU | Ні | Можна використовувати |
FontSize | ⚠️ Погана | CPU | Так | Уникайте |
Мета: Навчитися створювати базову анімацію через EventTrigger.
Завдання:
Створіть вікно з елементами, що плавно з'являються при завантаженні:
Критерії успіху:
Підказка:
<StackPanel>
<TextBlock Text="Заголовок" Opacity="0">
<TextBlock.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="0" To="1" Duration="0:0:0.5"
BeginTime="0:0:0">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
<TextBlock Text="Підзаголовок" Opacity="0">
<TextBlock.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="0" To="1" Duration="0:0:0.5"
BeginTime="0:0:0.2">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
</StackPanel>
Мета: Навчитися створювати складнішу анімацію з code-behind контролем.
Завдання:
Створіть бічну панель, що виїжджає зліва:
Критерії успіху:
Підказка:
private bool _isSidebarOpen = false;
private void ToggleSidebar_Click(object sender, RoutedEventArgs e)
{
_isSidebarOpen = !_isSidebarOpen;
var widthAnimation = new DoubleAnimation
{
To = _isSidebarOpen ? 250 : 0,
Duration = TimeSpan.FromMilliseconds(300),
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
};
var overlayAnimation = new DoubleAnimation
{
To = _isSidebarOpen ? 0.5 : 0,
Duration = TimeSpan.FromMilliseconds(300)
};
Sidebar.BeginAnimation(WidthProperty, widthAnimation);
Overlay.BeginAnimation(OpacityProperty, overlayAnimation);
Overlay.IsHitTestVisible = _isSidebarOpen;
}
Мета: Навчитися створювати нескінченні анімації з RepeatBehavior та AutoReverse.
Завдання:
Створіть індикатор завантаження з пульсуючими точками:
Критерії успіху:
Підказка:
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="8">
<Ellipse x:Name="Dot1" Width="20" Height="20" Fill="#3b82f6">
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard x:Name="Dot1Storyboard">
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="1" To="0.3" Duration="0:0:0.8"
AutoReverse="True"
RepeatBehavior="Forever"
BeginTime="0:0:0"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
<Ellipse Width="20" Height="20" Fill="#3b82f6">
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="1" To="0.3" Duration="0:0:0.8"
AutoReverse="True"
RepeatBehavior="Forever"
BeginTime="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
<Ellipse Width="20" Height="20" Fill="#3b82f6">
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="1" To="0.3" Duration="0:0:0.8"
AutoReverse="True"
RepeatBehavior="Forever"
BeginTime="0:0:0.4"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
</StackPanel>
Анімації у WPF через Storyboard — потужний інструмент для створення інтерактивних та професійних інтерфейсів. Ми детально розглянули всі аспекти анімацій: від базового синтаксису до складних KeyFrame анімацій та оптимізації продуктивності.
Ключові висновки:
📦 Storyboard
🎬 Типи анімацій
⏱️ Duration & Repeat
📈 Easing Functions
🔄 Transform анімації
🎯 KeyFrame анімації
💻 Code-behind
⚡ Продуктивність
Що далі?
Наступна стаття — Avalonia Animations покаже простіший підхід через Transitions та CSS-подібні анімації.
Avalonia TemplatedControl — Lookless Controls
Створення custom controls в Avalonia через TemplatedControl. StyledProperty замість DependencyProperty, Generic.axaml, ControlTheme та CSS-like стилізація з pseudo-classes.
Анімації в Avalonia
Transitions та KeyFrame анімації в Avalonia UI — простіший підхід до анімацій порівняно з WPF