[Unity3d] Source code analysis of the input box of Unity3d on the Android platform

I. Introduction

The Unity3d engine has a lot of native interaction functions with Android, such as how to call Android rendering, the realization of the Unity input box, the application of Unity permissions, Unity calling the camera function, etc., in fact, these are realized by calling the Android API. The implementation code of all Java layers is in unity-classes.jarthis jar package. This article mainly sorts out the realization of the Unity input box and how to transform the input box, and brings out some other important knowledge points by the way.

Two, the Java layer implementation of Unity3d graphics rendering

When the Unity game project exports the Android Gradle project, it will rely on a unity-classes.jarlibrary called Java, which is the implementation of the Java layer that interacts between Android and the Unity engine.
As shown in the figure below, unity exports unityLibrary/libs/unity-classes.jarthe files in the gradle project:
insert image description here

This unity-classes.jar not only contains the basic Unity3d rendering bridge code, but also contains the implementation of the input box function in Unity on the Android platform.

The path of unity-classes.jar is in the unity installation directory, for example, this path is for mac:
Unity/Mac/AndroidPlayer/Variations/mono/Release/Classes/classes.jar

Starting from the unity3d 2019.3 version, unity-classes.jar UnityPlayerActivity.classdeletes this class, moves it out and gives it in the form of java source code. The default entry Activity of the exported gradle project is UnityPlayerActivity.

The path of the UnityPlayerActivity.java file (take mac as an example):Unity/Mac/AndroidPlayer/Source/com/unity3d/player/UnityPlayerActivity.java

The source code of UnityPlayerActivity.java is as follows:

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

It can be seen that the source code of this class is not much, in fact, the UnityPlayer class is used as the Activity view. So Unity's rendering bridge code on Android is basically in the UnityPlayer class.

Since the code of UnityPlayer.java is relatively long, only part of the code is taken:

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 is a custom View that inherits FrameLayout, which uses SurfaceView to render the game page through the underlying graphics API.

3. Implementation mechanism of Unity3d input box

The detailed rendering process will not be described here. This article mainly sorts out the implementation of the Unity input box.

First look at the directory structure of unity-classes.jar:
insert image description here
it can be seen that the code has been confused and it is not easy to read.

But this does not affect us to find the code of the input box, because there are not many codes in this jar.

In UnityPlayerthis class there is a member variable:
k mSoftInputDialog;

Judging from the name, it is related to the input box. Then we opened kthis class and found that it was the implementation of the input box.

kThe decompiled code of this class:

//
// 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;
            }
        }
    }
}

This is the code decompiled by AS for us, so it is not the original java, but it does not affect the reading.

Unity's input box is actually a custom Dialog.

The UI of this Dialog is dynamically created through code, and the UI creation code is in
protected final View createSoftInputView()this method, see the source code above.

We focus on kthe constructor of this class.

The general process of the construction method:
1. Call createSoftInputView()the method to create the UI of the input box and return the View
2. Call the setContentView method of the Dialog (pass in the above View) to set the UI for the Dialog
3. Get the EditText and Button in the above View and configure them as defaults Attributes.
4. Monitor the pop-up and close of the soft keyboard, and report the event to Unity.
5. Call the EditText requestFocusmethod to get the focus. This step is very important, which is equivalent to actively popping up the soft keyboard.

The instance of the class k is a member variable of UnityPlayer mSoftInputDialog, which is showSoftInputinitialized in the method.

mSoftInputDialogThe initialization code is in showSoftInputthe method:

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

showSoftInputThe method is not called by the Java layer, but by Unity, that is, in the so file.

Here you can roughly guess the implementation mechanism of the Unity input control.

In fact, there is an input control in Unity InputField, which is essentially a display component. It cannot input content or get focus, which is far from Android's EditTex. It can be compared to Android's TextView control. Therefore, if Unity wants to accept user input, it can only call Android's EditText.

Unity's InputField can receive input, and the implementation process is: the player clicks on the InputField control to indicate that he wants to input content, and then Unity gets the click event, calls the method of the Java layer, and a showSoftInputcustom showSoftInputDialog will pop up. There is an EditText in this Dialog, and then call The requestFocus method of EditText pops up the soft keyboard of the system. Then monitor the content change of EditText, send the text back to Unity, and Unity will display the text on the InputField. This is the implementation process of Unity's entire input function.

4. How to transform the input box of Unity3d

Let's go back to kthe class of input box Dialog code.

The method of this class createSoftInputView()is to create the input box UI. The general process is: first create a RelativeLayout, then create an EditText and add it to the RelativeLayout, and add a Button to the right of the EditText. The whole UI is very simple. The ids of EditText and Button controls are hard-coded.

In many games, it is found that they have modified Unity's default input box, such as placing EditText on the top of the screen, modifying the EditText style, limiting the maximum number of input lines, etc., or modifying some properties of Button, in short, let the input The frame is more user-friendly or personalized.

If there is no source code, it can only be modified by inheritance or reflection. Because the input box Dialog is a final class and the ui creation method is also final, so there is no way to inherit. The only way of thinking is to use reflection.

As long as you can get the instance of the input box Dialog, you can get the EditTextt and Button of the input box, and then you can modify them.

Then, the instance of the input box Dialog is a member variable of UnityPlayer, and the instance of UnityPlayer is a member variable of UnityPlayerActivity, and is initialized in the onCreate function. Therefore, the entry point is to obtain the UnityPlayer instance in the onCreate of the Activity, and then follow the vines to obtain the control of the input box and modify it using reflection.

Then the problem is that the input box Dialog is not created when the game starts, so the reflected code is not executed in the onCreate of the Activity, but waits until the player clicks the Unity input control, or the InputFieldsoft keyboard pops up. Because the instance of the input box Dialog cannot be obtained immediately in onCreate.

After the analysis of the implementation process of the Unity input box above, we know that as long as the Java layer listens to the pop-up of the soft keyboard, it means that the member variables of the UnityPlayer have been mSoftInputDialoginitialized.

Therefore, in the onCreate method of Activity, we first listen to the pop-up of the soft keyboard, and put our reflected code into the callback of the pop-up of the soft keyboard.

So how to monitor the pop-up of the floppy disk? In fact, Android does not provide an api for monitoring floppy disks, but iOS provides notifications for monitoring. (Here I want to complain about Android, which is not very friendly to developers)

Listen to the api of the soft keyboard. In fact, this class in unity-classes.jar khas given an example. It is in the construction method of 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);
                }

            }
        });

Roughly, it is to monitor the change of the view. k.this.bIt is the UnityPlayer object, and UnityPlayer is the view of the entire game, which is a FrameLayout.

The simple point is as follows, the realization idea is the same:

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

I won’t talk about how to reflect and modify the input box here, it’s a relatively basic Java syntax.

Another question is how to add code in the onCreate lifecycle method of UnityPlayerActivity?

Regarding this issue, there are similar questions:
How to add code in UnityPlayerActivity's lifecycle onResume or other lifecycle methods?
How to add permission processing code in the permission callback method of UnityPlayerActivity onRequestPermissionsResult?

These are actually the same kind of problems. There are many ways:

1. You can directly modify its source code in unity. The advantage is that you don’t need to modify it every time you export the gradle project. The downside is that it affects other projects.
2. Export the gradle project and manually modify UnityPlayerActivity. Obviously not desirable.
3. Through Unity's packaged post-processing script, replace the UnityPlayerActivity with the same name with the modified class.
4. Inherit UnityPlayerActivity and configure the new entry Activity in AndroidManifest.
5. Monitor the life cycle of UnityPlayerActivity. The disadvantage is that it is difficult to distinguish when the game has multiple activities.

The above methods actually have a strong dependence on UnityPlayerActivity. Is there a way that does not depend on UnityPlayerActivity?

In fact, unity-classes.jarthe answer is also given in the source code.

The idea is to obtain UnityPlayerActivity through UnityPlayer static properties currentActivity, then add a custom Fragment to the Activity, and implement the functions we need in this Fragment life cycle method.

Take a look at the source code of this class in 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();
            }

        }
    }
}

You can see that aa Fragment is added to the method, and this Fragment is ithis class.

iThe decompiled code of this class:

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

You can see that it has added permission-related code in the method of onCreateand .onRequestPermissionsResult

See here to know how to apply for permissions in Unity.

Isn't it ingenious? It neither directly modifies the code of the Activity nor inherits from the Activity nor monitors the life cycle of the Activity, so it realizes adding code in the Activity life cycle function. In fact, many Android three-party frameworks use this idea, such as Glide, LeakCannary, etc.

OK, this article mainly combines the source code of unity-classes.jar to explain the implementation mechanism of the input box of Unity3d games on the Android platform, and also explains some other important knowledge in unity-classes.jar.

If there are any deficiencies, please leave a message to correct me~

Guess you like

Origin blog.csdn.net/devnn/article/details/130541829