android webview与js简单的交互方案
最近研究webview与js交互,看了几个开源库实现,感觉不尽如人意,存在主要问题是,耦合较高,使用不够简洁,后来参考Uri设定规则,格局Uri类似协议自定义了类似的js交互协议
比较简洁,自定义协议内容样式如:jsbridge://android-app/method123?a=123&b=345#jsMethod1(p1,p2)
协议说明:
scheme定义为jsbridge,用于区分别的网络请求(http),
authority定义为android-app,区分不同平台处理
path定义为 调用本地方法名称 method123
Query定义为调用本地方法参数 a=123&b=345
Fragment定义为回调js方法 #jsMethod1(p1,p2)
这样就可以做到精简,灵活,良好的可扩展,灵活使用的特点。再看解析实现·
public boolean parseJsBridge(String url,WebAppInterface webNative){//WebAppInterface为被调用本地方法类实例 if(!isProtocol(url)) return false; int i = -1; Uri uri = Uri.parse(url);//借助URI解析协议 String methodName = uri.getPath();//调用本地方法,method123 methodName = methodName.replace("/", ""); String params = uri.getQuery();//调用本地方法参数。a=123&b=345 String callback = uri.getFragment();//解析js回调方法,#jsMethod1(p1,p2) i = callback.lastIndexOf('('); String jsMethod = callback.substring(0,i); String jsParams = callback.substring(i+1,callback.length()-1); //将解析的结果封装为JsResponse,便于后续使用 mJsRes = new JsResponse(methodName,jsMethod); mJsRes.parseJsCallbackParams(jsParams); mJsRes.parseNativeParams(params);
//这里通过反射方式调用本地方法 String[] args = mJsRes.getMethodArgs(); if(null==args){//使用反射调用无参数方法 jsCallNoParamMethod(webNative,methodName); }else{//调用反射有参数方法 int count = args.length; Class[] javaParamsType = new Class[count]; for(int t=0;t<count;t++){ javaParamsType[t] = String.class; } jsCallParamMethod(webNative,methodName,javaParamsType,mJsRes.getMethodArgs()); } return true; }
协议解析完整实现类:
public class JsProcessor { // private JsProcessor(){} private JsResponse mJsRes; public JsResponse getJsResponse(){ return mJsRes; } public static boolean isProtocol(String url) { return !TextUtils.isEmpty(url) && url.startsWith("jsbridge://"); } // jsbridge://android-app/method123?a=123&b=345#jsMethod1(p1,p2)" public boolean parseJsBridge(String url,WebAppInterface webNative){ if(!isProtocol(url)) return false; int i = -1; Uri uri = Uri.parse(url); String methodName = uri.getPath();//method1 methodName = methodName.replace("/", ""); String params = uri.getQuery();//a=123&b=345 String callback = uri.getFragment();//#jsMethod1(p1,p2) i = callback.lastIndexOf('('); String jsMethod = callback.substring(0,i); String jsParams = callback.substring(i+1,callback.length()-1); mJsRes = new JsResponse(methodName,jsMethod); mJsRes.parseJsCallbackParams(jsParams); mJsRes.parseNativeParams(params); String[] args = mJsRes.getMethodArgs(); if(null==args){ jsCallNoParamMethod(webNative,methodName); }else{ int count = args.length; Class[] javaParamsType = new Class[count]; for(int t=0;t<count;t++){ javaParamsType[t] = String.class; } jsCallParamMethod(webNative,methodName,javaParamsType,mJsRes.getMethodArgs()); } return true; } public boolean jsCallNoParamMethod(Object obj,String mName){ boolean success = false; try { Method method=obj.getClass().getMethod(mName); method.invoke(obj); success = true; } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); }catch (Exception ex){ ex.printStackTrace(); } return success; } //getMethod("sayHello", String.class,int.class); public boolean jsCallParamMethod(Object obj,String mName,Class<?>[] parameterTypes,Object[] args){ if(parameterTypes.length!=args.length) return false; boolean success = false; try { Method method = obj.getClass().getMethod(mName,parameterTypes); method.invoke(obj, args); success = true; } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); }catch (Exception ex){ ex.printStackTrace(); } return success; } }
JSResponse解析后的封装实体类
public class JsResponse { //call native public String methodName; private Map<String,String> nativeParams = new HashMap<>(); //callback js method public String callJsMethod; // public Map<String,String> jsParams = new HashMap<>(); private int jsParamCount; public JsResponse(String javaMethod,String jsMethod){ this.methodName = javaMethod; this.callJsMethod = jsMethod; } public String[] getMethodArgs(){ if(0==nativeParams.size()) return null; String[] params = new String[nativeParams.size()]; int n = 0; for(Map.Entry<String,String> entry:nativeParams.entrySet()){ params[n] = entry.getValue(); n++; } return params; } public void parseNativeParams(String params){ // int i = 0,len = params.length(); if(!nativeParams.isEmpty()) nativeParams.clear(); String[] eles = params.split("&"); for(String ele:eles){ int eq = ele.indexOf('='); String value = ele.substring(eq + 1); String name = ele.substring(0,eq); nativeParams.put(name,value); } } public void parseJsCallbackParams(String jsparams){ String[] eles = jsparams.split(","); jsParamCount = eles.length; } }
js使用规则协议,触发调用本地方法如下,即可
function startRequest() { var iframe = document.createElement('iframe'); iframe.setAttribute('src', 'jsbridge://android-app/showHello?a=hello-&b=i_from_js&#callbackHello(p1,p2)'); document.body.appendChild(iframe); iframe.parentNode.removeChild(iframe); iframe = null; }
本地调用js回调方法
void executeJsCmd(String jsCmd){ Log.d(TAG,"pending execute js command="+jsCmd); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { //async execute mWebView.evaluateJavascript(jsCmd, new ValueCallback<String>() { @Override public void onReceiveValue(String value) { Log.d(TAG,"async execute result="+value); } }); }else { // This code is BAD and will block the UI thread mWebView.loadUrl(jsCmd); } }
下面就是js与本地通信了,通过WebViewClient的shouldOverrideUrlLoading拦截内容判断处理
private class MonitorWebClient extends WebViewClient { WebAppInterface webCallapp = new WebAppInterface(activity); ....... @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { boolean handle = super.shouldOverrideUrlLoading(view, url); JsProcessor jsPro = null; if (!handle) { jsPro = new JsProcessor(); handle = jsPro.parseJsBridge(url,webCallapp); } if(handle){ handleJsCallback(jsPro,"hello","callback js method"); } return handle; } @Override public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { //如果需要传文件,拦截WebView的资源请求,将文件以流的形式进行通讯 //return new WebResourceResponse("image/jpeg", "UTF-8", new FileInputStream(new File("xxxx"); return super.shouldInterceptRequest(view, request); } }
void handleJsCallback(JsProcessor jsPro,String... args){
//javascript:showJavaMessage('javaResult')"
//拼接js要执行方法名字与参数规则,如上样例 String jsM = jsPro.getJsResponse().callJsMethod; if(!TextUtils.isEmpty(jsM)){ // String jsCmd = new StringBuilder().append("javascript:").append(jsM). // append('(').append("\'java\'").append(',').append("\'hello\'").append(')').toString(); StringBuilder buf = new StringBuilder().append("javascript:").append(jsM).append('('); if(args!=null && args.length>0){ // int pos = buf.length()-1; for(String p:args) { buf.append('\'').append(p).append('\'').append(','); } buf.deleteCharAt(buf.length()-1); } buf.append(')'); executeJsCmd(buf.toString()); } }
除了上面shouldOverrideUrlLoading方法拦截处理外,js也可以通过调用 alert,prompt 方法的方式传递数据 ,我们需要重写WebChromeClient 通过回调本地方法onJsAlert(), onJsPrompt()方法即可收到对应函数回调,进行数据处理
private class AppWebChromeClient extends WebChromeClient { @Override public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) { quotaUpdater.updateQuota(spaceNeeded * 2); } @Override public boolean onConsoleMessage(ConsoleMessage cm) { Log.d("MyApplication", cm.message() + " -- From line " + cm.lineNumber() + " of " + cm.sourceId() ); return true; } @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { if("1001".equals(message)){ //解析参数defaultValue,调用java方法并得到结果 //textexeJs(); } //给js返回处理结果 result.confirm("result"); return false; } @Override public boolean onJsAlert(WebView view, String url, String message, JsResult result) { Log.d("MyApplication",url); AlertDialog alertDialog = new AlertDialog.Builder(activity).create(); // Setting Dialog Title alertDialog.setTitle("JS come message"); // Setting Dialog Message alertDialog.setMessage(message); alertDialog.show(); return false; } }
调用本地方法类对象
public class WebAppInterface { Context mContext; /** Instantiate the interface and set the context */ WebAppInterface(Context c) { mContext = c; } /** Show a toast from the web page */ @JavascriptInterface public void showToast(String toast) { Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show(); } @JavascriptInterface public void showDialog(String dialogMsg){ AlertDialog alertDialog = new AlertDialog.Builder(mContext).create(); // Setting Dialog Title alertDialog.setTitle("JS triggered Dialog"); // Setting Dialog Message alertDialog.setMessage(dialogMsg); alertDialog.show(); } public void showHello(String title,String msg){ ToastUtils.showLong(mContext,title+msg); } }
js示例内容
<html> <head> <style> body{ background-color: #FA5858; color:#fff; } input{ background-color: #F7D358; width: 300px; padding:10px; color: #000; } div#content{ padding:20px; background-color: #F7D358; color: #000; } </style> <script type="text/javascript"> function showAndroidToast(toastmsg) { Android.showToast(toastmsg); } function showAndroidDialog(dialogmsg) { Android.showDialog(dialogmsg); } function moveToScreenTwo() { Android.moveToNextScreen(); } function showPromptToJava() { var ret = prompt( "1001", "defaultMessage001" ); //ret值即为java传回的”result” //根据返回内容作相应处理 } function showJavaMessage(result) { alert("hello!"+result); } function startRequest() { var iframe = document.createElement('iframe'); iframe.setAttribute('src', 'jsbridge://android-app/showHello?a=hello-&b=i_from_js&#callbackHello(p1,p2)'); document.body.appendChild(iframe); iframe.parentNode.removeChild(iframe); iframe = null; } function callbackHello(p1,p2) { console.log("receive callback from app"); alert("receive callback "+p1+p2); } </script> </head> <body> <center> <h3>Binding JavaScript code to Android code</h3> <div id="content"> //some content here </div> <div> Here are few examples: </div> <div> <input type="button" value="Make Toast" onClick="showAndroidToast('Toast made by Javascript')" /><br/> <input type="button" value="Trigger Dialog" onClick="showAndroidDialog('This dialog is triggered by Javascript ')" /><br/> <input type="button" value="Take me to Next Screen" onClick="showPromptToJava()" /><br/> <input type="button" value="jsBridgeHandle" onClick="startRequest()" /><br/> </div> </center> </body> </html>
本地显示
mWebView.loadUrl("file:///android_asset/sample1.html");
以上就是整个js与本地方法调用整体流程
备注问题:
js与本地调用会产生的问题:
1,调用js函数参数注意
string 使用'' 需要单引号拼接,直接value或"value"则无法正常匹配
直接value为整数类型
2,onJsPrompt,onJsAlert,返回true,js默认不处理,导致下次无法调用(即无反应事件)