一、前言
Unity3d引擎中有很多与Android原生交互的功能,比如如何调用Android渲染、Unity输入框的实现、Unity权限的申请、Unity调用相机功能等等,其实这些就是调用Android的api实现的。所有Java层的实现代码都是在unity-classes.jar
这个jar包中。这篇文章主要梳理一下Unity输入框的实现以及如何对输入框进行改造,顺带带出一些其它重要知识点。
二、Unity3d图形渲染的Java层实现
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对游戏页面进行渲染。
三、Unity3d输入框的实现机制
详细渲染过程这里不进行赘述,这篇文章主要梳理一下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整个输入功能的实现流程就是这样。
四、如何对Unity3d的输入框进行改造
让我们重新回到输入框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生命周期函数添加代码。其实很多安卓的三方框架都用到了这个思路,比如Glide、LeakCannary等。
OK,这篇文章主要是结合unity-classes.jar的源码,讲解了Unity3d游戏的输入框在安卓平台上的实现机制,也顺带讲解了unity-classes.jar中一些其它重要知识。
如有不足,欢迎留言指正~