前言:
這里有兩個方案,第一個使用Andorid客戶端和JavaScript互相調用方法來實現,這種方法極力不推薦,它會增加服務端和客戶端的開發成本。
第二種就是繼承WebViewChromeClient了,WebChromeClient是Html/Js和Android客戶端進行交互的一個中間件,其將webview中js所產生的事件封裝,然后傳遞到Android客戶端。Google這樣做的其中一個很重要的原因就是安全問題。
一,使用Android本地和JS方法互相調用完成文件上傳與選擇(會增客戶端與服務端開發成本,不推薦)
這里我僅僅演示了Andorid客戶端和Javascript如何互相調用
1.Html代碼
<!doctype html> <html>
<head><meta charset="UTF-8"><title>Untitled Document</title></head> <script type="text/javascript"> function javaNoParam(){ Android.showToast(); } function javaWithParam(message){ Android.showToast(message); } function jsNoParam(){ alert("來自Java調用,無參") } function jsWithParam(message){ alert("來自Java調用,有參數:"+message) } </script> <body> <p> <input type="button" name="button" id="button" value="調用Java無參函數" onClick="javaNoParam()"></p> <p> <input type="button" name="button2" id="button2" value="調用Java有參函數" onClick="javaWithParam('有參數')"></p> <p> </p> </body> </html>
2.啟動WebView對JavaScript的支持 ,默認不支持。
WebSettings setting = webview.getSettings();setting.setJavaScriptEnable(true);
3.寫一個客戶端接口供JS端調用
public class WebAppInterface { private Context context; public WebAppInterface(Context context) { this.context = context; } public void showToast() { Toast.makeText(context, "js端調用,無參數", Toast.LENGTH_SHORT).show(); } public void showToast(String message) { Toast.makeText(context, "js端調用,有參數:" + message, Toast.LENGTH_SHORT).show(); } }
4.將WebAppInterface接口設置到WebView中
webview.addJavascriptInterface(new WebAppInterface(this), "Android");
第二個參數是個代號,供JS端調用,有點像JS和客戶端碰頭的接頭暗號:
function javaWithParam(message){ Android.showToast(message); //需要在addJavascriptInterface(new WebAppInterface(this), "Android")中設定的保持一致
}
設置完以上的部分,就可到達js調用客戶端代碼的目的
4.Android客戶端遠程調用JavaScript方法
webview.loadUrl("javascript:jsNoParam()");
webview.loadUrl("javascript:jsWithParam('" + "Hello!" + "')");
其中jsNoParam()和jsWithParam(param)都是javacript中的方法
上面的全部步驟即可實現Andorid客戶端和JavaScript的簡單調,但是這樣如果應用到實際開發中會增加服務端和客戶端的開發成本,每個接口都需要服務端和客戶端一起協商開發,這樣的在開發模式中耦合性很差,有沒有一種東西技能滿足web端與客戶端交互又能達到開發模式上解耦合?
當然是有,要不然google那幫高帥富們豈不是廢了。
二,繼承WebChromeCilent,重寫WebChromeClient的onFileChooser方法:
1.現提供一個簡單的版本,僅僅實現選擇文件上傳功能
1.Html
<!doctype html> <html> <head> <meta charset="UTF-8"> <title>WebView Test</title> </head> <script type="text/javascript"> function alertSomething(){ alert("你好") } function delete_confirm() <!--調用方法--> { event.returnValue = confirm("確定 or 取消"); } </script> <body> <input type="button" name="button" id="button" value="js提示對話框" onClick="alertSomething()"></p> <input type="button" name="button2" id="button2" value="js確定or取消對話框" onClick="delete_confirm()"></p>
<input type="file" value="" class='zj-up-btn pa' name="uploadfile" id="uploadfile" onchange="form.submit()" /></p> </body> </html>
Java:繼承WebChromeClient重寫onFileChooser方法
public class FileSelectionWebActivity extends FragmentActivity { private static final int FILE_SELECT_CODE = 0; private WebView webView; private ValueCallback<Uri> mUploadMessage; @Override protected void onCreate(Bundle arg0) { super.onCreate(arg0); setContentView(R.layout.selection_file_web_activity); initWebView(); } @SuppressLint("SetJavaScriptEnabled") private void initWebView() { webView = (WebView) findViewById(R.id.fileSelectionWebview);
WebSettings webSettings = webView.getSettings(); webSettings.setJavaScriptEnabled(true); webSettings.setBuiltInZoomControls(true);
webView.loadUrl("file:///android_asset/selectFileHtml/index.html"); webView.setWebViewClient(new MyWebViewClient(this)); webView.setWebChromeClient(new MyWebChromeClient()); } private class MyWebChromeClient extends WebChromeClient { // For Android 3.0+ public void openFileChooser(ValueCallback<Uri> uploadMsg) { mUploadMessage = uploadMsg; Intent i = new Intent(Intent.ACTION_GET_CONTENT); i.addCategory(Intent.CATEGORY_OPENABLE); i.setType("image/*"); startActivityForResult(Intent.createChooser(i, "File Chooser"), FILE_SELECT_CODE); } // For Android 3.0+ public void openFileChooser(ValueCallback uploadMsg, String acceptType) { mUploadMessage = uploadMsg; Intent i = new Intent(Intent.ACTION_GET_CONTENT); i.addCategory(Intent.CATEGORY_OPENABLE); i.setType("*/*"); startActivityForResult(Intent.createChooser(i, "File Browser"), FILE_SELECT_CODE); } // For Android 4.1 public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) { mUploadMessage = uploadMsg; Intent i = new Intent(Intent.ACTION_GET_CONTENT); i.addCategory(Intent.CATEGORY_OPENABLE); i.setType("image/*"); startActivityForResult(Intent.createChooser(i, "File Chooser"), FILE_SELECT_CODE); } } private class MyWebViewClient extends WebViewClient { private Context context; public DuomiWebViewClient(Context context) { super(); this.context = context; } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { super.onPageStarted(view, url, favicon); } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); } } // flipscreen not loading again @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode != RESULT_OK) { return; } switch (requestCode) { case FILE_SELECT_CODE : { Uri uri = data.getData(); Log.e("Tag", "Path:" + uri.toString()); mUploadMessage.onReceiveValue(uri); mUploadMessage = null; } break; } } }
上面的代碼只是提供了上傳文件的功能,有時候當你想上傳圖時可能需要拍照上傳,或者你想上傳各種多媒體類型的文件,怎么辦?
其實我們手機的瀏覽器已經有這些功能了,為何不Reading the fucking source code!
下面提供一個復雜的功能,代碼是從瀏覽器中移植過來的:
擁有的功能:
1.客戶端彈出服務端JS對話框
2.能夠拍照上傳
3.支持主流媒體文件選擇
廢話不多說,貼代碼:
public class WebViewActivity extends FragmentActivity { private WebView webview; private UploadHandler mUploadHandler; @Override protected void onCreate(Bundle arg0) { requestWindowFeature(Window.FEATURE_NO_TITLE); super.onCreate(arg0); setContentView(R.layout.activity_webview); webview = (WebView) findViewById(R.id.webview); webview.setWebChromeClient(new MyChromeViewClient()); webview.setWebViewClient(new MyWebViewClinet()); // webview.setDownloadListener(new MyDownloadListener()); initWebViewSettings(); initData(); } @SuppressLint({ "SetJavaScriptEnabled", "NewApi" }) private void initWebViewSettings() { WebSettings settings = webview.getSettings(); settings.setDefaultFontSize(50); settings.setDefaultFixedFontSize(30); settings.setJavaScriptEnabled(true); settings.setAllowFileAccess(true); settings.setDomStorageEnabled(true); settings.setLoadWithOverviewMode(true); settings.setUseWideViewPort(true); settings.setSupportZoom(true); // WebView inside Browser doesn't want initial focus to be set. settings.setNeedInitialFocus(false); // Browser supports multiple windows settings.setSupportMultipleWindows(true); // enable smooth transition for better performance during panning or } private void initData() { Intent intent = getIntent(); String url = intent.getStringExtra("url"); webview.loadUrl(url); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { if (requestCode == Controller.FILE_SELECTED) { // Chose a file from the file picker. if (mUploadHandler != null) { mUploadHandler.onResult(resultCode, intent); } } super.onActivityResult(requestCode, resultCode, intent); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if ((keyCode == KeyEvent.KEYCODE_BACK) && webview.canGoBack()) { webview.goBack(); return true; } return super.onKeyDown(keyCode, event); } class MyDownloadListener implements DownloadListener{ @Override public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) { // TODO Auto-generated method stub } } class MyChromeViewClient extends WebChromeClient { @Override public void onCloseWindow(WebView window) { WebViewActivity.this.finish(); super.onCloseWindow(window); } public void onProgressChanged(WebView view, final int progress) { } @Override public boolean onJsAlert(WebView view, String url, String message, final JsResult result) { new AlertDialog.Builder(WebViewActivity.this).setTitle("提示信息").setMessage(message) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { result.confirm(); } }).setCancelable(false).create().show(); return true; } @Override public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) { new AlertDialog.Builder(WebViewActivity.this).setTitle("提示信息").setMessage(message) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { result.confirm(); } }).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { result.cancel(); } }).setCancelable(false).create().show(); return true; } // Android 2.x public void openFileChooser(ValueCallback<Uri> uploadMsg) { openFileChooser(uploadMsg, ""); } // Android 3.0 public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) { openFileChooser(uploadMsg, "", "filesystem"); } // Android 4.1 public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) { mUploadHandler = new UploadHandler(new Controller()); mUploadHandler.openFileChooser(uploadMsg, acceptType, capture); } } class MyWebViewClinet extends WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { return true; } } // copied from android-4.4.3_r1/src/com/android/browser/UploadHandler.java class UploadHandler { /* * The Object used to inform the WebView of the file to upload. */ private ValueCallback<Uri> mUploadMessage; private String mCameraFilePath; private boolean mHandled; private boolean mCaughtActivityNotFoundException; private Controller mController; public UploadHandler(Controller controller) { mController = controller; } public String getFilePath() { return mCameraFilePath; } boolean handled() { return mHandled; } public void onResult(int resultCode, Intent intent) { if (resultCode == Activity.RESULT_CANCELED && mCaughtActivityNotFoundException) { // Couldn't resolve an activity, we are going to try again so skip // this result. mCaughtActivityNotFoundException = false; return; } Uri result = (intent == null || resultCode != Activity.RESULT_OK) ? null : intent.getData(); // As we ask the camera to save the result of the user taking // a picture, the camera application does not return anything other // than RESULT_OK. So we need to check whether the file we expected // was written to disk in the in the case that we // did not get an intent returned but did get a RESULT_OK. If it was, // we assume that this result has came back from the camera. if (result == null && intent == null && resultCode == Activity.RESULT_OK) { File cameraFile = new File(mCameraFilePath); if (cameraFile.exists()) { result = Uri.fromFile(cameraFile); // Broadcast to the media scanner that we have a new photo // so it will be added into the gallery for the user. mController.getActivity().sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, result)); } } mUploadMessage.onReceiveValue(result); mHandled = true; mCaughtActivityNotFoundException = false; } public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) { final String imageMimeType = "image/*"; final String videoMimeType = "video/*"; final String audioMimeType = "audio/*"; final String mediaSourceKey = "capture"; final String mediaSourceValueCamera = "camera"; final String mediaSourceValueFileSystem = "filesystem"; final String mediaSourceValueCamcorder = "camcorder"; final String mediaSourceValueMicrophone = "microphone"; // According to the spec, media source can be 'filesystem' or 'camera' or 'camcorder' // or 'microphone' and the default value should be 'filesystem'. String mediaSource = mediaSourceValueFileSystem; if (mUploadMessage != null) { // Already a file picker operation in progress. return; } mUploadMessage = uploadMsg; // Parse the accept type. String params[] = acceptType.split(";"); String mimeType = params[0]; if (capture.length() > 0) { mediaSource = capture; } if (capture.equals(mediaSourceValueFileSystem)) { // To maintain backwards compatibility with the previous implementation // of the media capture API, if the value of the 'capture' attribute is // "filesystem", we should examine the accept-type for a MIME type that // may specify a different capture value. for (String p : params) { String[] keyValue = p.split("="); if (keyValue.length == 2) { // Process key=value parameters. if (mediaSourceKey.equals(keyValue[0])) { mediaSource = keyValue[1]; } } } } //Ensure it is not still set from a previous upload. mCameraFilePath = null; if (mimeType.equals(imageMimeType)) { if (mediaSource.equals(mediaSourceValueCamera)) { // Specified 'image/*' and requested the camera, so go ahead and launch the // camera directly. startActivity(createCameraIntent()); return; } else { // Specified just 'image/*', capture=filesystem, or an invalid capture parameter. // In all these cases we show a traditional picker filetered on accept type // so launch an intent for both the Camera and image/* OPENABLE. Intent chooser = createChooserIntent(createCameraIntent()); chooser.putExtra(Intent.EXTRA_INTENT, createOpenableIntent(imageMimeType)); startActivity(chooser); return; } } else if (mimeType.equals(videoMimeType)) { if (mediaSource.equals(mediaSourceValueCamcorder)) { // Specified 'video/*' and requested the camcorder, so go ahead and launch the // camcorder directly. startActivity(createCamcorderIntent()); return; } else { // Specified just 'video/*', capture=filesystem or an invalid capture parameter. // In all these cases we show an intent for the traditional file picker, filtered // on accept type so launch an intent for both camcorder and video/* OPENABLE. Intent chooser = createChooserIntent(createCamcorderIntent()); chooser.putExtra(Intent.EXTRA_INTENT, createOpenableIntent(videoMimeType)); startActivity(chooser); return; } } else if (mimeType.equals(audioMimeType)) { if (mediaSource.equals(mediaSourceValueMicrophone)) { // Specified 'audio/*' and requested microphone, so go ahead and launch the sound // recorder. startActivity(createSoundRecorderIntent()); return; } else { // Specified just 'audio/*', capture=filesystem of an invalid capture parameter. // In all these cases so go ahead and launch an intent for both the sound // recorder and audio/* OPENABLE. Intent chooser = createChooserIntent(createSoundRecorderIntent()); chooser.putExtra(Intent.EXTRA_INTENT, createOpenableIntent(audioMimeType)); startActivity(chooser); return; } } // No special handling based on the accept type was necessary, so trigger the default // file upload chooser. startActivity(createDefaultOpenableIntent()); } private void startActivity(Intent intent) { try { mController.getActivity().startActivityForResult(intent, Controller.FILE_SELECTED); } catch (ActivityNotFoundException e) { // No installed app was able to handle the intent that // we sent, so fallback to the default file upload control. try { mCaughtActivityNotFoundException = true; mController.getActivity().startActivityForResult(createDefaultOpenableIntent(), Controller.FILE_SELECTED); } catch (ActivityNotFoundException e2) { // Nothing can return us a file, so file upload is effectively disabled. Toast.makeText(mController.getActivity(), "File uploads are disabled.", Toast.LENGTH_LONG).show(); } } } private Intent createDefaultOpenableIntent() { // Create and return a chooser with the default OPENABLE // actions including the camera, camcorder and sound // recorder where available. Intent i = new Intent(Intent.ACTION_GET_CONTENT); i.addCategory(Intent.CATEGORY_OPENABLE); i.setType("*/*"); Intent chooser = createChooserIntent(createCameraIntent(), createCamcorderIntent(), createSoundRecorderIntent()); chooser.putExtra(Intent.EXTRA_INTENT, i); return chooser; } private Intent createChooserIntent(Intent... intents) { Intent chooser = new Intent(Intent.ACTION_CHOOSER); chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents); chooser.putExtra(Intent.EXTRA_TITLE, "Choose file for upload"); return chooser; } private Intent createOpenableIntent(String type) { Intent i = new Intent(Intent.ACTION_GET_CONTENT); i.addCategory(Intent.CATEGORY_OPENABLE); i.setType(type); return i; } private Intent createCameraIntent() { Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); File externalDataDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); File cameraDataDir = new File(externalDataDir.getAbsolutePath() + File.separator + "browser-photos"); cameraDataDir.mkdirs(); mCameraFilePath = cameraDataDir.getAbsolutePath() + File.separator + System.currentTimeMillis() + ".jpg"; cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(mCameraFilePath))); return cameraIntent; } private Intent createCamcorderIntent() { return new Intent(MediaStore.ACTION_VIDEO_CAPTURE); } private Intent createSoundRecorderIntent() { return new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION); } } class Controller { final static int FILE_SELECTED = 4; Activity getActivity() { return WebViewActivity.this; } } }
有幾個類要說明下:
MyChromeViewClient 繼承WebChromeClient重寫了幾個關鍵方法。其中有三個重載方法openFileChooser,用來兼容不同的Andorid版本,以防出現NoSuchMethodError異常。
另外一個類UploadHandler,起到一個解耦合作用,它相當於WebChromeClient和Web網頁端的一個搬運工兼職翻譯,解析網頁端傳遞給WebChromeClient的動作,然后將onActivityResult接收用戶選擇的文件傳遞給司機ValueCallback。WebChromeClient提供了一個Web網頁端和客戶端交互的通道,而UploadHandler就是用來搬磚的~。
UploadHandler有個很重要的成員變量:ValueCallback<Uri> mUploadMessage。ValueCallback是WebView留下來的一個回調,就像是WebView的司機一樣,當WebChromeClient和UploadHandler合作將文件選擇后,ValueCallback開始將文件給WebView,告訴WebView開始干活了,磚頭已經運回來了,你可以蓋房子了。
