上一篇文章介紹了雲錨點的開發,需要依賴什麼文件和服務,本文主要會介紹雲錨點的功能是怎麼實現的
佈局文件
先看一下佈局文件,佈局文件很簡單,兩個提示框,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 模型就會自動加載到之前點擊的位置上