ARCore 使用 SceneForm 框架 —— 使用雲錨點功能(下)(功能實現)

上一篇文章介紹了雲錨點的開發,需要依賴什麼文件和服務,本文主要會介紹雲錨點的功能是怎麼實現的

佈局文件

先看一下佈局文件,佈局文件很簡單,兩個提示框,statusTips 提示框提示當前雲錨點同步的狀態,editText 提示框顯示雲錨點的 ID;兩個按鈕,clean 用於清空界面的錨點,ayns 用於加載雲錨點

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <fragment
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/ArFragmenView"
            android:name="com.hosh.shareanchor.CleanArFragment"/>

    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/status_tip"
            android:textColor="#ffffff"
            android:textSize="23sp"
            android:id="@+id/status"/>
    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#ffffff"
            android:textSize="23sp"
            android:layout_toRightOf="@+id/status"
            android:id="@+id/statusTips"/>

    <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="textPersonName"
            android:text=""
            android:textColor="#ffffff"
            android:textSize="20sp"
            android:layout_below="@+id/status"
            android:id="@+id/editText"/>
    <Button
            android:text="@string/clean"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/editText"
            android:id="@+id/clean"/>
    <Button
            android:text="@string/sync_anchor"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/editText"
            android:layout_toRightOf="@+id/clean"
            android:id="@+id/ayns"/>
</RelativeLayout>

配置文件

AndroidManifest.xml 需要配置支持雲錨點服務的 API key,在上一篇文章《ARCore 使用 SceneForm 框架 —— 使用雲錨點功能(上)(環境準備)》有詳細說明獲取步驟
在這裏插入圖片描述

        <meta-data
                android:name="com.google.android.ar.API_KEY"
                android:value="你的 API key" />

邏輯代碼

狀態機的設定,主要是突出雲錨點的效果,所以設定只能加載一個雲錨點,通過狀態機限定加載雲錨點的數量

public enum AnchorStatus {
    EMPTY(0),   //當前沒有錨點
    HOSTING(1), //錨點正在同步
    HOSTED(2),   //錨點同步成功
    HOST_FAILED(3);   //錨點同步失敗

    private int value;

    AnchorStatus(int value) {
        this.value = value;
    }
}

fragment 需要設置屬性以支持雲錨點

public class CleanArFragment extends BaseArFragment {
    private static final String TAG = CleanArFragment.class.getSimpleName();

    @Override
    public boolean isArRequired() {
        return true;
    }

    @Override
    public String[] getAdditionalPermissions() {
        return new String[0];
    }

    @Override
    protected void handleSessionException(UnavailableException sessionException) {

        String message;
        if (sessionException instanceof UnavailableArcoreNotInstalledException) {
            message = "Please install ARCore";
        } else if (sessionException instanceof UnavailableApkTooOldException) {
            message = "Please update ARCore";
        } else if (sessionException instanceof UnavailableSdkTooOldException) {
            message = "Please update this app";
        } else if (sessionException instanceof UnavailableDeviceNotCompatibleException) {
            message = "This device does not support AR";
        } else {
            message = "Failed to create AR session";
        }
        Log.e(TAG, "Error: " + message, sessionException);
        Toast.makeText(requireActivity(), message, Toast.LENGTH_LONG).show();
    }

    @Override
    protected Config getSessionConfiguration(Session session) {
        //配置 fragment 支持雲錨點
        Config config = new Config(session);
        config.setCloudAnchorMode(Config.CloudAnchorMode.ENABLED);
        return config;
    }

    @Override
    protected Set<Session.Feature> getSessionFeatures() {
        return Collections.emptySet();
    }

    @Override
    public void onUpdate(FrameTime frameTime) {
        super.onUpdate(frameTime);
        getPlaneDiscoveryController().hide();
    }

    @Override
    public void onResume() {
        super.onResume();
        getPlaneDiscoveryController().hide();
    }
}

剩下最後的重頭戲了,主頁面是怎麼實現上傳雲錨點和加載雲錨點
通過子線程獲取雲錨點的同步狀態,沒有發現關於雲錨點狀態的回調(可能有,自己沒找到),只好先開個線程自己來監控

//開線程獲取本地錨點的同步狀態(同時刷新狀態機)
    private void runStatus() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //只要狀態機線程跑起來,就設置狀態機爲同步中的狀態
                synchronized(currentStatus)
                {
                    currentStatus = AnchorStatus.HOSTING;
                }
                //通知界面刷新同步中的狀態提示
                handler.sendEmptyMessage(SYNC_START);

                //死循環檢測錨點同步狀態(暫時未發現回調)
                while (true) {
                    Log.e("XXX", "keep running");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    int tag = SYNC_START;
                    String showTip = "";
                    synchronized(currentStatus)
                    {

                        if (hostAnchor.getCloudAnchorState() == Anchor.CloudAnchorState.SUCCESS) {
                            //同步完成,並且成功
                            currentStatus = AnchorStatus.HOSTED;     //調整狀態機爲同步完成
                            Log.e("XXX", "runStatus 1 currentStatus = " + currentStatus);
                            tag = SYNC_OVER;
                            showTip = hostAnchor.getCloudAnchorId();
                        } else if (hostAnchor.getCloudAnchorState() == Anchor.CloudAnchorState.TASK_IN_PROGRESS) {
                            //同步中的狀態不做任何處理,也不跳出死循環
                            continue;
                        } else {
                            //同步完成,但是失敗
                            currentStatus = AnchorStatus.HOST_FAILED;  //調整狀態機爲同步失敗
                            Log.e("XXX", "runStatus 2 currentStatus = " + currentStatus);
                            showTip = "" + hostAnchor.getCloudAnchorState();
                            tag = SYNC_FAILED;
                        }
                    }
                    if (tag == SYNC_FAILED || tag == SYNC_OVER){
                        Message msg = handler.obtainMessage();
                        msg.what = tag;
                        msg.obj = showTip;
                        handler.sendMessage(msg);
                        break;
                    }
                }
            }
        }).start();
    }

點擊的事件監聽器,因爲需要將本地的錨點上傳到雲錨點服務上,那本地的錨點怎麼來的,就是點擊界面獲取的

//設置放置模型的點擊事件的監聽器
    BaseArFragment.OnTapArPlaneListener listener = new BaseArFragment.OnTapArPlaneListener() {
        @Override
        public void onTapPlane(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
            //模型資源加載失敗,不對錨點進行渲染處理
            if (model == null)
                return;
            synchronized(currentStatus)
            {
                //不是初始狀態,不對錨點渲染模型,用於限制只有一個錨點模型
                //不是初始狀態即表明有一個錨點已經渲染
                if (currentStatus != AnchorStatus.EMPTY) {
                    return;
                }
            }

            //設置繪製的錨點爲當前的本地錨點,同時將本地錨點同步至 google 的服務
            hostAnchor = arFragment.getArSceneView().getSession().hostCloudAnchor(hitResult.createAnchor());
            runStatus();
            placeModel();
        }
    };

    //在錨點上渲染模型
    private void placeModel() {
        AnchorNode node = new AnchorNode(hostAnchor);
        arFragment.getArSceneView().getScene().addChild(node);
        TransformableNode andy = new TransformableNode(arFragment.getTransformationSystem());
        andy.setParent(node);
        andy.setRenderable(model);
        andy.select();
        listNode.add(node); //每次在界面上對錨點渲染(加載)3D 模型,就將當前被操作的錨點記錄下來
    }

界面加載錨點和提示信息都在主線程完成

public class MainActivity extends AppCompatActivity {
    CleanArFragment arFragment = null;
    ModelRenderable model = null; //模型對象
    Anchor hostAnchor = null;     //被繪製的錨點信息(代表本地設置的錨點或者雲錨點)
    /**
     * 錨點狀態機,只允許設置一個錨點,有多餘錨點不允許添加
     */
    AnchorStatus currentStatus = AnchorStatus.EMPTY;
    TextView statusTip = null;    //顯示當前狀態的提示框
    EditText codeNo = null;       //顯示雲錨點 id 的編輯框
    Button cleanBtn = null;       //清理錨點按鈕
    Button aynsBtn = null;        //獲取雲錨點按鈕
    List<Node> listNode = new ArrayList();      //記錄被渲染的錨點
    Point size = new Point();
    final static int ClEAN_OVER = 0x2200;             //清理界面錨點信號
    final static int SYNC_START = 0x2201;             //開始同步信號
    final static int SYNC_OVER = 0x2202;             //同步完成信號
    final static int SYNC_FAILED = 0x2203;            //同步失敗信號

    //界面統一處理分發中心
    Handler handler = new Handler() {
        public void handleMessage(Message msg) {
            if (msg.what == SYNC_OVER) {
                statusTip.setText(R.string.sync_over);
                String toShowStr = (String)msg.obj;
                codeNo.setText(toShowStr);
            } else if (msg.what == SYNC_START) {
                statusTip.setText(R.string.sync_progress);
            } else if (msg.what == SYNC_FAILED) {
                statusTip.setText(R.string.sync_failed);
            } else if (msg.what == ClEAN_OVER) {
                statusTip.setText(R.string.empty);
            }
        }
    };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initAll();
        arFragment = (CleanArFragment) getSupportFragmentManager().findFragmentById(R.id.ArFragmenView);
        arFragment.setOnTapArPlaneListener(listener);
        cleanBtn.setOnClickListener(clickListener);
        aynsBtn.setOnClickListener(clickListener);
    }

    View.OnClickListener clickListener = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if(view.getId() == R.id.clean) {
                synchronized(currentStatus)
                {
                    cleanAllNode();
                    currentStatus = AnchorStatus.EMPTY;      //每次清理錨點,置狀態機爲初始狀態
                    handler.sendEmptyMessage(ClEAN_OVER);    //通知界面改變提示信息
                }
            } else if (view.getId() == R.id.ayns) {
                    if (codeNo.getText().toString().isEmpty()) {    //如果沒有云錨點的索引 ID 不使用雲錨點
                        return;
                    }
                    String str = codeNo.getText().toString();
                    hostAnchor = arFragment.getArSceneView().getSession().resolveCloudAnchor(str);
                    placeModel();
            }
        }
    };

    //清除界面錨點包括雲錨點和本地錨點
    private void cleanAllNode() {
        //沒有節點被渲染,就不清空錨點集合
        if (listNode.size() == 0) {
            return;
        }

        //從界面清除被渲染的錨點
        for (int i = 0; i < listNode.size(); i++) {
            arFragment.getArSceneView().getScene().removeChild(listNode.get(i));
        }
        //清空記錄的渲染錨點集合
        listNode.clear();
    }

    //界面空間映射,初始化模型資源
    private void initAll() {

        Display display = this.getWindowManager().getDefaultDisplay();
        display.getRealSize(size);
        codeNo = findViewById(R.id.editText);
        cleanBtn = findViewById(R.id.clean);
        aynsBtn = findViewById(R.id.ayns);
        statusTip = findViewById(R.id.statusTips);

        ModelRenderable.builder().setSource(this, R.raw.andy)
            .build().thenAccept(renderable -> model = renderable);
    }
}

實現效果

先上一下效果圖
在這裏插入圖片描述
:操作的基本流程,先讓程序掃描出界面,在掃描出的界面點擊,在點擊的錨點加載 3D 模型,同時將本地的錨點同步至服務器,同步成功後,清空錨點,最後獲取雲錨點,3D 模型就會自動加載到之前點擊的位置上

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章