PhoneGap:移动APP之跨平台解决方案

移动开发
最近看了一本书《Building Android Apps with HTML CSS and JavaScript》,顾名思义就是用开发web的方式来开发Android的APP,其中倒不是web的开发技术最吸我,而是这样的一种解决方案。像 我们现在的手持设备种类这么多,主流的不外乎Android,Iphone,Ipad等等,如果要对每一种平台都开发一个相应的APP版本,代价太大。而 基于浏览器的web app就容易解决这个问题,只需开发一次,便可以通用部署。

上面即是使用web技术来开发本地iphone app的一幅运行图,关于web的开发技术,这里就不多说了,这里重点提及Phonegap,利用它我们就可以把web app转换成各种移动平台的APP。

PhoneGap is an HTML5 app platform that allows you to author native applications with web technologies and get access to APIs and app stores. PhoneGap leverages web technologies developers already know best… HTML and JavaScript

这里简单介绍一下PhoneGap,它对各个移动平台的API进行了一次封装,屏蔽了每种移动平台的具体细节,而设计了一个公共的API层,对开发者来说,只要调用统一的API来进行开发就可以了。

把PhoneGap的源代码下载来通读了一下,了解了一下背后的实现原理,下面以Camera的功能为例来进行说明。

API 开发文档请参照 http://docs.phonegap.com/en/1.1.0/index.html,其中有关Camera的开发API请参照http://docs.phonegap.com/en/1.1.0/phonegap_camera_camera.md.html#Camera,我们的代码就可以写成

  1. navigator.camera.getPicture(onSuccess, onFail, { quality: 50 }); 
  2. function onSuccess(imageData) {     
  3.   var image = document.getElementById('myImage');     
  4.   image.src = "data:image/jpeg;base64," + imageData; 
  5. function onFail(message) {     
  6.   alert('Failed because: ' + message); 

是不是很简单,具体后台平台的差异我们根本不需要来进行区别,全部由PhoneGap来给处理了,那么它是怎么处理的呢,继续往下看。

先来看看JS端的API实现,它主要是定义了web层开发的接口,也即我们上面例子中的camera.getPicture 函数

  1. /** 
  2.  * Gets a picture from source defined by "options.sourceType", and returns the 
  3.  * image as defined by the "options.destinationType" option. 
  4.  
  5.  * The defaults are sourceType=CAMERA and destinationType=DATA_URL. 
  6.  * 
  7.  * @param {Function} successCallback 
  8.  * @param {Function} errorCallback 
  9.  * @param {Object} options 
  10.  */ 
  11. Camera.prototype.getPicture = function(successCallback, errorCallback, options) { 
  12.  
  13.     // successCallback required 
  14.     if (typeof successCallback !== "function") { 
  15.         console.log("Camera Error: successCallback is not a function"); 
  16.         return
  17.     } 
  18.  
  19.     // errorCallback optional 
  20.     if (errorCallback && (typeof errorCallback !== "function")) { 
  21.         console.log("Camera Error: errorCallback is not a function"); 
  22.         return
  23.     } 
  24.  
  25.     if (options === null || typeof options === "undefined") { 
  26.         options = {}; 
  27.     } 
  28.     if (options.quality === null || typeof options.quality === "undefined") { 
  29.         options.quality = 80; 
  30.     } 
  31.     if (options.maxResolution === null || typeof options.maxResolution === "undefined") { 
  32.         options.maxResolution = 0; 
  33.     } 
  34.     if (options.destinationType === null || typeof options.destinationType === "undefined") { 
  35.         options.destinationType = Camera.DestinationType.DATA_URL; 
  36.     } 
  37.     if (options.sourceType === null || typeof options.sourceType === "undefined") { 
  38.         options.sourceType = Camera.PictureSourceType.CAMERA; 
  39.     } 
  40.     if (options.encodingType === null || typeof options.encodingType === "undefined") { 
  41.         options.encodingType = Camera.EncodingType.JPEG; 
  42.     } 
  43.     if (options.mediaType === null || typeof options.mediaType === "undefined") { 
  44.         options.mediaType = Camera.MediaType.PICTURE; 
  45.     } 
  46.     if (options.targetWidth === null || typeof options.targetWidth === "undefined") { 
  47.         options.targetWidth = -1; 
  48.     } 
  49.     else if (typeof options.targetWidth == "string") { 
  50.         var width = new Number(options.targetWidth); 
  51.         if (isNaN(width) === false) { 
  52.             options.targetWidth = width.valueOf(); 
  53.         } 
  54.     } 
  55.     if (options.targetHeight === null || typeof options.targetHeight === "undefined") { 
  56.         options.targetHeight = -1; 
  57.     } 
  58.     else if (typeof options.targetHeight == "string") { 
  59.         var height = new Number(options.targetHeight); 
  60.         if (isNaN(height) === false) { 
  61.             options.targetHeight = height.valueOf(); 
  62.         } 
  63.     } 
  64.  
  65.     PhoneGap.exec(successCallback, errorCallback, "Camera""takePicture", [options]); 
  66. }; 
  67.  
  68. 上段代码中的最后一句就是呼叫本地的Camera模块的takePicture函数,具体的PhoneGap.exec实现代码如下: 
  69.  
  70. /** 
  71.  * Execute a PhoneGap command.  It is up to the native side whether this action is synch or async. 
  72.  * The native side can return: 
  73.  *      Synchronous: PluginResult object as a JSON string 
  74.  *      Asynchrounous: Empty string "" 
  75.  * If async, the native side will PhoneGap.callbackSuccess or PhoneGap.callbackError, 
  76.  * depending upon the result of the action. 
  77.  * 
  78.  * @param {Function} success    The success callback 
  79.  * @param {Function} fail       The fail callback 
  80.  * @param {String} service      The name of the service to use 
  81.  * @param {String} action       Action to be run in PhoneGap 
  82.  * @param {Array.<String>} [args]     Zero or more arguments to pass to the method 
  83.  */ 
  84. PhoneGap.exec = function(success, fail, service, action, args) { 
  85.     try { 
  86.         var callbackId = service + PhoneGap.callbackId++; 
  87.         if (success || fail) { 
  88.             PhoneGap.callbacks[callbackId] = {success:success, fail:fail}; 
  89.         } 
  90.  
  91.         var r = prompt(PhoneGap.stringify(args), "gap:"+PhoneGap.stringify([service, action, callbackId, true])); 
  92.  
  93.         // If a result was returned 
  94.         if (r.length > 0) { 
  95.             eval("var v="+r+";"); 
  96.  
  97.             // If status is OK, then return value back to caller 
  98.             if (v.status === PhoneGap.callbackStatus.OK) { 
  99.  
  100.                 // If there is a success callback, then call it now with 
  101.                 // returned value 
  102.                 if (success) { 
  103.                     try { 
  104.                         success(v.message); 
  105.                     } catch (e) { 
  106.                         console.log("Error in success callback: " + callbackId  + " = " + e); 
  107.                     } 
  108.  
  109.                     // Clear callback if not expecting any more results 
  110.                     if (!v.keepCallback) { 
  111.                         delete PhoneGap.callbacks[callbackId]; 
  112.                     } 
  113.                 } 
  114.                 return v.message; 
  115.             } 
  116.  
  117.             // If no result 
  118.             else if (v.status === PhoneGap.callbackStatus.NO_RESULT) { 
  119.  
  120.                 // Clear callback if not expecting any more results 
  121.                 if (!v.keepCallback) { 
  122.                     delete PhoneGap.callbacks[callbackId]; 
  123.                 } 
  124.             } 
  125.  
  126.             // If error, then display error 
  127.             else { 
  128.                 console.log("Error: Status="+v.status+" Message="+v.message); 
  129.  
  130.                 // If there is a fail callback, then call it now with returned value 
  131.                 if (fail) { 
  132.                     try { 
  133.                         fail(v.message); 
  134.                     } 
  135.                     catch (e1) { 
  136.                         console.log("Error in error callback: "+callbackId+" = "+e1); 
  137.                     } 
  138.  
  139.                     // Clear callback if not expecting any more results 
  140.                     if (!v.keepCallback) { 
  141.                         delete PhoneGap.callbacks[callbackId]; 
  142.                     } 
  143.                 } 
  144.                 return null
  145.             } 
  146.         } 
  147.     } catch (e2) { 
  148.         console.log("Error: "+e2); 
  149.     } 
  150. }; 

至此为止,web端js的代码工作就完毕了,程序流程转移到了本地代码中,我们以Android的本地代码为例,看看是怎么具体实现的。

  1. package com.phonegap; 
  2.  
  3. import android.content.ContentResolver; 
  4. import android.content.ContentValues; 
  5. import android.content.Intent; 
  6. import android.database.Cursor; 
  7. import android.graphics.Bitmap; 
  8. import android.graphics.Bitmap.CompressFormat; 
  9. import android.graphics.BitmapFactory; 
  10. import android.net.Uri; 
  11. import android.provider.MediaStore.Images.Media; 
  12. import com.phonegap.api.PhonegapActivity; 
  13. import com.phonegap.api.Plugin; 
  14. import com.phonegap.api.PluginResult; 
  15. import com.phonegap.api.PluginResult.Status; 
  16. import java.io.ByteArrayOutputStream; 
  17. import java.io.File; 
  18. import java.io.FileNotFoundException; 
  19. import java.io.FileOutputStream; 
  20. import java.io.IOException; 
  21. import java.io.OutputStream; 
  22. import java.io.PrintStream; 
  23. import org.apache.commons.codec.binary.Base64; 
  24. import org.json.JSONArray; 
  25. import org.json.JSONException; 
  26. import org.json.JSONObject; 
  27.  
  28. public class CameraLauncher extends Plugin 
  29.   private static final int DATA_URL = 0; 
  30.   private static final int FILE_URI = 1; 
  31.   private static final int PHOTOLIBRARY = 0; 
  32.   private static final int CAMERA = 1; 
  33.   private static final int SAVEDPHOTOALBUM = 2; 
  34.   private static final int PICTURE = 0; 
  35.   private static final int VIDEO = 1; 
  36.   private static final int ALLMEDIA = 2; 
  37.   private static final int JPEG = 0; 
  38.   private static final int PNG = 1; 
  39.   private static final String GET_PICTURE = "Get Picture"
  40.   private static final String GET_VIDEO = "Get Video"
  41.   private static final String GET_All = "Get All"
  42.   private static final String LOG_TAG = "CameraLauncher"
  43.   private int mQuality; 
  44.   private int targetWidth; 
  45.   private int targetHeight; 
  46.   private Uri imageUri; 
  47.   private int encodingType; 
  48.   private int mediaType; 
  49.   public String callbackId; 
  50.   private int numPics; 
  51.  
  52.   public PluginResult execute(String action, JSONArray args, String callbackId) 
  53.   { 
  54.     PluginResult.Status status = PluginResult.Status.OK; 
  55.     String result = ""
  56.     this.callbackId = callbackId; 
  57.     try 
  58.     { 
  59.       if (action.equals("takePicture")) { 
  60.         int srcType = 1; 
  61.         int destType = 0; 
  62.         this.targetHeight = 0; 
  63.         this.targetWidth = 0; 
  64.         this.encodingType = 0; 
  65.         this.mediaType = 0; 
  66.         this.mQuality = 80; 
  67.  
  68.         JSONObject options = args.optJSONObject(0); 
  69.         if (options != null) { 
  70.           srcType = options.getInt("sourceType"); 
  71.           destType = options.getInt("destinationType"); 
  72.           this.targetHeight = options.getInt("targetHeight"); 
  73.           this.targetWidth = options.getInt("targetWidth"); 
  74.           this.encodingType = options.getInt("encodingType"); 
  75.           this.mediaType = options.getInt("mediaType"); 
  76.           this.mQuality = options.getInt("quality"); 
  77.         } 
  78.  
  79.         if (srcType == 1) { 
  80.           takePicture(destType, this.encodingType); 
  81.         } 
  82.         else if ((srcType == 0) || (srcType == 2)) { 
  83.           getImage(srcType, destType); 
  84.         } 
  85.         PluginResult r = new PluginResult(PluginResult.Status.NO_RESULT); 
  86.         r.setKeepCallback(true); 
  87.         return r; 
  88.       } 
  89.       return new PluginResult(status, result); 
  90.     } catch (JSONException e) { 
  91.       e.printStackTrace(); 
  92.     }return new PluginResult(PluginResult.Status.JSON_EXCEPTION); 
  93.   } 
  94.  
  95.   public void takePicture(int returnType, int encodingType) 
  96.   { 
  97.     this.numPics = queryImgDB().getCount(); 
  98.  
  99.     Intent intent = new Intent("android.media.action.IMAGE_CAPTURE"); 
  100.  
  101.     File photo = createCaptureFile(encodingType); 
  102.     intent.putExtra("output", Uri.fromFile(photo)); 
  103.     this.imageUri = Uri.fromFile(photo); 
  104.  
  105.     this.ctx.startActivityForResult(this, intent, 32 + returnType + 1); 
  106.   } 
  107.  
  108.   private File createCaptureFile(int encodingType) 
  109.   { 
  110.     File photo = null
  111.     if (encodingType == 0) 
  112.       photo = new File(DirectoryManager.getTempDirectoryPath(this.ctx), "Pic.jpg"); 
  113.     else { 
  114.       photo = new File(DirectoryManager.getTempDirectoryPath(this.ctx), "Pic.png"); 
  115.     } 
  116.     return photo; 
  117.   } 
  118.  
  119.   public void getImage(int srcType, int returnType) 
  120.   { 
  121.     Intent intent = new Intent(); 
  122.     String title = "Get Picture"
  123.     if (this.mediaType == 0) { 
  124.       intent.setType("image/*"); 
  125.     } 
  126.     else if (this.mediaType == 1) { 
  127.       intent.setType("video/*"); 
  128.       title = "Get Video"
  129.     } 
  130.     else if (this.mediaType == 2) 
  131.     { 
  132.       intent.setType("*/*"); 
  133.       title = "Get All"
  134.     } 
  135.  
  136.     intent.setAction("android.intent.action.GET_CONTENT"); 
  137.     intent.addCategory("android.intent.category.OPENABLE"); 
  138.     this.ctx.startActivityForResult(this, Intent.createChooser(intent, new String(title)), (srcType + 1) * 16 + returnType + 1); 
  139.   } 
  140.  
  141.   public Bitmap scaleBitmap(Bitmap bitmap) 
  142.   { 
  143.     int newWidth = this.targetWidth; 
  144.     int newHeight = this.targetHeight; 
  145.     int origWidth = bitmap.getWidth(); 
  146.     int origHeight = bitmap.getHeight(); 
  147.  
  148.     if ((newWidth <= 0) && (newHeight <= 0)) { 
  149.       return bitmap; 
  150.     } 
  151.  
  152.     if ((newWidth > 0) && (newHeight <= 0)) { 
  153.       newHeight = newWidth * origHeight / origWidth; 
  154.     } 
  155.     else if ((newWidth <= 0) && (newHeight > 0)) { 
  156.       newWidth = newHeight * origWidth / origHeight; 
  157.     } 
  158.     else 
  159.     { 
  160.       double newRatio = newWidth / newHeight; 
  161.       double origRatio = origWidth / origHeight; 
  162.  
  163.       if (origRatio > newRatio) 
  164.         newHeight = newWidth * origHeight / origWidth; 
  165.       else if (origRatio < newRatio) { 
  166.         newWidth = newHeight * origWidth / origHeight; 
  167.       } 
  168.     } 
  169.  
  170.     return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true); 
  171.   } 
  172.  
  173.   public void onActivityResult(int requestCode, int resultCode, Intent intent) 
  174.   { 
  175.     int srcType = requestCode / 16 - 1; 
  176.     int destType = requestCode % 16 - 1; 
  177.  
  178.     if (srcType == 1) 
  179.     { 
  180.       if (resultCode == -1) { 
  181.         try 
  182.         { 
  183.           ExifHelper exif = new ExifHelper(); 
  184.           if (this.encodingType == 0) { 
  185.             exif.createInFile(DirectoryManager.getTempDirectoryPath(this.ctx) + "/Pic.jpg"); 
  186.             exif.readExifData(); 
  187.           } 
  188.  
  189.           try 
  190.           { 
  191.             bitmap = MediaStore.Images.Media.getBitmap(this.ctx.getContentResolver(), this.imageUri); 
  192.           } catch (FileNotFoundException e) { 
  193.             Uri uri = intent.getData(); 
  194.             ContentResolver resolver = this.ctx.getContentResolver(); 
  195.             bitmap = BitmapFactory.decodeStream(resolver.openInputStream(uri)); 
  196.           } 
  197.  
  198.           Bitmap bitmap = scaleBitmap(bitmap); 
  199.  
  200.           if (destType == 0) { 
  201.             processPicture(bitmap); 
  202.             checkForDuplicateImage(0); 
  203.           } 
  204.           else if (destType == 1) 
  205.           { 
  206.             ContentValues values = new ContentValues(); 
  207.             values.put("mime_type""image/jpeg"); 
  208.             Uri uri = null
  209.             try { 
  210.               uri = this.ctx.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); 
  211.             } catch (UnsupportedOperationException e) { 
  212.               System.out.println("Can't write to external media storage."); 
  213.               try { 
  214.                 uri = this.ctx.getContentResolver().insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, values); 
  215.               } catch (UnsupportedOperationException ex) { 
  216.                 System.out.println("Can't write to internal media storage."); 
  217.                 failPicture("Error capturing image - no media storage found."); 
  218.                 return
  219.               } 
  220.  
  221.             } 
  222.  
  223.             OutputStream os = this.ctx.getContentResolver().openOutputStream(uri); 
  224.             bitmap.compress(Bitmap.CompressFormat.JPEG, this.mQuality, os); 
  225.             os.close(); 
  226.  
  227.             if (this.encodingType == 0) { 
  228.               exif.createOutFile(FileUtils.getRealPathFromURI(uri, this.ctx)); 
  229.               exif.writeExifData(); 
  230.             } 
  231.  
  232.             success(new PluginResult(PluginResult.Status.OK, uri.toString()), this.callbackId); 
  233.           } 
  234.           bitmap.recycle(); 
  235.           bitmap = null
  236.           System.gc(); 
  237.  
  238.           checkForDuplicateImage(1); 
  239.         } catch (IOException e) { 
  240.           e.printStackTrace(); 
  241.           failPicture("Error capturing image."); 
  242.         } 
  243.  
  244.       } 
  245.       else if (resultCode == 0) { 
  246.         failPicture("Camera cancelled."); 
  247.       } 
  248.       else 
  249.       { 
  250.         failPicture("Did not complete!"); 
  251.       } 
  252.  
  253.     } 
  254.     else if ((srcType == 0) || (srcType == 2)) 
  255.       if (resultCode == -1) { 
  256.         Uri uri = intent.getData(); 
  257.         ContentResolver resolver = this.ctx.getContentResolver(); 
  258.  
  259.         if (this.mediaType != 0) { 
  260.           success(new PluginResult(PluginResult.Status.OK, uri.toString()), this.callbackId); 
  261.         } 
  262.         else if (destType == 0) { 
  263.           try { 
  264.             Bitmap bitmap = BitmapFactory.decodeStream(resolver.openInputStream(uri)); 
  265.             bitmap = scaleBitmap(bitmap); 
  266.             processPicture(bitmap); 
  267.             bitmap.recycle(); 
  268.             bitmap = null
  269.             System.gc(); 
  270.           } catch (FileNotFoundException e) { 
  271.             e.printStackTrace(); 
  272.             failPicture("Error retrieving image."); 
  273.           } 
  274.  
  275.         } 
  276.         else if (destType == 1) 
  277.         { 
  278.           if ((this.targetHeight > 0) && (this.targetWidth > 0)) { 
  279.             try { 
  280.               Bitmap bitmap = BitmapFactory.decodeStream(resolver.openInputStream(uri)); 
  281.               bitmap = scaleBitmap(bitmap); 
  282.  
  283.               String fileName = DirectoryManager.getTempDirectoryPath(this.ctx) + "/resize.jpg"
  284.               OutputStream os = new FileOutputStream(fileName); 
  285.               bitmap.compress(Bitmap.CompressFormat.JPEG, this.mQuality, os); 
  286.               os.close(); 
  287.  
  288.               bitmap.recycle(); 
  289.               bitmap = null
  290.  
  291.               success(new PluginResult(PluginResult.Status.OK, "file://" + fileName), this.callbackId); 
  292.               System.gc(); 
  293.             } catch (Exception e) { 
  294.               e.printStackTrace(); 
  295.               failPicture("Error retrieving image."); 
  296.             } 
  297.           } 
  298.           else { 
  299.             success(new PluginResult(PluginResult.Status.OK, uri.toString()), this.callbackId); 
  300.           } 
  301.         } 
  302.  
  303.       } 
  304.       else if (resultCode == 0) { 
  305.         failPicture("Selection cancelled."); 
  306.       } 
  307.       else { 
  308.         failPicture("Selection did not complete!"); 
  309.       } 
  310.   } 
  311.  
  312.   private Cursor queryImgDB() 
  313.   { 
  314.     return this.ctx.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[] { "_id" }, nullnullnull); 
  315.   } 
  316.  
  317.   private void checkForDuplicateImage(int type) 
  318.   { 
  319.     int diff = 1; 
  320.     Cursor cursor = queryImgDB(); 
  321.     int currentNumOfImages = cursor.getCount(); 
  322.  
  323.     if (type == 1) { 
  324.       diff = 2; 
  325.     } 
  326.  
  327.     if (currentNumOfImages - this.numPics == diff) { 
  328.       cursor.moveToLast(); 
  329.       int id = Integer.valueOf(cursor.getString(cursor.getColumnIndex("_id"))).intValue() - 1; 
  330.       Uri uri = Uri.parse(MediaStore.Images.Media.EXTERNAL_CONTENT_URI + "/" + id); 
  331.       this.ctx.getContentResolver().delete(uri, nullnull); 
  332.     } 
  333.   } 
  334.  
  335.   public void processPicture(Bitmap bitmap) 
  336.   { 
  337.     ByteArrayOutputStream jpeg_data = new ByteArrayOutputStream(); 
  338.     try { 
  339.       if (bitmap.compress(Bitmap.CompressFormat.JPEG, this.mQuality, jpeg_data)) { 
  340.         byte[] code = jpeg_data.toByteArray(); 
  341.         byte[] output = Base64.encodeBase64(code); 
  342.         String js_out = new String(output); 
  343.         success(new PluginResult(PluginResult.Status.OK, js_out), this.callbackId); 
  344.         js_out = null
  345.         output = null
  346.         code = null
  347.       } 
  348.     } 
  349.     catch (Exception e) { 
  350.       failPicture("Error compressing image."); 
  351.     } 
  352.     jpeg_data = null
  353.   } 
  354.  
  355.   public void failPicture(String err) 
  356.   { 
  357.     error(new PluginResult(PluginResult.Status.ERROR, err), this.callbackId); 
  358.   } 

可以看到Camera本身被设计一个插件的形式,实现了具体的takePicture函数,至于具体的js端代码和Android本地端代码的通信 是怎么样建立起来的,也即js代码怎么和Android的Activity的代码进行数据的交互,那么下面的代码会给你答案了。

  1. public class GapViewClient extends WebViewClient 
  2.   { 
  3.     DroidGap ctx; 
  4.  
  5.     public GapViewClient(DroidGap ctx) 
  6.     { 
  7.       this.ctx = ctx; 
  8.     } 
  9.  
  10.     public boolean shouldOverrideUrlLoading(WebView view, String url) 
  11.     { 
  12.       if (!this.ctx.pluginManager.onOverrideUrlLoading(url)) 
  13.       { 
  14.         if (url.startsWith("tel:")) { 
  15.           try { 
  16.             Intent intent = new Intent("android.intent.action.DIAL"); 
  17.             intent.setData(Uri.parse(url)); 
  18.             DroidGap.this.startActivity(intent); 
  19.           } catch (ActivityNotFoundException e) { 
  20.             System.out.println("Error dialing " + url + ": " + e.toString()); 
  21.           } 
  22.  
  23.         } 
  24.         else if (url.startsWith("geo:")) { 
  25.           try { 
  26.             Intent intent = new Intent("android.intent.action.VIEW"); 
  27.             intent.setData(Uri.parse(url)); 
  28.             DroidGap.this.startActivity(intent); 
  29.           } catch (ActivityNotFoundException e) { 
  30.             System.out.println("Error showing map " + url + ": " + e.toString()); 
  31.           } 
  32.  
  33.         } 
  34.         else if (url.startsWith("mailto:")) { 
  35.           try { 
  36.             Intent intent = new Intent("android.intent.action.VIEW"); 
  37.             intent.setData(Uri.parse(url)); 
  38.             DroidGap.this.startActivity(intent); 
  39.           } catch (ActivityNotFoundException e) { 
  40.             System.out.println("Error sending email " + url + ": " + e.toString()); 
  41.           } 
  42.  
  43.         } 
  44.         else if (url.startsWith("sms:")) { 
  45.           try { 
  46.             Intent intent = new Intent("android.intent.action.VIEW"); 
  47.  
  48.             String address = null
  49.             int parmIndex = url.indexOf('?'); 
  50.             if (parmIndex == -1) { 
  51.               address = url.substring(4); 
  52.             } 
  53.             else { 
  54.               address = url.substring(4, parmIndex); 
  55.  
  56.               Uri uri = Uri.parse(url); 
  57.               String query = uri.getQuery(); 
  58.               if ((query != null) && 
  59.                 (query.startsWith("body="))) { 
  60.                 intent.putExtra("sms_body", query.substring(5)); 
  61.               } 
  62.             } 
  63.  
  64.             intent.setData(Uri.parse("sms:" + address)); 
  65.             intent.putExtra("address", address); 
  66.             intent.setType("vnd.android-dir/mms-sms"); 
  67.             DroidGap.this.startActivity(intent); 
  68.           } catch (ActivityNotFoundException e) { 
  69.             System.out.println("Error sending sms " + url + ":" + e.toString()); 
  70.           } 
  71.  
  72.         } 
  73.         else if ((this.ctx.loadInWebView) || (url.startsWith("file://")) || (url.indexOf(this.ctx.baseUrl) == 0) || (DroidGap.this.isUrlWhiteListed(url) != 0)) { 
  74.           try 
  75.           { 
  76.             HashMap params = new HashMap(); 
  77.             this.ctx.showWebPage(url, truefalse, params); 
  78.           } catch (ActivityNotFoundException e) { 
  79.             System.out.println("Error loading url into DroidGap - " + url + ":" + e.toString()); 
  80.           } 
  81.  
  82.         } 
  83.         else { 
  84.           try 
  85.           { 
  86.             Intent intent = new Intent("android.intent.action.VIEW"); 
  87.             intent.setData(Uri.parse(url)); 
  88.             DroidGap.this.startActivity(intent); 
  89.           } catch (ActivityNotFoundException e) { 
  90.             System.out.println("Error loading url " + url + ":" + e.toString()); 
  91.           } 
  92.         } 
  93.       } 
  94.       return true
  95.     } 
  96.  
  97.     public void onPageFinished(WebView view, String url) 
  98.     { 
  99.       super.onPageFinished(view, url); 
  100.  
  101.       DroidGap.access$208(this.ctx); 
  102.  
  103.       if (!url.equals("about:blank")) { 
  104.         DroidGap.this.appView.loadUrl("javascript:try{ PhoneGap.onNativeReady.fire();}catch(e){_nativeReady = true;}"); 
  105.       } 
  106.  
  107.       Thread t = new Thread(new Runnable() { 
  108.         public void run() { 
  109.           try { 
  110.             Thread.sleep(2000L); 
  111.             DroidGap.GapViewClient.this.ctx.runOnUiThread(new Runnable() { 
  112.               public void run() { 
  113.                 DroidGap.this.appView.setVisibility(0); 
  114.                 DroidGap.GapViewClient.this.ctx.spinnerStop(); 
  115.               } 
  116.             }); 
  117.           } 
  118.           catch (InterruptedException e) 
  119.           { 
  120.           } 
  121.         } 
  122.       }); 
  123.       t.start(); 
  124.  
  125.       if (this.ctx.clearHistory) { 
  126.         this.ctx.clearHistory = false
  127.         this.ctx.appView.clearHistory(); 
  128.       } 
  129.  
  130.       if (url.equals("about:blank")) { 
  131.         if (this.ctx.callbackServer != null) { 
  132.           this.ctx.callbackServer.destroy(); 
  133.         } 
  134.         this.ctx.finish(); 
  135.       } 
  136.     } 
  137.  
  138.     public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) 
  139.     { 
  140.       System.out.println("onReceivedError: Error code=" + errorCode + " Description=" + description + " URL=" + failingUrl); 
  141.  
  142.       DroidGap.access$208(this.ctx); 
  143.  
  144.       this.ctx.spinnerStop(); 
  145.  
  146.       this.ctx.onReceivedError(errorCode, description, failingUrl); 
  147.     } 
  148.  
  149.     public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) 
  150.     { 
  151.       String packageName = this.ctx.getPackageName(); 
  152.       PackageManager pm = this.ctx.getPackageManager(); 
  153.       try 
  154.       { 
  155.         ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 128); 
  156.         if ((appInfo.flags & 0x2) != 0) 
  157.         { 
  158.           handler.proceed(); 
  159.           return
  160.         } 
  161.  
  162.         super.onReceivedSslError(view, handler, error); 
  163.       } 
  164.       catch (PackageManager.NameNotFoundException e) 
  165.       { 
  166.         super.onReceivedSslError(view, handler, error); 
  167.       } 
  168.     } 
  169.   } 
  170.  
  171.   public class GapClient extends WebChromeClient 
  172.   { 
  173.     private String TAG = "PhoneGapLog"
  174.     private long MAX_QUOTA = 104857600L; 
  175.     private DroidGap ctx; 
  176.  
  177.     public GapClient(Context ctx) 
  178.     { 
  179.       this.ctx = ((DroidGap)ctx); 
  180.     } 
  181.  
  182.     public boolean onJsAlert(WebView view, String url, String message, JsResult result) 
  183.     { 
  184.       AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx); 
  185.       dlg.setMessage(message); 
  186.       dlg.setTitle("Alert"); 
  187.       dlg.setCancelable(false); 
  188.       dlg.setPositiveButton(17039370, new DialogInterface.OnClickListener(result) 
  189.       { 
  190.         public void onClick(DialogInterface dialog, int which) { 
  191.           this.val$result.confirm(); 
  192.         } 
  193.       }); 
  194.       dlg.create(); 
  195.       dlg.show(); 
  196.       return true
  197.     } 
  198.  
  199.     public boolean onJsConfirm(WebView view, String url, String message, JsResult result) 
  200.     { 
  201.       AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx); 
  202.       dlg.setMessage(message); 
  203.       dlg.setTitle("Confirm"); 
  204.       dlg.setCancelable(false); 
  205.       dlg.setPositiveButton(17039370, new DialogInterface.OnClickListener(result) 
  206.       { 
  207.         public void onClick(DialogInterface dialog, int which) { 
  208.           this.val$result.confirm(); 
  209.         } 
  210.       }); 
  211.       dlg.setNegativeButton(17039360, new DialogInterface.OnClickListener(result) 
  212.       { 
  213.         public void onClick(DialogInterface dialog, int which) { 
  214.           this.val$result.cancel(); 
  215.         } 
  216.       }); 
  217.       dlg.create(); 
  218.       dlg.show(); 
  219.       return true
  220.     } 
  221.  
  222.     public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) 
  223.     { 
  224.       boolean reqOk = false
  225.       if ((url.indexOf(this.ctx.baseUrl) == 0) || (DroidGap.this.isUrlWhiteListed(url) != 0)) { 
  226.         reqOk = true
  227.       } 
  228.  
  229.       if ((reqOk) && (defaultValue != null) && (defaultValue.length() > 3) && (defaultValue.substring(0, 4).equals("gap:"))) 
  230.       { 
  231.         try { 
  232.           JSONArray array = new JSONArray(defaultValue.substring(4)); 
  233.           String service = array.getString(0); 
  234.           String action = array.getString(1); 
  235.           String callbackId = array.getString(2); 
  236.           boolean async = array.getBoolean(3); 
  237.           String r = DroidGap.this.pluginManager.exec(service, action, callbackId, message, async); 
  238.           result.confirm(r); 
  239.         } catch (JSONException e) { 
  240.           e.printStackTrace(); 
  241.         } 
  242.  
  243.       } 
  244.       else if ((reqOk) && (defaultValue != null) && (defaultValue.equals("gap_poll:"))) { 
  245.         String r = DroidGap.this.callbackServer.getJavascript(); 
  246.         result.confirm(r); 
  247.       } 
  248.       else if ((reqOk) && (defaultValue != null) && (defaultValue.equals("gap_callbackServer:"))) { 
  249.         String r = ""
  250.         if (message.equals("usePolling")) { 
  251.           r = "" + DroidGap.this.callbackServer.usePolling(); 
  252.         } 
  253.         else if (message.equals("restartServer")) { 
  254.           DroidGap.this.callbackServer.restartServer(); 
  255.         } 
  256.         else if (message.equals("getPort")) { 
  257.           r = Integer.toString(DroidGap.this.callbackServer.getPort()); 
  258.         } 
  259.         else if (message.equals("getToken")) { 
  260.           r = DroidGap.this.callbackServer.getToken(); 
  261.         } 
  262.         result.confirm(r); 
  263.       } 
  264.       else if ((reqOk) && (defaultValue != null) && (defaultValue.equals("gap_init:"))) { 
  265.         DroidGap.this.appView.setVisibility(0); 
  266.         this.ctx.spinnerStop(); 
  267.         result.confirm("OK"); 
  268.       } 
  269.       else 
  270.       { 
  271.         JsPromptResult res = result; 
  272.         AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx); 
  273.         dlg.setMessage(message); 
  274.         EditText input = new EditText(this.ctx); 
  275.         if (defaultValue != null) { 
  276.           input.setText(defaultValue); 
  277.         } 
  278.         dlg.setView(input); 
  279.         dlg.setCancelable(false); 
  280.         dlg.setPositiveButton(17039370, new DialogInterface.OnClickListener(input, res) 
  281.         { 
  282.           public void onClick(DialogInterface dialog, int which) { 
  283.             String usertext = this.val$input.getText().toString(); 
  284.             this.val$res.confirm(usertext); 
  285.           } 
  286.         }); 
  287.         dlg.setNegativeButton(17039360, new DialogInterface.OnClickListener(res) 
  288.         { 
  289.           public void onClick(DialogInterface dialog, int which) { 
  290.             this.val$res.cancel(); 
  291.           } 
  292.         }); 
  293.         dlg.create(); 
  294.         dlg.show(); 
  295.       } 
  296.       return true
  297.     } 
  298.  
  299.     public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize, long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) 
  300.     { 
  301.       Log.d(this.TAG, "event raised onExceededDatabaseQuota estimatedSize: " + Long.toString(estimatedSize) + " currentQuota: " + Long.toString(currentQuota) + " totalUsedQuota: " + Long.toString(totalUsedQuota)); 
  302.  
  303.       if (estimatedSize < this.MAX_QUOTA) 
  304.       { 
  305.         long newQuota = estimatedSize; 
  306.         Log.d(this.TAG, "calling quotaUpdater.updateQuota newQuota: " + Long.toString(newQuota)); 
  307.         quotaUpdater.updateQuota(newQuota); 
  308.       } 
  309.       else 
  310.       { 
  311.         quotaUpdater.updateQuota(currentQuota); 
  312.       } 
  313.     } 
  314.  
  315.     public void onConsoleMessage(String message, int lineNumber, String sourceID) 
  316.     { 
  317.       Log.d(this.TAG, sourceID + ": Line " + Integer.toString(lineNumber) + " : " + message); 
  318.     } 
  319.  
  320.     public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) 
  321.     { 
  322.       super.onGeolocationPermissionsShowPrompt(origin, callback); 
  323.       callback.invoke(origin, truefalse); 
  324.     } 
  325.   } 

相信开发过webview的同学都知道其中的奥秘了,通过重载webview的WebChromeClient和WebViewClient接口,实现其中的js代码的处理部分,即可和phoneGap中js端的代码进行交互了。

相信现在大家都熟悉了一种利用web方式开发移动APP的解决方式了吧,期待更好的解决方案能够出来。

最后再向大家推荐一个js开发库,来方便的定制APP的样式,怎么样让你的APP看起来更本地化,它就是http://jqtouch.com/,运行结果见开始的图片,如果我把浏览器的部分隐藏掉,谁能轻易分辨出来是一个本地化的APP,还是web的APP?

原文链接:http://blog.zhourunsheng.com/2011/10/%E7%A7%BB%E5%8A%A8-app-%E4%B9%8B%E8%B7%A8%E5%B9%B3%E5%8F%B0%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/

责任编辑:佚名 来源: 润物无声的博客
相关推荐

2010-10-09 15:01:27

PhoneGapiPhoneAndroid

2012-01-04 16:14:17

2011-04-08 09:13:13

游戏跨平台iOS

2013-07-02 15:26:10

APP企业移动商城

2012-07-06 13:50:44

跨平台工具Adobe Phone

2011-07-11 14:16:37

DPIRASTAP

2011-11-08 19:09:08

2012-05-24 13:25:37

TitaniumPhoneGapAppcelerato

2011-05-05 16:00:12

深信服移动办公

2015-07-10 15:19:21

2013-12-16 10:32:37

2022-08-12 08:38:08

携程小程序Taro跨端解决方案

2011-02-24 11:15:45

应用商店

2016-03-13 17:58:57

2018-01-26 08:39:03

2018-12-12 15:50:13

2012-05-09 10:08:41

跨机房

2023-05-06 15:32:04

2020-09-17 17:09:35

戴尔

2016-03-13 17:35:18

点赞
收藏

51CTO技术栈公众号