写在前面,该入坑指南是以Mob发送短信验证码为例,实现MUI第三方插件的开发,仅供参考。

123
123

HTML5+ 基座扩展采用三层结构,JS层、PluginBridge层和Native层。 三层功能分别是:
JS层: 在Webview页面调用,触发Native层代码,获取执行结果。
PluginBridge层: 将JS层请求进行处理,触发Native层扩展插件代码。
Native层: 插件扩展的平台原生代码,负责执行业务逻辑并执行结果返回到请求页面。

1. 预备环境

  • AndroidStudio开发环境,要求安装Android4.0或以上(API 14)SDK
  • 下载HBuilder离线打包Android版SDK(5+SDK下载)

1.1 离线SDK目录结构

1
2
3
4
5
6
|-HBuilder-Hello // 离线打包演示应用
|-HBuilder-Integrate // 5+ SDK 集成和插件开发示例
|-SDK // SDK库文件目录
|-Feature-Android.xls // Android平台各扩展Feature API对应的permission
|-readme.txt // 版本说明文件
|-重要提示.txt

2. 配置编译工程

使用Android Studio创建一个新的Android项目

2.1 导入jar包

  • SDK/libs/目录中选择需要导入的jar包,导入到工程目录的libs下。
    根据官方文档,至少需要添加
    1
    2
    3
    4
    5
    pdr.jar
    ui.jar
    nineoldandroids-2.4.0.jar
    my-imageloader.jar
    my-nineoldandroids-2.4.0.jar

在SDK/libs目录下并没有找到nineoldandroids-2.4.0.jar,所以并没有导入nineoldandroids-2.4.0.jar。

这里贴出我导入的jar包

1
2
3
4
5
6
7
8
9
10
11
// Mob
MobCommons-2017.0607.1736.jar
MobTools-2017.0607.1736.jar
SMSSDK-3.0.0.aar
SMSSDKGUI-3.0.0.aar
// MUI
my-imageloader.jar
my-nineoldandroids-2.4.0.jar
pdr.jar
ui.jar

添加成功后选中libs目录里的库文件,鼠标右键打开菜单,选择“Add As Library…”将填加到工程内的jar文件链接到工程里

2.2 添加资源到工程中

  • 打开工程目录的res文件夹,删除目录下已有的目录
  • 拷贝SDK/res目录下的全部目录到工程的res目录下
  • 在app->src->main目录下添加assets目录
  • 拷贝SDK/assets/目录下的data目录和res目录到工程的目录下

3. 修改原生工程的Androidmanifest.xml文件

3.1 修改application节点

  • 修改android:icon属性值为@drawable/icon
  • 删除android:theme属性
  • 删除allowBackup属性
  • 添加android:largeHeap=”true”
  • 添加 android:name=”io.dcloud.application.DCloudApplication”
  • 添加android:allowClearUserData=”true”

注意: 点击这里查看io.watasi.EntryActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<application
android:allowClearUserData="true"
android:icon="@drawable/icon"
android:label="@string/app_name"
android:largeHeap="true"
android:name="com.mob.MobApplication">
<activity
android:name="io.watasi.EntryActivity"
android:configChanges="orientation|keyboardHidden|keyboard|navigation"
android:label="@string/app_name"
android:launchMode="singleTask"
android:hardwareAccelerated="true"
android:theme="@style/TranslucentTheme"
android:screenOrientation="user"
android:windowSoftInputMode="adjustResize" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.mob.tools.MobUIShell"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:configChanges="keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="stateHidden|adjustResize"/>
<meta-data android:name="Mob-AppKey" android:value="f8be3fe06a28"/>
<meta-data android:name="Mob-AppSecret" android:value="d8add527dce9a9ba67cb8003ca49b937"/>
<service
android:name="io.dcloud.common.adapter.io.MiniServerService"
android:exported="true" />
</application>

3.2 添加文件支持的屏幕旋转方向

1
2
3
4
5
6
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:resizeable="true"
android:smallScreens="true" />

3.3 添加最低支持系统版本

1
2
<!--5 sdk支持的最低系统版本为8-->
<uses-sdk android:minSdkVersion="8" />

3.4 添加5+SDK必需引用的系统权限

1
2
3
4
5
6
<!-- HBuilder Runtime 基础权限 begin -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.GET_TASKS" />
<!-- Html5Plus runtime基础权限 end -->

3.5 添加Mob必须的系统权限

1
2
3
4
5
6
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

3.6 贴出整个Androidmanifest.xml文件

  • android:versionCode与mainfest.json下的version->code一致
  • android:versionName与mainfest.json下的version->name一致
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    <!--package="com.hyxk.ggmall.personal"-->
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="83" // 注意阅读上面
    android:versionName="1.0.0" // 注意阅读上面
    package="com.infozx.yaoqingba">
    <supports-screens
    android:anyDensity="true"
    android:largeScreens="true"
    android:normalScreens="true"
    android:resizeable="true"
    android:smallScreens="true" />
    <uses-sdk android:minSdkVersion="8" />
    <!-- HBuilder Runtime 基础权限 begin -->
    <!-- 联网 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <!-- 使用存储卡 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.GET_TASKS" />
    <!-- Html5Plus runtime基础权限 end -->
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.RECEIVE_SMS" />
    <uses-permission android:name="android.permission.READ_SMS" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <application
    android:allowClearUserData="true"
    android:icon="@drawable/icon"
    android:label="@string/app_name"
    android:largeHeap="true"
    android:name="com.mob.MobApplication">
    <activity
    android:name="io.watasi.EntryActivity"
    android:configChanges="orientation|keyboardHidden|keyboard|navigation"
    android:label="@string/app_name"
    android:launchMode="singleTask"
    android:hardwareAccelerated="true"
    android:theme="@style/TranslucentTheme"
    android:screenOrientation="user"
    android:windowSoftInputMode="adjustResize" >
    <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    </activity>
    <activity
    android:name="com.mob.tools.MobUIShell"
    android:theme="@android:style/Theme.Translucent.NoTitleBar"
    android:configChanges="keyboardHidden|orientation|screenSize"
    android:windowSoftInputMode="stateHidden|adjustResize"/>
    <!--配置Mob-->
    <meta-data android:name="Mob-AppKey" android:value="你的KEY"/>
    <meta-data android:name="Mob-AppSecret" android:value="你的Secret"/>
    <service
    android:name="io.dcloud.common.adapter.io.MiniServerService"
    android:exported="true" />
    </application>
    </manifest>

4. 修改工程源码

4.1 EntryActivity

这里贴上整个activity的源码,其中有部分地方需要修改
将appBasePath变量改为/apps/mainfest.json下的id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
package io.watasi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.FrameLayout;
import io.dcloud.EntryProxy;
import io.dcloud.RInformation;
import io.dcloud.common.DHInterface.IApp;
import io.dcloud.common.DHInterface.ICore;
import io.dcloud.common.DHInterface.IOnCreateSplashView;
import io.dcloud.common.DHInterface.ISysEventListener;
import io.dcloud.common.DHInterface.IWebview;
import io.dcloud.common.DHInterface.IWebviewStateListener;
import io.dcloud.feature.internal.sdk.SDK;
public class EntryActivity extends Activity {
boolean doHardAcc = true;
EntryProxy mEntryProxy = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
if (mEntryProxy == null) {
FrameLayout f = new FrameLayout(this);
// 创建5+内核运行事件监听
WebappModeListener wm = new WebappModeListener(this, f);
// 初始化5+内核
mEntryProxy = EntryProxy.init(this, wm);
// 启动5+内核
mEntryProxy.onCreate(this, savedInstanceState, SDK.IntegratedMode.WEBAPP, null);
setContentView(f);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
return mEntryProxy.onActivityExecute(this, ISysEventListener.SysEventType.onCreateOptionMenu, menu);
}
@Override
public void onPause() {
super.onPause();
mEntryProxy.onPause(this);
}
@Override
public void onResume() {
super.onResume();
mEntryProxy.onResume(this);
}
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (intent.getFlags() != 0x10600000) {// 非点击icon调用activity时才调用newintent事件
mEntryProxy.onNewIntent(this, intent);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
mEntryProxy.onStop(this);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
boolean _ret = mEntryProxy.onActivityExecute(this, ISysEventListener.SysEventType.onKeyDown, new Object[] { keyCode, event });
return _ret ? _ret : super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
boolean _ret = mEntryProxy.onActivityExecute(this, ISysEventListener.SysEventType.onKeyUp, new Object[] { keyCode, event });
return _ret ? _ret : super.onKeyUp(keyCode, event);
}
@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
boolean _ret = mEntryProxy.onActivityExecute(this, ISysEventListener.SysEventType.onKeyLongPress, new Object[] { keyCode, event });
return _ret ? _ret : super.onKeyLongPress(keyCode, event);
}
public void onConfigurationChanged(Configuration newConfig) {
try {
int temp = this.getResources().getConfiguration().orientation;
if (mEntryProxy != null) {
mEntryProxy.onConfigurationChanged(this, temp);
}
super.onConfigurationChanged(newConfig);
} catch (Exception e) {
e.printStackTrace();
}
}
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
mEntryProxy.onActivityExecute(this, ISysEventListener.SysEventType.onActivityResult, new Object[] { requestCode, resultCode, data });
}
}
class WebappModeListener implements ICore.ICoreStatusListener, IOnCreateSplashView {
Activity activity;
View splashView = null;
ViewGroup rootView;
IApp app = null;
public WebappModeListener(Activity activity, ViewGroup rootView) {
this.activity = activity;
this.rootView = rootView;
}
/**
* 5+内核初始化完成时触发
* */
@Override
public void onCoreInitEnd(ICore coreHandler) {
// 表示Webapp的路径在 file:///android_asset/apps/HelloH5
String appBasePath = "/apps/H5512A60A";
// 设置启动参数,可在页面中通过plus.runtime.arguments;方法获取到传入的参数
String args = "{url:'http://www.baidu.com'}";
// 启动启动独立应用的5+ Webapp
app = SDK.startWebApp(activity, appBasePath, args, new IWebviewStateListener() {
// 设置Webview事件监听,可在监监听内获取WebIvew加载内容的进度
@Override
public Object onCallBack(int pType, Object pArgs) {
switch (pType) {
case IWebviewStateListener.ON_WEBVIEW_READY:
// WebApp准备加载事件
// 准备完毕之后添加webview到显示父View中,
// 设置排版不显示状态,避免显示webview时html内容排版错乱问题
View view = ((IWebview) pArgs).obtainApp().obtainWebAppRootView().obtainMainView();
view.setVisibility(View.INVISIBLE);
if(view.getParent() != null){
((ViewGroup)view.getParent()).removeView(view);
}
rootView.addView(view, 0);
break;
case IWebviewStateListener.ON_PAGE_STARTED:
// 首页面开始加载事件
break;
case IWebviewStateListener.ON_PROGRESS_CHANGED:
// WebApp首页面加载进度变化事件
break;
case IWebviewStateListener.ON_PAGE_FINISHED:
// WebApp首页面加载完成事件
// 页面加载完毕,设置显示webview
// 如果不需要显示spalsh页面将此行代码移动至onCloseSplash事件内
app.obtainWebAppRootView().obtainMainView().setVisibility(View.VISIBLE);
break;
}
return null;
}
}, this);
app.setIAppStatusListener(new IApp.IAppStatusListener() {
// 设置APP运行事件监听
@Override
public boolean onStop() {
// 应用运行停止时调用
rootView.removeView(app.obtainWebAppRootView().obtainMainView());
// TODO Auto-generated method stub
return false;
}
@Override
public void onStart() {
// 独立应用启动时触发事件
}
@Override
public void onPause(IApp arg0, IApp arg1) {
// WebApp暂停运行时触发事件
}
@Override
public String onStoped(boolean arg0, String arg1) {
// TODO Auto-generated method stub
return null;
}
});
}
@Override
public void onCoreReady(ICore coreHandler) {
// 初始化5+ SDK,
// 5+SDK的其他接口需要在SDK初始化后才能調用
SDK.initSDK(coreHandler);
// 设置当前应用可使用的5+ API
SDK.requestAllFeature();
}
@Override
public boolean onCoreStop() {
// 当返回false时候回关闭activity
return false;
}
// 在Widget集成时如果不需要显示splash页面可按照如下步骤操作
// 1 删除onCreateSplash方法内的代码
// 2 将5+mainView添加到rootview时将页面设置为不可见
// 3 在onCloseSplash方法中将5+mainView设置为可见
// 4 修改androidmanifest.xml文件 将SDK_WebApp的主题设置为透明
// 注意!
// 如果不显示splash页面会造成用户点击后页面短时间内会没有变化,
// 可能会给用户造成程序没响应的错觉,
// 所以开发者需要对5+内核启动到5+应用页面显示之间的这段事件进行处理
@Override
public Object onCreateSplash(Context pContextWrapper) {
splashView = new FrameLayout(activity);
splashView.setBackgroundResource(RInformation.DRAWABLE_SPLASH);
rootView.addView(splashView);
return null;
}
@Override
public void onCloseSplash() {
rootView.removeView(splashView);
}
}

4.2 在java目录下添加io.dcloud包

4.3 拷贝SDK\src\io\dcloud\Rinfomation.java文件到工程的io.dcloud包下

1
2
3
4
5
6
7
8
package io.dcloud;
/**
* 本文件是5+SDK使用的资源索引
* 工程引入本文件的包名必须是“io.dcloud”
* */
public class RInformation extends PdrR {
public static int VIEW_LAYOUT_SPLASH = 0;
}

5. 添加Web应用资源到工程

在assets目录下创建apps/[appid]/www目录,将web应用拷贝到www目录下,[appid]为Web应用mainfest.json文件id节点的内容。

  • 修改src/main/assets/data/dcloud_control.xml文件,修改app节点的appid属性和appver属性的值。appid属性填写Web应用的mainfest.json文件的ID节点内容,appver属性填写manifest.json文件的version节点下的name节点的内容。

6. 插件开发

这里以发送短信验证码为例,创建SendMsg.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package io.watasi;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import org.json.JSONArray;
import cn.smssdk.EventHandler;
import cn.smssdk.OnSendMessageHandler;
import cn.smssdk.SMSSDK;
import io.dcloud.common.DHInterface.IWebview;
import io.dcloud.common.DHInterface.StandardFeature;
import io.dcloud.common.util.JSUtil;
public class SendMsg extends StandardFeature {
public void onStart(Context pContext, Bundle pSavedInstanceState, String[] pRuntimeArgs) {
initSDK();
}
public static void initSDK() {
// 创建EventHandler对象
EventHandler eventHandler = new EventHandler() {
@Override
public void afterEvent(int event, int result, Object data) {
if (result == SMSSDK.RESULT_COMPLETE) {
//回调完成
if (event == SMSSDK.EVENT_SUBMIT_VERIFICATION_CODE) {
//提交验证码成功
} else if (event == SMSSDK.EVENT_GET_VERIFICATION_CODE) {
//获取验证码成功
} else if (event == SMSSDK.EVENT_GET_SUPPORTED_COUNTRIES) {
//返回支持发送验证码的国家列表
}
} else {
((Throwable) data).printStackTrace();
}
}
};
// 注册监听器
SMSSDK.registerEventHandler(eventHandler);
}
public void send(final IWebview pWebview, JSONArray array){
final String CallBackID = array.optString(0);
String tel = array.optString(1);
SMSSDK.getVerificationCode("86", tel, new OnSendMessageHandler() {
@Override
public boolean onSendMessage(String s, String s1) {
JSUtil.execCallback(pWebview, CallBackID, "登录成功", JSUtil.OK, false);
return false;
}
});
}
}

注意:插件编写完成后,一定要在assets/data/dcloud_properties.xml文件中声明插件类别名和Native层扩展插件类的对应关系

1
2
3
4
5
6
7
8
9
10
11
12
<properties>
<features>
<!--name为类名, value为类所在的路径-->
<feature name="SendMsg" value="io.watasi.SendMsg" />
</features>
<services>
<service name="push" value="io.dcloud.feature.aps.APSFeatureImpl"/>
<service name="Statistic" value="io.dcloud.feature.statistics.StatisticsBootImpl"/>
<service name="Downloader" value="io.dcloud.net.DownloaderBootImpl"/>
</services>
</properties>

7. 编写插件JS API && 调用JS API

7.1 编写API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
document.addEventListener("plusready", function() {
// 声明的JS“扩展插件别名”
var _BARCODE = 'SendMsg', // 与dcloud_properties.xml中声明插件类别名相同
B = window.plus.bridge;
window.plus.sendMsg = {
send: function(phone, successCallback, errorCallback) {
var success = typeof successCallback !== 'function' ? null : function(args) {
successCallback(args);
};
var fail = typeof errorCallback !== 'function' ? null : function(code) {
errorCallback(code);
};
callbackID = B.callbackId(success, fail);
// 第二个参数与原生android实现的方法名相同
return B.exec(_BARCODE, "send", [callbackID, phone]);
}
}
}, true);

7.2 页面调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<title></title>
<link href="css/mui.min.css" rel="stylesheet"/>
<style type="text/css">
.mt20{margin-top: 20px;}
.btn-wrapper{padding: 0 15px;}
.btn-wrapper button {padding: 10px 0;}
</style>
</head>
<body>
<header class="mui-bar mui-bar-nav">
<h1 class="mui-title">登录</h1>
</header>
<div class="mui-content">
<form class="mui-input-group mt20">
<div class="mui-input-row">
<input type="tel" class="phone" class="mui-input-clear" placeholder="请输入手机号">
</div>
<div class="mui-input-row">
<input type="number" class="code" class="mui-input-clear" placeholder="验证码">
</div>
</form>
<div class="btn-wrapper">
<button id="sendCode" class="mui-btn mui-btn-block mui-btn-primary mt20">发送验证码</button>
<button class="mui-btn mui-btn-block mui-btn-danger">登录</button>
</div>
</div>
<script src="js/mui.min.js"></script>
<script src="plugs/sendMsg.js"></script>
<script type="text/javascript">
mui.init();
document.querySelector("#sendCode").addEventListener("tap", function() {
var phone = document.querySelector(".phone").value;
plus.sendMsg.send(phone, function(){}, function(){}) ;
}, false);
</script>
</body>
</html>