一 、前言

最近都好忙好忙,感觉很累,曾好几次想继续把关于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内核的多媒体播放功能),希望大家多多支持。