【Unity3d】Unity3d在Android平台上输入框的实现源码分析

news2024/11/25 16:36:06

一、前言

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();
            }
        }
    }
}

可以看到它在onCreateonRequestPermissionsResult的方法中添加了权限相关的代码。

看到这里就知道Unity中申请权限是怎么实现的了。

是不是很巧妙?它并没有直接修改Activity的代码也没有继承Activity,就实现了在Activity生命周期函数添加代码。其实很多安卓的三方框架都用到了这个思路,比如Glide、LeakCannary等。

OK,这篇文章主要是结合unity-classes.jar的源码,讲解了Unity3d游戏的输入框在安卓平台上的实现机制,也顺带讲解了unity-classes.jar中一些其它重要知识。

如有不足,欢迎留言指正~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/499264.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

chatgpt的150个指令大全

chatGPT输出结果的质量高低&#xff0c;和你使用什么样质量的输入内容有关。 今天&#xff0c;小编整理了一些常用的ChatGPT指令&#xff0c;可以通过这些指令让AI帮你整理资料、撰写报告、知识学习、准备面试等等&#xff0c;赶紧收藏起来吧&#xff01; 如果还没有找到工具…

两万字详解!Netty经典32连问

两万字详解&#xff01;Netty经典32连问&#xff01; 前言 Netty是什么&#xff0c;它的主要特点是什么&#xff1f; Netty 应用场景了解么&#xff1f; Netty 核心组件有哪些&#xff1f;分别有什么作用&#xff1f; Netty的线程模型是怎样的&#xff1f;如何优化性能&…

四、Spring Cloud Alibaba-Ribbon

一、什么是Ribbon 目前主流的负载方案分为以下两种: 集中式负载均衡&#xff0c;在消费者和服务提供方中间使用独立的代理方式进行负载&#xff0c;有硬件的(比如 F5)&#xff0c;也有软件的(比如 Nginx) 。客户端根据自己的请求情况做负载均衡&#xff0c;Ribbon 就属于客户…

2023年全国硕士研究生入学统一考试英语(一)试题

2023年全国硕士研究生入学统一考试英语&#xff08;一&#xff09;试题 Section I Use of Englis Directions: Read the following text. Choose the best word(s) for each numbered blank and mark A, B , C or D on the ANSWER SHEET.(10 points) Caravanserais were roads…

【P9】JMeter 用户定义的变量(User Defined Variables)

一、准备工作 慕慕生鲜&#xff1a; http://111.231.103.117/#/login 进入网页后&#xff0c;登录&#xff0c;页面提供了账户和密码 右键检查或按F12&#xff0c;打开调试工具&#xff0c;点击搜索 二、测试计划设计 &#xff08;1&#xff09;、Test Plan 右键 <<…

Linux cgroup

前言 Cgroup和namespace类似&#xff0c;也是将进程进程分组&#xff0c;但是目的与namespace不一样&#xff0c;namespace是为了隔离进程组之前的资源&#xff0c;而Cgroup是为了对一组进程进行统一的资源监控和限制。 Cgroup的组成 subsystem 一个subsystem就是一个内核模…

【HCIP】VLAN实验(Hybrid模式)

目录 需求&#xff1a; 一、设计 二、VLAN配置 三、交换机间实现trunk的功能 四、路由器配置 五、验证 需求&#xff1a; 1、PC1和PC3所在接口为access 2、PC2/4/5/6处于同一网段&#xff0c;其中PC2可以访问PC4/5/6&#xff1b;但PC4可以访问PC5&#xff0c;不能访问PC…

ARM嵌入式编译器-volatile关键字对编译器优化的影响

volatile限定符告知计算机&#xff0c;其他agent&#xff08;而不是变量所在的程序&#xff09;可以改变该变量的值。通常它被用于硬件地址以及在其他程序或同时运行的线程中共享数据。要求编译器不要对其描述的对象作优化处理&#xff0c;对它的读写都需要从内存中访问。 使用…

文献阅读:LLaMA: Open and Efficient Foundation Language Models

文献阅读&#xff1a;LLaMA: Open and Efficient Foundation Language Models 1. 文章简介2. 模型训练 1. 训练数据2. 模型结构3. 模型训练 1. Optimizer2. 效率优化 3. 效果评估 1. 经典任务下效果 1. Commen Sense Reasoning2. Closed-book Question Answering3. Reading Co…

数据分析03——矩阵常用计算方法和函数

0、前言&#xff1a; 数组&#xff1a;计算机领域的概念矩阵&#xff1a;数学领域的概念对于Numpy而言&#xff0c;矩阵是数组的分支 1、创建矩阵&#xff1a; 字符串创建矩阵&#xff1a;mat1 np.matrix(‘1 2;3 4’)列表形式创建矩阵&#xff1a;mat2 np.matrix([[5, 6],…

MySQL基础(八)聚合函数

上一章讲到了 SQL 单行函数。实际上 SQL 函数还有一类&#xff0c;叫做聚合&#xff08;或聚集、分组&#xff09;函数&#xff0c;它是对一组数据进行汇总的函数&#xff0c;输入的是一组数据的集合&#xff0c;输出的是单个值。 1. 聚合函数介绍 什么是聚合函数 聚合函数作…

深度学习目标检测项目实战(五)—基于mobilenetv2和resnet的图像背景抠图及其界面封装

深度学习目标检测项目实战(五)—基于mobilenetv2和resnet的图像背景抠图及其界面封装 该项目很有意思&#xff0c;也是比较前沿&#xff0c;项目主要参考了开源代码&#xff1a; https://github.com/PeterL1n/BackgroundMattingV2 环境搭建 kornia0.4.1 tensorboard2.3.0 to…

图像修复_criminis算法及改进算法学习小结

摘要 对图像修复专题学习情况的一个总结&#xff0c;学习内容包括&#xff1a; &#xff08;1&#xff09;综述文献的阅读及对图像修复的理解。 &#xff08;2&#xff09;criminis算法的仿真情况。 &#xff08;3&#xff09;criminis算法的改进算法的仿真 一、 前言 1&…

【leetcode】138.复制带随机指针的链表

《力扣》138.复制带随机指针的链表 给你一个长度为 n 的链表&#xff0c;每个节点包含一个额外增加的随机指针 random &#xff0c;该指针可以指向链表中的任何节点或空节点。 构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成&#xff0c;其中每个新节点的值都设…

[羊城杯 2020]a_piece_of_java

首先jd-gui进行反编译 简单查看发现有用的类就两个一个是 MainContrller.class和InfoInvocationHandler.class public class MainController {GetMapping({"/index"})public String index(CookieValue(value "data", required false) String cookieDa…

【C++】类和对象(初阶认识)#上篇#

目录 对面向过程和面向对象的初步认识 类的引入 封装 和 类的访问限定符 所以祖师爷在类中还引入了访问权限 用类定义变量 类的理解和对象的实例化 sizeof 计算类对象的大小 类对象的成员函数在公共代码区 this 指针 对面向过程和面向对象的初步认识 什么&#xff0c;是…

分片集群-搭建

分片集群 高数据量和吞吐量的数据库应用会对单机的性能造成较大压力,大的查询量会将单机的CPU耗尽,大的数据量对单机的存储压力较大,最终会耗尽系统的内存而将压力转移到磁盘IO上。 为了解决这些问题,有两个基本的方法: 垂直扩展和水平扩展。 垂直扩展&#xff1a;增加更多的…

阿里云服务器地域和可用区怎么选择合适?

阿里云服务器地域和可用区怎么选择&#xff1f;地域是指云服务器所在物理数据中心的位置&#xff0c;地域选择就近选择&#xff0c;访客距离地域所在城市越近网络延迟越低&#xff0c;速度就越快&#xff1b;可用区是指同一个地域下&#xff0c;网络和电力相互独立的区域&#…

ROHM常见的电冰箱控制电路图,轻松了解冰箱工作原理

​冰箱是我们日常生活中使用频率非常高的电器&#xff0c;它可以将食物和饮料保存在低温下&#xff0c;以延长它们的保质期。冰箱的工作原理主要基于制冷循环和温度控制。 先看电路图&#xff1a; 1. 单门直冷式电冰箱重锤式控制电路 电路的基本组成&#xff1a;采用重锤式启…

win10安装pytorch全网最好用的教程[2023.5.7更新]

目录 0.关于pytorch a. 什么是 PyTorch &#xff1f; b. 为何选择 PyTorch &#xff1f; 1.安装pytorch 1.1确定关联性 1.2下载最新版本的pytorch 1.3.pytorch历史版本下载 1.4 避坑 1.4.1、猜测 1.4.2、验证 1.4.3、解决方案 1.5、检验 0.关于pytorch a. 什么是…