2011年4月12日

ラボ

HTML5でiOS Webアプリを作ってみました -えほんのじかん(iPad版)-

  • このエントリーをはてなブックマークに追加

はじめに

こんにちは、EveryWhere開発部の久下孝順、高橋淳史と申します。

先日リリースされました、iPad用Webアプリ「えほんのじかん」(iPad版)の紹介をさせていただきます。

このサービスの内容はごく単純で、たくさん並んだサムネイルから気に入ったものを選択すると、アニメーションと朗読によって構成された絵本風の動画が流れる、というものです。

もともとこの「えほんのじかん」の元祖は、「テレビ版」でした。
インターネット対応テレビ向けに提供しているテレビ版Yahoo! JAPANYahoo! JAPAN for AQUOSなどからみることができます。
iPad版より作品収録数は多く、30作品以上を掲載しています。
テレビ版で提供しているコンテンツは1本あたり10~15分程度の作品が中心で、テレビリモコンを大人が操作し、親子で一緒にみられるということがコンセプトでした。

iPad版については、幼児が一人でも操作して遊べることを狙いました。そのため、操作をとにかく簡単にし、作品についてもテレビ版のコンテンツの中から主に就学前の幼児に喜んでもらえるような数分程度の作品を選り抜きました。
また、画面内を飛ぶ気球や飛行機など「意外なところも触ってみると反応する」といった仕掛けを取り入れ、画面を触る楽しさを提供できるようにしています。

デジタルネイティブたちの世代に向けて、小さい頃から愛用してもらえるような、安心して触っていただける世界観・操作性・作品ラインアップなどの条件を備えたサービスづくりをめざしております。

今回のえほんのじかん(iPad版)は、リリースまで約2週間という短い期間のプロジェクトでした。
そして、EveryWhere開発部に配属されて約一ヶ月の私たち二人に、企画・開発のメインを任せていただき進めていきました。

今回の開発で使用された技術について、軽く紹介していきます。

touchイベント、webkitアニメーション

iPad専用のWebアプリとして開発したので、せっかくなのでtouchイベントやwebkitのCSSを使ってリッチなページにしていこうと考えました。

トップページではサムネイルをドラッグすることで並び替えができるのですが、この機能ではwebkitブラウザ特有のCSSアニメーションをtouchイベントで制御し実装しています。
top,leftを操作したり、jQueryのanimateを使って動かすよりも遥かにサクサク動きます。

ドラッグ操作によるアニメーションのコードはざっとこのようになっています。

function touchMoveHandler(event) {
  event.preventDefault();
  //lefts,topsには絵本の各サムネイルの、宣言時のx座標,y座標
  var diffX = event.touches[0].pageX - lefts[event.target.id];
  var diffY = event.touches[0].pageY - tops[event.target.id];

  document.getElementById(event.target.id).style.webkitTransition = 
    "-webkit-transform 100ms linear";
  document.getElementById(event.target.id).style.webkitTransform = 
    "translate3d(" + diffX + "px, " + diffY + "px, 1px) scale(1.2)";
}
  • event.preventDefault();で、ウィンドウのスクロールの禁止させます。
    これを設定しないと、ドラッグに合わせてウィンドウが動いてしまうため、正常に動作しません。
  • event.touches[0]ではタッチしている1本目の指の座標を取得できます。
    [1]で2本目の指、[2]で3本目の指...と取得できますが、今回のサービスではマルチタッチには対応させていません。仕様です。
    ちなみにiPadでは最大11本の指まで認識出来るようです。複数人でも同時に使えるアプリを開発するためでしょうか??
  • webkitTransformでは、「オブジェクトの初期の座標からの差分」を動かす範囲として設定します。
    「オブジェクトの現在の座標からの差分」ではありません。角度や拡大縮小を操作する際も同じです。
  • webkitTransitionでは何秒かけて、どのような動きでアニメーションするかを設定します。
    0msでも良いのですが、あえて少し遅らせることで雲の動きっぽくしたつもりです。
  • translateではなくtranslate3dを使えば、zindexの操作をせずともドラッグ中のオブジェクトが前面に来ます。
    そしてtranslateを使うと時々起きてしまう画面のチラつきも、translate3dを使えば解消されます。
    scaleではオブジェクトの拡大縮小を設定します。

絵本詳細画面での「スライドして隣の絵本の詳細を見る」という動きもこのCSSで実装しました。

地味ながら、並び替えた順番は随時HTML5のlocalStorageに保存されています。
次回ページを読み込んだ時も、並び替えた順番を記憶しています。
しかし初期の順番に戻す機能はありません。仕様です。

HTML5 - video

iOS Safariでは、動画再生においてFlash等のプラグインが利用できない代わりに、HTML5のvideo要素がサポートされています。
このvideo要素は、iPhone, iPadの普及やプラグイン不要といった点からどんどん注目を集めています。

iPad版えほんのじかんの動画再生ページも、video要素を用いて実装を行いました。
ブラウザにはデフォルトでプレイヤー機能が搭載されているのですが、コントローラーのUIがお子様向けではないのではという考えから、自前で動画プレイヤーを開発し、操作がわかりやすいUIで実装しました。

ここでは、本サービスの開発過程で調査したvideo要素の情報を、できるだけiOS Safariに特化した形で紹介させていただきたいと思います。

video要素のHTML

video要素のHTMLでの記述は以下のようになります。

<video id='myVideo' width=640 height=480 poster='poster.jpg' controls loop>
  <source src='video.mp4' type='video/mp4' />
  <source src='video.ogg' type='video/ogg' />
  <p>not available message</p>
</video>

iOS Safariでの特筆事項として、preloadとautoplay属性は使えません(記載しても無視されます)。
Apple社のドキュメントによると、従量課金の回線を利用しているユーザが、意図しない大量のデータ受信を防止するためのようです。
ユーザが再生ボタンまたはそれに代わるものを実際にタップするまで、動画データの読み込みは一切行われません。

この仕様を掻い潜る為に、Javascriptで擬似的にクリックイベントを発生させる等、色々な施策がWeb上に公開されていましたが、ブラウザのアップデートに伴い対策されたようで、本サービス開発時(2011年3月 iOS4.2.1)では自動再生は不可能のようでした。

動画の再生・一時停止

動画の再生はplay()メソッドを、一時停止はpause()メソッドを呼ぶだけです。
具体的なJavascriptコードは以下の様になります。

var videoElement = document.getElementById('myVideo');
videoElement.play()  //再生
videoElement.pause() //一時停止

video要素には動画の停止中を表すプロパティpausedがあり、play()はpausedにfalseを、pause()はpausedにtrueを代入すると言い換えることもできます。

シークバー

今回実装したシークバーには以下の様な機能があります。
・経過時間表示
・スライダー
・プログレスバー
・バッファ表示

これらの機能の実装方法について順を追って見ていきたいと思います。

経過時間表示

動画再生の経過時間を表示する機能です。動画の経過時間は、video要素のcurrentTimeプロパティから読み取ることができます。
currentTimeには秒オーダーの実数(例 : 123.456789)が格納されていますので、適宜加工してから表示する必要があります。
また、video要素にはtimeupdateというイベントが用意されています。これはcurrentTimeが更新される度に発生するイベントなので、このイベントに紐付けて経過時間表示を更新していきます。
以下、動画の経過時間をm:ss形式で表示する簡単な具体例です。

var videoElement = document.getElementById('myVideo'); //video要素
var timerElement = document.getElementById('timer');   //div要素など
videoElement.addEventListener('timeupdate', function() {
  var time = videoElement.currentTime;
  var min = ~~(time / 60);
  var sec = ~~(time % 60);
  if (sec < 10) {
    sec = '0' + sec;
  }
  timerElement.innerHTML = min + ':' + sec;
}, false);

ただし、iOS Safariのtimeupdateイベントの発生タイミングは不定期(等間隔でない)なので、秒数更新のタイミングが若干不自然に感じることがあります。
今回はtimeupdateに紐付けましたが、setIntervalで等間隔に更新する方法も良いかもしれません。

スライダー

スライダーには経過時間に合わせて左から右へ徐々に移動していったり、ドラッグして任意の時間から再生できる機能があります。
えほんのじかんでは青い丸として実装しました。

経過時間と連動したスライダーの移動量は、

移動量 = (シークバーの横幅 - スライダーの横幅) * 経過時間 / 動画の長さ

で算出することができます。経過時間はvideo要素のcurrentTime、そして動画の長さはvideo要素のdurationというプロパティから取得できます。
この処理をtimeupdateイベントに紐付ければ経過時間と連動して移動するスライダーを作ることができます。

var videoElement   = document.getElementById('myVideo'); //video要素
var seekbarElement = document.getElementById('seekbar'); //div要素など
var sliderElement  = document.getElementById('slider');  //div要素など

//sliderが動く範囲[pixcel]
var range = seekbarElement.offsetWidth - sliderElement.offsetWidth;

//アニメーション設定
sliderElement.style.webkitTransition = "-webkit-transform 0ms linear";

videoElement.addEventListener('timeupdate', function() {
  var transfer = ~~(videoElement.currentTime / videoElement.duration * range);
  sliderElement.style.webkitTransform = "translateX(" + transfer + "px)";
}, false);

sliderの位置更新には、トップページと同じくwebkitTransisionによるアニメーションを用いています。

また、スライダーをドラッグして任意の場所から再生するには、上の移動量の式を経過時間で解けば、

経過時間 = 動画の長さ * 移動量 / (シークバーの横幅 - スライダーの横幅)

となるので、この値をcurrentTimeに代入することで実現できます。

プログレスバー

プログレスバーは、シークバー内で再生完了した領域を示すものです。えほんのじかんでは水色のバーとして実装しました。
プログレスバーはスライダーと連動しているため、ほぼ同じ処理で実装できます。
違いとしては、スライダーでは位置を更新していた部分を、プログレスバーでは要素の幅を更新するという点だと思います。

progressElement.style.width = transfer + 'px'; //要素の幅を更新
バッファ表示

バッファ表示は、プレイヤーが動画の読み込みを完了した部分を表す機能です。
えほんのじかんでは灰色のバーとして実装しました。
動画のバッファ情報は、video要素のbufferedというプロパティから取得することができます。
bufferedプロパティにはTimeRangesオブジェクトが格納されています。
TimeRangesオブジェクトには、バッファ済み領域の始点を表すstartと終点を表すendプロパティがあり、currentTimeと同様に秒オーダーの実数値が格納されています。
ここで、startおよびendは配列となっていることに注意が必要です。なぜ配列になるかというと、例えばある動画を再生中にシークバーを操作して、まだバッファが完了していない領域から再生したとします。
すると動画プレイヤーはそれまでのバッファを中断し、新しい再生位置からまたバッファを始めます。
この時、下の図のようにバッファ済みの領域が非連続に複数存在することになるので、startおよびendは配列となっているようです。
これらの配列の長さはTimeRangesオブジェクトのlengthプロパティに格納されています。(参考:Apple社ドキュメント)

この非連続なバッファ情報を全て考慮していると複雑になってしまうので、今回は配列の最後の値だけを参照するようにしました。
具体的には以下のようにして動画のバッファ情報を取得しています。

var videoElement = document.getElementById('myVideo'); //video要素
var buffer    = videoElement.buffered; //TimeRangesオブジェクト
var lastIdx   = buffer.length - 1;
var buffStart = buffer.start(lastIdx); //バッファ済み始点
var buffEnd   = buffer.end(lastIdx);   //バッファ済み終点

この情報を用いて、プログレスバーと同様にバッファバーの横幅を制御しています。

※こちらのアプリは、Yahoo!ラボへの掲載を終了しました。

Yahoo! JAPANでは情報技術を駆使して人々や社会の課題を一緒に解決していける方を募集しています。詳しくは採用情報をご覧ください。

  • このエントリーをはてなブックマークに追加