[Android note.] PhoneGap 初學筆記

自從買了 HTC Desire 之後,我整天都在玩 Angry Bird 呢(揍飛)。

對於 Java 不熟的人,在開發 Android App 上自然苦手了點,不過,市面上就出現了這種不太需要接觸 Java 那方面的套件出現。PhoneGap 是一種,另外還有 Titanium Mobile 爾等。目前就這兩個比較火紅一點,所以就把玩了這兩套 framework。就功能性來說,TM 畢竟是修改自原生的 SDK,所以能做的事情比較多。反觀 PG 就沒有那麼多東西可以玩了(除非你自己刻 Java 丟進去作成 plugin 就另當別論)。

為什麼要介紹 PG 呢?因為 TM 有點讓人苦惱的是,你用他的 framework 他會傳一份資料回他的主機,而且,他打包出來的 apk 檔出奇的大(光是 hello world 就快 2MB 是哪招),所以,以輕便為主的話,還是以 PhoneGap 為開發主力吧。

首先,需要準備的東西有:請參考 PhoneGap Get Started 說明(揍飛)。手邊如果有 Android 手機的話最好,因為有些東西在模擬器上面跑起來就是會有問題,而且這個問題還無解。因為系統不會跟你說有啥錯誤,只是程式就是無法動作而已(特別是相機之類的)。所以,能弄到 Android 週邊是最好的。

基於 PhoneGap 的開發方式,因為全部都是以 HTML+CSS+Javascript 開發,所以,你還是需要一套像是 jQuery 之類的工具來協助。當然,你要直接使用 jQuery 也是可以的,只不過 jQuery 比較粗勇一點,所以找一套輕量化的 Javascript Framework 也是不錯的選擇。

官方的建議是使用 XUI 來做,而我個人則偏向於 Zepto 這一套。各有利弊,好處是 Zepto 跟 jQuery 比較相近,XUI 則是非常輕量化(意味著許多 jQuery 功能你要自己刻)。當然,個人是覺得,對於原生的 Javascript 有一點概念再來用這幾套會比較好一點。如果對 DOM 操作不熟,還是回去用 jQuery 可能會比較好,起碼不用刻很多所謂的 raw code。

然後?並不是就開始做了,上述的工具,還少了一套 UI(除非你想自己刻)工具。回到上一個 Javascript framewrok,包含 UI Components 的,目前為止最有名的應該就是 jQuery Mobile 了,其他的呢?有,像是 UnifySencha Touch 等就有包含 UI Components。

只是,如果要用上那些包含 UI Components 的工具,當然你所要載入的東西就會爆炸的多!

這邊就以最近測試的拍照上傳的範例來簡單介紹,我將範例的 Zepto 修改為 jQuery Mobile 畫面會好看一些。畢竟如果要自己畫 UI 的話,這篇文章應該會沒完沒了吧(倒)。

<!DOCTYPE html>
<html class="ui-mobile landscape">
    <head>
        <title>LibookMyFilm</title>
        <link rel="stylesheet" href="jquery.mobile-1.0a3.min.css" />
        <style type="text/css">
            body {
                    font-size: 16px;
            }
            #preview_box {
                    position: relative;
                    width: 100px;
                    height: 100px;
                    margin: 0;
                    padding: 0;
                    border: 0;
                    overflow: hidden;
            }
            #preview, #film_mask {
                    position: absolute;
                    top: 0px;
                    left: 0px;
                    width: auto;
                    height: auto;
                    margin: 0;
                    border: 0;
                    padding: 0;
                    overflow: hidden;
                    background-color: transparent;
            }
            #film_mask > img {
                    opacity: 0.55;
            }
            #preview > img {
                    margin: 0 auto;
            }
        </style>
        <script type="text/javascript" charset="utf-8" src="phonegap.js"></script>
        <script type="text/javascript" charset="utf-8" src="jquery-1.5.min.js"></script>
        <script type="text/javascript" charset="utf-8" src="jquery.mobile-1.0a3.min.js"></script>
        <script type="text/javascript" charset="utf-8">
           function pasteImage(imgURI) {
               var imgURI = (imgURI!=null && imgURI != "") ? imgURI : "images/sample.jpg", preview = $('#preview');

               preview.empty();
               $('<img/>')
               .attr('src', imgURI)
               .appendTo(preview)
               .load(function() {
                   if($(this).height() >= $(this).width()) {
                           $(this).css({
                           'width':'auto',
                           'height': preview.parent().width(),
                           'transform': 'rotate(270deg)',
                           '-webkit-transform': 'rotate(270deg)'
                           });

                           var shift = Math.round(Math.abs($(this).width() - $(this).height()))/2;
                           preview.css({ width: $(this).height(), height: $(this).width() });
                           $(this).css({ 'margin-top': -shift, 'margin-left': shift });
                           if($(this).width() >= preview.parent().height()) {
                                    $(this).css({ width: preview.parent().height(), height: 'auto', 'margin-left': Math.abs($(this).height()-preview.parent().height())/2});
                           }
                   } else {
                           $(this).css({width: preview.parent().width(), height:'auto'});
                           if($(this).height() >= preview.parent().height()) {
                           $(this).css({height: preview.parent().height(), width:'auto'});
                           preview.css({left: (preview.parent().width() - preview.width())/2 });
                           }
                   }
               });
           }

           function onDeviceReady() {
               var box = $('#preview_box'), preview = $('#preview');

                   box.width(box.parent().width());
                   box.height((box.parent().width())*9/16);

                   $('#film_mask').css({
                   'background-image': '-webkit-gradient(radial, center center, 0, center center, '+box.height()*1.5+', from(transparent), to(#000000))'
                   });
                $('<img/>').attr('src', 'images/film_mask.png')
                   .css({
                   width: box.width(),
                   height: box.height()
                   }).appendTo($('#film_mask'));

               if(window.localStorage && localStorage.getItem("lastimage")) {
                    pasteImage(localStorage.getItem("lastimage"));
               } else {
                   pasteImage(null);
               }

               $('#take_photo').bind('click', function(evt) {
                   navigator.camera.getPicture(function(imageURI) {
                       if(window.localStorage) {
                           localStorage.setItem("lastimage", imageURI);
                       }
                       pasteImage(imageURI);
                   }, function(msg) {
                       alert("Failed message:"+msg);
                   }, { 
                       quality: 100,
                       destinationType: navigator.camera.DestinationType.FILE_URI
                   });
               });
               $('#upload_photo').bind('click', function(evt) {
                   alert('test');
               });
           }
           document.addEventListener("deviceready", onDeviceReady, false);
        </script>
    </head>
    <body>
        <div data-role="page" data-theme="b" id="home" class="ui-page ui-body-b ui-page-active">
            <div data-role="header" class="page-header">
                    <h1>MyFilm App</h1>
            </div>
            <div data-role="content" class="ui-content" role="main">
                <span>Take a shot and your photo will preview here.</span>
                <br />
                <div id="preview_box">
                        <div id="preview"></div>
                        <div id="film_mask"></div>
                </div>
            </div>
            <div data-role="button" data-theme="b" id="take_photo">
                <span>Take a Picture</span>
            </div>
            <div data-role="button" data-theme="a" id="upload_photo">
                <span>Upload Your Photo</span>
            </div>
            <div data-role="footer" class="page-footer">
                <span>Powered by PhoneGap.</span>
            </div>
        </div>
    </body>
</html>

畫面展示。

範例中的 Upload Your Photo 是沒有作用的,這裡分開來寫。將檔案上傳的動作,在 PhoneGap API Documents 中有說明,不過,由於我取回的 Camera.destinationType 是 FILE_URI,所以他會返回給我一個類似這種的路徑:

content://media/external/images/media/1234

這種方式。

本來我以為這樣的路徑是沒辦法使用 PhoneGap 的檔案上傳方法,不過,在測試過之後發現,其實他也可以將這樣的路徑方式所指定的檔案,上傳到遠端的主機上面的。具體實作的方式如下:

var options = new FileUploadOptions();
options.fileKey="image";
options.fileName="image.jpg";
options.mimeType="image/jpeg";
var ft = new FileTransfer();
ft.upload(imageURI, "http://some.server.com/upload.php", function(r) {
    alert('upload success.');
    return false;
}, function(e) {
    alert('upload error.');
    return false;
}, options);

那伺服器端怎麼接收?其實就跟我們一般在 Web 開發的時候一樣。上述的接收程式碼如下:

// the actual uploaded image
$uploaded_image = $_FILES['image']['tmp_name'];
// the uploaded image name
$uploaded_image_name = $_FILES['image']['name'];
// location where you want to save the image
$saved_image = "/tmp/".time().".jpg";
// saves the image
move_uploaded_file($uploaded_image, $saved_image);

大抵上就是這樣,扣掉現成的 UI 的部份,其實用 PhoneGap 做一些簡單應用是不錯的選擇。麻煩就是麻煩在 UI 了吧。如果把 jQuery Mobile 拿掉的話,我實在不知道自己刻一個畫面需要多久的時間。在盡量輕量化與方便性之間,可能得做個取捨吧。

範例中,有使用到 HTML5 的 localStorage,目的是要將使用者操作相機過後的資訊儲存起來。而,PhoneGap 本身也能使用 Web Database 的 HTML5 的特性,這一點就留給大家去實驗了。

另外附上 Eclipse 關於這個專案的檔案清單。

Hina Chen
偏執與強迫症的患者,算不上是無可救藥,只是我已經遇上我的良醫了。
Taipei