The Round

合同会社ナイツオの開発ブログ

静岡でGo言語やりたい人!!→こちら

AngularJSのAnimationチュートリアルを見てみる

Applying Animation

こんにちわ。新米エンジニアのマツウラです。
最近英文をよく読んでいるためか英語の苦手意識が薄れた気がします。

今回はAngular1.2から新たに追加されたAnimationのチュートリアルです。
とはいえ、ほぼ公式の翻訳みたいなもんです。
英語はまだまだなので間違ってたらツッコミください^^;


出典:12 - Applying Animations

directiveにcssjavascriptからなるアニメーションを追加することができます。
angular-animate.jsにバンドルされ、アプリモジュールと依存関係とするとアニメーションが可能になります。

ngdirectiveは自動でアニメーションを割りこませて起動します。(ngRepeatへの挿入・削除、ngClassへの追加・削除など。)

Template

HTMLテンプレートで変更が必要なのは、アニメーションが定義されているassetファイルと、angular-animate.jsをリンクさせることです。
アニメーションモジュールであるngAnimateangular-animate.jsで定義されています。

Important: jQuery1.10.xを使ってください。AngularJSは2.xをサポートしてません。

アニメーションは現在(2014/03/13)CSS(animations.css)だけでなくJavascript(animations.js)内にも作れます。アニメーションを構築する前にngAnimateモジュールを依存関係として新しいモジュールを作成します。

Module & Animations

angular.module('phonacatAnimations', ['ngAnimate']).
    // 後にここでアニメーションを定義します。
...
angular.module('phonecat', [
    'ngRoute',
    'phonecatAnimations',
    'phonecatControllers',
    'phonecatFilters',
    'phonecatServices'
]).

これでphonecatモジュールがアニメーションを認識しました。
次にアニメーションを作ります。

ngRepeatをアニメーション化

phone-list.htmlngRepeatdirectiveにCSS Transitionアニメーションを追加します。
CSSアニメーションコードでdirectiveにフック出来るように、リピート要素にCSSクラスを追加します。

<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クラスを追加しました。これはアニメーション動作に必要なHTMLコードです。
実際のCSS Transitionコードは次のとおり。

.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;
}

phone-listingクラスはリストに挿入、削除したときに起こるアニメーションのフックと一体化してます。

  • ng-enter新しい携帯がリストに追加され、ページ上に表示された時に適用
  • ng-move項目がリスト内を移動している時に適用
  • ng-leaveリストから削除された時に適用

phone-listingはng-repeat属性に渡されたデータに応じて追加・削除されます。
例えば、フィルタを変更するとリストがアニメーションします。

重要な点は、アニメーション実行の際、要素に2組のCSSクラスが追加されることです。

  1. アニメーション開始を表す"starting"クラス
  2. アニメーション終了を表す"active"クラス

"starting"クラスの名前は発生したイベント名(enter,move,leaveのような)にng-の接頭辞が付いたものです。つまりenterイベントならng-enterということです。

"active"クラス名も"starting"クラスと同じですが、-activeの接尾辞が付いています。
この2つのクラスの命名規則に従うことで開発者はアニメーションを作ることができます。

上記の例では、要素のheightが0~120pxに展開します。
同時にフェードイン、フェードアウトが発生します。
これら全ては上記サンプルCSSの最上部、CSS transition宣言によって処理されます。

多くのモダンブラウザはCSS TransitionsCSS Animationsをサポートしていますが、IE9とそれ以前は非サポートです。
古いブラウザとの下位互換性を維持したい場合は、以下に記載されているJavascriptベースのアニメーションの使用を検討してください。

CSS Keyframe AnimationsでngViewをアニメーション化

次はngViewでルート変更をする間にtransitionsアニメーションを追加します。

はじめにng-view directiveを含む要素にCSSクラスを追加します。
Viewを変更する際のアニメーションをより細かく制御するため、HTMLに若干の変更を加えます。

<div class="view-container">
  <div ng-view class="view-frame"></div>
</div>

ng-viewはview-containerクラスを基準としてアニメーションするように変更します。
view-containerにはposition: relativeクラスを追加します。(以下のcss参照)

ということで、animations.cssにtransitionアニメーションのための記述を追加します。

.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! */

このCSSで特別なことは何もしてません。
ページ間のフェードインとフェードアウト効果だけです。

ここで少々特殊な記述なのは、クロスフェードアニメーションを実行している間に、前ページの上に次ページを配置するためposition:absoluteを使っているabsoluteを使っている部分だけです。
(前ページはng-leaveを持つ要素。次ページはng-enterを介して特定する。)

前ページが削除される時、新しいページが上に重なるようにしてフェードインします。

一度leaveアニメーションが終了すると要素は削除されます。
一度enterアニメーションが完了すると、その後、元のCSSに書かれたポジションと同じならng-enterng-enter-activeクラスは要素から削除されます。

ルートを変更している間にも流れるようにアニメーションは動作します。

ng-repeat同様にCSSクラスが適用されます。(startとendクラス)
新しいページが読み込まれるたび、ng-view directiveは自身のコピーを作成してテンプレートをダウンロードしてコンテンツを追加します。
これは全てのviewが単一のHTMLに含まれることを保証し、アニメーションの制御を簡単にします。

CSSアニメーションの詳細についてはWeb Platform documentationを参照してください。

JavascriptでngClassのアニメーション化

アプリに別のアニメーションを追加します。
phone-list.htmlページに切り替えると、サムネイルがあります。
サムネイルをクリックすると携帯画像が変わります。
このあたりにアニメーションを追加するにはどうすれば良いでしょうか?

基本的に、サムネイルをクリックすると新しく選択されたサムネイルを反映させるためプロファイル画像の状態を変更します。
HTML内の状態を変更する最良方法はCSSクラスを使うことです。
先ほどのCSSを使ったアニメーション指定ではなく、今回はCSSクラス自体が変更されるたびアニメーションが発生します。

新しい携帯サムネイルが選択されるたび状態が変化し、.activeクラスが一致するプロファイル画像とアニメーションの再生に追加されます。

はじめにphone-detail.htmlを微調整します。

<!-- 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>

サムネイルも同様に、リストとして全てのプロファイル画像を表示するためにng-repeatを使っています。
しかしng-repeat関連のアニメーション化はしていません。
代わりにng-class directiveでactiveクラスがtrueであれば、要素に適用され表示されます。
その他のプロファイル画像は非表示です。
今回はactiveクラスを持つ要素が常に1つ存在します。そのため常に画面上には1つプロファイル画像が表示されています。

activeクラスが要素に追加されたとき、active-addactive-add-activeクラスがアニメーションの終了をAngularJSに通知する直前に追加されます。
削除されると、active-removeactive-remove-activeクラスが次々と他のアニメーショントリガの要素に適用されます。

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は必須ではありませんが、独自のアニメーションライブラリを作成することはチュートリアルの範疇外なので使用しています。
jQueryアニメーションの詳細はjQuery documentaionを参照してください。

addClassremoveClassのコールバック関数は、登録したクラスが含まれる要素でクラスが追加・削除された際に呼ばれます。今回なら.phoneクラスです。
要素に.activeクラスが追加されると(ng-class directiveを介して)、addClassコールバックはパラメータとして渡されたelementで呼び出されます。
最後のパラメータに渡されたのはdoneコールバック関数です。
Javascriptアニメーションが終了したことを、doneが呼ばれることでAngularJSは知ることができます。

removeClassコールバック関数も同様に動作しますが、クラスが要素から削除された時にトリガされます。

Javascriptコールバックでは、DOM操作によりアニメーションを作成します。
上記のコードでは、element.css()element.animate()が行っています。
コールバックは各項目を500px上昇させることで、前の項目と新しい項目の両方をアニメーションさせて次の要素を配置しています。
この結果、ベルトコンベアーのようなアニメーションになります。
animate関数が役目を終えた後、doneが呼ばれます。

addClassremoveClassの各関数の戻り値に注意してください。
これはオプション関数で、アニメーションが完了した時だけでなくキャンセルされた時(他のアニメーションが同じ要素で起きた時)にも呼ばれます。
bool型のパラメータが関数に渡されるので、アニメーションがキャンセルされたか否か開発者が知ることができます。
この関数はアニメーションが終了した際に必要なクリーンアップを行うことができます。


以上、追加されたAnimationチュートリアルでした。