Анімації у WPF: Storyboard та Easing Functions
Анімації у WPF: Storyboard та Easing Functions
Анімації — це те, що робить інтерфейс живим. Плавне відкриття бічної панелі, fade-in при завантаженні сторінки, пульсуючий індикатор завантаження, зміна кольору кнопки при наведенні — все це анімації. Вони покращують user experience, роблять інтерфейс зрозумілішим та приємнішим у використанні.
WPF має потужну систему анімацій через Storyboard — контейнер для анімацій, що може змінювати будь-які властивості контролів у часі. Можна анімувати числа (Width, Opacity), кольори (Background), товщину (Margin), та навіть складні трансформації (обертання, масштабування).
У цій статті ми детально розберемо всі аспекти анімацій у WPF: від базового синтаксису до складних easing functions та code-behind контролю. Ви навчитесь створювати професійні анімації, що роблять ваш інтерфейс сучасним та інтерактивним.
Storyboard: контейнер для анімацій
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
Запуск анімації через EventTrigger
<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 підтримує різні типи анімацій для різних типів властивостей.
DoubleAnimation: анімація чисел
Для властивостей типу 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"/>
ColorAnimation: анімація кольорів
Для властивостей типу 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.
ThicknessAnimation: анімація Margin/Padding
Для властивостей типу 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>
ObjectAnimationUsingKeyFrames: дискретні значення
Для властивостей будь-якого типу, що не підтримують інтерполяцію (наприклад, 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 | Будь-який тип | Мигання, дискретні зміни |
Duration та RepeatBehavior
Duration: тривалість анімації
Формат: 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"/>
RepeatBehavior: повторення
<!-- Виконати 1 раз (за замовчуванням) -->
<DoubleAnimation RepeatBehavior="1x"/>
<!-- Виконати 3 рази -->
<DoubleAnimation RepeatBehavior="3x"/>
<!-- Повторювати нескінченно -->
<DoubleAnimation RepeatBehavior="Forever"/>
<!-- Повторювати протягом 5 секунд -->
<DoubleAnimation RepeatBehavior="0:0:5"/>
AutoReverse: повернення назад
<!-- Анімація виконується туди-назад -->
<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: нелінійна зміна значень
Easing Functions роблять анімації природнішими, додаючи прискорення та уповільнення. Замість лінійної зміни (постійна швидкість), значення змінюється за кривою.
Базові 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— прискорення на початку + уповільнення в кінці
Типи Easing Functions
| Функція | Опис | Використання |
|---|---|---|
QuadraticEase | Квадратична крива | Універсальна, природна |
CubicEase | Кубічна крива | Більш виражена |
QuarticEase | Крива 4-го степеня | Дуже виражена |
QuinticEase | Крива 5-го степеня | Екстремальна |
SineEase | Синусоїдальна | Плавна, м'яка |
ExponentialEase | Експоненційна | Різке прискорення |
CircleEase | Кругова | Плавна, природна |
BackEase | З "відскоком" назад | Ефект пружини |
ElasticEase | Еластична | Ефект гумки |
BounceEase | З відскоком | Ефект м'яча |
Приклади з різними Easing Functions
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
Щоб зрозуміти різницю між EasingMode, уявіть автомобіль, що рухається від точки A до точки B:
- EaseIn — автомобіль починає повільно, потім прискорюється. Використовується рідко, бо виглядає неприродньо для UI.
- EaseOut — автомобіль починає швидко, потім уповільнюється перед зупинкою. Найприродніший варіант для UI-анімацій.
- EaseInOut — автомобіль прискорюється на початку та уповільнюється в кінці. Підходить для циклічних анімацій.
<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 Functions
Кожна Easing Function створює унікальну криву руху. Ось як вони відрізняються:
QuadraticEase vs CubicEase vs QuarticEase:
Ці функції відрізняються "силою" прискорення/уповільнення:
- QuadraticEase (x²) — м'яке, ледь помітне прискорення. Підходить для тонких анімацій.
- CubicEase (x³) — середнє прискорення. Універсальний вибір для більшості UI.
- QuarticEase (x⁴) — сильне прискорення. Для драматичних ефектів.
<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: анімація вкладених властивостей
Для анімації властивостей всередині інших об'єктів використовується Property Path.
Синтаксис 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"/>
Анімація Transform
<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>
Анімація окремих значень Thickness
<!-- Анімація лише 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 анімації: обертання, масштабування, переміщення
Transform — це потужний механізм WPF для зміни візуального представлення елементів без зміни їх логічного розташування. Анімація Transform дозволяє створювати ефекти обертання, масштабування, нахилу та переміщення.
RenderTransform vs LayoutTransform
WPF має два типи трансформацій:
- RenderTransform — застосовується після layout. Не впливає на розташування інших елементів. Використовується для анімацій.
- LayoutTransform — застосовується під час layout. Впливає на розташування інших елементів. Рідко використовується для анімацій.
Для анімацій завжди використовуйте RenderTransform!
TranslateTransform: переміщення
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: масштабування
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: обертання
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: нахил
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: комбінування трансформацій
Для одночасного застосування кількох трансформацій використовується 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 анімації: складні траєкторії руху
KeyFrame анімації дозволяють створювати складні анімації з кількома проміжними точками. Замість простого руху від A до B, ви можете визначити точки C, D, E з різними швидкостями та easing functions.
DoubleAnimationUsingKeyFrames
Найпопулярніший тип 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>
Типи KeyFrame
WPF підтримує три типи KeyFrame:
| Тип | Опис | Використання |
|---|---|---|
LinearDoubleKeyFrame | Лінійна інтерполяція | Постійна швидкість між точками |
EasingDoubleKeyFrame | З Easing Function | Природні анімації з прискоренням/уповільненням |
DiscreteDoubleKeyFrame | Миттєва зміна | Стрибок без інтерполяції |
SplineDoubleKeyFrame | Кубічна крива Безьє | Максимальний контроль над кривою |
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" — дві контрольні точки кривої Безьє.
ColorAnimationUsingKeyFrames
Для анімації кольорів з кількома проміжними точками:
<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>
ObjectAnimationUsingKeyFrames: дискретні зміни
Для властивостей, що не підтримують інтерполяцію (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>
- Складні траєкторії руху (не просто A → B)
- Анімації з паузами між етапами
- Зміна кольору через кілька проміжних кольорів
- Комбінування різних Easing Functions в одній анімації
- Синхронізація кількох властивостей з різними таймінгами
Реальні приклади анімацій
Notification Toast: slide-in + fade-out
Типова анімація для сповіщень — з'являється зверху, затримується, зникає.
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>
Modal Dialog: fade-in overlay + scale-in dialog
Типова анімація для модальних вікон — затемнення фону + масштабування діалогу.
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>
Progress Bar: плавне заповнення
Анімація прогрес-бару з плавним заповненням.
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 Loading: пульсуючі плейсхолдери
Сучасний підхід до індикації завантаження — 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>
Code-behind анімації: програмний контроль
Іноді потрібно запускати анімації з коду, а не через EventTrigger.
Створення та запуск Storyboard у коді
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();
}
Контроль виконання Storyboard
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;
}
Події 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();
}
Продуктивність анімацій: оптимізація та best practices
Анімації можуть суттєво вплинути на продуктивність додатку, особливо на слабких пристроях. Розуміння того, як WPF рендерить анімації, допоможе створювати плавні та ефективні інтерфейси.
Hardware Acceleration: GPU vs CPU
WPF підтримує апаратне прискорення через DirectX. Деякі анімації виконуються на GPU (швидко), інші на CPU (повільно).
Анімації на GPU (швидкі):
Opacity— найшвидша анімаціяRenderTransform(TranslateTransform, ScaleTransform, RotateTransform, SkewTransform)Clip(RectangleGeometry)
Анімації на CPU (повільні):
Width,Height— викликають перерахунок layoutMargin,Padding— викликають перерахунок layoutLayoutTransform— викликає перерахунок layout- Складні
Pathгеометрії - Анімації з великою кількістю елементів
RenderTransform замість Width/Height/Margin для анімацій переміщення та масштабування. RenderTransform виконується на GPU і не викликає перерахунок layout.Приклад: правильна vs неправильна анімація
❌ Неправильно (повільно):
<!-- Анімація 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: кешування візуального дерева
Для складних елементів з багатьма дочірніми елементами використовуйте 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:
- Анімація складних елементів з багатьма дочірніми елементами
- Анімація елементів з градієнтами або складними Brush
- Анімація елементів з ефектами (DropShadowEffect, BlurEffect)
Коли НЕ використовувати BitmapCache:
- Прості елементи (один Border, один TextBlock)
- Елементи, що часто змінюють вміст під час анімації
- Статичні елементи без анімацій
Timeline.DesiredFrameRate: контроль FPS
За замовчуванням WPF намагається досягти 60 FPS. Для деяких анімацій це надмірно і можна знизити FPS для економії ресурсів.
<Storyboard Timeline.DesiredFrameRate="30">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="0" To="1" Duration="0:0:2"/>
</Storyboard>
Рекомендації:
- 60 FPS — для швидких, критичних анімацій (hover ефекти, transitions)
- 30 FPS — для повільних анімацій (fade-in при завантаженні)
- 15 FPS — для фонових анімацій (пульсуючі індикатори)
Freeze: заморожування об'єктів
Об'єкти, що не змінюються, можна "заморозити" через 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:
- Об'єкт стає thread-safe (можна використовувати в кількох потоках)
- WPF оптимізує використання пам'яті
- Швидший рендеринг
Профілювання анімацій
Для виявлення проблем з продуктивністю використовуйте 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
);
}
Best Practices: чек-лист
✅ Використовуйте 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 | Так | Уникайте |
Практичні завдання
Рівень 1: Fade-in при завантаженні
Мета: Навчитися створювати базову анімацію через EventTrigger.
Завдання:
Створіть вікно з елементами, що плавно з'являються при завантаженні:
- UI:
- Заголовок (TextBlock)
- Підзаголовок (TextBlock)
- Кнопка
- Анімація:
- Всі елементи починають з Opacity="0"
- При Loaded → Opacity змінюється до 1
- Тривалість: 0.5 секунди
- Easing: QuadraticEase EaseOut
- Додатково:
- Затримка між елементами (BeginTime)
- Заголовок з'являється першим
- Підзаголовок через 0.2 секунди
- Кнопка через 0.4 секунди
Критерії успіху:
- Fade-in працює при завантаженні
- Використано EventTrigger з Loaded
- Easing Function застосовано
- Затримки між елементами працюють
Підказка:
<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>
Рівень 2: Анімована sidebar панель
Мета: Навчитися створювати складнішу анімацію з code-behind контролем.
Завдання:
Створіть бічну панель, що виїжджає зліва:
- UI:
- Кнопка "Відкрити меню"
- Sidebar (Border) з Width="0" за замовчуванням
- Вміст sidebar (список пунктів меню)
- Анімація:
- При натисканні кнопки → Width змінюється з 0 до 250
- Тривалість: 0.3 секунди
- Easing: CubicEase EaseOut
- Повторне натискання → закриває sidebar (Width до 0)
- Додатково:
- Напівпрозорий overlay при відкритій sidebar
- Клік на overlay закриває sidebar
- Fade-in/out для overlay
Критерії успіху:
- Sidebar відкривається/закривається плавно
- Використано code-behind для контролю
- Easing Function застосовано
- Overlay працює правильно
Підказка:
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;
}
Рівень 3: Пульсуючий індикатор завантаження
Мета: Навчитися створювати нескінченні анімації з RepeatBehavior та AutoReverse.
Завдання:
Створіть індикатор завантаження з пульсуючими точками:
- UI:
- 3 кола (Ellipse)
- Розташовані горизонтально
- Анімація:
- Кожне коло пульсує (Opacity 1 → 0.3 → 1)
- Тривалість: 0.8 секунди
- AutoReverse="True"
- RepeatBehavior="Forever"
- Затримка між колами (BeginTime)
- Додатково:
- Анімація ScaleTransform (збільшення/зменшення)
- Зміна кольору через ColorAnimation
- Кнопка Start/Stop для контролю
Критерії успіху:
- Пульсація працює нескінченно
- Затримки між колами створюють ефект хвилі
- 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