一 、前言
最近都好忙好忙,感覺很累,曾好幾次想繼續把關於Unity和Android相互通信的這部分技術分享的博客寫完,但是實在是無法提起寫博客的精神,所以就一拖再拖,從一月份拖到了五月份,好在當時的思路想法都還在,今天就讓這部分博客畫個句號吧。
在前一篇文章:Unity與Android通信的中間件(一) 裏我說了我寫這個插件主要是爲了解決兩個問題:一個是降低android端代碼和unity端代碼的耦合度,還有一個是降低android端的接入複雜度。降低android端代碼和unity端代碼的耦合度,在那篇文章裏已經解釋清楚了,並且提供了源碼,大家直接去看源碼就可以了,今天就接着來講降低android端的接入複雜度的問題。
二 、實現思路
其實降低android端的接入複雜度的問題根本不算問題,這個肯定是仁者見仁智者見智,我在這裏只是提供一種我所想到的實現思路,說白了就是封裝一些基礎代碼,讓開發的人可以添加少量的代碼即可完成Unity和Android相互通信的功能。由於我本身技術的問題,封裝的可能就是寫laji,希望大家輕拍,謝謝。
2.1 解決android端的接入複雜度高的問題
Unity最終提供給Android端的其實就是個View,所謂降低android端的接入複雜度,就是讓Android端可以裝載View的一些模塊可以簡單、快速的實現顯示Unity端視圖的功能,比如ViewGroup、Dialog、Activity、Fragment,等等。基於以上考慮,所以我只是封裝了Activity、Fragment的基類,至於ViewGroup、Dialog,完全可以在Fragment和Activity裏顯示,我就不做過多示例了。
2.1.1 Fragment和Activity都需要實現的接口——IBaseView
/**
* Description:Basic interface of all {@link Activity}
* or
* {@link Fragment}
* or
* {@link android.app.Fragment}
* <p>
* Creator:yankebin
* CreatedAt:2018/12/18
*/
public interface IBaseView {
/**
* Return the layout resource
*
* @return Layout Resource
*/
@LayoutRes
int contentViewLayoutId();
/**
* Call after {@link Activity#onCreate(Bundle)}
* or
* {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}
* or
* {@link android.app.Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}
*
* @param params
* @param contentView
*/
void onViewCreated(@NonNull Bundle params, @NonNull View contentView);
/**
* Call after
* {@link Activity#onDestroy()} (Bundle)}
* or
* {@link Fragment#onDestroyView()}
* or
* {@link android.app.Fragment#onDestroyView()}
*/
void onVieDestroyed();
}
這個類很簡單的,只是統一下Activity、Fragment、v4包的Fragment的一些抽象方法,方便封裝BaseActivity、BaseFragment、BaseFragmentV4。比如contentViewLayoutId方法,就是讓開發者可以直接返回佈局的id,而不用自己去寫加載佈局的代碼。
2.1.2 顯示Unity端視圖的模塊的基類——BaseActivity、BaseFragment、BaseFragmentV4
/**
* Description:Activity基類
* Creator:yankebin
* CreatedAt:2018/12/18
*/
public abstract class BaseActivity extends AppCompatActivity implements IBaseView {
protected View mainContentView;
protected Unbinder unbinder;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mainContentView = getLayoutInflater().inflate(contentViewLayoutId(),
(ViewGroup) getWindow().getDecorView(), false);
setContentView(mainContentView);
unbinder = ButterKnife.bind(this, mainContentView);
Bundle bundle = getIntent().getExtras();
if (null == bundle) {
bundle = new Bundle();
}
if (null != savedInstanceState) {
bundle.putAll(savedInstanceState);
}
onViewCreated(bundle, mainContentView);
}
@Override
protected void onDestroy() {
onVieDestroyed();
if (null != unbinder) {
unbinder.unbind();
}
mainContentView = null;
super.onDestroy();
}
}
/**
* Description:app包下Fragment作爲基類封裝
* Creator:yankebin
* CreatedAt:2018/12/18
*/
@Deprecated
public abstract class BaseFragment extends Fragment implements IBaseView {
protected Unbinder unbinder;
protected View mainContentView;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
if (null == mainContentView) {
mainContentView = inflater.inflate(contentViewLayoutId(), container, false);
unbinder = ButterKnife.bind(this, mainContentView);
Bundle bundle = getArguments();
if (null == bundle) {
bundle = new Bundle();
}
if (null != savedInstanceState) {
bundle.putAll(savedInstanceState);
}
onViewCreated(bundle, mainContentView);
} else {
unbinder = ButterKnife.bind(this, mainContentView);
}
return mainContentView;
}
@Override
public void onDetach() {
mainContentView = null;
super.onDetach();
}
@Override
public void onDestroyView() {
onVieDestroyed();
if (null != unbinder) {
unbinder.unbind();
}
if (mainContentView != null && mainContentView.getParent() != null) {
((ViewGroup) mainContentView.getParent()).removeView(mainContentView);
}
super.onDestroyView();
}
}
/**
* Description:V4包下Fragment作爲基類封裝
* Creator:yankebin
* CreatedAt:2018/12/18
*/
public abstract class BaseFragmentV4 extends Fragment implements IBaseView {
protected Unbinder unbinder;
protected View mainContentView;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
if (null == mainContentView) {
mainContentView = inflater.inflate(contentViewLayoutId(), container, false);
unbinder = ButterKnife.bind(this, mainContentView);
Bundle bundle = getArguments();
if (null == bundle) {
bundle = new Bundle();
}
if (null != savedInstanceState) {
bundle.putAll(savedInstanceState);
}
onViewCreated(bundle, mainContentView);
} else {
unbinder = ButterKnife.bind(this, mainContentView);
}
return mainContentView;
}
@Override
public void onDetach() {
mainContentView = null;
super.onDetach();
}
@Override
public void onDestroyView() {
onVieDestroyed();
if (null != unbinder) {
unbinder.unbind();
}
if (mainContentView != null && mainContentView.getParent() != null) {
((ViewGroup) mainContentView.getParent()).removeView(mainContentView);
}
super.onDestroyView();
}
}
這三個類主要是簡化了開發者加載佈局相關的代碼,以及簡化生命週期回調的方法數量,讓開發者只關心該關注的生命週期回調。
2.1.3 顯示Unity端視圖的模塊的基類進一步封裝——UnityPlayerActivity、UnityPlayerFragment、UnityPlayerFragmentV4
UnityPlayerActivity:
/**
* Desription:Base Unity3D Player Activity.
* <BR/>
* If you want to implement Fragment to load the Unity scene, then the Fragment needs to refer to {@link UnityPlayerFragment} and {@link UnityPlayerFragmentV4}
* To implement, then overload the {@link #generateIOnUnity3DCallDelegate(UnityPlayer, Bundle)} method to return the conforming Fragment.
* <BR/>
* Creator:yankebin
* <BR/>
* CreatedAt:2018/12/1
*/
public abstract class UnityPlayerActivity extends BaseActivity implements IGetUnity3DCall, IOnUnity3DCall, IUnityPlayerContainer {
protected UnityPlayer mUnityPlayer; // don't change the name of this variable; referenced from native code
protected IOnUnity3DCall mOnUnity3DCallDelegate;
@Override
@CallSuper
public void onViewCreated(@NonNull Bundle params, @NonNull View contentView) {
initUnityPlayer(params);
}
/**
* Initialize Unity3D related
*
* @param bundle {@link Bundle}
*/
protected void initUnityPlayer(@NonNull Bundle bundle) {
mUnityPlayer = new UnityPlayer(this);
mOnUnity3DCallDelegate = generateIOnUnity3DCallDelegate(mUnityPlayer, bundle);
if (null != mOnUnity3DCallDelegate) {
if (mOnUnity3DCallDelegate instanceof Fragment) {
getSupportFragmentManager().beginTransaction().replace(unityPlayerContainerId(),
(Fragment) mOnUnity3DCallDelegate, ((Fragment) mOnUnity3DCallDelegate)
.getClass().getName()).commit();
} else if (mOnUnity3DCallDelegate instanceof android.app.Fragment) {
getFragmentManager().beginTransaction().replace(unityPlayerContainerId(),
(android.app.Fragment) mOnUnity3DCallDelegate, ((android.app.Fragment) mOnUnity3DCallDelegate)
.getClass().getName()).commit();
} else if (mOnUnity3DCallDelegate instanceof View) {
final ViewGroup unityContainer = findViewById(unityPlayerContainerId());
unityContainer.addView((View) mOnUnity3DCallDelegate);
} else {
throw new IllegalArgumentException("Not support type : " + mOnUnity3DCallDelegate.toString());
}
} else {
mOnUnity3DCallDelegate = this;
final ViewGroup unityContainer = findViewById(unityPlayerContainerId());
unityContainer.addView(mUnityPlayer);
mUnityPlayer.requestFocus();
}
}
@Override
protected void onNewIntent(Intent intent) {
// To support deep linking, we need to make sure that the client can get access to
// the last sent intent. The clients access this through a JNI api that allows them
// to get the intent set on launch. To update that after launch we have to manually
// replace the intent with the one caught here.
setIntent(intent);
}
// Quit Unity
@Override
protected void onDestroy() {
if (null != mUnityPlayer) {
mUnityPlayer.quit();
}
super.onDestroy();
}
// Pause Unity
@Override
protected void onPause() {
super.onPause();
if (null != mUnityPlayer) {
mUnityPlayer.pause();
}
}
// Resume Unity
@Override
protected void onResume() {
super.onResume();
if (null != mUnityPlayer) {
mUnityPlayer.resume();
}
}
@Override
protected void onStart() {
super.onStart();
if (null != mUnityPlayer) {
mUnityPlayer.start();
}
}
@Override
protected void onStop() {
super.onStop();
if (null != mUnityPlayer) {
mUnityPlayer.stop();
}
}
// Low Memory Unity
@Override
public void onLowMemory() {
super.onLowMemory();
if (null != mUnityPlayer) {
mUnityPlayer.lowMemory();
}
}
// Trim Memory Unity
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
if (level == TRIM_MEMORY_RUNNING_CRITICAL) {
if (null != mUnityPlayer) {
mUnityPlayer.lowMemory();
}
}
}
// This ensures the layout will be correct.
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (null != mUnityPlayer) {
mUnityPlayer.configurationChanged(newConfig);
}
}
// Notify Unity of the focus change.
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (null != mUnityPlayer) {
mUnityPlayer.windowFocusChanged(hasFocus);
}
}
// For some reason the multiple keyevent type is not supported by the ndk.
// Force event injection by overriding dispatchKeyEvent().
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_MULTIPLE) {
if (null != mUnityPlayer) {
return mUnityPlayer.injectEvent(event);
}
}
return super.dispatchKeyEvent(event);
}
// Pass any events not handled by (unfocused) views straight to UnityPlayer
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
return super.onKeyUp(keyCode, event);
}
if (null != mUnityPlayer) {
return mUnityPlayer.injectEvent(event);
}
return super.onKeyUp(keyCode, event);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
return super.onKeyDown(keyCode, event);
}
if (null != mUnityPlayer) {
return mUnityPlayer.injectEvent(event);
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (null != mUnityPlayer) {
return mUnityPlayer.injectEvent(event);
}
return super.onTouchEvent(event);
}
/*API12*/
public boolean onGenericMotionEvent(MotionEvent event) {
if (null != mUnityPlayer) {
return mUnityPlayer.injectEvent(event);
}
return super.onGenericMotionEvent(event);
}
@Nullable
@Override
public Context gatContext() {
return this;
}
@NonNull
@Override
public IOnUnity3DCall getOnUnity3DCall() {
//Perhaps this method is called after Unity is created after the activity is created,
// so there is no problem for the time being.
return mOnUnity3DCallDelegate;
}
@Nullable
protected IOnUnity3DCall generateIOnUnity3DCallDelegate(@NonNull UnityPlayer unityPlayer,
@Nullable Bundle bundle) {
return null;
}
}
別看這個類有200多行,其實絕大部分代碼都是重載,是爲了滿足Unity端的需求,真正要關注的方法就那麼三四個,只有generateIOnUnity3DCallDelegate(@NonNull UnityPlayer unityPlayer,@Nullable Bundle bundle)這個方法需要繼承的實現者去關注,這個方法的作用就是生成真正去加載和顯示Unity端使視圖的模塊,不關你是View也好,Fragment也好,只要你實現了IOnUnity3DCall接口和IUnityPlayerContainer接口,你就可以加載和顯示Unity端的視圖。
默認情況下,繼承至UnityPlayerActivity的類就是加載和顯示Unity端視圖的模塊,除非你重寫generateIOnUnity3DCallDelegate(@NonNull UnityPlayer unityPlayer,@Nullable Bundle bundle)方法,返回合適的代理,這個可以從initUnityPlayer(@NonNull Bundle bundle) 方法裏面直觀的看出來。
如果你還想再進一步,實現了IOnUnity3DCall接口和IUnityPlayerContainer接口的代理模塊還想讓其他模塊來顯示Unity短的View,那麼實現了IOnUnity3DCall接口和IUnityPlayerContainer接口的代理模塊就可以在實現IGetUnity3DCall接口,重寫generateIOnUnity3DCallDelegate(@NonNull UnityPlayer unityPlayer,@Nullable Bundle bundle)方法,返回合適的代理。當然,可能大多數情況下我們也不需要這麼做吧。
基於以上的原理,要讓Fragment作爲顯示Unity短的View的模塊的方法就呼之欲出了:
v4包下的Fragment
/**
* Description:The base Unity3D fragment, the Activity holding this Fragment needs to implement the {@link IGetUnity3DCall} interface and implement {@link IGetUnity3DCall#getOnUnity3DCall()}
* 方法返回此Fragment的實例
* Creator:yankebin
* CreatedAt:2018/12/1
*/
public abstract class UnityPlayerFragmentV4 extends BaseFragmentV4 implements IOnUnity3DCall, IUnityPlayerContainer {
protected UnityPlayer mUnityPlayer; // don't change the name of this variable; referenced from native code
public void setUnityPlayer(@NonNull UnityPlayer mUnityPlayer) {
this.mUnityPlayer = mUnityPlayer;
}
@Override
@CallSuper
public void onViewCreated(@NonNull Bundle params, @NonNull View contentView) {
if (null != mUnityPlayer) {
final ViewGroup unityContainer = contentView.findViewById(unityPlayerContainerId());
unityContainer.addView(mUnityPlayer);
mUnityPlayer.requestFocus();
}
}
@Nullable
@Override
public Context gatContext() {
return getActivity();
}
}
app包下的Fragment
/**
* Description:The base Unity3D fragment, the Activity holding this Fragment needs to implement the {@link IGetUnity3DCall} interface and implement {@link IGetUnity3DCall#getOnUnity3DCall()}
* Method returns an instance of this Fragment
* Creator:yankebin
* CreatedAt:2018/12/1
*/
public abstract class UnityPlayerFragment extends BaseFragment implements IOnUnity3DCall, IUnityPlayerContainer {
protected UnityPlayer mUnityPlayer; // don't change the name of this variable; referenced from native code
public void setUnityPlayer(@NonNull UnityPlayer mUnityPlayer) {
this.mUnityPlayer = mUnityPlayer;
}
@Override
@CallSuper
public void onViewCreated(@NonNull Bundle params, @NonNull View contentView) {
if (null != mUnityPlayer) {
final ViewGroup unityContainer = contentView.findViewById(unityPlayerContainerId());
unityContainer.addView(mUnityPlayer);
mUnityPlayer.requestFocus();
}
}
@Nullable
@Override
public Context gatContext() {
return getActivity();
}
}
兩者的實現完全一致,只是爲了讓開發者少封裝一種Fragment。我想,當你們看到這裏的時候,心中對如何讓一個ViewGroup或者Dialog顯示Unity端的View的方法已經很清晰了吧。
2.2 嘮叨的結語
在我的Demo裏,讓一個Activity加載和控制並且響應Unity做的一個小遊戲只需要不到100行代碼,看起來是不是已經很簡單啦?就像我在開頭所說的一樣,降低android端的接入複雜度其實是一個仁者見仁智者見智的問題,我所能做到的簡化大概就是這個樣子,但是我相信這絕對是一個很low的封裝,大神們肯定可以用各種各樣的設計模式來設計出牛b的架構,所以我只是在這裏給出我的一種見解,希望大家輕拍。
還有就是,其實做Unity的難點一直都不在於Android這端,也不在於兩端的通信,我做這個項目只適合入門級開發者閒時無事來看一看,擴寬點知識面,瞭解點新東西,順便練練自己的文筆,哈哈哈哈哈。
提前透露下吧,後面可能會寫幾篇關於基於Google chromium內核的Android Chrome瀏覽器相關的內容,主要涉及源碼改造和嵌入ijkplayer實現視頻播放的功能(類似騰訊X5內核的多媒體播放功能),希望大家多多支持。