Master PG

プログラムし続ける...

【Polymer Starter Kit】プロジェクトを作成してみよう


目次


概要

Polymerでアプリケーションを作成する土台として「Polymer Starter Kit」が提供されています。この中にはプロジェクトを運用する上で必要なものが多く含まれていますが、最初から全てが詰まっていると「ここはどことつながりがあるんだろう?」という状態になってしまいます (自分は...) 。そこで今回は本家のStarter Kitから「画面を動かす」部分を抽出して、自分なりに再構成したプロジェクトをgithubにアップしました。

⇒ github

f:id:masterpg:20160923114458p:plain


環境設定

ここから本サイトで独自に作成したStarter Kitのリポジトリを自身の環境へクローンしてください。

クローンを作成したら次のフォルダへ移動します:

cd /【任意のフォルダ】/polymer-starter-kits/projects/basic

次にbowerで依存ライブラリをダウンロードします。

bower update
PythonのWEBサーバーを利用する

Pythonが動作する環境では標準ライブラリにWEBサーバーがあり、これを利用することで簡単にStarter Kitを起動させることができます。polymer-starter-kits/projects/basicへ移動し以下のコマンドを実行するとWEBサーバーが起動します (Python2と3では起動方法が異なります) :

Python2

python -m SimpleHTTPServer 8080

Python3

python3 -m http.server 8080

ブラウザで次のURLにアクセスするとStarter Kitが起動します:

http://localhost:8080/
自身のWEBサーバーを利用する

自身の環境にWEBサーバーがある場合は、basicフォルダをWEBサーバーに配置します。

次に「ページ遷移 - 基本設定」の指示に従って、「index.html」と「app.js」に基準パスを設定してください。

WEBサーバーを起動して、ブラウザで次のようなURLにアクセスすると、Starter Kitが起動します (URLは自身の環境に読み替えてください) 。

http://myserver.com/apps/basic/index.html


外部HTMLファイルのインポート

Sterter KItで使用する外部HTMLファイル (カスタムエレメント、共有スタイルなど) やJavaScriptファイル (ベンダーライブラリとapp.js) は「app.html」というファイルの中で全てインポートするようにしています。

【src/app.html】

<!-- Iron elements -->
<link rel="import" href="../bower_components/iron-flex-layout/classes/iron-flex-layout.html">
<link rel="import" href="../bower_components/iron-icons/iron-icons.html">
...

<!-- Paper elements -->
<link rel="import" href="../bower_components/paper-drawer-panel/paper-drawer-panel.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
...

<!-- Vendor Libraries -->
<script src="../bower_components/page/page.js"></script>
<script src="../libs/page/query.js"></script>

<!-- Custom elements -->
<link rel="import" href="../styles/app-theme.html">
<link rel="import" href="../styles/shared-styles.html">
<link rel="import" href="elements/home-view/home-view.html">
...

<!-- Application -->
<script src="app.js"></script>

app.htmlはindex.htmlの中でインポートしています。

【index.html】

<html lang="">
<head>
  ...
  <link rel="import" href="src/app.html">
</head>
...


アプリケーションのメインビュー

ブラウザの画面全体を覆うアプリケーションビューとして<app-view>というカスタムエレメントを作成しました。このビューには「左メニューエリア」と「メインエリア」に分割されています。

f:id:masterpg:20160923114540p:plain

メインエリアの中には次のビューが配置され、左メニューエリアのメニューが選択されると対応するビューがメインエリアに表示されます (ユーザー情報ビューはユーザーリストのユーザーを選択すると表示されます) :

  • ホームビュー (<home-view> : home-view/home-view.html)
  • ユーザーリストビュー (<users-view> : users-view/users-view.html)
  • ユーザー情報ビュー (<user-info-view> : user-info-view/user-info-view.html)
  • コンタクトビュー (<contact-view> : contact-view/contact-view.html)


<app-view>は画面とスクリプトが分離しています (画面とスクリプトの分離の詳細はここを参照) :

【src/elements/app-view.html】

...
<dom-module id="app-view">
  ...
  <!-- このビューのスクリプトを読み込み -->
  <script src="app-view.js"></script>
</dom-module>


<app-view>はindex.htmlで使用されています:

【index.html】

<html lang="">
...
<body unresolved class="fullbleed layout vertical">
  ...
  <app-view id="app"></app-view>
  ...
</body>
...


ページ遷移

本家のStarter Kitはページ遷移にpage.jsを使用しています。page.jsはURLに移動先のパスやクエリストリングを付加してページ遷移を行います。また、HTML5のpushState()を使用しており、内部で画面遷移の履歴をとっているので、ブラウザの進む/戻るで画面を進めたり、戻したりできます。


基本設定

では順を追ってpage.jsの利用方法を見ていきましょう。page.jsはページ遷移でURLにパスを付加しますが、「基準パス」が指定されていないとURLが意図しないものに変わってしまうことがあります。次のコードでは基準パスを設定しています。基準パスには、WEBサーバーのドキュメントルートからみた場合のindex.htmlのパスを指定します。

Note: 基準パスの初期値は「/」となっており、これはドキュメントルート直下にStarter Kitが配置されることを想定しています。

【index.html】

<html lang="">
<head>
  ...
  <!--
    補足: <base>タグで「/」以外のパスを指定するとvulcanizeする際にエラーが発生する。
          このためスクリプトで<base>タグをwrite()している。
          ※vulcanize: リリース時にパッケージ化を行うツール。今回これを使用することはない。
  -->
  <script>
    // 基準パスを設定。パスの最後には「/」が必要。これがないとパスが一段上になってしまう。
    document.write('<base href="/apps/starter-kit/" />');
  </script>


ページ遷移のルールは「src/app.js」ファイルの中にまとめられています。

【src/app.js】

var App = {
  ...
  /**
   * 画面遷移のルーティングを設定します。
   */
  setupRouting: function () {
    // 基準パスを指定。指定する値は、基準パスの最後尾から「/」を削除したものを指定。
    page.base('/apps/starter-kit');
    ...

page.base()の指定ですが、これを指定しないとページ遷移で不具合を起こす場合があります。page.jsのサイトのサンプルでも、page.base()の指定と、先ほど説明したindex.htmlでの基準パスの設定が行われているので、この2つの設定は行うようにしてください。

Note: page.base()の初期値は空文字となっていますが、これはindex.htmlで設定されていた基準パスの初期値「/」を削除した結果であり、意図した空文字です。

補足

Note: 「src/app.html」で読み込んでいる「query.js」ですが、本家のStarter KitにはURLからクエリストリングを取り出す機構がなかったので、これを実現するためにpage.jsのサイトからこのファイルをコピーして「libs/page」フォルダに配置しました。


基本的なページ遷移

次にどのようにページ遷移が行われるか見ていきましょう。再度app.jsの中をみてみます。

【src/app.js】

// スクロール位置をトップへ移動させる
function scrollToTop(ctx, next) {
  App.view.scrollPageToTop();
  next();
}
...

// ルートパスからホームビューへリダイレクト
page.redirect('/', '/home');

// ホームビューへのルーティング
page('/home', scrollToTop, function (ctx) {
  App.view.route = 'home';
});

まず「ホームビューへのルーティング」ではpage()関数が使用されています:

  • page(path, callback[, callback ...])

pathにはルーティング先のパスを指定し、callbackにはその際に実行される関数を指定します。callbackに指定する関数にはctxnextという引数が渡されます。ctxはルーティング情報が格納されたオブジェクトです。nextcallbackが複数指定された際に次のコールバックへ移動させるための関数です。

では上記コードの「ルートパスが指定されてからホームビューへリダイレクト」される流れを見てみましょう:

  1. ルートパスへの遷移が要求されると、page.redirect('/', '/home');によって/homeのパスへリダイレクトされる。
  2. 次に「ホームビューへのルーティング」で指定されているscrollToTop()関数が実行される (遷移前のビューでスクロールが下に移動されていた場合の対応) 。
  3. scrollToTop()関数の最終行でnext()が呼び出されると「ホームビューへのルーティング」で指定された次のコールバックが実行され、ホームビューが表示される。

これでホームビューが表示されますが、「ホームビューへのルーティング」の最後のコールバックで指定されているApp.view.route = 'home'はどのような意味なのでしょうか。これについて見ていきましょう。

まずApp.viewは、ブラウザの画面全体を覆う<app-view>というアプリケーションビューが格納されるプロパティです。App.viewの設定は次で行われています:

【src/app.js】

var App = {
  /**
   * アプリケーションビューです。
   */
  view: undefined,
  ...
}

(function (document) {
  ...
  // アプリケーションビューを保存
  App.view = document.querySelector('#app');
  ...
})(document);

<app-view>にはrouteというプロパティが定義されており、このプロパティには表示したいビューの名前 ("home", "users", "user-info", "contact") を文字列で指定します:

【src/elements/app-view.js】

Polymer({
  is: 'app-view',
  properties: {
    route: {
      type: String
    },
  ...

routeプロパティは、ページ切り替えを行う<iron-pages>というエレメントのselectedプロパティとバインディングしています:

【src/elements/app-view.html】

<iron-pages attr-for-selected="data-route" selected="{{route}}">
  <!-- Home(ホームビュー) -->
  <section data-route="home">
    <home-view></home-view>
  </section>
  <!-- Users(ユーザーリストビュー) -->
  <section data-route="users">
    <users-view></users-view>
  </section>
  <!-- User Info(ユーザー情報ビュー)  -->
  <section data-route="user-info">
    <user-info-view></user-info-view>
  </section>
  <!-- Contact(コンタクトビュー) -->
  <section data-route="contact">
    <contact-view></contact-view>
  </section>
</iron-pages>

<iron-pages>にはattr-for-selectedというプロパティがあり、ここでは"data-route"と指定しています。この指定された値は子エレメントである<section>data-route属性を指しています。これによりrouteプロパティの値が変更されると値が伝播し、対応する<section>が表示されることになります。


遷移先ページにデータを渡すには

遷移先のページにデータを渡したい場合には、クエリストリングを付加します。ではこの方法を見ていきましょう。

「ユーザーリストビュー」ではクエリストリングを付加してページ遷移を行っています。

画面上のユーザーリンクをクリックすると次のイベントハンドラが実行されます:

f:id:masterpg:20160923114623p:plain

【src/elements/users-view/users-view.html】

_userOnTap: function (event) {
  var model = event.model;
  var user = model.item;
  // 例: "/users/0001?first=Bob&last=Smith"
  page.show('/users/' + user.id + '?first=' + user.first + '&last=' + user.last);
  event.preventDefault();
}

パスにクエリストリングを付加した文字列をpage.show()の引数に渡してページ遷移を行っています。

Note: eventからアプリケーション固有のデータを取り出していますが、この詳細については「テンプレートリピーターのイベントハンドリング」を参照ください。

次は上記のユーザー情報ビューへのページ遷移要求に対するルーティングの定義です:

【src/app.js】

// スクロール位置をトップへ移動させる
function scrollToTop(ctx, next) {
  App.view.scrollPageToTop();
  next();
}

// 全てのルーティングの前に行われる処理を記述
page('*', function parse(ctx, next) {
  // クエリストリングをJSONへパース
  ctx.query = qs.parse(ctx.querystring);
  next();
});

...

// ユーザー情報ビューへのルーティング
// パス例: "/users/0001?first=Bob&last=Smith"
page('/users/:id', scrollToTop, function (ctx) {
  App.view.userInfoView.user = {
    id: ctx.params.id,
    first: ctx.query.first,
    last: ctx.query.last
  };
  App.view.route = 'user-info';
});

ページ遷移要求として"/users/0001?first=Bob&last=Smith"というパスが指定された場合を想定してみます。

まず呼び出されるのは、ワイルドカード*とマッピングされたコールバックです。page()のパスに*が指定されると全てのルーティングの前にこのコールバックが実行されます。上記コードでは全てのルーティングの前にURLからクエリストリングを取り出してctx.queryに設定しています。これにより、各ルーティングのコールバックではctx.queryを参照してクエリストリングを取得することができます。

次に'/users/:id'とマッピングされたコールバックが呼びだされます。まずscrollToTopが実行され、画面のスクロール位置を先頭に戻します。次に本体のコールバックが実行されます。ctx.params.idは、パス"/users/:id"で指定された:idの値、つまり0001が取得されます。ctx.queryにはクエリストリングが格納されており、ctx.query.firstでは"Bob"が、ctx.query.lastでは"Smith"が取得されます。

取得された値はオブジェクトに固められ、userInfoViewuserプロパティに設定されます。

ちなみにこのuserInfoView (ユーザー情報ビュー:<user-info-view>) はアプリケーションビューでプロパティとして公開されています。

【src/elements/app-view.js】

Polymer({
  is: 'app-view',
  properties: {
    ...
    // ユーザー情報ビュー(<user-info-view>)
    userInfoView: {
      type: Object
    }
  },

最後にApp.view.routeにユーザー情報ビューの名前"user-info"を指定することで、ユーザー情報ビューが画面に表示されることになります。


<a>タグでページ遷移する

ここまではプログラムの中でpage()を使用してページ遷移を行ってきましたが、<a>タグを使用してページ遷移することもできます。ユーザー情報ビューにある「Return to Users」というリンクは、<a>タグを使用してページ遷移を行っています:

f:id:masterpg:20160923114710p:plain

<a>タグでページ遷移をするには、href属性にパスを指定します。ページ遷移先のパスは基準パスから指定する必要があります:

【src/elements/user-info-view/user-info-view.html】

<!-- ページ遷移先のパスは基準パスから指定する必要がある -->
<a href="../../../users">Return to Users</a>


hashbang有効/無効の切り替え

本サイトのStarter KitではURLクエリでhashbangの有効/無効を切り替えれるようにしました。URLクエリでhashbang=trueのようにtruefalseを指定することでhashbangを切り替えることができます:

http://myserver.com/apps/starter-kit/index.html?hashbang=true

URLに指定したhashbangの指定はlocalStorageでローカルに保存されるため、次回URLにhashbangの指定をしなくても前回の指定が適用されます。

次はhashbangの切り替えを行っているコードです:

【src/app.js】

setupRouting: function () {
  ...

  // URLクエリストリングによってhashbangの有効/無効を切り替える。
  // URLに切り替え用のクエリが指定されると、まずこの指定が優先され、
  // URLに指定がない場合は、前回ローカルに保存された指定が適用される。
  // 例: http://myserver.com/apps/starter-kit/index.html?hashbang=true
  var queryString = qs.parse(window.location.search.substring(1));
  var hashbangString = queryString['hashbang'];
  if (!hashbangString) {
    hashbangString = window.localStorage['hashbang'];
  }
  var hashbang = hashbangString == 'false' ? false : true;
  window.localStorage['hashbang'] = hashbang;

  // page.jsを開始
  page.start({
    hashbang: hashbang
  });
}


hashbang有効時の動作

hashbangを有効にすると、URL後部に#!が付加され、続いてページを示すパスが付加されます:

http://myserver.com/starter-kit/#!/home

このURLは左メニューエリアで「Home」を選択すると生成されます。この状態でブラウザのリロードを行ってください。場合によっては画面が表示されません。

このような現象が起きる理由ですが、上記のURLの#以下はサーバーに送られず、次のURLがサーバーに送られます:

http://myserver.com/starter-kit/

このURLにはファイルが指定されていないため、サーバーがこのURLを受け取った際にデフォルトファイルを返す設定をしていないと画面が表示されません。

このようなリロードの問題に対応するには、サーバー側でhttp://myserver.com/starter-kit/がリクエストされたとき、デフォルトファイルとして「index.html」を返すように設定してください。これによりクライアント側ではpage.jsが次のような動作を行うためリロードに対応することができます:

  1. ブラウザでhttp://myserver.com/starter-kit/#!/homeをリクエストする (#以下は送られない) 。
  2. サーバーはhttp://myserver.com/starter-kit/を受信し、デフォルトファイルである「index.html」を返す。
  3. ブラウザは「index.html」を表示する。
  4. page.jsはURLから遷移先ページのパス/homeを抽出し、このページへ遷移させる。
  5. ホームビューが表示される。


hashbangを使用する理由

hashbangを使用する理由は「サーバー側との連携が楽」からだと思います。確かにサーバーに基準パスがリクエストされた場合にデフォルトファイルを返す必要がありますが、それ以外サーバー側に設定をお願いすることはありません。

ではhashbangを使用しない場合を考えてみましょう。hashbangを使用しないとURLは次のようになります。

hashbang使用時:

http://myserver.com/starter-kit/#!/home

hashbang未使用時:

http://myserver.com/starter-kit/home

例えば、hashbang未使用時に、左メニューエリアで「Home」を選択します。この状態でブラウザのリロードを行ってください。するとhomeが含まれた状態のURLhttp://myserver.com/starter-kit/homeがリクエストされます。この場合、サーバー側で「index.html」を返してくれれば、クライアント側ではhashbang使用時と同じようにpage.jsがホームビューを表示してくれます。

次の場合は「index.html」を返すのが正しい挙動です:

  • /starter-kit/home
  • /starter-kit/users
  • /starter-kit/users/0001?first=Bob&last=Smith
  • /starter-kit/contact

ただし次の場合に「index.html」を返すのはまったく筋違いです:

  • /starter-kit/bower_components/page/page.js

このようにhashbang未使用時は、リクエストされるURLによってサーバーは「index.html」を返すのか、それともURL通りの処理を実行するかを判断しなくてはなりません。このような理由からhashbangを使用した方が「サーバー側との連携が楽」ということが言えると思います。


hashbangの使用が向かないケース

常にhashbangを使用すればよいかといえばそうではありません。URLに#!が含まれる場合は指定された対策を行わないとGoogleなどのサーチエンジンがクロールしてくれません。つまり検索エンジンにクロールしてもらって、検索結果の上位に表示されることを望むサイトでは、hashbangを使用する場合に対策を行う必要があります:

ただし、将来的にこの対策方法が効かなくなる可能性があります:

このような状況をみると、検索エンジンにクロールしてもらうことが必要なサイトではhashbangを使用しないで、SEO対策を行う方が良いのではないかと思います。


スタイリング

アプリケーション共通のスタイリング

フォント、背景色、枠線など、アプリケーション全体で共通で利用するスタイルは「app-theme.html」に定義しています。ここではスタイル定義とその利用の一例を示します。

Note: アプリケーションで共通に利用されるスタイルの詳細は「メインドキュメントでのスタイリング」を参照ください。

まずアプリケーション全体で利用するスタイルを定義するには次のように<style is="custom-style">:rootセレクタ使用し、この中にカスタムCSSプロパティを定義します (カスタムCSSプロパティの詳細はここを参照):

【styles/app-theme.html】

<style is="custom-style">
  :root {
    ...
    --secondary-text-color: #727272;

    ...
    --drawer-menu-color: #ffffff;
    --drawer-toolbar-border-color: 1px solid rgba(0, 0, 0, 0.22);
  }
</style>

「app-theme.html」は「app.html」の中でインポートされており、これによってどこからでも定義したカスタムCSSプロパティを利用することができます。

次にカスタムCSSプロパティを使用する側を見てみます。カスタムCSSプロパティの値を取得するにはvar()関数を使用します:

【src/elements/app-view.html】

<dom-module id="app-view">
  <template>
    <style include="shared-styles">
      ...
      #drawerToolbar {
        color: var(--secondary-text-color);
        background-color: var(--drawer-menu-color);
        border-bottom: var(--drawer-toolbar-border-color);
      }

赤枠がスタイルが適用された結果です:

f:id:masterpg:20160923114754p:plain


カスタムエレメントで共通的に利用するスタイル

カスタムエレメントで共通的に利用されるスタイルは「shared-styles.html」で定義しています。ここではスタイル定義とその利用の一例を示します。

Note: カスタムエレメントで共通的に利用されるスタイルを共有する方法についての詳細は「共有スタイルと外部スタイルシートを参照ください)

まずカスタムエレメントで共通的に利用されるスタイルを定義するには、次のように<dom-module>idでスタイルモージュールの名前を付けて定義します:

【styles/shared-styles.html】

<dom-module id="shared-styles">
  <template>
    <style>
      ...
      paper-material {
        border-radius: 2px;
        height: 100%;
        padding: 16px 0 16px 0;
        width: calc(98.66% - 16px);
        margin: 16px auto;
        background: white;
      }
      ...
    </style>
  </template>
</dom-module>

「shared-styles.html」は「app.html」の中でインポートされており、これによってどのカスタムエレメントからでも定義したスタイルモジュールを利用することができます。

次にスタイルモジュールを使用する側を見てみます。スタイルモジュールを使用するには、<style>タグのincludeでインクルードするスタイルモジュールを指定します:

【src/elements/contact-view/contact-view.html】

<dom-module id="contact-view">
  <template>

    <!-- スタイルモジュールをインクルード -->
    <style include="shared-styles">
      ...
    </style>

    <!-- このエレメントにスタイルモジュールで定義したスタイルが適用される -->
    <paper-material elevation="1">
      <h2 class="page-title">Contact</h2>
      <p>This is the contact section</p>
    </paper-material>

  </template>

赤枠がスタイルが適用された結果です:

f:id:masterpg:20160923114840p:plain


レスポンシブ対応

ここではレスポンシブ対応している箇所を見ていきましょう。

ブラウザの幅を伸縮させて見てください。すると丸番号を振った部分が変化するのを確認できます:

【src/elements/app-view.html】

<dom-module id="app-view">
  <template>
    <style include="shared-styles">
      ...
  
      /* Small */
      /* (定義された幅以下の場合にルールが適用されます) */
      @media (max-width: 600px) {
        /* ① メインタイトル(「Polymer Starter Kit」)のスタイル */
        paper-toolbar.tall .app-name {
          font-size: 24px;
          font-weight: 400;
        }
      }
  
      /* Tablet+ */
      /* (定義された幅以上の場合にルールが適用されます) */
      @media (min-width: 601px) {
        /* ② */
        paper-drawer-panel {
        --paper-drawer-panel-left-drawer-container: {
           border-right: 1px solid rgba(0, 0, 0, 0.14);
         };
        }
    
        /* ③ */
        iron-pages {
          padding: 48px 62px;
        }
      }
    </style>

    <!-- ④ 600px以下になると左メニューエリアが自動的に閉じ、大きくなると開く -->
    <paper-drawer-panel id="paperDrawerPanel" responsive-width="600px">
      ...
    </paper-drawer-panel>

f:id:masterpg:20160923114909p:plain


カスタムエレメント共有のスタイルモジュールでもレスポンシブ対応をしています:

【styles/shared-styles.html】

<dom-module id="shared-styles">
  <template>
    <style>
      .page-title {
        @apply(--paper-font-display2);
      }

      paper-material {
        border-radius: 2px;
        height: 100%;
        padding: 16px 0 16px 0;
        width: calc(98.66% - 16px);
        margin: 16px auto;
        background: white;
      }

      /* Small */
      /* (定義された幅以下の場合にルールが適用されます) */
      @media (max-width: 600px) {
        /* ① */
        .page-title {
          font-size: 24px!important;
        }
        /* ② */
        paper-material {
          --menu-container-display: none;
          width: calc(97.33% - 32px);
          padding-left: 16px;
          padding-right: 16px;
        }
      }

      /* Tablet+ */
      /* (定義された幅以上の場合にルールが適用されます) */
      @media (min-width: 601px) {
        /* ② */
        paper-material {
          width: calc(98% - 46px);
          margin-bottom: 32px;
          padding-left: 30px;
          padding-right: 30px;
        }
      }

f:id:masterpg:20160923114957p:plain


GoogleのMaterial Designチームは、レスポンシブ対応での画面の切り替えポイントについてガイダンスを作成しています。以下はそのガイダンスをもとに作成されたテンプレートです。自身のアプリケーションで適合する切り替えポイントは、対象のテンプレートをコピーして使用してください:

/* mobile-small */
@media all and (min-width: 0) and (max-width: 360px) and (orientation: portrait) { }
/* mobile-large */
@media all and (min-width: 361px) and (orientation: portrait) { }
/* mobile-small-landscape */
@media all and (min-width: 0) and (max-width: 480px) and (orientation: landscape) { }
/* mobile-large-landscape */
@media all and (min-width: 481px) and (orientation: landscape) { }
/* tablet-small-landscape */
@media all and (min-width: 600px) and (max-width: 960px) and (orientation: landscape) { }
/* tablet-large-landscape */
@media all and (min-width: 961px) and (orientation: landscape) { }
/* tablet-small */
@media all and (min-width: 600px) and (orientation: portrait) { }
/* tablet-large */
@media all and (min-width: 601px) and (max-width: 840px) and (orientation : portrait) { }
/* desktop-x-small-landscape */
@media all and (min-width: 0) and (max-width: 480px) and (orientation: landscape) { }
/* desktop-x-small */
@media all and (min-width: 0) and (max-width: 480px) and (max-aspect-ratio: 4/3) { }
/* desktop-small-landscape */
@media all and (min-width: 481px) and (max-width: 840px) and (orientation: landscape) { }
/* desktop-small */
@media all and (min-width: 481px) and (max-width: 840px) and (max-aspect-ratio: 4/3) { }
/* desktop-medium-landscape */
@media all and (min-width: 841px) and (max-width: 1280px) and (orientation: landscape) { }
/* desktop-medium */
@media all and (min-width: 841px) and (max-width: 1280px) and (max-aspect-ratio: 4/3) { }
/* desktop-large */
@media all and (min-width: 1281px) and (max-width: 1600px) { }
/* desktop-xlarge */
@media all and (min-width: 1601px) and (max-width: 1920px) { }


タイトルのアニメーション

画面をスクロールするとタイトルエリアの高さが伸縮し、またメインタイトルとサブタトルが拡大縮小されます。これを実現している箇所を示します。

f:id:masterpg:20160923115037p:plain

まずメインエリア (タイトル + コンテンツのエリア) ですが<paper-scroll-header-panel>エレメントを使用しており、このエレメント内にはヘッダーセクションとコンテンツセクションを含みます。画面をスクロールした際にタイトルエリアの高さを伸縮させるにはcondensesプロパティを指定します (このプロパティに値は指定しない) 。

次にメインタイトルとサブタトルの拡大縮小についてです。<paper-scroll-header-panel>はヘッダーが変化するとpaper-header-transformイベントを発火します。このイベントに対して_transformHeaderというハンドラを設定しています:

【src/elements/app-view.html】

<paper-scroll-header-panel 
  main
  condenses
  keep-condensed-header
  on-paper-header-transform="_transformHeader">

Note: イベントリスナの設定については「アノテーションによるイベントリスナの設定」を参照ください。

_transformHeaderハンドラは次のようなコーディングになっており、これによりメインタイトルとサブタトルの拡大縮小が実現されています:

【src/elements/app-view.js】

// アプリケーションのメインタイトルとサブタイトルのトランスフォームを行う。
// ・コンテンツ下部へスクロールされると、メインタイトルは縮小され、
//   またサブタイトルも縮小されて最終的に見えなくなる。
// ・コンテンツ上部へスクロールされると、メインタイトルは拡大され、
//   またサブタイトルも拡大される。
transformHeader: function (e) {
  var appName = Polymer.dom(this.$.mainToolbar).querySelector('#mainToolbar .app-name');
  var middleContainer = Polymer.dom(this.$.mainToolbar).querySelector('.middle-container');
  var bottomContainer = Polymer.dom(this.$.mainToolbar).querySelector('.bottom-container');
  var detail = e.detail;
  var heightDiff = detail.height - detail.condensedHeight;
  var yRatio = Math.min(1, detail.y / heightDiff);
  var maxMiddleScale = 0.50;  // メインタイトルのラベル最小時の割合
  var scaleMiddle = Math.max(maxMiddleScale, (heightDiff - detail.y) / (heightDiff / (1 - maxMiddleScale)) + maxMiddleScale);
  var scaleBottom = 1 - yRatio;
  
  // メインタイトルのコンテナの拡大縮小
  Polymer.Base.transform('translate3d(0,' + yRatio * 100 + '%,0)', middleContainer);
  
  // サブタイトルのコンテナの拡大縮小
  Polymer.Base.transform('scale(' + scaleBottom + ') translateZ(0)', bottomContainer);
  
  // メインタイトルのラベルの拡大縮小
  Polymer.Base.transform('scale(' + scaleMiddle + ') translateZ(0)', appName);
}