ARCore 使用 SceneForm 框架 —— 使用云锚点功能(下)(功能实现)

上一篇文章介绍了云锚点的开发,需要依赖什么文件和服务,本文主要会介绍云锚点的功能是怎么实现的

布局文件

先看一下布局文件,布局文件很简单,两个提示框,statusTips 提示框提示当前云锚点同步的状态,editText 提示框显示云锚点的 ID;两个按钮,clean 用于清空界面的锚点,ayns 用于加载云锚点


<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 框架 —— 使用云锚点功能(上)(环境准备)》有详细说明获取步骤
ARCore 使用 SceneForm 框架 —— 使用云锚点功能(下)(功能实现)_第1张图片

        <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 模型就会自动加载到之前点击的位置上

你可能感兴趣的:(ARCore,Sceneform,android,开发)