はじめまして、ブロググループでフロントエンドの開発を担当している泉と申します。
クリエイティブアカデミー※1を経て、2012年5月に中途入社いたしました。

今回は、スマートフォン版アメーバブログの画像コンテンツで使ったタッチイベントの技術について少しお話させていただきます。
スマートフォンフォン版のページ制作ということで、HTML5/CSS3/JavaScriptを組み合わせて制作していきます。
※1 弊社が実施したクリエイター・エンジニア向けの無料セミナーで、私は「HTML5/CSS3/JavaScript」コースでスマートフォンWebアプリケーションの制作を学びました。アカデミーの模様は、弊社渡辺が書いた記事をご参照下さい。
クリエイティブアカデミー レポート


仕様について

1、フリックを使った画像の切り替え


1 pixel|サイバーエージェント公式クリエイターズブログ-flick_image

今までは、現在見ている画像から次(前)の画像を見るためには、リンクをタップしてもらって該当の画像ページに遷移させる必要がありました。
しかし今回はユーザにストレスを与えないために、ページ遷移しなくても画像が切り替わるように実装します。

2、表示されている画像のピンチイン/アウト(拡大/縮小)


1 pixel|サイバーエージェント公式クリエイターズブログ-pinch_image


タップされた時のイベントハンドリング

スマートフォンでタップされた時のイベントのハンドリングの方法は様々あると思いますが、今回はTouchイベントとGestureイベントを使用します。

Touchイベント

Touchイベントは指が1本以上タップされた時に発生します。
・touchstart・・・スクリーンに指が触れた
・touchmove・・・スクリーンに指が動いた
・touchend・・・スクリーンから指が離れた
・touchcancel・・・システム側でキャンセルされた(着信など)

Gestureイベント

2本以上の指でタップされた段階でTouchイベントに加えてGestureイベントが発生します
・gesturestart・・・2本目(2本以上)の指がスクリーンに触れた
・gesturechange・・・2本以上の指が動いた
・gestureend・・・触れているどれかの指が離れた

これらのイベントをハンドリングしながら、実際に制作していきます。

フリックを使った画像の切り替え

ここからはサンプルコードを使ってご説明します。

HTML 例
<div id="imgList" style="-webkit-transform:translate3d(0%, 0%, 0px);">

<div style="-webkit-transform:translate3d(-100%, 0%, 0px);"><img src="sample01.jpg" id="img01"></div>・・・画像a

<div style="-webkit-transform:translate3d(0%, 0%, 0px);"><img src="sample02.jpg" id="img02"></div>・・・画像b

<div style="-webkit-transform:translate3d(100%, 0%, 0px);"><img src="sample03.jpg" id="img03"><div>・・・画像c

</div>

<div id="animationLayer"></div>

JavaScript 例
var startX;
var moveX;
var eventKind = '';
var elm = document.getElementById('animationLayer'); //タップを取得するエレメントを定義
var slide = document.getElementById('imgList'); //スライドさせるエレメントを定義
var slidePosition = 0;
var imgPosition = 0; //初回アクセス時の画像を0番目として、見ている画像の番号を格納する

elm.addEventListener('touchstart', touchStart, false);//タップされた瞬間
elm.addEventListener('touchstart', touchMove, false);//指を動かしている
elm.addEventListener('touchstart', touchEnd, false);//指が画面から離れた

//タップされた瞬間に実行されるメソッド
function touchStart(e) {
switch (e.touches.length) {
case 1:
eventKind = 'flick';
flickStart(e);
break;
default:
break;
}
}

//指が動いている時に実行されるメソッド
function touchMove(e) {
e.preventDefault();
switch (e.touches.length) {
case 1:
flickMove(e);
break;
default:
break;
}
}

//指が離れた時に実行されるメソッド
function touchEnd(e) {
switch (eventKind) {
case 'flick':
flickEnd(e);
break;
default:
break;
}
}


function flickStart(e){

//スタート時のX座標を保持
startX = e.touches[0].pageX;

//タップした瞬間の id="imgList" のX方向のポジションを取得
slidePosition = window.innerWidth * imgPosition;

}

function flickMove(e){

//指が動いた距離を計算
moveX = startX - e.touches[0].pageX;

//指が動いた分 id="imgList" を移動させる
slide.style.webkitTransform =
'translate3d(' + ((slidePosition - moveX) * -1) + 'px, 0px, 0px)';
}

function flickEnd(e){

//指が動いた距離を計算
moveX = startX - e.touches[0].pageX;

//指を離した場所と最初にタップ場所が100px以上離れていたら自動的に次の画像を表示する
//スライドさせる方向を判定
//右の画像を表示させる時は+、逆はー
if(moveX > 100){
imgPosition += 1;
}
else if(moveX < -100){
imgPosition -= 1;
}

//(表示させている画像のポジション × -100)% 分移動させる
slide.style.webkitTransform =
'translate3d(' + (imgPosition * -100) + '%, 0px, 0px)';
}



今回はサンプルとしてスライドさせる部分のHTMLと、実際にスライドさせるJavascriptのソースを用意しました。

タップを検知するエレメントに<div id="animationLayer"></div>を用意しました。
<div id="animationLayer"></div>はスクリーンいっぱいのサイズで、レイヤー最前面に
用意します。

<div id="imgList"></div>が画像を囲っているエレメントで、その下に表示する画像が
横並びになるようにマークアップします。
<div id="imgList"></div>をスライドさせることで前後の画像を表示させたいと思います。

大まかな処理の流れは以下のようになります。
1:タップされた指の数を判定
2:id="imgList" のX方向のポジションを取得
3:指を動かした距離分 id="imgList" をスライドさせる
4:ある程度(今回は100pxに指定)スライドさせて指を離したら、自動的に次(前)の
  画像をスライドさせる

大事なポイントを簡単にご説明します。

まず、タップしている指の数を取得するときはe.touches.lengthを使います。
touchXXXX系のイベントはtouchesという名前でタップした指の分だけ指の情報の
オブジェクトを配列で持ちます。
例:1本目の指・・・e.touches[0]、2本目の指・・・e.touches[1]
1本だけしかタップされていない場合は配列に1つしかオブジェクトが存在しないことに
なるので、指の数を取得するときはlengthプロパティで取得することができます。

そしてtouchesの中のpagexXプロパティでタップした指のX座標を取得することができます。
例:e.touches[0].pageX。

JavaScriptでインラインstyleを指定していますが、今回のようにベンダープレフィックス付きの
プロパティを指定したい時は"webkitXxxxx"という風に記述します。
例:translate3dを使いたい時
  ・・・element.style.webkitTransform = 'translate3d(Xpx, Ypx, Zpx)';

【注意点】
touchendの時はe.touchesが空になってしまうため、指の本数が取れません。
そこでtouchstart時にどのようなアクションが実行されているかあらかじめ保持して、
touchend時はその保持されたアクションの内容を見て実行するメソッドを出し分ける必要が
あります。

また、指を動かしている時にデフォルトの画面の動きを止めないといけないので、
各メソッドを実行する前にe.preventDefaultをコールして何も動作しないように制御する必要が
あります。

表示されている画像のピンチイン/アウト(拡大/縮小)

以下、サンプルコードになります。
HTMLはフリックと同じ物を使用します。(拡大する画像はsample01.jpgとします)

HTML 例
<div id="imgList" style="-webkit-transform:translate3d(0%, 0%, 0px);">

<div style="-webkit-transform:translate3d(-100%, 0%, 0px);"><img src="sample01.jpg" id="img01"></div>・・・画像a

<div style="-webkit-transform:translate3d(0%, 0%, 0px);"><img src="sample02.jpg" id="img02"></div>・・・画像b

<div style="-webkit-transform:translate3d(100%, 0%, 0px);"><img src="sample03.jpg" id="img03"><div>・・・画像c

</div>

<div id="animationLayer"></div>

JavaScript 例
var startDistance;
var moveDistance;
var currentScale = 1;
var saveScale = 1; //拡大率(縮小率)の初期値
var eventKind = '';
var elm = document.getElementById('animationLayer'); //タップを取得するエレメントを定義
var img = document.getElementById('img01'); //拡大させる画像を定義

elm.addEventListener('touchstart', touchStart, false);//タップされた瞬間
elm.addEventListener('touchstart', touchMove, false);//指を動かしている
elm.addEventListener('touchstart', touchEnd, false);//指が画面から離れた

//タップされた瞬間に実行されるメソッド
function touchStart(e) {
switch (e.touches.length) {
case 1:
eventKind = 'flick';
case 2:
eventKind = 'pinch';
pinchStart(e);
break;
default:
break;
}
}

//指が動いている時に実行されるメソッド
function touchMove(e) {
e.preventDefault();
switch (e.touches.length) {
case 2:
pinchMove(e);
break;
default:
break;
}
}

//指が離れた時に実行されるメソッド
function touchEnd(e) {
switch (eventKind) {
case 'pinch':
pinchEnd(e);
break;
default:
break;
}
}


function pinchStart(e){

//スタート時の指の距離を保持
startDistance = getDistance(e);

}

function pinchMove(e){

//2本指の距離を計算
moveDistance = getDistance(e);

//start時の距離を基準に動いた比率を計算
currentScale = moveDistance / startDistance;

//拡大率を算出
saveScale = saveScale * currentScale;

img.style.webkitTransform =
'scale3d(' + saveScale + ',' + saveScale + ', 1)';
}

function pinchEnd(e){
if(saveScale < 1){
saveScale = 1;
img.style.webkitTransform =
'scale3d(' + saveScale + ',' + saveScale + ', 1)';
}
}

//指の距離を測るメソッド.
function getMeasure(e) {
return Math.sqrt(
Math.pow(
Number(e.touches[0].pageX) - Number(e.touches[1].pageX), 2) +
Math.pow(
Number(e.touches[0].pageY) - Number(e.touches[1].pageY), 2));
}
ピンチの大まかな処理の流れは以下のようになります。
1:タップされた指の数を判定
2:2本指でタップ瞬間に、指の間の距離を測定
3:動かす度に指の間の距離を測定し、拡大率を保持させつつ画像を拡大
4:どちらかの指が離れた瞬間に拡大率が1以下だったら、拡大率を1にして
  画像を元のサイズに戻す
5:2回目以降のピンチは保持された拡大率と動かしている比率を乗算して拡大率を算出する

2本の指でタップされたのでgestureイベントが使えるようになります。
gestureイベントにはピンチ時の拡大率を返してくれるevent.scaleという素晴らしいプロパティがあります。
よし、これを使おう!っと思ったのですが、Androidで非対応だったので今回は自力で計算することにします。

2本の指の距離は以下の公式で出すことができます。
√((X2 - X1)2 + (Y2 - Y1)2)
数学の授業でやった記憶がある方もいるかと思います。
この公式を頑張って処理してくれているのがgetMeasureメソッドです。

そして動かしている時の距離をピンチスタート時の距離で除算ことで拡大率を算出することが
できます。
currentScale = moveDistance / startDistance;

次に保持してある拡大率に、動かしている時の拡大率を乗算することで最終的な拡大率を
算出します。
(初回はsaveScaleに拡大率1を保持しておく)
saveScale = saveScale * currentScale

あとはフリックの時と同じようにtransformの値をエレメントに適用するだけで
ピンチイン/アウトされるようになります。
今回は拡大縮小させるのでscaleプロパティを使います。
img.style.webkitTransform = 'scale3d(' + saveScale + ',' + saveScale + ', 1)';

最後に

今回ご紹介した内容はタッチ系イベントの基礎的な部分ですが、基礎的な技術でフリックや
ピンチイン/アウトの実装も簡単にできてしまします。
もし機会がありましたら、オリジナルのフォトギャラリー等を作ってみてはいかがでしょうか。

私ももっと深く追求して、Webブラウザで表現出来る可能性を探ってみたいと思っております。

最後まで読んでいただきありがとうございました。