Android集成Unity的兩種方案
?Android平臺常見動效
現(xiàn)在市面上的形形色色Android客戶端,為了更優(yōu)的用戶體驗,我們開發(fā)的上游產(chǎn)品和交互往往會在界面里設(shè)計很多動效。傳統(tǒng)的一頁頁的靜態(tài)展示頁面已經(jīng)不足以滿足用戶的審美需求了。
而動效的分類也是花樣百出的,以播放時機(jī)來說有點擊觸發(fā),打開頁面觸發(fā),還有可跟隨手指的交互持續(xù)觸發(fā)的等等。有時候一些和數(shù)據(jù)耦合性較大的動效甚至需要我們自己來手寫復(fù)雜的自定義View,比如曲線圖、圖表類型。
而我 日常碰到的大部分的動效需求,還是依賴UI設(shè)計的同時來制作提供的,像那些短時間單次的展示類動效,往往實現(xiàn)方式比較隨意,對資源的格式要求也不太嚴(yán)苛。一般有以下幾種方案:
幀動畫
在Android中,幀動畫是通過Drawable動畫實現(xiàn)的。你可以創(chuàng)建一個AnimationDrawable對象,然后在XML中定義一系列的幀(frames),每幀可以是一個Drawable資源。然后在代碼中啟動這個動畫。
以下是兩個簡單的例子:
1. 在res/drawable目錄下創(chuàng)建一個名為frame_animation.xml的文件,并定義動畫的幀:
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false"> <item android:drawable="@drawable/frame1" android:duration="100" /> <item android:drawable="@drawable/frame2" android:duration="100" /> <item android:drawable="@drawable/frame3" android:duration="100" /> <!-- 更多幀 --> </animation-list>
這里android:oneshot="false"表示動畫會循環(huán)播放,如果設(shè)置為true則播放一次。android:duration表示每幀顯示的時間。
2. 在你的布局文件中(例如activity_main.xml),添加一個ImageView來展示動畫:
<ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/frame_animation" />
然后在調(diào)用處啟動動畫:
ImageView imageView = (ImageView) findViewById(R.id.imageView); // 獲取AnimationDrawable final AnimationDrawable frameAnimation = (AnimationDrawable) imageView.getDrawable(); // 在UI線程之外啟動動畫 imageView.post(new Runnable() { @Override public void run() { frameAnimation.start(); } });
注意確保你的每個Drawable資源的尺寸是一致的,以便在動畫過程中保持幀的正確顯示。這樣就創(chuàng)建了一個簡單的幀動畫,當(dāng)Activity加載時,動畫會自動開始循環(huán)播放。
PAG動畫
pag相較于上面的幀動畫對性能更加友好。PAG是騰訊公司自主研發(fā)的一套完整動畫工作流解決方案。 PAG誕生于2016年,最初的原因是為了解決更為復(fù)雜的視頻編輯場景下動畫渲染問題,同時又覆蓋了UI動畫和直播場景,于2022年1月在Github開源。
其使用方法可以說相當(dāng)簡單,只需要先從github主頁確定版本,到gradle里引入依賴,
implementation 'com.tencent.tav:libpag:3.2.7.40'
然后在我們應(yīng)用的xml布局中放置pagView,沒有額外的屬性需要配置:
<org.libpag.PAGView android:id="@+id/pagview" android:layout_width="@dimen/dp_1190" android:layout_height="@dimen/dp_1110" android:layout_marginTop="@dimen/dp_290" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" />
最后在代碼里設(shè)置其文件源,循環(huán)方式,調(diào)用播放即可
@Override protected void onCreate(Bundle savedInstanceState) { LogUtils.d("AirConditioner Start"); super.onCreate(savedInstanceState); setContentView(R.layout.instance); PAGView pagView = findViewById(R.id.pagview); PAGFile pf = PAGFile.Load(getContext().getAssets(), fileName); pagView.setComposition(pf); pagView.setRepeatCount(-1); pagView.play(); }
MP4動畫
這個單次動效的實現(xiàn)方案是最簡單的,不寫demo演示了,直接獲取mediaplayer實例,綁定surfaceView或者TextureView,再填文件,播放視頻即可。需要關(guān)注的是surfaceView播放視頻一開始可能會有黑屏問題,可以用靜態(tài)圖占位。
可交互的動效
顧名思義,這一類動效需要能跟隨用戶的操作而實時改變表現(xiàn)形式。最常見的就是吸頂動效,例如在一個列表滑動過程中,會監(jiān)聽列表的滑動距離,對界面頂部或者側(cè)邊的其他View位置和透明度,顏色等做動態(tài)設(shè)置。還有systemui的allapp界面的翻頁動畫,負(fù)一屏下拉的時候,根據(jù)距離對桌面背景做高斯模糊處理等等。
Kanzi動效
跟手可互動的動效,也不得不談kanzi動效。以下介紹來自百科與官網(wǎng):
Kanzi產(chǎn)品是行業(yè)領(lǐng)先的3D引擎和UI開發(fā)工具,支持高效率沉浸式3D效果,跨系統(tǒng)多屏互聯(lián)并能與安卓生態(tài)完美融合,已經(jīng)成為全球主流車廠智能座艙首選的UI開發(fā)工具和引擎。更新后的Kanzi架構(gòu)可與安卓操作系統(tǒng)、生態(tài)系統(tǒng)深度兼容。Kanzi可基于安卓的任何功能提供強(qiáng)大的圖形設(shè)計支持,確保高質(zhì)量的圖像效果。
對于Kanzi動效的集成使用方式,因為沒有自己從頭開始對接,我只按照順序一筆帶過,有不對的地方歡迎指正。首先我們集成kanzi運行所需的Runtime.aar,kanziJava支持庫aar,資源文件,資源列表的txt等等,還需要在gradle里寫明不可壓縮的文件類型,以防止無法加載資源。
在使用上,我們先在XML布局中聲明,同時通過屬性填入asstes里的資源名,和資源文件綁定:
<com.rightware.kanzi.KanziTextureView android:id="@+id/tx_KanziSurfaceView" android:layout_width="@dimen/dp_2560" android:layout_height="@dimen/dp_1190" app:clearColor="@android:color/transparent" app:kzbPathList="climate.kzb" app:layout_constraintTop_toTopOf="parent" app:name="climate" app:startupPrefabUrl="kzb://climate/StartupPrefab" tools:ignore="MissingConstraints" />
在代碼里我們需要設(shè)置通信的工具類,在里面添加監(jiān)聽器來接收和上行下行信號的交互:
// 數(shù)據(jù)接口定義 public interface AndroidNotifyListener { void notifyDataChanged(String name, String value); void dataSourceFinish(); } // 添加數(shù)據(jù)接收監(jiān)聽和下行通信 AndroidUtils.setListeners(this); AndroidUtils.removeListeners(this); AndroidUtils.setValue(SourceData.RightMidMove_up2down, y);
Unity動效
Unity的大名在游戲界可謂如雷貫耳,記得小時候玩的很多游戲的開屏界面即有一個大大的Unity字樣和圖標(biāo)。
以下介紹來自百科和官網(wǎng):Unity是實時3D互動內(nèi)容創(chuàng)作和運營平臺。包括游戲開發(fā)、美術(shù)、建筑、汽車設(shè)計、影視在內(nèi)的所有創(chuàng)作者,借助Unity將創(chuàng)意變成現(xiàn)實。Unity平臺提供一整套完善的軟件解決方案,可用于創(chuàng)作、運營和變現(xiàn)任何實時互動的2D和3D內(nèi)容,支持平臺包括手機(jī)、平板電腦、PC、游戲主機(jī)、增強(qiáng)現(xiàn)實和虛擬現(xiàn)實設(shè)備。Unity作為全球領(lǐng)先的 3D 引擎之一,團(tuán)結(jié)引擎可以為 3D HMI提供全棧支持。即為從概念設(shè)計到量產(chǎn)部署的整個 HMI 工作流程提供創(chuàng)意咨詢、性能調(diào)優(yōu)、項目開發(fā)等解決方案,從而為車載信息娛樂系統(tǒng)和智能駕駛座艙打造令人驚嘆的交互式體驗。
其實在第一版我們項目集成的是Kanzi方案,其性能表現(xiàn)較Unity要差一些,關(guān)鍵是項目推進(jìn)的過程中,對方工程師對動效樣式的優(yōu)化也達(dá)不到評測部門的要求,后來更新迭代我們就更換了Unity方案。而本文的重點也是在于Unity3D動效的使用,案例為車載IVI系統(tǒng)空調(diào)app的風(fēng)向調(diào)節(jié),交互邏輯比上面舉的例子更加復(fù)雜,需要實時跟手,在交互熱區(qū)范圍內(nèi)需要不斷變化動效形態(tài),并完成雙向通信,保證動效和車載信號的一致性。
Unity集成的兩種方案
前面做了這些動效的鋪墊,終于進(jìn)入正題了。本文暫時不深究Unity的渲染原理,只談集成和使用。
通信協(xié)議制定
第一步不是創(chuàng)建工程,而是要提前根據(jù)HMI的產(chǎn)品交互定義來指定和Unity之間的通信協(xié)議。有哪些功能是有開關(guān)的,需要調(diào)整哪些屬性??照{(diào)app里就涉及幾個出風(fēng)口的打開關(guān)閉,可以以0/1來區(qū)分。還有風(fēng)口的方向調(diào)節(jié),需要互傳x,y坐標(biāo)值。Android和Unity之間的是采用JSON字符串來通信的,對于JSON字符串的的打包與解包通過谷歌的Gson等三方庫來操作,相當(dāng)簡單。
而且,兩方通信鏈路和Unity的集成方式還有關(guān),像下面要談到的第一種進(jìn)程隔離方案,就是通過集成全量的Unity依賴包,利用其中的JNI接口來通信的,而Client/Server架構(gòu)就是通過Android的AIDL接口來和單獨的服務(wù)端進(jìn)程通信的。另外交互的車載信號鏈路方案涉及項目架構(gòu)機(jī)密,此處不作描述。
進(jìn)程隔離方案-UAAL(Render As Library)
這種方式集成的話,Unity會將渲染引擎,資源文件,和Android上層的通信代碼都打包導(dǎo)出到一個aar中,其體積隨動效的復(fù)雜程度而變化,同時會使集成方的apk包體積增加。而且項目里有多少方要使用Unity動效,就需要多少份的渲染引擎。這個方案由客戶端來負(fù)責(zé)Unity控件的創(chuàng)建銷毀,顯示隱藏,一般適用一對一,通信鏈路簡單的,即項目中可能只有一個模塊需要使用Unity動效的情況。在多模塊需要使用Unity的情況下,進(jìn)程隔離的方案對性能的占用也比較高。
上層使用到的控件——UnityPlayer,它是一個Unity自定義的FrameLayout,里面有他們自己實現(xiàn)的一系列添加view,顯示,和渲染邏輯。資源文件均存在于Unity打的依賴包中,對外不開放。
集成步驟
進(jìn)程隔離的集成方式如下:
第一步,將Unity提供的aar放置于libs文件夾中,并在gradle里添加其編譯引用。
implementation files('libs/UnityAnimation_0321V4.aar')
第二步,gradle中配置Unity所需的NDK版本,配置abifilters,設(shè)置要將哪些架構(gòu)的動態(tài)庫打包到apk中,對于車機(jī)項目來說只需要固定的某一種架構(gòu)即可。還有設(shè)置不壓縮的文件類型,使Unity可以順利找到資源使用。
ndkVersion "23.1.7779620" aaptOptions { noCompress = ['.tj3d', '.ress', '.resource', '.obb', '.bundle', '.tuanjieexp', 'global-metadata.so'] + tuanjieStreamingAssets.tokenize(', ') ignoreAssetsPattern = "!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~" } ndk{ abiFilters 'arm64-v8a' }
注意,我們還需要在項目的string.xml資源文件中添加Unity所需的一條String資源,否則Unity側(cè)會空指針。
<string name="game_view_content_description">Game view</string>
第三步,將要顯示Unity動效的頁面Activity改為繼承自UnityPlayerActivity,Unity的核心顯示控件,UnityPlayer,它的創(chuàng)建銷毀,顯示隱藏,由這個UnityPlayerActivity來統(tǒng)一管理,項目中集成這個Activity的子類再將mUnityPlayer通過addView添加到自己的根布局ViewGroup中當(dāng)背景即可,而且可以在xml上面繼續(xù)增加其他View控件。
第四步,封裝Unity通信工具類,Android給Unity發(fā)消息可以直接通過UnityPlayer的sendMessage靜態(tài)方法,傳入Unity通信協(xié)議中指定的類名。
UnityPlayer.UnitySendMessage(OBJ_NAME, METHOD_NAME, communicateMessage)
Unity使用C#開發(fā),其給Android上層發(fā)消息則是通過反射回調(diào)信號類里的方法實現(xiàn)的,所以我們最好將信號管理類做成單例的,并給其Unity留下一個方法或者成員,可以拿到我們類的實例,順利反射回調(diào)。我這里使用的是一個Kotlin類聲明,并對外暴露一個公開的unityInstance成員。而這個方法onReceiveMsgFromUnity,即是Unity的反射調(diào)用,我們在其中進(jìn)行信號的解析,并傳到View中去,注意這個方法不是在主線程中反射的,所以后面需要優(yōu)化一波。
object UnityMessageHelper { val unityInstance = this // Unity給Android的消息回調(diào) fun onReceiveMsgFromUnity(msg: String) { LogUtils.d(TAG, "onReceiveMsgFromUnity: $msg") if (listenerList.size > 0) { listenerList.forEach { it.onReceiveUnityMessage(msg) } } } }
信號類UnityMessageHelper的優(yōu)化
由于我們的目標(biāo)工程是空調(diào)app,在用戶調(diào)節(jié)風(fēng)向時的回調(diào)頻率相當(dāng)高,而自動掃風(fēng)模式下,底層上傳的數(shù)據(jù)頻率也相當(dāng)高,所以不適合到主線程中操作這么多的數(shù)據(jù),我們用協(xié)程,配合Default調(diào)度器來處理這種CPU密集型的任務(wù)。兩條鏈路,用戶手指的拖動操作時,Unity反射回調(diào)的線程本身都是工作線程了,所以我們在使用自定義的接口回調(diào)到View類的時候,使用MainScope.launch包一層,確保是到主線程更新我們的UI。而自動掃風(fēng)模式從域控制器接收到風(fēng)口點擊的坐標(biāo)值時,我們拿到數(shù)據(jù)后給Unity下發(fā)信號,更新動效的指向位置??梢允褂脜f(xié)程上下文切換,withContext(Dispatcher.Default)將其切到工作線程里發(fā)送給Unity。
遇到的問題
Unity方給的aar里的基類Activity適用與絕大多數(shù)的普通應(yīng)用,但是我這里空調(diào)app的定位是一個懸浮的臨時面板,其實我的工程里壓根就沒有Activity,而是采用WindowManager來添加高層級窗口的View的形式來展示界面。
這個時候我們用不上他們定義的UnityPlayerActivity,只能使用原生Raw的UnityPlayer,自己管理其創(chuàng)建,銷毀,resume和pause。這里需要注意的是,UnityPlayer的創(chuàng)建需要傳一個Context上下文,而應(yīng)用里又沒有Activity類型的Context,故只能使用非Activity類型的Context,而且實踐中發(fā)現(xiàn),這個UnityPlayer的實例必須是我們的應(yīng)用拿到可用的窗口句柄之后,才能被成功創(chuàng)建,否則就會報錯。
所以正確的創(chuàng)建與初始化順序是先使用WindowManager添加一個xml布局inflate來的ViewGroup,在其onAttachToWindow的方法回調(diào)之后,再創(chuàng)建UnityPlayer的實例,并添加到這個ViewGroup的布局中去,調(diào)用其resume方法。
public void initUnity() { if (mUnityPlayer == null) { LogUtils.i(TAG, "initUnity"); unityInitView = (LinearLayout) LayoutInflater.from(mContext).inflate(R.layout.layout_unity_init, null); unityInitView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(@NonNull View v) { LogUtils.w(TAG, "unityInitView onViewAttachedToWindow"); mUnityPlayer = new UnityPlayer(mContext); unityInitView.addView(mUnityPlayer); mUnityPlayer.requestFocus(); mUnityPlayer.resume(); mUnityPlayer.windowFocusChanged(true); } @Override public void onViewDetachedFromWindow(@NonNull View v) { LogUtils.w(TAG, "unityInitView onViewDetachedFromWindow"); // Unityplayer已經(jīng)成功移除,通知airView將player添加進(jìn)去 airConditionerView.addUnityToAirView(); } }); mWindowManager.addView(unityInitView, initUnityWindowParams()); } }
注意,這樣添加的UnityPlayer有一個無法解決的黑屏問題,因為Unity的渲染加載至少都需要4,5秒,期間我們只能在更上層的View里設(shè)置靜態(tài)背景圖覆蓋上去,等Unity加載完畢,發(fā)送ready的回調(diào)之后,我們移除掉這個占位的靜態(tài)圖,展示Unity動效的界面。這也是進(jìn)程隔離的方案的一個很棘手的問題。我的解決方案是在開機(jī)的時候往屏幕外添加一個View專門來初始化加載Unity,加載完畢后,再將UnityPlayer給從里面remove掉,重新添加到實際的要展示的窗口中去,這樣打開界面的時候可以略去加載的耗時,稍微減少頁面僵直的時間。
單進(jìn)程-URAS(Render As Service)
Unity Rendering as Service(簡稱URAS) 的渲染方案是團(tuán)結(jié)引擎特有的,無需在多個安卓應(yīng)用中集成多個Unity 3D player,而是后臺運行,前端應(yīng)用可直接調(diào)用,節(jié)省系統(tǒng)資源,更適合多應(yīng)用動效一鏡到底的設(shè)計。
相較進(jìn)程隔離方案的優(yōu)勢
這個方案是在UAAL方案的基礎(chǔ)上升級的,所以有一些前期工作是重復(fù)的,不作重復(fù)的闡述。
它是將要顯示的幾個Unity引擎都打包到同一個Server服務(wù)端去統(tǒng)一管控。其實服務(wù)端的apk打包也是拿到Unity提供的服務(wù)端AAR打進(jìn)一個空工程,內(nèi)部邏輯也隱藏到了AAR中。服務(wù)端和客戶端的通信采用我們熟知的AIDL接口來實現(xiàn)。而且這個服務(wù)端我們需要設(shè)置為persistent應(yīng)用,使其能開機(jī)自啟,自動執(zhí)行渲染等工作,其他應(yīng)用有顯示需求可以秒開,并且長時間不顯示也不會自己回收資源了,客戶端的黑屏問題也可以解決了。
相比于UAAL方案,客戶端需要集成的是一個體積很小的Client.aar,對于客戶端apk的體積控制是有優(yōu)勢的。
集成與使用方式
我們只需要在gradle里引入這個客戶端aar。在gradle sync之后,將遠(yuǎn)程的UnityView添加到自己的布局中去,配置好display參數(shù)(用來給服務(wù)端區(qū)分是哪個引擎的內(nèi)容),并指定服務(wù)端的包名。承載的View類型有SurfaceView和TextureView兩種,而我的應(yīng)用界面因為是一個懸浮窗口,設(shè)計有進(jìn)出場的漸隱漸出動效,而SurfaceView不可以線性地設(shè)置alpha動畫,所以選取TextureView來當(dāng)作容器。
<com.unity3d.renderservice.client.TuanjieView android:id="@+id/unityview" android:layout_width="match_parent" android:layout_height="match_parent" app:tuanjieDisplay="2" app:tuanjieServicePkgName="com.tuanjie.renderservice" app:tuanjieViewType="TextureView" />
剩余的代碼邏輯僅僅是服務(wù)端Service的啟動,添加服務(wù)連接的回調(diào),消息回調(diào)。由于服務(wù)端為若干個Client的公共引擎,所以連resume和pause都不需要處理,因為這兩個操作會對所有的客戶端都生效。我們只需要確保啟動服務(wù),并使用正確的display即可,面板退到后臺可以使用setVisbility來控制其顯示隱藏。除此之外,我們的通信工具類,UnityMessageHelper還需要實現(xiàn)兩個接口,一個服務(wù)連接狀態(tài)接口,一個業(yè)務(wù)數(shù)據(jù)的消息回調(diào)接口,代碼如下:
object UnityMessageHelper : TuanjieRenderService.Callback, SendMessageCallback { fun initUnityService() { LogUtils.i(TAG, "initUnityService") unityRenderService = TuanjieRenderService.init(appContext,TUANJIE_PACKAGENAME).apply { enableAutoReconnect = true addCallback(this@UnityMessageHelper) addSendMessageCallback(this@UnityMessageHelper) ensureStarted() } } override fun onServiceConnected() { LogUtils.w(TAG, "onUnityRenderServiceConnected") } override fun onServiceDisconnected() { LogUtils.w(TAG, "onUnityRenderServiceDisConnected") messageScope.launch { delay(500L) initUnityService() unityRenderService.resume() } } override fun onServiceStartRenderView(p0: Int) { LogUtils.i(TAG, "onServiceStartRenderView") } override fun onClientRecvMessage(message: String?) = null // 服務(wù)端的消息回調(diào) override fun onClientRecvMessageWithNoRet(msg: String?) { // 回調(diào)消息的解析 } }
同時Android給Unity發(fā)信號的方法也從UnityPlayer的靜態(tài)方法換成了這個服務(wù)實例的方法調(diào)用:
unityRenderService.c2sSendMessage(OBJ_NAME, METHOD_NAME, communicateMessage)
接收Unity的回調(diào)消息也換成了addSendMessageCallback里設(shè)置的回調(diào)方法。
可以說URAS方案由于其統(tǒng)一管控,一對多的特點,在性能和客戶端的易集成性方面,是優(yōu)于UAAL方案的??梢詮募軜?gòu)層面上,聯(lián)動更多的動效使用模塊,實現(xiàn)一鏡到底的絲滑轉(zhuǎn)場。
結(jié)語
以上就是本文對于Android常用幾種動效的闡述以及對兩種常見的Unity集成方案記錄。后續(xù)我們除了在最表面的使用層面上,還可以進(jìn)一步挖掘其原理,甚至自己使用Unity的開發(fā)工具,自己體驗一把動效的制作和接入,做到全鏈路知己知彼,才可以更高效的集成Unity為自己的應(yīng)用錦上添花。
到此這篇關(guān)于Android集成Unity的兩種方案的文章就介紹到這了,更多相關(guān)Android集成Unity內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android自定義ViewGroup實現(xiàn)標(biāo)簽浮動效果
這篇文章主要為大家詳細(xì)介紹了Android自定義ViewGroup實現(xiàn)標(biāo)簽浮動效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-06-06OpenHarmony實現(xiàn)屏幕亮度動態(tài)調(diào)節(jié)方法詳解
大家在拿到dayu之后,都吐槽說,會經(jīng)常熄屏,不利于調(diào)試,那么有沒有一種辦法,可以讓app不熄屏呢,答案是有的,今天我們就來揭秘一下,如何控制屏幕亮度2022-11-11FloatingActionButton增強(qiáng)版一個按鈕跳出多個按鈕第三方開源之FloatingActionButton
這篇文章主要介紹了FloatingActionButton增強(qiáng)版一個按鈕跳出多個按鈕第三方開源之FloatingActionButton 的相關(guān)資料,需要的朋友可以參考下2015-12-12Android 關(guān)于“NetworkOnMainThreadException”問題的原因分析及解決辦法
這篇文章主要介紹了Android 關(guān)于“NetworkOnMainThreadException”的相關(guān)知識,本文介紹的非常詳細(xì),具有參考借鑒價值,感興趣的朋友一起學(xué)習(xí)吧2016-02-02快速了解Android?Room使用細(xì)則進(jìn)階
這篇文章主要為大家介紹了快速了解Android?Room使用細(xì)則進(jìn)階,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03