Androidで動く HTMLとJavaScriptで作る電子書籍アプリ
2011/01/19 最終更新
クロノス・クラウン合資会社
柳井政和
HP:http://crocro.com/
Twitter:http://twitter.com/ruten
はじめに
本資料とプログラムは、2011/01/16のSwapSkillsの勉強会用に作成したものです。この勉強会に、講師として登壇することになりましたので、Android+JavaScriptという内容で用意しました。
Androidのアプリケーションは、WebViewというクラスを使うことで、簡単にHTMLファイルを表示させることができます。このWebViewは、Androidのブラウザと同じで、WebKitをレンダリングエンジンとして使用したものです。
この仕組みと、既存のWebページ作成技術を利用して、Androidの簡単なアプリケーションを作成してみようというのが今回の主眼です。また、その一環として、JavaScriptを軽く学んでみようと計画しています。
サンプルのソースコードは、Android側が50行、HTML側が300行程度の短いものです。内容的には、多岐に渡っていますが、やっていること自体はかなり単純です。
一応「電子書籍」と書いていますが、「なんちゃって電子書籍」といった感じの「電子書籍風アプリ」ということでご理解いただければと思います。
というわけで、資料とプログラムを公開しておきますので、ご自由に利用していただければと思います。
作成したアプリの動作確認
どんな感じになっているのか、Youtubeの動画で確認できるようにしました。
また、ブラウザ上で確認できるようにもしました。あまり多くのブラウザで検証していないので、バグがあるかもしれません。そこはご愛嬌ということで。
簡単な操作説明もしておきます。
,<<,最後のページに ,<,次のページに ,>,前のページに ,>>,最初のページに ,□+,50%拡大 ,□□,画面にフィット ,□-,50%縮小 ,画像をドラッグ,表示位置移動 ,タイトル,ページ数と書籍名を表示
勉強会用資料
PDFとして作成した資料です。Googleドキュメントにアップしましたので、自由に閲覧することができます。
内容的には、以下のことに触れています。
*Androidの開発環境設定 *AndroidとiPhoneの比較。ビジネスの主体(AppleとGoogleのスタンスの違い) *HTMLとJavaScriptで電子書籍アプリを作成 *JavaScriptの書き方、関数の書き方・呼び出し、自分で作る変数など。 *プログラムの準備 仕様の策定(UIと機能の決定、設計図、策定した仕様の関数を整理など) *アプリケーションの完成
Androidのプロジェクトを、ZIPファイルで固めてアップしていますので、Androidの開発を行っている方は、自分の環境で動作を確かめることもできます。サイズは2MBほどです。
Android側ソースコード
以下、Android側のソースコードです。短いです。
package com.crocro.android.mangaJS; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.os.Bundle; import android.webkit.WebChromeClient; import android.webkit.WebView; public class MangaJS extends Activity { WebChromeClient mWebChromeClient = new WebChromeClient() { // document.titleの変更を実装 @Override public void onReceivedTitle (WebView view, String title) { MangaJS.this.setTitle(title); } }; // メイン部分 @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); WebView webView = new WebView(this); webView.getSettings().setJavaScriptEnabled(true); webView.setWebChromeClient(mWebChromeClient); webView.loadUrl("file:///android_asset/index.html"); setContentView(webView); } }
以下、簡単な解説を行っておきます。
基本的には「メイン部分」の処理が全てです。アプリ起動時に呼び出される「onCreate」メソッド内で、「WebView」クラスを初期化して、画面に貼り付けています。
作成した「WebView」では、「JavaScriptの許可」(getSettings().setJavaScriptEnabled(true))、「WebChromeClientの設定」(setWebChromeClient(mWebChromeClient))、「URLの読み込み」(loadUrl("file:///android_asset/index.html"))を行っています。「file:///android_asset/」というのは、Androidアプリのプロジェクト内の「assets」フォルダになります。
「WebChromeClient」についても補足しておきます。ここでは「mWebChromeClient」として、「document.titleの変更」の実装を行っています。これらの設定をしなければ、「WebView」を画面に貼り付けても、JavaScriptからタイトルを変更することはできません。
HTML側ソースコード
以下、HTML側のソースコードです。少し長めです。
<html> <head> <meta http-equiv="Pragma" content="no-cache"> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <meta http-equiv="content-Style-Type" content="text/css"> <meta http-equiv="content-Script-Type" content="text/javascript"> <title></title> <style> /* Basic */ body { margin: 0px; padding: 0px; height:100%; overflow: hidden; } /* Tool range */ #tools { height: 50px; overflow: hidden; text-align: center; } #tools input { margin: 0px; padding: 0px; height: 100%; font-size: 20px; /* fit for a tool range */ width: 14.28%; width: expression(document.body.clientWidth / 7); /* for IE */ } /* Page range */ #page { background: silver; margin: 0px; padding: 0px; overflow: scroll; /* fit for a page range */ position: absolute; top: 50px; left: 0; right: 0; bottom: 0; /* for IE */ width: 100%; height: expression(document.body.clientHeight - 50); } #pageImg { position: absolute; cursor : move; } </style> <script src="jquery.min.js" type="text/javascript"></script> <script type="text/javascript"> <!-- //================================================== // 変数 var bookTitle = "マンガでわかるJavaScript"; // 本のタイトル var pageMax = 9; // ページ数 全体 var pageNow = 1; // ページ数 現在 var screenW = 0; // 画面サイズ 横幅 var screenH = 0; // 画面サイズ 高さ var imageW = 0; // 画像サイズ 横幅 var imageH = 0; // 画像サイズ 高さ var rateFit = 0; // 倍率 フィット var rateNow = 0; // 倍率 現在 var rateStep = 0.5; // 倍率の変動量 //================================================== // 「ページ読み込み時の初期化」関数 function myLoad() { // IEでブラウザ起動直後に値が取得できないバグ対策 if ($("#page").width() == 0 || $("#page").height() == 0) { window.setTimeout(myLoad, 100); return; } // スクロール・バーのサイズを計算 var scrollBarSize = getScollBarSize(); // 画面サイズの初期化 screenW = $("#page").width() - scrollBarSize; screenH = $("#page").height() - scrollBarSize; // 1ページ目を読み込み pageNow = movePage(1); } // スクロール・バーのサイズを取得 function getScollBarSize() { // スクロール・バーのサイズを計算 var obj = $("#calcScrollBar").get(0); var doc = obj.contentWindow.document; var iframeHtml = $("#calcScrollBarIn").html() .replace(/<!--|-->/g, ""); // HTML用文字列取得 doc.write(iframeHtml); // iframeの中身を作成 var docEle = doc.documentElement; var scrollBarW = obj.width - docEle.clientWidth; var scrollBarH = obj.height - docEle.clientHeight; if (scrollBarW <= 8) scrollBarW = 8; // Androidバグ対策 if (scrollBarH <= 8) scrollBarH = 8; // Androidバグ対策 $("#calcScrollBar").hide(); // 計算が終わったので隠す return Math.max(scrollBarW, scrollBarH); // 大きい方を戻す } //================================================== // 「ページ移動」関数 function movePage(pageNo) { // エラー処理 if (pageNo < 1 | pageNo > pageMax) { // ページ範囲外なので、戻り値を戻して終了 return pageNow; } // 画像終了時のイベント処理用関数を設定 var obj = $("#pageImg").get(0); obj.onload = function() { // 画像サイズの初期化 imageW = this.width; imageH = this.height; // 倍率 フィットの計算 var rateW = screenW / imageW; var rateH = screenH / imageH; rateFit = (rateW < rateH) ? rateW : rateH; // 倍率 現在の設定と、倍率の変更 rateNow = changeRate(rateFit); }; // 画像の読み込み obj.src = "page/" + pageNo + ".gif"; // タイトルの表示 document.title = "[" + pageNo + "/" + pageMax + "] " + bookTitle; // 戻り値を戻して終了 return pageNo; } //================================================== // 「倍率変更」関数 function changeRate(rateNo) { // エラー対策(10%を最小値にする) if (rateNo <= 0.1 * rateFit) { rateNo += rateStep * rateFit; // 縮小を元に戻す } // 画像サイズの初期化 var newW = Math.floor(imageW * rateNo); var newH = Math.floor(imageH * rateNo); // 画像表示位置の調整 var newLeft = Math.floor((screenW - newW) / 2); var newTop = Math.floor((screenH - newH) / 2); // 画像サイズと表示位置の変更 var obj = $("#pageImg"); obj.width(newW); obj.height(newH); obj.css("left", newLeft + "px"); obj.css("top", newTop + "px"); // 戻り値を戻して終了 return rateNo; } //================================================== // 「最後」「次に」「前に」「最初」関数 function lastPage() {pageNow = movePage(pageMax);} function nextPage() {pageNow = movePage(pageNow + 1);} function backPage() {pageNow = movePage(pageNow - 1);} function firstPage() {pageNow = movePage(1);} //================================================== // 「拡大」「フィット」「縮小」関数 function zoomIn() {rateNow = changeRate(rateNow + rateStep * rateFit);} function fit() {rateNow = changeRate(rateFit);} function zoomOut() {rateNow = changeRate(rateNow - rateStep * rateFit);} //================================================== // マウス位置の取得(環境依存分岐) function getMousePos(e) { var obj = new Object(); if(e) { obj.x = e.pageX; obj.y = e.pageY; } else { obj.x = event.x + document.body.scrollLeft; obj.y = event.y + document.body.scrollTop; } return obj; } // スマートフォンのタッチイベントからマウス用イベントを生成 function emulateMouse() { event.preventDefault(); return event.touches[0]; // マルチタッチの1番目 } // ドラッグ用変数 var offsetX = 0; // 移動開始時のオフセットX var offsetY = 0; // 移動開始時のオフセットY var dragFlag = false; // ドラッグ中かどうかのフラグ変数 // ドラッグ開始 function dragStart(ele, arg) { var obj = getMousePos(arg); offsetX = obj.x - $(ele).position().left; offsetY = obj.y - $(ele).position().top; dragFlag = true; } // ドラッグ中 function dragMove(ele, arg) { if (! dragFlag) return; var obj = getMousePos(arg); $(ele).css("left", (obj.x - offsetX) + "px"); $(ele).css("top", (obj.y - offsetY) + "px"); } // ドラッグ終了 function dragEnd(ele, arg) { dragFlag = false; } // --> </script> </head> <body onLoad="myLoad();"> <!-- ツール領域 --> <div id="tools"><nobr> <input type="button" value="<<" onClick="lastPage();" ><input type="button" value="<" onClick="nextPage();" ><input type="button" value=">" onClick="backPage();" ><input type="button" value=">>" onClick="firstPage();" ><input type="button" value="□+" onClick="zoomIn();" ><input type="button" value="□□" onClick="fit();" ><input type="button" value="□-" onClick="zoomOut();"> </nobr></div> <!-- ページ領域 --> <div id="page"> <img src="page/dumy.gif" id="pageImg" onMouseDown="dragStart(this, arguments[0]); return false;" onMouseMove="dragMove (this, arguments[0]); return false;" onMouseUp=" dragEnd (this, arguments[0]);" onTouchStart="dragStart(this, emulateMouse());" onTouchMove=" dragMove (this, emulateMouse());" onTouchEnd=" dragEnd (this, emulateMouse());" > </div> <!-- スクロール・バーのサイズを計算するためのHTML --> <iframe id="calcScrollBar" frameborder=0 width=100 height=100></iframe> <div id="calcScrollBarIn"><!-- <!DOCTYPE html> <html> <head> <style type="text/css"> html,body,div { margin: 0; padding: 0; } #box { width: 200px; height: 200px; } </style> </head> <body> <div id="box"></div> </body> </html> --></div> </body> </html>
以下、プログラマ向けの技術的な解説を行っておきます。
CSSの中に、以下のような行があります。
width: expression(document.body.clientWidth / 7); /* for IE */
この「expression(~)」というのは、IEだけで動作する設定です。内部がJavaScriptとして解釈されて実行されます。ブラウザ間の表示の違いを吸収させるために設定しています。
次に「IEでブラウザ起動直後に値が取得できないバグ対策」です。ブラウザ起動とともにファイルを読み込んだ際、IEでは、onLoad直後にwidthとheightの値が0になってしまうというバグがあったので、下記のように、遅延処理させています。通常はリンクからたどるか、ブラウザを開いた状態でブックマークから閲覧すると思うので、あまり遭遇しない挙動だと思います。
以下のように、正常に値が取れない際は、100mscecずつ待って、初期化処理を行うようにしています。
// IEでブラウザ起動直後に値が取得できないバグ対策 if ($("#page").width() == 0 || $("#page").height() == 0) { window.setTimeout(myLoad, 100); return; }
次の「スクロール・バーのサイズを取得」では、自動でスクロール・バーのサイズを計算して、幅を取得するようにしています。
「iframeHtml = $("#calcScrollBarIn").html().replace(/<!--|-->/g, ""); doc.write(iframeHtml);」という部分は、ID「calcScrollBarIn」の中身をドキュメントから取得して、iframeに流し込むための処理です。外部のHTMLファイルを用意するのが面倒でしたので、同じファイル内に記述しました。
さて、実はこの「スクロール・バーのサイズ取得」関数は、IEやChromeなど、PC向けのブラウザでは正しく動作しますが、肝心のAndroidでは正しく動作しませんでした。原因は、「obj.width」「obj.height」の値が、PCのブラウザのように、スクロール・バーの内側のサイズで取得できないためでした。
Androidでは、そのまま200ピクセルと戻ってきて、スクロール・バーの内側のサイズにはなりませんでした。そのため、Androidでは8ドット決め打ちで、スクロール・バーのサイズを戻すようにしています。
// スクロール・バーのサイズを取得 function getScollBarSize() { // スクロール・バーのサイズを計算 var obj = $("#calcScrollBar").get(0); var doc = obj.contentWindow.document; var iframeHtml = $("#calcScrollBarIn").html() .replace(/<!--|-->/g, ""); // HTML用文字列取得 doc.write(iframeHtml); // iframeの中身を作成 var docEle = doc.documentElement; var scrollBarW = obj.width - docEle.clientWidth; var scrollBarH = obj.height - docEle.clientHeight; if (scrollBarW <= 8) scrollBarW = 8; // Androidバグ対策 if (scrollBarH <= 8) scrollBarH = 8; // Androidバグ対策 $("#calcScrollBar").hide(); // 計算が終わったので隠す return Math.max(scrollBarW, scrollBarH); // 大きい方を戻す }
最後は、スマートフォン向けの処理です。
スマートフォンでは、画面内のオブジェクトに触れた場合、「MouseDown」「MouseUp」といったイベントではなく、「TouchStart」「TouchEnd」といったイベントが発生します。
そのため、これらのスマートフォン向けのイベントを、PCのブラウザのイベントにバイパスするための仕掛けを用意しています。
以下、その部分のソースコードの抜粋です。
// スマートフォンのタッチイベントからマウス用イベントを生成 function emulateMouse() { event.preventDefault(); return event.touches[0]; // マルチタッチの1番目 }
<div id="page"> <img src="page/dumy.gif" id="pageImg" onMouseDown="dragStart(this, arguments[0]); return false;" onMouseMove="dragMove (this, arguments[0]); return false;" onMouseUp=" dragEnd (this, arguments[0]);" onTouchStart="dragStart(this, emulateMouse());" onTouchMove=" dragMove (this, emulateMouse());" onTouchEnd=" dragEnd (this, emulateMouse());" > </div>
というわけで、350行程度の小さなアプリケーションですが、いろいろな技術を詰め込んでみました。参考になりましたでしょうか?