diff --git a/android/build.gradle b/android/build.gradle index 359f843..a79a3c1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -97,20 +97,20 @@ dependencies { // For < 0.71, this will be from the local maven repo // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin - implementation 'com.google.code.gson:gson:2.11.0' + implementation 'com.google.code.gson:gson:2.13.1' //noinspection GradleDynamicVersion implementation "com.facebook.react:react-android:+" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" // Kotlin lang - implementation 'androidx.core:core-ktx:1.8.0' + implementation 'androidx.core:core-ktx:1.12.0' // App compat and UI things - implementation 'androidx.appcompat:appcompat:1.5.1' - implementation 'com.google.android.material:material:1.7.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.fragment:fragment-ktx:1.5.4' + implementation 'androidx.fragment:fragment-ktx:1.6.2' // Navigation library def nav_version = "2.5.3" @@ -118,7 +118,7 @@ dependencies { implementation "androidx.navigation:navigation-ui-ktx:$nav_version" // CameraX core library - def camerax_version = '1.2.0-alpha02' + def camerax_version = '1.2.3' implementation "androidx.camera:camera-core:$camerax_version" // CameraX Camera2 extensions @@ -131,16 +131,16 @@ dependencies { implementation "androidx.camera:camera-view:$camerax_version" // WindowManager - implementation 'androidx.window:window:1.1.0-alpha03' + implementation 'androidx.window:window:1.1.0' // Unit testing testImplementation 'junit:junit:4.13.2' // Instrumented testing - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' // MediaPipe Library - implementation 'com.google.mediapipe:tasks-vision:0.20230731' -} + implementation 'com.google.mediapipe:tasks-vision:0.10.15' +} \ No newline at end of file diff --git a/android/src/main/java/com/tsmediapipe/TsMediapipeViewManager.java b/android/src/main/java/com/tsmediapipe/TsMediapipeViewManager.java index 75dc4ad..1bac227 100644 --- a/android/src/main/java/com/tsmediapipe/TsMediapipeViewManager.java +++ b/android/src/main/java/com/tsmediapipe/TsMediapipeViewManager.java @@ -9,6 +9,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableArray; @@ -22,11 +24,12 @@ public class TsMediapipeViewManager extends ViewGroupManager { - public static final String REACT_CLASS = "TsMediapipeViewManager"; public final int COMMAND_CREATE = 1; private int propWidth; private int propHeight; + private Choreographer.FrameCallback frameCallback; + private CameraFragment currentFragment; ReactApplicationContext reactContext; @@ -41,14 +44,134 @@ public String getName() { private OverlayView overlayView; + + private void createFragmentAuto(FrameLayout root) { + Log.d("TsMediapipe", "Auto-creating fragment - START"); + + final FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity(); + if (activity == null) { + Log.e("TsMediapipe", "Activity is null, cannot create fragment"); + return; + } + + // Clean up existing fragment if any + cleanupFragment(); + + try { + Log.d("TsMediapipe", "Creating new CameraFragment instance"); + currentFragment = new CameraFragment(); + final FragmentManager fragmentManager = activity.getSupportFragmentManager(); + + int containerId = root.getId(); + if (containerId == View.NO_ID) { + containerId = View.generateViewId(); + root.setId(containerId); + Log.d("TsMediapipe", "Generated new container ID: " + containerId); + } else { + Log.d("TsMediapipe", "Using existing container ID: " + containerId); + } + + final int finalContainerId = containerId; // Make final for lambda + final CameraFragment finalFragment = currentFragment; // Make final for lambda + + + if (root.getParent() == null) { + Log.w("TsMediapipe", "Container has no parent - this might cause issues"); + } + + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + try { + Log.d("TsMediapipe", "Starting fragment transaction"); + + fragmentManager + .beginTransaction() + .replace(finalContainerId, finalFragment, "camera_fragment_" + finalContainerId) + .commitAllowingStateLoss(); + + Log.d("TsMediapipe", "Fragment transaction committed"); + + boolean executed = fragmentManager.executePendingTransactions(); + Log.d("TsMediapipe", "Pending transactions executed: " + executed); + + Fragment addedFragment = fragmentManager.findFragmentByTag("camera_fragment_" + finalContainerId); + if (addedFragment != null) { + Log.d("TsMediapipe", "Fragment successfully added to manager"); + } else { + Log.e("TsMediapipe", "Fragment not found in manager after transaction!"); + } + + } catch (Exception e) { + Log.e("TsMediapipe", "Error in fragment transaction: " + e.getMessage(), e); + } + } + }); + + } catch (Exception e) { + Log.e("TsMediapipe", "Error auto-creating fragment: " + e.getMessage(), e); + } + } + /** * Return a FrameLayout which will later hold the Fragment */ @Override public FrameLayout createViewInstance(ThemedReactContext reactContext) { - Log.d("hello", "createViewInstance"); + Log.d("TsMediapipe", "createViewInstance"); + FrameLayout frameLayout = new FrameLayout(reactContext); + frameLayout.setId(View.generateViewId()); + + + return frameLayout; + } + + + private void createCameraViewDirectly(FrameLayout container, ThemedReactContext context) { + try { + Log.d("TsMediapipe", "Camera view created directly"); + } catch (Exception e) { + Log.e("TsMediapipe", "Error creating camera view directly: " + e.getMessage(), e); + } + } - return new FrameLayout(reactContext); + + @Override + public void onAfterUpdateTransaction(@NonNull FrameLayout view) { + super.onAfterUpdateTransaction(view); + setupLayout(view); + + if (currentFragment == null && propWidth > 0 && propHeight > 0) { + view.post(new Runnable() { + @Override + public void run() { + if (view.getParent() != null) { + createFragmentAuto(view); + } else { + Log.w("TsMediapipe", "View not yet attached to parent, retrying..."); + view.postDelayed(new Runnable() { + @Override + public void run() { + createFragmentAuto(view); + } + }, 100); + } + } + }); + } + } + + + @Override + public void onDropViewInstance(@NonNull FrameLayout view) { + Log.d("TsMediapipe", "onDropViewInstance"); + if (frameCallback != null) { + Choreographer.getInstance().removeFrameCallback(frameCallback); + frameCallback = null; + } + + cleanupFragment(); + super.onDropViewInstance(view); } /** @@ -65,12 +188,17 @@ public Map getCommandsMap() { */ @Override public void receiveCommand( - @NonNull FrameLayout root, - String commandId, - @Nullable ReadableArray args + @NonNull FrameLayout root, + String commandId, + @Nullable ReadableArray args ) { - super.receiveCommand(root, commandId, args); + + if (args == null || args.size() == 0) { + Log.e("TsMediapipe", "receiveCommand: args is null or empty"); + return; + } + int reactNativeViewId = args.getInt(0); int commandIdInt = Integer.parseInt(commandId); @@ -78,19 +206,25 @@ public void receiveCommand( case COMMAND_CREATE: createFragment(root, reactNativeViewId); break; - default: { - } + default: + Log.w("TsMediapipe", "Received unknown command: " + commandIdInt); } } @ReactProp(name = "width") public void setWidthProp(FrameLayout view, Integer width) { - propWidth = width; + if (width != null && width > 0) { + propWidth = width; + Log.d("TsMediapipe", "Width set to: " + width); + } } @ReactProp(name = "height") public void setHeightProp(FrameLayout view, Integer height) { - propHeight = height; + if (height != null && height > 0) { + propHeight = height; + Log.d("TsMediapipe", "Height set to: " + height); + } } @ReactProp(name = "face") @@ -143,57 +277,123 @@ public void setRightAnkleProp(View view, boolean rightAnkle) { GlobalState.isRightAnkleEnabled = rightAnkle; } - /** * Replace your React Native view with a custom fragment */ public void createFragment(FrameLayout root, int reactNativeViewId) { - ViewGroup parentView = (ViewGroup) root.findViewById(reactNativeViewId);//reactNativeViewId - setupLayout(parentView); + Log.d("TsMediapipe", "createFragment called with viewId: " + reactNativeViewId); + + final FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity(); + if (activity == null) { + Log.e("TsMediapipe", "Activity is null, cannot create fragment"); + return; + } + + // Clean up existing fragment if any + cleanupFragment(); + + try { + currentFragment = new CameraFragment(); + final FragmentManager fragmentManager = activity.getSupportFragmentManager(); + + // Use the FrameLayout's ID instead of reactNativeViewId + int containerId = root.getId(); + if (containerId == View.NO_ID) { + containerId = View.generateViewId(); + root.setId(containerId); + } + + final int finalContainerId = containerId; + final CameraFragment finalFragment = currentFragment; + + Log.d("TsMediapipe", "Adding fragment to container: " + containerId); + + fragmentManager + .beginTransaction() + .replace(finalContainerId, finalFragment, "camera_fragment_" + finalContainerId) + .commitNowAllowingStateLoss(); // Use commitNow to ensure immediate execution - final CameraFragment myFragment = new CameraFragment(); - FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity(); - activity.getSupportFragmentManager() - .beginTransaction() - .replace(reactNativeViewId, myFragment, String.valueOf(reactNativeViewId)) - .commit(); + Log.d("TsMediapipe", "Fragment transaction completed"); + + } catch (Exception e) { + Log.e("TsMediapipe", "Error creating fragment: " + e.getMessage(), e); + } + } + + private void cleanupFragment() { + if (currentFragment != null) { + final FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity(); + if (activity != null) { + try { + final FragmentManager fragmentManager = activity.getSupportFragmentManager(); + final Fragment existingFragment = fragmentManager.findFragmentByTag("camera_fragment_" + currentFragment.getId()); + if (existingFragment != null) { + fragmentManager.beginTransaction() + .remove(existingFragment) + .commitNowAllowingStateLoss(); + } + } catch (Exception e) { + Log.e("TsMediapipe", "Error cleaning up fragment: " + e.getMessage()); + } + } + currentFragment = null; + } } public void setupLayout(View view) { - Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { + if (frameCallback != null) { + Choreographer.getInstance().removeFrameCallback(frameCallback); + } + + frameCallback = new Choreographer.FrameCallback() { @Override public void doFrame(long frameTimeNanos) { - manuallyLayoutChildren(view); - view.getViewTreeObserver().dispatchOnGlobalLayout(); - Choreographer.getInstance().postFrameCallback(this); + try { + manuallyLayoutChildren(view); + view.getViewTreeObserver().dispatchOnGlobalLayout(); + Choreographer.getInstance().postFrameCallback(this); + } catch (Exception e) { + Log.e("TsMediapipe", "Error in frame callback: " + e.getMessage()); + } } - }); + }; + + Choreographer.getInstance().postFrameCallback(frameCallback); } /** * Layout all children properly */ public void manuallyLayoutChildren(View view) { - // propWidth and propHeight coming from react-native props - int width = propWidth; - int height = propHeight; - View parent = (View) view.getParent(); - if (parent == null) { - return; // Ensure the parent is not null - } - view.measure( - View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY) - ); + try { + // Use default dimensions if props are not set + int width = propWidth > 0 ? propWidth : ViewGroup.LayoutParams.MATCH_PARENT; + int height = propHeight > 0 ? propHeight : ViewGroup.LayoutParams.MATCH_PARENT; + + View parent = (View) view.getParent(); + if (parent == null) { + return; // Ensure the parent is not null + } -// Get parent's dimensions - int parentWidth = parent.getWidth(); - int parentHeight = parent.getHeight(); + // Only measure if the view needs it + if (view.isLayoutRequested()) { + view.measure( + View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY) + ); + } - // Calculate the top-left coordinates to center the view - int x = 0; - int y = 0; + // Get parent's dimensions + int parentWidth = parent.getWidth(); + int parentHeight = parent.getHeight(); - view.layout(x, y, parentWidth, parentHeight); + // Only layout if parent has valid dimensions + if (parentWidth > 0 && parentHeight > 0) { + view.layout(0, 0, parentWidth, parentHeight); + } + + } catch (Exception e) { + Log.e("TsMediapipe", "Error in manuallyLayoutChildren: " + e.getMessage()); + } } -} +} \ No newline at end of file diff --git a/example/src/App.tsx b/example/src/App.tsx index 5560ae5..a49b9c0 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -10,6 +10,8 @@ import { import { RNMediapipe, switchCamera } from '@thinksys/react-native-mediapipe'; export default function App() { + const { width, height } = Dimensions.get('window'); + const onFlip = () => { switchCamera(); }; @@ -22,8 +24,8 @@ export default function App() {