一、背景

公司项目因为一些原因,需要把某些页面用 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控制返回键,使用第三种方案。

测试apk和h5 demo

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐