From c0d616e6ef797571709f7a53651cfcf65563418f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C5=8Dan?= Date: Fri, 10 Apr 2026 02:01:26 +0000 Subject: [PATCH] fix(android): resolve black screen and improve Unity player reliability Replace Thread.sleep(1000) on UI thread with Handler.postDelayed to prevent ANR and rendering failures. Fix _isUnityReady flag being set when player creation fails. Fix broken setZ reflection (missing float parameter) that prevented background Z-ordering. Add null-safety for requestFrame() results. Fix pauseUnity ignoring the pause parameter. Replace silent catch blocks with Log statements for debuggability. Remove hardcoded Google Maps API key from example app. Closes #3 Co-Authored-By: Claude Opus 4.6 --- .../ReactNativeUnity.java | 96 ++++++++++++------- .../ReactNativeUnityView.java | 4 +- .../ReactNativeUnityViewManager.java | 19 +++- .../com/azesmwayreactnativeunity/UPlayer.java | 18 ++-- .../android/app/src/main/AndroidManifest.xml | 2 +- 5 files changed, 95 insertions(+), 44 deletions(-) diff --git a/android/src/main/java/com/azesmwayreactnativeunity/ReactNativeUnity.java b/android/src/main/java/com/azesmwayreactnativeunity/ReactNativeUnity.java index 95adcbb..cd6b36a 100644 --- a/android/src/main/java/com/azesmwayreactnativeunity/ReactNativeUnity.java +++ b/android/src/main/java/com/azesmwayreactnativeunity/ReactNativeUnity.java @@ -3,6 +3,9 @@ import android.app.Activity; import android.graphics.PixelFormat; import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; import android.view.ViewGroup; import android.view.WindowManager; @@ -11,6 +14,8 @@ import java.lang.reflect.InvocationTargetException; public class ReactNativeUnity { + private static final String TAG = "ReactNativeUnity"; + private static UPlayer unityPlayer; public static boolean _isUnityReady; public static boolean _isUnityPaused; @@ -51,36 +56,51 @@ public void run() { try { unityPlayer = new UPlayer(activity, callback); - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | InvocationTargetException e) {} - - try { - // wait a moment. fix unity cannot start when startup. - Thread.sleep(1000); - } catch (Exception e) {} - - // start unity - try { - addUnityViewToBackground(); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {} - - unityPlayer.windowFocusChanged(true); - - try { - unityPlayer.requestFocusPlayer(); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {} - - unityPlayer.resume(); - - if (!fullScreen) { - activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); - activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | InvocationTargetException e) { + Log.e(TAG, "Failed to create Unity player", e); } - _isUnityReady = true; + if (unityPlayer == null) { + Log.e(TAG, "Unity player was not created — aborting initialization"); + return; + } - try { - callback.onReady(); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {} + // Defer post-init work to avoid blocking the UI thread. + // The old Thread.sleep(1000) caused ANR risk and rendering issues. + final boolean wasFullScreen = fullScreen; + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + try { + addUnityViewToBackground(); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Log.e(TAG, "Failed to add Unity view to background", e); + } + + unityPlayer.windowFocusChanged(true); + + try { + unityPlayer.requestFocusPlayer(); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + Log.w(TAG, "Failed to request focus on Unity player", e); + } + + unityPlayer.resume(); + + if (!wasFullScreen) { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + + _isUnityReady = true; + + try { + callback.onReady(); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Log.e(TAG, "Callback onReady failed", e); + } + } + }, 1000); } }); } @@ -112,6 +132,12 @@ public static void addUnityViewToBackground() throws InvocationTargetException, return; } + android.widget.FrameLayout frame = unityPlayer.requestFrame(); + if (frame == null) { + Log.w(TAG, "requestFrame() returned null — cannot add Unity view to background"); + return; + } + if (unityPlayer.getParentPlayer() != null) { // NOTE: If we're being detached as part of the transition, make sure // to explicitly finish the transition first, as it might still keep @@ -119,8 +145,8 @@ public static void addUnityViewToBackground() throws InvocationTargetException, // prevents a crash on an `addContentView()` later on. // Otherwise, if there's no transition, it's a no-op. // See https://stackoverflow.com/a/58247331 - ((ViewGroup) unityPlayer.getParentPlayer()).endViewTransition(unityPlayer.requestFrame()); - ((ViewGroup) unityPlayer.getParentPlayer()).removeView(unityPlayer.requestFrame()); + ((ViewGroup) unityPlayer.getParentPlayer()).endViewTransition(frame); + ((ViewGroup) unityPlayer.getParentPlayer()).removeView(frame); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { @@ -129,7 +155,7 @@ public static void addUnityViewToBackground() throws InvocationTargetException, final Activity activity = ((Activity) unityPlayer.getContextPlayer()); ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(1, 1); - activity.addContentView(unityPlayer.requestFrame(), layoutParams); + activity.addContentView(frame, layoutParams); } public static void addUnityViewToGroup(ViewGroup group) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { @@ -137,12 +163,18 @@ public static void addUnityViewToGroup(ViewGroup group) throws NoSuchMethodExcep return; } + android.widget.FrameLayout frame = unityPlayer.requestFrame(); + if (frame == null) { + Log.w(TAG, "requestFrame() returned null — cannot add Unity view to group"); + return; + } + if (unityPlayer.getParentPlayer() != null) { - ((ViewGroup) unityPlayer.getParentPlayer()).removeView(unityPlayer.requestFrame()); + ((ViewGroup) unityPlayer.getParentPlayer()).removeView(frame); } ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT); - group.addView(unityPlayer.requestFrame(), 0, layoutParams); + group.addView(frame, 0, layoutParams); unityPlayer.windowFocusChanged(true); unityPlayer.requestFocusPlayer(); unityPlayer.resume(); diff --git a/android/src/main/java/com/azesmwayreactnativeunity/ReactNativeUnityView.java b/android/src/main/java/com/azesmwayreactnativeunity/ReactNativeUnityView.java index 60cb1b1..9b0b17e 100644 --- a/android/src/main/java/com/azesmwayreactnativeunity/ReactNativeUnityView.java +++ b/android/src/main/java/com/azesmwayreactnativeunity/ReactNativeUnityView.java @@ -6,12 +6,14 @@ import android.annotation.SuppressLint; import android.content.res.Configuration; +import android.util.Log; import android.widget.FrameLayout; import java.lang.reflect.InvocationTargetException; @SuppressLint("ViewConstructor") public class ReactNativeUnityView extends FrameLayout { + private static final String TAG = "ReactNativeUnityView"; private UPlayer view; public boolean keepPlayerMounted = false; @@ -62,7 +64,7 @@ protected void onDetachedFromWindow() { try { addUnityViewToBackground(); } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) { - throw new RuntimeException(e); + Log.e(TAG, "Failed to move Unity view to background on detach", e); } } diff --git a/android/src/main/java/com/azesmwayreactnativeunity/ReactNativeUnityViewManager.java b/android/src/main/java/com/azesmwayreactnativeunity/ReactNativeUnityViewManager.java index b7f6310..eacd92f 100644 --- a/android/src/main/java/com/azesmwayreactnativeunity/ReactNativeUnityViewManager.java +++ b/android/src/main/java/com/azesmwayreactnativeunity/ReactNativeUnityViewManager.java @@ -3,6 +3,8 @@ import static com.azesmwayreactnativeunity.ReactNativeUnity.*; import android.os.Handler; +import android.os.Looper; +import android.util.Log; import android.view.View; import androidx.annotation.NonNull; @@ -26,6 +28,7 @@ @ReactModule(name = ReactNativeUnityViewManager.NAME) public class ReactNativeUnityViewManager extends ReactNativeUnityViewManagerSpec implements LifecycleEventListener, View.OnAttachStateChangeListener { + private static final String TAG = "RNUnityViewManager"; ReactApplicationContext context; static ReactNativeUnityView view; public static final String NAME = "RNUnityView"; @@ -51,7 +54,9 @@ public ReactNativeUnityView createViewInstance(@NonNull ThemedReactContext conte if (getPlayer() != null) { try { view.setUnityPlayer(getPlayer()); - } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {} + } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) { + Log.e(TAG, "Failed to set existing Unity player on view", e); + } } else { try { createPlayer(context.getCurrentActivity(), new UnityPlayerCallback() { @@ -76,7 +81,9 @@ public void onQuit() { reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(view.getId(), "onPlayerQuit", data); } }); - } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {} + } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) { + Log.e(TAG, "Failed to create Unity player", e); + } } return view; @@ -140,7 +147,11 @@ public void unloadUnity(ReactNativeUnityView view) { public void pauseUnity(ReactNativeUnityView view, boolean pause) { if (isUnityReady()) { assert getPlayer() != null; - getPlayer().pause(); + if (pause) { + getPlayer().pause(); + } else { + getPlayer().resume(); + } } } @@ -201,7 +212,7 @@ public void onHostDestroy() { private void restoreUnityUserState() { // restore the unity player state if (isUnityPaused()) { - Handler handler = new Handler(); + Handler handler = new Handler(Looper.getMainLooper()); handler.postDelayed(new Runnable() { @Override public void run() { diff --git a/android/src/main/java/com/azesmwayreactnativeunity/UPlayer.java b/android/src/main/java/com/azesmwayreactnativeunity/UPlayer.java index f206197..43fba3d 100644 --- a/android/src/main/java/com/azesmwayreactnativeunity/UPlayer.java +++ b/android/src/main/java/com/azesmwayreactnativeunity/UPlayer.java @@ -2,6 +2,7 @@ import android.app.Activity; import android.content.res.Configuration; +import android.util.Log; import android.widget.FrameLayout; import com.unity3d.player.*; @@ -11,6 +12,7 @@ import java.lang.reflect.Method; public class UPlayer { + private static final String TAG = "UPlayer"; private static UnityPlayer unityPlayer; public UPlayer(final Activity activity, final ReactNativeUnity.UnityPlayerCallback callback) throws ClassNotFoundException, InvocationTargetException, IllegalAccessException, InstantiationException { @@ -91,7 +93,7 @@ public void requestFocusPlayer() throws NoSuchMethodException, InvocationTargetE } } - public FrameLayout requestFrame() throws NoSuchMethodException { + public FrameLayout requestFrame() { try { Method getFrameLayout = unityPlayer.getClass().getMethod("getFrameLayout"); @@ -101,17 +103,21 @@ public FrameLayout requestFrame() throws NoSuchMethodException { if (FrameLayout.class.isInstance(unityPlayer)) { return FrameLayout.class.cast(unityPlayer); } else { + Log.w(TAG, "UnityPlayer is not a FrameLayout and has no getFrameLayout() — requestFrame() returning null"); return null; } } } - public void setZ(float v) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + public void setZ(float v) { try { - Method setZ = unityPlayer.getClass().getMethod("setZ"); - - setZ.invoke(unityPlayer, v); - } catch (NoSuchMethodException e) {} + FrameLayout frame = this.requestFrame(); + if (frame != null) { + frame.setZ(v); + } + } catch (Exception e) { + Log.w(TAG, "Failed to set Z on Unity player", e); + } } public Object getContextPlayer() { diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 977f0da..3eac082 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -71,7 +71,7 @@ + android:value="YOUR_GOOGLE_MAPS_API_KEY" />