Android與Unity通信的SDK(二)

一 、前言

最近都好忙好忙,感覺很累,曾好幾次想繼續把關於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內核的多媒體播放功能),希望大家多多支持。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章