5. Android - 基于EasyAR SDK的AR红包的实现

时间:2022-08-31 23:00:10

一、综述

还记得支付宝在新年里推出的 AR 红包吗?你只要用手机扫一扫“福”字就可以得到一个红包,是不是很神奇很好玩?那么你是否思考过这类程序如何实现呢?
从原理上来说,手机会将你摄像头中的图案与你手机本地或者是云端的图片进行比对,当相似度达到一个程度的时候,扫描就成功了。听上去很简单,但是这种匹配算法可不是我们能轻易写出来的。那么我们就把目光转向集成了 AR 的 SDK 吧。恰好目前国内有一家 EasyAR,我们就使用他们的 SDK 来实现 AR 红包。
官网在这里:EasyAR官网

二、官方程序示例解析

1. 下载运行官网的程序

在官网的下载中下载 Android 非 Unity 的示例程序,解压后显示如下:
5. Android - 基于EasyAR SDK的AR红包的实现
这时打开第一个 HelloAR 应用,目前还不能运行,因为没有你这个应用的 key。
那我们就去官网申请一个吧,在开发选项中创建应用,根据示例应用的信息来填写就行了,如下所示。
5. Android - 基于EasyAR SDK的AR红包的实现
将你的 Key 复制到程序的 MainActivity 中,程序就可以运行了,让我们来看下运行时的效果吧~
扫描项目 assets 下的任意一张图片,图片*就会出现一个彩色的立方体。不管手机怎么移动,立方体始终显示在图片正*,如下如所示。
5. Android - 基于EasyAR SDK的AR红包的实现
看到这里,你也应该明白怎么实现 AR 红包了,只要在扫描到图像的那一块代码中跳转到红包界面不就得了嘛。闲话不多说,那就让我们先来分析一下源代码吧。

2. 源码分析

GLView

先说源码中的 GLView,它是样例中的自定义控件,继承自 GLSurfaceView。
GLSurfaceView 是 Android 实现 OpenGL 画图的重要组成部分,它的本质是 Surface,也就是一个平面。想要在这个平面绘制你的图案,那你还需要什么?
没错,一支画笔!也就是所谓的渲染器,Renderer。当我们为一个 Surface 设置渲染器的时候,我们需要实现渲染器的三个重载方法,如下所示。

// 设置渲染器
this.setRenderer(new GLSurfaceView.Renderer() {

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// ......
}

@Override
public void onSurfaceChanged(GL10 gl, int w, int h) {
// ......
}

@Override
public void onDrawFrame(GL10 gl) {
try {
synchronized (helloAR) {
helloAR.render();
}
} catch (Throwable ex) { }
}
});

我们的重点在第三个方法上,onDrawFrame() 每隔很短的时间(几十毫秒)就会调用一次来重绘 Surface 上的图案,我们发现这里面调用了 helloAR 的 render() 方法,这么 HelloAR 应该是整个程序的核心类,那我们就来研究一下它。

HelloAR

在 HelloAR 类中,我们最先看到三个差不多的方法:

private void loadFromImage(ImageTracker tracker, String path)

private void loadFromJsonFile(ImageTracker tracker, String path, String targetname)

private void loadAllFromJsonFile(ImageTracker tracker, String path)

这三个方法功能大同小异,都是用来加载 assets 中的图像文件的,最后一个方法最方便,直接将指定的 json 文件中的所有资源对象加载。我们就看这个方法:

private void loadAllFromJsonFile(ImageTracker tracker, String path) {
for (ImageTarget target : ImageTarget.setupAll(path, StorageType.Assets)) {
tracker.loadTarget(target, new FunctorOfVoidFromPointerOfTargetAndBool() {
@Override
public void invoke(Target target, boolean status) {
Log.e("HelloAR", "loadAllFromJsonFile: " +
String.format("load target (%b): %s (%d)", status, target.name(), target.runtimeID()));
}
});
}
}

这个方法的形参有两个。
第一个是 ImageTracker,从名字可以看出来这个对象实现了 target 的检测和跟踪,在 Target 可以被 ImageTracker 跟踪之前,你需要通过异步方法 loadTarge 或同步方法 loadTargetBlocked / unloadTargetBlocked 将它载入。
第二个形参是 json 文件的路径,json 文件中包含了所有要被 ImageTracker 载入的图像的路径和名字。让我们来看一下这个 json 文件:

{
"images" :
[
{
"image" : "sightplus/argame01.jpg",
"name" : "argame01"
},
{
"image" : "sightplus/argame02.jpg",
"name" : "argame02"
},
{
"image" : "sightplus/argame03.jpg",
"name" : "argame03"
}
]
}

简单明了,一共有三张图片的路径和名字。
再回头来看这个 loadAllFromJsonFile() 方法,进入方法以后,ImageTarget.setupAll 方法直接加载并返回了所有图片资源代表的 ImageTarget 对象,然后通过 for 循环将他们都添加到了 ImageTracker 中。
了解图片资源是怎么加载的之后,你就可以在 assets 中添加你自己的图片文件并扫描了,不要忘了修改 json 文件哦。

现在再来看 HelloAR 中的 render() 方法,重点是这一段:

for (TargetInstance targetInstance : frame.targetInstances()) {
int status = targetInstance.status();
// 判断是否有 ImageTarget 被追踪到
if (status == TargetStatus.Tracked) {
Target target = targetInstance.target();
ImageTarget imagetarget = target instanceof ImageTarget ? (ImageTarget) (target) : null;
if (imagetarget == null) {
continue;
}
if (box_renderer != null) {
box_renderer.render(camera.projectionGL(0.2f, 500.f), targetInstance.poseGL(), imagetarget.size());
}
}
}

首先你要清楚,render() 方法的调用非常频繁,每次调用它都会检测是否已经有 ImageTarget 被追踪到了。这段代码里,status == TargetStatus.Tracked就是用来进行这个判断的。讲到这里你肯定已经很清楚了,如果我们需要在检测到图像后有什么操作的话,在这里执行就行了。

三、自定义功能

1. 跳转到红包 Activity

跳转?跳转有什么好说的,先在构造函数中获取上下文,再 startActivity() 不就行了吗?
但在这里似乎没那么简单,因为 render() 函数的调用过于频繁,可能系统会一下启动好几个 Activity。那将 Activity 的启动模式调整成 SingleInstance 试一下吧。
这样的确只会启动一个 Activity,但是对这个 Activity 执行 finish() 操作后,系统又会莫名其妙地再启动一个红包 Activity,这对用户体来说是毁灭性的打击。
那我们尝试使用标志量来控制一下跳转吧。
先定义一个常量:

public static int flag = 0;

然后修改 render() 函数,注释很多,我就不细说了。

for (TargetInstance targetInstance : frame.targetInstances()) {

int status = targetInstance.status();
// 如果已经被追踪到
if (status == TargetStatus.Tracked && Constants.flag < 1) {
// 每次追踪到目标,标志量都 +1
Constants.flag ++;
// 获取目标的信息
Target target = targetInstance.target();
ImageTarget imagetarget = target instanceof ImageTarget ? (ImageTarget) (target) : null;
// 判断标志量,如果是 1 就跳转
if (Constants.flag == 1) {
String fileName = target.name();
Intent intent = new Intent(mContext, RedPackageActivity.class);
intent.putExtra("fileName", fileName);
mContext.startActivity(intent);
}
if (imagetarget == null) {
continue;
}
if (box_renderer != null) {
box_renderer.render(camera.projectionGL(0.2f, 500.f),
targetInstance.poseGL(), imagetarget.size());
}
}
}

这样一来跳转后只有一个 Activity,回退后也直接显示扫描界面。但是就这样的话也有一个问题。成功扫描图片一次后标志量一直大于 1 ,没办法再次扫图案抢红包了啊。所以我们还需要在红包 Activity 将标志量重置为 0。
但是你直接在红包页面的 onCreate() 函数中重置 flag 的话,再次跳转的老 bug 又出现了。这个 bug 的出现很奇怪,也许是扫描时出现了延迟,导致退出红包 Activity 后程序还认为你扫描到了图案,再次带你去抢红包。
那么我们把重置标志量的操作延迟一段时间吧,延迟靠什么?Handler 咯~

在 HelloAR 中新建一个 Handler:

public static Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 1) {
Constants.flag = 0;
removeCallbacksAndMessages(0);
}
}
};

在红包 Activity 中的 onDestroy() 中给 Handler 发送一个延时消息:

HelloAR.handler.sendEmptyMessageDelayed(1, 3000);

这样程序就能正常运行了。
当然这只是我的办法,如果大家有更好的解决方法欢迎交流。

2. 扫描 SD 卡图片资源

目前我们只能扫描 assets 中的图片文件,那如何将手机 SD 卡中的图片添加到 ImageTarget 中来扫描呢?
还记得上面的 loadAllFromJsonFile(ImageTracker tracker, String path)方法吗,我们将它改一下:

/**
* 加载外部文件路径的图片
*/

private void loadExternalFromJsonFile(ImageTracker tracker, String path) {
for (ImageTarget target : ImageTarget.setupAll(path, StorageType.Absolute)) {
tracker.loadTarget(target, new FunctorOfVoidFromPointerOfTargetAndBool() {
@Override
public void invoke(Target target, boolean status) {
Log.e("HelloAR", "loadAllFromJsonFile: " +
String.format("load target (%b): %s (%d)", status, target.name(), target.runtimeID()));
}
});
}
}

其实我只是将 StorageType.Assets 变成了 StorageType.Absolute,但是这么一来,如果你在参数中给出的 path 是外部文件路径中的 json 文件,那么你就可以加载手机 SD 卡的图片来扫描了。
不要忘记 Manifest 中添加权限就行了。
具体代码不贴了,给个关键代码的下载地址,感兴趣的朋友看一下吧:Demo