插件开发手册-Android(Beta版)


目录

第一章 SDK 简介

369Cloud引擎以实现对操作系统底层能力的封装和扩展,通过WebKit引擎开放API给Javascript调用的形式,实现了HTML+CSS+Javascript开发语言和android/Java等Plugin开发语言之间的桥接,极大的丰富和增强了标准Javascript的能力。令前端开发者通过JS即可调用移动设备的底层功能。

第二章 阅读对象

本文档面向所有使用该SDK的Android开发人员、测试人员、合作伙伴以及对此感兴趣的其他用户。阅读该文档要求用户熟悉Android应用开发,并且对Html、CSS、Javascript有一定了解,369Cloud引擎强调传输数据的简洁和统一性,因此支持轻量级的JSON(当然也支持其他格式如:xml、文本等格式)作为Javascript和Plugin之间通讯的数据载体,所以要求开发者同时要熟悉Java和Javascript中JSON格式数据的操作。

第三章 SDK功能说明

Android SDK是开发者快速入门使用369Cloud跨平台移动应用开发引擎,并熟练掌握开发Plugin摸块(Android平台)技巧及了解369Cloud移动应用云平台的桥梁。

1. 框架设计

插件开放桥接机制,方便具有一定Android基础的开发者自由开发定义Plugin扩展模块,丰富JS的能力,提升App的用户体验。

369Cloud引擎框架桥接层设计如下图:

image

第四章 开发前准备

4.1 开发环境:

PC:Windows XP/Win7/8/Mac OS
Android Studio或Eclipse
Android SDK 21
JDK1.7及以上
Android环境推荐使用Google的AndroidStudio:SDK ADT Bundle,
下载地址: https://developer.android.com/sdk/installing/studio.html#download

Eclipse下载地址:http://www.eclipse.org/downloads

点击此处跳转到插件开发模版下载地址请保持插件工程结构如下图,否则makePluginZip脚本会执行失败。

image

4.2 开发帮助参考:

Android在线API文档: http://developer.android.com/reference/packages.html
Javascript规范及入门: http://www.w3school.com.cn
JSON数据在线 Viewer :http://www.bejson.com/go.html?u=http://www.bejson.com/jsonview2/

第五章 使用SDK开发Plugin模块

开发Plugin模块可通过Android StudioEclipse开发平台开发。Eclipse平台google已经停止了更新维护,推荐使用Android Studio开发,这也是以后的趋势。两者的区别主要是前者多了gradle配置方便打插件包以及更新依赖库,其他过程大同小异。具体开发流程请看下面详细介绍。

5.0 工作机制

5.0.1 设计原理

因为Javascript无法访问本地功能,设计了一个Javascript桥可以将类似rd.插件名.功能名(参数)或者var plugin = rd.require('插件名'); plugin.功能名(参数);的调用方式桥接到本地代码:

  • 方式1 - rd.插件名.功能名(参数);
    比如一个插件叫做imageTools,有一个功能convertToPNG(uri, to, callback)(将地址所制定的图片转换为PNG)。
    调用方式为:rd.imageTools.convertToPNG('/folder/1.jpg', '/folder/1.png',function(uri){alert(uri);});
    功能描述:将目录folder下的1.jpg图片转换为PNG并将转换得到的图片命名为1.png保存到目录folder里。
  • 方式2 - var plugin = rd.require('插件名'); plugin.功能名(参数);
    还是以imageTools为例,我们的调用方式就变为:
    var imageTools = rd.require('imageTools');
    imageTools.convertToPNG('/folder/1.jpg', '/folder/1.png', function(uri){alert(uri);});
5.0.2 运行过程

还是以imageTools为例,引擎根据plugin.xml里的配置对插件进行初始化(这里又会通过domain来确定插件名字,通过scope确定生命周期范围)。

  • 初始化时:引擎在APP载入时根据scope和domain,如果scope为非createNew时初始化一个Javascript对象实例并将之附着到rd实例上,如果scope为createNew就在rd.require函数里加上一段初始化此对象的代码,在调用到require时再初始化,然后将初始化后的对象返回。
  • 调用:
    • 引擎初始化插件对象时生成的JS对象可以认为是插件在JS端的一个代理,插件的每一个属性都会映射成JS端对象的property,每一个方法映射成JS端对象的function。每一个function中都包含一段特殊定义的字符串用于描述调用哪个插件的哪个方法。
    • 在插件功能调用时,引擎负责搜索哪个插件哪个方法,并将参数包装成字符串数组传入插件的方法(插件方法的申明形式:public void convertToPNG(RDCloudView view, String[] params)),这里的params即是JS端传入的参数。如果有返回值且是同步返回,则引擎根据配置将返回值返回到JS端,如果是异步返回方式则需要手动调用JsCallback方法。

5.1 Android Studio开发配置

5.1.1 导入工程

首先打开Android Studio导入引擎工程,File->New->Import project,选择引擎工程,所有的外部插件都放到Plugin_libs目录下。工程结构如下图:

image

  • Hybrid_Framework:

    • assets/ 外部资源存放目录,存放html、js、css等测试代码和资源
    • build/ 编译使用的目录,打好的APK包,插件包都存放到这里
    • libs/ 依赖包和so库存放目录
    • res/ 引擎资源存放目录
    • src/ 空目录
    • AndroidManifest.xml Android配置文件
    • build.gradle 编译脚本文件
    • Hybrid_Framework.iml AndroidStudio配置文件
  • 插件目录:

    • build/ 编译使用的目录,打好的APK包,插件包都存放到这里
    • libs/ 依赖包和so库存放目录
    • res/ 引擎资源存放目录
    • src/ 代码目录
    • AndroidManifest.xml Android配置文件
    • build.gradle 编译脚本文件
    • plugin.xml 插件配置文件
    • 插件名.iml AndroidStudio配置文件
5.1.2 新建Module

右键点击Plugin_libs——>New——>Module,需要注意的是创建Module的时候选择Android Library,创建Module完成之后先配置build.gradle,打开build.gradle配置引擎sdk的依赖。

    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        provided project.hde_version //插件开发时的引擎依赖版本
        compile project.supportv4_version
    }

详细配置见下图:

image

Android Studio编译内置gradle,这是一款强大的编译系统,详情请参考官方网站http://developer.android.com/tools/building/configuring-gradle.html

以下是编译配置的简要说明下:

  • apply plugin 'com.android.library' 申明此工程为库工程
  • android:
    • compileSdkVersion 编译sdk版本
    • buildToolsVersion 构建sdk版本
    • defaultConfig:
      • minSdkVersion 最小适配版本(目前引擎最小适配版本为14,如果插件的最小适配版本较大,使用此插件建立的项目以插件最小适配版本为主)
      • versionCode 版本号
      • versionName 版本名
    • buildTypes
      • release
        • minifyEnabled 是否混淆
        • proguardFiles 混淆配置文件
  • dependencies 依赖配置块
    • compile 依赖并编译进工程
    • provided 依赖但不编译进工程

值得注意的是:project.supportv4_version和project.hde_version在工程根目录的gradle.properties里配置 查看最新依赖库版本请点击这里。

如果有引用so文件,需要在根目录创建一个jniLibs目录把so文件放里面。打开在build.gradle文件在android标签里面配置如下代码:

 sourceSets {
     main {
         jniLibs.srcDirs = ['jniLibs']
     }
 }

打开settings.gradle文件把新增的plugin包含进去,添加如下代码

include ':Plugin_Libs:plugin_demo'

右键Hybrid_Framework引擎工程——>Open Module Settings——>Dependencies——>点击右侧的“+”——>Module Dependency 选择新增的plugin,添加对Plugin的依赖。如下图

image 如果设置完成引起编译失败这很有可能是因为同步块中project.hde_version和其他依赖配置被放到了一行引起的,请到Hybrid_Framework->build.gradle里的dependencies块去手动设置依赖,如下:

dependencies {
    compile fileTree(dir: 'libs', include: '*.jar')
    compile 'com.android.support:appcompat-v7:21.0.3'
    compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.4'
    compile project.hde_version
    compile project(':Plugin_Libs:plugin_demo')
    compile project(':Plugin_Libs:plugin_imageTools')
}

至此,Android studio的相关配置已设置完成(注意gradle配置修改后需要同步脚本后,修改才会生效)。具体开发Plugin模块的流程请参见开发Plugin模块

5.2 Eclipse开发流程

5.2.1 导入工程

首先打开Eclipse导入369Cloud引擎工程及android-support-v7-appcompat工程,File->Import->Android->Existing Android Code Into Workspace->选择项目路径->完成。

5.2.2 新建Plugin工程

369Cloud引擎工程已导入到项目中,下面就是创建Plugin工程。 File->New->Project->Android->Android Application Project 有个地方需要注意一下,Package Name的前缀必选带着com.xhrd.mobile.hybridframework.plugin,否则引擎无法解析。 完善其他信息创建完成,项目结构如下图: image

开发Plugin工程之前需要先依赖369Cloud引擎的jar包,369Cloud引擎的jar存放在引擎工程的libs目录下hde-obfuscated-1.3.2.jar(369Cloud引擎jar包是以版本号命名的,随着版本的升级369Cloud引擎ar包名也会随着改变),369Cloud引擎jar包以后简称hde.jar。我们需要在Plugin工程根目录单独创建一个目录来存放hde.jar。建议以hde_libs命名,并把hde.jar拷贝到此目录下,之后添加到构建路径里面,如下图:

image

Plugin工程是依赖369引擎运行的,所以需要把插件工程属性类型改为Is Library,并把此Plugin工程添加到369引擎库中。右键Plugin工程->property->android->Is Library打勾。如下图: image

插件依赖的jar包不能打进插件里,否则会引起编译失败。在eclipse->Java Build Path->Order and Export里将依赖的jar取消打勾,就不打进插件包了,如下图:

image

369引擎添加Plugin工程库,右键369引擎工程->property->android->Add 选择Plugin工程。如下图 image

注意事项

Eclipse下新建的工程一般都会自动创建android-support-v4.jar或android-support-v7.jar的引用,369cloud引擎里已有相关的引用。所以需要把插件工程里面的android-support-v4.jar或android-support-v7.jar删除,不然会引起冲突报错。插件AndroidManifest.xml里面的sdk版本也需要和369引擎的sdk版本一致。

至此,Eclipse的相关配置已设置完成。具体开发Plugin模块的流程和Android Studio大同小异请参见开发Plugin模块

5.3 开发Plugin模块

5.3.1 创建对象

在项目中新建Java类(以下以PluginDemo类为例,映射的JS对象调用方式:rd.require("PluginDemo")或者 rd.PluginDemo(根据配置在plugin.xml里面的scope区分调用方式)),继承自引擎包中的PluginBase类,并重写相关函数。 如下图: image

5.3.2 创建方法

若想将Java类中的某个函数映射至JS对象供JS调用,需要将该函数声明为public,同时接收两个参数:RDCloudView类型的rdCloudView(不需要传递,引擎自动传递当前调用窗口webview核心过来,也就是当前RDCloudView的实例传递),String[]类型的params(注意:js端传递的参数都会包装成字符串数组通过params获取), 并且在函数加上注解@JavascriptFunction。前端开发者就可以在html页面中即可使用“rd.PluginDemo.函数名”的方式直接调用java的函数,并进行相关操作。

5.3.3 方法类别一般分为三种:无返回值、有返回值、有回调方法
  • 无返回值方法和普通void方法一样。
  • 有返回值方法的返回类型可以是字符串,基础类型和JSString、JSNumber、JSObject、JSScript、JSFuncResult(不支持其他类),如果返回null就不会给等号左边的变量赋值。有返回值有分为两种:
    • 异步回调:即是有回调函数,js前端在调用的时候需要传递回调函数(js函数)给Plugin端等待触发,如不传递回调函数将无法执行回调事件。此方式适用于执行耗时费事的任务,在任务结束时再通过回调函数返回结果。回调函数触发主要有两种方式:jsCallback(推荐),jsonCallBack
    • 同步回调:即是有返回值的方法,此方法会在执行完所有代码后才返回。适用于任务量小,不耗时不费事的任务,以免阻塞主线程导致卡顿。

下面列出插件接口的方法原型:
//winName的格式为"<?comp?>当前component名字<?comp?><?window?>当前window的名字<?window?>"的组合方式(如<?comp?>main<?comp?><?window?>index<?window?>)。推荐使用-返回类型 函数名(RDCloudView rdCloudView, String params[])的方式

以component和window组合为参数

@JavascriptFunction
public [void|String|JSFuncResult|JSString...] 方法名(String name, String[] params){
}
  • @JavascriptFunction:申明此方法为plugin暴露的接口(注意:这里只是申明了此方法可暴露给JS端。还需要在plugin.xml里配置接口的接口名、是否有返回值和属性)
  • winName: 当前调用window/popover的名字
  • params: 参数列表

以RDCloudView为参数

@JavascriptFunction
public [void|String|JSFuncResult...] 方法名(RDCloudView rdView, String[] params){
}
  • @JavascriptFunction:申明此方法为plugin暴露的接口(注意:这里只是申明了此方法可暴露给JS端。还需要在plugin.xml里配置接口的接口名、是否有返回值和属性)
  • rdView: 当前调用window/popover的实例
  • params: 参数列表

值得注意的是:

  1. 推荐使用RDCloudView类型的申明方法。
  2. 方法的参数都是可选的,可以任意组合,也可以不需要。
  3. String:普通java字符串,适合JS熟悉的人使用
  4. JSString:JS字符串,用于返回字符串
  5. JSNumber:JS数字,用于返回数字,其中第二个boolean参数代表是否有小数
  6. JSObject:JS对象,用于返回JS对象(比如JS Object和Array等等),详情参考模版例子
  7. JSScript:JS代码块对象,用于执行一段脚本
  8. JSFunction:JS函数,用于异步回调
  9. IJSDataList:JS数据集合,用于JSFunction添加参数
  10. JSFuncResult:返回结构,用于返回前执行一段JS代码,其中包含了preScript(String)和result(Object)
    • preScript:在返回Result前执行的一段Javascript
      • 注意:要想成功执行preScript需要在plugin.xml里把convertReturn配置为true。如果convertReturn=false并且return=true,则preScript的内容会做为字符串拼接到result的内容之前(比如:preScript="a",result="b",则结果为"a,b")。如果convertReturn=false并且return=false,则preScript不会执行
        • result:返回的结果,可以是String,JSString,JSNumber,JSObject的实例

可用的函数申明格式的例子:

  • 无返回值,传入window/popover的名字
    @JavascriptFunction
    public void 函数名(String winName,String[] params){ }

  • 有返回值的方法是同步返回的,传入window/popover的名字
    @JavascriptFunction
    public String 函数名(String winName,String[] params){

      return "hello world";
    

    }

  • 无返回值,传入RDCloudView实例
    @JavascriptFunction
    public void 函数名(RDCloudView rdView,String[] params){ }

  • 有返回值的方法是同步返回的,传入RDCloudView实例
    @JavascriptFunction
    public String 函数名(RDCloudView rdView,String[] params){

       return "hello world";
    

    }

  • 回调事件 @JavascriptFunction public void 函数名(String winName,String[] params){

      //也可异步回调
      jsCallback(getTargetView(winName),false, succFunc, "hello world");//传递普通格式数据回调
      jsonCallBack(getTargetView(winName),false, succFunc, "{msg:'hello world'}");//传递json格式数据回调
    

    }

通过JSFunction回调事件:

@JavascriptFunction
public void 函数名(String winName,String[] params){
    JSFunction func = new JSFunction(params[0], true);
    //加入字符串参数
    func.addParam(new JSString("plugin_demo_getStringAsync()函数被回调了"));
    //加入整数参数
    func.addParam(new JSNumber(1000, false));
    //加入JSObject参数
    func.addParam(new JSObject("{a:1}"));
    //加入字符串参数
    func.addParam(new JSString(params[1]));
    //加入参数数组
    func.addParams(new JSNumber(1000.001, true), new JSString("hahahaha"));
    //参加
    List<IJSData> datas = new ArrayList<IJSData>();
    datas.add(new JSBoolean(true));
    datas.add(new JSString("hello world"));
    func.addParams(datas);
    //加入IJSDataList的参数
    IJSDataList dataList = new IJSDataList();
    dataList.addParamString("by addParamString");
    dataList.addParamNumber(123.12);//dataList.addParamNumber(123, true)
    dataList.addParamObject("{a:'by addParamObject'}");
    func.addParams(dataList);
    //回调
    jsCallback(rdCloudView, func);
  }
  • 以下是插件方法的常用写法格式

    • 只传入window名称

       @JavascriptFunction<br/>
      

      public [void|String|...] 函数名(String winName){ }

    • 只传入参数

       @JavascriptFunction<br/>
      

      public [void|String|...] 函数名(String[] params){ }

    • 无参

       @JavascriptFunction<br/>
      

      public [void|String|...] 函数名(){ }

    • 只传入RDCloudView实例

       @JavascriptFunction<br/>
      

      public [void|String|...] 函数名(RDCloudView rdView){ }

  • 以下是异步回调常用写法格式

    • jsCallback(final RDCloudView view, boolean remove, String func, Object... params)
      • jsCallback(final RDCloudView rdCloudView, boolean remove, String func, Object content)
    • jsCallback(final RDCloudView rdCloudView, final JSFuncResult result)
    • jsCallback(final RDCloudView rdCloudView, final JSFunction function)
5.3.4 调用方法

369Cloud引擎使用String格式数据作为JS与Plugin之间交换数据的传参。引擎会对JS传入的参数进行以String[]形式进行封装,例如js端传给java端两个参数,插件接口的参数申明为String[] params,则通过params[0]获取第一个参数值,params[1]获取第二个参数值。

例如 Html页面这样写: image

同时JS端也可以传入JS端的回调函数,如图传入了currentFunc和cancelFunc,那么在插件中则可以这样获取JS传入的参数值,然后将操作结果回调至JS

image

这里采用了异步回调方式,Plugin做完相关操作后,才将操作结果通知给JS页面或者通知页面本次操作成功与否、错误提示等。该回调过程由Java端PluginBasejsCallbackjsonCallBack等函数实现现了对“confirm”结果的回调。

5.3.5 配置插件信息(名字、属性、方法等待)

当Java类中写完功能后,需要把插件的一些信息配置到相应位置或文件,这样引擎在初始化时,会根据配置信息转为JS对象供调用!

5.3.5.1 第三方插件配置plugin.xml

对于第三方插件,369Cloud引擎要求Plugin模块扩展开发者必须在plugin.xml文件中声明被映射Java类的全限定名(className),以及其所映射的JS对象名称(domain)和其他属性,后面会提到。369Cloud引擎根据该文件寻找相应Java类,并在适当时候将其初始化。 plugin.xml文件固定存放在Plugin工程的根目录,Android Studio开发平台不需要在本地assets/hybrid/plugin/plugins.xml配置文件里添加每个插件的配置信息,编译时会自动将依赖的所有插件的plugin.xml合并Eclipse开发平台只支持手动合并到工程Hybrid_Framework的assets/hybrid/plugin/plugins.xml中。。格式声明如下:

<plugin className="com.xhrd.mobile.hybridframework.plugin.plugin_demo.PluginDemo"
    domain="PluginDemo" <!--用于在js端调用的对象名称-->
    scope="createNew" <!-- 映射对象的有效范围,可看下面的详细解释 -->
    lifeCycleCallback="true"<!--是否接受component的声明周期回调-->
    version="1.0.0">
     <property <!--属性一般用于状态的判断-->
        name="Succeed" <!--属性名称-->
        value="0" <!--属性值-->
        />
    <method name="toast" <!--方法名称-->
            convertReturn="true" <!--如果没有返回值可以忽略不写, 返回值是否转换js格式-->
            return="true" <!--如果没有返回值可以忽略不写,是否有返回值-->
        />
    <method name="confirm" />
</plugin>

属性解释:

  • domain:域名,用于定义该扩展模块所对应的JS对象名,类似于标准JS中的window、document、Math等对象。
  • className:全限定名,用于定义该扩展模块对应被JS映射的Java类。
  • scope:作用域,用于定义插件的生命周期范围,不同的作用域,生命周期也就不同。有app、window、local、createNew四种,详情参考插件生命周期
  • method:暴露的方法名称,会映射成JS端的函数,包含几个属性:
    • name:暴露方法的名称。
    • return:true表示方法有返回值,反之和不写表示没有返回值。
    • convertReturn:true表示将返回值转换成JS,反之和不写表示不转换。
  • property:对应模块的属性。
    • name:属性名称。
    • value:属性值。
  • lifeCycleCallback
    :true表示接受component的生命周期回调,反之和不写表示不回调,详情请参考onLifeCycleChanged
  • version:插件版本号。
5.3.6
Scope生命周期

每个插件都有自己生命周期,不同的生命周期就意味着不同的作用域,它是通过plugin.xml里的scope属性来确定的。他们有四种分别如下:

  • app:全局,插件生命周期跟app生命周期相同,调用对象方式rd.JS对象名(例如rd.demo)。
  • window:窗口,插件生命周期跟window生命周期相同。调用对象方式rd.JS对象名(例如rd.demo)。
  • local:window或popover,插件生命周期跟window或popover生命周期相同。调用对象方式rd.JS对象名(例如rd.demo)。
  • createNew:window或popover,在存在UI的时候设置,每次使用都会创建一个新实例,非UI也可以是createNew。调用对象方式rd.require('JS对象名')(例如rd.require('demo'))。

Plugin有UI插件和非UI插件之分,不同插件需要通过设置不同的scope。带UI的插件scope必须设为createNew,其它的根据情况设置。

5.3.7 编译期属性配置

有一些功能在开发时需要申请第三方服务,比如QQ分享:需要申请微信开发平台账号,再通过APP的包名和签名去申请功能的服务id,这些id可能需要在AndroidManifest.xml里配置,此小段专门为处理这种情况而生。

当Plugin里面有第三方sdk并且需要配置appkey的时候,还需要在plugin根目录新建template文件(没有后缀名)进行配置appkey的键值对,代码如下:

<application>
    <meta-data android:name="app_key" android:value="app_key_value"/>
</application>
  • app_key:AndroidManifest.xml里要求配置的key
  • app_key_value:占位符,对应key的值。需要在369Cloud IDE里编辑pluginConfigs.xml新增一个键值对对应起来。
5.3.7.1 pluginConfigs.xml

pluginConfigs.xml文件的存在是由于某些插件需要编译期添加配置信息,pluginConfigs.xml就可以配置这些信息,这些信息在运行期间也是可以通过引擎提供的方法(AppKeyXmlUtil.getPluginAppKeyMap)拿到的。 新增如下代码,具体的配置可点击PluginConfigs.xml查看

pluginConfigs.xml的格式为:

<config pluginName="对应的插件domain">
    <android>
        <param key="app_key_value" value="tencent100371282"/>
    </android>
    <ios>...</ios>
</config>

比如要配置极光推送:

<config pluginName="JPush">
    <android>
        <param key="JPUSH_CHANNEL_VALUE" value="my_android_channel"/>
        <param key="JPUSH_APPKEY_VALUE" value="my_android_app_key"/>
    </android>
    <ios>
        <param key="JPUSH_CHANNEL_VALUE" value="my_ios_channel"/>
        <param key="JPUSH_APPKEY_VALUE" value="my_ios_app_key"/>
    </ios>
</config>
5.3.8
编译期需替换修改内容、文件等操作
  • 在插件项目根目录下建立replacement文件(文件无后缀名,内容为properties格式)
5.3.8.1
需引擎使用插件中自定义的application

有一些特殊的功能,需自定义application,打包项目时并让引擎使用(继承)此application,此小段专门为处理这种情况而生。

  • 根据相应功能,自定义application(例:com.xhrd.mobile.test.MyApplication)
  • 在文件中写入配置信息:application=com.xhrd.mobile.test.MyApplication
5.3.8.2
修改插件中某个类的包名

有一些插件使用的第三方库要求要把某些类放到项目编译的包名(apk包名)中。此问题,需把这些类的配置到配置文件中,类与类之间用|隔开,服务器编译时自动移动相关类文件,如果类文件在manifest有注册,也会修改注册name字段。

  • 在replacement文件中写入replacePackage=com.xhrd.test.A.java|com.xhrd.test.B.java
5.3.9 PluginBase
5.3.9.1 PluginBase类

PluginBase是所有的插件的基类,插件必须继承PluginBase,下面介绍一下常用的方法。

  • protected View genUI(RDCloudView view); 所有插件必须重写此方法,一般都用于存在UI的时候,存在UI的scope必须设为createNew。也必须重写isUI(),不然无法获取View。如果不存在UI可忽略,UI插件可动态创建,也可通过资源文件获取。获取资源文需要注意,引擎已经封装了获取资源对象RDResourceManager,所有获取id的行为均必须通过RDResourceManager提供的方法获取。

        Override
      protected View genUI(RDCloudView view) {
          //动态创建UI
          TextView tv =  new TextView(view.getContext());
          //通过资源文件获取UI
          LayoutInflater mLayoutInflater = LayoutInflater.from(view.getContext());
          View view = mLayoutInflater.inflate(RDResourceManager.getInstance().getLayoutId("layout name"), null);
          TextView tv2 = (TextView)view.findViewById(RDResourceManager.getInstance().getId("textview"));
          tv.setText("hello world");
          return tv;
      }
    
      @Override
      public boolean isUI() {
          return true;
      }
    
      /**
       * 设置UI插件的绑定类型,非UI插件设置无效
       * @param rdCloudView
       * @param params
      */
      public void setUIBindingType(int type){}
    
  • public void onCreate(RDCloudView view); 在创建插件时调用

      @Override
           public void onCreate(RDCloudView view) {
          super.onCreate(view);
          //执行相关初始化操作
           }
    
  • public void onDestroy(RDCloudView view); 在销毁插件时回调

      @Override
          public void onDestroy(RDCloudView view) {
          super.onDestroy(view);
          //执行相关销毁操作
          }
    
  • public void onRegistered(RDCloudView view); 在注册插件时回调

      @Override
          public void onRegistered(RDCloudView view) {
          super.onRegistered(view);
          //执行相关注册操作
          }
    
  • public void onDeregistered(RDCloudView view); 在注销插件时回调

      @Override
          public void onDeregistered(RDCloudView view) {
          super.onDeregistered(view);
          //执行相关注销操作
           }
    

    以上四个方法是插件的生命周期方法,回调顺序onCreate->onRegistered->onDeregistered->onDestroy 这几个回调在不同的scope下调用时机也不一样,下面分类说明:

    1. onCreate:
      • app:在插件第一次被调用时回调
        • window:在window里插件第一次被调用时回调,在不同的window里调用同样的插件都会调用,popover属于window,所以window和其中的popover公用同一个插件
        • local:在window或popover里第一次调用插件时回调
        • createNew:每次调用rd.require()都会生成一个新的插件实例,其他的和local一样
    2. onRegistered:
      • app:如果插件是第一次调用,那么就在window或popover中插件第一次被调用时回调。否则在window或popover打开时回调
      • window、local、createNew:window或popover中第一次调用插件时回调
    3. onDeregistered: window或popover关闭时回调
    4. onDestroy:
      • app:在app关闭时回调
      • window:在window关闭时回调
      • local:window或popover关闭时回调
      • createNew:window或popover关闭时回调
  • protected RDCloudView getTargetView(String name); 通过name获取WebView实例,多用于事件触发参数传递。已过期,不推荐使用

      RDCloudView view = getTargetView(name);
      jsCallback(view, false, succFunc, "hello world");
    
  • public void jsCallback(final RDCloudView view, boolean remove, String func, Object content);

    JS回调触发事件,传递普通格式数据,参数介绍:

    • view:当前的WebView,如果使用了窗口字符串回调方式(void xxx(String name, String[] params)),需要通过getTargetView(name)获取
    • remove:是否删除回调事件,true代表回调本地会在js端删除,适用于每次调用都传递回调。false代表不删除回调,适用于一次设置,多次回调。从js端传递过来的函数会在js引擎留下一个引用副本,如果是一次性的回调请用true删除它避免内存浪费,否则false
    • func:回调事件。
    • content:回调信息。

      执行回调的时候一定要加上回调事件的验证,以下是例子:

       if(succFunc!=null) {
       jsCallback(getTargetView(windowName),false, succFunc, "hello world");
       }
      
  • public void jsCallback(final RDCloudView view, JSFunction function);
    JS回调触发事件,传递普通格式数据,参数介绍:

    • view:当前的WebView,如果使用了窗口字符串回调方式(void xxx(String name, String[] params)),需要通过getTargetView(name)获取。
    • func:JSFunction实例,参见JSFunction
  • public void jsCallback(final RDCloudView view, JSScript script);
    JS回调触发事件,传递普通格式数据,参数介绍:

    • view:当前的WebView,如果使用了窗口字符串回调方式(void xxx(String name, String[] params)),需要通过getTargetView(name)获取。
    • script:JSScript实例,一段可执行的JS脚本参见JSScript
  • public void jsonCallBack(final RDCloudView view, boolean remove, String func, String json);

    JS回调触发事件,传递json格式数据,参数介绍:

    • view:当前RDCloudView,如果使用了窗口字符串回调方式(void xxx(String name, String[] params)),通过getTargetView(windowName)获取。
    • remove:是否删除回调事件,true代表回调会在js端删除,适用于每次调用都传递回调。false代表不删除回调,适用于一次设置,多次回调。
    • func:回调事件。
    • content:json格式信息。

      执行回调的时候一定要加上回调事件的验证

            if(succFunc!=null) {
        jsonCallBack(getTargetView(pWindowName),false, succFunc, "{msg:'hello world'}");
             }
      
  • public void onLifeCycleChanged(int tag, Bundle bundle);
    系统生命周期回调方法,桥接自每个component(一个component对应一个fragment),每个回调和fragment一样。参数介绍:

    • tag 声明周期标志,取值如下:

      1. LIFE_CYCLE_CREATE:onCreate(Bundle savedInstanceState)
      2. LIFE_CYCLE_START:onStart()
      3. LIFE_CYCLE_RESUME:onResume()
      4. LIFE_CYCLE_PAUSE:onPause()
      5. LIFE_CYCLE_STOP:onStop()
      6. LFET_CYCLE_DESTROY:onDestroy()
      7. LIFE_CYCLE_SAVEINSTANCESTATE:onSaveInstanceState(Bundle outState)
    • 以下是代码中的定义片段:

      1. public static final int LIFE_CYCLE_CREATE = 0; // onCreate(Bundle savedInstanceState)
      2. public static final int LIFE_CYCLE_START = 1; // onStart()
      3. public static final int LIFE_CYCLE_RESUME = 2; // onResume()
      4. public static final int LIFE_CYCLE_PAUSE = 3; // onPause()
      5. public static final int LIFE_CYCLE_STOP = 4; // onStop()
      6. public static final int LIFE_CYCLE_DESTROY = 5; // onDestroy()
      7. public static final int LIFE_CYCLE_SAVEINSTANCESTATE = 6; // onSaveInstanceState(Bundle outState)
    • bundle 此参数只有tag为LIFE_CYCLE_CREATE和LIFE_CYCLE_SAVEINSTANCESTATE时才会有值。

    • 代码实例:

        @Override
        public void onLifeCycleChanged(int tag, Bundle bundle) {
        switch (tag) {
            case LIFE_CYCLE_CREATE: //onCreate
                break;
            case LIFE_CYCLE_START: //onStart
                break;
            case LIFE_CYCLE_RESUME: //onResume
                    break;
            case LIFE_CYCLE_PAUSE: //onPause
                break;
            case LIFE_CYCLE_STOP: //onStop
                break;
            case LIFE_CYCLE_DESTROY: //onDestroy
                break;
            case LIFE_CYCLE_SAVEINSTANCESTATE: //onSaveInstanceState
                break;
            }
        }
      
      注:要记得在plugin.xml中配置接受回调lifeCycleCallback
  • JSFuncResult类

    JSFuncResult是同步和异步返回的一个数据包装类。它支持在返回数据前之前执行一段JS脚本,也可以配合JSString、JSNumber、JSObject、JSScript返回字符串、数字、JS对象等等。

    • 方法列表:
      • JSFuncResult(String preScript, Object action):构造一个实例
  • JSFunction类

    JSFunction是同步和异步返回的一个JS函数包装类,可用于执行JS函数。

    • 方法列表:
      • JSFunction(String function):构造一个实例
      • addParam(JSString|JSNumber|JSObject):增加一个参数
      • addParams(JSString...|JSNumber...|JSObject...):增加一系列参数
      • addParams(List):增加一系列参数
  • JSString类

    JSString是同步和异步返回的一个JS字符串包装类,可用于返回字符串。

    • 方法列表:
      • JSString(String str):构造一个实例
  • JSScript类

    JSScript是异步返回的一个JS脚本包装类,可用于执行一段JS脚本。

    • 方法列表:
      • JSScript(String script):构造一个实例
  • JSNumber类

    JSNumber是同步和异步返回的一个JS数字包装类。

    • 方法列表:
      • JSNumber(Number number, boolean hasDecPoint):根据是否待小数点构造一个数字实例
      • JSNumber(Number number):构造一个待小数点的数字实例
  • JSObject类

    JSObject是同步和异步返回的一个JS对象包装类。

    • 方法列表:
      • JSObject(String script):构造一个JSObject实例
  • JSBoolean类

    JSBoolean是同步和异步返回的一个JS布尔包装类。

    • 方法列表:
      • JSBoolean(Boolean bool):构造一个JSBoolean实例
5.3.9.2 权限申请

Android6.0加入了运行时申请权限的机制,它允许在APP运行时让用户选择是否同意当前的权限申请,而不是在安装的时候同意。

  • 权限申请方法:requestPermissions。弹出权限授权对话框,让用户选择是否同意授权。
    • 原型:protected void requestPermissions(RDCloudView view, String[] permissions, int requestCode)
    • 参数:
      • view:当前窗口实例
      • permissions:权限代码
      • requestCode:权限申请码
  • 权限回调方法:onRequestPermissionsResult。需要覆写此方法来判断当前权限是否被用户授权
    • 原型:public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
    • 参数:
      • requestCode:权限申请码
      • permissions:权限代码
      • grantResults:权限授权状态码
  • 是否需要显示申请权限的原因:shouldShowRequestPermissionRationale
    • 原型:public boolean shouldShowRequestPermissionRationale (String permission)
  • 检查是否拥有权限的授权方法:checkSelfPermission。
    • 原型:protected int checkSelfPermission(String permission)
    • 参数:
      • permission:权限代码
5.3.10 js对象构造工具

步骤:

  • 生成JsObjectBuilder对象
  • 执行addProperty添加属性
  • 执行addFunction添加方法
    • 其execPre参数:js方法执行前的代码块
    • 其execPost参数:js方法执行后的代码块,返回值对象为ret
  • 执行build方法生成js对象

js对象实例:

"{" +
    "id:%d," +
    "hostId:%d," +
    "param1:'%s'," +
    "param2:%d," +
    "method1:function(){" +
    "if (arguments == null || arguments.length == 0)" +
        "return; " +
        "exec('RDCloud://com.xhrd.mobile..../A/call/'+this.hostId, ['method1',this.id,arguments[0],arguments[1]], false, false);" +
    "}," +
    "method2:function(){" +
        "exec('RDCloud://com.xhrd.mobile..../A/call/'+this.hostId, ['method2',this.id], false, false);" +
    "}" +
"}";

用JsObjectBuilder工具生成上面的js对象:

JsObjectBuilder jsBuilder = new JsObjectBuilder(this.id, this.hostId);
jsBuilder.addProperty("param1", this.param1);
jsBuilder.addProperty("param1", this.param1);

// ['method1',this.id,arguments[0],arguments[1]] 中除method和id后面的附加参数,参数用逗号连接,无参为null
String extraArguments = "arguments[0], arguments[1]";

String pre = "if (arguments == null || arguments.length == 0)" +
                      "return;";
String post = "return ret;";
jsBuilder.addFunction("method1", "call", A.class.getName(), extraArguments, true, true, pre, post);
jsBuilder.addFunction("method2", "call", A.class.getName(), null);
String js = jsBuilder.build();

第六章 资源使用说明

6.1 插件资源

因资源id在编译时会固定下来,从而导致无法找到资源,故而RDResourceManager对象封装了系统获取资源的方法,下面列出几个常用的方法:

  • getInstance():获取RDResourceManager实例
  • getLayoutId:通过名字获取布局id
  • getStringId:通过名字获取字符串id
  • getId:通过名字获取id
  • getDrawableId:通过名字获取drawable id

使用方式如下:

  • 获取layout id

       int layoutId = RDResourceManager.getInstance().getLayoutId("main");
    
  • 获取布局id

       int id = RDResourceManager.getInstance().getId("id_name");
    
  • 获取字符串id

       int stringId = RDResourceManager.getInstance().getStringId("string_name");
    
  • 获取drawable id

       int drawableId = RDResourceManager.getInstance().getDrawableId("drawable_name");
    
  • 获取color id

      int colorId = RDResourceManager.getInstance().getColorId("color_name");
    
  • 注意:使用到的任何资源必须加上前缀,否则容易引起资源冲突导致编译失败。推荐使用自己的插件的domain作为前缀。比如:插件domain是RDListView,使用了一张背景图片black_background.png和红色字体颜色:

    • 背景图片应命名为:RDListView_black_background.png。
    • 颜色应命名为:RDListView_text_color。

6.2 前端资源

因前端资源都是放在沙盒内(沙盒地址随应用而定),从而导致无法找到资源,故而设置ResManager对象封装了获取协议路径的绝对地址的方法,下面列出:

  • ResManagerFactory.getInstance():获取ResManager实例
  • getPath:将协议路径转换成绝对路径

6.3 协议路径

协议路径是混合引擎封装的协议。旨在开发中易于访问APP包内沙盒目录。各种协议所对应的路径在正常模式和appLoader模式下对应的真实路径也不同。(需注意:iOS的两种模式下对应的路径一样,这点和Android不一样)。

  • res:程序资源路径,相当于app目录
  • data:用户自定义数据路径,相当于数据目录
  • cache:缓存路径
  • cpts:components路径,相当于component所在的上级目录
  • cpt:当前component所在的路径

详情参考协议路径

第七章 测试Plugin插件

  • 配置测试文件:
    1. 测试需要使用JS编写测试页面,在这之前需要配置application.xml和component.xml。
    2. application.xml是app的全局配置文件,配置entry属性。
      image
    3. component.xml是一个模块的配置文件,配置name和url属性。
      image
  • 编写测试页面并运行:
    1. 创建测试页面有两个方式(二选一)
      • 在plugin_demo工程assets/hybrid/app/component/main目录下建立加入测试页面(详情请参考plugin_demo的assets目录),编译时测试页面会自动同步到主工程里。
      • 在hybrid_framework工程assets/hybrid/app/component/main目录下建立加入测试页面。
    2. 编写测试代码
    3. 确定application.xml的entry为main
    4. 确定component/main目录下的component.xml的url为plugin_demo.html
    5. 在下拉菜单中选择Hybrid_Framework点击下图的绿色三角图标运行,也可以点击绿色小虫图标debug
      image

第八章 Android 6.0 适配

Android 6.0适配主要有三个方面,权限、网络请求和白名单适配,适配之前需要先确认gradle.properties配置文件的版本信息是否和下面一致,否则请更新最新的开发模板。

  • TARGET_SDK_VERSION=23
  • BUILD_TOOLS_VERSION=23.0.2
  • COMPILE_SDK_VERSION=23
  • MINI_SDK_VERSION=14
  • supportv4_version=com.android.support:support-v4:23.1.1
  • appcompat_version=com.android.support:appcompat-v7:23.1.1

8.1 权限

  • 安卓6.0提供了全新的权限系统,用户可以拒绝应用访问系统的特定部分。现在,6.0的开发框架提供了按需请求权限的功能,因此,今后的应用无需在安装的时候就要求用户决定一切。以往的应用权限功能沿用了安卓4.3以来的设计思路,当针对某个应用的权限被阻止之后,用户并不知情,而此举可能让应用的部分功能无法工作,严重的时候甚至会导致应用崩溃,而用户却完全不知道问题究竟出在哪里。而安卓6.0的权限系统让开发者和用户对应用需要的具体权限项目都能够清楚了解。因此,应用不会因为某个具体的权限被禁止就导致崩溃,而用户就自己的某个决定也能够获得更多的相关信息。这样一来,即便在阻止了某个或某些权限之后,整个应用出现问题,用户也清楚的知道问题的根源所在。

  • 采用全新方式开发的应用在安装时不再向用户询问具体的权限选项,而是在今后运行时,当需要用到特定功能时,再逐一向用户询问。在对话框中,允许的决定只用做出一次,而拒绝的决定需要在第二次做出时才会永久生效,有效避免因错误决定导致的麻烦。当然,即便用户改变主意,还是可以到权限设置中直接调整,只是过程麻烦一点而已。

8.1.1 权限配置

  • 内部插件(未开放)和外部插件权限适配是不同的。

  • 内部插件是通过java代码控制的javascript映射,我们重写了java代码的相关方法。只要在addMethodProp方法里面调用相关的重写方法,传递权限及提醒信息即可。

  • 方法原型:addMethodProp(String methodName, String[] permissions, String[] rationales)
    • methodName: Javascript映射方法名
    • permissions: 权限数组,详情参考这里
    • rationales: 权限拒绝时,提示给用户的信息
  • 内部插件示例:

          普通方法调用
          data.addMethod("dial");
    
          /**Android 6.0请求权限适配调用,第一个参数保持不变,第二个数组参数传递相关的权限,第三个数组参数传递勾选“不在提醒”拒绝后的提醒信息,也可以传递字符串资源id数组。**/
          data.addMethod("dial",new String[]{Manifest.permission.CALL_PHONE}, new String[]{"您拒绝了拨号的请求,将无法执行拨号。"});
    
  • 外部插件是通过plugin.xml配置权限请求。只要在相关的方法标签里面配置属性就可以。
  • 配置原型:
    //1、普通方法权限配置
    <method name="installApp" />
    //Android 6.0请求权限配置,添加相关权限属性配置
    <method name="installApp" >
        <permission name="android.permission.REQUEST_INSTALL_PACKAGES" rationale="你拒绝了安装app的权限,将不能安装app"/>
        //rationale属性也可传递资源id,如rationale="@string/id"
    </method>


    //2、一对多权限配置。把需要申请的权限都配置到method标签里面。
    <method name="doSomething" >
        <permission name="android.permission.REQUEST_INSTALL_PACKAGES" rationale="你拒绝了安装app的权限,将不能安装app"/>
        <permission name="android.permission.CALL_PHONE" rationale="你拒绝了打电话权限,将不能拨打电话"/>
    </method>


    //3、多对多权限配置。首先声明方法,把需要配置相同权限的方法写在permissionDefine标签里面。
    <method name="installApp"></method>
    <method name="openApp"></method>
    <method name="closeApp"></method>
    <permissionDefine>
        <permission>
            <item name="android.permission.READ_PHONE_STATE" rationale="你拒绝了app的操作权限" />
        </permission>
        <method name="installApp" /> <!-- 注意:此处只是配置method需要的权限,和上面的method用处不同 -->
        <method name="openApp" />
    </permissionDefine>

8.1.2 网络请求

android 6.0已经废除HttpClient。推荐使用HttpURLConnection,此API是更有效的,通过透明压缩和缓存响应网络请求并减少功耗。如果要继续使用Apache HTTP API,必须在编译时在build.gradle文件里面配置依赖:

    android {
        useLibrary 'org.apache.http.legacy' //在6.0版本才可使用
    }

8.1.3 白名单

如果用户断开了充电连接或是手机关屏一段时间之后,设备会自动进入Doze(休眠)模式。在Doze模式中,系统尝试通过减少应用的网络访问和CPU敏感的服务来保护电池。它也阻止应用访问网络,并且延缓任务、同步和标准alarms的执行。 系统定期退出Doze模式(maintenance window)去让app完成他们被延缓的动作。在maintenance window期间,系统运行所有挂起的同步、任务和alarms,同时也能访问网络。 而白名单中的app能够在Doze和App Standby模式时使用网络。

8.1.3.1 内部插件和外部插件白名单适配
  • 内部插件是通过java代码控制的javascript映射,我们重写了java代码的相关方法。只要在addMethodProp方法里面调用相关的重写方法,传递是否申请加入白名单的相关参数就可以。
    内部插件示例:

      //普通方法调用
      data.addMethod("dial");
    
      //Android 6.0请求加入白名单,第一个参数保持不变,第二个参数是否申请加入白名单。
      data.addMethodWithWhitelist("dial", true);
    
  • 外部插件是通过plugin.xml配置。只要在相关的方法标签里面配置属性就可以。
    外部插件配置示例:

      //普通方法配置
      <method name="download" />
    
      //Android 6.0请求加入白名单配置,添加相关属性配置
      <method name="download" isAddWhitelist="true" />
    

第九章 打包Plugin插件

  • Android Studio中,执行gradle中的makePluginZip脚本:

    1. 配置脚本环境
      • 将gradle.properties里的SDK_PATH改为你自己的目录。
      • ZIP_PATH_WIN改为7z.exe的存放路径,7z是一个windows下流行的压缩软件,我们提供了一个下载地址,如果无法使用请自行搜索下载。
        image
    2. 在Android Studio中打开Gradle Projects
      image
    3. 找到其中的makePluginZip,双击执行它:
      image
    4. 等待执行完毕,插件包存放在Hybrid_Framework/build/outputs/plugin目录下。如果在windows下并且ZIP_PATH_WIN没有配置正确将不会自动打zip,脚本只会拷贝整个目录过来(也是存放在Hybrid_Framework/build/outputs/plugin目录下),请手动打zip包(请直接打包插件内容)image
  • Eclipse中,因不支持打包脚本,需要导出jar和资源并打包成特定格式的zip包:

    1. 需要建立一个目录,命名为插件的名字,目录里包含以下目录和文件:
      img
      • AndroidManifest.xml - 包含插件使用的Activity,Service,Receiver等相关配置。
      • dex - 存放插件本身以及依赖的dex文件。
      • libs - 存放插件本身以及依赖的jar,如果有so文件也放到这里面(请根据armeabi格式存放)。
      • plugin.xml - 包含插件暴露的接口以及属性的配置信息。
      • processor - 存放template模版,请参考pluginConfigs
      • replacement - 存放自定义的application 详情
      • res - 存放资源文件。
    2. 使用eclipse导出功能,将插件相关的代码导出为jar并存放到libs。
    3. 拷贝插件依赖的jar包并存放到libs。
    4. 使用Android SDK下build-tools目录里的dx命令(windows下为dx.bat)将jar转换为dex并存放到dex。
      • 命令:dx --dex --output=<存放目录> <输出目录>
      • --ouput参数为存放目录,其后跟着的目录为jar的存放目录。
    5. 将plugin.xml拷贝根目录。
    6. 如有template,将template文件拷贝到processor。
    7. 将replacement拷贝根目录
    8. 将插件工程里的res下的内容整体考入插件包的res目录。
    9. 将此目录打包,格式要求为1里提到的文件和目录在zip包的根目录。

附录

  1. 引擎本身使用的依赖库如下(请不要改动):
    • com.android.support:appcompat-v7:21.0.3
    • com.nostra13.universalimageloader:universal-image-loader:1.9.4
  2. 最新依赖包:
    • com.xhrd.mobile.hybridframework:hde-obfuscated:1.3.2.3(在gradle.properties中的hde_version处修改)
  3. 模版: