読者です 読者をやめる 読者になる 読者になる

Touch/Gestureイベントの勘所

iPhone/iPad向けのWebアプリでタッチでの操作を扱う際のメモ。

ユーザによるズームは無効にしておいた方が良さそう。

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

【追記】
contentの区切りは","が正です

Touch

Touchイベントは

  • touchstart :スクリーンに指が触れた
  • touchmove :スクリーン上で指が動いてる最中
  • touchend :指がスクリーンから離れた
  • touchcancel :システムがタッチイベントをキャンセルした場合?

の4種類です。


注意しないといけないのは、例えばタッチした場所の座標を表示しようとしたとき

target.addEventListener('touchstart', touchStart, false);

function touchStart(ev) {
  var x = ev.screenX;
  var y = ev.screenY;
}

の様にやってしまいがちですが、これは動きません(x/yはundefindになる)。


Touchイベントは、タッチを検知するための

  • touches : Touchオブジェクトの配列
  • targetTouches : イベント発生元のTouchオブジェクトの配列
  • changedTouches : 最後のTouchイベントから変更が起こっているTouchオブジェクトの配列

というプロパティを持っているので、こいつらに入っているTouchオブジェクトのプロパティを参照する必要があります。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=no;" />
  <title>test</title>
  <style>
  div {
    display: inline-block;
    margin: 0 10px;
  }
  div#target {
    display: block;
    width: 200px;
    height: 200px;
    background-color: blue;
  }
  </style>
</head>
<body>
<h1>Gesture test.</h1>
<div id="start">
  <p>X:<span class="x"></span></p>
  <p>Y:<span class="y"></span></p>
</div>
<div id="move">
  <p>X:<span class="x"></span></p>
  <p>Y:<span class="y"></span></p>
</div>
<div id="end">
  <p>X:<span class="x"></span></p>
  <p>Y:<span class="y"></span></p>
</div>
<div id="target"></div>

<script>
  var startx  = document.querySelector('div#start span.x'),
      starty  = document.querySelector('div#start span.y'),
      movex   = document.querySelector('div#move span.x'),
      movey   = document.querySelector('div#move span.y'),
      endx    = document.querySelector('div#end span.x'),
      endy    = document.querySelector('div#end span.y'),
      target  = document.getElementById('target');


target.addEventListener('touchstart', touchStart, false);
target.addEventListener('touchmove', touchMove, false);
target.addEventListener('touchend', touchEnd, false);

function touchStart(ev) {
  var touch = ev.touches[0];
  startx.innerHTML = touch.screenX;
  starty.innerHTML = touch.screenY;
}

function touchMove(ev) {
  ev.preventDefault();
  var touch = ev.changedTouches[0];
  movex.innerHTML = touch.screenX;
  movey.innerHTML = touch.screenY;
}

function touchEnd(ev) {
  var touch = ev.changedTouches[0];
  endx.innerHTML = touch.screenX;
  endy.innerHTML = touch.screenY;
}
</script>
</body>
</html>

ターゲットの要素の上でスクロールしたい時にページ自体がスクロールしてしまうので
touchMoveでpreventDefault()する様にします。


そもそも、ページ全体でスクロールさせたくない場合には

document.addEventListener('touchmove', function(ev) {
  ev.preventDefault();
}, false);

の様にします。


さらに、touchendイベントが起こっている時点で、touchesには何も無い状態なので、changedTouchesのTouchオブジェクトを使います。

Gestures

Gestureイベントはユーザが2本以上の指を使ってスクリーンに触れたときに発生します。

Touch Gesture Reference Guide
http://www.lukew.com/ff/entry.asp?1071


Touch/Gestureイベントは以下の様な流れで発生します。

  1. スクリーンにタッチ -> touchstart
  2. 他の指もスクリーンにタッチ(都合2本指でタッチ) -> gesturestart -> touchstart
  3. 指が動く -> gesturestart
  4. どちらかの指がスクリーンから離れる -> gestureend -> touchend
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=no;" />
  <title>test</title>
  <style>
  div#log {
    border: 1px solid #333;
    width: 400px;
    height: 500px;
    margin-left: 50px;
    overflow: scroll;
  }
  div#target {
    display: inline-block;
    float: left;
    width: 200px;
    height: 200px;
    background-color: blue;
  }
  </style>
</head>
<body>
<h1>Gesture test.</h1>
<div id="target"></div>
<div id="log"></div>

<script>
  var log = document.getElementById('log'),
      target = document.getElementById('target');


  target.addEventListener('touchstart', touchStart, false);
  target.addEventListener('touchmove', touchMove, false);
  target.addEventListener('touchend', touchEnd, false);
  target.addEventListener('gesturestart', gestureStart, false);
  target.addEventListener('gesturechange', gestureChange, false);
  target.addEventListener('gestureend', gestureEnd, false);

  function logger(msg) {
    var p = document.createElement('p');
    var t = document.createTextNode(msg);
    p.appendChild(t);
    log.appendChild(p);
  }

  function touchStart(ev) {
    logger('touchstart');
  }

  function touchMove(ev) {
    ev.preventDefault();
    logger('touchMove');
  }

  function touchEnd(ev) {
    logger('touchEnd');
  }

  function gestureStart(ev) {
    logger('gestureStart');
  }

  function gestureChange(ev) {
    logger('gestureChange');
  }

  function gestureEnd(ev) {
    logger('gestureEnd');
  }

</script>
</body>
</html>


Gestureイベントが持っているプロパティでTouchイベントには無いものがevent.rotationとevent.scaleです。
要素を操作したい場合などにTouchイベントで2本の指をトラッキングするよりも簡単に扱う事ができます。


要素のドラッグ&ドロップ + 拡大/縮小

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=no;" />
  <title>test</title>
  <style>
  div#target {
    position: absolute;
    height: 200px;
    width: 200px;
    background-color: blue;
  }
  </style>
</head>
<body>
<h1>Gesture test.</h1>
<div id="target"></div>

<script>
var target = document.getElementById('target'),
    styles = document.defaultView.getComputedStyle(target, ''),
    posX, posY, startX, startY,
    width = stripPx(styles.width),
    height = stripPx(styles.height),
    rotation = 0,
    dragging = false;

function touchStart(ev) {
  if(ev.touches.length == 1) {
    var touch = ev.touches[0],
        styles = touch.style;

    startX = touch.pageX;
    startY = touch.pageY;
    posX = stripPx(target.style.left);
    posY = stripPx(target.style.top);
    dragging = true;
  }
}

function touchMove(ev) {
  if(ev.touches.length == 1) {
    if(dragging) {
      var touch = ev.touches[0],
          styles = target.style;

      styles.left = posX + (touch.pageX - startX) + 'px';
      styles.top = posY + (touch.pageY - startY) + 'px';
    }
  }
}

function touchEnd(ev) {
  dragging = false;
}

function gestureChange(ev) {
  target.style.width = (width * ev.scale) + 'px';
  target.style.height = (height * ev.scale) + 'px';
  target.style.webkitTransform =
    'rotate(' + ((rotation + ev.rotation) % 360) + 'deg)';
}

function gestureEnd(ev) {
  width *= ev.scale;
  height *= ev.scale;
  rotation = (rotation + ev.rotation) % 360;
}

function stripPx(v) {
  if(v == '') { return 0 };
  return parseFloat(v.substring(0, v.length - 2));
}

target.addEventListener('touchstart', touchStart, false);
target.addEventListener('touchmove', touchMove, false);
target.addEventListener('touchend', touchEnd, false);
target.addEventListener('gesturechange', gestureChange, false);
target.addEventListener('gestureend', gestureEnd, false);

document.addEventListener('touchmove', function(ev) {
  ev.preventDefault();
}, false);

</script>
</body>
</html>

【追記】
拡大、縮小は

target.style.webkitTransform = 'scale(' + ev.scale + ')';

の方が楽ですね。