@capgo/camera-preview 7.4.0-beta.15 → 7.4.0-beta.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -725,14 +725,15 @@ Represents EXIF data extracted from an image.
725
725
 
726
726
  Defines the options for capturing a picture.
727
727
 
728
- | Prop | Type | Description | Default | Since |
729
- | ---------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- | ----- |
730
- | **`height`** | <code>number</code> | The desired height of the picture in pixels. If not provided, the device default is used. | | |
731
- | **`width`** | <code>number</code> | The desired width of the picture in pixels. If not provided, the device default is used. | | |
732
- | **`quality`** | <code>number</code> | The quality of the captured image, from 0 to 100. Does not apply to `png` format. | <code>85</code> | |
733
- | **`format`** | <code><a href="#pictureformat">PictureFormat</a></code> | The format of the captured image. | <code>"jpeg"</code> | |
734
- | **`saveToGallery`** | <code>boolean</code> | If true, the captured image will be saved to the user's gallery. | <code>false</code> | 7.5.0 |
735
- | **`withExifLocation`** | <code>boolean</code> | If true, the plugin will attempt to add GPS location data to the image's EXIF metadata. This may prompt the user for location permissions. | <code>false</code> | 7.6.0 |
728
+ | Prop | Type | Description | Default | Since |
729
+ | ---------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----- |
730
+ | **`height`** | <code>number</code> | The desired height of the picture in pixels. If not provided, the device default is used. | | |
731
+ | **`width`** | <code>number</code> | The desired width of the picture in pixels. If not provided, the device default is used. | | |
732
+ | **`aspectRatio`** | <code>string</code> | The desired aspect ratio of the captured image (e.g., '4:3', '16:9'). If specified without width/height, captures the largest possible image with this ratio. Cannot be used together with width or height - the capture will be rejected with an error. | | 7.7.0 |
733
+ | **`quality`** | <code>number</code> | The quality of the captured image, from 0 to 100. Does not apply to `png` format. | <code>85</code> | |
734
+ | **`format`** | <code><a href="#pictureformat">PictureFormat</a></code> | The format of the captured image. | <code>"jpeg"</code> | |
735
+ | **`saveToGallery`** | <code>boolean</code> | If true, the captured image will be saved to the user's gallery. | <code>false</code> | 7.5.0 |
736
+ | **`withExifLocation`** | <code>boolean</code> | If true, the plugin will attempt to add GPS location data to the image's EXIF metadata. This may prompt the user for location permissions. | <code>false</code> | 7.6.0 |
736
737
 
737
738
 
738
739
  #### CameraSampleOptions
@@ -9,6 +9,7 @@ import android.location.Location;
9
9
  import android.util.DisplayMetrics;
10
10
  import android.util.Log;
11
11
  import android.util.Size;
12
+ import android.view.View;
12
13
  import android.view.ViewGroup;
13
14
  import android.webkit.WebView;
14
15
  import com.ahm.capacitor.camera.preview.model.CameraDevice;
@@ -171,8 +172,9 @@ public class CameraPreview
171
172
  final boolean saveToGallery = call.getBoolean("saveToGallery", false);
172
173
  Integer width = call.getInt("width");
173
174
  Integer height = call.getInt("height");
175
+ String aspectRatio = call.getString("aspectRatio");
174
176
 
175
- cameraXView.capturePhoto(quality, saveToGallery, width, height, location);
177
+ cameraXView.capturePhoto(quality, saveToGallery, width, height, aspectRatio, location);
176
178
  }
177
179
 
178
180
  @PluginMethod
@@ -574,6 +576,41 @@ public class CameraPreview
574
576
 
575
577
  // Calculate pixel ratio
576
578
  float pixelRatio = metrics.density;
579
+
580
+ // The key insight: JavaScript coordinates are relative to the WebView's viewport
581
+ // If the WebView is positioned below the status bar (webViewLocationOnScreen[1] > 0),
582
+ // we need to add that offset when placing native views
583
+ int webViewTopInset = webViewLocationOnScreen[1];
584
+ boolean isEdgeToEdgeActive = webViewLocationOnScreen[1] > 0;
585
+
586
+ // Log all the positioning information for debugging
587
+ Log.d("CameraPreview", "WebView Position Debug:");
588
+ Log.d("CameraPreview", " - webView.getTop(): " + webViewTop);
589
+ Log.d("CameraPreview", " - webView.getLeft(): " + webViewLeft);
590
+ Log.d("CameraPreview", " - webView locationInWindow: (" + webViewLocationInWindow[0] + ", " + webViewLocationInWindow[1] + ")");
591
+ Log.d("CameraPreview", " - webView locationOnScreen: (" + webViewLocationOnScreen[0] + ", " + webViewLocationOnScreen[1] + ")");
592
+ Log.d("CameraPreview", " - parent locationInWindow: (" + parentLocationInWindow[0] + ", " + parentLocationInWindow[1] + ")");
593
+ Log.d("CameraPreview", " - parent locationOnScreen: (" + parentLocationOnScreen[0] + ", " + parentLocationOnScreen[1] + ")");
594
+
595
+ // Check if WebView has margins
596
+ View webView = getBridge().getWebView();
597
+ ViewGroup.LayoutParams webViewLayoutParams = webView.getLayoutParams();
598
+ if (webViewLayoutParams instanceof ViewGroup.MarginLayoutParams) {
599
+ ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) webViewLayoutParams;
600
+ Log.d("CameraPreview", " - webView margins: left=" + marginParams.leftMargin +
601
+ ", top=" + marginParams.topMargin +
602
+ ", right=" + marginParams.rightMargin +
603
+ ", bottom=" + marginParams.bottomMargin);
604
+ }
605
+
606
+ // Check WebView padding
607
+ Log.d("CameraPreview", " - webView padding: left=" + webView.getPaddingLeft() +
608
+ ", top=" + webView.getPaddingTop() +
609
+ ", right=" + webView.getPaddingRight() +
610
+ ", bottom=" + webView.getPaddingBottom());
611
+
612
+ Log.d("CameraPreview", " - Using webViewTopInset: " + webViewTopInset);
613
+ Log.d("CameraPreview", " - isEdgeToEdgeActive: " + isEdgeToEdgeActive);
577
614
 
578
615
  // Calculate position - center if x or y is -1
579
616
  int computedX;
@@ -588,7 +625,13 @@ public class CameraPreview
588
625
  : getBridge().getWebView().getHeight();
589
626
  computedHeight -= (int) (paddingBottom * pixelRatio);
590
627
 
591
- Log.d("CameraPreview", "Positioning logic - x: " + x + ", y: " + y);
628
+ Log.d("CameraPreview", "========================");
629
+ Log.d("CameraPreview", "POSITIONING CALCULATIONS:");
630
+ Log.d("CameraPreview", "1. INPUT - x: " + x + ", y: " + y + ", width: " + width + ", height: " + height);
631
+ Log.d("CameraPreview", "2. PIXEL RATIO: " + pixelRatio);
632
+ Log.d("CameraPreview", "3. SCREEN - width: " + metrics.widthPixels + ", height: " + metrics.heightPixels);
633
+ Log.d("CameraPreview", "4. WEBVIEW - width: " + getBridge().getWebView().getWidth() + ", height: " + getBridge().getWebView().getHeight());
634
+ Log.d("CameraPreview", "5. COMPUTED DIMENSIONS - width: " + computedWidth + ", height: " + computedHeight);
592
635
 
593
636
  if (x == -1) {
594
637
  // Center horizontally
@@ -617,20 +660,51 @@ public class CameraPreview
617
660
  }
618
661
 
619
662
  if (y == -1) {
620
- // Center vertically using full screen height
621
- int screenHeight = metrics.heightPixels;
622
- computedY = (screenHeight - computedHeight) / 2;
623
- Log.d(
624
- "CameraPreview",
625
- "Centering vertically: screenHeight=" +
626
- screenHeight +
627
- ", computedHeight=" +
628
- computedHeight +
629
- ", computedY=" +
630
- computedY
631
- );
663
+ // Center vertically
664
+ if (isEdgeToEdgeActive) {
665
+ // When WebView is offset from top, center within the available space
666
+ // The camera should be centered in the full screen, not just the WebView area
667
+ computedY = (metrics.heightPixels - computedHeight) / 2;
668
+ Log.d(
669
+ "CameraPreview",
670
+ "Centering vertically with WebView offset: screenHeight=" +
671
+ metrics.heightPixels +
672
+ ", webViewTop=" +
673
+ webViewTopInset +
674
+ ", computedHeight=" +
675
+ computedHeight +
676
+ ", computedY=" +
677
+ computedY
678
+ );
679
+ } else {
680
+ // Normal mode - use full screen height
681
+ computedY = (metrics.heightPixels - computedHeight) / 2;
682
+ Log.d(
683
+ "CameraPreview",
684
+ "Centering vertically (normal): screenHeight=" +
685
+ metrics.heightPixels +
686
+ ", computedHeight=" +
687
+ computedHeight +
688
+ ", computedY=" +
689
+ computedY
690
+ );
691
+ }
632
692
  } else {
633
693
  computedY = (int) (y * pixelRatio);
694
+ // If edge-to-edge is active, JavaScript Y is relative to WebView content area
695
+ // We need to add the inset to get absolute screen position
696
+ if (isEdgeToEdgeActive) {
697
+ computedY += webViewTopInset;
698
+ Log.d(
699
+ "CameraPreview",
700
+ "Edge-to-edge adjustment: Y position " +
701
+ (int)(y * pixelRatio) +
702
+ " + inset " +
703
+ webViewTopInset +
704
+ " = " +
705
+ computedY
706
+ );
707
+ }
634
708
  Log.d(
635
709
  "CameraPreview",
636
710
  "Using provided Y position: " +
@@ -638,10 +712,15 @@ public class CameraPreview
638
712
  " * " +
639
713
  pixelRatio +
640
714
  " = " +
641
- computedY
715
+ computedY +
716
+ (isEdgeToEdgeActive ? " (adjusted for edge-to-edge)" : "")
642
717
  );
643
718
  }
644
719
 
720
+ Log.d(
721
+ "CameraPreview",
722
+ "2b. EDGE-TO-EDGE - " + (isEdgeToEdgeActive ? "ACTIVE (inset=" + webViewTopInset + ")" : "INACTIVE")
723
+ );
645
724
  Log.d(
646
725
  "CameraPreview",
647
726
  "3. COMPUTED POSITION - x=" + computedX + ", y=" + computedY
@@ -776,18 +855,16 @@ public class CameraPreview
776
855
  .getDisplayMetrics();
777
856
  float pixelRatio = metrics.density;
778
857
 
779
- // Check if edge-to-edge mode is active by looking at WebView insets
780
- // If the WebView has a top margin, it means edge-to-edge is active
781
- // and JavaScript positions are relative to WebView content area
858
+ // When WebView is offset from the top (e.g., below status bar),
859
+ // we need to convert between JavaScript coordinates (relative to WebView)
860
+ // and native coordinates (relative to screen)
861
+ WebView webView = getBridge().getWebView();
782
862
  int webViewTopInset = 0;
783
863
  boolean isEdgeToEdgeActive = false;
784
- WebView webView = getBridge().getWebView();
785
- if (
786
- webView != null &&
787
- webView.getLayoutParams() instanceof ViewGroup.MarginLayoutParams
788
- ) {
789
- webViewTopInset =
790
- ((ViewGroup.MarginLayoutParams) webView.getLayoutParams()).topMargin;
864
+ if (webView != null) {
865
+ int[] location = new int[2];
866
+ webView.getLocationOnScreen(location);
867
+ webViewTopInset = location[1];
791
868
  isEdgeToEdgeActive = webViewTopInset > 0;
792
869
  }
793
870
 
@@ -972,8 +1049,24 @@ public class CameraPreview
972
1049
  .getDisplayMetrics();
973
1050
  float pixelRatio = metrics.density;
974
1051
 
1052
+ // Check if edge-to-edge mode is active
1053
+ WebView webView = getBridge().getWebView();
1054
+ int webViewTopInset = 0;
1055
+ boolean isEdgeToEdgeActive = false;
1056
+ if (webView != null) {
1057
+ int[] location = new int[2];
1058
+ webView.getLocationOnScreen(location);
1059
+ webViewTopInset = location[1];
1060
+ isEdgeToEdgeActive = webViewTopInset > 0;
1061
+ }
1062
+
975
1063
  int x = (xParam != null && xParam > 0) ? (int) (xParam * pixelRatio) : 0;
976
1064
  int y = (yParam != null && yParam > 0) ? (int) (yParam * pixelRatio) : 0;
1065
+
1066
+ // Add edge-to-edge inset to Y if active
1067
+ if (isEdgeToEdgeActive && y > 0) {
1068
+ y += webViewTopInset;
1069
+ }
977
1070
  int width = (widthParam != null && widthParam > 0)
978
1071
  ? (int) (widthParam * pixelRatio)
979
1072
  : 0;
@@ -360,6 +360,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
360
360
  Log.d(TAG, "setupPreviewView: Setting grid mode to: " + currentGridMode);
361
361
  gridOverlayView.setGridMode(currentGridMode);
362
362
  });
363
+
364
+ // Add a layout listener to update grid bounds when preview view changes size
365
+ previewView.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
366
+ if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
367
+ Log.d(TAG, "PreviewView layout changed, updating grid bounds");
368
+ updateGridOverlayBounds();
369
+ }
370
+ });
363
371
 
364
372
  ViewGroup parent = (ViewGroup) webView.getParent();
365
373
  if (parent != null) {
@@ -480,6 +488,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
480
488
 
481
489
  if (height != oldHeight) {
482
490
  int screenHeight = metrics.heightPixels;
491
+ // Always center based on full screen height
483
492
  y = (screenHeight - height) / 2;
484
493
  Log.d(
485
494
  TAG,
@@ -516,27 +525,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
516
525
  height
517
526
  );
518
527
 
519
- // Only add insets for positioning coordinates, not for full-screen sizes or centered content
520
- int webViewTopInset = getWebViewTopInset();
521
- int webViewLeftInset = getWebViewLeftInset();
522
-
523
- // Don't add insets if centered or full-screen
524
- if (sessionConfig.isCentered() || (x == 0 && y == 0)) {
525
- layoutParams.leftMargin = x;
526
- layoutParams.topMargin = y;
527
- Log.d(
528
- TAG,
529
- "calculatePreviewLayoutParams: Centered/Full-screen mode - keeping position without insets. isCentered=" +
530
- sessionConfig.isCentered()
531
- );
532
- } else {
533
- layoutParams.leftMargin = x + webViewLeftInset;
534
- layoutParams.topMargin = y + webViewTopInset;
535
- Log.d(
536
- TAG,
537
- "calculatePreviewLayoutParams: Positioned mode - applying insets"
538
- );
539
- }
528
+ // The X and Y positions passed from CameraPreview already include webView insets
529
+ // when edge-to-edge is active, so we don't need to add them again here
530
+ layoutParams.leftMargin = x;
531
+ layoutParams.topMargin = y;
540
532
 
541
533
  Log.d(
542
534
  TAG,
@@ -562,18 +554,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
562
554
  " height:" +
563
555
  height
564
556
  );
565
- Log.d(
566
- TAG,
567
- "calculatePreviewLayoutParams: Final margins - leftMargin:" +
568
- layoutParams.leftMargin +
569
- " topMargin:" +
570
- layoutParams.topMargin +
571
- " (WebView insets: left=" +
572
- webViewLeftInset +
573
- ", top=" +
574
- webViewTopInset +
575
- ")"
576
- );
577
557
  return layoutParams;
578
558
  }
579
559
 
@@ -766,6 +746,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
766
746
  actualHeight
767
747
  );
768
748
 
749
+ // Update grid overlay bounds after camera is started
750
+ updateGridOverlayBounds();
751
+
769
752
  listener.onCameraStarted(
770
753
  actualWidth,
771
754
  actualHeight,
@@ -852,9 +835,18 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
852
835
  final boolean saveToGallery,
853
836
  Integer width,
854
837
  Integer height,
838
+ String aspectRatio,
855
839
  Location location
856
840
  ) {
857
- Log.d(TAG, "capturePhoto: Starting photo capture with quality: " + quality);
841
+ Log.d(TAG, "capturePhoto: Starting photo capture with quality: " + quality + ", aspectRatio: " + aspectRatio);
842
+
843
+ // Check for conflicting parameters
844
+ if (aspectRatio != null && (width != null || height != null)) {
845
+ if (listener != null) {
846
+ listener.onPictureTakenError("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.");
847
+ }
848
+ return;
849
+ }
858
850
 
859
851
  if (imageCapture == null) {
860
852
  if (listener != null) {
@@ -902,7 +894,63 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
902
894
 
903
895
  JSONObject exifData = getExifData(exifInterface);
904
896
 
905
- if (width != null && height != null) {
897
+ // Handle aspect ratio if no width/height specified
898
+ if (width == null && height == null && aspectRatio != null && !aspectRatio.isEmpty()) {
899
+ // Get the original image dimensions
900
+ Bitmap originalBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
901
+ int originalWidth = originalBitmap.getWidth();
902
+ int originalHeight = originalBitmap.getHeight();
903
+
904
+ // Parse aspect ratio
905
+ String[] ratios = aspectRatio.split(":");
906
+ if (ratios.length == 2) {
907
+ try {
908
+ float widthRatio = Float.parseFloat(ratios[0]);
909
+ float heightRatio = Float.parseFloat(ratios[1]);
910
+
911
+ // For capture in portrait orientation, swap the aspect ratio (16:9 becomes 9:16)
912
+ boolean isPortrait = originalHeight > originalWidth;
913
+ float targetAspectRatio = isPortrait ? heightRatio / widthRatio : widthRatio / heightRatio;
914
+ float originalAspectRatio = (float) originalWidth / originalHeight;
915
+
916
+ int targetWidth, targetHeight;
917
+
918
+ if (originalAspectRatio > targetAspectRatio) {
919
+ // Original is wider than target - fit by height
920
+ targetHeight = originalHeight;
921
+ targetWidth = (int) (targetHeight * targetAspectRatio);
922
+ } else {
923
+ // Original is taller than target - fit by width
924
+ targetWidth = originalWidth;
925
+ targetHeight = (int) (targetWidth / targetAspectRatio);
926
+ }
927
+
928
+ // Center crop the image
929
+ int xOffset = (originalWidth - targetWidth) / 2;
930
+ int yOffset = (originalHeight - targetHeight) / 2;
931
+
932
+ Bitmap croppedBitmap = Bitmap.createBitmap(
933
+ originalBitmap,
934
+ xOffset,
935
+ yOffset,
936
+ targetWidth,
937
+ targetHeight
938
+ );
939
+
940
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
941
+ croppedBitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream);
942
+ bytes = stream.toByteArray();
943
+
944
+ // Write EXIF data back to cropped image
945
+ bytes = writeExifToImageBytes(bytes, exifInterface);
946
+
947
+ originalBitmap.recycle();
948
+ croppedBitmap.recycle();
949
+ } catch (NumberFormatException e) {
950
+ Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio, e);
951
+ }
952
+ }
953
+ } else if (width != null && height != null) {
906
954
  Bitmap bitmap = BitmapFactory.decodeByteArray(
907
955
  bytes,
908
956
  0,
@@ -1543,6 +1591,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1543
1591
  throw new Exception("Preview view not initialized");
1544
1592
  }
1545
1593
 
1594
+ // Validate that coordinates are within bounds (0-1 range)
1595
+ if (x < 0f || x > 1f || y < 0f || y > 1f) {
1596
+ Log.w(TAG, "setFocus: Coordinates out of bounds - x: " + x + ", y: " + y);
1597
+ throw new Exception("Focus coordinates must be between 0 and 1");
1598
+ }
1599
+
1546
1600
  // Cancel any ongoing focus operation
1547
1601
  if (currentFocusFuture != null && !currentFocusFuture.isDone()) {
1548
1602
  Log.d(TAG, "setFocus: Cancelling previous focus operation");
@@ -1551,16 +1605,18 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1551
1605
 
1552
1606
  int viewWidth = previewView.getWidth();
1553
1607
  int viewHeight = previewView.getHeight();
1554
- float indicatorX = x * viewWidth;
1555
- float indicatorY = y * viewHeight;
1556
- showFocusIndicator(indicatorX, indicatorY);
1557
-
1608
+
1558
1609
  if (viewWidth <= 0 || viewHeight <= 0) {
1559
1610
  throw new Exception(
1560
1611
  "Preview view has invalid dimensions: " + viewWidth + "x" + viewHeight
1561
1612
  );
1562
1613
  }
1563
1614
 
1615
+ // Only show focus indicator after validation passes
1616
+ float indicatorX = x * viewWidth;
1617
+ float indicatorY = y * viewHeight;
1618
+ showFocusIndicator(indicatorX, indicatorY);
1619
+
1564
1620
  // Create MeteringPoint using the preview view
1565
1621
  MeteringPointFactory factory = previewView.getMeteringPointFactory();
1566
1622
  MeteringPoint point = factory.createPoint(x * viewWidth, y * viewHeight);
@@ -2576,19 +2632,30 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2576
2632
 
2577
2633
  // Wait for camera rebinding to complete, then call callback
2578
2634
  if (callback != null) {
2579
- previewContainer.post(() -> previewContainer.post(callback));
2635
+ previewContainer.post(() -> {
2636
+ updateGridOverlayBounds();
2637
+ previewContainer.post(callback);
2638
+ });
2639
+ } else {
2640
+ previewContainer.post(() -> updateGridOverlayBounds());
2580
2641
  }
2581
2642
  } else {
2582
2643
  // No camera rebinding needed, wait for layout to complete then call callback
2583
- if (callback != null) {
2584
- previewContainer.post(callback);
2585
- }
2644
+ previewContainer.post(() -> {
2645
+ updateGridOverlayBounds();
2646
+ if (callback != null) {
2647
+ callback.run();
2648
+ }
2649
+ });
2586
2650
  }
2587
2651
  } else {
2588
2652
  // No sessionConfig, just wait for layout then call callback
2589
- if (callback != null) {
2590
- previewContainer.post(callback);
2591
- }
2653
+ previewContainer.post(() -> {
2654
+ updateGridOverlayBounds();
2655
+ if (callback != null) {
2656
+ callback.run();
2657
+ }
2658
+ });
2592
2659
  }
2593
2660
  } else {
2594
2661
  Log.w(
@@ -2727,6 +2794,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2727
2794
  finalY +
2728
2795
  ")"
2729
2796
  );
2797
+
2798
+ // Update grid overlay bounds after aspect ratio change
2799
+ previewContainer.post(() -> updateGridOverlayBounds());
2730
2800
  }
2731
2801
  } catch (NumberFormatException e) {
2732
2802
  Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio, e);
@@ -2736,10 +2806,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2736
2806
  private int getWebViewTopInset() {
2737
2807
  try {
2738
2808
  if (webView != null) {
2739
- ViewGroup.LayoutParams layoutParams = webView.getLayoutParams();
2740
- if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
2741
- return ((ViewGroup.MarginLayoutParams) layoutParams).topMargin;
2742
- }
2809
+ // Get the actual WebView position on screen
2810
+ int[] location = new int[2];
2811
+ webView.getLocationOnScreen(location);
2812
+ return location[1]; // Y position is the top inset
2743
2813
  }
2744
2814
  } catch (Exception e) {
2745
2815
  Log.w(TAG, "Failed to get WebView top inset", e);
@@ -2750,10 +2820,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2750
2820
  private int getWebViewLeftInset() {
2751
2821
  try {
2752
2822
  if (webView != null) {
2753
- ViewGroup.LayoutParams layoutParams = webView.getLayoutParams();
2754
- if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
2755
- return ((ViewGroup.MarginLayoutParams) layoutParams).leftMargin;
2756
- }
2823
+ // Get the actual WebView position on screen for consistency
2824
+ int[] location = new int[2];
2825
+ webView.getLocationOnScreen(location);
2826
+ return location[0]; // X position is the left inset
2757
2827
  }
2758
2828
  } catch (Exception e) {
2759
2829
  Log.w(TAG, "Failed to get WebView left inset", e);
@@ -2769,33 +2839,48 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2769
2839
  return new int[] { 0, 0, 0, 0 }; // x, y, width, height
2770
2840
  }
2771
2841
 
2772
- ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
2773
- int x = 0, y = 0, width = 0, height = 0;
2842
+ // Get actual camera preview bounds (accounts for letterboxing/pillarboxing)
2843
+ int actualX = getPreviewX();
2844
+ int actualY = getPreviewY();
2845
+ int actualWidth = getPreviewWidth();
2846
+ int actualHeight = getPreviewHeight();
2774
2847
 
2775
- if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
2776
- ViewGroup.MarginLayoutParams params =
2777
- (ViewGroup.MarginLayoutParams) layoutParams;
2848
+ // Convert to logical pixels for JavaScript
2849
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
2850
+ float pixelRatio = metrics.density;
2778
2851
 
2779
- // Remove insets to get original coordinates in DP
2780
- DisplayMetrics metrics = context.getResources().getDisplayMetrics();
2781
- float pixelRatio = metrics.density;
2852
+ // Remove WebView insets from coordinates
2853
+ int webViewTopInset = getWebViewTopInset();
2854
+ int webViewLeftInset = getWebViewLeftInset();
2782
2855
 
2783
- int webViewTopInset = getWebViewTopInset();
2784
- int webViewLeftInset = getWebViewLeftInset();
2856
+ int x = Math.max(
2857
+ 0,
2858
+ (int) ((actualX - webViewLeftInset) / pixelRatio)
2859
+ );
2860
+ int y = Math.max(
2861
+ 0,
2862
+ (int) ((actualY - webViewTopInset) / pixelRatio)
2863
+ );
2864
+ int width = (int) (actualWidth / pixelRatio);
2865
+ int height = (int) (actualHeight / pixelRatio);
2785
2866
 
2786
- x = Math.max(
2787
- 0,
2788
- (int) ((params.leftMargin - webViewLeftInset) / pixelRatio)
2789
- );
2790
- y = Math.max(
2791
- 0,
2792
- (int) ((params.topMargin - webViewTopInset) / pixelRatio)
2867
+ return new int[] { x, y, width, height };
2868
+ }
2869
+
2870
+ private void updateGridOverlayBounds() {
2871
+ if (gridOverlayView != null && previewView != null) {
2872
+ // Get the actual camera bounds
2873
+ Rect cameraBounds = getActualCameraBounds();
2874
+
2875
+ // Update the grid overlay with the camera bounds
2876
+ gridOverlayView.setCameraBounds(cameraBounds);
2877
+
2878
+ Log.d(
2879
+ TAG,
2880
+ "updateGridOverlayBounds: Updated grid bounds to " +
2881
+ cameraBounds.toString()
2793
2882
  );
2794
- width = (int) (params.width / pixelRatio);
2795
- height = (int) (params.height / pixelRatio);
2796
2883
  }
2797
-
2798
- return new int[] { x, y, width, height };
2799
2884
  }
2800
2885
 
2801
2886
  private void triggerAutoFocus() {
@@ -3,6 +3,7 @@ package com.ahm.capacitor.camera.preview;
3
3
  import android.content.Context;
4
4
  import android.graphics.Canvas;
5
5
  import android.graphics.Paint;
6
+ import android.graphics.Rect;
6
7
  import android.util.AttributeSet;
7
8
  import android.view.View;
8
9
 
@@ -10,6 +11,7 @@ public class GridOverlayView extends View {
10
11
 
11
12
  private Paint gridPaint;
12
13
  private String gridMode = "none";
14
+ private Rect cameraBounds = null;
13
15
 
14
16
  public GridOverlayView(Context context) {
15
17
  super(context);
@@ -38,6 +40,11 @@ public class GridOverlayView extends View {
38
40
  gridPaint.setAntiAlias(true);
39
41
  }
40
42
 
43
+ public void setCameraBounds(Rect bounds) {
44
+ this.cameraBounds = bounds;
45
+ invalidate();
46
+ }
47
+
41
48
  public void setGridMode(String mode) {
42
49
  String previousMode = this.gridMode;
43
50
  this.gridMode = mode != null ? mode : "none";
@@ -62,34 +69,44 @@ public class GridOverlayView extends View {
62
69
  return;
63
70
  }
64
71
 
72
+ // Use camera bounds if available, otherwise use full view bounds
73
+ int left = 0;
74
+ int top = 0;
65
75
  int width = getWidth();
66
76
  int height = getHeight();
67
77
 
78
+ if (cameraBounds != null) {
79
+ left = cameraBounds.left;
80
+ top = cameraBounds.top;
81
+ width = cameraBounds.width();
82
+ height = cameraBounds.height();
83
+ }
84
+
68
85
  if (width <= 0 || height <= 0) {
69
86
  return;
70
87
  }
71
88
 
72
89
  if ("3x3".equals(gridMode)) {
73
- drawGrid(canvas, width, height, 3);
90
+ drawGrid(canvas, left, top, width, height, 3);
74
91
  } else if ("4x4".equals(gridMode)) {
75
- drawGrid(canvas, width, height, 4);
92
+ drawGrid(canvas, left, top, width, height, 4);
76
93
  }
77
94
  }
78
95
 
79
- private void drawGrid(Canvas canvas, int width, int height, int divisions) {
96
+ private void drawGrid(Canvas canvas, int left, int top, int width, int height, int divisions) {
80
97
  float stepX = (float) width / divisions;
81
98
  float stepY = (float) height / divisions;
82
99
 
83
100
  // Draw vertical lines
84
101
  for (int i = 1; i < divisions; i++) {
85
- float x = i * stepX;
86
- canvas.drawLine(x, 0, x, height, gridPaint);
102
+ float x = left + (i * stepX);
103
+ canvas.drawLine(x, top, x, top + height, gridPaint);
87
104
  }
88
105
 
89
106
  // Draw horizontal lines
90
107
  for (int i = 1; i < divisions; i++) {
91
- float y = i * stepY;
92
- canvas.drawLine(0, y, width, y, gridPaint);
108
+ float y = top + (i * stepY);
109
+ canvas.drawLine(left, y, left + width, y, gridPaint);
93
110
  }
94
111
  }
95
112
  }