一、背景
公司项目因为一些原因,需要把某些页面用 h5 来开发,我们做的是机顶盒上的项目,要用遥控器来操作,在电脑浏览器上调试好项目后就打算测试下功能了,结果发现遥控器上某些按键在网页上居然监听不到按键事件,例如返回键,菜单键,home 键等。
二、分析
就拿返回键来验证问题好了,下意识会觉得键盘的 ESC 就相当于返回键了,因为可以用键盘的 ESC 控制机顶盒返回,实际并不是,在 PC 端可以监听到键盘上的 ESC 按键,但是机顶盒上是监听不到返回键的,猜测是否是厂家修改了rom,但测试发现手机上也是监听不到的,结论是移动端浏览器就是这么设计的。
为什么在移动端上监听不到返回键,这样设计其实是合理的,大致流程是设备硬件接收到遥控器红外信号后,会把按键事件分发给 framework 层 -> app 应用层 -> webview层,但是这流程中会有一部分按键被拦截,例如在 app 应用层就已经收不到 Home 键的事件了,在 webview 层收不到返回键、设置键、音量键等的按键了。
返回键、Home 键这么重要的按键,怎么能随意交给开发者处理!
为什么不能交给开发者处理?因为开发者得听产品的。
如果允许开发者完全控制返回键,可能会收到类似这样的产品需求:
1.用户进到我们的活动广告页面后,不允许他立即按返回键退出,要让用户最少看 10 秒才能退出;
2.用户打开我们的网页后,就不允许他按返回键退出了;
3.用户只要打开了我们的网页,就不允许他按任何按键退出了,Home 键也不行!!
如果大家都这样设计产品交互,会影响整个android生态的用户体验。
手机自带浏览器及 webview 加载的网页上收不到返回键监听是因为浏览器是一个公共的 app,没有人去干涉它,那自然不能允许开发者在返回键上做文章,但是如果你是在 app 里嵌入网页,有能力干预一些事件且能保证产品流程没问题,你就可以通过 native 和 js 相结合的方式实现在网页里控制返回键。
三、实现方案
方案一:在 native 层(app 的 webview页面)控制返回键,相当于做成手机自带浏览器的处理方式
...
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (webview != null && webview.canGoBack()) {
webview.goBack();
} else {
finish();
}
return true;
}
return super.onKeyDown(keyCode, event);
}
...
这样就相当于和设备自带浏览器一样的处理方式了,通过系统返回键直接就控制回退和退出,网页里不需要处理返回。
方案二:在网页里控制返回键
1.让 app 的开发者在webview所在页面实现如下逻辑
...
private WebView webview;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_web);
webview = findViewById(R.id.webView);
webview.getSettings().setJavaScriptEnabled(true);
webview.addJavascriptInterface(new JsObject(), "nativeObj");
webview.setWebViewClient(new MyWebviewClient());
String url = getIntent().getStringExtra("url");
}
class JsObject {
@JavascriptInterface
public void web_exit() {
finish();
}
}
private boolean loadFinished = false;
private class MyWebviewClient extends WebViewClient {
@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);
loadFinished = true;
}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return super.shouldOverrideUrlLoading(view, url);
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (!loadFinished) finish();//按返回键时,网页没加载成功且使用的是网页控制返回键的方案二,native关闭网页
callWebBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
public void callWebBack() {
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("which", 23);
jsonObject.put("type", "keydown");
} catch (JSONException localJSONException) {
localJSONException.printStackTrace();
}
String js = "javascript: window.event=" +
jsonObject +
";if(typeof document.myBack=='function'){document.myBack(" +
jsonObject +
")}";
this.webview.evaluateJavascript(js, null);
}
2.在网页里控制返回键
例如在 main.js 里加入如下代码:
document.myBack = () => {
if (已经到了最后一页不可回退了) {
nativeObj.web_exit()
} else {
router.go(-1);
}
};
这个myBack
方法是自己商定名字,可随意定义;
方案三:在 native 和网页里都控制返回键
1.让 app 的开发者在网页窗口页面实现如下控制
...
private WebView webview;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_web);
webview = findViewById(R.id.webView);
webview.getSettings().setJavaScriptEnabled(true);
String url = getIntent().getStringExtra("url");
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
callWebBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
public void callWebBack() {
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("which", 23);
jsonObject.put("type", "keydown");
} catch (JSONException localJSONException) {
localJSONException.printStackTrace();
}
String js = "javascript: window.event=" +
jsonObject +
";if(typeof document.myBack=='function'){document.myBack(" +
jsonObject +
")}";
webview.evaluateJavascript(js, new ValueCallback<String>() {
public void onReceiveValue(String value) {
if (!value.equals("1")) {
WebActivity.this.goBack();
}
}
});
}
...
只要网页里的myBack
方法不返回1,就由 native 来控制网页返回事件;
所以当使用方案三时就算加载失败也不需要判断,因为会通过js回调执行关闭。
2.网页里可以在页面 js 里加入如下方法
例如在vue里面,main.js 可以这么写
...
import router from './router'
document.myBack = () => {
if (如果想在网页里控制返回键) {
router.go(-1);
return 1
}
return 0
};
如果想在网页里控制返回键就在网页的myBack
方法里返回1;
以上几种方案就实现了在网页里适配返回键,其它的音量键、菜单键等,只要 apk 应用层能接收到,都可以通过这种方案来实现网页监听;
总结如下:
如果项目希望和运行在设备自带浏览器里一样的返回交互,使用第一种方案;网页无需做任何处理,app端监听按键来处理返回键。
如果项目以网页端逻辑为主,apk只是当作一个加载网页的容器,希望在网页里完全控制返回键,使用第二种方案;网页来控制返回键,app端要通过js通知网页端,并提供退出页面的方法。
如果希望在网页里根据接口数据动态决定由网页还是native控制返回键,使用第三种方案。