业务场景:
由于公司项目中包含移动端的功能,并且需要使用硬件的配合才能完成业务,比如蓝牙连接打印机,二维码等条码的扫描,如果按照以往编写适配移动端的网页肯定是不合适的,但是目前团队也没有熟练使用android进行开发的小伙伴,所以急需一个能使用手机硬件并且使用网页开发移动端的解决方案。
为此提出了以下的解决方案:
1. 微信小程序:可以使用硬件能力,并且跨平台,开发简单,但是由于我们的项目是内网使用,所以被无情的抛弃了
2. HTML+:公信部下HTML5中国产业联盟推出,看上去很强大提供了大量调用硬件能力的API,但是由于文档太烂,社区貌似也不咋地,主要是没那么多时间,两天内要决定使用哪种方案,并且要把框架搭建起来,试错成本太高,因此拒绝,当然我是不会说是因为我太笨学不会的,看上去挺不错的,以后有时间研究一下。
3. android的webView控件: 底层是webkit的内核,解决webView与原生安卓的通信问题即可,百度看了下webView是支持的,并且有很成熟的解决方案,所以当时直接就决定使用此方式来构建webApp。这种方式的好处是技术成熟,网络上文章很多,至少不会掉坑了爬不出来,支持内网使用,可以打包为APP,逼格提升不少,并且可以借助html来编写漂亮的页面,相对于原生安卓来说简单很多。
html打包apk的方式有很多,但是都大同小异,这里使用自己写壳的方式相对来说并不复杂,但是安卓那一块相对可控,并不是一个黑匣子,只需解决通信问题即可。
技术栈:
android:不需要很熟,知道如何创建项目,了解android的项目结构,控件与activity的交互方式。
webView:webView底层就是webkit内核在驱动,所以这个对象你可以看成是js中的window对象。
JSBridge:android与webView内通信的桥梁,android将方法注册暴露给js调用,js注册暴露给android调用
vue:使用vue编写页面,其它的亦可,html就行
Zxing:一个开源的扫码框架
效果图:
实现步骤:
1.新建一个安卓工程(主界面白色背景,hello world正中间的那种)
2.加入JSBridge的相关依赖
在项目的【build.gradle】加入:maven { url "https://jitpack.io" }
在module的【build.gradle】加入:compile 'com.github.lzyzsd:jsbridge:1.0.4'
3.加入Zxing的相关依赖,扫码插件
我这里并没有直接使用它,而是使用封装好的项目QcCodeLib,从GitHub上下载下来后复制zxing-lib文件夹
复制到当前android项目的根路径下
在module的【build.gradle】加入:implementation project(':zxing-lib')
修改【settings.gradle】
4.配置Android的项目清单文件【app/src/main/AndroidManifest.xml】,该文件用来说明Android的配置信息,如app名称,主界面,权限等等
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.mywebapp">
<!-- android:usesCleartextTraffic="true",9.0以后默认不支持明文通信,需配置 -->
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 注册扫码的activity -->
<activity android:name="com.google.zxing.activity.CaptureActivity" />
</application>
<!-- 权限相关 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.hardware.usb.UsbAccessory" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA"/>
</manifest>
5. 安卓主界面,使用线性布局,里面的颜色是使用的常量,自己去资源文件下建好即可
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/blue"
android:orientation="horizontal">
<TextView
android:layout_marginTop="10dp"
android:layout_marginLeft="10dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textSize = "20dp"
android:textColor="@color/write"
android:text = "android与js通信"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="70dp"
android:orientation="horizontal">
<Button
android:id="@+id/updBlue"
android:layout_margin="10dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/write"
android:background="@color/blue"
android:text="更改webView为蓝色"/>
<Button
android:id="@+id/updRed"
android:layout_margin="10dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/write"
android:background="@color/blue"
android:text="更改webView为红色" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/res"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_marginLeft="20dp"
android:text="显示安卓端扫码结果:" />
<!-- 通过JSBridge增强过的WebView控件,只做增强不改变原有功能 -->
<com.github.lzyzsd.jsbridge.BridgeWebView
android:id="@+id/test_bridge_webView"
android:layout_margin="20dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
</LinearLayout>
6. MainActivity,具体看注释
package com.example.mywebapp;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.*;
import com.github.lzyzsd.jsbridge.BridgeHandler;
import com.github.lzyzsd.jsbridge.BridgeWebView;
import com.github.lzyzsd.jsbridge.CallBackFunction;
import com.google.zxing.activity.CaptureActivity;
import com.google.zxing.util.Constant;
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
/** 使用了JSBridge增强过的WebView */
private BridgeWebView mBridgeWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/** 初始化 */
init();
}
/** 初始化页面 */
public void init() {
/** 获得WebView控件并配置 */
mBridgeWebView= (BridgeWebView) findViewById( R.id.test_bridge_webView);
/** 以http的模式加载页面,这里也可以将写好的页面放到项目下,以文件的形式加载 */
mBridgeWebView.loadUrl("http://192.168.1.100:8083");
mBridgeWebView.getSettings().setAllowContentAccess(true);
/** 是否启用JS */
mBridgeWebView.getSettings().setJavaScriptEnabled(true);
/** 注册让js调用的方法 */
mBridgeWebView.registerHandler("scanQrCode", new BridgeHandler() {
@Override
public void handler(String data, CallBackFunction function) {
/** 扫码 */
startQrCode();
/** js传递的参数 */
Toast.makeText(MainActivity.this,"JS调用扫码发送的参数:" + data,Toast.LENGTH_SHORT).show();
/** 执行安卓的方法后将结果返回给JS */
function.onCallBack("正在处理,处理后返回结果");
}
});
/** 点击修改背景为蓝色的按钮 */
findViewById(R.id.updBlue).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
/** 调用html注册的修改颜色的函数,值为blue */
mBridgeWebView.callHandler("updateBackColor","blue",new CallBackFunction(){
@Override
public void onCallBack(String data) {
Log.e(TAG, "来自web的回传数据:" + data);
}
});
}
});
/** 点击修改背景为红色的按钮 */
findViewById(R.id.updRed).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
/** 调用html注册的修改颜色的函数,值为red */
mBridgeWebView.callHandler("updateBackColor","red",new CallBackFunction(){
@Override
public void onCallBack(String data) {
Log.e(TAG, "来自web的回传数据:" + data);
}
});
}
});
}
/** 开始扫码 */
private void startQrCode() {
/** 申请相机权限 */
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
/** 申请权限 */
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
Toast.makeText(MainActivity.this,"Toast提示消息",Toast.LENGTH_SHORT).show();
}
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CAMERA}, Constant.REQ_PERM_CAMERA);
return;
}
/** 申请文件写入权限 */
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)!=PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, Constant.REQ_PERM_CAMERA);
}
/** 二维码扫码,调用注册的Activity,之前导入的包已经里面已经写好 */
Intent intent = new Intent(MainActivity.this, CaptureActivity.class);
/** 启动并标识Activity,后面扫描结束时需要唯一标识 */
startActivityForResult(intent, Constant.REQ_QR_CODE);
}
/**
* 当前页面启用其它活动时返回结果的回调
* @param requestCode
* @param resultCode
* @param data
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
/** 扫描结果回调 */
if (requestCode == Constant.REQ_QR_CODE && resultCode == RESULT_OK) {
Bundle bundle = data.getExtras();
String scanResult = bundle.getString(Constant.INTENT_EXTRA_KEY_QR_SCAN);
/** 将扫描出的信息显示出来 */
Toast.makeText(MainActivity.this,"扫描结果: " + scanResult,Toast.LENGTH_SHORT).show();
TextView res = findViewById(R.id.res);
res.setText("安卓端显示扫码结果:" + scanResult);
/** 调用接收扫码结束的函数,参数是扫码结果 */
mBridgeWebView.callHandler("scanResult",scanResult, new CallBackFunction() {
@Override
public void onCallBack(String data) {
Log.e("TAG","JS反馈的结果:" + data);
}
});
}
}
private long exitTime = 0;
/**
* 监听按键事件
* @param keyCode
* @param event
* @return
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
/** 监听回退事件,一秒点击两次回退才退出APP,否则只是使WebView回退到上一次访问的路径*/
if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
if (System.currentTimeMillis() - exitTime > 1000) {
Toast.makeText(this, "再按一次退出程序",Toast.LENGTH_LONG);
exitTime = System.currentTimeMillis();
/** WebView回退,与浏览器的回退一样*/
mBridgeWebView.goBack();
} else {
finish();
System.exit(0);
}
return true;
}
return false;
}
}
android的东西就那么多了,接下来看一下移动端的代码。
---------------------------------------------------------------------------我是分割线 ------------------------------------------------------------------------------
前端的关键不是用什么写页面,而是能用js注册于调用函数即可
7.使用JSBridge.js用来调用android注册的方法或者注册方法给android调用,写法比较固定,这里只针对安卓,IOS也支持,写法有点差异,JSBridge原理的文章很多,感兴趣自行百度
//执行回调函数
function setupWebViewJavascriptBridge(callback) {
//如果该对象已存在则直接执行
if (window.WebViewJavascriptBridge) {
callback(WebViewJavascriptBridge)
} else {
//否则添加事件监听再执行
document.addEventListener(
'WebViewJavascriptBridgeReady'
, function() {
callback(WebViewJavascriptBridge)
},
false
);
}
}
//注册回调函数
setupWebViewJavascriptBridge(function (bridge) {
//初始化
bridge.init(function (message, responseCallback) {
var data = {
'Javascript Responds': 'Wee!'
};
responseCallback(data);
})
})
//暴露当前模块的方法
export default {
// js调APP方法 (参数分别为:app提供的方法名 传给app的数据 回调)
callHandler(name, data, callback) {
setupWebViewJavascriptBridge(function (bridge) {
bridge.callHandler(name, data, callback)
})
},
// APP调js方法 (参数分别为:js提供的方法名 回调)
registerHandler(name, callback) {
setupWebViewJavascriptBridge(function (bridge) {
bridge.registerHandler(name, function (data, responseCallback) {
callback(data, responseCallback)
})
})
}
}
8.页面如何使用JSBridge
<template>
<div class="def" v-bind:class="backColor">
<button
v-bind:class="backColor"
type=""
style="width:80%;margin-top:100px;"
@click="scanQrCode()"
>扫码</button>
<div>当前颜色: {{backColor}}</div>
<div>HTML显示扫描结果: {{code}}</div>
</div>
</template>
<script>
import $JSbridge from './JSbridge'
export default {
name: "JSBridgeTest",
data() {
return {
backColor: 'black',
code:''
}
},
mounted() {
//执行注册给安卓调用的方法
this.registerUpdateBackColor();
this.registerScanResultFunction();
},
methods:{
//调用安卓的扫码方法,这里需要传递参数给安卓所以为空
scanQrCode() {
$JSbridge.callHandler('scanQrCode','',response => {
this.code = response;//
})
},
//注册接收扫码返回值的方法
registerScanResultFunction() {
$JSbridge.registerHandler("scanResult",(data,responseCallback) => {
//获得安卓的扫描结果
this.code = data;
//接收成功并返回值给安卓
responseCallback("success");
})
},
//注册修改背景色的方法给安卓调用
registerUpdateBackColor() {
$JSbridge.registerHandler("updateBackColor",(data,responseCallback) => {
this.backColor = data;
console.log(this.backColor)
responseCallback("success")
})
}
}
};
</script>
<style >
.def{
width: 100%;
height: 1000px;
}
.black{
background-color: black
}
.red{
background-color: red;
}
.blue{
background-color: blue;
}
</style>
到这里就可以达到开始图片时的效果了。