CSS и JavaScript анимация в AngularJS

В этом уроке мы улучшим наше веб-приложение путем добавления CSS и JavaScript анимаций для наших шаблонов.
  • Мы будем использовать модуль ngAnimate для обеспечения возможности анимации.
  • Мы будем использовать основные ng директивы для того чтобы автоматически запускать анимации.
  • Когда в приложении встречается анимация, она работает в промежутке времени в котором выполняется стандартная DOM-операция над элементом (например, вставка или удаление узлов в ngRepeat или вставка или удаление классов в ngClass).

Переключим проект на состояние соответствующее данному уроку:


git checkout -f step-12
Изменения относительно предыдущего урока.


Зависимости

Функциональность анимаций обеспечивается модулем ngAnimate, который не входит в поставку Angular-фреймворка. В дополнении мы будем использовать jQuery-анимации для дополнительных JavaScript-анимаций.

Мы используем Bower для установки зависимостей на клиентской стороне. Включим в bower.json новую зависимость:

    {
      "name": "angular-seed",
      "description": "A starter project for AngularJS",
      "version": "0.0.0",
      "homepage": "https://github.com/angular/angular-seed",
      "license": "MIT",
      "private": true,
      "dependencies": {
        "angular": "1.4.x",
        "angular-mocks": "1.4.x",
        "jquery": "~2.1.1",
        "bootstrap": "~3.1.1",
        "angular-route": "1.4.x",
        "angular-resource": "1.4.x",
        "angular-animate": "1.4.x"
      }
    }

    Строка "angular-animate": "1.4.x" означает установить компонент angular-animate совместимый с версией 1.4.x.

    Строка "jquery": "~2.1.1" означает установить версию jQuery 2.1.1. Мы можем использовать bower для установки широкого набора сторонних библиотек.

    Теперь нужно сделать так чтобы bower скачал и установил зависимость:
    npm install

    Если с момента, когда вы запускали последний раз npm install, вышла новая версия Angular, то у вас могут быть конфликты при выполнении команды bower install из-за того что потребуется установка различных версий Angular. Для решения проблемы надо просто удалить папку app/bower_components и снова запустить npm install.

    Если bower установлен в системе глобально, то вы можете выполнить команду bower install, но наш проект так настроен, что npm install сделает это за нас.

    Устройство анимаций сделанных с помощью ngAnimate


    Шаблон

    Нужно подключить в HTML-шаблон файлы ресурсов в которых описаны анимации. И нужно подключить файл angular-animate.js. Модуль для анимаций ngAnimate определяется в файле angular-animate.js.

    ...
      <!-- for CSS Transitions and/or Keyframe Animations -->
      <link rel="stylesheet" href="css/animations.css">
    
      ...
    
      <!-- jQuery is used for JavaScript animations (include this before angular.js) -->
      <script src="bower_components/jquery/dist/jquery.js"></script>
    
      ...
    
      <!-- required module to enable animation support in AngularJS -->
      <script src="bower_components/angular-animate/angular-animate.js"></script>
    
      <!-- for JavaScript Animations -->
      <script src="js/animations.js"></script>
    
    ...

    Когда вы используете Angular 1.4 убедитесь также, что вы используете jQuery 2.1 или выше. jQuery 1.x официально не поддерживается. jQuery должен загружаться перед AngularJS скриптами. В противном случае AngularJS не определит jQuery и анимации не будут работать так как ожидается.

    Анимации прописаны в CSS-коде (animations.css), а JavaScript анимации соответственно в JavaScript-коде (animations.js). Но сначала надо создать модуль, который будет использовать модуль ngAnimate как зависимость аналогично тому как мы это делали для ngResource.

    Модуль и анимации

    angular.module('phonecatAnimations', ['ngAnimate']);
    // ...
    // this module will later be used to define animations
    // ...

    Подключим модуль к нашему модулю приложения...
    // ...
    angular.module('phonecatApp', [
      'ngRoute',
    
      'phonecatAnimations',
      'phonecatControllers',
      'phonecatFilters',
      'phonecatServices',
    ]);
    // ...

    Теперь phonecat модуль знает об анимациях. Переходим к созданию анимаций.

    Анимация ngRepeat с помощью CSS анимации переходов

    Мы начнем с добавления CSS анимации переходов для директивы ngRepeat, представленной в файле phone-list.html. Сперва добавим CSS класс phone-listing к повторяющемуся элементу.
    <!--
      Let's change the repeater HTML to include a new CSS class
      which we will later use for animations:
    -->
    <ul class="phones">
      <li ng-repeat="phone in phones | filter:query | orderBy:orderProp"
          class="thumbnail phone-listing">
        <a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
        <a href="#/phones/{{phone.id}}">{{phone.name}}</a>
        <p>{{phone.snippet}}</p>
      </li>
    </ul>

    Код анимации:
    .phone-listing.ng-enter,
    .phone-listing.ng-leave,
    .phone-listing.ng-move {
      -webkit-transition: 0.5s linear all;
      -moz-transition: 0.5s linear all;
      -o-transition: 0.5s linear all;
      transition: 0.5s linear all;
    }
    
    .phone-listing.ng-enter,
    .phone-listing.ng-move {
      opacity: 0;
      height: 0;
      overflow: hidden;
    }
    
    .phone-listing.ng-move.ng-move-active,
    .phone-listing.ng-enter.ng-enter-active {
      opacity: 1;
      height: 120px;
    }
    
    .phone-listing.ng-leave {
      opacity: 1;
      overflow: hidden;
    }
    
    .phone-listing.ng-leave.ng-leave-active {
      opacity: 0;
      height: 0;
      padding-top: 0;
      padding-bottom: 0;
    }

    Как видно в CSS-коде класс phone-listing комбинируются хуками анимации, которые позволяют перехватить и обработать события вставки и удаления элементов из списка:
    • класс ng-enter применяется к элементу, когда новый телефон добавился в список и отобразился на странице;
    • класс ng-move применяется, когда элементы перемещаются по списку;
    • класс ng-leave применяется, когда элемент удаляется из списка.
    Когда происходит анимация два множества CSS-классов цепляется к элементу:
    • класс "starting" представляет стиль в начале анимации;
    • класс "active" представляет стиль в конце анимации.
    Имя starting-класса это имя случившегося события (например, entermove или leave) с префиксом ng-. Например, в результате события enter получится класс с именем ng-enter.

    Имя active-класса такое же как имя starting-класса, только с суффиксом -active. Эти два правила наименований CSS-классов позволяют разрабатывать анимации от начала до конца.

    В примере выше элементы расширяются по высоте от 0 до 120 пикселов, когда они добавляются в список и сжимают обратно до 0, когда удаляются из списка. В то же время применяется эффект fade-in (постепенное появление) и fade-out (постепенное исчезновение). Всё это управляется CSS transition объявлениями.

    Все современные браузеры хорошо поддерживают CSS transitions и CSS animations, кроме IE9 и раних версий. Если вы хотите, чтобы анимация поддерживалась в старых браузерах, то используйте JavaScript-based анимации.

    Анимация ngView с помощью CSS анимации на основе ключевых кадров

    Давай добавим анимацию для переходов между сменой маршрута в ngView. Добавим новый класс в HTML:
    <div class="view-container">
      <div ng-view class="view-frame"></div>
    </div>

    Директива ng-view вложена в родительский элемент с CSS-классом view-container. Этот класс добавляет position: relative для того чтобы позиционировать ng-view относительно ее родителя при анимации переходов.
    .view-container {
      position: relative;
    }
    
    .view-frame.ng-enter, .view-frame.ng-leave {
      background: white;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
    }
    
    .view-frame.ng-enter {
      -webkit-animation: 0.5s fade-in;
      -moz-animation: 0.5s fade-in;
      -o-animation: 0.5s fade-in;
      animation: 0.5s fade-in;
      z-index: 100;
    }
    
    .view-frame.ng-leave {
      -webkit-animation: 0.5s fade-out;
      -moz-animation: 0.5s fade-out;
      -o-animation: 0.5s fade-out;
      animation: 0.5s fade-out;
      z-index:99;
    }
    
    @keyframes fade-in {
      from { opacity: 0; }
      to { opacity: 1; }
    }
    @-moz-keyframes fade-in {
      from { opacity: 0; }
      to { opacity: 1; }
    }
    @-webkit-keyframes fade-in {
      from { opacity: 0; }
      to { opacity: 1; }
    }
    
    @keyframes fade-out {
      from { opacity: 1; }
      to { opacity: 0; }
    }
    @-moz-keyframes fade-out {
      from { opacity: 1; }
      to { opacity: 0; }
    }
    @-webkit-keyframes fade-out {
      from { opacity: 1; }
      to { opacity: 0; }
    }
    
    /* don't forget about the vendor-prefixes! */

    Ничего сложного тут нет. Просто fade in и fade out эффекты при смене страниц. Единственная необычная вещь тут в том, что мы использовали абсолютное позиционирование для позиционирования следующей страницы (определяется как ng-enter) сверху предыдущей страницы (та которая с классом ng-leave) при выполнении cross fade анимации. В то время, когда предыдущая страница готовится к удалению она постепенно исчезает, а новая страница постепенно появляется сверху неё.

    После завршения leave-анимации элемент удаляется, а после завершения enter-анимации CSS-классы ng-enter и ng-enter-active удаляются из элемента вызывая перерисовку и перепозиционирование элемента с CSS-кодом для него по умолчанию (поэтому нет никакого абсолютного позиционирования после завершения анимации). Это всё работает плавно, поэтому нет никаких скачков при изменении маршрута в приложении.

    CSS-классы (start и end классы) применяются также как и для ng-repeat. Каждый раз при загрузке новой страницы директива ng-view делает свою копию, загружает шаблон и добавляет контент. Это гарантирует то, что все представления содержатся в единственном HTML-элементе, который можно легко анимировать и управлять анимацией.

    Дополнительная информация о CSS-анимациях.

    Анимация ngClass с помощью JavaScript

    Давайте добавим другую анимацию в наше приложение. На странице phone-detail.html у нас есть переключатель изображений. При клике на миниатюру меняется изображение профиля телефона. Давайте добавим анимацию на это действие.

    При клике на миниатюру меняется состояние изображения профиля для того чтобы установить новое изображение. Лучший способ показать смену состояний в HTML это использовать классы. Также как мы ранее использовали CSS классы чтобы указать анимацию, в этом случае мы будем запускать анимацию когда меняется CSS-класс.

    Когда выбирается новая миниатюра, состояние меняется и CSS-класс .active добавляется к соответствующем изображению профиля и запускается проигрывание анимации.

    Измененный способ отображения большого изображения:
    <!-- We're only changing the top of the file -->
    <div class="phone-images">
      <img ng-src="{{img}}"
           class="phone"
           ng-repeat="img in phone.images"
           ng-class="{active:mainImageUrl==img}">
    </div>
    
    <h1>{{phone.name}}</h1>
    
    <p>{{phone.description}}</p>
    
    <ul class="phone-thumbs">
      <li ng-repeat="img in phone.images">
        <img ng-src="{{img}}" ng-mouseenter="setImage(img)">
      </li>
    </ul>

    Мы использовали repeater для отображения всех изображений в профиле в виде списка. Но мы не анимировали ничего связанное с этим списком. В директиве ng-class у нас находится условный класс, каждый раз когда его выражение истинно он применяется к элементу и визуализируется. В противном случае изображение профиля скрывается. В нашем случае есть только один элемент с классом active, поэтому будет отображаться только одно большое изображение профиля.

    Когда класс active добавляется к элементу, добавляются классы active-add и active-add-active прямо перед сигналом AngularJS пальнуть анимацией. Когда удаляется классы active-remove и active-remove-active, прицепляются к элементу чем запускают другую анимацию.

    Чтобы убедиться в том что изображения телефонов отображаются корректно, когда страница впервые загружена мы также поправили CSS-стили страницы деталей:
    .phone-images {
      background-color: white;
      width: 450px;
      height: 450px;
      overflow: hidden;
      position: relative;
      float: left;
    }
    
    ...
    
    img.phone {
      float: left;
      margin-right: 3em;
      margin-bottom: 2em;
      background-color: white;
      padding: 2em;
      height: 400px;
      width: 400px;
      display: none;
    }
    
    img.phone:first-child {
      display: block;
      }

    Тут можно подумать, что мы собираемся создавать другую CSS-анимацию, но давай посмотрим как создать JavaScript-анимацию с помощью модульного метода animation():
    var phonecatAnimations = angular.module('phonecatAnimations', ['ngAnimate']);
    
    phonecatAnimations.animation('.phone', function() {
    
      var animateUp = function(element, className, done) {
        if(className != 'active') {
          return;
        }
        element.css({
          position: 'absolute',
          top: 500,
          left: 0,
          display: 'block'
        });
    
        jQuery(element).animate({
          top: 0
        }, done);
    
        return function(cancel) {
          if(cancel) {
            element.stop();
          }
        };
      }
    
      var animateDown = function(element, className, done) {
        if(className != 'active') {
          return;
        }
        element.css({
          position: 'absolute',
          left: 0,
          top: 0
        });
    
        jQuery(element).animate({
          top: -500
        }, done);
    
        return function(cancel) {
          if(cancel) {
            element.stop();
          }
        };
      }
    
      return {
        addClass: animateUp,
        removeClass: animateDown
      };
    });

    Тут мы использовали jQuery для реализации анимации. Необязательно использовать jQuery, просто мы не хотим сейчас писать JavaScript-библиотеку для выполнения анимаций, а в jQuery это уже есть. Для того чтобы подробнее узнать о функции jQuery.animate смотрите документацию.

    Callback-функции addClass и removeClass вызываются всякий раз, когда класс добавляется или удаляется с элемента, который содержит зарегистрированный нами класс, в данном случае .phone. Когда класс .active добавляется к элементу (через директиву ng-class) JavaScript callback addClass вызывается с передаваемым ему параметром element. Последний параметр done это callback-функция, которую надо вызвать чтобы оповестить Angular об окончании JavaScript-анимации.

    Callback-функция removeClass, работает таким же образом, но приводится в действие, когда класс удаляется с элемента.

    С помощью JavaScript callback мы создали анимацию посредством манипуляций с DOM. В коде выше это что делают element.css() и element.animate(). Callback позиционирует следующий элемент со смещением 500 pixels и анимирует оба, и старый и новый элементы смещая их вверх на 500 pixels. В результате это дает конвейер-ленточную анимацию. После того как функция animate выполняет свою задачу она вызывает done.

    И addClass и removeClass возвращают функцию. Это опциональная функция, которая вызывается, когда анимация отменяется (например, когда должна быть выполнена другая анимация над тем же элементом) или когда анимация завершает свое выполнение. Логический параметр передаваемый в эту функцию позволяет разработчику знать была ли отменена анимация. Эта функция может быть использована для любых необходимых чисток после завершения анимации.