ラベル HTMLオフラインアプリ の投稿を表示しています。 すべての投稿を表示
ラベル HTMLオフラインアプリ の投稿を表示しています。 すべての投稿を表示

2013年2月8日金曜日

safari / iPhone5のホーム画面スクリーンサイズ



iPhone5のsafariの画面をホーム画面におくと、スクリーンサイズが360x480になってしまう場合があります。これはそう遠くないうちに改善されると思いますが、当面の対策がBreaking the Mobile Webに紹介されていました。

ポイントはmetaタグで width=device-width または width=32 が設定されているとスクリーンサイズが360x480に設定されてしまうということで、次のように替えることで回避できます。

<meta name="viewport" content="initial-scale=1.0">
<meta name="viewport" content="width=320.1">

4インチ画面の場合だけこの設定にするためには、2行目について次のJavaScriptで適用します。

if (window.screen.height==568) { // iPhone 4"
    document.querySelector("meta[name=viewport]").content="width=320.1";
}


この他iOS 6のHTML、CSS関連の情報が多く紹介されているのでBreaking the Mobile Webを訪問してみてください。


2012年8月7日火曜日

3D Translate実装例(2) Diceについて



3D 四面、八面ではY軸中心の回転だけでしたが、DiceではX、Yの2軸で回転させ、さらに落下を出すためにZ軸方向に移動しています。
加えてTransform後のどの面が正面を向いているかを判断しています。

HTMLでのDIVの配置、CSS設定は3D 四面の応用です。

5つの立方体のDiceを配置する。
<div id="view">
  <div id="cube1" onclick="selCube(this, false);">
    <div class="face f1">1</div>
    <div class="face f2">2</div>
    <div class="face f3">3</div>
    <div class="face f4">4</div>
    <div class="face f5">5</div>
    <div class="face f6">6</div>
  </div>
  <div id="cube2" onclick="selCube(this, false);">
    <div class="face f1">1</div>
    <div class="face f2">2</div>
    <div class="face f3">3</div>
    <div class="face f4">4</div>
    <div class="face f5">5</div>
    <div class="face f6">6</div>
  </div>
以下同様にcube3~cube5を配置する。
</div>

(selCubeはtapされたときにそのDiceを選択状態にするfunction)

上記DIVを配置するCSS
全体の位置
#view {
  width:640px;
  height:600px;
  margin:0px auto 0px auto;
  -webkit-transform:translateX(1px);
  -webkit-perspective: 1600;
  -webkit-perspective-origin-x: 550px;
  -webkit-perspective-origin-y: 600px;
}

各cube共通設定
#cube1, #cube2, #cube3, #cube4, #cube5 {
  position:absolute;
  display:none;
  height:200px;
  width:200px;
  -webkit-transform-style: preserve-3d;
  -webkit-transform:translateZ(-3000px);
}

上列3個のDiceのtop位置
#cube1, #cube2, #cube3 {
  margin-top: -240px;
}

下列3個のDiceのY位置
#cube4, #cube5 {
  margin-top: 60px;
}
各Diceの横位置。cube2は中央なので設定不要。
#cube1 { margin-left:320px; }
#cube3 { margin-left:-320px; }
#cube4 { margin-left:160px; }
#cube5 { margin-left:-160px; }

Dice各面の共通設定
.face {
  position: absolute;
  height: 160px;
  width: 160px;
  padding: 20px;
  font-size: 150px;
  text-align;center;
  vertical-align;center;
  color: transparent;
  border:solid 4px darkgray;
  background-color: rgba(50, 50, 50, 0.5);
  -webkit-border-radius: 40px;
}

6面各々の位置決め、およびイメージ設定。
.f1 {
  -webkit-transform: rotateX(90deg) translateZ(100px);
  background-image:url("../Images/1.png");
}

.f2 {
  -webkit-transform: translateZ(100px);
  background-image:url("../Images/2.png");
}

.f3 {
  -webkit-transform: rotateY(90deg) translateZ(100px);
  background-image:url("../Images/3.png");
}

.f4 {
  -webkit-transform: rotateX(180deg) translateZ(100px);
  background-image:url("../Images/4.png");
}

.f5 {
  -webkit-transform: rotateY(-90deg) translateZ(100px);
  background-image:url("../Images/5.png");
}

.f6 {
  -webkit-transform: rotateX(-90deg) translateZ(100px) ;
  background-image:url("../Images/6.png");
}

JavaScript


//Diceコンストラクタ
Dice = function(cube) {
  this.cube = cube;
  this.x = Dice.rand() * 360;
  this.y = Dice.rand() * 360;
  this.z = Dice.rand() * 360;

  this.dx = this.dy = this.dz = 0;
  this.dist = -200;  //traslateZ
  this.sec = 0.1;
  this.interval = 100; //msec;
  this.timer = null;
  //同一DiceでTimerイベントを複数実行しないようにするためのフラグ。
  this.busy = false;
  //trueの間、落下・回転を続ける。
  this.flag = true;
  //何故かiOSでは2,3が入れ替わるので、OSに合わせてfacesのindexを設定
  this.fArr = isIOS ? new Array(1,3,2,4,5,6) : new Array(1,2,3,4,5,6);
}

//Dice初期化. 5個のDiceに対応するインスタンスを作り、cubes配列にセット.
//選択されていないDiceは0とする.
//spotsArrは正面を向いている面が何かを判断するための配列。
Dice.init = function() {
  Dice.spotsArr = new Array(
    (cubes[0].getAttribute("selected") != "true" ? Dice.spotsArr[0] : 0),
    (cubes[1].getAttribute("selected") != "true" ? Dice.spotsArr[1] : 0),
    (cubes[2].getAttribute("selected") != "true" ? Dice.spotsArr[2] : 0),
    (cubes[3].getAttribute("selected") != "true" ? Dice.spotsArr[3] : 0),
    (cubes[4].getAttribute("selected") != "true" ? Dice.spotsArr[4] : 0));
  Dice._seed = Math.random(new Date().getTime());
}

//選択状態を表すために背景色を変更するとBackground-Imageが無効になるので再設定する。
Dice.prototype.setImageUrl = function() {
  this.cube.setAttribute("needsImage", false);
  for(var i=0; i<this.cube.childNodes.length; i++) {
    var face = this.cube.childNodes[i];
    if (face.nodeType == 1) {
      face.style.backgroundImage = face.getAttribute("url");
      face.style.color = "transparent";
    }
  }
}

//乱数生成
Dice.rand = function() {
  Dice._seed = Math.random(Dice._seed);
  return Math.random(new Date().getTime());
}

//タイマーイベントで呼び出される。
Dice.roll = function(o) {
  //selected属性をリセットする。
  o.cube.setAttribute("selected", "false");
  //引数のDiceインスタンスのrollを呼ぶ。
  o.roll();
}

//Diceを振る。WebKitの場合のみ実行。
Dice.prototype.roll = function() {
  if (!isWebKit || this.busy) return;
  this.busy = true;
  //画像がセットされていないときに画像をセット。
  if (this.cube.getAttribute("needsImage") == "true") this.setImageUrl();
  //this.cube.setAttribute("selected", "false");
  try {
    //視点の高さ(距離)設定
    var dist = (isIOS ? -1560 : -800);
    //高さによりロジックを変える。
    if (this.flag && this.dist > dist) {
      //まだdistまで落下していない場合。
      //乱数初期化
      Dice._seed = Math.random(new Date().getTime());
      //乱数を用いて回転角設定。
      var r1 = Dice.rand() * 40;
      var r2 = Dice.rand() * 40;
      var r3 = Dice.rand() * 40;
      //直前の回転角に加え、今回の回転をセット。
      this.x += r1;
      this.y += r2;
      this.z += r3;
      //落下速度調整
      this.dist -= 70;
      //次回の回転をTimerでセット。
      this.timer = setTimeout(Dice.roll, this.interval, this);
    } else {
      //distまで落下した一番正面を向いている面を真正面に向ける。
      var mX = this.x % 90; var amX = Math.abs(mX);
      var mY = this.y % 90; var amY = Math.abs(mY);
      var mZ = this.z % 90; var amZ = Math.abs(mZ);
      if (amX > 1 || amY > 1 || amZ > 1) {
        if (this.interval > 10) this.interval -= 1;
        this.x = (amX > 3 ?  this.x - 3 : this.x - mX);
        this.y = (amY > 3 ?  this.y - 3 : this.y - mY);
        this.z = (amZ > 3 ?  this.z - 3 : this.z - mZ);
        this.flag = false;
        this.timer = setTimeout(Dice.roll, this.interval, this);
        this.interval = 10;
      } else {
        //真正面を向いたら組み合わせ結果を表示。
        this.setSpots();
      }
    }
    //上の計算結果でTransformを実行。
    this.cube.style.webkitTransform = "translateZ(" + this.dist + "px) "
                                    + "rotateX("+ this.x + "deg) "
                                    + "rotateY("+ this.y + "deg) "
                                    + "rotateZ("+ this.z + "deg) ";
    this.cube.style.display = "inline";
  } catch(e) {
    alert("Dice.roll: " + e);
  } finally {
    this.busy = false;
  }
}

//正面を向いている目を判断する。
//X,Y,Z軸各々について回転角から面の位置を判断する。
//Timerイベントで実行されるときはthis=window。
Dice.prototype.setSpots = function() {
  //360°回転すると元に戻るので残りの角度で判断。
  //rotateの順に合わせ、Z、Y、Xの順で判断。順序は重要。
  var rz = this.z % 360;
  //90°毎に面をシフト。
  while(rz >= 90) {
    rz -= 90;
    var t = this.fArr[0];
    this.fArr[0] = this.fArr[4];
    this.fArr[4] = this.fArr[5];
    this.fArr[5] = this.fArr[1];
    this.fArr[1] = t;
  }
  var ry = this.y % 360;
  while(ry >= 90) {
    ry -= 90;
    var t = this.fArr[2];
    this.fArr[2] = this.fArr[4];
    this.fArr[4] = this.fArr[3];
    this.fArr[3] = this.fArr[1];
    this.fArr[1] = t;
  }
  var rx = this.x % 360;
  while(rx >= 90) {
    rx -= 90;
    var t = this.fArr[2];
    this.fArr[2] = this.fArr[5];
    this.fArr[5] = this.fArr[3];
    this.fArr[3] = this.fArr[0];
    this.fArr[0] = t;
  }
  //5つのcubeのどれかを判断。
  var n = new Number(this.cube.id.charAt(4));
  Dice.spotsArr[n-1] = this.fArr[2];
  //5つのDice全部の目がセットされたら役を表示。
  if (Dice.spotsArr[0]>0
   && Dice.spotsArr[1]>0
   && Dice.spotsArr[2]>0
   && Dice.spotsArr[3]>0
   && Dice.spotsArr[4]>0) {
    Dice.showSpots();
  }
}

//役判断用フラグ
Dice.spotsArr = new Array(0,0,0,0,0);
Dice.pair1 = null;
Dice.pair2 = null;
Dice.three = null;
Dice.four = null;
Dice.five = null;
Dice.fullHouse = null;

//回転終了時の正面の目の組み合わせから役を判断、表示する。
Dice.showSpots = function() {
  //役のフラグをクリア
  Dice.pair1 = Dice.pair2 = Dice.three = Dice.four = Dice.five = Dice.fullHouse = null;
  //正面を向いている目の配列をコピーし、ソートする。
  var arr = new Array(Dice.spotsArr[0],Dice.spotsArr[1],Dice.spotsArr[2],Dice.spotsArr[3],Dice.spotsArr[4]);
  arr.sort();
  //役の判断。
  Dice.findCombinations(arr);
  //役のフラグから役の名前を設定。
  var str = "";
  if (Dice.pair1 != null) {
    if (Dice.pair2 == null) str += "One Pair[" + Dice.pair1 +"]<br>";
    else str += "Tow Pairs[" + Dice.pair1 + "," + Dice.pair2 +"]<br>";
  } else if (Dice.three != null) {
     str += "Three Of A Kind[" + Dice.three +"]<br>";
  } else if (Dice.four != null) {
     str += "Four Of A Kind[" + Dice.four +"]<br>";
  } else if (Dice.five != null) {
     str += "Five[" + Dice.five +"]<br>";
  } else if (Dice.fullHouse != null) {
     str += "Full House[" + Dice.fullHouse +"]<br>";
  } else {
     str += "All Different[" + arr +"]<br>";
  }
  //役を表示。
  var div = document.getElementById("memo");
  div.innerHTML = str + "\n" + Dice.spotsArr;
}

//役を見つける。
Dice.findCombinations = function(arr) {
  var i=1, j=0, n=0, s=arr[0];
  for(; i<5; i++) {
    if (s == arr[i]) {
      n++;
    } else {
      Dice.setPairs(n+1, s);
      n = 0;
      s = arr[i];
    }
  }
  if (n > 0) Dice.setPairs(n+1, s);
  if (Dice.three && Dice.pair1) {
    Dice.fullHouse = Dice.three + "," + Dice.pair1;
    Dice.three = Dice.pair1 = null;
  }
}

//pair, three, four, fiveのフラグをセット。
Dice.setPairs = function(n, s) {
  if (n == 2) {
    if (Dice.pair1 == null) Dice.pair1 = s;
    else Dice.pair2 = s;
  } else if (n == 3) {
    Dice.three = s;
  } else if (n == 4) {
    Dice.four = s;
  } else if (n == 5) {
    Dice.five = s;
  }
}

//----- Diceを使用するfunction -----

var cubes = null;

//初期化(body.onloadなどで実行)
function init() {
  //cubeのDIVを配列にセット
  cubes = new Array(cube1, cube2, cube3, cube4, cube5);
  var divs = document.getElementsByTagName("DIV");
  //全Diceを選択状態にする。
  selAll();
  //Diceを振る。
  roll();
}

//選択状態のDiceを振る。
function roll() {
  Dice.init();
  for(var i=0; i<5; i++) {
    if (cubes[i].getAttribute("selected") == "true") new Dice(cubes[i]).roll();
  }
}

//Diceを選択状態にセット、または反転する。
function selCube(c, selAll) {
  if (!selAll && c.getAttribute("selected") == "true") {
//touchまたはclickされ、選択状態の場合は非選択状態にする。
    c.setAttribute("selected", "false");
    new Dice(c).setImageUrl();
  } else {
//sellAllから呼ばれた時、または非選択状態のときは選択状態にする。
    c.setAttribute("needsImage", "true");
    c.setAttribute("selected", true);
    var state = "true";
    //cの子要素の背景色、文字色を設定。
    var color = "rgba(130, 130, 255, 0.5)"; //半透明
    for(var i=0; i<c.childNodes.length; i++) {
      if (c.childNodes[i].nodeType == 1) { //ElementType
        if (state == "true") c.childNodes[i].style.background = color;
        if (state == "true") c.childNodes[i].style.color = "black";
      }
    }
  }
}

function selAll() {
  for(var i=0; i<5; i++) {
    selCube(cubes[i], true);
  }
}

3D Translate実装例(2) Diceについて

2012年7月25日水曜日

3D Translateの実装例



Nack Labで紹介している"ダイス"、およびTech Sampleの"3D 四面"、"3D 八面"はcss trahnsformを使用して3Dの動きを実現しています。

主にPaul R. Hayes氏のExper­i­ment: 3D cube with touch ges­tures and click and dragを参考にしています。

ここでは直方体を横方に回転させる"3D 四面"を例に取り上げて説明します。

4平面を作るためにDIV

<div id="divFaces">
  <div id="cube">
    <div class="face one"   id="one">One</div>
    <div class="face two"   id="two">Two</div>
    <div class="face three" id="three">Three</div>
    <div class="face four"  id="four">Four</div>
  </div>
</div>

上記DIV対応するCSS

全体を包むdivFacesの設定

#divFaces
{
  -webkit-perspective: 420;
  -webkit-perspective-origin: 50% 100px;
}

-webkit-perspective:
視点からの距離。数値が大きいほど望遠、小さいほど広角な効果となります。

-webkit-perspective-origin:
座標軸の中心の位置。この場合、横方向中央(50%)、縦は上から100pxとなります。

divFaces内の直方体の設定

#cube {
  -webkit-transition: -webkit-transform 0.2s linear;
  -webkit-transform-style: preserve-3d;
  margin: -150px auto 0px auto;
  height: 150px;
  width: 200px;
}

-webkit-transform-style: preserve-3d;
3D座標変換を行うための設定。

-webkit-transition: -webkit-transform 0.2s linear;
アニメーションの設定。3D座標変換とは関係ない。
margin、height、widthは適宜設定する。

四面に共通の設定

.face {
  position:absolute;
  height: 100%;
  width: 100%
  padding:0px;
  background-color: rgba(50, 50, 50, 0.5);
  font-size: 27px;
  line-height: 1em;
  color: #fff;
  border: 1px solid #555;
}

position:absolute;

同一平面上に配置するため。これがないと四面のDIVが縦方向に重ならずに並ぶ。

height: 100%;
width: 100%;

cubeのサイズと一致するように100%をセット。100%である必要はなく、pxなどの設定でもよい。

各面についての設定

#cube .one {
  -webkit-transform: translateZ(100px);
}
第1面はZ軸方向に+100px移動。手前に近付いている。

#cube .two {
  -webkit-transform: rotateY(90deg) translateZ(100px);
}
第2面はY軸で+90度回転(まわれ右)してから、Z軸で100px移動(右へ移動)。

#cube .three {
    -webkit-transform: rotateY(180deg) translateZ(100px);
}
第3面はY軸で+180度回転(後向き)してから、Z軸で100px移動(遠ざかる)。

#cube .four {
    -webkit-transform: rotateY(-90deg) translateZ(100px);
}
第4面はY軸で-90度回転(まれれ左)してから、Z軸で100px移動(左へ移動)。

直方体回転

次のようなループで直方体を連続回転させることができます。

//touch, mouseの移動量を yAngle にセットします。
var yAngle = 0;

//TransformでrotateYを行い、Y軸を中心に回転させます。
function rotate() {
  yAngle += 10;
  cube.style.webkitTransform = "rotateY(" + yAngle + "deg)";
  setTimeout("rotate()", 20);
}

yAngleは初期状態からの回転角で、直前に行ったTransformからの差分ではありません。
ループで回転させる場合はsetTimeoutを使用します。

3D Transformの留意点

3Dのtransformで頭が混乱するのは主に次の二点でしょう。
  • XYZについてのtransformの順序で結果が異なる。
    上の例ではY軸で回転してからZ軸で移動しているが、Z軸で移動してからY軸で回転したのでは全く異なる結果となります。
  • rotateは座標の回転で、オブジェクトの回転ではない。
    上の例ではY軸で回転してていますが、Z軸も共に回転しているため、回転後の移動は前後の移動とは限らず、回転角に応じた方向に移動することになります。
3D 四面、八面のソースコード

//コンストラクタ
Faces = function(divFaces, divCube) {
  //直方体が配置されていDIV(divFaces)
  this.div = divFaces;
  this.cubeDiv = divCube;
  //移動量計算のため直前のtouch位置をセット。初期値=Number.MIN_VALUE
  this.x = Number.MIN_VALUE;
  //Y軸を中心とした回転角。
  this.y = 0;
  //Startイベントハンドラをセット。
  this.div.addEventListener(Faces.EventStart, Faces.touchStart, false);
  Faces.instance = this;
  Faces.div = this.div;
}

//iOS, デスクトップでイベント名を使い分ける。
Faces.EventStart = isIOS ? "touchstart" : "mousedown";
Faces.EventMove = isIOS ? "touchmove" : "mousemove";
Faces.EventEnd = isIOS ? "touchend" : "mouseup";
Faces.EventOut = isIOS ? null : "mouseout";

//Facesが作られたときの引数のdiv。イベントが発生するDIVと異なる。
Faces.div = null;

//Startイベントで作られたオブジェクト。
Faces.instance = null;

//Startイベントハンドラ。Moveイベントハンドラセット。
//thisはイベントが発生するDIV.
Faces.touchStart = function() {
  if (Faces.div == null) return;
  Faces.div.addEventListener(Faces.EventMove, Faces.touchMove, false);
  Faces.div.addEventListener(Faces.EventEnd, Faces.touchEnd, false);
  if (!isIOS) {
    Faces.div.addEventListener(Faces.EventOut, Faces.touchEnd, false);
  }
}

//Moveイベントハンドラ。
Faces.touchMove = function() {
  if (Faces.instance == null) return;
  Faces.instance.newX = event.pageX;
  with (Faces.instance) {
   if (x != Number.MIN_VALUE) {
      y += (newX - x);
      var m = y % 90;
      if (m <= 2 &&  m >= -2) {
        y -= m;
      }
      cubeDiv.style.webkitTransform = "rotateY(" + y + "deg)";
    }
    x = newX;
  }
}

//Endイベントハンドラ。End, Moveイベントハンドラ解除。
Faces.touchEnd = function() {
  if (Faces.instance == null) return;
  Faces.instance.x = Number.MIN_VALUE;
  var div = Faces.instance.div;
  div.removeEventListener(Faces.EventMove, Faces.touchMove);
  div.removeEventListener(Faces.EventEnd, Faces.touchEnd);
  div.removeEventListener(Faces.EventOut, Faces.touchEnd);
}

Faces使用例

上記のようにDIVを配置し、body.onloadなどでFacesを作る。

<body onload="new Faces(divFaces, cube)">

3D 八面の場合

八面の場合もJavaScriptは共通で、面のモデリング、CSSが異なります。

<div id="divFaces8" style="margin-top:180px">
    <div id="cube8">
      <div class="face8 f1" id="f1">One</div>
      <div class="face8 f2" id="f2">Two</div>
      <div class="face8 f3" id="f3">Three</div>
      <div class="face8 f4" id="f4">Four</div>
      <div class="face8 f5" id="f5">Five</div>
      <div class="face8 f6" id="f6">Six</div>
      <div class="face8 f7" id="f7">Seven</div>
      <div class="face8 f8" id="f8">Eight</div>
  </div>
</div>

Facesを作成するときの引数を8面のものに変更します。

<body onload="new Faces(divFaces8, cube8)">

8面の場合のCSS

#divFaces8
{
  -webkit-perspective: 250;
  -webkit-perspective-origin: 50% 80px;
}

#cube8 {
  position:relative;
  margin: -100px auto 0px auto;
  height: 133px;
  width: 100px;
  -webkit-transition: -webkit-transform 0.2s linear;
  -webkit-transform-style: preserve-3d;
}

.face8 {
  position:absolute;
  height: 120px;
  width: 100px;
  padding: 0px;
  background-color: rgba(50, 50, 50, 0.5);
  font-size: 20px;
  line-height: 1em;
  color: #fff;
  border: none;
}

#cube8 .f1 {
    -webkit-transform: translateZ(120px);
    background:gray;
}

#cube8 .f2 {
    -webkit-transform: rotateY(45deg) translateZ(120px);
    background:green;
}

#cube8 .f3 {
    -webkit-transform: rotateY(90deg) translateZ(120px);
    ---webkit-transform: translateZ(-100px);
    background:blue;
}

#cube8 .f4 {
    -webkit-transform: rotateY(135deg) translateZ(120px);
    background:yellow;
}

#cube8 .f5 {
    -webkit-transform: rotateY(180deg) translateZ(120px);
    background:red;
}

#cube8 .f6 {
    -webkit-transform: rotateY(225deg) translateZ(120px);
    background:green;
}

#cube8 .f7 {
    -webkit-transform: rotateY(270deg) translateZ(120px);
    background:blue;
}

#cube8 .f8 {
    -webkit-transform: rotateY(315deg) translateZ(120px);
    background:yellow;
}

Manifest+LocalStorageによるオフラインアプリ作成

2012年7月24日火曜日

Cache Manifestを無効にする方法



以下の記述はSafari/iOSで試した結果のもので、Safari/デスクトップでは若干異なる場合があるようです。また他のブラウザではチェックしていません。

いったん有効にしたCache Manifestを無効にする場合、単純にhtmlタグから宣言を削除しただけではうまくいきません。

既にキャッシュが有効になっている場合、htmlタグからmanifest宣言を削除してもその変更はブラウザに伝わりません。manifestファイルも更新する必要があります。
こうすると一見manifestは使われないように見えますが、ブラウザはその後もmanifestファイルをチェックしつづけ、サーバーにmanifestファイルが残っていると変更なしとなり、キャッシュを使い続けます。
こうなってからサーバーのmanifestファイルを削除しても実は手遅れで、Net不通状態と判断されるためか、キャッシュが使われ続けます。
こうして、リソースの変更が反映しないWebページになってしまうことがあります。
この動作はバグとも言えそうなので、今後変更される可能性があるでしょう。

次の手順で行うと、manifestファイルを参照しなくなります。

  1. サーバーからmanifestファイルを削除またはりネーム
  2. ブラウザからアクセス(いったんFile Not Foundの状態を作る)
  3. htmlタグからmanifest宣言を削除

開発環境では上記手順を実行することが可能ですが、実稼働環境ではブラウザからのアクセスを確認することが困難です。次善の策として、次のような手順が考えられます。


  1. htmlタグのmanifest宣言のファイル名を存在しないファイル名に変更し、manifestファイルを更新する。
    <html manifest="a.manifest">を<html manifest="-.manifest">のように変更します。
    拡張子はMIME TYPEに登録されているものにしておきます。さもないと、再度manifestを有効にしたいときに苦労します。Safari/iOSではキャッシュデータの削除を行わないと回復できませんでした。
  2. このまましばらく運用し、対象ブラウザが全て一度は"-.manifest"にアクセスしたと判断できたら、htmlタグからmanifest宣言を削除、manifestファイルを更新する。

manifest="-"へのアクセスの負荷は軽いので、1のままでも実害はほとんどないでしょう。再度Cacheを有効にしたい場合はmanifest宣言のファイル名を元に戻します。

Safari/iOSからIISにアクセスしたときのログ

設定
a.manifestファイルでindex.htmlとXmlReq.jsをキャッシュ。
NETWORK:*を設定し、XMLHttpRequestは常にNETWORKから取得。
acuse指示XMLHttpRequestでdata.txtにアクセス。

Cacheが有効な場合
manifestファイルのstatush304(Not Modified)
01:57:45 172.30.10.11 GET /a/a.manifest 304
01:57:45 172.30.10.11 GET /a/Resources/data.txt 200

index.htmlのhtmlタグからmanifest宣言を削除
manifestが更新されていないのでキャッシュが使用される。
01:58:42 172.30.10.11 GET /a/a.manifest 304
01:58:42 172.30.10.11 GET /a/Resources/data.txt 200

a.manifest更新
キャッシュが更新される。
01:59:30 172.30.10.11 GET /a/a.manifest 200
01:59:30 172.30.10.11 GET /a/Resources/data.txt 200
01:59:30 172.30.10.11 GET /a/index.html 200
01:59:30 172.30.10.11 GET /a/Resources/XmlReq.js 304
01:59:30 172.30.10.11 GET /a/index.html 200

再度アクセス
indexから宣言が削除されているがmanifest参照は有効なまま。index.html、XmlReq.jsはキャッシュが使用される。
01:59:37 172.30.10.11 GET /a/a.manifest 304
01:59:37 172.30.10.11 GET /a/Resources/data.txt 200

XMLHttpRequestでdata.txtにアクセス
既にmanifestチェック済みなのでdata.txtのみアクセスされる。
02:44:27 172.30.10.11 GET /a/Resources/data.txt 200

index.htmlのmanifest宣言復活、a.manifest更新
02:49:18 172.30.10.11 GET /a/a.manifest 200
02:49:18 172.30.10.11 GET /a/Resources/data.txt 200
02:49:18 172.30.10.11 GET /a/index.html 200
02:49:18 172.30.10.11 GET /a/Resources/XmlReq.js 304
02:49:18 172.30.10.11 GET /a/index.html 200

a.manifest削除
manifestファイルのstatusは404(Not Found)となる。
この時点ではindex.html、XmlReq.jsはキャッシュが使用される。
02:50:32 172.30.10.11 GET /a/a.manifest 404
02:50:32 172.30.10.11 GET /a/Resources/data.txt 200

index.htmlからmanifest宣言を削除
manifest参照は行われず、index.html、XmlReq.jsへのアクセス発生。
02:53:37 172.30.10.11 GET /a/index.html 200
02:53:37 172.30.10.11 GET /a/Resources/XmlReq.js 304
02:53:39 172.30.10.11 GET /a/Resources/data.txt 200

manifestが有効な状態で、index.htmlのmanifest宣言のファイル名を存在しないもの("-.manifest")に変更、manifestファイル更新
キャッシュが更新される。
02:58:48 172.30.10.11 GET /a/a.manifest 200
02:58:48 172.30.10.11 GET /a/Resources/data.txt 200
02:58:48 172.30.10.11 GET /a/index.html 200
02:58:48 172.30.10.11 GET /a/Resources/XmlReq.js 304
02:58:48 172.30.10.11 GET /a/index.html 200

再度アクセス
"-.manifest"は存在しないのでstatusは404となる。一度もmanifestファイルが読まれていない状態のため、キャッシュは無効になり、index.html、XmlReq.jsへのアクセスが発生する。
03:00:41 172.30.10.11 GET /a/index.html 304
03:00:41 172.30.10.11 GET /a/- 404
03:00:41 172.30.10.11 GET /a/Resources/XmlReq.js 304
03:00:41 172.30.10.11 GET /a/Resources/data.txt 304

index.htmlのmanifest宣言を削除
manifest参照は行われず、index.html、XmlReq.jsへのアクセス発生。
03:03:24 172.30.10.11 GET /a/index.html 200
03:03:24 172.30.10.11 GET /a/Resources/XmlReq.js 304
03:03:24 172.30.10.11 GET /a/Resources/data.txt 200

Cache Manifestを無効にする方法

2012年7月22日日曜日

Cache Manifest使用時のXMLHttpRequest



Cache Manifestを使用したオフラインアプリでもXMLHttpRequestを使用することができます。次のような点に注意すれば、XMLHttpRequestを利用してCacheを残しつつ、オンライン時に一部のデータだけ更新することができます。
以下の方法はSafari/iOSでのみチェックしています。

1.Cache Manifestの設定
次の設定をCache Manifestに含める。
NETWORK:
*
これを設定しないと、Cacheが有効なときはXMLHttpRequestのレスポンスが空となります。

2.XMLHttpRequeststatusのstatus=0チェック
Cacheが有効なときはXMLHttpRequeststatusのstatusコードが0となり、レスポンスが空になります。
ただし、XMLHttpRequeststatusのQuery StringがついたURLの場合、ホーム画面のアイコンからアクセスする場合はstatus=0ですが、Safariのアドレスバーからアクセスするとstatus=200となり、レスポンスはCacheされているデータとなります。

3.localStroageとの併用がお勧め
status=0のときにネットワークから取得したデータを使うことができるようにするため、XMLHttpRequeststatusで取得したデータはlocalStroageに保存しておくのがお勧めです。

XMLHttpRequest実装例

XmlReq = function(url) {
  this.url = url;
}

//elm = responseのcontentをセットする要素。
XmlReq.prototype.send = function(elm) {
  var req = this.createHttpRequest();
  req.open("GET", this.url, true); //true = 非同期。
  req.onreadystatechange =
    function() { //受信時に起動するイベントハンドラ
      if (req.readyState == 4) { //4 = 受信完了 (サーバ処理終了)
        var content = null;
        if (req.status == 200) {
          content = req.responseText;
          localStorage["content"] = content;
        } if (req.status == 0) {
          content = localStorage["content"];
        }
        if (content != null) {
          if (elm.tagName == "INPUT") {
            elm.value = content;
          } else if (elm.tagName == "DIV") {
            elm.innerText = content;
          } else {
            //適宜追加
          }
        }
      }
    };
  req.send(null);
}

XmlReq.prototype.createHttpRequest = function() {
  if(window.ActiveXObject){ //Win IE
    try {
      return new ActiveXObject("Msxml2.XMLHTTP");
    } catch(e) { //MSXML2以前
      return null;
    }
  } else if(window.XMLHttpRequest){
    //Win ie以外のXMLHttpRequestオブジェクト実装ブラウザ用
    return new XMLHttpRequest();
  } else {
    logDiv.innerHTML += "<br><null>";
    return null;
  }
}

XmlReq.prototype.toString = function() {
  return this.url;
}

XmlReqの利用例

function sendRequest() {
  var ID = new Date().getTime();
  var req = new XmlReq("http://" + location.host + "/SampleApp/Resources/data.txt?ID=" + ID);
  req.send(hiddenField);
}
function showData() {
  alert(hiddenField.value);
}

<input type="hidden" id="hiddenField">

2012年7月11日水曜日

localStorageの実装例



NackLabに載せているHTMLアプリは、ほとんどがlocalStorageを利用しています。その中の一例を紹介します。

localStorageのメソッドの拡張例(エラー処理などを簡略化してあります)
localStorageにptorotypeでメソッドを追加することができないため、LocalStorageをつくり、Stringの値と、それをNumber、boolに変換して返すfunctionを追加しています。

//コンストラクタ。この例ではLocalStorageを宣言しているだけ。
LocalStorage = function() { }

//key、valueのセット。
LocalStorage.set = function(key, val) {
  localStorage[key] = val;
}

//LocalStorageからデータを読み出す。Stringが返される。
LocalStorage.stringValue = function(key, defaultValue) {
  var str = localStorage[key];
  if (str == null) return defaultValue;
  return str;
}

//keyの値を数値として返す。値が設定されていない場合は、数値化でエラーとなった場合はdefaultValueを返す。
LocalStorage.numberValue = function(key, defaultValue) {
  var val = null;
  try {
    val = localStorage[key];
    if (val == null) val = defaultValue;
    return new Number(val);
  } catch(e) {
    return defaultValue;
  }
}

//keyの値をboolとして返す。値が設定されていない場合はdefaultValueを返す。
LocalStorage.boolValue = function(key, defaultValue) {
  var val = null;
  val = localStorage[key];
  if (val == null) val = defaultValue;
  return (val.toLowerCase() == "true");
}

LocalStorageの呼び出し例

//body.onloadで呼ぶ
function init() {
  divCount.innerText = LocalStorage.numberValue("count", 5);
  cbTraining.checked = LocalStorage.boolValue("training", false);
}

//onunloadで呼ぶ。
function save() {
  LocalStorage.set("count", divCount.innerText);
  LocalStorage.set("training", cbTraining.checked);
}

localStorageの実装例

2012年6月23日土曜日

HTML5+JavaScriptによるパノラマ写真実装例



Manifest+LocalStorageによるオフラインアプリ作成(5)

360°パノラマ写真をスワイプで回転させるサンプルアプリです。
水平方向のみの対応です。
デスクトップではマウスドラッグで回転します。IEでもヘッダのレイアウトが崩れますが、回転動作はします。

このアプリではManifestは設定していますが、LocalStorageは使用していません。

このサンプルアプリのパノラマ写真回転に関するコードは以下のとおりです。

メニュー画面で[Panorama]タップ時に表示するDIV
    <div id="divPic" class="pic"></div>

スタイルシート
     div.pic {
width:100%;
         height:220px;
background-position-x:0px;
    }

JavaSctipt(簡略化のためiOS対応の場合のみ抽出)
    //Picオブジェクト作成
    var pic = new Pic(divPic, "Images/001.jpg");
    //Picクラス
    Pic = function(div, url) {
        this.div = div;
        this.url = url;
        //background-imageセット
        div.style.backgroundImage = 'url("' + url + '")';
        //swipeまたはdrag操作のためのイベントハンドラ
        div.addEventListener("touchstart", Pic.touchStart, false);
        Pic.dict[div.id] = this;
    }
    //DIVのidをkey、valueをPicオブジェクトとするArray
    Pic.dict = new Array();
    //移動量計算のため直前のtouch位置をセット. 初期値=Number.MIN_VALUE
    Pic.prevX = Number.MIN_VALUE;
    //イベントハンドラから対応するPicオブジェクトを探す.
    Pic.getInstance = function(id) {
        return Pic.dict[id];
    }
    //touch 開始
    Pic.touchStart = function(event) {
        Pic.prevX = Number.MIN_VALUE;
        var x = event.srcElement.style.backgroundPositionX;
        var div = event.srcElement;
        //イベントハンドラ設定
        div.addEventListener("touchmove", Pic.touchMove, false);
        div.addEventListener("touchend", Pic.touchEnd, false);
    }
    //swipe
    Pic.touchMove = function(event) {
        var x = event.pageX;
        if (Pic.prevX == Number.MIN_VALUE) {
            //一回目はprevXをセット
            Pic.prevX = x;
        } else {
            //二回目以降
            var pic = Pic.getInstance(event.srcElement.id);
            //x位置の末尾の"px"を取り、数値化.
            var px =    pic.div.style.backgroundPositionX;
            var p = new Number(px.substr(0, px.length-2));
            p += (x - Pic.prevX);
            //swipe量を加減した位置にbackgroundImageを移動
            pic.div.style.backgroundPositionX = p + "px";
            Pic.prevX = x;
        }
    }
    //touch 終了
    Pic.touchEnd = function(event) {
        Pic.prevX = Number.MIN_VALUE;
        var div = event.srcElement;
        div.removeEventListener("touchmove", Pic.touchMove);
        div.removeEventListener("touchend", Pic.touchEnd);
    }

HTML5+JavaScriptによるパノラマ写真実装例

2012年5月22日火曜日

HTML5+JavaScriptによるSwipeの実装例

Manifest+LocalStorageによるオフラインアプリ作成(4)

サンプルアプリで、iOSに似せたSwipeを実装してあります。Swipe画面でテーブルセルをSwipeすると「削除」ボタンが表示がされ、「削除」ボタンタップでテーブルセルが削除されます。
デスクトップではマウス操作で行うことができます。IEではレイアウトが崩れますが、一応Swipe動作します。

このサンプルアプリで使用しているのswipeに関するJavaScriptは次のとおりです。

//Table Cellなどでイベント発生
function swipeCellClicked(event) {
  var div = event.srcElement;
  //Swipeオブジェクトを作る
  new Swipe(div, event);
}
//イベントハンドラの設定などを行う。
Swipe = function(div, event) {
  this.start = new Date().getTime();
  this.div = div;
  //iOSの場合
  if (isIOS) {
    div.addEventListener("touchmove", Swipe.touchMove, false);
    div.addEventListener("touchend", Swipe.touchEnd, false);
  } else {
    //SafariなどaddEventListenerを使うブラウザの場合
    if (typeof div.addEventListener != "undefined") {
      div.addEventListener("mousemove", Swipe.touchMove, false);
      //div.addEventListener("mouseout", Swipe.touchEnd, false);
      div.addEventListener("mouseleave", Swipe.touchEnd, false);
      div.addEventListener("mouseup", Swipe.touchEnd, false);
    } else {
      //IEなどattachEventを使う場合
      div.attachEvent("onmousemove", Swipe.touchMove);
      div.attachEvent("onmouseout", Swipe.touchEnd);
      div.attachEvent("mouseup", Swipe.touchEnd);
    }
  }
  if (typeof event.pageX != "undefined") {
    Swipe.x0 = event.pageX;
    Swipe.y0 = event.pageY;
  } else {
    Swipe.x0 = event.x;
    Swipe.y0 = event.y;
  }
}
//touchMoveまたはmouseMoveイベント。イベント発生位置を保存。
Swipe.touchMove = function(event) {
  if (event != null) {
    if (typeof event.pageX != "undefined") {
      Swipe.x1 = event.pageX;
      Swipe.y1 = event.pageY;
    } else {
      Swipe.x1 = event.x;
      Swipe.y1 = event.y;
    }
  } else {
    Swipe.x0 = Swipe.y0 = Swipe.x1 = Swipe.y1 = 0;
  }
}
//イベント終了時
Swipe.touchEnd = function(event) {
  div = (event == null ? this : event.srcElement);
  //イベントハンドラ削除
  if (isIOS) {
    div.removeEventListener("touchstart", Swipe.touchMove);
    div.removeEventListener("touchmove", Swipe.touchMove);
    div.removeEventListener("touchend", Swipe.touchEnd);
  } else {
    if (typeof div.removeEventListener != "undefined") {
      div.removeEventListener("mousedown", Swipe.touchMove);
      div.removeEventListener("mousemove", Swipe.touchMove);
      div.removeEventListener("mouseout", Swipe.touchEnd);
      div.removeEventListener("mouseleave", Swipe.touchEnd);
      div.removeEventListener("mouseup", Swipe.touchEnd);
    } else {
      div.detachEvent("mousedown", Swipe.touchMove);
      div.detachEvent("onmousemove", Swipe.touchMove);
      div.detachEvent("onmouseout", Swipe.touchEnd);
      div.detachEvent("mouseup", Swipe.touchEnd);
    }
  }
  var dx = Swipe.x1 - Swipe.x0;
  var dy = Swipe.y1 - Swipe.y0;
  //上下移動があまりなく、左右移動が十分ある場合にSwipeとする。
  if ((dx < -50 || dx > 50) && dy > -50 && dy < 50) {
    Swipe.div = div;
    setTimeout(Swipe.appendButton, 10);
  }
  Swipe.x0 = Swipe.y0 = Swipe.x1 = Swipe.y1 = 0;
}

Swipe.div = null;
Swipe.prevSwipedDiv = null;
//Swipeされたときに「削除」ボタンを追加、あるいは既に追加されている場合は削除する。
Swipe.appendButton = function() {
  var div = Swipe.findCellBoxDiv(Swipe.div);
  if (Swipe.prevSwipedDiv != null && Swipe.prevSwipedDiv != div) {
    Swipe.removeButton(Swipe.prevSwipedDiv);
    Swipe.prevSwipedDiv = null;
    return;
  }
  if (Swipe.removeButton(div)) {
    Swipe.prevSwipedDiv = null;
    return;
  }
  Swipe.prevSwipedDiv = div;
  var btn = document.createElement("INPUT");
  btn.type = "button";
  btn.value = "削除";
  btn.className = "swipeDelete";
  if (typeof btn.addEventListener == "undefined") {
    btn.attachEvent("onclick", Swipe.deleteCell);
  } else {
    btn.addEventListener("click", Swipe.deleteCell, false);
  }
  div.appendChild(btn);
}
//既に「削除」ボタンがセットされていたらそれを削除する。
//子要素にINPUT要素があったら削除し、trueを返す。ない場はfalseを返す。
Swipe.removeButton = function(div) {
  if (div.childNodes) {
    for(var i=div.childNodes.length - 1; i>= 0; i--) {
      if (div.childNodes[i].nodeType == 1 && div.childNodes[i].tagName == "INPUT") {
        div.removeChild(div.childNodes[i]);
        return true;
      }
    }
  }
  return false;
}
//「削除」ボタンがタップされたテーブルセルを削除
Swipe.deleteCell = function() {
  var div = Swipe.findCellBoxDiv(event.srcElement);
  div.parentElement.removeChild(div);
  Swipe.prevSwipedDiv = null;
}
//イベントが発生した要素を含むテーブルセル(DIV)を探す。
Swipe.findCellBoxDiv = function(div) {
  var cbDiv = div;
  while(cbDiv != null) {
    if (cbDiv.className == "TableCellBox") {
      return cbDiv;
    }
    cbDiv = cbDiv.parentElement;
  }
  return null;
}

HTML5+JavaScriptによるSwipeの実装例

2012年5月16日水曜日

HTML5+JavaScriptで画面をスライドさせて切り替える方法


Manifest+LocalStorageによるオフラインアプリ作成(3)

サンプルアプリではメイン画面のテーブル行を選択すると左にスライドして次の画面へ移動し、「戻る」ボタンでメイン画面に戻ります。

画面AからBへスライドして移動させる場合、次のような手順となります。
  • 同一HTML内にA、Bの両方のDIVを作る。
  • スライド前はAのみ表示する。
    画面固定にするために同時に表示される領域をデバイスのスクリーン内に収まるようにします。
  • スライド実行時にBをAの右側の位置に表示させる。
  • A、Bを同時に左方向にスライドさせる。
  • Aを非表示にする。
transformとtransitを用いて画面をスライドさせますが、アニメーションは非同期で行われるため、上記の手順を単純に記述したのでは一気に処理が終わり、アニメーション効果が出せません。そこでtimeoutを使用し、transit終了後に次の処理を開始するようにします。

サンプルアプリで画面スライドに関わる部分のJavaScriptは次のようになっています。

//グローバル変数
var mainBox = null;
var slideBox = null;

//body.onloadなどで初期化する。
function init() {
  mainBox = new SlideBox(divMainBox, null);
}

//テーブル行がタップされたとき呼ばれる。
function cellClicked(event) {
slideBox = new SlideBox(slideDiv, mainBox);
  slideBox.show();
}

//画面スライドを行うオブジェクト
SlideBox = function(div, prevBox) {
  this.div = div;
  this.div.className = "SlideBox";
  this.prevBox = prevBox;
}

SlideBox.sec = "0.3s";
SlideBox.msec = 300;
SlideBox.currBox = null;
SlideBox.divR = null;
SlideBox.divL = null;

//cellClickedから呼ばれ、画面スライドを開始する。
SlideBox.slide = function(moveX) {
  SlideBox.divL.style.webkitTransition = "all " + sec + " linear";
  SlideBox.divR.style.webkitTransition = "all " + sec + " linear";
  SlideBox.divL.style.webkitTransform = "translateX("+moveX+"px)";
  SlideBox.divR.style.webkitTransform = "translateX("+moveX+"px)";
}

SlideBox.hideBox = function(div1, div2) {
  div1.style.webkitTransition = "all 0s linear";
  div1.style.webkitTransform = "translateX(0px)";
  div2.style.display = "none";
}

SlideBox.prototype.show = function() {
  SlideBox.divR = this.div;
  SlideBox.divL = this.prevBox.div;
  SlideBox.divR.style.display = "block";
  setTimeout("SlideBox.postSlide()", 0);
}

SlideBox.postSlide = function() {
  SlideBox.slide(-320);
  setTimeout("SlideBox.postSlide2()", msec);
}

SlideBox.postSlide2 = function() {
  SlideBox.hideBox(SlideBox.divR, SlideBox.divL);
  return;
}

SlideBox.prototype.back = function() {
  SlideBox.divR = this.div;
  SlideBox.divL = this.prevBox.div;
  SlideBox.divL.style.display = "block";
  this.div.style.webkitTransform = "translateX(-320px)";
  setTimeout("SlideBox.postBack()", 0);
}

SlideBox.postBack = function() {
  SlideBox.slide(0);
  setTimeout("SlideBox.postBack2()", msec);
}

SlideBox.postBack2 = function() {
  SlideBox.hideBox(SlideBox.divL, SlideBox.divR);
  return;
}

HTML5+JavaScriptで画面をスライドさせて切り替える方法

2012年5月15日火曜日

HTML5+JavaScriptでiPhoneでスクロールしない画面を作る方法



Manifest+LocalStorageによるオフラインアプリ作成(2)

HTML+JavaScriptで作った画面は、通常はiPhoneの画面サイズに合わせてリサイズされ、ピンチ操作で拡大すると上下左右にスクロールします。これをネイティブアプリらしくiPhone画面に合わせ、スクロールを上下のみとするか、無効にする方法です。
サンプルはこのURLに置いてあります。

iPhone(およびiPod touch)に合わせて作ってありますが、デスクトップのSafari、Chiromeでも動作の様子を確認できます。Androideでも動作するはずですが、確認していません。(動作報告頂けると幸いです。)

次の点がポイントです。

どちらにも共通の設定

画面の横サイズをデバイスサイズ以下にする

iPhoneの画面サイズはScale=1の場合にwidth=320px, height=480pxですが、heightは状況により異なります。
Scale=1以外の場合はScale後のサイズに合わせます。scale=2であればwidth=640px, height=960pxとなります。

ユーザーによるリサイズを無効にする

次のメタタグを設定します。
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
initial-scaleとmaximum-scaleを同じ値にすることでピンチされても画面の拡大、縮小が起こらなくなります。scaleの値は1以外でも構いません。

アドレスバーが表示されない位置に移動する

画面表示時に次のJavaScriptを実行します。
window.scrollTo(0,0);

これは画面固定とは直接関係ありませんが、 アドレスバーが隠れるとよりネイティブアプらしくなります。また、上下方向も固定にした場合は「戻る」ボタンが隠れたままになることがあるため、必ずこれを行うようにします。

画面固定の場合の設定

上下方向も固定にしたい場合はbodyのontouchmoveイベントハンドラで、このイベントを無効にします。これを行うと画面の大きさに関わらず画面スクロールしなくなります。
<body ontouchmove="event.preventDefault()">

この方法では全ての画面でスクロールが無効になります。特定の画面でだけスクロールを無効にしたい場合はフラグを使用して、フラグがtrueのときだけイベントを無効にすればよいでしょう。
<body ontouchmove="event.preventDefault();">

パノラマ写真をiPhoneやiPadを左右にパンして見るアプリです。
自分の周りをぐるりと回すと、臨場感のあるパノラマ写真閲覧ができます。
写真ライブラリの画像を見ることができ、自分で撮影したパノラマ写真を見ることができます。
iCloudにある画像も見ることができます。
視野角度を画像の縦横比較からアプリが設定します。
視野角度は60°〜360°の範囲で調整できます。
ontouchmoveはかなりな頻度で発生するので、パフォーマンスへの影響が気になる場合は必要なときだけイベントハンドラをadd/removeしてください。

ホーム画面にアイコンを置いた場合の画面サイズ調整

ホーム画面にアイコンを置いた場合は、次のメタタグにより全画面表示となり、アドレスバー、ステイタスバーが非表示となります。
<meta name="apple-mobile-web-app-capable" content="yes" />

この場合の画面サイズはステイタスバーのheight=20pxを除き、360px×460pxとなります。

ステイタスバーのスタイルを次のメタタグでblack-translucentとすると、ステイタスバーに隠れる部分も表示領域となり、360px×480pxとなります。
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />

これに合わせて画面レイアウトを調整したい場合は次の方法で画面サイズをチェックできます。
if (document.documentElement.clientHeight >= 460) {
//ホーム画面時のレイアウト設定
}

デスクトップとの共存

HTMLなのでデスクトップのブラウザでも表示可能です。デスクトップではなにもしなければ左寄せとなります。また右マージンでレイアウトしている場合は画面の幅の変動でレイアウトが崩れることがあります。デスクトップではセンタリングしたい、右マージンもそのまま有効にしたい、という場合はDIVのBOXを組み合わせることでiPhone、デスクトップの両方で同等の表示を行うことができます。
また、こうすることで画面サイズが異なるAndroid端末でもレイアウトが崩れないはずです(未確認)。

<body style="text-align:center;marigin:0px;padding:0px;">
    <div style="display:-webkit-box;width:320px;margin:0px auto 0px auto;padding:0px;">
        この中にHTMLを書く
    </div>
</body>

2012年4月20日金曜日

Manifest+localStorageによるオフラインアプリ作成(1)



iPhone, iPod touchをメインターゲットに、HTML5のCache Manifest+localStorage+JavaScriptでネイティブアプリのような操作性を実現する方法を紹介していきます。

HTML5のmanifestと localStorage はオンライン時のレスポンス向上、負荷軽減が主目的と言えますが、これらを利用することでiPod TouchのようなWiFiしかない機器でネットに接続されていないときでも動作するアプリを作ることが可能です。

これまでに作ったアプリは Nack Lab で公開しています。
HTML+JavaScriptですから他のプラットフォームでも動作可能ですが、主にHTML5/CSS3についてはSafari/iOSを対象としているため、それ以外では動作不可/不良もあります。
iPhoneの場合はアイコンを「ホーム画面に追加」するとネイティブアプリらしくなります。

Cache Manifestの概要

詳細は様々ページで紹介されているのでそちらに譲り、ここではこのようなアプリを作るときの留意点を挙げておきます。

manifestはhtmlの最初の二行に次の要領で宣言します。一行目はHTML5であること、二行目は使用するmanifestファイルの指定です。

<!DOCTYPE html>
<html manifest="index.manifest">

これによりindex.manifestに列挙したリソースをローカルにキャッシュし、ネットワークにアクセスせずに使用することができます。manifestには次の3つの区分があります。


#CACHEキャッシュ済みのデータがあればそれを使うリソース。
#NETWORK必ずネットワークから取得するリソース。
#FALLBACKネットワークから取得できなかった場合の代替。

基本的な使い方は、変更があまりないものを#CACHEに設定しておき、通常はキャッシュされたリソースを使うようにするというものです。

ネット切断時に動作するWebアプリを作るには、対象Webページのhtmlと、そのロード中にアクセスされるリソースを全てmanifestに含めます。これにはLINK、SCRIPT、IMGなどでアクセスされるCSS、JavaScript、イメージも含まれます。また、manifestを宣言したhtmlも含めておく必要があります。

Aタグやボタンなどがクリック(タップ)されてからアクセスが発生するものや、XMLHttpRequestでアクセスするものは必須ではありません。そのhtml内で使用するリソースはキャッシュする意味がありますが、他のhtmlに移動する場合は移動元のhtmlでキャッシュしておいても意味はなく、移動先のhtmlでmanifestを設定しておかなければオフライン時はページにアクセスできす、エラーとなります。

キャッシュされたデータを更新するにはmanifestファイルの内容を変更します。タイムスタンプを変更しただけではキャッシュは更新されません。また、Webブラウザのキャッシュをクリアするとmanifestによるキャッシュもクリアされます。

manifestを変更した場合、その直後のアクセスではキャッシュが使われ、バックグランドでmanifestのチェック、変更があった場合はダウンロードが実行されます。そのため、変更が反映されるのは次のアクセス(以降)になります。

このバックグランド処理は結構時間がかかるため、manifest変更後にリロードを繰り返してもなかなか変更が反映しないという経験をしがちです。applicationCacheのonupdatereadyイベントが発生するまではダウンロードは完了していません。

またindex.htmをmanifestに加えると、htmlを変更したときはmanifestも更新する必要があります。htmlタグからmanifestをはずした場合も、その変更を有効にするためにmanifestファイルを更新する必要があります。これを忘れると一見manifestを使っていないのに、いつまでもキャッシュデータにアクセスし、変更が反映されないhtmlになってしまいます。

参照:Cache Manifestを無効にする方法

WWWサーバーにCache ManifestのMIME TYPEが設定されている必要があります。Apacheの場合はCache Manifestを置くディレクトリに、次の行を含む.htaccessを置きます。
拡張子の推奨が".manifest"から".appcache"に変更されています。
HTML5 Tracker ― 5812.

AddType text/cache-manifest .appcache

サーバーがCache ManifestのMIME TYPEを設定しておらず.htaccessも参照しない環境の場合は、Cache Manifestは利用できません。

localStorage概要

HTML5のlocalStorageはCookieと似ている点がありますが、主に次の点が異なっています。
  • 容量がCookieの4Kバイトに対し、2M~10Mバイト程度ある。
  • キー/バリュー形式でデータを扱う。
また、localStorageはウインドウを閉じたあとも保存されます。

Cookieでも類似のことはできますが、より広範囲に応用できると言えるでしょう。
IE8でもオプションの「DOMストレージを有効にする」がオンになっていれば使えます。

localStorageの消去はブラウザによって異なりますが、iOSのSafariの場合は「Cookieとデータを消去」により消去されます。

localStorage自体はオフラインで使う上で特に問題はありませんが、オフラインアプリとして利用するにはhtmlにCache Manifestが設定されていることが前提となります。

localStorageの使い方はいたってシンプルで、次のことを知っておけば十分です。

  //データ読み出し(keyは項目名)
  var stringData = localStorage[key];
  //または
 localStorage.getItem(key);

  //データ書き込み(dataは文字列として保存される)
  localStorage[key] = data;
  //または
  localStorage.setItem(key, data);

  //localStrageからの個別データ削除
  localStorage.removeItem(key);

  //localStrageからの全データ削除
  localStorage.clear();

  //サイズチェック
 localStorage.length;

localStrageサポートチェック

次のようにlocalStorageが定義されているかどうかでチェックしたいところですが...
  if (typeof localStorage == "object") {
    //localStorageを使った処理
  }

IEでは[DOMストレージを有効にする]にチェックが入っていないとlocalStorageが使えませんが、(typeof localStorage)が"object"となります。次のようにすればより確実です。

 if  (typeof localStorage == "object" && localStorage != null) {
    //localStorageを使った処理
  }