Что общего между конечными автоматами, анимацией и Xamarin.Forms

Habrahabr

Если вы были студентом технической специальности, то наверняка помните курс, посвященный конечным автоматам. Эта простая, но очень емкая модель (конечный автомат, он же finite state machine, он же FSM) используется довольно широко, хотя и большинство программистов о ней незаслуженно забывают. Сегодня мы поговорим о конечных автоматах и их применении в создании комплексных анимаций в приложениях на Xamarin.Forms.

Анимации в Xamarin.Forms

Если вы уже используете Xamarin.Forms в реальных проектах, то наверняка сталкивались со встроенным механизмом анимаций. Если нет, то рекомендуем начать знакомство со статей «Creating Animations with Xamarin.Forms» и «Compound Animations».

Чаще всего требуется анимировать следующие свойства:

  • Scale, масштаб элемента;
  • Opacity, прозрачность;
  • Translation, дополнительное смещение по x, y, относительно полученного при компоновке положения;
  • Rotation, вращение вокруг осей x, y, z.

В Xamarin.Forms для задания обозначенных свойств используются механизмы ОС низкого уровня, что отлично сказывается на производительности — нет проблем анимировать сразу целую кучу объектов. В нашем примере мы остановимся именно на этих свойствах, но при желании вы сможете самостоятельно расширить описанные ниже механизмы.

Конечные автоматы

Если описать конечный автомат человеческим языком, то это некий объект, который может находится в различных устойчивых состояниях (например, “загрузка” или “ошибка”). Свои состояния автомат меняет под воздействием внешних событий. Количество состояний конечно. Примерами таких автоматов являются лифты и светофоры.

Если вы по какой-то причине не учились в институте на технической специальности или не изучали теорию, то рекомендуем начать знакомство с этой статьи.

Какое это все имеет отношение к анимациям и тем более к Xamarin.Forms? Давайте посмотрим.

Один экран, много состояний, есть анимации перехода

В статье «Работаем с состояниями экранов в Xamarin.Forms» мы уже описывали компонент StateContainer, упрощающий разработку сложных интерфейсов и подходящий для большинства экранов в бизнес-приложениях. Этот компонент хорошо работает, когда у нас все состояния существуют независимо друг от друга и между ними достаточно простого перехода «один исчез — второй появился».

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

В качестве примера давайте рассмотрим экран ввода адреса и работы с картой, как это реализуется в большинстве навигаторов.

Представим, что у нас анимированные переходы между следующими состояниями ОДНОГО экрана:

Как видим, у нас получается такой конечный автомат:

Необходимо реализовать следующие анимации при переходе из состояния в состояние:

  • При входе в FindAddress нужно скрыть с анимацией старый контент и плавно показать новый. Плюс для пикантности будем анимировать кнопки во время появления;
  • При переходе в ShowRoute необходимо скрыть старое состояние, а снизу экрана должна выехать табличка с информацией о маршруте;
  • При переходе в Drive необходимо скрыть старое состояние и сверху должна выехать табличка с информацией о маршруте;
  • При переходе в Main (кроме первого запуска) необходимо скрыть текущее состояние и плавно отобразить кнопку, добавим к ней также небольшую анимацию изменения масштаба.

Пишем свой автомат

Мы возьмем самую простую реализацию:

  • У автомата есть фиксированный набор состояний, которые задаются при инициализации;
  • Каждое из состояний описывается набором необходимых анимаций (конечные значения properties) для элементов UI;
  • При входе в новое состояние параллельно запускаются все анимации из массива, добавленного при инициализации автомата.

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

Итак, простейший автомат, который мы назовем Storyboard, будет выглядеть следующим образом:

public enum AnimationType {
    Scale,
    Opacity,
    TranslationX,
    TranslationY,
    Rotation
}

public class Storyboard {
    readonly Dictionary<string, ViewTransition[]> _stateTransitions = new Dictionary<string, ViewTransition[]>();

    public void Add(object state, ViewTransition[] viewTransitions) {
        var stateStr = state?.ToString().ToUpperInvariant();
        _stateTransitions.Add(stateStr, viewTransitions);
    }

    public void Go(object newState, bool withAnimation = true) {
        var newStateStr = newState?.ToString().ToUpperInvariant();
        
        // Get all ViewTransitions 
        var viewTransitions = _stateTransitions[newStateStr];
        
        // Get transition tasks
        var tasks = viewTransitions.Select(viewTransition => viewTransition.GetTransition(withAnimation));
        
        // Run all transition tasks
        Task.WhenAll(tasks);
    }
}

public class ViewTransition {
        // Skipped. See complete sample in repository below
        
    public async Task GetTransition(bool withAnimation) {
        VisualElement targetElement;
        if( !_targetElementReference.TryGetTarget(out targetElement) )
           throw new ObjectDisposedException("Target VisualElement was disposed");
        
        if( _delay > 0 ) await Task.Delay(_delay);
        
        withAnimation &= _length > 0;
        
        switch ( _animationType ) {
            case AnimationType.Scale:
                if( withAnimation )
                   await targetElement.ScaleTo(_endValue, _length, _easing);
                else
                   targetElement.Scale = _endValue;
                break;
                
                // See complete sample in repository below
            default:
                throw new ArgumentOutOfRangeException();
        }
    }
}

В примере выше опущены проверки входных данных, полную версию можете найти в репозитории (ссылка в конце статьи).

Как видим, при переходе в новое состояние просто в параллели происходят плавные изменения необходимых свойств. Есть также возможность перейти в новое состояние без анимации.

Используем конечный автомат

Итак, автомат у нас есть и мы можем подключить его для задания необходимых состояния элементов. Пример добавления нового состояния:

_storyboard.Add(States.Drive, new[] {
   new ViewTransition(ShowRouteView, AnimationType.TranslationY, 200),
   new ViewTransition(ShowRouteView, AnimationType.Opacity, 0, 0, delay: 250),
   new ViewTransition(DriveView, AnimationType.TranslationY, 0, 300, delay: 250), // Active and visible
   new ViewTransition(DriveView, AnimationType.Opacity, 1, 0) // Active and visible
});

Как видим, для состояния Drive мы задали массив индивидуальных анимаций. ShowRouteView и DriveView — обычные View, заданные в XAML, пример ниже.

А вот для перехода в новое состояние достаточно простоы вызвать метод Go():

_storyboard.Go(States.ShowRoute);

Кода получается относительно немного и групповые анимации создаются по факту просто набором чисел. Работать наш конечный автомат может не только со страницами, но и с отдельными View, что расширяет варианты его применения. Использовать Storyboard лучше внутри кода страницы (Page), не перемешивая его с бизнес-логикой.

Также приведем пример XAML, в котором описаны все элементы пользовательского интерфейса.

Если вы решите добавить возможность смены цвета элементов с помощью анимаций, то рекомендуем познакомиться с реализацией, описанной в статье «Building Custom Animations in Xamarin.Forms».

Полный код проекта из статьи вы можете найти в нашем репозитории: https://bitbucket.org/binwell/statemachine.

И как всегда, задавайте ваши вопросы в комментариях. До связи!

Об авторе

Вячеслав Черников — руководитель отдела разработки компании Binwell, Microsoft MVP и Xamarin Certified Developer. В прошлом — один из Nokia Champion и Qt Certified Specialist, в настоящее время — специалист по платформам Xamarin и Azure. В сферу mobile пришел в 2005 году, с 2008 года занимается разработкой мобильных приложений: начинал с Symbian, Maemo, Meego, Windows Mobile, потом перешел на iOS, Android и Windows Phone. Статьи Вячеслава вы также можете прочитать в блоге на Medium.

Другие статьи автора: