@capgo/camera-preview 7.4.0-alpha.3 → 7.4.0-alpha.35

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.
@@ -1,6 +1,9 @@
1
1
  package com.ahm.capacitor.camera.preview;
2
2
 
3
+ import static androidx.core.content.ContextCompat.getSystemService;
4
+
3
5
  import android.content.Context;
6
+ import android.content.res.Configuration;
4
7
  import android.graphics.Bitmap;
5
8
  import android.graphics.BitmapFactory;
6
9
  import android.graphics.Color;
@@ -20,17 +23,13 @@ import android.util.Size;
20
23
  import android.view.MotionEvent;
21
24
  import android.view.View;
22
25
  import android.view.ViewGroup;
23
- import android.view.animation.AlphaAnimation;
24
- import android.view.animation.Animation;
25
- import android.view.animation.AnimationSet;
26
- import android.view.animation.AnimationUtils;
27
- import android.view.animation.ScaleAnimation;
28
- import android.webkit.WebView;
26
+ import android.view.WindowManager;
27
+ import android.view.WindowMetrics;
29
28
  import android.webkit.WebView;
30
29
  import android.widget.FrameLayout;
31
- import android.widget.FrameLayout;
32
30
  import androidx.annotation.NonNull;
33
31
  import androidx.annotation.OptIn;
32
+ import androidx.annotation.RequiresApi;
34
33
  import androidx.camera.camera2.interop.Camera2CameraInfo;
35
34
  import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
36
35
  import androidx.camera.core.AspectRatio;
@@ -78,7 +77,6 @@ import java.util.Set;
78
77
  import java.util.concurrent.Executor;
79
78
  import java.util.concurrent.ExecutorService;
80
79
  import java.util.concurrent.Executors;
81
- import java.util.concurrent.TimeUnit;
82
80
  import org.json.JSONObject;
83
81
 
84
82
  public class CameraXView implements LifecycleOwner, LifecycleObserver {
@@ -102,6 +100,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
102
100
  private GridOverlayView gridOverlayView;
103
101
  private FrameLayout previewContainer;
104
102
  private View focusIndicatorView;
103
+ private long focusIndicatorAnimationId = 0; // Incrementing token to invalidate previous animations
105
104
  private CameraSelector currentCameraSelector;
106
105
  private String currentDeviceId;
107
106
  private int currentFlashMode = ImageCapture.FLASH_MODE_OFF;
@@ -141,6 +140,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
141
140
  return isRunning;
142
141
  }
143
142
 
143
+ public View getPreviewContainer() {
144
+ return previewContainer;
145
+ }
146
+
144
147
  private void saveImageToGallery(byte[] data) {
145
148
  try {
146
149
  // Detect image format from byte array header
@@ -274,9 +277,24 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
274
277
  previewContainer.setClickable(true);
275
278
  previewContainer.setFocusable(true);
276
279
 
280
+ // Disable any potential drawing artifacts that might cause 1px offset
281
+ previewContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
282
+
283
+ // Ensure no clip bounds that might cause visual offset
284
+ previewContainer.setClipChildren(false);
285
+ previewContainer.setClipToPadding(false);
286
+
277
287
  // Create and setup the preview view
278
288
  previewView = new PreviewView(context);
279
- previewView.setScaleType(PreviewView.ScaleType.FIT_CENTER);
289
+ // Match iOS behavior: FIT when no aspect ratio, FILL when aspect ratio is set
290
+ String initialAspectRatio = sessionConfig != null
291
+ ? sessionConfig.getAspectRatio()
292
+ : null;
293
+ previewView.setScaleType(
294
+ (initialAspectRatio == null || initialAspectRatio.isEmpty())
295
+ ? PreviewView.ScaleType.FIT_CENTER
296
+ : PreviewView.ScaleType.FILL_CENTER
297
+ );
280
298
  // Also make preview view touchable as backup
281
299
  previewView.setClickable(true);
282
300
  previewView.setFocusable(true);
@@ -433,9 +451,64 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
433
451
  int height = sessionConfig.getHeight();
434
452
  String aspectRatio = sessionConfig.getAspectRatio();
435
453
 
454
+ // Get comprehensive display information
455
+ int screenWidthPx, screenHeightPx;
456
+ float density;
457
+
458
+ // Get density using DisplayMetrics (available on all API levels)
459
+ WindowManager windowManager = (WindowManager) this.context.getSystemService(
460
+ Context.WINDOW_SERVICE
461
+ );
462
+ DisplayMetrics displayMetrics = new DisplayMetrics();
463
+ windowManager.getDefaultDisplay().getMetrics(displayMetrics);
464
+ density = displayMetrics.density;
465
+
466
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
467
+ // API 30+ (Android 11+) - use WindowMetrics for screen dimensions
468
+ WindowMetrics metrics = windowManager.getCurrentWindowMetrics();
469
+ Rect bounds = metrics.getBounds();
470
+ screenWidthPx = bounds.width();
471
+ screenHeightPx = bounds.height();
472
+ } else {
473
+ // API < 30 - use legacy DisplayMetrics for screen dimensions
474
+ screenWidthPx = displayMetrics.widthPixels;
475
+ screenHeightPx = displayMetrics.heightPixels;
476
+ }
477
+
478
+ int screenWidthDp = (int) (screenWidthPx / density);
479
+ int screenHeightDp = (int) (screenHeightPx / density);
480
+
481
+ // Get WebView dimensions
482
+ int webViewWidth = webView != null ? webView.getWidth() : 0;
483
+ int webViewHeight = webView != null ? webView.getHeight() : 0;
484
+
485
+ // Get parent dimensions
486
+ ViewGroup parent = (ViewGroup) webView.getParent();
487
+ int parentWidth = parent != null ? parent.getWidth() : 0;
488
+ int parentHeight = parent != null ? parent.getHeight() : 0;
489
+
490
+ Log.d(
491
+ TAG,
492
+ "======================== CALCULATE PREVIEW LAYOUT PARAMS ========================"
493
+ );
494
+ Log.d(
495
+ TAG,
496
+ "Screen dimensions - Pixels: " +
497
+ screenWidthPx +
498
+ "x" +
499
+ screenHeightPx +
500
+ ", DP: " +
501
+ screenWidthDp +
502
+ "x" +
503
+ screenHeightDp +
504
+ ", Density: " +
505
+ density
506
+ );
507
+ Log.d(TAG, "WebView dimensions: " + webViewWidth + "x" + webViewHeight);
508
+ Log.d(TAG, "Parent dimensions: " + parentWidth + "x" + parentHeight);
436
509
  Log.d(
437
510
  TAG,
438
- "calculatePreviewLayoutParams: Using sessionConfig values - x:" +
511
+ "SessionConfig values - x:" +
439
512
  x +
440
513
  " y:" +
441
514
  y +
@@ -444,83 +517,97 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
444
517
  " height:" +
445
518
  height +
446
519
  " aspectRatio:" +
447
- aspectRatio
520
+ aspectRatio +
521
+ " isCentered:" +
522
+ sessionConfig.isCentered()
448
523
  );
449
524
 
450
- // Apply aspect ratio if specified and no explicit size was given
451
- if (aspectRatio != null && !aspectRatio.isEmpty()) {
525
+ // Apply aspect ratio if specified
526
+ if (
527
+ aspectRatio != null &&
528
+ !aspectRatio.isEmpty() &&
529
+ sessionConfig.isCentered()
530
+ ) {
452
531
  String[] ratios = aspectRatio.split(":");
453
532
  if (ratios.length == 2) {
454
533
  try {
455
- // For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
456
- float ratio =
457
- Float.parseFloat(ratios[1]) / Float.parseFloat(ratios[0]);
458
-
459
- // Calculate optimal size while maintaining aspect ratio
460
- int optimalWidth = width;
461
- int optimalHeight = (int) (width / ratio);
462
-
463
- if (optimalHeight > height) {
464
- // Height constraint is tighter, fit by height
465
- optimalHeight = height;
466
- optimalWidth = (int) (height * ratio);
467
- }
534
+ // Match iOS logic exactly
535
+ double ratioWidth = Double.parseDouble(ratios[0]);
536
+ double ratioHeight = Double.parseDouble(ratios[1]);
537
+ boolean isPortrait =
538
+ context.getResources().getConfiguration().orientation ==
539
+ Configuration.ORIENTATION_PORTRAIT;
468
540
 
469
- // Store the old dimensions to check if we need to recenter
470
- int oldWidth = width;
471
- int oldHeight = height;
472
- width = optimalWidth;
473
- height = optimalHeight;
541
+ Log.d(
542
+ TAG,
543
+ "Aspect ratio parsing - Original: " +
544
+ aspectRatio +
545
+ " (width=" +
546
+ ratioWidth +
547
+ ", height=" +
548
+ ratioHeight +
549
+ ")"
550
+ );
551
+ Log.d(
552
+ TAG,
553
+ "Device orientation: " + (isPortrait ? "PORTRAIT" : "LANDSCAPE")
554
+ );
474
555
 
475
- // If we're centered and dimensions changed, recalculate position
476
- if (sessionConfig.isCentered()) {
477
- DisplayMetrics metrics = context.getResources().getDisplayMetrics();
556
+ // iOS: let ratio = !isPortrait ? ratioParts[0] / ratioParts[1] : ratioParts[1] / ratioParts[0]
557
+ double ratio = !isPortrait
558
+ ? (ratioWidth / ratioHeight)
559
+ : (ratioHeight / ratioWidth);
478
560
 
479
- if (width != oldWidth) {
480
- int screenWidth = metrics.widthPixels;
481
- x = (screenWidth - width) / 2;
482
- Log.d(
483
- TAG,
484
- "calculatePreviewLayoutParams: Recentered X after aspect ratio - " +
485
- "oldWidth=" +
486
- oldWidth +
487
- ", newWidth=" +
488
- width +
489
- ", screenWidth=" +
490
- screenWidth +
491
- ", newX=" +
492
- x
493
- );
494
- }
561
+ Log.d(
562
+ TAG,
563
+ "Computed ratio: " +
564
+ ratio +
565
+ " (iOS formula: " +
566
+ (!isPortrait ? "width/height" : "height/width") +
567
+ ")"
568
+ );
495
569
 
496
- if (height != oldHeight) {
497
- int screenHeight = metrics.heightPixels;
498
- // Always center based on full screen height
499
- y = (screenHeight - height) / 2;
500
- Log.d(
501
- TAG,
502
- "calculatePreviewLayoutParams: Recentered Y after aspect ratio - " +
503
- "oldHeight=" +
504
- oldHeight +
505
- ", newHeight=" +
506
- height +
507
- ", screenHeight=" +
508
- screenHeight +
509
- ", newY=" +
510
- y
511
- );
512
- }
513
- }
570
+ // For centered mode with aspect ratio, calculate maximum size that fits
571
+ int availableWidth = screenWidthPx;
572
+ int availableHeight = screenHeightPx;
514
573
 
515
574
  Log.d(
516
575
  TAG,
517
- "calculatePreviewLayoutParams: Applied aspect ratio " +
518
- aspectRatio +
519
- " - new size: " +
520
- width +
576
+ "Available space for preview: " +
577
+ availableWidth +
521
578
  "x" +
522
- height
579
+ availableHeight
580
+ );
581
+
582
+ // Calculate maximum size that fits the aspect ratio in available space
583
+ double maxWidthByHeight = availableHeight * ratio;
584
+ double maxHeightByWidth = availableWidth / ratio;
585
+
586
+ Log.d(
587
+ TAG,
588
+ "Aspect ratio calculations - maxWidthByHeight: " +
589
+ maxWidthByHeight +
590
+ ", maxHeightByWidth: " +
591
+ maxHeightByWidth
523
592
  );
593
+
594
+ if (maxWidthByHeight <= availableWidth) {
595
+ // Height is the limiting factor
596
+ width = (int) maxWidthByHeight;
597
+ height = availableHeight;
598
+ Log.d(TAG, "Height-limited sizing: " + width + "x" + height);
599
+ } else {
600
+ // Width is the limiting factor
601
+ width = availableWidth;
602
+ height = (int) maxHeightByWidth;
603
+ Log.d(TAG, "Width-limited sizing: " + width + "x" + height);
604
+ }
605
+
606
+ // Center the preview
607
+ x = (availableWidth - width) / 2;
608
+ y = (availableHeight - height) / 2;
609
+
610
+ Log.d(TAG, "Auto-centered position: x=" + x + ", y=" + y);
524
611
  } catch (NumberFormatException e) {
525
612
  Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio, e);
526
613
  }
@@ -539,28 +626,20 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
539
626
 
540
627
  Log.d(
541
628
  TAG,
542
- "calculatePreviewLayoutParams: Position calculation - x:" +
543
- x +
544
- " (leftMargin=" +
629
+ "Final layout params - Margins: left=" +
545
630
  layoutParams.leftMargin +
546
- "), y:" +
547
- y +
548
- " (topMargin=" +
631
+ ", top=" +
549
632
  layoutParams.topMargin +
550
- ")"
633
+ ", Size: " +
634
+ width +
635
+ "x" +
636
+ height
551
637
  );
552
-
553
638
  Log.d(
554
639
  TAG,
555
- "calculatePreviewLayoutParams: Final layout - x:" +
556
- x +
557
- " y:" +
558
- y +
559
- " width:" +
560
- width +
561
- " height:" +
562
- height
640
+ "================================================================================"
563
641
  );
642
+
564
643
  return layoutParams;
565
644
  }
566
645
 
@@ -622,13 +701,19 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
622
701
  ResolutionSelector resolutionSelector =
623
702
  resolutionSelectorBuilder.build();
624
703
 
704
+ int rotation = previewView != null && previewView.getDisplay() != null
705
+ ? previewView.getDisplay().getRotation()
706
+ : android.view.Surface.ROTATION_0;
707
+
625
708
  Preview preview = new Preview.Builder()
626
709
  .setResolutionSelector(resolutionSelector)
710
+ .setTargetRotation(rotation)
627
711
  .build();
628
712
  imageCapture = new ImageCapture.Builder()
629
713
  .setResolutionSelector(resolutionSelector)
630
714
  .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
631
715
  .setFlashMode(currentFlashMode)
716
+ .setTargetRotation(rotation)
632
717
  .build();
633
718
  sampleImageCapture = imageCapture;
634
719
  preview.setSurfaceProvider(previewView.getSurfaceProvider());
@@ -687,6 +772,50 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
687
772
  if (previewResolution != null) {
688
773
  currentPreviewResolution = previewResolution.getResolution();
689
774
  Log.d(TAG, "Preview resolution: " + currentPreviewResolution);
775
+
776
+ // Log the actual aspect ratio of the selected resolution
777
+ if (currentPreviewResolution != null) {
778
+ double actualRatio =
779
+ (double) currentPreviewResolution.getWidth() /
780
+ (double) currentPreviewResolution.getHeight();
781
+ Log.d(
782
+ TAG,
783
+ "Actual preview aspect ratio: " +
784
+ actualRatio +
785
+ " (width=" +
786
+ currentPreviewResolution.getWidth() +
787
+ ", height=" +
788
+ currentPreviewResolution.getHeight() +
789
+ ")"
790
+ );
791
+
792
+ // Compare with requested ratio
793
+ if ("4:3".equals(sessionConfig.getAspectRatio())) {
794
+ double expectedRatio = 4.0 / 3.0;
795
+ double difference = Math.abs(actualRatio - expectedRatio);
796
+ Log.d(
797
+ TAG,
798
+ "4:3 ratio check - Expected: " +
799
+ expectedRatio +
800
+ ", Actual: " +
801
+ actualRatio +
802
+ ", Difference: " +
803
+ difference
804
+ );
805
+ } else if ("16:9".equals(sessionConfig.getAspectRatio())) {
806
+ double expectedRatio = 16.0 / 9.0;
807
+ double difference = Math.abs(actualRatio - expectedRatio);
808
+ Log.d(
809
+ TAG,
810
+ "16:9 ratio check - Expected: " +
811
+ expectedRatio +
812
+ ", Actual: " +
813
+ actualRatio +
814
+ ", Difference: " +
815
+ difference
816
+ );
817
+ }
818
+ }
690
819
  }
691
820
  ResolutionInfo imageCaptureResolution =
692
821
  imageCapture.getResolutionInfo();
@@ -698,6 +827,16 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
698
827
  );
699
828
  }
700
829
 
830
+ // Update scale type based on aspect ratio whenever (re)binding
831
+ String ar = sessionConfig != null
832
+ ? sessionConfig.getAspectRatio()
833
+ : null;
834
+ previewView.setScaleType(
835
+ (ar == null || ar.isEmpty())
836
+ ? PreviewView.ScaleType.FIT_CENTER
837
+ : PreviewView.ScaleType.FILL_CENTER
838
+ );
839
+
701
840
  // Set initial zoom if specified, prioritizing targetZoom over default zoomFactor
702
841
  float initialZoom = sessionConfig.getTargetZoom() != 1.0f
703
842
  ? sessionConfig.getTargetZoom()
@@ -726,7 +865,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
726
865
  }
727
866
  }
728
867
 
729
- setZoomInternal(initialZoom);
868
+ setZoom(initialZoom);
730
869
  }
731
870
 
732
871
  isRunning = true;
@@ -913,26 +1052,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
913
1052
 
914
1053
  JSONObject exifData = getExifData(exifInterface);
915
1054
 
916
- // Use the stored aspectRatio if none is provided and no width/height is specified
1055
+ // Determine final output: explicit size wins, then explicit aspectRatio,
1056
+ // otherwise crop to match what is visible in the preview (iOS parity)
917
1057
  String captureAspectRatio = aspectRatio;
918
- if (
919
- width == null &&
920
- height == null &&
921
- aspectRatio == null &&
922
- sessionConfig != null
923
- ) {
924
- captureAspectRatio = sessionConfig.getAspectRatio();
925
- // Default to "4:3" if no aspect ratio was set at all
926
- if (captureAspectRatio == null) {
927
- captureAspectRatio = "4:3";
928
- }
929
- Log.d(
930
- TAG,
931
- "capturePhoto: Using stored aspectRatio: " + captureAspectRatio
932
- );
933
- }
934
-
935
- // Handle aspect ratio if no width/height specified
936
1058
  if (
937
1059
  width == null &&
938
1060
  height == null &&
@@ -1026,14 +1148,22 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1026
1148
  // Write EXIF data back to resized image
1027
1149
  bytes = writeExifToImageBytes(bytes, exifInterface);
1028
1150
  } else {
1029
- // For non-resized images, ensure EXIF is saved
1030
- exifInterface.saveAttributes();
1031
- bytes = new byte[(int) tempFile.length()];
1032
- java.io.FileInputStream fis2 = new java.io.FileInputStream(
1033
- tempFile
1151
+ // No explicit size/ratio: crop to match current preview content
1152
+ Bitmap originalBitmap = BitmapFactory.decodeByteArray(
1153
+ bytes,
1154
+ 0,
1155
+ bytes.length
1156
+ );
1157
+ Bitmap previewCropped = cropBitmapToMatchPreview(originalBitmap);
1158
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
1159
+ previewCropped.compress(
1160
+ Bitmap.CompressFormat.JPEG,
1161
+ quality,
1162
+ stream
1034
1163
  );
1035
- fis2.read(bytes);
1036
- fis2.close();
1164
+ bytes = stream.toByteArray();
1165
+ // Preserve EXIF
1166
+ bytes = writeExifToImageBytes(bytes, exifInterface);
1037
1167
  }
1038
1168
 
1039
1169
  if (saveToGallery) {
@@ -1379,6 +1509,49 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1379
1509
  return bytes;
1380
1510
  }
1381
1511
 
1512
+ private Bitmap cropBitmapToMatchPreview(Bitmap image) {
1513
+ if (previewContainer == null || previewView == null) {
1514
+ return image;
1515
+ }
1516
+ int containerWidth = previewContainer.getWidth();
1517
+ int containerHeight = previewContainer.getHeight();
1518
+ if (containerWidth == 0 || containerHeight == 0) {
1519
+ return image;
1520
+ }
1521
+ // Compute preview aspect based on actual camera content bounds
1522
+ Rect bounds = getActualCameraBounds();
1523
+ int previewW = Math.max(1, bounds.width());
1524
+ int previewH = Math.max(1, bounds.height());
1525
+ float previewRatio = (float) previewW / (float) previewH;
1526
+
1527
+ int imgW = image.getWidth();
1528
+ int imgH = image.getHeight();
1529
+ float imgRatio = (float) imgW / (float) imgH;
1530
+
1531
+ int targetW = imgW;
1532
+ int targetH = imgH;
1533
+ if (imgRatio > previewRatio) {
1534
+ // Image wider than preview: crop width
1535
+ targetW = Math.round(imgH * previewRatio);
1536
+ } else if (imgRatio < previewRatio) {
1537
+ // Image taller than preview: crop height
1538
+ targetH = Math.round(imgW / previewRatio);
1539
+ }
1540
+ int x = Math.max(0, (imgW - targetW) / 2);
1541
+ int y = Math.max(0, (imgH - targetH) / 2);
1542
+ try {
1543
+ return Bitmap.createBitmap(
1544
+ image,
1545
+ x,
1546
+ y,
1547
+ Math.min(targetW, imgW - x),
1548
+ Math.min(targetH, imgH - y)
1549
+ );
1550
+ } catch (Exception ignore) {
1551
+ return image;
1552
+ }
1553
+ }
1554
+
1382
1555
  // not workin for xiaomi https://xiaomi.eu/community/threads/mi-11-ultra-unable-to-access-camera-lenses-in-apps-camera2-api.61456/
1383
1556
  @OptIn(markerClass = ExperimentalCamera2Interop.class)
1384
1557
  public static List<
@@ -1633,7 +1806,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1633
1806
  }
1634
1807
  }
1635
1808
 
1636
- public void setZoom(float zoomRatio, boolean autoFocus) throws Exception {
1809
+ public void setZoom(float zoomRatio) throws Exception {
1637
1810
  if (camera == null) {
1638
1811
  throw new Exception("Camera not initialized");
1639
1812
  }
@@ -1642,26 +1815,16 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1642
1815
 
1643
1816
  // Just let CameraX handle everything - it should automatically switch lenses
1644
1817
  try {
1645
- ListenableFuture<Void> zoomFuture = camera
1646
- .getCameraControl()
1647
- .setZoomRatio(zoomRatio);
1818
+ ZoomFactors zoomFactors = getZoomFactors();
1648
1819
 
1649
- // Add callback to see what actually happened
1650
- zoomFuture.addListener(
1651
- () -> {
1652
- try {
1653
- zoomFuture.get();
1654
- Log.d(TAG, "Zoom successfully set to " + zoomRatio);
1655
- // Trigger autofocus after zoom if requested
1656
- if (autoFocus) {
1657
- triggerAutoFocus();
1658
- }
1659
- } catch (Exception e) {
1660
- Log.e(TAG, "Error setting zoom: " + e.getMessage());
1661
- }
1662
- },
1663
- ContextCompat.getMainExecutor(context)
1664
- );
1820
+ if (zoomRatio < zoomFactors.getMin()) {
1821
+ zoomRatio = zoomFactors.getMin();
1822
+ } else if (zoomRatio > zoomFactors.getMax()) {
1823
+ zoomRatio = zoomFactors.getMax();
1824
+ }
1825
+
1826
+ camera.getCameraControl().setZoomRatio(zoomRatio);
1827
+ // Note: autofocus is intentionally not triggered on zoom because it's done by CameraX
1665
1828
  } catch (Exception e) {
1666
1829
  Log.e(TAG, "Failed to set zoom: " + e.getMessage());
1667
1830
  throw e;
@@ -1707,12 +1870,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1707
1870
  MeteringPointFactory factory = previewView.getMeteringPointFactory();
1708
1871
  MeteringPoint point = factory.createPoint(x * viewWidth, y * viewHeight);
1709
1872
 
1710
- // Create focus and metering action
1873
+ // Create focus and metering action (persistent, no auto-cancel) to match iOS behavior
1711
1874
  FocusMeteringAction action = new FocusMeteringAction.Builder(
1712
1875
  point,
1713
1876
  FocusMeteringAction.FLAG_AF | FocusMeteringAction.FLAG_AE
1714
1877
  )
1715
- .setAutoCancelDuration(3, TimeUnit.SECONDS) // Auto-cancel after 3 seconds
1878
+ .disableAutoCancel()
1716
1879
  .build();
1717
1880
 
1718
1881
  try {
@@ -1776,15 +1939,18 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1776
1939
  return;
1777
1940
  }
1778
1941
 
1779
- // Remove any existing focus indicator
1942
+ // Remove any existing focus indicator and cancel its animation
1780
1943
  if (focusIndicatorView != null) {
1944
+ try {
1945
+ focusIndicatorView.clearAnimation();
1946
+ } catch (Exception ignore) {}
1781
1947
  previewContainer.removeView(focusIndicatorView);
1782
1948
  focusIndicatorView = null;
1783
1949
  }
1784
1950
 
1785
1951
  // Create an elegant focus indicator
1786
- View container = new View(context);
1787
- int size = (int) (60 * context.getResources().getDisplayMetrics().density); // 60dp size
1952
+ FrameLayout container = new FrameLayout(context);
1953
+ int size = (int) (80 * context.getResources().getDisplayMetrics().density); // match iOS size
1788
1954
  FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(size, size);
1789
1955
 
1790
1956
  // Center the indicator on the touch point with bounds checking
@@ -1800,22 +1966,73 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1800
1966
  Math.min((int) (y - size / 2), containerHeight - size)
1801
1967
  );
1802
1968
 
1803
- // Create an elegant focus ring - white stroke with transparent center
1804
- GradientDrawable drawable = new GradientDrawable();
1805
- drawable.setShape(GradientDrawable.OVAL);
1806
- drawable.setStroke(
1807
- (int) (2 * context.getResources().getDisplayMetrics().density),
1808
- Color.WHITE
1809
- ); // 2dp white stroke
1810
- drawable.setColor(Color.TRANSPARENT); // Transparent center
1811
- container.setBackground(drawable);
1969
+ // iOS Camera style: square with mid-edge ticks
1970
+ GradientDrawable border = new GradientDrawable();
1971
+ border.setShape(GradientDrawable.RECTANGLE);
1972
+ int stroke = (int) (2 * context.getResources().getDisplayMetrics().density);
1973
+ border.setStroke(stroke, Color.YELLOW);
1974
+ border.setCornerRadius(0);
1975
+ border.setColor(Color.TRANSPARENT);
1976
+ container.setBackground(border);
1977
+
1978
+ // Add 4 tiny mid-edge ticks inside the square
1979
+ int tickLen = (int) (12 *
1980
+ context.getResources().getDisplayMetrics().density);
1981
+ int inset = stroke; // ticks should touch the sides
1982
+ // Top tick (perpendicular): vertical inward from top edge
1983
+ View topTick = new View(context);
1984
+ FrameLayout.LayoutParams topParams = new FrameLayout.LayoutParams(
1985
+ stroke,
1986
+ tickLen
1987
+ );
1988
+ topParams.leftMargin = (size - stroke) / 2;
1989
+ topParams.topMargin = inset;
1990
+ topTick.setLayoutParams(topParams);
1991
+ topTick.setBackgroundColor(Color.YELLOW);
1992
+ container.addView(topTick);
1993
+ // Bottom tick (perpendicular): vertical inward from bottom edge
1994
+ View bottomTick = new View(context);
1995
+ FrameLayout.LayoutParams bottomParams = new FrameLayout.LayoutParams(
1996
+ stroke,
1997
+ tickLen
1998
+ );
1999
+ bottomParams.leftMargin = (size - stroke) / 2;
2000
+ bottomParams.topMargin = size - inset - tickLen;
2001
+ bottomTick.setLayoutParams(bottomParams);
2002
+ bottomTick.setBackgroundColor(Color.YELLOW);
2003
+ container.addView(bottomTick);
2004
+ // Left tick (perpendicular): horizontal inward from left edge
2005
+ View leftTick = new View(context);
2006
+ FrameLayout.LayoutParams leftParams = new FrameLayout.LayoutParams(
2007
+ tickLen,
2008
+ stroke
2009
+ );
2010
+ leftParams.leftMargin = inset;
2011
+ leftParams.topMargin = (size - stroke) / 2;
2012
+ leftTick.setLayoutParams(leftParams);
2013
+ leftTick.setBackgroundColor(Color.YELLOW);
2014
+ container.addView(leftTick);
2015
+ // Right tick (perpendicular): horizontal inward from right edge
2016
+ View rightTick = new View(context);
2017
+ FrameLayout.LayoutParams rightParams = new FrameLayout.LayoutParams(
2018
+ tickLen,
2019
+ stroke
2020
+ );
2021
+ rightParams.leftMargin = size - inset - tickLen;
2022
+ rightParams.topMargin = (size - stroke) / 2;
2023
+ rightTick.setLayoutParams(rightParams);
2024
+ rightTick.setBackgroundColor(Color.YELLOW);
2025
+ container.addView(rightTick);
1812
2026
 
1813
2027
  focusIndicatorView = container;
1814
-
1815
- // Set initial state for smooth animation
1816
- focusIndicatorView.setAlpha(1f); // Start visible
1817
- focusIndicatorView.setScaleX(1.8f); // Start larger for scale-in effect
1818
- focusIndicatorView.setScaleY(1.8f);
2028
+ // Bump animation token; everything after this must validate against this token
2029
+ final long thisAnimationId = ++focusIndicatorAnimationId;
2030
+ final View thisIndicatorView = focusIndicatorView;
2031
+
2032
+ // Set initial state for smooth animation (mirror iOS)
2033
+ focusIndicatorView.setAlpha(0f);
2034
+ focusIndicatorView.setScaleX(1.5f);
2035
+ focusIndicatorView.setScaleY(1.5f);
1819
2036
  focusIndicatorView.setVisibility(View.VISIBLE);
1820
2037
 
1821
2038
  // Ensure container doesn't intercept touch events
@@ -1839,127 +2056,88 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1839
2056
  // Force a layout pass to ensure the view is properly positioned
1840
2057
  previewContainer.requestLayout();
1841
2058
 
1842
- // Smooth scale down animation with easing (no fade needed since we start visible)
1843
- ScaleAnimation scaleAnimation = new ScaleAnimation(
1844
- 1.8f,
1845
- 1.0f,
1846
- 1.8f,
1847
- 1.0f,
1848
- Animation.RELATIVE_TO_SELF,
1849
- 0.5f,
1850
- Animation.RELATIVE_TO_SELF,
1851
- 0.5f
1852
- );
1853
- scaleAnimation.setDuration(300);
1854
- scaleAnimation.setInterpolator(
1855
- new android.view.animation.OvershootInterpolator(1.2f)
1856
- );
2059
+ // First phase: fade in and scale to 1.0 over 150ms
2060
+ focusIndicatorView
2061
+ .animate()
2062
+ .alpha(1f)
2063
+ .scaleX(1f)
2064
+ .scaleY(1f)
2065
+ .setDuration(150)
2066
+ .start();
1857
2067
 
1858
- // Start the animation
1859
- focusIndicatorView.startAnimation(scaleAnimation);
1860
-
1861
- // Schedule fade out and removal with smoother timing
2068
+ // Phase 2: after 500ms, fade to 0.3 over 200ms
1862
2069
  focusIndicatorView.postDelayed(
1863
2070
  new Runnable() {
1864
2071
  @Override
1865
2072
  public void run() {
1866
- if (focusIndicatorView != null) {
1867
- // Smooth fade to semi-transparent
1868
- AlphaAnimation fadeToTransparent = new AlphaAnimation(1f, 0.4f);
1869
- fadeToTransparent.setDuration(400);
1870
- fadeToTransparent.setInterpolator(
1871
- new android.view.animation.AccelerateInterpolator()
1872
- );
1873
-
1874
- fadeToTransparent.setAnimationListener(
1875
- new Animation.AnimationListener() {
1876
- @Override
1877
- public void onAnimationStart(Animation animation) {
1878
- Log.d(TAG, "showFocusIndicator: Fade to transparent started");
1879
- }
1880
-
1881
- @Override
1882
- public void onAnimationEnd(Animation animation) {
1883
- Log.d(
1884
- TAG,
1885
- "showFocusIndicator: Fade to transparent ended, starting final fade out"
1886
- );
1887
- // Final smooth fade out and scale down
1888
- if (focusIndicatorView != null) {
1889
- AnimationSet finalAnimation = new AnimationSet(false);
1890
-
1891
- AlphaAnimation finalFadeOut = new AlphaAnimation(0.4f, 0f);
1892
- finalFadeOut.setDuration(500);
1893
- finalFadeOut.setStartOffset(300);
1894
- finalFadeOut.setInterpolator(
1895
- new android.view.animation.AccelerateInterpolator()
1896
- );
1897
-
1898
- ScaleAnimation finalScaleDown = new ScaleAnimation(
1899
- 1.0f,
1900
- 0.9f,
1901
- 1.0f,
1902
- 0.9f,
1903
- Animation.RELATIVE_TO_SELF,
1904
- 0.5f,
1905
- Animation.RELATIVE_TO_SELF,
1906
- 0.5f
1907
- );
1908
- finalScaleDown.setDuration(500);
1909
- finalScaleDown.setStartOffset(300);
1910
- finalScaleDown.setInterpolator(
1911
- new android.view.animation.AccelerateInterpolator()
1912
- );
1913
-
1914
- finalAnimation.addAnimation(finalFadeOut);
1915
- finalAnimation.addAnimation(finalScaleDown);
1916
-
1917
- finalAnimation.setAnimationListener(
1918
- new Animation.AnimationListener() {
1919
- @Override
1920
- public void onAnimationStart(Animation animation) {
1921
- Log.d(
1922
- TAG,
1923
- "showFocusIndicator: Final animation started"
1924
- );
1925
- }
1926
-
2073
+ // Ensure this runnable belongs to the latest indicator
2074
+ if (
2075
+ focusIndicatorView != null &&
2076
+ thisIndicatorView == focusIndicatorView &&
2077
+ thisAnimationId == focusIndicatorAnimationId
2078
+ ) {
2079
+ focusIndicatorView
2080
+ .animate()
2081
+ .alpha(0.3f)
2082
+ .setDuration(200)
2083
+ .withEndAction(
2084
+ new Runnable() {
2085
+ @Override
2086
+ public void run() {
2087
+ // Phase 3: after 200ms more, fade out to 0 and scale to 0.8 over 300ms
2088
+ focusIndicatorView.postDelayed(
2089
+ new Runnable() {
1927
2090
  @Override
1928
- public void onAnimationEnd(Animation animation) {
1929
- Log.d(
1930
- TAG,
1931
- "showFocusIndicator: Final animation ended, removing indicator"
1932
- );
1933
- // Remove the focus indicator
2091
+ public void run() {
1934
2092
  if (
1935
2093
  focusIndicatorView != null &&
1936
- previewContainer != null
2094
+ thisIndicatorView == focusIndicatorView &&
2095
+ thisAnimationId == focusIndicatorAnimationId
1937
2096
  ) {
1938
- previewContainer.removeView(focusIndicatorView);
1939
- focusIndicatorView = null;
2097
+ focusIndicatorView
2098
+ .animate()
2099
+ .alpha(0f)
2100
+ .scaleX(0.8f)
2101
+ .scaleY(0.8f)
2102
+ .setDuration(300)
2103
+ .setInterpolator(
2104
+ new android.view.animation.AccelerateInterpolator()
2105
+ )
2106
+ .withEndAction(
2107
+ new Runnable() {
2108
+ @Override
2109
+ public void run() {
2110
+ if (
2111
+ focusIndicatorView != null &&
2112
+ previewContainer != null &&
2113
+ thisIndicatorView == focusIndicatorView &&
2114
+ thisAnimationId ==
2115
+ focusIndicatorAnimationId
2116
+ ) {
2117
+ try {
2118
+ focusIndicatorView.clearAnimation();
2119
+ } catch (Exception ignore) {}
2120
+ previewContainer.removeView(
2121
+ focusIndicatorView
2122
+ );
2123
+ focusIndicatorView = null;
2124
+ }
2125
+ }
2126
+ }
2127
+ );
1940
2128
  }
1941
2129
  }
1942
-
1943
- @Override
1944
- public void onAnimationRepeat(Animation animation) {}
1945
- }
2130
+ },
2131
+ 200
1946
2132
  );
1947
-
1948
- focusIndicatorView.startAnimation(finalAnimation);
1949
2133
  }
1950
2134
  }
1951
-
1952
- @Override
1953
- public void onAnimationRepeat(Animation animation) {}
1954
- }
1955
- );
1956
-
1957
- focusIndicatorView.startAnimation(fadeToTransparent);
2135
+ );
1958
2136
  }
1959
2137
  }
1960
2138
  },
1961
- 800
1962
- ); // Optimal timing for smooth focus feedback
2139
+ 500
2140
+ );
1963
2141
  }
1964
2142
 
1965
2143
  public static List<Size> getSupportedPictureSizes(String facing) {
@@ -1985,89 +2163,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1985
2163
  return sizes;
1986
2164
  }
1987
2165
 
1988
- private void setZoomInternal(float zoomRatio) {
1989
- if (camera != null) {
1990
- try {
1991
- float minZoom = Objects.requireNonNull(
1992
- camera.getCameraInfo().getZoomState().getValue()
1993
- ).getMinZoomRatio();
1994
- float maxZoom = camera
1995
- .getCameraInfo()
1996
- .getZoomState()
1997
- .getValue()
1998
- .getMaxZoomRatio();
1999
- float currentZoom = camera
2000
- .getCameraInfo()
2001
- .getZoomState()
2002
- .getValue()
2003
- .getZoomRatio();
2004
-
2005
- Log.d(
2006
- TAG,
2007
- "setZoomInternal: Current camera range: " +
2008
- minZoom +
2009
- "-" +
2010
- maxZoom +
2011
- ", current: " +
2012
- currentZoom
2013
- );
2014
- Log.d(TAG, "setZoomInternal: Requesting zoom: " + zoomRatio);
2015
-
2016
- // Try to set zoom directly - let CameraX handle lens switching
2017
- ListenableFuture<Void> zoomFuture = camera
2018
- .getCameraControl()
2019
- .setZoomRatio(zoomRatio);
2020
-
2021
- zoomFuture.addListener(
2022
- () -> {
2023
- try {
2024
- zoomFuture.get(); // Check if zoom was successful
2025
- float newZoom = Objects.requireNonNull(
2026
- camera.getCameraInfo().getZoomState().getValue()
2027
- ).getZoomRatio();
2028
- Log.d(
2029
- TAG,
2030
- "setZoomInternal: Zoom set successfully to " +
2031
- newZoom +
2032
- " (requested: " +
2033
- zoomRatio +
2034
- ")"
2035
- );
2036
-
2037
- // Check if CameraX switched cameras
2038
- String newCameraId = getCameraId(camera.getCameraInfo());
2039
- if (!newCameraId.equals(currentDeviceId)) {
2040
- currentDeviceId = newCameraId;
2041
- Log.d(
2042
- TAG,
2043
- "setZoomInternal: CameraX switched to camera: " + newCameraId
2044
- );
2045
- }
2046
- } catch (Exception e) {
2047
- Log.w(
2048
- TAG,
2049
- "setZoomInternal: Zoom operation failed: " + e.getMessage()
2050
- );
2051
- // Fallback: clamp to current camera's range
2052
- float clampedZoom = Math.max(
2053
- minZoom,
2054
- Math.min(zoomRatio, maxZoom)
2055
- );
2056
- camera.getCameraControl().setZoomRatio(clampedZoom);
2057
- Log.d(
2058
- TAG,
2059
- "setZoomInternal: Fallback - clamped zoom to " + clampedZoom
2060
- );
2061
- }
2062
- },
2063
- mainExecutor
2064
- );
2065
- } catch (Exception e) {
2066
- Log.e(TAG, "setZoomInternal: Error setting zoom", e);
2067
- }
2068
- }
2069
- }
2070
-
2071
2166
  public static List<String> getSupportedFlashModesStatic() {
2072
2167
  try {
2073
2168
  // For static method, we can return common flash modes
@@ -2293,13 +2388,48 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2293
2388
  Float y,
2294
2389
  Runnable callback
2295
2390
  ) {
2391
+ Log.d(
2392
+ TAG,
2393
+ "======================== SET ASPECT RATIO ========================"
2394
+ );
2395
+ Log.d(
2396
+ TAG,
2397
+ "Input parameters - aspectRatio: " +
2398
+ aspectRatio +
2399
+ ", x: " +
2400
+ x +
2401
+ ", y: " +
2402
+ y
2403
+ );
2404
+
2296
2405
  if (sessionConfig == null) {
2406
+ Log.d(TAG, "SessionConfig is null, returning");
2297
2407
  if (callback != null) callback.run();
2298
2408
  return;
2299
2409
  }
2300
2410
 
2301
2411
  String currentAspectRatio = sessionConfig.getAspectRatio();
2302
2412
 
2413
+ // Get current display information
2414
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
2415
+ int screenWidthPx = metrics.widthPixels;
2416
+ int screenHeightPx = metrics.heightPixels;
2417
+ boolean isPortrait =
2418
+ context.getResources().getConfiguration().orientation ==
2419
+ Configuration.ORIENTATION_PORTRAIT;
2420
+
2421
+ Log.d(
2422
+ TAG,
2423
+ "Current screen: " +
2424
+ screenWidthPx +
2425
+ "x" +
2426
+ screenHeightPx +
2427
+ " (" +
2428
+ (isPortrait ? "PORTRAIT" : "LANDSCAPE") +
2429
+ ")"
2430
+ );
2431
+ Log.d(TAG, "Current aspect ratio: " + currentAspectRatio);
2432
+
2303
2433
  // Don't restart camera if aspect ratio hasn't changed and no position specified
2304
2434
  if (
2305
2435
  aspectRatio != null &&
@@ -2307,12 +2437,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2307
2437
  x == null &&
2308
2438
  y == null
2309
2439
  ) {
2310
- Log.d(
2311
- TAG,
2312
- "setAspectRatio: Aspect ratio " +
2313
- aspectRatio +
2314
- " is already set and no position specified, skipping"
2315
- );
2440
+ Log.d(TAG, "Aspect ratio unchanged and no position specified, skipping");
2316
2441
  if (callback != null) callback.run();
2317
2442
  return;
2318
2443
  }
@@ -2320,22 +2445,16 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2320
2445
  String currentGridMode = sessionConfig.getGridMode();
2321
2446
  Log.d(
2322
2447
  TAG,
2323
- "setAspectRatio: Changing from " +
2324
- currentAspectRatio +
2325
- " to " +
2326
- aspectRatio +
2327
- (x != null && y != null
2328
- ? " at position (" + x + ", " + y + ")"
2329
- : " with auto-centering") +
2330
- ", preserving grid mode: " +
2331
- currentGridMode
2448
+ "Changing aspect ratio from " + currentAspectRatio + " to " + aspectRatio
2332
2449
  );
2450
+ Log.d(TAG, "Auto-centering will be applied (matching iOS behavior)");
2333
2451
 
2452
+ // Match iOS behavior: when aspect ratio changes, always auto-center
2334
2453
  sessionConfig = new CameraSessionConfiguration(
2335
2454
  sessionConfig.getDeviceId(),
2336
2455
  sessionConfig.getPosition(),
2337
- sessionConfig.getX(),
2338
- sessionConfig.getY(),
2456
+ -1, // Force auto-center X (iOS: self.posX = -1)
2457
+ -1, // Force auto-center Y (iOS: self.posY = -1)
2339
2458
  sessionConfig.getWidth(),
2340
2459
  sessionConfig.getHeight(),
2341
2460
  sessionConfig.getPaddingBottom(),
@@ -2349,12 +2468,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2349
2468
  aspectRatio,
2350
2469
  currentGridMode
2351
2470
  );
2471
+ sessionConfig.setCentered(true);
2352
2472
 
2353
2473
  // Update layout and rebind camera with new aspect ratio
2354
2474
  if (isRunning && previewContainer != null) {
2355
2475
  mainExecutor.execute(() -> {
2356
- // First update the UI layout
2357
- updatePreviewLayoutForAspectRatio(aspectRatio, x, y);
2476
+ // First update the UI layout - always pass null for x,y to force auto-centering (matching iOS)
2477
+ updatePreviewLayoutForAspectRatio(aspectRatio, null, null);
2358
2478
 
2359
2479
  // Then rebind the camera with new aspect ratio configuration
2360
2480
  Log.d(
@@ -2384,8 +2504,121 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2384
2504
  previewContainer.post(callback);
2385
2505
  }
2386
2506
  }
2507
+
2508
+ Log.d(
2509
+ TAG,
2510
+ "=================================================================="
2511
+ );
2387
2512
  });
2388
2513
  } else {
2514
+ Log.d(TAG, "Camera not running, just saving configuration");
2515
+ Log.d(
2516
+ TAG,
2517
+ "=================================================================="
2518
+ );
2519
+ if (callback != null) callback.run();
2520
+ }
2521
+ }
2522
+
2523
+ // Force aspect ratio recalculation (used during orientation changes)
2524
+ public void forceAspectRatioRecalculation(
2525
+ String aspectRatio,
2526
+ Float x,
2527
+ Float y,
2528
+ Runnable callback
2529
+ ) {
2530
+ Log.d(
2531
+ TAG,
2532
+ "======================== FORCE ASPECT RATIO RECALCULATION ========================"
2533
+ );
2534
+ Log.d(
2535
+ TAG,
2536
+ "Input parameters - aspectRatio: " +
2537
+ aspectRatio +
2538
+ ", x: " +
2539
+ x +
2540
+ ", y: " +
2541
+ y
2542
+ );
2543
+
2544
+ if (sessionConfig == null) {
2545
+ Log.d(TAG, "SessionConfig is null, returning");
2546
+ if (callback != null) callback.run();
2547
+ return;
2548
+ }
2549
+
2550
+ String currentGridMode = sessionConfig.getGridMode();
2551
+ Log.d(TAG, "Forcing aspect ratio recalculation for: " + aspectRatio);
2552
+ Log.d(TAG, "Auto-centering will be applied (matching iOS behavior)");
2553
+
2554
+ // Match iOS behavior: when aspect ratio changes, always auto-center
2555
+ sessionConfig = new CameraSessionConfiguration(
2556
+ sessionConfig.getDeviceId(),
2557
+ sessionConfig.getPosition(),
2558
+ -1, // Force auto-center X (iOS: self.posX = -1)
2559
+ -1, // Force auto-center Y (iOS: self.posY = -1)
2560
+ sessionConfig.getWidth(),
2561
+ sessionConfig.getHeight(),
2562
+ sessionConfig.getPaddingBottom(),
2563
+ sessionConfig.getToBack(),
2564
+ sessionConfig.getStoreToFile(),
2565
+ sessionConfig.getEnableOpacity(),
2566
+ sessionConfig.getEnableZoom(),
2567
+ sessionConfig.getDisableExifHeaderStripping(),
2568
+ sessionConfig.getDisableAudio(),
2569
+ sessionConfig.getZoomFactor(),
2570
+ aspectRatio,
2571
+ currentGridMode
2572
+ );
2573
+ sessionConfig.setCentered(true);
2574
+
2575
+ // Update layout and rebind camera with new aspect ratio
2576
+ if (isRunning && previewContainer != null) {
2577
+ mainExecutor.execute(() -> {
2578
+ // First update the UI layout - always pass null for x,y to force auto-centering (matching iOS)
2579
+ updatePreviewLayoutForAspectRatio(aspectRatio, null, null);
2580
+
2581
+ // Then rebind the camera with new aspect ratio configuration
2582
+ Log.d(
2583
+ TAG,
2584
+ "forceAspectRatioRecalculation: Rebinding camera with aspect ratio: " +
2585
+ aspectRatio
2586
+ );
2587
+ bindCameraUseCases();
2588
+
2589
+ // Preserve grid mode and wait for completion
2590
+ if (gridOverlayView != null) {
2591
+ gridOverlayView.post(() -> {
2592
+ Log.d(
2593
+ TAG,
2594
+ "forceAspectRatioRecalculation: Re-applying grid mode: " +
2595
+ currentGridMode
2596
+ );
2597
+ gridOverlayView.setGridMode(currentGridMode);
2598
+
2599
+ // Wait one more frame for grid to be applied, then call callback
2600
+ if (callback != null) {
2601
+ gridOverlayView.post(callback);
2602
+ }
2603
+ });
2604
+ } else {
2605
+ // No grid overlay, wait one frame for layout completion then call callback
2606
+ if (callback != null) {
2607
+ previewContainer.post(callback);
2608
+ }
2609
+ }
2610
+
2611
+ Log.d(
2612
+ TAG,
2613
+ "=================================================================="
2614
+ );
2615
+ });
2616
+ } else {
2617
+ Log.d(TAG, "Camera not running, just saving configuration");
2618
+ Log.d(
2619
+ TAG,
2620
+ "=================================================================="
2621
+ );
2389
2622
  if (callback != null) callback.run();
2390
2623
  }
2391
2624
  }
@@ -2496,15 +2729,105 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2496
2729
  return new Rect(0, 0, containerWidth, containerHeight);
2497
2730
  }
2498
2731
 
2499
- // The preview is rotated 90 degrees for portrait mode
2500
- // So we swap the dimensions
2501
- int cameraWidth = currentPreviewResolution.getHeight();
2502
- int cameraHeight = currentPreviewResolution.getWidth();
2732
+ // CameraX delivers preview in sensor orientation (always landscape)
2733
+ // But PreviewView internally rotates it to match device orientation
2734
+ // So we need to swap dimensions in portrait mode
2735
+ int cameraWidth = currentPreviewResolution.getWidth();
2736
+ int cameraHeight = currentPreviewResolution.getHeight();
2737
+
2738
+ // Check if we're in portrait mode
2739
+ boolean isPortrait =
2740
+ context.getResources().getConfiguration().orientation ==
2741
+ Configuration.ORIENTATION_PORTRAIT;
2742
+
2743
+ // Swap dimensions if in portrait mode to match how PreviewView displays it
2744
+ if (isPortrait) {
2745
+ int temp = cameraWidth;
2746
+ cameraWidth = cameraHeight;
2747
+ cameraHeight = temp;
2748
+ }
2749
+
2750
+ // When we have an aspect ratio set, we use FILL_CENTER which scales to fill
2751
+ // the container while maintaining aspect ratio, potentially cropping
2752
+ boolean usesFillCenter =
2753
+ sessionConfig != null && sessionConfig.getAspectRatio() != null;
2754
+
2755
+ // For FILL_CENTER with aspect ratio, we need to calculate the actual visible bounds
2756
+ // The preview might extend beyond the container bounds and get clipped
2757
+ if (usesFillCenter) {
2758
+ // Calculate how the camera preview is scaled to fill the container
2759
+ float widthScale = (float) containerWidth / cameraWidth;
2760
+ float heightScale = (float) containerHeight / cameraHeight;
2761
+ float scale = Math.max(widthScale, heightScale); // max for FILL_CENTER
2762
+
2763
+ // Calculate the scaled dimensions
2764
+ int scaledWidth = Math.round(cameraWidth * scale);
2765
+ int scaledHeight = Math.round(cameraHeight * scale);
2766
+
2767
+ // Calculate how much is clipped on each side
2768
+ int excessWidth = Math.max(0, scaledWidth - containerWidth);
2769
+ int excessHeight = Math.max(0, scaledHeight - containerHeight);
2770
+
2771
+ // For the actual visible bounds, we need to account for potential
2772
+ // internal misalignment of PreviewView's SurfaceView
2773
+ int adjustedWidth = containerWidth;
2774
+ int adjustedHeight = containerHeight;
2775
+
2776
+ // Apply small adjustments for 4:3 ratio to prevent blue line
2777
+ // This compensates for PreviewView's internal SurfaceView misalignment
2778
+ String aspectRatio = sessionConfig != null
2779
+ ? sessionConfig.getAspectRatio()
2780
+ : null;
2781
+ if ("4:3".equals(aspectRatio)) {
2782
+ // For 4:3, reduce the reported width slightly to account for
2783
+ // the SurfaceView drawing outside its bounds
2784
+ adjustedWidth = containerWidth - 2;
2785
+ adjustedHeight = containerHeight - 2;
2786
+ }
2787
+
2788
+ Log.d(
2789
+ TAG,
2790
+ "getActualCameraBounds FILL_CENTER: container=" +
2791
+ containerWidth +
2792
+ "x" +
2793
+ containerHeight +
2794
+ ", camera=" +
2795
+ cameraWidth +
2796
+ "x" +
2797
+ cameraHeight +
2798
+ " (portrait=" +
2799
+ isPortrait +
2800
+ ")" +
2801
+ ", scale=" +
2802
+ scale +
2803
+ ", scaled=" +
2804
+ scaledWidth +
2805
+ "x" +
2806
+ scaledHeight +
2807
+ ", excess=" +
2808
+ excessWidth +
2809
+ "x" +
2810
+ excessHeight +
2811
+ ", adjusted=" +
2812
+ adjustedWidth +
2813
+ "x" +
2814
+ adjustedHeight +
2815
+ ", ratio=" +
2816
+ aspectRatio
2817
+ );
2818
+
2819
+ // Return slightly inset bounds for 4:3 to avoid blue line
2820
+ if ("4:3".equals(aspectRatio)) {
2821
+ return new Rect(1, 1, adjustedWidth + 1, adjustedHeight + 1);
2822
+ } else {
2823
+ return new Rect(0, 0, containerWidth, containerHeight);
2824
+ }
2825
+ }
2503
2826
 
2504
- // Calculate the scaling factor to fit the camera in the container
2827
+ // For FIT_CENTER (no aspect ratio), calculate letterboxing
2505
2828
  float widthScale = (float) containerWidth / cameraWidth;
2506
2829
  float heightScale = (float) containerHeight / cameraHeight;
2507
- float scale = Math.min(widthScale, heightScale); // FIT_CENTER uses min scale
2830
+ float scale = Math.min(widthScale, heightScale);
2508
2831
 
2509
2832
  // Calculate the actual size of the camera content after scaling
2510
2833
  int scaledWidth = Math.round(cameraWidth * scale);
@@ -2516,7 +2839,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2516
2839
 
2517
2840
  Log.d(
2518
2841
  TAG,
2519
- "getActualCameraBounds: container=" +
2842
+ "getActualCameraBounds FIT_CENTER: container=" +
2520
2843
  containerWidth +
2521
2844
  "x" +
2522
2845
  containerHeight +
@@ -2524,6 +2847,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2524
2847
  cameraWidth +
2525
2848
  "x" +
2526
2849
  cameraHeight +
2850
+ " (swapped=" +
2851
+ isPortrait +
2852
+ ")" +
2527
2853
  ", scale=" +
2528
2854
  scale +
2529
2855
  ", scaled=" +
@@ -2539,10 +2865,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2539
2865
 
2540
2866
  // Return the bounds relative to the container
2541
2867
  return new Rect(
2542
- offsetX,
2543
- offsetY,
2544
- offsetX + scaledWidth,
2545
- offsetY + scaledHeight
2868
+ Math.max(0, offsetX),
2869
+ Math.max(0, offsetY),
2870
+ Math.min(containerWidth, offsetX + scaledWidth),
2871
+ Math.min(containerHeight, offsetY + scaledHeight)
2546
2872
  );
2547
2873
  }
2548
2874
 
@@ -2774,27 +3100,155 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2774
3100
  ) {
2775
3101
  if (previewContainer == null || aspectRatio == null) return;
2776
3102
 
2777
- // Parse aspect ratio
3103
+ Log.d(
3104
+ TAG,
3105
+ "======================== UPDATE PREVIEW LAYOUT FOR ASPECT RATIO ========================"
3106
+ );
3107
+ Log.d(
3108
+ TAG,
3109
+ "Input parameters - aspectRatio: " +
3110
+ aspectRatio +
3111
+ ", x: " +
3112
+ x +
3113
+ ", y: " +
3114
+ y
3115
+ );
3116
+
3117
+ // Get comprehensive display information
3118
+ WindowManager windowManager = (WindowManager) this.context.getSystemService(
3119
+ Context.WINDOW_SERVICE
3120
+ );
3121
+ int screenWidthPx, screenHeightPx;
3122
+ float density;
3123
+
3124
+ // Get density using DisplayMetrics (available on all API levels)
3125
+ DisplayMetrics displayMetrics = new DisplayMetrics();
3126
+ windowManager.getDefaultDisplay().getMetrics(displayMetrics);
3127
+ density = displayMetrics.density;
3128
+
3129
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
3130
+ // API 30+ (Android 11+) - use WindowMetrics for screen dimensions
3131
+ WindowMetrics metrics = windowManager.getCurrentWindowMetrics();
3132
+ Rect bounds = metrics.getBounds();
3133
+ screenWidthPx = bounds.width();
3134
+ screenHeightPx = bounds.height();
3135
+ } else {
3136
+ // API < 30 - use legacy DisplayMetrics for screen dimensions
3137
+ screenWidthPx = displayMetrics.widthPixels;
3138
+ screenHeightPx = displayMetrics.heightPixels;
3139
+ }
3140
+
3141
+ // Get WebView dimensions
3142
+ int webViewWidth = webView.getWidth();
3143
+ int webViewHeight = webView.getHeight();
3144
+
3145
+ // Get current preview container info
3146
+ ViewGroup.LayoutParams currentParams = previewContainer.getLayoutParams();
3147
+ int currentWidth = currentParams != null ? currentParams.width : 0;
3148
+ int currentHeight = currentParams != null ? currentParams.height : 0;
3149
+ int currentX = 0;
3150
+ int currentY = 0;
3151
+ if (currentParams instanceof ViewGroup.MarginLayoutParams) {
3152
+ ViewGroup.MarginLayoutParams marginParams =
3153
+ (ViewGroup.MarginLayoutParams) currentParams;
3154
+ currentX = marginParams.leftMargin;
3155
+ currentY = marginParams.topMargin;
3156
+ }
3157
+
3158
+ Log.d(
3159
+ TAG,
3160
+ "Screen dimensions: " +
3161
+ screenWidthPx +
3162
+ "x" +
3163
+ screenHeightPx +
3164
+ " pixels, density: " +
3165
+ density
3166
+ );
3167
+ Log.d(TAG, "WebView dimensions: " + webViewWidth + "x" + webViewHeight);
3168
+ Log.d(
3169
+ TAG,
3170
+ "Current preview position: " +
3171
+ currentX +
3172
+ "," +
3173
+ currentY +
3174
+ " size: " +
3175
+ currentWidth +
3176
+ "x" +
3177
+ currentHeight
3178
+ );
3179
+
3180
+ // Parse aspect ratio as width:height (e.g., 4:3 -> r=4/3)
2778
3181
  String[] ratios = aspectRatio.split(":");
2779
- if (ratios.length != 2) return;
3182
+ if (ratios.length != 2) {
3183
+ Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio);
3184
+ return;
3185
+ }
2780
3186
 
2781
3187
  try {
2782
- // For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
2783
- float ratio = Float.parseFloat(ratios[1]) / Float.parseFloat(ratios[0]);
3188
+ // Match iOS logic exactly
3189
+ double ratioWidth = Double.parseDouble(ratios[0]);
3190
+ double ratioHeight = Double.parseDouble(ratios[1]);
3191
+ boolean isPortrait =
3192
+ context.getResources().getConfiguration().orientation ==
3193
+ Configuration.ORIENTATION_PORTRAIT;
3194
+
3195
+ Log.d(
3196
+ TAG,
3197
+ "Aspect ratio parsing - Original: " +
3198
+ aspectRatio +
3199
+ " (width=" +
3200
+ ratioWidth +
3201
+ ", height=" +
3202
+ ratioHeight +
3203
+ ")"
3204
+ );
3205
+ Log.d(
3206
+ TAG,
3207
+ "Device orientation: " + (isPortrait ? "PORTRAIT" : "LANDSCAPE")
3208
+ );
3209
+
3210
+ // iOS: let ratio = !isPortrait ? ratioParts[0] / ratioParts[1] : ratioParts[1] / ratioParts[0]
3211
+ double ratio = !isPortrait
3212
+ ? (ratioWidth / ratioHeight)
3213
+ : (ratioHeight / ratioWidth);
3214
+
3215
+ Log.d(
3216
+ TAG,
3217
+ "Computed ratio: " +
3218
+ ratio +
3219
+ " (iOS formula: " +
3220
+ (!isPortrait ? "width/height" : "height/width") +
3221
+ ")"
3222
+ );
2784
3223
 
2785
3224
  // Get available space from webview dimensions
2786
- int availableWidth = webView.getWidth();
2787
- int availableHeight = webView.getHeight();
3225
+ int availableWidth = webViewWidth;
3226
+ int availableHeight = webViewHeight;
3227
+
3228
+ Log.d(
3229
+ TAG,
3230
+ "Available space from WebView: " +
3231
+ availableWidth +
3232
+ "x" +
3233
+ availableHeight
3234
+ );
2788
3235
 
2789
3236
  // Calculate position and size
2790
3237
  int finalX, finalY, finalWidth, finalHeight;
2791
3238
 
2792
3239
  if (x != null && y != null) {
2793
- // Account for WebView insets from edge-to-edge support
3240
+ // Manual positioning mode
2794
3241
  int webViewTopInset = getWebViewTopInset();
2795
3242
  int webViewLeftInset = getWebViewLeftInset();
2796
3243
 
2797
- // Use provided coordinates with boundary checking, adjusted for insets
3244
+ Log.d(
3245
+ TAG,
3246
+ "Manual positioning mode - WebView insets: left=" +
3247
+ webViewLeftInset +
3248
+ ", top=" +
3249
+ webViewTopInset
3250
+ );
3251
+
2798
3252
  finalX = Math.max(
2799
3253
  0,
2800
3254
  Math.min(x.intValue() + webViewLeftInset, availableWidth)
@@ -2808,6 +3262,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2808
3262
  int maxWidth = availableWidth - finalX;
2809
3263
  int maxHeight = availableHeight - finalY;
2810
3264
 
3265
+ Log.d(
3266
+ TAG,
3267
+ "Max available space from position: " + maxWidth + "x" + maxHeight
3268
+ );
3269
+
2811
3270
  // Calculate optimal size while maintaining aspect ratio within available space
2812
3271
  finalWidth = maxWidth;
2813
3272
  finalHeight = (int) (maxWidth / ratio);
@@ -2816,76 +3275,147 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2816
3275
  // Height constraint is tighter, fit by height
2817
3276
  finalHeight = maxHeight;
2818
3277
  finalWidth = (int) (maxHeight * ratio);
3278
+ Log.d(TAG, "Height-constrained sizing");
3279
+ } else {
3280
+ Log.d(TAG, "Width-constrained sizing");
2819
3281
  }
2820
3282
 
2821
3283
  // Ensure final position stays within bounds
2822
3284
  finalX = Math.max(0, Math.min(finalX, availableWidth - finalWidth));
2823
3285
  finalY = Math.max(0, Math.min(finalY, availableHeight - finalHeight));
2824
3286
  } else {
2825
- // Auto-center the view
2826
- // Use full available space to match iOS behavior
2827
- int maxAvailableWidth = availableWidth;
2828
- int maxAvailableHeight = availableHeight;
2829
-
2830
- // Start with width-based calculation
2831
- finalWidth = maxAvailableWidth;
2832
- finalHeight = (int) (finalWidth / ratio);
2833
-
2834
- // If height exceeds available space, use height-based calculation
2835
- if (finalHeight > maxAvailableHeight) {
2836
- finalHeight = maxAvailableHeight;
2837
- finalWidth = (int) (finalHeight * ratio);
3287
+ // Auto-center mode - match iOS behavior exactly
3288
+ Log.d(TAG, "Auto-center mode");
3289
+
3290
+ // Calculate maximum size that fits the aspect ratio in available space
3291
+ double maxWidthByHeight = availableHeight * ratio;
3292
+ double maxHeightByWidth = availableWidth / ratio;
3293
+
3294
+ Log.d(
3295
+ TAG,
3296
+ "Aspect ratio calculations - maxWidthByHeight: " +
3297
+ maxWidthByHeight +
3298
+ ", maxHeightByWidth: " +
3299
+ maxHeightByWidth
3300
+ );
3301
+
3302
+ if (maxWidthByHeight <= availableWidth) {
3303
+ // Height is the limiting factor
3304
+ finalWidth = (int) maxWidthByHeight;
3305
+ finalHeight = availableHeight;
3306
+ Log.d(
3307
+ TAG,
3308
+ "Height-limited sizing: " + finalWidth + "x" + finalHeight
3309
+ );
3310
+ } else {
3311
+ // Width is the limiting factor
3312
+ finalWidth = availableWidth;
3313
+ finalHeight = (int) maxHeightByWidth;
3314
+ Log.d(TAG, "Width-limited sizing: " + finalWidth + "x" + finalHeight);
2838
3315
  }
2839
3316
 
2840
- // Center the view
3317
+ // Center the preview
2841
3318
  finalX = (availableWidth - finalWidth) / 2;
2842
3319
  finalY = (availableHeight - finalHeight) / 2;
2843
3320
 
2844
3321
  Log.d(
2845
3322
  TAG,
2846
- "updatePreviewLayoutForAspectRatio: Auto-center mode - ratio=" +
2847
- ratio +
2848
- ", calculated size=" +
3323
+ "Auto-center mode: calculated size " +
2849
3324
  finalWidth +
2850
3325
  "x" +
2851
3326
  finalHeight +
2852
- ", available=" +
2853
- availableWidth +
2854
- "x" +
2855
- availableHeight
3327
+ " at position (" +
3328
+ finalX +
3329
+ ", " +
3330
+ finalY +
3331
+ ")"
2856
3332
  );
2857
3333
  }
2858
3334
 
3335
+ Log.d(
3336
+ TAG,
3337
+ "Final calculated layout - Position: (" +
3338
+ finalX +
3339
+ "," +
3340
+ finalY +
3341
+ "), Size: " +
3342
+ finalWidth +
3343
+ "x" +
3344
+ finalHeight
3345
+ );
3346
+
3347
+ // Calculate and log the actual displayed aspect ratio
3348
+ double displayedRatio = (double) finalWidth / (double) finalHeight;
3349
+ Log.d(
3350
+ TAG,
3351
+ "Displayed aspect ratio: " +
3352
+ displayedRatio +
3353
+ " (width=" +
3354
+ finalWidth +
3355
+ ", height=" +
3356
+ finalHeight +
3357
+ ")"
3358
+ );
3359
+
3360
+ // Compare with expected ratio based on orientation
3361
+ if (aspectRatio != null) {
3362
+ String[] parts = aspectRatio.split(":");
3363
+ if (parts.length == 2) {
3364
+ double expectedDisplayRatio = isPortrait
3365
+ ? (ratioHeight / ratioWidth)
3366
+ : (ratioWidth / ratioHeight);
3367
+ double difference = Math.abs(displayedRatio - expectedDisplayRatio);
3368
+ Log.d(
3369
+ TAG,
3370
+ "Display ratio check - Expected: " +
3371
+ expectedDisplayRatio +
3372
+ ", Actual: " +
3373
+ displayedRatio +
3374
+ ", Difference: " +
3375
+ difference +
3376
+ " (tolerance should be < 0.01)"
3377
+ );
3378
+ }
3379
+ }
3380
+
2859
3381
  // Update layout params
2860
- ViewGroup.LayoutParams currentParams = previewContainer.getLayoutParams();
2861
- if (currentParams instanceof ViewGroup.MarginLayoutParams) {
3382
+ ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
3383
+ if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
2862
3384
  ViewGroup.MarginLayoutParams params =
2863
- (ViewGroup.MarginLayoutParams) currentParams;
3385
+ (ViewGroup.MarginLayoutParams) layoutParams;
2864
3386
  params.width = finalWidth;
2865
3387
  params.height = finalHeight;
2866
3388
  params.leftMargin = finalX;
2867
3389
  params.topMargin = finalY;
2868
3390
  previewContainer.setLayoutParams(params);
2869
3391
  previewContainer.requestLayout();
2870
- Log.d(
2871
- TAG,
2872
- "updatePreviewLayoutForAspectRatio: Updated to " +
2873
- finalWidth +
2874
- "x" +
2875
- finalHeight +
2876
- " at (" +
2877
- finalX +
2878
- "," +
2879
- finalY +
2880
- ")"
2881
- );
3392
+
3393
+ Log.d(TAG, "Layout params applied successfully");
2882
3394
 
2883
3395
  // Update grid overlay bounds after aspect ratio change
2884
- previewContainer.post(() -> updateGridOverlayBounds());
3396
+ previewContainer.post(() -> {
3397
+ Log.d(
3398
+ TAG,
3399
+ "Post-layout verification - Actual position: " +
3400
+ previewContainer.getLeft() +
3401
+ "," +
3402
+ previewContainer.getTop() +
3403
+ ", Actual size: " +
3404
+ previewContainer.getWidth() +
3405
+ "x" +
3406
+ previewContainer.getHeight()
3407
+ );
3408
+ updateGridOverlayBounds();
3409
+ });
2885
3410
  }
2886
3411
  } catch (NumberFormatException e) {
2887
3412
  Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio, e);
2888
3413
  }
3414
+
3415
+ Log.d(
3416
+ TAG,
3417
+ "========================================================================================"
3418
+ );
2889
3419
  }
2890
3420
 
2891
3421
  private int getWebViewTopInset() {
@@ -2938,10 +3468,51 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2938
3468
  int webViewTopInset = getWebViewTopInset();
2939
3469
  int webViewLeftInset = getWebViewLeftInset();
2940
3470
 
2941
- int x = Math.max(0, (int) ((actualX - webViewLeftInset) / pixelRatio));
2942
- int y = Math.max(0, (int) ((actualY - webViewTopInset) / pixelRatio));
2943
- int width = (int) (actualWidth / pixelRatio);
2944
- int height = (int) (actualHeight / pixelRatio);
3471
+ // Use proper rounding strategy to avoid gaps:
3472
+ // - For positions (x, y): floor to avoid gaps at top/left
3473
+ // - For dimensions (width, height): ceil to avoid gaps at bottom/right
3474
+ int x = Math.max(
3475
+ 0,
3476
+ (int) Math.ceil((actualX - webViewLeftInset) / pixelRatio)
3477
+ );
3478
+ int y = Math.max(
3479
+ 0,
3480
+ (int) Math.ceil((actualY - webViewTopInset) / pixelRatio)
3481
+ );
3482
+ int width = (int) Math.floor(actualWidth / pixelRatio);
3483
+ int height = (int) Math.floor(actualHeight / pixelRatio);
3484
+
3485
+ // Debug logging to understand the blue line issue
3486
+ Log.d(
3487
+ TAG,
3488
+ "getCurrentPreviewBounds DEBUG: " +
3489
+ "actualBounds=(" +
3490
+ actualX +
3491
+ "," +
3492
+ actualY +
3493
+ "," +
3494
+ actualWidth +
3495
+ "x" +
3496
+ actualHeight +
3497
+ "), " +
3498
+ "logicalBounds=(" +
3499
+ x +
3500
+ "," +
3501
+ y +
3502
+ "," +
3503
+ width +
3504
+ "x" +
3505
+ height +
3506
+ "), " +
3507
+ "pixelRatio=" +
3508
+ pixelRatio +
3509
+ ", " +
3510
+ "insets=(" +
3511
+ webViewLeftInset +
3512
+ "," +
3513
+ webViewTopInset +
3514
+ ")"
3515
+ );
2945
3516
 
2946
3517
  return new int[] { x, y, width, height };
2947
3518
  }
@@ -2987,12 +3558,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2987
3558
  MeteringPointFactory factory = previewView.getMeteringPointFactory();
2988
3559
  MeteringPoint point = factory.createPoint(viewWidth / 2f, viewHeight / 2f);
2989
3560
 
2990
- // Create focus and metering action
3561
+ // Create focus and metering action (persistent, no auto-cancel) to match iOS behavior
2991
3562
  FocusMeteringAction action = new FocusMeteringAction.Builder(
2992
3563
  point,
2993
3564
  FocusMeteringAction.FLAG_AF | FocusMeteringAction.FLAG_AE
2994
3565
  )
2995
- .setAutoCancelDuration(3, TimeUnit.SECONDS) // Auto-cancel after 3 seconds
3566
+ .disableAutoCancel()
2996
3567
  .build();
2997
3568
 
2998
3569
  try {