你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

教程:有关使用 Azure 空间定位点创建新 Android 应用的分步说明

本教程介绍如何使用 Azure 空间定位点创建与 ARCore 功能集成的新 Android 应用。

先决条件

若要完成本教程,请确保做好以下准备:

入门

启动 Android Studio。 在“欢迎使用 Android Studio”窗口中,选择“启动新的 Android Studio 项目”。

  1. 选择“文件”->“新建项目”。
  2. 在“创建新项目”窗口中的“手机和平板电脑”部分下,选择“空活动”并单击“下一步”。
  3. 在“新建项目 - 空活动”窗口中,更改以下值:
    • 将“名称”、“包名称”和“保存位置”更改为所需值
    • 将“语言”设置为 Java
    • 将“最小 API 级别”设置为 API 26: Android 8.0 (Oreo)
    • 将其他选项保持不变
    • 单击“完成”。
  4. “组件安装程序”随即运行。 经过某种处理后,Android Studio 将打开 IDE。

Android Studio - 新项目

体验一下

若要测试新应用,请使用 USB 线缆将开发人员启用的设备连接到开发计算机。 在 Android Studio 右上方,选择已连接的设备,然后单击“运行应用”图标。 Android Studio 将在连接的设备上安装并启动该应用。 你现在应该会看到设备上运行的应用中显示“Hello World!”。 单击“运行”->“停止‘应用’”。 Android Studio - 运行

集成 ARCore

ARCore 是用于构建增强现实体验的 Google 平台,可让设备在移动时跟踪自身的位置,并建立自身对真实世界的理解。

修改 app\manifests\AndroidManifest.xml,以在根 <manifest> 节点中包含以下条目。 此代码片段的作用如下:

  • 允许应用访问设备相机。
  • 确保你的应用在 Google Play 商店中仅向支持 ARCore 的设备显示。
  • 安装应用后,此代码片段会配置 Google Play 商店以下载并安装 ARCore(如果尚未安装)。
<manifest ...>

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera.ar" />

    <application>
        ...
        <meta-data android:name="com.google.ar.core" android:value="required" />
        ...
    </application>

</manifest>

修改 Gradle Scripts\build.gradle (Module: app) 以包含以下条目。 此代码将确保应用面向 ARCore 版本 1.25。 完成此项更改后,Gradle 可能会发出一条通知,询问是否要同步:请单击“立即同步”。

dependencies {
    ...
    implementation 'com.google.ar:core:1.25.0'
    ...
}

集成 Sceneform

使用 Sceneform 能够轻松地在增强现实应用中渲染逼真的 3D 场景,且无需学习 OpenGL。

修改 Gradle Scripts\build.gradle (Module: app) 以包含以下条目。 此代码允许应用使用 Java 8 中的语言构造,而 Sceneform 要求使用此类构造。 它还将确保应用面向 Sceneform 版本 1.15。 完成此项更改后,Gradle 可能会发出一条通知,询问是否要同步:请单击“立即同步”。

android {
    ...

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    ...
    implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.15.0'
    ...
}

打开 app\res\layout\activity_main.xml,将现有的 Hello Wolrd <TextView ... /> 元素替换为以下 ArFragment。 此代码导致相机源显示在屏幕上,使 ARCore 能够跟踪设备在移动时所处的位置。

<fragment android:name="com.google.ar.sceneform.ux.ArFragment"
    android:id="@+id/ux_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

注意

若要查看主活动的原始 xml,请单击 Android Studio 右上方的“代码”或“拆分”按钮。

将应用重新部署到设备,以再次对其进行验证。 这一次,系统应会请求提供相机权限。 在批准后,你应该会看到屏幕上渲染相机源。

将对象放入真实世界

让我们使用该应用创建并放置一个对象。 首先,将以下 import 语句添加到 app\java\<PackageName>\MainActivity

import com.google.ar.core.HitResult;
import com.google.ar.core.Plane;
import com.google.ar.sceneform.AnchorNode;
import com.google.ar.sceneform.math.Vector3;
import com.google.ar.sceneform.rendering.Color;
import com.google.ar.sceneform.rendering.MaterialFactory;
import com.google.ar.sceneform.rendering.Renderable;
import com.google.ar.sceneform.rendering.ShapeFactory;
import com.google.ar.sceneform.ux.ArFragment;

import android.view.MotionEvent;

然后,将以下成员变量添加到 MainActivity 类:

private boolean tapExecuted = false;
private final Object syncTaps = new Object();
private ArFragment arFragment;
private AnchorNode anchorNode;
private Renderable nodeRenderable = null;
private float recommendedSessionProgress = 0f;

接下来,将以下代码添加到 app\java\<PackageName>\MainActivity onCreate() 方法中。 此代码将挂接名为 handleTap() 的侦听器,当用户点击设备上的屏幕时,该侦听器可以检测到此动作。 如果恰好是在 ARCore 跟踪功能已识别到的表面上进行点击,则会运行该侦听器。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    this.arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);
    this.arFragment.setOnTapArPlaneListener(this::handleTap);
}

最后,添加以下 handleTap() 方法,用于将所有元素关联到一起。 此方法将创建一个球体,并将其放在点击位置。 该球体最初为黑色,因为 this.recommendedSessionProgress 目前设置为零。 稍后将调整此值。

protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
    synchronized (this.syncTaps) {
        if (this.tapExecuted) {
            return;
        }

        this.tapExecuted = true;
    }

    this.anchorNode = new AnchorNode();
    this.anchorNode.setAnchor(hitResult.createAnchor());

    MaterialFactory.makeOpaqueWithColor(this, new Color(
            this.recommendedSessionProgress,
            this.recommendedSessionProgress,
            this.recommendedSessionProgress))
            .thenAccept(material -> {
                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                this.anchorNode.setRenderable(nodeRenderable);
                this.anchorNode.setParent(arFragment.getArSceneView().getScene());
            });
}

将应用重新部署到设备,以再次对其进行验证。 此时,可以四处移动设备,让 ARCore 开始识别环境。 然后点击屏幕,以创建黑色球体并将其放在所选的表面上。

附加本地 Azure 空间定位点

修改 Gradle Scripts\build.gradle (Module: app) 以包含以下条目。 此示例代码片段面向 Azure 空间定位点 SDK 版本 2.10.2。 请注意,SDK 版本 2.7.0 是目前支持的最低版本,引用任何较新版本的 Azure 空间定位点也应能正常运行。 建议使用最新版本的 Azure 空间定位点 SDK。 你可在此处找到 SDK 的版本说明。

dependencies {
    ...
    implementation 'com.microsoft.azure.spatialanchors:spatialanchors_jni:[2.10.2]'
    implementation 'com.microsoft.azure.spatialanchors:spatialanchors_java:[2.10.2]'
    ...
}

如果面向 Azure 空间定位点 SDK 2.10.0 或更高版本,请在你项目的 settings.gradle 文件的 repositories 部分中包含以下条目。 这其中包括了 Maven 包源的 URL,该 URL 托管了适用于 SDK 2.10.0 或更高版本的 Azure 空间定位点 Android 包:

dependencyResolutionManagement {
    ...
    repositories {
        ...
        maven {
            url 'https://pkgs.dev.azure.com/aipmr/MixedReality-Unity-Packages/_packaging/Maven-packages/maven/v1'
        }
        ...
    }
}

右键单击 app\java\<PackageName>->“新建”->“Java 类”。 将“名称”设置为 MyFirstApp,并设置“类”。 将创建名为 MyFirstApp.java 的文件。 将以下 import 语句添加到该文件:

import com.microsoft.CloudServices;

android.app.Application 定义为其超类。

public class MyFirstApp extends android.app.Application {...

然后,在新的 MyFirstApp 类中添加以下代码,以确保使用应用程序的上下文初始化 Azure 空间定位点。

    @Override
    public void onCreate() {
        super.onCreate();
        CloudServices.initialize(this);
    }

现在修改 app\manifests\AndroidManifest.xml,以在根 <application> 节点中包含以下条目。 此代码将创建的 Application 类挂接到应用中。

    <application
        android:name=".MyFirstApp"
        ...
    </application>

返回到 app\java\<PackageName>\MainActivity,在其中添加以下 import 语句:

import android.view.MotionEvent;
import android.util.Log;

import com.google.ar.sceneform.ArSceneView;
import com.google.ar.sceneform.Scene;
import com.microsoft.azure.spatialanchors.CloudSpatialAnchor;
import com.microsoft.azure.spatialanchors.CloudSpatialAnchorSession;
import com.microsoft.azure.spatialanchors.SessionLogLevel;

然后,将以下成员变量添加到 MainActivity 类:

private float recommendedSessionProgress = 0f;

private ArSceneView sceneView;
private CloudSpatialAnchorSession cloudSession;
private boolean sessionInitialized = false;

接下来,在 mainActivity 类中添加以下 initializeSession() 方法。 调用该方法后,它会确保在启动应用期间创建并正确初始化 Azure 空间定位点会话。 此代码通过提前返回来确保通过 cloudSession.setSession 调用传递到 ASA 会话的 sceneview 会话不为空。

private void initializeSession() {
    if (sceneView.getSession() == null) {
        //Early return if the ARCore Session is still being set up
        return;
    }

    if (this.cloudSession != null) {
        this.cloudSession.close();
    }
    this.cloudSession = new CloudSpatialAnchorSession();
    this.cloudSession.setSession(sceneView.getSession());
    this.cloudSession.setLogLevel(SessionLogLevel.Information);
    this.cloudSession.addOnLogDebugListener(args -> Log.d("ASAInfo", args.getMessage()));
    this.cloudSession.addErrorListener(args -> Log.e("ASAError", String.format("%s: %s", args.getErrorCode().name(), args.getErrorMessage())));

    sessionInitialized = true;
}

由于 initializeSession() 在 sceneView 会话尚未设置时(即,如果 sceneView.getSession() 为 null)可以进行提前返回,因此我们会添加 onUpdate 调用,以确保在创建 sceneView 会话后初始化 ASA 会话。

private void scene_OnUpdate(FrameTime frameTime) {
    if (!sessionInitialized) {
        //retry if initializeSession did an early return due to ARCore Session not yet available (i.e. sceneView.getSession() == null)
        initializeSession();
    }
}

现在,将 initializeSession()scene_OnUpdate(...) 方法挂接到 onCreate() 方法。 另外,请确保将相机源中的帧发送到 Azure 空间定位点 SDK 进行处理。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    this.arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);
    this.arFragment.setOnTapArPlaneListener(this::handleTap);

    this.sceneView = arFragment.getArSceneView();
    Scene scene = sceneView.getScene();
    scene.addOnUpdateListener(frameTime -> {
        if (this.cloudSession != null) {
            this.cloudSession.processFrame(sceneView.getArFrame());
        }
    });
    scene.addOnUpdateListener(this::scene_OnUpdate);
    initializeSession();
}

最后,将以下代码添加到 handleTap() 方法中。 此代码将本地 Azure 空间定位点附加到要放入真实世界的黑色球体。

protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
    synchronized (this.syncTaps) {
        if (this.tapExecuted) {
            return;
        }

        this.tapExecuted = true;
    }

    this.anchorNode = new AnchorNode();
    this.anchorNode.setAnchor(hitResult.createAnchor());
    CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
    cloudAnchor.setLocalAnchor(this.anchorNode.getAnchor());

    MaterialFactory.makeOpaqueWithColor(this, new Color(
            this.recommendedSessionProgress,
            this.recommendedSessionProgress,
            this.recommendedSessionProgress))
            .thenAccept(material -> {
                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                this.anchorNode.setRenderable(nodeRenderable);
                this.anchorNode.setParent(arFragment.getArSceneView().getScene());
            });
}

再次重新部署应用。 四处移动设备,点击屏幕,然后放置黑色球体。 不过,代码这一次会创建本地 Azure 空间定位点并将其附加到球体。

在继续进一步的操作之前,需要创建 Azure 空间定位点帐户以获取帐户标识符、密钥和域(如果尚未创建)。 遵循以下部分获取这些信息。

创建空间定位点资源

转到 Azure 门户

在左窗格中,选择“创建资源”。

使用搜索框以搜索“空间定位点”。

显示空间定位点搜索结果的屏幕截图。

选择“空间定位点”,然后选择“创建” 。

在“空间定位点帐户”窗格中,执行以下操作:

  • 使用常规字母数字字符输入唯一的资源名称。

  • 选择想要将资源附加到的订阅。

  • 选择“新建”可创建资源组。 将其命名为 myResourceGroup,然后选择“确定” 。

    资源组是在其中部署和管理 Azure 资源(例如 Web 应用、数据库和存储帐户)的逻辑容器。 例如,可以选择在使用完之后通过一个简单的步骤删除整个资源组。

  • 选择可在其中放置资源的位置(区域)。

  • 选择“创建”开始创建资源。

用于创建资源的“空间定位点”窗格的屏幕截图。

创建资源后,Azure 门户显示部署已完成。

显示资源部署已完成的屏幕截图。

选择“转到资源”。 你现在可以查看资源属性。

将资源的“帐户 ID”值复制到文本编辑器中,供稍后使用。

“资源属性”窗格的屏幕截图。

另外,将资源的“帐户域”值复制到文本编辑器中,供稍后使用。

显示资源的帐户域值的屏幕截图。

在“设置”下,选择“访问密钥” 。 将“帐户密钥”的“主密钥”值复制到文本编辑器中,供稍后使用 。

帐户的“密钥”窗格的屏幕截图。

将本地定位点上传到云中

创建 Azure 空间定位点帐户标识符、密钥和域后,可以返回到 app\java\<PackageName>\MainActivity 并在其中添加以下 import 语句:

import com.microsoft.azure.spatialanchors.SessionLogLevel;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

然后,将以下成员变量添加到 MainActivity 类:

private boolean sessionInitialized = false;

private String anchorId = null;
private boolean scanningForUpload = false;
private final Object syncSessionProgress = new Object();
private ExecutorService executorService = Executors.newSingleThreadExecutor();

现在,将以下代码添加到 initializeSession() 方法中。 首先,此代码允许应用监视 Azure 空间定位点 SDK 从相机源收集帧的进度。 在收集期间,球体颜色将从最初的黑色开始变为灰色。 收集到足够的帧,可将定位点提交到云中之后,球体将变为白色。 其次,此代码将提供所需的凭据来与云后端通信。 可在以下位置将应用配置为使用你的帐户标识符、密钥和域。 在设置空间定位点资源时,将它们复制到文本编辑器中。

private void initializeSession() {
    if (sceneView.getSession() == null) {
        //Early return if the ARCore Session is still being set up
        return;
    }

    if (this.cloudSession != null) {
        this.cloudSession.close();
    }
    this.cloudSession = new CloudSpatialAnchorSession();
    this.cloudSession.setSession(sceneView.getSession());
    this.cloudSession.setLogLevel(SessionLogLevel.Information);
    this.cloudSession.addOnLogDebugListener(args -> Log.d("ASAInfo", args.getMessage()));
    this.cloudSession.addErrorListener(args -> Log.e("ASAError", String.format("%s: %s", args.getErrorCode().name(), args.getErrorMessage())));

    sessionInitialized = true;

    this.cloudSession.addSessionUpdatedListener(args -> {
        synchronized (this.syncSessionProgress) {
            this.recommendedSessionProgress = args.getStatus().getRecommendedForCreateProgress();
            Log.i("ASAInfo", String.format("Session progress: %f", this.recommendedSessionProgress));
            if (!this.scanningForUpload) {
                return;
            }
        }

        runOnUiThread(() -> {
            synchronized (this.syncSessionProgress) {
                MaterialFactory.makeOpaqueWithColor(this, new Color(
                        this.recommendedSessionProgress,
                        this.recommendedSessionProgress,
                        this.recommendedSessionProgress))
                        .thenAccept(material -> {
                            this.nodeRenderable.setMaterial(material);
                        });
            }
        });
    });

    this.cloudSession.getConfiguration().setAccountId(/* Copy your account Identifier in here */);
    this.cloudSession.getConfiguration().setAccountKey(/* Copy your account Key in here */);
    this.cloudSession.getConfiguration().setAccountDomain(/* Copy your account Domain in here */);
    this.cloudSession.start();
}

接下来,在 mainActivity 类中添加以下 uploadCloudAnchorAsync() 方法。 调用此方法后,它会以异步方式等到从设备中收集了足够的帧为止。 收集到足够的帧后,此方法会立即将球体颜色切换为黄色,然后开始将本地 Azure 空间定位点上传到云中。 上传完成后,该代码会返回定位标识符。

private CompletableFuture<String> uploadCloudAnchorAsync(CloudSpatialAnchor anchor) {
    synchronized (this.syncSessionProgress) {
        this.scanningForUpload = true;
    }


    return CompletableFuture.runAsync(() -> {
        try {
            float currentSessionProgress;
            do {
                synchronized (this.syncSessionProgress) {
                    currentSessionProgress = this.recommendedSessionProgress;
                }
                if (currentSessionProgress < 1.0) {
                    Thread.sleep(500);
                }
            }
            while (currentSessionProgress < 1.0);

            synchronized (this.syncSessionProgress) {
                this.scanningForUpload = false;
            }
            runOnUiThread(() -> {
                MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.YELLOW))
                        .thenAccept(yellowMaterial -> {
                            this.nodeRenderable.setMaterial(yellowMaterial);
                        });
            });

            this.cloudSession.createAnchorAsync(anchor).get();
        } catch (InterruptedException | ExecutionException e) {
            Log.e("ASAError", e.toString());
            throw new RuntimeException(e);
        }
    }, executorService).thenApply(ignore -> anchor.getIdentifier());
}

最后,让我们将所有元素挂接到一起。 在 handleTap() 方法中添加以下代码。 创建球体后,此代码将立即调用 uploadCloudAnchorAsync() 方法。 该方法返回后,以下代码将对球体执行一次最终更新,将其颜色更改为蓝色。

protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
    synchronized (this.syncTaps) {
        if (this.tapExecuted) {
            return;
        }

        this.tapExecuted = true;
    }

    this.anchorNode = new AnchorNode();
    this.anchorNode.setAnchor(hitResult.createAnchor());
    CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
    cloudAnchor.setLocalAnchor(this.anchorNode.getAnchor());

    MaterialFactory.makeOpaqueWithColor(this, new Color(
            this.recommendedSessionProgress,
            this.recommendedSessionProgress,
            this.recommendedSessionProgress))
            .thenAccept(material -> {
                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                this.anchorNode.setRenderable(nodeRenderable);
                this.anchorNode.setParent(arFragment.getArSceneView().getScene());
            });


    uploadCloudAnchorAsync(cloudAnchor)
            .thenAccept(id -> {
                this.anchorId = id;
                Log.i("ASAInfo", String.format("Cloud Anchor created: %s", this.anchorId));
                runOnUiThread(() -> {
                    MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.BLUE))
                            .thenAccept(blueMaterial -> {
                                this.nodeRenderable.setMaterial(blueMaterial);
                                synchronized (this.syncTaps) {
                                    this.tapExecuted = false;
                                }
                            });
                });
            });
}

再次重新部署应用。 四处移动设备,点击屏幕,然后放置球体。 不过,这一次,在收集相机帧的过程中,球体颜色将从黑色更改为白色。 收集到足够的帧后,球体将变为黄色,并且云上传操作将会开始。 请确保你的手机已连接到 Internet。 上传完成后,球体将变为蓝色。 (可选)可以在 Android Studio 中监视 Logcat 窗口以查看应用发送的日志消息。 记录的消息示例包括帧捕获期间的会话进度,以及上传完成后由云返回的定位点标识符。

注意

如果未看到 recommendedSessionProgress 值(在调试日志中称为 Session progress),请确保在放置的球体周围移动和旋转手机。

查找云空间定位点

将定位点上传到云后,可以再次尝试查找该定位点。 首先,将以下 import 语句添加到代码中。

import java.util.concurrent.Executors;

import com.microsoft.azure.spatialanchors.AnchorLocateCriteria;
import com.microsoft.azure.spatialanchors.LocateAnchorStatus;

然后,将以下代码添加到 handleTap() 方法中。 此代码将会:

  • 从屏幕中删除现有的蓝色球体。
  • 再次初始化 Azure 空间定位点会话。 此操作确保要查找的定位点来自云,而不是创建的本地定位点。
  • 针对上传到云的定位点发出查询。
protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
    synchronized (this.syncTaps) {
        if (this.tapExecuted) {
            return;
        }

        this.tapExecuted = true;
    }

    if (this.anchorId != null) {
        this.anchorNode.getAnchor().detach();
        this.anchorNode.setParent(null);
        this.anchorNode = null;
        initializeSession();
        AnchorLocateCriteria criteria = new AnchorLocateCriteria();
        criteria.setIdentifiers(new String[]{this.anchorId});
        cloudSession.createWatcher(criteria);
        return;
    }

    this.anchorNode = new AnchorNode();
    this.anchorNode.setAnchor(hitResult.createAnchor());
    CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
    cloudAnchor.setLocalAnchor(this.anchorNode.getAnchor());

    MaterialFactory.makeOpaqueWithColor(this, new Color(
            this.recommendedSessionProgress,
            this.recommendedSessionProgress,
            this.recommendedSessionProgress))
            .thenAccept(material -> {
                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                this.anchorNode.setRenderable(nodeRenderable);
                this.anchorNode.setParent(arFragment.getArSceneView().getScene());
            });


    uploadCloudAnchorAsync(cloudAnchor)
            .thenAccept(id -> {
                this.anchorId = id;
                Log.i("ASAInfo", String.format("Cloud Anchor created: %s", this.anchorId));
                runOnUiThread(() -> {
                    MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.BLUE))
                            .thenAccept(blueMaterial -> {
                                this.nodeRenderable.setMaterial(blueMaterial);
                                synchronized (this.syncTaps) {
                                    this.tapExecuted = false;
                                }
                            });
                });
            });
}

现在,让我们挂接在找到要查询的定位点之后所要调用的代码。 在 initializeSession() 方法中添加以下代码。 找到云空间定位点后,此代码片段将创建并放置一个绿色球体。 它还支持再次点击屏幕,以便可以再次重复整个方案:创建另一个本地定位点,将其上传,然后再次找到它。

private void initializeSession() {
    if (sceneView.getSession() == null) {
        //Early return if the ARCore Session is still being set up
        return;
    }

    if (this.cloudSession != null) {
        this.cloudSession.close();
    }
    this.cloudSession = new CloudSpatialAnchorSession();
    this.cloudSession.setSession(sceneView.getSession());
    this.cloudSession.setLogLevel(SessionLogLevel.Information);
    this.cloudSession.addOnLogDebugListener(args -> Log.d("ASAInfo", args.getMessage()));
    this.cloudSession.addErrorListener(args -> Log.e("ASAError", String.format("%s: %s", args.getErrorCode().name(), args.getErrorMessage())));

    sessionInitialized = true;

    this.cloudSession.addSessionUpdatedListener(args -> {
        synchronized (this.syncSessionProgress) {
            this.recommendedSessionProgress = args.getStatus().getRecommendedForCreateProgress();
            Log.i("ASAInfo", String.format("Session progress: %f", this.recommendedSessionProgress));
            if (!this.scanningForUpload) {
                return;
            }
        }

        runOnUiThread(() -> {
            synchronized (this.syncSessionProgress) {
                MaterialFactory.makeOpaqueWithColor(this, new Color(
                        this.recommendedSessionProgress,
                        this.recommendedSessionProgress,
                        this.recommendedSessionProgress))
                        .thenAccept(material -> {
                            this.nodeRenderable.setMaterial(material);
                        });
            }
        });
    });

    this.cloudSession.addAnchorLocatedListener(args -> {
        if (args.getStatus() == LocateAnchorStatus.Located) {
            runOnUiThread(() -> {
                this.anchorNode = new AnchorNode();
                this.anchorNode.setAnchor(args.getAnchor().getLocalAnchor());
                MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.GREEN))
                        .thenAccept(greenMaterial -> {
                            this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), greenMaterial);
                            this.anchorNode.setRenderable(nodeRenderable);
                            this.anchorNode.setParent(arFragment.getArSceneView().getScene());

                            this.anchorId = null;
                            synchronized (this.syncTaps) {
                                this.tapExecuted = false;
                            }
                        });
            });
        }
    });

    this.cloudSession.getConfiguration().setAccountId(/* Copy your account Identifier in here */);
    this.cloudSession.getConfiguration().setAccountKey(/* Copy your account Key in here */);
    this.cloudSession.getConfiguration().setAccountDomain(/* Copy your account Domain in here */);
    this.cloudSession.start();
}

就这么简单! 最后一次重新部署应用,以从头到尾体验整个方案。 四处移动设备,并放置黑色球体。 然后,不断移动设备以捕获相机帧,直到球体变为黄色。 本地定位点将会上传,球体将变为蓝色。 最后,再次点击屏幕以删除本地定位点,然后查询该定位点在云中的对应定位点。 继续移动设备,直到找到云空间定位点。 绿色球体应会显示在正确的位置;可以再次清除并重复整个方案。

将所有内容放在一起

下面是将所有不同的元素放在一起后,完整的 MainActivity 类文件看起来的样子。 可以将它用做参考与自己的文件进行比较,看是否有任何差异。

package com.example.myfirstapp;

import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;

import androidx.appcompat.app.AppCompatActivity;

import com.google.ar.core.HitResult;
import com.google.ar.core.Plane;
import com.google.ar.sceneform.AnchorNode;
import com.google.ar.sceneform.ArSceneView;
import com.google.ar.sceneform.FrameTime;
import com.google.ar.sceneform.Scene;
import com.google.ar.sceneform.math.Vector3;
import com.google.ar.sceneform.rendering.Color;
import com.google.ar.sceneform.rendering.MaterialFactory;
import com.google.ar.sceneform.rendering.Renderable;
import com.google.ar.sceneform.rendering.ShapeFactory;
import com.google.ar.sceneform.ux.ArFragment;

import com.microsoft.azure.spatialanchors.AnchorLocateCriteria;
import com.microsoft.azure.spatialanchors.CloudSpatialAnchor;
import com.microsoft.azure.spatialanchors.CloudSpatialAnchorSession;
import com.microsoft.azure.spatialanchors.LocateAnchorStatus;
import com.microsoft.azure.spatialanchors.SessionLogLevel;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MainActivity extends AppCompatActivity {

    private boolean tapExecuted = false;
    private final Object syncTaps = new Object();
    private ArFragment arFragment;
    private AnchorNode anchorNode;
    private Renderable nodeRenderable = null;
    private float recommendedSessionProgress = 0f;

    private ArSceneView sceneView;
    private CloudSpatialAnchorSession cloudSession;
    private boolean sessionInitialized = false;

    private String anchorId = null;
    private boolean scanningForUpload = false;
    private final Object syncSessionProgress = new Object();
    private ExecutorService executorService = Executors.newSingleThreadExecutor();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        this.arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);
        this.arFragment.setOnTapArPlaneListener(this::handleTap);

        this.sceneView = arFragment.getArSceneView();
        Scene scene = sceneView.getScene();
        scene.addOnUpdateListener(frameTime -> {
            if (this.cloudSession != null) {
                this.cloudSession.processFrame(sceneView.getArFrame());
            }
        });
        scene.addOnUpdateListener(this::scene_OnUpdate);
        initializeSession();
    }

    // <scene_OnUpdate>
    private void scene_OnUpdate(FrameTime frameTime) {
        if (!sessionInitialized) {
            //retry if initializeSession did an early return due to ARCore Session not yet available (i.e. sceneView.getSession() == null)
            initializeSession();
        }
    }
    // </scene_OnUpdate>

    // <initializeSession>
    private void initializeSession() {
        if (sceneView.getSession() == null) {
            //Early return if the ARCore Session is still being set up
            return;
        }

        if (this.cloudSession != null) {
            this.cloudSession.close();
        }
        this.cloudSession = new CloudSpatialAnchorSession();
        this.cloudSession.setSession(sceneView.getSession());
        this.cloudSession.setLogLevel(SessionLogLevel.Information);
        this.cloudSession.addOnLogDebugListener(args -> Log.d("ASAInfo", args.getMessage()));
        this.cloudSession.addErrorListener(args -> Log.e("ASAError", String.format("%s: %s", args.getErrorCode().name(), args.getErrorMessage())));

        sessionInitialized = true;

        this.cloudSession.addSessionUpdatedListener(args -> {
            synchronized (this.syncSessionProgress) {
                this.recommendedSessionProgress = args.getStatus().getRecommendedForCreateProgress();
                Log.i("ASAInfo", String.format("Session progress: %f", this.recommendedSessionProgress));
                if (!this.scanningForUpload) {
                    return;
                }
            }

            runOnUiThread(() -> {
                synchronized (this.syncSessionProgress) {
                    MaterialFactory.makeOpaqueWithColor(this, new Color(
                            this.recommendedSessionProgress,
                            this.recommendedSessionProgress,
                            this.recommendedSessionProgress))
                            .thenAccept(material -> {
                                this.nodeRenderable.setMaterial(material);
                            });
                }
            });
        });

        this.cloudSession.addAnchorLocatedListener(args -> {
            if (args.getStatus() == LocateAnchorStatus.Located) {
                runOnUiThread(() -> {
                    this.anchorNode = new AnchorNode();
                    this.anchorNode.setAnchor(args.getAnchor().getLocalAnchor());
                    MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.GREEN))
                            .thenAccept(greenMaterial -> {
                                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), greenMaterial);
                                this.anchorNode.setRenderable(nodeRenderable);
                                this.anchorNode.setParent(arFragment.getArSceneView().getScene());

                                this.anchorId = null;
                                synchronized (this.syncTaps) {
                                    this.tapExecuted = false;
                                }
                            });
                });
            }
        });

        this.cloudSession.getConfiguration().setAccountId(/* Copy your account Identifier in here */);
        this.cloudSession.getConfiguration().setAccountKey(/* Copy your account Key in here */);
        this.cloudSession.getConfiguration().setAccountDomain(/* Copy your account Domain in here */);
        this.cloudSession.start();
    }
    // </initializeSession>

    // <handleTap>
    protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
        synchronized (this.syncTaps) {
            if (this.tapExecuted) {
                return;
            }

            this.tapExecuted = true;
        }

        if (this.anchorId != null) {
            this.anchorNode.getAnchor().detach();
            this.anchorNode.setParent(null);
            this.anchorNode = null;
            initializeSession();
            AnchorLocateCriteria criteria = new AnchorLocateCriteria();
            criteria.setIdentifiers(new String[]{this.anchorId});
            cloudSession.createWatcher(criteria);
            return;
        }

        this.anchorNode = new AnchorNode();
        this.anchorNode.setAnchor(hitResult.createAnchor());
        CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
        cloudAnchor.setLocalAnchor(this.anchorNode.getAnchor());

        MaterialFactory.makeOpaqueWithColor(this, new Color(
                this.recommendedSessionProgress,
                this.recommendedSessionProgress,
                this.recommendedSessionProgress))
                .thenAccept(material -> {
                    this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                    this.anchorNode.setRenderable(nodeRenderable);
                    this.anchorNode.setParent(arFragment.getArSceneView().getScene());
                });


        uploadCloudAnchorAsync(cloudAnchor)
                .thenAccept(id -> {
                    this.anchorId = id;
                    Log.i("ASAInfo", String.format("Cloud Anchor created: %s", this.anchorId));
                    runOnUiThread(() -> {
                        MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.BLUE))
                                .thenAccept(blueMaterial -> {
                                    this.nodeRenderable.setMaterial(blueMaterial);
                                    synchronized (this.syncTaps) {
                                        this.tapExecuted = false;
                                    }
                                });
                    });
                });
    }
    // </handleTap>

    // <uploadCloudAnchorAsync>
    private CompletableFuture<String> uploadCloudAnchorAsync(CloudSpatialAnchor anchor) {
        synchronized (this.syncSessionProgress) {
            this.scanningForUpload = true;
        }


        return CompletableFuture.runAsync(() -> {
            try {
                float currentSessionProgress;
                do {
                    synchronized (this.syncSessionProgress) {
                        currentSessionProgress = this.recommendedSessionProgress;
                    }
                    if (currentSessionProgress < 1.0) {
                        Thread.sleep(500);
                    }
                }
                while (currentSessionProgress < 1.0);

                synchronized (this.syncSessionProgress) {
                    this.scanningForUpload = false;
                }
                runOnUiThread(() -> {
                    MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.YELLOW))
                            .thenAccept(yellowMaterial -> {
                                this.nodeRenderable.setMaterial(yellowMaterial);
                            });
                });

                this.cloudSession.createAnchorAsync(anchor).get();
            } catch (InterruptedException | ExecutionException e) {
                Log.e("ASAError", e.toString());
                throw new RuntimeException(e);
            }
        }, executorService).thenApply(ignore -> anchor.getIdentifier());
    }
    // </uploadCloudAnchorAsync>

}

后续步骤

本教程介绍了如何使用 Azure 空间定位点创建与 ARCore 功能集成的新 Android 应用。 若要了解有关 Azure 空间定位点库的详细信息,请继续阅读我们有关如何创建并找到定位点的教程。