Unity3d引擎中有很多与Android原生交互的功能,比如如何调用Android渲染、Unity输入框的实现、Unity权限的申请、Unity调用相机功能等等,其实这些就是调用Android的api实现的。所有Java层的实现代码都是在unity-classes.jar
这个jar包中。这篇文章主要梳理一下Unity输入框的实现以及如何对输入框进行改造,顺带带出一些其它重要知识点。
上图是Unity的输入框控件:InputField,点击它时会弹出系统的软键盘并且会有一个原生的输入框附在软键盘上面,输入内容时两个输入框的内容会同步。
因为Unity的输入框控件不能随便变换位置,比如软键盘弹出时像安卓那个把输入框顶上去,所以Unity使用原生技术做了一个输入框固定在软键盘上面,防止Unity输入框被软键盘摭挡,用户看不到输入内容。
Unity游戏工程导出Android Gradle工程时,会依赖一个名叫unity-classes.jar
的库,它就是Android与Unity引擎交互的Java层的实现。
如下图unity导出gradle工程中unityLibrary/libs/unity-classes.jar
文件:
这个unity-classes.jar除了包含了基本的Unity3d渲染桥接代码,还包含了Unity里输入框功能在Android平台上的实现。
unity-clases.jar的路径在unity安装目录下的,比如mac是这个路径:
Unity/Mac/AndroidPlayer/Variations/mono/Release/Classes/classes.jar
在unity3d 2019.3版本开始,unity-classes.jar将UnityPlayerActivity.class
这个类的删除了,将其移出并以java源码的方式给出了。而导出的gradle工程默认入口Activity就是UnityPlayerActivity。
UnityPlayerActivity.java文件的路径(以mac为例):
Unity/Mac/AndroidPlayer/Source/com/unity3d/player/UnityPlayerActivity.java
UnityPlayerActivity.java的源码如下:
// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN
package com.unity3d.player;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.os.Process;
public class UnityPlayerActivity extends Activity implements IUnityPlayerLifecycleEvents
{
protected UnityPlayer mUnityPlayer; // don't change the name of this variable; referenced from native code
// Override this in your custom UnityPlayerActivity to tweak the command line arguments passed to the Unity Android Player
// The command line arguments are passed as a string, separated by spaces
// UnityPlayerActivity calls this from 'onCreate'
// Supported: -force-gles20, -force-gles30, -force-gles31, -force-gles31aep, -force-gles32, -force-gles, -force-vulkan
// See https://docs.unity3d.com/Manual/CommandLineArguments.html
// @param cmdLine the current command line arguments, may be null
// @return the modified command line string or null
protected String updateUnityCommandLineArguments(String cmdLine)
{
return cmdLine;
}
// Setup activity layout
@Override protected void onCreate(Bundle savedInstanceState)
{
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);
String cmdLine = updateUnityCommandLineArguments(getIntent().getStringExtra("unity"));
getIntent().putExtra("unity", cmdLine);
mUnityPlayer = new UnityPlayer(this, this);
setContentView(mUnityPlayer);
mUnityPlayer.requestFocus();
}
// When Unity player unloaded move task to background
@Override public void onUnityPlayerUnloaded() {
moveTaskToBack(true);
}
// When Unity player quited kill process
@Override public void onUnityPlayerQuitted() {
Process.killProcess(Process.myPid());
}
@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);
mUnityPlayer.newIntent(intent);
}
// Quit Unity
@Override protected void onDestroy ()
{
mUnityPlayer.destroy();
super.onDestroy();
}
// Pause Unity
@Override protected void onPause()
{
super.onPause();
mUnityPlayer.pause();
}
// Resume Unity
@Override protected void onResume()
{
super.onResume();
mUnityPlayer.resume();
}
// Low Memory Unity
@Override public void onLowMemory()
{
super.onLowMemory();
mUnityPlayer.lowMemory();
}
// Trim Memory Unity
@Override public void onTrimMemory(int level)
{
super.onTrimMemory(level);
if (level == TRIM_MEMORY_RUNNING_CRITICAL)
{
mUnityPlayer.lowMemory();
}
}
// This ensures the layout will be correct.
@Override public void onConfigurationChanged(Configuration newConfig)
{
super.onConfigurationChanged(newConfig);
mUnityPlayer.configurationChanged(newConfig);
}
// Notify Unity of the focus change.
@Override public void onWindowFocusChanged(boolean hasFocus)
{
super.onWindowFocusChanged(hasFocus);
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)
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) { return mUnityPlayer.injectEvent(event); }
@Override public boolean onKeyDown(int keyCode, KeyEvent event) { return mUnityPlayer.injectEvent(event); }
@Override public boolean onTouchEvent(MotionEvent event) { return mUnityPlayer.injectEvent(event); }
/*API12*/ public boolean onGenericMotionEvent(MotionEvent event) { return mUnityPlayer.injectEvent(event); }
}
可见,这个类的源码并不多,其实是将UnityPlayer这个类作为了Activity视图。所以Unity在Android上的渲染桥接代码基本就在UnityPlayer这个类中。
由于UnityPlayer.java代码比较长,只取了部分代码:
public class UnityPlayer extends FrameLayout implements IUnityPlayerLifecycleEvents, com.unity3d.player.f {
public static Activity currentActivity = null;
private int mInitialScreenOrientation;
private boolean mMainDisplayOverride;
private boolean mIsFullscreen;
private n mState;
private final ConcurrentLinkedQueue m_Events;
private BroadcastReceiver mKillingIsMyBusiness;
private OrientationEventListener mOrientationListener;
private int mNaturalOrientation;
private static final int ANR_TIMEOUT_SECONDS = 4;
private static final int RUN_STATE_CHANGED_MSG_CODE = 2269;
e m_MainThread;
private boolean m_AddPhoneCallListener;
private c m_PhoneCallListener;
private TelephonyManager m_TelephonyManager;
private ClipboardManager m_ClipboardManager;
private l m_SplashScreen;
private GoogleARCoreApi m_ARCoreApi;
private a m_FakeListener;
private Camera2Wrapper m_Camera2Wrapper;
private HFPStatus m_HFPStatus;
private AudioVolumeHandler m_AudioVolumeHandler;
private Uri m_launchUri;
private NetworkConnectivity m_NetworkConnectivity;
private IUnityPlayerLifecycleEvents m_UnityPlayerLifecycleEvents;
private Context mContext;
private SurfaceView mGlView;
private boolean mQuitting;
private boolean mProcessKillRequested;
private q mVideoPlayerProxy;
k mSoftInputDialog;
private static final String SPLASH_ENABLE_METADATA_NAME = "unity.splash-enable";
private static final String SPLASH_MODE_METADATA_NAME = "unity.splash-mode";
private static final String TANGO_ENABLE_METADATA_NAME = "unity.tango-enable";
public UnityPlayer(Context var1) {
this(var1, (IUnityPlayerLifecycleEvents)null);
}
public UnityPlayer(Context var1, IUnityPlayerLifecycleEvents var2) {
super(var1);
this.mInitialScreenOrientation = -1;
this.mMainDisplayOverride = false;
this.mIsFullscreen = true;
this.mState = new n();
this.m_Events = new ConcurrentLinkedQueue();
this.mKillingIsMyBusiness = null;
this.mOrientationListener = null;
this.m_MainThread = new e((byte)0);
this.m_AddPhoneCallListener = false;
this.m_PhoneCallListener = new c((byte)0);
this.m_ARCoreApi = null;
this.m_FakeListener = new a();
this.m_Camera2Wrapper = null;
this.m_HFPStatus = null;
this.m_AudioVolumeHandler = null;
this.m_launchUri = null;
this.m_NetworkConnectivity = null;
this.m_UnityPlayerLifecycleEvents = null;
this.mProcessKillRequested = true;
this.mSoftInputDialog = null;
this.m_UnityPlayerLifecycleEvents = var2;
if (var1 instanceof Activity) {
currentActivity = (Activity)var1;
this.mInitialScreenOrientation = currentActivity.getRequestedOrientation();
this.m_launchUri = currentActivity.getIntent().getData();
}
this.EarlyEnableFullScreenIfVrLaunched(currentActivity);
this.mContext = var1;
Configuration var5 = this.getResources().getConfiguration();
this.mNaturalOrientation = this.getNaturalOrientation(var5.orientation);
if (currentActivity != null && this.getSplashEnabled()) {
this.m_SplashScreen = new l(this.mContext, com.unity3d.player.l.a.a()[this.getSplashMode()]);
this.addView(this.m_SplashScreen);
}
String var6 = loadNative(this.mContext.getApplicationInfo());
if (!n.c()) {
String var3 = "Your hardware does not support this application.";
g.Log(6, var3);
AlertDialog var4;
(var4 = (new AlertDialog.Builder(this.mContext)).setTitle("Failure to initialize!").setPositiveButton("OK", new DialogInterface.OnClickListener() {
public final void onClick(DialogInterface var1, int var2) {
UnityPlayer.this.finish();
}
}).setMessage(var3 + "\n\n" + var6 + "\n\n Press OK to quit.").create()).setCancelable(false);
var4.show();
} else {
this.initJni(var1);
this.mState.c(true);
this.mGlView = this.CreateGlView();
this.mGlView.setContentDescription(this.GetGlViewContentDescription(var1));
this.addView(this.mGlView);
this.bringChildToFront(this.m_SplashScreen);
this.mQuitting = false;
this.hideStatusBar();
this.m_TelephonyManager = (TelephonyManager)this.mContext.getSystemService("phone");
this.m_ClipboardManager = (ClipboardManager)this.mContext.getSystemService("clipboard");
this.m_Camera2Wrapper = new Camera2Wrapper(this.mContext);
this.m_HFPStatus = new HFPStatus(this.mContext);
this.m_MainThread.start();
}
}
UnityPlayer就是一个继承FrameLayout的自定义View,里面使用到了SurfaceView,通过底层的图形api对游戏页面进行渲染。
详细渲染过程这里不进行赘述,这篇文章主要梳理一下Unity输入框的实现。
先看一下unity-classes.jar的目录结构:
可见,代码已经进行了混淆,不太好阅读。
不过这并不影响我们去找输入框的代码,因为这个jar的代码并不多。
在UnityPlayer
这个类中有一个成员变量:
k mSoftInputDialog;
从名字来看是跟输入框有关的。然后我们打开k
这个类发现果真是输入框的实现。
k
这个类反编译后的代码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.unity3d.player;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.text.Editable;
import android.text.InputFilter;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import android.widget.Button;
import android.widget.EditText;
import android.widget.RelativeLayout;
import android.widget.TextView;
public final class k extends Dialog implements TextWatcher, View.OnClickListener {
private Context a = null;
private UnityPlayer b = null;
private static int c = 1627389952;
private static int d = -1;
private int e;
public k(Context var1, UnityPlayer var2, String var3, int var4, boolean var5, boolean var6, boolean var7, String var8, int var9, boolean var10) {
super(var1);
this.a = var1;
this.b = var2;
Window var12;
(var12 = this.getWindow()).requestFeature(1);
WindowManager.LayoutParams var14;
(var14 = var12.getAttributes()).gravity = 80;
var14.x = 0;
var14.y = 0;
var12.setAttributes(var14);
var12.setBackgroundDrawable(new ColorDrawable(0));
final View var15 = this.createSoftInputView();
this.setContentView(var15);
var12.setLayout(-1, -2);
var12.clearFlags(2);
var12.clearFlags(134217728);
var12.clearFlags(67108864);
EditText var13 = (EditText)this.findViewById(1057292289);
Button var11 = (Button)this.findViewById(1057292290);
this.a(var13, var3, var4, var5, var6, var7, var8, var9);
var11.setOnClickListener(this);
this.e = var13.getCurrentTextColor();
this.a(var10);
this.b.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public final void onGlobalLayout() {
if (var15.isShown()) {
Rect var1 = new Rect();
k.this.b.getWindowVisibleDisplayFrame(var1);
int[] var2 = new int[2];
k.this.b.getLocationOnScreen(var2);
Point var4 = new Point(var1.left - var2[0], var1.height() - var15.getHeight());
Point var5 = new Point();
k.this.getWindow().getWindowManager().getDefaultDisplay().getSize(var5);
int var6 = k.this.b.getHeight() - var5.y;
int var3 = k.this.b.getHeight() - var4.y;
var6 += var15.getHeight();
if (var3 != var6) {
k.this.b.reportSoftInputIsVisible(true);
} else {
k.this.b.reportSoftInputIsVisible(false);
}
var1 = new Rect(var4.x, var4.y, var15.getWidth(), var3);
k.this.b.reportSoftInputArea(var1);
}
}
});
var13.setOnFocusChangeListener(new View.OnFocusChangeListener() {
public final void onFocusChange(View var1, boolean var2) {
if (var2) {
k.this.getWindow().setSoftInputMode(5);
}
}
});
var13.requestFocus();
}
public final void a(boolean var1) {
EditText var2 = (EditText)this.findViewById(1057292289);
Button var3 = (Button)this.findViewById(1057292290);
View var4 = this.findViewById(1057292291);
if (var1) {
var2.setBackgroundColor(0);
var2.setTextColor(0);
var2.setCursorVisible(false);
var3.setClickable(false);
var3.setTextColor(0);
var4.setBackgroundColor(0);
} else {
var2.setBackgroundColor(d);
var2.setTextColor(this.e);
var2.setCursorVisible(true);
var3.setClickable(true);
var3.setTextColor(this.e);
var4.setBackgroundColor(d);
}
}
private void a(EditText var1, String var2, int var3, boolean var4, boolean var5, boolean var6, String var7, int var8) {
var1.setImeOptions(6);
var1.setText(var2);
var1.setHint(var7);
var1.setHintTextColor(c);
var1.setInputType(a(var3, var4, var5, var6));
var1.setImeOptions(33554432);
if (var8 > 0) {
var1.setFilters(new InputFilter[]{new InputFilter.LengthFilter(var8)});
}
var1.addTextChangedListener(this);
var1.setSelection(var1.getText().length());
var1.setClickable(true);
}
public final void afterTextChanged(Editable var1) {
this.b.reportSoftInputStr(var1.toString(), 0, false);
}
public final void beforeTextChanged(CharSequence var1, int var2, int var3, int var4) {
}
public final void onTextChanged(CharSequence var1, int var2, int var3, int var4) {
}
private static int a(int var0, boolean var1, boolean var2, boolean var3) {
int var4 = (var1 ? '耀' : 524288) | (var2 ? 131072 : 0) | (var3 ? 128 : 0);
if (var0 >= 0 && var0 <= 11) {
int[] var5;
return ((var5 = new int[]{1, 16385, 12290, 17, 2, 3, 8289, 33, 1, 16417, 17, 8194})[var0] & 2) != 0 ? var5[var0] : var4 | var5[var0];
} else {
return var4;
}
}
private void a(String var1, boolean var2) {
((EditText)this.findViewById(1057292289)).setSelection(0, 0);
this.b.reportSoftInputStr(var1, 1, var2);
}
public final void onClick(View var1) {
this.a(this.b(), false);
}
public final void onBackPressed() {
this.a(this.b(), true);
}
protected final View createSoftInputView() {
RelativeLayout var1;
(var1 = new RelativeLayout(this.a)).setLayoutParams(new ViewGroup.LayoutParams(-1, -1));
var1.setBackgroundColor(d);
var1.setId(1057292291);
EditText var3 = new EditText(this.a) {
public final boolean onKeyPreIme(int var1, KeyEvent var2) {
if (var1 == 4) {
k.this.a(k.this.b(), true);
return true;
} else {
return var1 == 84 ? true : super.onKeyPreIme(var1, var2);
}
}
public final void onWindowFocusChanged(boolean var1) {
super.onWindowFocusChanged(var1);
if (var1) {
((InputMethodManager)k.this.a.getSystemService("input_method")).showSoftInput(this, 0);
}
}
protected final void onSelectionChanged(int var1, int var2) {
k.this.b.reportSoftInputSelection(var1, var2 - var1);
}
};
RelativeLayout.LayoutParams var2;
(var2 = new RelativeLayout.LayoutParams(-1, -2)).addRule(15);
var2.addRule(0, 1057292290);
var3.setLayoutParams(var2);
var3.setId(1057292289);
var1.addView(var3);
Button var4;
(var4 = new Button(this.a)).setText(this.a.getResources().getIdentifier("ok", "string", "android"));
(var2 = new RelativeLayout.LayoutParams(-2, -2)).addRule(15);
var2.addRule(11);
var4.setLayoutParams(var2);
var4.setId(1057292290);
var4.setBackgroundColor(0);
var1.addView(var4);
((EditText)var1.findViewById(1057292289)).setOnEditorActionListener(new TextView.OnEditorActionListener() {
public final boolean onEditorAction(TextView var1, int var2, KeyEvent var3) {
if (var2 == 6) {
k.this.a(k.this.b(), false);
}
return false;
}
});
var1.setPadding(16, 16, 16, 16);
return var1;
}
private String b() {
EditText var1;
return (var1 = (EditText)this.findViewById(1057292289)) == null ? null : var1.getText().toString().trim();
}
public final void a(String var1) {
EditText var2;
if ((var2 = (EditText)this.findViewById(1057292289)) != null) {
var2.setText(var1);
var2.setSelection(var1.length());
}
}
public final void a(int var1) {
EditText var2;
if ((var2 = (EditText)this.findViewById(1057292289)) != null) {
if (var1 > 0) {
var2.setFilters(new InputFilter[]{new InputFilter.LengthFilter(var1)});
return;
}
var2.setFilters(new InputFilter[0]);
}
}
public final void a(int var1, int var2) {
EditText var3;
if ((var3 = (EditText)this.findViewById(1057292289)) != null && var3.getText().length() >= var1 + var2) {
var3.setSelection(var1, var1 + var2);
}
}
public final String a() {
InputMethodSubtype var1;
if ((var1 = ((InputMethodManager)this.a.getSystemService("input_method")).getCurrentInputMethodSubtype()) == null) {
return null;
} else {
String var2;
if ((var2 = var1.getLocale()) != null && !var2.equals("")) {
return var2;
} else {
var2 = var1.getMode();
String var3 = var1.getExtraValue();
return var2 + " " + var3;
}
}
}
}
这是AS帮我们反编译的代码,所以并不是原始的java,但不影响阅读。
Unity的输入框,其实就是一个自定义的Dialog。
这个Dialog的UI是通过代码动态创建的,UI创建代码在
protected final View createSoftInputView()
这个方法中,见以上源码。
我们重点要关注k
这个类的构造方法。
构造方法的大致流程:
1、调用createSoftInputView()
方法创建输入框的UI,返回View
2、调用Dialog的setContentView方法(传入以上View),给Dialog设置UI
3、获取以上View中的EditText和Button给它们配置默认属性。
4、监听软键盘弹出和收起,将事件报告给Unity。
5、调用EditText的requestFocus
方法,获取焦点。这一步很重要,相当于主动弹出软键盘。
k这个类的实例是UnityPlayer的成员变量mSoftInputDialog
,它是在showSoftInput
方法中初始化的。
mSoftInputDialog
初始化的代码在showSoftInput
方法中:
//com.unity3d.player.UnityPlayer.java
protected void showSoftInput(final String var1, final int var2, final boolean var3, final boolean var4, final boolean var5, final boolean var6, final String var7, final int var8, final boolean var9) {
this.postOnUiThread(new Runnable() {
public final void run() {
UnityPlayer.this.mSoftInputDialog = new k(UnityPlayer.this.mContext, UnityPlayer.this, var1, var2, var3, var4, var5, var7, var8, var9);
UnityPlayer.this.mSoftInputDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
public final void onCancel(DialogInterface var1x) {
UnityPlayer.this.nativeSoftInputLostFocus();
UnityPlayer.this.reportSoftInputStr((String)null, 1, false);
}
});
UnityPlayer.this.mSoftInputDialog.show();
UnityPlayer.this.nativeReportKeyboardConfigChanged();
}
});
}
showSoftInput
方法并不是Java层调用的,而是Unity调用的,也即是so文件中。
这里已经能大致猜到Unity输入控件的实现机制了。
其实就是Unity中有一个输入控件InputField
,它本质是一个展示类的组件,并不能输入内容也不能获取焦点,跟安卓的EditTex相差甚远。可以类比Android的TextView控件。因此要Unity要接受用户输入,只能调用安卓的EditText。
Unity的InputField能接收输入,实现流程就是:玩家点击InputField控件表示要输入内容,然后Unity获取到点击事件后,调用Java层的showSoftInput
方法,showSoftInput
会弹出一个自定义Dialog,这个Dialog里有一个EditText,然后调用EditText的requestFocus方法将系统的软键盘弹出来。然后监听EditText的内容变化,将文本发回到Unity,Unity将文本显示到InputField上面。Unity整个输入功能的实现流程就是这样。
让我们重新回到输入框Dialog代码k
这个类。
这个类的createSoftInputView()
方法就是创建输入框UI。大致过程是:先创建一个RelativeLayout然后创建EditText并添加到RelativeLayout中,并在EditText右边添加了一个Button。整个UI是非常的简单。EditText和Button控件的id是写死的。
在很多游戏中发现它们对Unity默认输入框进行了改造,比如将EditText放到屏幕的顶部、修改EditText样式、限制最大输入行数等等,或者对Button的一些属性进行了修改,总之就是让输入框更加人性化或者个性化。
如果没有源代码的话,只能使用继承或者反射的方式进行修改。因为这个输入框Dialog是final类并且创建ui方法也是final的,所以继承就没办法了。那目前唯一的思路就是使用反射。
只要能获取到这个输入框Dialog的实例,就能获取到输入框的EditTextt和Button,然后就能对它们进行修改。
然后,输入框Dialog的实例是UnityPlayer的成员变量,UnityPlayer的实例是UnityPlayerActivity的成员变量,并且是在onCreate函数中初始化的。因此入口点是在Activity的onCreate中获取UnityPlayer的实例,然后顺藤摸瓜就能获取到输入框的控件,利用反射进行修改。
那么问题是输入框Dialog并不是游戏启动就创建的,因此反射的代码不是在Activity的onCreate中执行,而是要等到玩家点击Unity输入控件InputField
,或者说软键盘弹出来之后才可以。因为不能在onCreate里立即就能获取到输入框Dialog的实例。
经过上文Unity输入框实现流程的分析,我们知道,Java层只要监听到软键盘弹起,就代表UnityPlayer的成员变量mSoftInputDialog
已经初始化了。
因此,在Activity的onCreate方法,我们先监听一下软键盘弹起,将我们反射的代码放到软键盘弹起的回调中进行。
那么如何监听软健盘弹起呢?Android其实并没有提供监听软健盘的api,iOS倒是提供了通知的方式进行监听。(这里要吐槽一下Android,对开发者不太友好)
监听软键盘的api,其实unity-classes.jar中的k
这个类已经给出了示例。就是在k的构造方法中:
this.b.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public final void onGlobalLayout() {
if (var15.isShown()) {
Rect var1 = new Rect();
k.this.b.getWindowVisibleDisplayFrame(var1);
int[] var2 = new int[2];
k.this.b.getLocationOnScreen(var2);
Point var4 = new Point(var1.left - var2[0], var1.height() - var15.getHeight());
Point var5 = new Point();
k.this.getWindow().getWindowManager().getDefaultDisplay().getSize(var5);
int var6 = k.this.b.getHeight() - var5.y;
int var3 = k.this.b.getHeight() - var4.y;
var6 += var15.getHeight();
if (var3 != var6) {
//软键盘弹出
k.this.b.reportSoftInputIsVisible(true);
} else {
//软键盘收起
k.this.b.reportSoftInputIsVisible(false);
}
var1 = new Rect(var4.x, var4.y, var15.getWidth(), var3);
k.this.b.reportSoftInputArea(var1);
}
}
});
大致就是监听视图的变化。k.this.b
就是UnityPlayer对象,UnityPlayer就是整个游戏的视图,它是一个FrameLayout。
简单一点就是以下这样,实现思路是一样的:
unityPlayer.viewTreeObserver.addOnGlobalLayoutListener(OnGlobalLayoutListener {
val r = Rect()
unityPlayer.getWindowVisibleDisplayFrame(r)
val visibleHeight = r.height()
if (lastVisibleHeight == 0) {
lastVisibleHeight = visibleHeight
return@OnGlobalLayoutListener
}
if (lastVisibleHeight == visibleHeight) {
return@OnGlobalLayoutListener
}
//根视图显示高度变小超过200,可以看作软键盘显示了
if (lastVisibleHeight - visibleHeight > 200) {
//软键盘显示
lastVisibleHeight = visibleHeight
//这里可以开始对输入框Dialog进行反射修改
return@OnGlobalLayoutListener
}
//根视图显示高度变大超过200,可以看作软键盘隐藏了
if (visibleHeight - lastVisibleHeight > 200) {
//软键盘隐藏
lastVisibleHeight = visibleHeight
return@OnGlobalLayoutListener
}
})
这里如何反射修改输入框就不讲了,算是比较基本的Java语法。
还有一个问题就是如何在UnityPlayerActivity的onCreate生命周期方法中添加代码呢?
关于此问题,还有类似的问题:
如何在UnityPlayerActivity的生命周期onResume或其它生命周期方法中添加代码?
如何在UnityPlayerActivity的权限回调onRequestPermissionsResult
方法中添加权限处理代码?
这些实际就是同类的问题。方式比较多:
1、可以直接修改它在unity中的源码,好处是不用每次导出gradle工程都要修改。坏处就是对其它项目有影响。
2、导出gradle工程然后手动修改UnityPlayerActivity。显然不可取。
3、通过Unity的打包后处理脚本,用修改的类替换同名的UnityPlayerActivity。
4、继承UnityPlayerActivity,将新入口Activity配置到AndroidManifest中。
5、监听UnityPlayerActivity的生命周期,坏处就是游戏有多个Activity时不好区分。
以上方式其实对UnityPlayerActivity有强依赖关系,有没有不依赖UnityPlayerActivity的方式呢?
其实在unity-classes.jar
的源码中也给出了答案。
思路就是通过UnityPlayer静态属性currentActivity
获取UnityPlayerActivity,然后在Activity中添加一个自定义的Fragment,在这个Fragment生命周期方法中实现我们需要的功能。
看一下unity-classes.jar中h
这个类的源码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.unity3d.player;
import android.app.Activity;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageItemInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
public final class h implements e {
public h() {
}
//省略无关代码
public final void a(Activity var1, String var2) {
if (var1 != null && var2 != null) {
FragmentManager var6 = var1.getFragmentManager();
String var3 = "96489";
if (var6.findFragmentByTag(var3) == null) {
i var4 = new i();
Bundle var5;
(var5 = new Bundle()).putString("PermissionNames", var2);
var4.setArguments(var5);
FragmentTransaction var7;
(var7 = var6.beginTransaction()).add(0, var4, var3);
var7.commit();
}
}
}
}
可以看到在a
方法中添加了一个Fragment,这个Frament就是i
这个类。
i
这个类反编译后的代码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.unity3d.player;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.os.Bundle;
public final class i extends Fragment {
public i() {
}
public final void onCreate(Bundle var1) {
super.onCreate(var1);
String[] var2 = new String[]{this.getArguments().getString("PermissionNames")};
this.requestPermissions(var2, 96489);
}
public final void onRequestPermissionsResult(int var1, String[] var2, int[] var3) {
if (var1 == 96489) {
if (var2.length == 0) {
String[] var5 = new String[]{this.getArguments().getString("PermissionNames")};
this.requestPermissions(var5, 96489);
} else {
FragmentTransaction var4;
(var4 = this.getActivity().getFragmentManager().beginTransaction()).remove(this);
var4.commit();
}
}
}
}
可以看到它在onCreate
和onRequestPermissionsResult
的方法中添加了权限相关的代码。
看到这里就知道Unity中申请权限是怎么实现的了。
是不是很巧妙?它既没有直接修改Activity的代码也没有继承Activity更没有监听Activity的生命周期,就实现了在Activity生命周期函数添加代码。其实很多安卓的三方框架都用到了这个思路,比如Glide、LeakCannary等。
OK,这篇文章主要是结合unity-classes.jar的源码,讲解了Unity3d游戏的输入框在安卓平台上的实现机制,也顺带讲解了unity-classes.jar中一些其它重要知识。
如有不足,欢迎留言指正~