上一篇文章介绍了云锚点的开发,需要依赖什么文件和服务,本文主要会介绍云锚点的功能是怎么实现的
先看一下布局文件,布局文件很简单,两个提示框,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 框架 —— 使用云锚点功能(上)(环境准备)》有详细说明获取步骤
<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 模型就会自动加载到之前点击的位置上