WEB

AngularJSでマテリアルデザイン風のページ遷移アニメーションを作ろう

AngularJSでマテリアルデザイン風のページ遷移アニメーションを作ろう

こんにちは。
6月から役職が変わりフロントエンドリーダーからCTOになりました、先生です。

もうAngular1.4にアップデートしましたか? Angular1.4になりマテリアルデザインを意識したアニメーション関連の変更や実装がおこなわれました。その分Breaking Changesもあるのでアップデートには注意する必要があります。

今日はそんなAngularJS 1.4を使って、マテリアルデザイン風にページ遷移するアニメーションを作ってみたいと思います。

▼目次

今回のデモ

デモは下記リンクになります。
各メンバーをクリックするとスムーズにアニメーションして詳細ページに遷移します。まずは触ってみてください。
http://frontainer.com/ligblog-sample/angular-morphing-animation/#/

必要なもの

Angular Routerでも機能しますが、今回はよく使われているUI Routerを使用します。
また、Materializeは見た目をマテリアルデザイン風にしてくれるフレームワークで、今回はスタイルだけを使用します。

Animation Anchoring

Angular Animate1.4でAnimation Anchoringという機能が追加されました。
これを使うことで、遷移前と後の同じ要素をアニメーションさせながら画面を切り替えることができます。
使い方は簡単で、紐付けたい要素に同じ値のng-animate-ref属性を付け、CSSでtransitionを設定してあげるだけです。

<!-- ページA -->
<span class="page-a dummy" ng-animate-ref="hoge">dummy text</span>

<!-- ページB -->
<span class="page-b dummy" ng-animate-ref="hoge">dummy text</span>

<!-- css -->
}
.dummy-anchor-in {
    transition: 0.5s ease-out all;
}

Angular記法で変数を指定してもOKです。

<span class="page-a dummy" ng-animate-ref="{{myValue}}">dummy text</span>

ng-animate-refに関しては、下記の公式ページに記載があります。
https://docs.angularjs.org/api/ngAnimate#animation-anchoring-via-ng-animate-ref-

デモと同じものを作ってみよう

デモのコードはGithubで公開していますので、そちらもご覧ください。
https://github.com/frontainer/ligblog-sample/tree/master/angular-morphing-animation

構成

構成は下図のとおり。

index.html  
home.html  
profile.html
┣ css/  
┃ ┗ style.css  
┣ js/  
┃ ┗ app.js  
┗ images/  
    ┗ *.jpg

index.html

はじめに、index.htmlを書きましょう。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.96.1/css/materialize.min.css"/>
        <link rel="stylesheet" href="css/style.css">
    </head>
    <body ng-app="app">
        <nav>
            <div class="nav-wrapper">
                <a ui-sref="/" class="brand-logo">Sample</a>
            </div>
        </nav>
        <div class="view-container">
          <div ui-view class="view"></div>
        </div>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.0/angular.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.0/angular-animate.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>
        <script src="js/app.js"></script>
    </body>

</html>

おそらく説明不要なほどシンプルなものだと思います。
必要なangular,angular-animte,ui-routerと、これから作るapp.jsを読み込んでおきます。

app.js

つづいて、app.jsを書いていきましょう。

(function (angular) {
    // メンバー情報を配列にいれておく
    var MEMBERS = [
        {
            id: 1,
            name: "おじいちゃん",
            description: "フロントエンドエンジニアのおじいちゃんと言います。本当は24歳です。よろしくお願いします。"
        },
        {
            id: 2,
            name: "はやち",
            description: "フロントエンドエンジニアのはやちです( ˘ω˘)☝以前までは顔隠しておりましたが思い切ることにしました…。相変わらず顔文字乱舞ですがブログもコーディングも楽しくやっていこうと思います✌(´ʘ‿ʘ`)✌Androidの方は相変わらず文字化けすいません(◞‸◟)"
        },
        {
            id: 3,
            name: "先生",
            description: "フロントエンドエンジニアの林です。業務効率化するアプリが大好き。AngularJS好きのJavaScripter。ディレクション・サーバーサイド開発・デザインにもぐいぐい入り込んでいきます。あとテニスが好きです。"
        },
        {
            id: 4,
            name: "せいと",
            description: "最近フロントエンドエンジニアになりました。第一回HTML5カルタ大会で優勝しました。休日の過ごし方は、\"Jazz Barでスコッチを片手に『世界の終りとハードボイルド・ワンダーランド』を読む\"です。"
        },
        {
            id: 5,
            name: "まろC",
            description: "フロントエンドエンジニアのまろCです。最近はAWSもやってCMSも構築して、手タレもやっています。"
        },
        {
            id: 6,
            name: "いなば",
            description: "フロントエンドエンジニアの稲葉です。Web制作→ソーシャルゲーム開発を経てまたWeb制作に戻ってきました。趣味はランニングと一眼レフです。TRIPに続くWebサービスの立ち上げに参加する事と東京マラソン出場&完走が密かな目標です。"
        },
        {
            id: 7,
            name: "店長",
            description: "ロントエンドエンジニアの店長です。LIGに入社と同時に店長(あだ名が)になりました。偉くはありません。以前、某カフェで働いていました。音楽とコーヒーが大好きです。よろしくお願いいたします。"
        }
    ];
    // ngAnimateとui.routerモジュールを使う
    angular.module('app', ['ngAnimate', 'ui.router'])
        .config(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) {
            // 以下ルーティングの設定
            $urlRouterProvider.otherwise("/");
            $stateProvider
                .state('/', {
                    url: "/",
                    controller: 'HomeController as home',
                    templateUrl: "home.html"
                }).state('profile', {
                    url: '/profile/:id',
                    templateUrl: 'profile.html',
                    controller: 'ProfileController as profile'
                });
        }])
        // 最初のページ
        .controller('HomeController', [function () {
            this.members = MEMBERS;
        }])
        // プロフィールページ
        .controller('ProfileController', ['$stateParams', function ($stateParams) {
            var index = parseInt($stateParams.id, 10);
            this.member = MEMBERS[index - 1];
        }]);

})(angular);

MEMBERSという変数にメンバーの情報を格納しています。
configでui-routerの設定をしています。ui-routerの使い方は公式のドキュメントをご覧ください。

https://github.com/angular-ui/ui-router

コントローラーはあまり処理をおこなっていません。
ProfileControllerではパラメータでもらったID番号を元に、メンバーのデータ1件を取得しているだけです。

home.html

つづいて最初のページ(home.html)を作ります。

<ul class="collection">
    <li class="collection-item avatar" ng-repeat="member in home.members" ui-sref="profile({id:member.id})">
        <!-- サムネイル -->
        <img ng-src="./images/{{member.id}}.jpg" alt="" class="circle thumb"
             ng-animate-ref="thumb-{{ member.id }}" width="60" height="60">
        <!-- 名前 -->
        <div class="name" ng-animate-ref="title-{{ member.id }}">{{member.name}}</div>
        <!-- 影付きの枠をアニメーションさせるための空要素 -->
        <div class="z-depth-1" ng-animate-ref="shadow-{{member.id}}"></div>
    </li>
</ul>

ng-repeatで繰り返しをするので、ng-animate-refにもIDを付けて一意の値にします。
ng-animate-ref=”thumb-{{ member.id }} この部分が重要です。
今回はIDを連番にしているのでthumb-1,thumb-2と出力されていきます。

profile.html

さらにプロフィールページ(profile.html)を作ります。

<div class="row profile-container">
    <div class="col s8 offset-s2">
        <!-- 影付きの枠 -->
        <div class="z-depth-1 profile shadow" ng-animate-ref="shadow-{{profile.member.id}}">
            <div>
                <!-- サムネイル -->
                <img ng-src="./images/{{profile.member.id}}.jpg" alt="" class="profile circle thumb"
                     ng-animate-ref="thumb-{{ profile.member.id }}" width="80" height="80">
                <!-- 名前 -->
                <div class="profile name" ng-animate-ref="title-{{ profile.member.id }}">{{ profile.member.name }}</div>
            </div>
            <!-- 説明文 -->
            <div>
                {{profile.member.description}}
            </div>
        </div>
        <!-- 戻るボタン -->
        <a ui-sref="/" class="waves-effect waves-light btn"><i class="mdi-navigation-arrow-back left"></i>戻る</a>
    </div>
</div>

プロフィールページではmemberにクリックされたメンバーの情報が格納されています。こちらのng-animate-refは、home.htmlについているのと同じ値になるようにしておきます。

<img ng-animate-ref="thumb-{{ profile.member.id }}"

上記のようにしておけば、thumb-1やthumb-2などとなります。

CSSとアニメーションの設定

最後にCSSを作りアニメーションの設定をおこないます。

.view-container {
    position: relative;
}
.view.ng-animate {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    min-height: 500px;
}
.view.ng-enter, .view.ng-leave {
    transition: 0.5s cubic-bezier(.55,0,.1,1) all;
}
.view.ng-enter {
    opacity: 0;
}
.view.ng-enter.ng-enter-active, .view.ng-leave {
    opacity: 1;
}
.view.ng-leave.ng-leave-active {
    opacity: 0;
}

.name.ng-anchor-in, .thumb.ng-anchor-in,.shadow.ng-anchor-in {
    transition: 0.5s cubic-bezier(.55,0,.1,1) all;
}
.profile.shadow {
    padding: 20px;
}
.profile.name {
    font-size: 24px;
    font-weight: bold;
}
.profile-container {
    padding: 20px;
    margin: 20px;
}

.view関連はページ遷移をフェードで切り替えるためのものです。
ng-anchor-inというのが記述に追加されています。ここにtransitionの設定を記述します。

1.3で同様のアニメーションを実装する方法

こうして作ったのが先ほどのデモになります。
このように遷移前と後のng-animate-refにつけた名称と一致するものが、アニメーションして切り替わるようになります。複雑そうに見えるアニメーションが簡単に実装できてしまいました。

http://frontainer.com/ligblog-sample/angular-morphing-animation/#/

諸事情により1.4に上げるのが難しい場合は「Angular-Hero」というモジュールがあります。これを使うと1.3でも同様のアニメーションを実装することができます。
Angular-Heroは1.4だとページ遷移のアニメーションが動作しなかったため、修正したものをプルリクエストしマージされています。そのため、1.3で作った後に1.4にあげても変わらず動作するようになりました。(バージョン0.0.7)

ハマりどころ

app.jsを非同期読み込みにするためにasync属性を付けてangular.bootstrapで初期化していると、ng-animate-refによるアニメーションが正しく動作しませんでした。
JSエラーが出て動作しないなぁと思ったら下記を確認してみてください。(これは本当にわからなかった)

うまく動作しなかった例
HTML

<script src="app.js" async></script>

app.js

angular.bootstrap(document, ["app"], {
    strictDi: true
});

最後に

Angular Animate1.4はBreaking Changesがあるので、1.3でアニメーションをガンガン使っている場合にはアップデートに注意が必要です。
しかし、1.4はパフォーマンスの改善だけでなくアニメーションについても強力で魅力的なものになっているので、制限がなければバージョンアップしてみてはいかがでしょうか。

 

【関連リンク】

この記事を書いた人

先生
先生 最高技術責任者 2014年入社
CTOの林です。フロントエンドを専門とし、AngularJSのコミュニティをはじめ、様々な勉強会に顔を出しています。効率化マニアでGrunt,Gulpをはじめ、プロジェクト進行やサーバーサイド、インフラ周りの効率化を目指し日々活動しています。