cordova 开发(Android相关)

867 阅读12分钟

新增补充
插件的配置信息地址: cordova.apache.org/docs/en/lat…

cordova 分享文档

目的

能在android手机跑起来一个android App,显示内容是我们预期的内容,并且使用自定义的插件。

cordova

为啥是cordova,不是weex,rn不是其他技术。 大前端环境下,直面客户的都可以说是前端的内容,不过是web端,桌面端,移动端还是嵌入式其他系统等,都可以归纳为前端。cordova在移动端和外部交互的历史上,可算是稳如老狗的老。其实没有其他的太多原因,cordova简单。hybird这些原理都是一个,weex rn这些还更详细的做了其他的原生渲染处理,比如说web端渲染的dom树,用原生的节点渲染等,优化上是做到了,也复杂了。对于我们现在目标,玩起来再说。

cordova 安装过程不再详细描述,部分可以参照原有文档confluence.51baiwang.com/pages/viewp…

评论有安装遇到的一些问题。

在这里再次说说项目目录结构的内容。

// tree -L 1
├── config.xml
├── hooks
├── node_modules
├── package.json
├── platforms
├── plugins
├── res
└── www

主要的文件夹。
/www 如同大多数项目的结构一样,/www 目录下面的是网页端的代码。

// tree -L 2
├── css
│   └── index.css
├── img
│   └── logo.png
├── index.html
└── js
    └── index.js

熟悉的前端代码,不再多说。

/plugins 接着再来看插件文件目录。插件目录属于不常手动修改的目录。

//▶ tree -L 1
.
├── android.json
├── cordova-plugin-whitelist
├── fetch.json
├── ios.json
└── org.lee.cordova.qrcodeanalysis

这块主要是下载的cordova plugin文件目录,和一些平台相关的json文件,引入某些插件或者其他。插件的引入删除操作建议直接用指令来操作,影响到的文件不少,很容易出问题 尽量直接只用cordova plugin add / remove 操作。除非某些插件,在直接使用指令引入会产生报错的时候,可以稍做处理。 这里涉及到一个平台引入,也就是生成平台代码的过程中。按照测试的流程,平台插件的代码,是从plugin里面获取的,也就是说,在我添加了plugin,还没有添加platform的情况下,修改了plugin里面的代码,再添加platform,所引入的plugin代码为修改过后的代码。所以当某一个插件添加,某个环境报错的时候,可以尝试先删除平台,修改代码,重新引入平台。

/platforms:各平台构建(或者说自动生成的项目代码)文件目录 比如说添加了如下ios和android两个平台,分别有xcode的ios项目文件,ios dir;和gradle构建的android项目文件,android dir。 使用对应的studio打开,即可对项目进行更多配置之外的操作。

PS:需要注意的是。platform文件夹,相当于vue-cli默认build的dist文件夹。cordova项目里的HTML代码修改了,没有再次构建,在platform项目里面是不包含的。同时,重新build会修改platform内容代码。

// tree -L 2
.
├── android
│   ├── CordovaLib
│   ├── android.json
│   ├── app
│   ├── build.gradle
│   ├── cordova
│   ├── org.lee.cordova.qrcodeanalysis
│   ├── platform_www
│   ├── project.properties
│   ├── settings.gradle
│   └── wrapper.gradle
└── ios
    ├── CordovaLib
    ├── HelloCordova
    ├── HelloCordova.xcodeproj
    ├── HelloCordova.xcworkspace
    ├── cordova
    ├── ios.json
    ├── platform_www
    ├── pods-debug.xcconfig
    ├── pods-release.xcconfig
    └── www

在对应的项目目录下,我们都可以看到一个/www目录

  • /platforms/android/app/src/main/assets/www
  • /platforms/ios/www
▶ tree -L 1
.
├── cordova-js-src
├── cordova.js
├── cordova_plugins.js
├── css
├── img
├── index.html
├── js
└── plugins

发现目录结构和外面的www有点儿相像,多了些文件,也就是cordova 自动生成的部分js,详情可以打开这些js文件看看,通过cordova.js和插件的js相关联,也是通过cordova.js以某种协议发送信息。详细内容可以自行寻找部分hybird原理的资料。hybird是打开一个webview(类似iframe),再通过这样打开一个外部的网页,原生部分可以通过拦截url scheme,简单说就是拦截请求,获取到对应的内容,进行了原生部分的操作,然后原生通过webview可以随意调用js的函数,这时候调用到对应的callback函数,完成数据的交互。 交互的这个过程,封装成Bridge文件,也就是这些cordova.js

config.xml

config.xml可以说是我们修改得稍微多一点的文件了。 比如说app的唯一值id <widget id="io.cordova.hellocordova" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">

描述、作者等,还有平台定制化的内容,引入插件时候的插件添加等配置。
最不能忽略的一点也就是入口。 <content src="index.html" /> 显而易见的,入口是指向的是www下的index。 既然入口是可以指定的,那么,入口可以指定为某一个已经部署项目的服务端地址吗? 答案是当然的。 比如说将www目录下部署到一个node服务上,把content的内容设置为192.168.xxx.xxx:8080,访问到这个服务的主页面。一切都是这么的美好,真机开发的时候,只需要安装一次android apk或ipa就可以在真机上看到开发的效果了。 然后你会发现,这些网站都是从浏览器打开的,一脸懵逼,生无可恋,每次开发验证难道又得一遍又一遍的打包了?针对性的排查这个问题,我们可以修改了allow-navigation Controls 就可以允许应用内打开外部页面了。更多参考文档~cordova.apache.org/docs/en/lat…
接着,页面是显示了,可是要怎么打开原生的功能呢?如果不能调用原生的功能,在真机上开发又有什么意义呢,还不如在浏览器开发。 抛开外部链接或者是内部文件,打开原生功能的流程是什么呢,cordova.js啊,对,那就得了,我们再在远端链接给html引入cordova.js,cordovajs依赖了哪个js呢?到这里,可以联想到上面说的平台内的www文件目录。直接将依赖到的内容,放置到远端部署的文件目录内即可。 注意:,在web文件内引入了cordova相关文件,也只是引入了一半的【桥】,还有另一半的还没有确认搭建,如果在原生模块,还没有引入相关的功能,web端通过桥通知到原生,但是没有这个功能的时候,是没法调用起来相关内容的。对应的前端报错就是undefined。

<?xml version='1.0' encoding='utf-8'?>
<widget id="io.cordova.hellocordova" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
    <name>HelloCordova</name>
    <description>
        A sample Apache Cordova application that responds to the deviceready event.
    </description>
    <author email="dev@cordova.apache.org" href="http://cordova.io">
        Apache Cordova Team
    </author>
    <content src="index.html" />
    <access origin="*" />
    <allow-intent href="http://*/*" />
    <allow-intent href="https://*/*" />
    <allow-intent href="tel:*" />
    <allow-intent href="sms:*" />
    <allow-intent href="mailto:*" />
    <allow-intent href="geo:*" />
    <platform name="android">
        <allow-intent href="market:*" />
    </platform>
    <platform name="ios">
        <allow-intent href="itms:*" />
        <allow-intent href="itms-apps:*" />
    </platform>
    <engine name="ios" spec="^4.5.5" />
    <plugin name="cordova-plugin-whitelist" spec="^1.3.3" />
    <plugin name="org.lee.cordova.qrcodeanalysis" spec="/Users/lizhaowen/work/plugin/qrcodeanalysis" />
    <engine name="android" spec="~7.0.0" />
</widget>

到这里cordova 更详细的说明到一个段落了。


下面是android部分的一些简单内容,针对的目标是没有java基础,没有android开发基础。请选择性跳过。

Android

android studio打开/platforms/android 这个项目。 develop type

(android studio打开的项目是会以gradle构建的,如果项目结构内没有找到gradle文件,或者说直接打开的是cordova project,弹窗提示之后,让自动生成gradle结构的文件目录,studio就会去下载好多东西)

打开项目,如上图developType,点击左上角切换目录结构类型,使用比较多的是android 和 project,project的目录结构就是finder的目录结构。

使用android模式打开

manifest文件,android设置activity,主入口和授权等内容的地方 manifest

接着看到java文件夹 按照包的路径打开,我们可以看到MainActivity,程序的主入口。既然路过我们就探头看一眼主入口都干了什么吧

public class MainActivity extends CordovaActivity
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        // enable Cordova apps to be started in the background
        Bundle extras = getIntent().getExtras();
        if (extras != null && extras.getBoolean("cdvStartInBackground", false)) {
            moveTaskToBack(true);
        }

        // Set by <content src="index.html" /> in config.xml
        loadUrl(launchUrl);
    }
    ...
}

继承自cordovaActivity 写了这么久的vue,相信都有生命周期的这个概念了吧,每一个Activity不也就相当于一个vue么,有它自己的生命周期。这里只展示了一个onCreate,走进来就一点儿代码,主要的是loadUrl这个方法。很明显,我们可以猜测到是干什么用的了,就是用webview打开一个url,这个url也就是我们web程序的主入口了。除了在config.xml 修改url入口,我们也可以在这里修改为服务器地址的入口。

其他java文件也就是插件的java文件了。后续再讨论这块。 接下来是一个/assets 资源文件。emmm上面说过的一个文件夹。web资源和生成的cordova web桥文件。

最后是/res文件夹,是android的一些静态资源存放的文件夹,icon和img等,对我们影响较大的就是下面xml配置相关的文件夹,这个config同等于cordova的config,获取的配置信息就是在这里。同样的,修改它也会影响工程。

app同级下有个CordovaLib,可以说是一个android的module project,相关工程导包在这里不做延伸。

最后在看看Gradle script脚本。 gradle 可以看到有好几个的build.gradle,后面都有括号标出来后缀。简单说一下这个,每一个android project只有一个project的build脚本,这个工程引入了多个module,app也是作为一个module被工程引入,只不过是同时也作为了入口的module。cordovaLib作为了其他的依赖module。 这两个又是怎么样区分的呢。勇敢打点开属于app的这个build.gradle文件~ 第一行代码 apply plugin: 'com.android.application' 这个插件作为android 的application。 然后我们再打开cordova的,呀,什么都没。再往下看看。像这样 apply plugin: 'com.android.library' 。当导入项目作为依赖的时候,我们就需要将项目的apply plugin设置为lib啦。 然后再settings.gradle 把module include进来。

// GENERATED FILE - DO NOT EDIT
include ":"
include ":CordovaLib"
include ":app"

说到这儿,也没说gradle脚本里面的内容,顺序不太对呀嘿嘿。其实里面就是一些json,也类似于xml的属于配置项,接在第一行代码之下的

buildscript {
    repositories {
        mavenCentral()
        jcenter()
        maven {
            url "https://maven.google.com"
        }
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0'
    }
}

资源库,maven仓库等一些构建项目的东西,dependencies,是项目需要引入的一些依赖,有远端下载的依赖也有本地jar aar包的依赖等。

说了一大堆没啥用的,还不如直接手机插到电脑上,配置好开发模式,按一下那个绿色的启动按钮,跑到手机上,遇到问题再查嘿嘿。

plugin

终于到了感觉挺复杂的,其实并没有这么复杂的这个插件介绍。 作为JS和native沟通的桥梁。plugin都分别对接了两方,沟通对接这个过程,永远的都是一个难题。但是插件也起到的作用也只是单纯的作为一个管道的作用,流通js和native的数据。

定位清楚了,我们在写插件的时候,就完成了插件所应有的功能就好了。

下面是使用android 扫码SDK,然后写的一个插件。 插件的生成 cordova.apache.org/docs/en/lat… 类似vue-cli,自动生成插件文件(需要注意的是不会生成package.json,插件引入的时候需要这个文件的数据,直接用npm init生成,直接回车一直下去就可以了)

▶ tree -L 3
.
├── package.json
├── plugin.xml
├── src
│   ├── android
│   │   ├── qrcodeanalysis.gradle
│   │   ├── qrcodeanalysis.java
│   │   └── zxinglibrary-debug.aar
│   └── ios
│       └── qrcodeanalysis.m
└── www
    └── qrcodeanalysis.js

从/www/qrcodeanalysis导出scan函数

var exec = require('cordova/exec');

exports.coolMethod = function (arg0, success, error) {
    exec(success, error, 'qrcodeanalysis', 'coolMethod', [arg0]);
};
exports.scan = function (arg0, success, error) {
    exec(success, error, 'qrcodeanalysis', 'scan', [arg0]);
};

exec调用的是cordova/exec方法。 调用到qrcodeanalysis这个execute => private scan 下面直接给出代码,结合着看

/**
 * This class echoes a string called from JavaScript.
 */
public class qrcodeanalysis extends CordovaPlugin {
    private CallbackContext callbackContext = null;
    private int REQUEST_CODE_SCAN = 199;

    @Override
    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
        if (action.equals("coolMethod")) {
            String message = args.getString(0);
            this.coolMethod(message, callbackContext);
            return true;
        }
        if (action.equals("scan")) {
            String message = args.getString(0);
            this.scan(message, callbackContext);
            return true;
        }
        return false;
    }

    private void coolMethod(String message, CallbackContext callbackContext) {
        if (message != null && message.length() > 0) {
            callbackContext.success(message);
        } else {
            callbackContext.error("Expected one non-empty string argument.");
        }
    }

    private void scan(String message, CallbackContext callbackContext) {
        this.callbackContext = callbackContext;
        cordova.setActivityResultCallback(this);
        AndPermission.with(cordova.getActivity())
                        .permission(Permission.CAMERA, Permission.READ_EXTERNAL_STORAGE)
                        .onGranted(new Action() {
                            @Override
                            public void onAction(List<String> permissions) {
                                Intent intent = new Intent(cordova.getActivity(), CaptureActivity.class);
                                /*ZxingConfig是配置类
                                 *可以设置是否显示底部布局,闪光灯,相册,
                                 * 是否播放提示音  震动
                                 * 设置扫描框颜色等
                                 * 也可以不传这个参数
                                 * */
                                ZxingConfig config = new ZxingConfig();
                                // config.setPlayBeep(false);//是否播放扫描声音 默认为true
                                //  config.setShake(false);//是否震动  默认为true
                                // config.setDecodeBarCode(false);//是否扫描条形码 默认为true
//                                config.setReactColor(R.color.colorAccent);//设置扫描框四个角的颜色 默认为白色
//                                config.setFrameLineColor(R.color.colorAccent);//设置扫描框边框颜色 默认无色
//                                config.setScanLineColor(R.color.colorAccent);//设置扫描线的颜色 默认白色
                                config.setFullScreenScan(false);//是否全屏扫描  默认为true  设为false则只会在扫描框中扫描
                                intent.putExtra(Constant.INTENT_ZXING_CONFIG, config);
                                cordova.getActivity().startActivityForResult(intent, REQUEST_CODE_SCAN);
                            }
                        })
                        .onDenied(new Action() {
                            @Override
                            public void onAction(List<String> permissions) {
                                Uri packageURI = Uri.parse("package:" + cordova.getActivity().getPackageName());
                                Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageURI);
                                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

                                cordova.getActivity().startActivity(intent);

                                Toast.makeText(cordova.getActivity(), "没有权限无法扫描呦", Toast.LENGTH_LONG).show();
                            }
                        }).start();
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        // 扫描二维码/条码回传
        if (requestCode == REQUEST_CODE_SCAN) {
            if (data != null) {

                String content = data.getStringExtra(Constant.CODED_CONTENT);
                PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, content);
                pluginResult.setKeepCallback(true);
                callbackContext.sendPluginResult(pluginResult);
            }
        }
    }
}

我们把调用扫码的过程给划分开。

SDK

  • 唤起摄像头
  • 获取视频流
  • 解析视频流
  • 得到扫码的数据

plugin

  • 接收扫码数据
  • 返回扫码数据

主要的步骤都在sdk上面了。 首先,我们默认导入了扫码SDK,可以直接使用。直接在scan函数体AndPermission.with(cordova.getActivity())....startActivityForResult(intent, REQUEST_CODE_SCAN);调用起来扫码。扫码过程是个类似js异步的过程,需要等到数据解析完成之后才执行callback。在看过SDK的demo之后,使用android自带的数据接收方式

@Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        // 扫描二维码/条码回传
        if (requestCode == REQUEST_CODE_SCAN) {
            LOG.i("111", "111")
            ...
            }
        }
    }

我们看到一个单词@Override,重写的自带的函数。 再个就是,android调用Activity的时候,会用一个自定义的int数据来标记返回。REQUEST_CODE_SCAN。

接下来,我们先测试一下这个步骤是否能走通。一步一步来嘛~ 跑起来,扫码~在控制台Logcat一看,log info没有输出任何111相关的。
挠头.gif
再来一次,确定没有输出。 再看看demo,没错啊。
懵逼.jpg 那就是数据没到onActivity,为啥呢?

搬运: 解决方法:

不是使用cordova.getActivity().startActivityForResult();这样调试跟踪后会发现被主Activity的OnActivityResult给拦截了。

解决方法使用 cordova.StartActivityForResult(cordovaplugin,Intent,int)

如下代码

cordova.setActivityResultCallback(this);

cordova.setActivityForResult(this,intent,RESULT);

原因是:plugin会通过CordovaInterface中的startActivityForResult(cordovaPlugin,intent,int)方法启动该Activity。

当 Activity 结束后,系统将调用回调函数 onActivityResult(int requestCode, int resultCode, Intent intent)

原文:blog.csdn.net/xiangwang66…

于是我们在调用扫码之前set一下。哎,ok了。 接着就是最后一个步骤了。返回数据。喵了一眼自动生成的coolMethod,是使用一个传进来的callbackContent里面的方法。可是在异步还是另外的函数,怎么办呢? 联想了一下js的window,我也给这个android的东西挂到一个两个函数都可以访问的地方去呗。 于是定义了一个private CallbackContext callbackContext = null;再喵一眼coolMethod是怎么返回数据的,测试一下。

完事儿啦 作者:斯文的烟鬼去shi吧链接:juejin.cn/post/684490…