本文背景:delphi XE10.1
Firemonkey自带的TWebBrower对于JavaScript的交互支持一直不是很好,仅仅提供了一个本地执行JavaScript的方法EvaluateJavaScript,而且该方法不提供JS执行的返回结果。在安卓平台上,EvaluateJavaScript是通过WebView的loadUrl('javascript:' + JavaScript)实现的。
在Android 4.4之后,WebView提供了一个新的执行JS的接口:
procedure evaluateJavascript(script: JString; resultCallback: JValueCallback); cdecl;
该接口可以注册一个获取JS执行结果的回调函数以便在JS异步执行完时返回结果。
TJavaScriptCallBack = procedure(const AResult: string) of object;
TJSResultCallback = class(TJavaLocal, JValueCallback)
private
fCallBack: TJavaScriptCallBack;
public
procedure onReceiveValue(value: JObject); cdecl;
end;
{ TResultCallback }
procedure TJSResultCallback.onReceiveValue(value: JObject);
begin
if Assigned(fCallBack) and (value<>nil) then
fCallBack(JStringToString(value.toString));
end;
这样我们只需创建 一个TJSResultCallBack对象, 并作为WebView.evaluateJavascript接口的第二个参数,就可以异步获取本地执行JS代码的结果。
WebView还提供一个本地代码扩展JavaScript功能的接口:
procedure addJavascriptInterface(object_: JObject; name: JString); cdecl;
该接口注册一个本地实现类,在JS中可以使用指定的名称直接调用该类实现的本地方法。 由于目前不知道怎么直接用Delphi的JNI直接产生一个Java本地对象【注1】,而且Android在高版本中基于安全JS只支持调用本地实现类中标记了“@JavascriptInterface”属性的方法【注2】,所以这里直接使用Java编写了一个代理类:
package tu2.com.jshellper;
import android.webkit.JavascriptInterface;
/**
* Created by tutu on 2017-01-01.
*/
public class JavaScriptHelper {
public interface LocalCallBack {
public String executeCustomJavaScript(String cmd, String param);
}
protected LocalCallBack mLocalCallBack;
public void setLocalCallBack(LocalCallBack callBack) {
mLocalCallBack = callBack;
}
@JavascriptInterface
public String executeCmd(String cmd, String param) {
String sRet= "";
if (mLocalCallBack != null) {
sRet = mLocalCallBack.executeCustomJavaScript(cmd, param);
}
return sRet;
}
}
将这个Java实现类编译成Jar包,就可以在Delphi中使用。
[JavaSignature('tu2/com/jshellper/JavaScriptHelper$LocalCallBack')]
JJavaScriptHelper_LocalCallBack = interface(IJavaInstance)
['{74BA78C9-00E5-474A-8FAE-D9DE8D1219F3}']
function executeCustomJavaScript(cmd: JString; param: JString): JString; cdecl;
end;
TJavaScriptHelper_LocalCallBack = class(TJavaLocal, JJavaScriptHelper_LocalCallBack)
private
FCallBack: TLocalCallBack;
public
function executeCustomJavaScript(cmd: JString; param: JString): JString; cdecl;
end;
[JavaSignature('tu2/com/jshellper/JavaScriptHelper')]
JJavaScriptHelper = interface(JObject)
['{B70ED7E1-37E5-4F7A-B26B-478A9D36A128}']
procedure setLocalCallBack(callBack: JJavaScriptHelper_LocalCallBack); cdecl;
function executeCmd(cmd: JString; param: JString): JString; cdecl;
end;
JJavaScriptHelperClass = interface(JObjectClass)
['{9A522A31-9D8A-4748-9704-1E97C84E5657}']
function init: JJavaScriptHelper; cdecl;
end;
TJJavaScriptHelper = class(TJavaGenericImport<JJavaScriptHelperClass, JJavaScriptHelper>) end;
基于上面的介绍,可以扩展TWebBrower在android平台上的实现,这里为了不引起其他单元代码的修改,采取外部主动注册的方式。
RegisterJavaScriptCallBack方法为浏览器控件注册本地扩展JS方法回调和本地异步执行JS的结果回调,EvaluateJavaScriptAsync方法使浏览器异步调用JS,如果JS有返回值将触发TJavaScriptCallBack方法的执行。
unit FMX.WebBrowser.Android;
interface
{$SCOPEDENUMS ON}
uses FMX.WebBrowser;
type
TLocalCallBack = procedure(const ACmd, AParam: string; var AResult: string) of object;
TJavaScriptCallBack = procedure(const AResult: string) of object;
procedure RegisterJavaScriptCallBack(const AWebControl: TCustomWebBrowser;
const ALocal: TLocalCallBack; const AJavaScript: TJavaScriptCallBack);
procedure EvaluateJavaScriptAsync(const AWebControl: TCustomWebBrowser;
const JavaScript: string);
在TAndroidWebBrowserService的字段部分增加:
FBounds: TRect;
FRealBounds: TRect;
//[Add By Tu2
FJSHelper: JJavaScriptHelper;
FJSLocal: TJavaScriptHelper_LocalCallBack;
FJSResult: TJSResultCallback;
procedure DoEvaluateJavaScriptAsync(const JavaScript: string);
//Add By Tu2]
procedure InitUIThread;
procedure CalcRealBorder;
方法InitUIThread中增加:
procedure TAndroidWebBrowserService.InitUIThread;
begin
FJWebBrowser := TJWebBrowser.JavaClass.init(TAndroidHelper.Activity);
FJWebBrowser.getSettings.setJavaScriptEnabled(True);
FListener := TWebBrowserListener.Create(Self);
FJWebBrowser.SetWebViewListener(FListener);
FJNativeLayout := TJNativeLayout.JavaClass.init(TAndroidHelper.Activity,
MainActivity.getWindow.getDecorView.getWindowToken);
FJNativeLayout.setPosition(100,100);
FJNativeLayout.setSize(300,300);
FJNativeLayout.setControl(FJWebBrowser);
FFocusChangeListener := TFocusChangeListener.Create(Self);
FJNativeLayout.setOnFocusChangeListener(FFocusChangeListener);
FJWebBrowser.getSettings.setGeolocationEnabled(True);
FJWebBrowser.getSettings.setAppCacheEnabled(True);
FJWebBrowser.getSettings.setDatabaseEnabled(True);
FJWebBrowser.getSettings.setDomStorageEnabled(True);
FJWebBrowser.getSettings.setBuiltInZoomControls(True);
FJWebBrowser.getSettings.setDisplayZoomControls(False);
//Add By TU2
FJSResult := TJSResultCallback.Create;
FJSHelper := TJJavaScriptHelper.JavaClass.init;
FJSLocal := TJavaScriptHelper_LocalCallBack.Create;
FJSHelper.setLocalCallBack(FJSLocal);
FJWebBrowser.addJavascriptInterface(FJSHelper, StringToJString('TU2JSHelper'));
end;
最后实现开头定义的注册扩展方法:
procedure TAndroidWebBrowserService.DoEvaluateJavaScriptAsync(const JavaScript: string);
begin
CallInUIThread(procedure
begin
FJWebBrowser.evaluateJavascript(StringToJString(JavaScript), FJSResult);
end);
end;
{ TAndroidWebBrowserService.TResultCallback }
procedure TAndroidWebBrowserService.TJSResultCallback.onReceiveValue(value: JObject);
begin
if Assigned(fCallBack) and (value<>nil) then
begin
TThread.Queue(nil, procedure begin
fCallBack(JStringToString(value.toString));
end);
end;
end;
{ TAndroidWebBrowserService.TJavaScriptHelper_LocalCallBack }
function TAndroidWebBrowserService.TJavaScriptHelper_LocalCallBack.executeCustomJavaScript(
cmd, param: JString): JString;
var
AResult: string;
begin
if Assigned(FCallBack) then
begin
AResult := '';
FCallBack(JStringToString(cmd), JStringToString(param), AResult);
Result := StringToJString(AResult);
end;
end;
type
TMyCustomWebBrowser = class(TControl)
private
FWeb: ICustomBrowser;
end;
procedure RegisterJavaScriptCallBack(const AWebControl: TCustomWebBrowser;
const ALocal: TLocalCallBack; const AJavaScript: TJavaScriptCallBack);
begin
with (TMyCustomWebBrowser(AWebControl).FWeb as TAndroidWebBrowserService) do
begin
FJSLocal.FCallBack := ALocal;
FJSResult.fCallBack := AJavaScript;
end;
end;
procedure EvaluateJavaScriptAsync(const AWebControl: TCustomWebBrowser; const JavaScript: string);
begin
if TOSVersion.Check(4,4) then
(TMyCustomWebBrowser(AWebControl).FWeb as TAndroidWebBrowserService).
DoEvaluateJavaScriptAsync(JavaScript)
else begin
//暂未实现
end;
end;
至此,TWebBrower在android平台上的JS扩展交互就实现了。只需为WebBrowser控件调用一次RegisterJavaScriptCallBack,就可以在JS代码中如下使用:
function CallHost() {
document.getElementById("demoHost").innerHTML = window.TU2JSHelper.executeCmd("JSCallLocal","读取Label的Tag值:");
}
完整的FMX.WebBrowser.Android和Demo: http://download.csdn.net/detail/tht2009/9730534
注1:Delphi使用两种方式创建Java对象,一种是TJavaGenericImport导入Java中的类,一种是使用(TJavaLocal, JXXX)创建本地实例对象,两种都需要已知的签名接口,不能直接用delphi定义一个新Java Object对象。但在 turbococoa 中介绍了可以直接使用TJavaObject继承扩展Java类,只是这是一个第三方商业项目,不知道其实现原理。
注2:详情参见android相关开发文档。