Pull to refresh

Сервелат, анимация и старый добрый code-behind

Reading time4 min
Views3.3K
Решил немножко покопаться в Silverlight, да смастерить на нём что-нибудь прикольное. Это прикольное, конечно, должно шевелиться, переливаться и плавно подёргиваться, ибо вебдваноль у нас или где? :). И вот тут мне пришлось столкнуться с неплохой, по сути, системой анимаций в WPF/Silverlight. Покурив MSDN, я бодренько приступил к написанию анимаций в XAML. Одну написал, вторую, третью… А потом мне захотелось сделать так, чтобы они шли в определённой последовательности. И вот тут-то я и понял, что XAML, зараза, очень избыточный. Для описания интерфейсов он подходит идеально: сразу видно, что к чему относится и надобность в визуальном редакторе отпадает чуть менее, чем полностью. Но вот когда пытаешься написать в этом XAMLе какую-то логику, начинает проявляться вся его несуразность. Покурив гугл, я был сильно удивлён тем, что большинство людей упорно пытаются впихнуть в XAML абсолютно всё. Ругаются, путаются в коде, плачут, но продолжают писать. Прямо как те мыши с кактусом, чесслово. И тут мне пришла идея аккуратно описать анимации обычным кодом на C#. Мы, так сказать, олдфаги, рисовали интерфейс прямыми вызовами к WinAPI, неужто нас какие-то анимации испугают? :)


В результате получился вот такой портабельный класс AnimationBag. Портабельный он потому, что его безо всяких изменений можно использовать как в WPF, так и в Silverlight.

public class AnimationItem
{
	public event EventHandler Completed;

	private void OnStoryboardComplete(object sender, EventArgs e)
	{
		if (Completed != null)
			Completed(this, EventArgs.Empty);
	}

	private Storyboard storyboard;
	public Storyboard Storyboard
	{
		get { return storyboard; }
		set
		{
			if (storyboard != null)
				storyboard.Completed -= OnStoryboardComplete;

			storyboard = value;
			storyboard.Completed += OnStoryboardComplete;
		}
	}

	public Action BeginAction { get; set; }
	public Action EndAction { get; set; }
}

public class AnimationBag
{
	private readonly Dictionary<string, AnimationItem> storyboards = new Dictionary<string, AnimationItem>();

	public void AddAnimation(string name, DependencyObject control, string propertyName, Timeline animation, Action beginAction = null, Action endAction = null)
	{
		Storyboard board = new Storyboard();
		AnimationItem item = new AnimationItem { BeginAction = beginAction, EndAction = endAction };

		Storyboard.SetTarget(animation, control);
		Storyboard.SetTargetProperty(animation, new PropertyPath(propertyName));
		board.Children.Add(animation);
		if (endAction != null)
			item.Completed += item_Completed;
		item.Storyboard = board;

		storyboards[name] = item;
	}

	private void item_Completed(object sender, EventArgs e)
	{
		KeyValuePair<string, AnimationItem> pair = storyboards.Where(x => x.Value.Equals(sender)).FirstOrDefault();

		if (pair.Value != null && pair.Value.EndAction != null)
			pair.Value.EndAction.Invoke();
	}

	public void StartAnimation(string name)
	{
		if (!storyboards.ContainsKey(name))
			return;

		if (storyboards[name].BeginAction != null)
			storyboards[name].BeginAction.Invoke();

		storyboards[name].Storyboard.Begin();
	}

	public void StopAnimation(string name)
	{
		if (!storyboards.ContainsKey(name))
			return;

		storyboards[name].Storyboard.Stop();

		if (storyboards[name].EndAction != null)
			storyboards[name].EndAction.Invoke();
	}

	public static DoubleAnimation CreateDoubleAnimation(double? to, long durationMs, double? from = null, bool repeat = false)
	{
		DoubleAnimation ret = new DoubleAnimation { To = to, From = from, Duration = new Duration(TimeSpan.FromMilliseconds(durationMs)) };

		if (repeat)
			ret.RepeatBehavior = RepeatBehavior.Forever;

		return ret;
	}
}


Как видно из кода, класс предельно простой. Вот пример использования.

XAML:
<Window x:Class="Animation.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Rectangle Height="110" HorizontalAlignment="Left" Margin="55,71,0,0" Name="rectangle1" Stroke="Black" VerticalAlignment="Top" Width="181" Fill="#FF9D3434" />
        <Button Content="Button" Height="36" HorizontalAlignment="Left" Margin="286,93,0,0" Name="button1" VerticalAlignment="Top" Width="149" Click="button1_Click" />
    </Grid>
</Window>


Code-behind:
public partial class MainWindow : Window
{
	private readonly AnimationBag animations = new AnimationBag();

	public MainWindow()
	{
		InitializeComponent();
		InitAnimations();
	}

	private void InitAnimations()
	{
		animations.AddAnimation(
			"fadeOut",
			rectangle1,
			"Opacity",
			AnimationBag.CreateDoubleAnimation(0, 500),
			null,
			() => animations.StartAnimation("fadeIn"));

		animations.AddAnimation(
			"fadeIn",
			rectangle1,
			"Opacity",
			AnimationBag.CreateDoubleAnimation(1, 500));
	}

	private void button1_Click(object sender, RoutedEventArgs e)
	{
		animations.StartAnimation("fadeOut");
	}
}


Сам класс представляет собой что-то вроде словаря с ключами — именами анимаций и значениями — анимациями. В методе InitAnimations в коллекцию добавляются две анимации, при этом указывается имя, контрол, над которым будет производится действо, свойство этого контрола и сам объект анимации. Его можно создавать ручками, а можно добавить статические хэлперы к уже имеющемуся методу для DoubleAnimation. Кроме всего прочего, метод AddAnimation может принимать два делегата, которые будут выполняться до и после самой анимации. Например, здесь после завершения анимации “fadeOut” сразу запускается “fadeIn”.

В итоге, получился довольно удобный механизм, позволяющий описывать анимации одной строкой кода вместо килобайтов перегруженного XAML.

Скачать исходники с тестовыми проектами
Tags:
Hubs:
Total votes 31: ↑18 and ↓13+5
Comments10

Articles