برنامج تعليمي: إرشادات خطوة بخطوة لإنشاء تطبيق Android جديد باستخدام Azure Spatial Anchors

سيوضح لك هذا البرنامج التعليمي كيفية إنشاء تطبيق Android جديد يعمل على تكامل وظائف ARCore مع Azure Spatial Anchors.

المتطلبات الأساسية

لإكمال هذا البرنامج التعليمي، تأكد من أن لديك ما يلي:

الشروع في العمل

بدء تشغيل Android Studio. في نافذة Welcome to Android Studio، انقر فوق Start a new Android Studio project.

  1. حدد File-New> Project.
  2. في نافذة Create New Project، ضمن قسم Phone and Tablet، اختر Empty Activity، وانقر على Next.
  3. في نافذة New Project - Empty Activity، قم بتغيير القيم التالية:
    • غيّر Name وPackage name وSave location إلى القيم التي تريدها
    • قم بتعيين Language إلى Java
    • قم بتعيين Minimum API level إلى API 26: Android 8.0 (Oreo)
    • اترك الخيارات الأخرى كما هي
    • انقر فوق إنهاء.
  4. سيتم تشغيل Component Installer. بعد بعض المعالجة، سيفتح Android Studio بيئة التطوير المتكامل.

Android Studio - New Project

قُم بمحاولة

لتجربة تطبيقك الجديد، قم بتوصيل جهازك الذي يدعمه المطور بجهاز التطوير لديك باستخدام كابل USB. في الجزء العلوي الأيسر من Android Studio، حدد جهازك المتصل وانقر على أيقونة Run 'app'. سيقوم Android Studio بتثبيت التطبيق على جهازك المتصل وتشغيله. يجب أن تشاهد الآن "مرحبًا بالعالم!" معروضة في التطبيق الذي يعمل على جهازك. انقر فوق Run-Stop> 'app'. Android Studio - Run

تكامل 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) لتضمين الإدخال التالي. ستضمن التعليمات البرمجية هذه أن تطبيقك يستهدف الإصدار 1.25 من ARCore. بعد هذا التغيير، قد تتلقى إعلاماً من Gradle يطالبك بالمزامنة: انقر على Sync now.

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

تكامل Sceneform

يجعل Sceneform من السهل عرض مشاهد واقعية ثلاثية الأبعاد في تطبيقات الواقع المعزز، دون الحاجة إلى تعلم OpenGL.

قم بتعديل Gradle Scripts\build.gradle (Module: app) لتضمين الإدخالات التالي. ستسمح التعليمات البرمجية هذه لتطبيقك باستخدام تركيبات لغة برمجية من Java 8، والتي تتطلبها Sceneform. وستضمن أيضاً أن تطبيقك يستهدف الإصدار Sceneform 1.15. بعد هذا التغيير، قد تتلقى إعلاماً من Gradle يطالبك بالمزامنة: انقر على Sync now.

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 World <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 غير المنسقة لنشاطك الرئيسي، انقر فوق الزر "Code" أو الزر "Split" في أعلى يسار Android Studio.

أعد نشر تطبيقك على جهازك للتحقق من صحته مرة أخرى. في هذه المرة، يجب أن يُطلب منك أذونات الكاميرا. وبمجرد الموافقة، سترى عرض موجز الكاميرا على شاشتك.

وضع عنصر في العالم الحقيقي

لننشئ عنصراً ونضعه باستخدام التطبيق. أولاً، أضف عمليات الاستيراد التالية إلى 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>\MainActivityonCreate() . ستقوم التعليمات البرمجية هذه بتوصيل المستمع المسمى 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 Spatial Anchor محلية

قم بتعديل Gradle Scripts\build.gradle (Module: app) لتضمين الإدخال التالي. يستهدف نموذج التعليمات البرمجية هذا الإصدار 2.10.2 من Azure Spatial Anchors SDK. لاحظ أن الإصدار 2.7.0 من SDK هو حالياً أقل إصدار مدعم، ويجب أن يعمل أي إصدار أحدث من Azure Spatial Anchors أيضاً. نوصي باستخدام أحدث إصدار من Azure Spatial Anchors SDK. ويمكنك العثور على ملاحظات إصدار SDK هنا.

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

إذا كنت تستهدف Azure Spatial Anchors SDK 2.10.0 أو إصدار أحدث، فقم بتضمين الإدخال التالي في قسم المستودعات من ملف settings.gradle الخاص بمشروعك. وسيتضمن ذلك عنوان URL لموجز حزمة Maven التي تستضيف حزم Azure Spatial Anchors Android لـ SDK 2.10.0 أو أحدث:

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

انقر بزر الماوس app\java\<PackageName>الأيمن فوق ->New-Java> Class. عيّن Name إلى MyFirstApp، وحدد Class. سيتم إنشاء ملف يسمى MyFirstApp.java. أضف الاستيراد التالي إليها:

import com.microsoft.CloudServices;

حدد android.app.Application على أنها فئتها العليا.

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

ثم أضف التعليمات البرمجية التالية داخل فئة MyFirstApp الجديدة، والتي ستضمن تهيئة Azure Spatial Anchors مع سياق التطبيق.

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

والآن، قم بتعديل app\manifests\AndroidManifest.xml لتضمين الإدخال التالي داخل عقدة الجذر <application>. ستؤدي التعليمات البرمجية هذه إلى ربط فئة التطبيق التي أنشأتها في تطبيقك.

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

رجوعاً إلى app\java\<PackageName>\MainActivity، أضف عمليات الاستيراد التالية إليه:

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;

ثم لنضيف أسلوب initializeSession() التالي داخل الفئة mainActivity. بمجرد استدعائها، ستضمن إنشاء جلسة Anchors مكانية Azure وتهيئة بشكل صحيح أثناء بدء تشغيل التطبيق الخاص بك. تضمن التعليمات البرمجية هذه أن جلسة عرض المشهد التي تم تمريرها إلى جلسة ASA عبر استدعاء cloudSession.setSession ليست خالية من خلال الإرجاع المبكر.

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.getSession() فارغة)، فإننا نضيف استدعاء onUpdate للتأكد من تهيئة جلسة 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 Spatial Anchors 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 Spatial Anchor محلية بالكرة السوداء التي نضعها في العالم الحقيقي.

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 Spatial Anchor إلى الكرة.

وقبل المضي قدماً، ستحتاج إلى إنشاء حساب Azure Spatial Anchors للحصول على معرف الحساب والمفتاح والمجال، إذا لم تكن لديك بالفعل. اتبع القسم التالي للحصول عليها.

إنشاء مورد Spatial Anchors

انتقل إلى مدخل Azure.

في الجزء الأيسر، حدد Create a resource.

استخدم مربع البحث للبحث عن Spatial Anchors.

Screenshot showing the results of a search for Spatial Anchors.

حدد Spatial Anchors، ثم حدد Create.

في جزء Spatial Anchors Account قم بتنفيذ ما يلي:

  • أدخل اسماً فريداً للمورد باستخدام أحرف أبجدية رقمية عادية.

  • قم بتحديد الاشتراك الذي تريد إرفاق المورد به.

  • إنشاء مجموعة الموارد عن طريق تحديد Create new. قم بتسميتها myResourceGroup، ثم حدد OK.

    مجموعة الموارد هي حاوية منطقية يتم فيها نشر موارد Azure وإدارتها، مثل تطبيقات الويب وقواعد البيانات وحسابات التخزين. على سبيل المثال، يمكنك اختيار حذف مجموعة الموارد بأكملها في خطوة واحدة بسيطة لاحقاً.

  • قم بتحديد موقع (منطقة) لوضع المورد فيه.

  • حدد Create لبدء إنشاء المورد.

Screenshot of the Spatial Anchors pane for creating a resource.

بعد إنشاء المورد، يظهر مدخل Microsoft Azure اكتمال عملية النشر.

Screenshot showing that the resource deployment is complete.

حدِّد الانتقال إلى المورد. يمكنك الآن أن تعرض خصائص المورد.

نسخ قيمة Account ID للمورد إلى محرر النص لاستخدامها لاحقا.

Screenshot of the resource properties pane.

أيضاً قم بنسخ قيمة Account Domain للمورد إلى محرر النص لاستخدامها لاحقا.

Screenshot showing the resource's account domain value.

ضمن "Settings"، حددAccess key. نسخ قيمتي Primary key وAccount Key إلى محرر النص لاستخدامهما لاحقا.

Screenshot of the Keys pane for the account.

قم بتحميل نقطة الإرساء المحلية على السحابة

وبمجرد حصولك على معرّف ومفتاح ومجال حساب Azure Spatial Anchors، يمكننا الرجوع إلى app\java\<PackageName>\MainActivity وإضافة عمليات الاستيراد التالية إليه:

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 Spatial Anchors SDK حيث يقوم بتجميع الإطارات من موجز الكاميرا. وأثناء قيامه بذلك، سيبدأ لون الكرة في التغير من اللون الأسود الأصلي لها إلى الرمادي. ثم سيتحول اللون إلى الأبيض بمجرد جمع عدد كافٍ من الإطارات لإرسال نقطة الإرساء إلى السحابة. ثانياً، ستوفر التعليمات البرمجية هذه بيانات الاعتماد اللازمة للتواصل مع الخلفية السحابية. يعد هنا المكان حيث ستقوم بتكوين تطبيقك لاستخدام معرف حسابك والمفتاح والمجال. لقد قمت بنسخها في محرر نصوص عند إعداد مورد Spatial Anchors.

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();
}

ثم أضف أسلوب uploadCloudAnchorAsync() التالي داخل الفئة mainActivity. وبمجرد استدعائه، سينتظر هذا الأسلوب بشكل غير متزامن حتى يتم جمع إطارات كافية من جهازك. وبمجرد حدوث ذلك، سيتحول لون الكرة إلى الأصفر، ومن ثَم سيبدأ في تحميل Azure Spatial Anchor المحلي إلى السحابة. وبمجرد انتهاء التحميل، ستُرجع التعليمات البرمجية معرف نقطة الإرساء.

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;
                                }
                            });
                });
            });
}

أعد نشر تطبيقك مرة أخرى. حرك جهازك، واضغط على الشاشة، ثم ضع الكرة. ولكن هذه المرة، ستغير الكرة لونها من الأسود إلى الأبيض، حيث يتم تجميع إطارات الكاميرا. وبمجرد أن يكون لدينا إطارات كافية، ستتحول الكرة إلى اللون الأصفر، وسيبدأ التحميل إلى السحابة. تأكد من أن هاتفك متصل بالإنترنت. وبمجرد انتهاء التحميل، ستتحول الكرة إلى اللون الأزرق. اختيارياً، يمكنك مراقبة النافذة Logcat في Android Studio لعرض رسائل السجل التي يرسلها تطبيقك. وتتضمن أمثلة الرسائل التي سيتم تسجيلها تقدم الجلسة أثناء التقاط الإطارات ومعرف نقطة الإرساء الذي ترجع إليه السحابة بمجرد اكتمال التحميل.

إشعار

إذا كنت لا ترى قيمة recommendedSessionProgress (يشار إليها في سجلات تصحيح الأخطاء باسم Session progress) تتغير، فتأكد من أنك تتحرك وتدور أنت وهاتفك حول الكرة التي وضعتها.

تحديد موقع نقطة الإرساء المكانية السحابية

بمجرد تحميل نقطة الإرساء على السحابة، نحن على استعداد لمحاولة تحديد موقعها مرة أخرى. أولاً، أضف عمليات الاستيراد التالية إلى التعليمات البرمجية.

import java.util.concurrent.Executors;

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

ومن ثَم، أضف التعليمات البرمجية التالية إلى الأسلوب handleTap(). وتتولى هذه التعليمة البرمجية ما يلي:

  • إزالة الكرة الزرقاء الموجودة من الشاشة.
  • بدء جلسة Azure Spatial Anchors مرة أخرى. سيضمن هذا الإجراء أن نقطة الإرساء التي سنحددها تأتي من السحابة بدلاً من نقطة الإرساء المحلية التي أنشأناها.
  • إصدار استعلام عن نقطة الإرساء التي قمنا بتحميلها إلى السحابة.
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>

}

الخطوات التالية

في هذا البرنامج التعليمي، اطلعت على كيفية إنشاء تطبيق Android جديد يعمل على تكامل وظائف ARCore مع Azure Spatial Anchors. لمعرفة المزيد حول مكتبة Azure Spatial Anchors، تفضل بزيارة دليلنا حول كيفية إنشاء نقاط الإرساء وتحديد موقعها.