@capgo/camera-preview 7.4.0-alpha.19 → 7.4.0-alpha.22

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,7 @@
1
1
  package com.ahm.capacitor.camera.preview;
2
2
 
3
3
  import android.content.Context;
4
+ import android.content.res.Configuration;
4
5
  import android.graphics.Bitmap;
5
6
  import android.graphics.BitmapFactory;
6
7
  import android.graphics.Color;
@@ -20,14 +21,7 @@ import android.util.Size;
20
21
  import android.view.MotionEvent;
21
22
  import android.view.View;
22
23
  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
24
  import android.webkit.WebView;
29
- import android.webkit.WebView;
30
- import android.widget.FrameLayout;
31
25
  import android.widget.FrameLayout;
32
26
  import androidx.annotation.NonNull;
33
27
  import androidx.annotation.OptIn;
@@ -78,7 +72,6 @@ import java.util.Set;
78
72
  import java.util.concurrent.Executor;
79
73
  import java.util.concurrent.ExecutorService;
80
74
  import java.util.concurrent.Executors;
81
- import java.util.concurrent.TimeUnit;
82
75
  import org.json.JSONObject;
83
76
 
84
77
  public class CameraXView implements LifecycleOwner, LifecycleObserver {
@@ -142,6 +135,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
142
135
  return isRunning;
143
136
  }
144
137
 
138
+ public View getPreviewContainer() {
139
+ return previewContainer;
140
+ }
141
+
145
142
  private void saveImageToGallery(byte[] data) {
146
143
  try {
147
144
  // Detect image format from byte array header
@@ -275,9 +272,24 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
275
272
  previewContainer.setClickable(true);
276
273
  previewContainer.setFocusable(true);
277
274
 
275
+ // Disable any potential drawing artifacts that might cause 1px offset
276
+ previewContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
277
+
278
+ // Ensure no clip bounds that might cause visual offset
279
+ previewContainer.setClipChildren(false);
280
+ previewContainer.setClipToPadding(false);
281
+
278
282
  // Create and setup the preview view
279
283
  previewView = new PreviewView(context);
280
- previewView.setScaleType(PreviewView.ScaleType.FIT_CENTER);
284
+ // Match iOS behavior: FIT when no aspect ratio, FILL when aspect ratio is set
285
+ String initialAspectRatio = sessionConfig != null
286
+ ? sessionConfig.getAspectRatio()
287
+ : null;
288
+ previewView.setScaleType(
289
+ (initialAspectRatio == null || initialAspectRatio.isEmpty())
290
+ ? PreviewView.ScaleType.FIT_CENTER
291
+ : PreviewView.ScaleType.FILL_CENTER
292
+ );
281
293
  // Also make preview view touchable as backup
282
294
  previewView.setClickable(true);
283
295
  previewView.setFocusable(true);
@@ -434,9 +446,45 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
434
446
  int height = sessionConfig.getHeight();
435
447
  String aspectRatio = sessionConfig.getAspectRatio();
436
448
 
449
+ // Get comprehensive display information
450
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
451
+ int screenWidthPx = metrics.widthPixels;
452
+ int screenHeightPx = metrics.heightPixels;
453
+ float density = metrics.density;
454
+ int screenWidthDp = (int) (screenWidthPx / density);
455
+ int screenHeightDp = (int) (screenHeightPx / density);
456
+
457
+ // Get WebView dimensions
458
+ int webViewWidth = webView != null ? webView.getWidth() : 0;
459
+ int webViewHeight = webView != null ? webView.getHeight() : 0;
460
+
461
+ // Get parent dimensions
462
+ ViewGroup parent = (ViewGroup) webView.getParent();
463
+ int parentWidth = parent != null ? parent.getWidth() : 0;
464
+ int parentHeight = parent != null ? parent.getHeight() : 0;
465
+
437
466
  Log.d(
438
467
  TAG,
439
- "calculatePreviewLayoutParams: Using sessionConfig values - x:" +
468
+ "======================== CALCULATE PREVIEW LAYOUT PARAMS ========================"
469
+ );
470
+ Log.d(
471
+ TAG,
472
+ "Screen dimensions - Pixels: " +
473
+ screenWidthPx +
474
+ "x" +
475
+ screenHeightPx +
476
+ ", DP: " +
477
+ screenWidthDp +
478
+ "x" +
479
+ screenHeightDp +
480
+ ", Density: " +
481
+ density
482
+ );
483
+ Log.d(TAG, "WebView dimensions: " + webViewWidth + "x" + webViewHeight);
484
+ Log.d(TAG, "Parent dimensions: " + parentWidth + "x" + parentHeight);
485
+ Log.d(
486
+ TAG,
487
+ "SessionConfig values - x:" +
440
488
  x +
441
489
  " y:" +
442
490
  y +
@@ -445,83 +493,97 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
445
493
  " height:" +
446
494
  height +
447
495
  " aspectRatio:" +
448
- aspectRatio
496
+ aspectRatio +
497
+ " isCentered:" +
498
+ sessionConfig.isCentered()
449
499
  );
450
500
 
451
- // Apply aspect ratio if specified and no explicit size was given
452
- if (aspectRatio != null && !aspectRatio.isEmpty()) {
501
+ // Apply aspect ratio if specified
502
+ if (
503
+ aspectRatio != null &&
504
+ !aspectRatio.isEmpty() &&
505
+ sessionConfig.isCentered()
506
+ ) {
453
507
  String[] ratios = aspectRatio.split(":");
454
508
  if (ratios.length == 2) {
455
509
  try {
456
- // For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
457
- float ratio =
458
- Float.parseFloat(ratios[1]) / Float.parseFloat(ratios[0]);
459
-
460
- // Calculate optimal size while maintaining aspect ratio
461
- int optimalWidth = width;
462
- int optimalHeight = (int) (width / ratio);
463
-
464
- if (optimalHeight > height) {
465
- // Height constraint is tighter, fit by height
466
- optimalHeight = height;
467
- optimalWidth = (int) (height * ratio);
468
- }
510
+ // Match iOS logic exactly
511
+ double ratioWidth = Double.parseDouble(ratios[0]);
512
+ double ratioHeight = Double.parseDouble(ratios[1]);
513
+ boolean isPortrait =
514
+ context.getResources().getConfiguration().orientation ==
515
+ Configuration.ORIENTATION_PORTRAIT;
469
516
 
470
- // Store the old dimensions to check if we need to recenter
471
- int oldWidth = width;
472
- int oldHeight = height;
473
- width = optimalWidth;
474
- height = optimalHeight;
517
+ Log.d(
518
+ TAG,
519
+ "Aspect ratio parsing - Original: " +
520
+ aspectRatio +
521
+ " (width=" +
522
+ ratioWidth +
523
+ ", height=" +
524
+ ratioHeight +
525
+ ")"
526
+ );
527
+ Log.d(
528
+ TAG,
529
+ "Device orientation: " + (isPortrait ? "PORTRAIT" : "LANDSCAPE")
530
+ );
475
531
 
476
- // If we're centered and dimensions changed, recalculate position
477
- if (sessionConfig.isCentered()) {
478
- DisplayMetrics metrics = context.getResources().getDisplayMetrics();
532
+ // iOS: let ratio = !isPortrait ? ratioParts[0] / ratioParts[1] : ratioParts[1] / ratioParts[0]
533
+ double ratio = !isPortrait
534
+ ? (ratioWidth / ratioHeight)
535
+ : (ratioHeight / ratioWidth);
479
536
 
480
- if (width != oldWidth) {
481
- int screenWidth = metrics.widthPixels;
482
- x = (screenWidth - width) / 2;
483
- Log.d(
484
- TAG,
485
- "calculatePreviewLayoutParams: Recentered X after aspect ratio - " +
486
- "oldWidth=" +
487
- oldWidth +
488
- ", newWidth=" +
489
- width +
490
- ", screenWidth=" +
491
- screenWidth +
492
- ", newX=" +
493
- x
494
- );
495
- }
537
+ Log.d(
538
+ TAG,
539
+ "Computed ratio: " +
540
+ ratio +
541
+ " (iOS formula: " +
542
+ (!isPortrait ? "width/height" : "height/width") +
543
+ ")"
544
+ );
496
545
 
497
- if (height != oldHeight) {
498
- int screenHeight = metrics.heightPixels;
499
- // Always center based on full screen height
500
- y = (screenHeight - height) / 2;
501
- Log.d(
502
- TAG,
503
- "calculatePreviewLayoutParams: Recentered Y after aspect ratio - " +
504
- "oldHeight=" +
505
- oldHeight +
506
- ", newHeight=" +
507
- height +
508
- ", screenHeight=" +
509
- screenHeight +
510
- ", newY=" +
511
- y
512
- );
513
- }
514
- }
546
+ // For centered mode with aspect ratio, calculate maximum size that fits
547
+ int availableWidth = metrics.widthPixels;
548
+ int availableHeight = metrics.heightPixels;
515
549
 
516
550
  Log.d(
517
551
  TAG,
518
- "calculatePreviewLayoutParams: Applied aspect ratio " +
519
- aspectRatio +
520
- " - new size: " +
521
- width +
552
+ "Available space for preview: " +
553
+ availableWidth +
522
554
  "x" +
523
- height
555
+ availableHeight
556
+ );
557
+
558
+ // Calculate maximum size that fits the aspect ratio in available space
559
+ double maxWidthByHeight = availableHeight * ratio;
560
+ double maxHeightByWidth = availableWidth / ratio;
561
+
562
+ Log.d(
563
+ TAG,
564
+ "Aspect ratio calculations - maxWidthByHeight: " +
565
+ maxWidthByHeight +
566
+ ", maxHeightByWidth: " +
567
+ maxHeightByWidth
524
568
  );
569
+
570
+ if (maxWidthByHeight <= availableWidth) {
571
+ // Height is the limiting factor
572
+ width = (int) maxWidthByHeight;
573
+ height = availableHeight;
574
+ Log.d(TAG, "Height-limited sizing: " + width + "x" + height);
575
+ } else {
576
+ // Width is the limiting factor
577
+ width = availableWidth;
578
+ height = (int) maxHeightByWidth;
579
+ Log.d(TAG, "Width-limited sizing: " + width + "x" + height);
580
+ }
581
+
582
+ // Center the preview
583
+ x = (availableWidth - width) / 2;
584
+ y = (availableHeight - height) / 2;
585
+
586
+ Log.d(TAG, "Auto-centered position: x=" + x + ", y=" + y);
525
587
  } catch (NumberFormatException e) {
526
588
  Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio, e);
527
589
  }
@@ -540,28 +602,20 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
540
602
 
541
603
  Log.d(
542
604
  TAG,
543
- "calculatePreviewLayoutParams: Position calculation - x:" +
544
- x +
545
- " (leftMargin=" +
605
+ "Final layout params - Margins: left=" +
546
606
  layoutParams.leftMargin +
547
- "), y:" +
548
- y +
549
- " (topMargin=" +
607
+ ", top=" +
550
608
  layoutParams.topMargin +
551
- ")"
609
+ ", Size: " +
610
+ width +
611
+ "x" +
612
+ height
552
613
  );
553
-
554
614
  Log.d(
555
615
  TAG,
556
- "calculatePreviewLayoutParams: Final layout - x:" +
557
- x +
558
- " y:" +
559
- y +
560
- " width:" +
561
- width +
562
- " height:" +
563
- height
616
+ "================================================================================"
564
617
  );
618
+
565
619
  return layoutParams;
566
620
  }
567
621
 
@@ -623,13 +677,19 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
623
677
  ResolutionSelector resolutionSelector =
624
678
  resolutionSelectorBuilder.build();
625
679
 
680
+ int rotation = previewView != null && previewView.getDisplay() != null
681
+ ? previewView.getDisplay().getRotation()
682
+ : android.view.Surface.ROTATION_0;
683
+
626
684
  Preview preview = new Preview.Builder()
627
685
  .setResolutionSelector(resolutionSelector)
686
+ .setTargetRotation(rotation)
628
687
  .build();
629
688
  imageCapture = new ImageCapture.Builder()
630
689
  .setResolutionSelector(resolutionSelector)
631
690
  .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
632
691
  .setFlashMode(currentFlashMode)
692
+ .setTargetRotation(rotation)
633
693
  .build();
634
694
  sampleImageCapture = imageCapture;
635
695
  preview.setSurfaceProvider(previewView.getSurfaceProvider());
@@ -688,6 +748,50 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
688
748
  if (previewResolution != null) {
689
749
  currentPreviewResolution = previewResolution.getResolution();
690
750
  Log.d(TAG, "Preview resolution: " + currentPreviewResolution);
751
+
752
+ // Log the actual aspect ratio of the selected resolution
753
+ if (currentPreviewResolution != null) {
754
+ double actualRatio =
755
+ (double) currentPreviewResolution.getWidth() /
756
+ (double) currentPreviewResolution.getHeight();
757
+ Log.d(
758
+ TAG,
759
+ "Actual preview aspect ratio: " +
760
+ actualRatio +
761
+ " (width=" +
762
+ currentPreviewResolution.getWidth() +
763
+ ", height=" +
764
+ currentPreviewResolution.getHeight() +
765
+ ")"
766
+ );
767
+
768
+ // Compare with requested ratio
769
+ if ("4:3".equals(sessionConfig.getAspectRatio())) {
770
+ double expectedRatio = 4.0 / 3.0;
771
+ double difference = Math.abs(actualRatio - expectedRatio);
772
+ Log.d(
773
+ TAG,
774
+ "4:3 ratio check - Expected: " +
775
+ expectedRatio +
776
+ ", Actual: " +
777
+ actualRatio +
778
+ ", Difference: " +
779
+ difference
780
+ );
781
+ } else if ("16:9".equals(sessionConfig.getAspectRatio())) {
782
+ double expectedRatio = 16.0 / 9.0;
783
+ double difference = Math.abs(actualRatio - expectedRatio);
784
+ Log.d(
785
+ TAG,
786
+ "16:9 ratio check - Expected: " +
787
+ expectedRatio +
788
+ ", Actual: " +
789
+ actualRatio +
790
+ ", Difference: " +
791
+ difference
792
+ );
793
+ }
794
+ }
691
795
  }
692
796
  ResolutionInfo imageCaptureResolution =
693
797
  imageCapture.getResolutionInfo();
@@ -699,6 +803,16 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
699
803
  );
700
804
  }
701
805
 
806
+ // Update scale type based on aspect ratio whenever (re)binding
807
+ String ar = sessionConfig != null
808
+ ? sessionConfig.getAspectRatio()
809
+ : null;
810
+ previewView.setScaleType(
811
+ (ar == null || ar.isEmpty())
812
+ ? PreviewView.ScaleType.FIT_CENTER
813
+ : PreviewView.ScaleType.FILL_CENTER
814
+ );
815
+
702
816
  // Set initial zoom if specified, prioritizing targetZoom over default zoomFactor
703
817
  float initialZoom = sessionConfig.getTargetZoom() != 1.0f
704
818
  ? sessionConfig.getTargetZoom()
@@ -914,26 +1028,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
914
1028
 
915
1029
  JSONObject exifData = getExifData(exifInterface);
916
1030
 
917
- // Use the stored aspectRatio if none is provided and no width/height is specified
1031
+ // Determine final output: explicit size wins, then explicit aspectRatio,
1032
+ // otherwise crop to match what is visible in the preview (iOS parity)
918
1033
  String captureAspectRatio = aspectRatio;
919
- if (
920
- width == null &&
921
- height == null &&
922
- aspectRatio == null &&
923
- sessionConfig != null
924
- ) {
925
- captureAspectRatio = sessionConfig.getAspectRatio();
926
- // Default to "4:3" if no aspect ratio was set at all
927
- if (captureAspectRatio == null) {
928
- captureAspectRatio = "4:3";
929
- }
930
- Log.d(
931
- TAG,
932
- "capturePhoto: Using stored aspectRatio: " + captureAspectRatio
933
- );
934
- }
935
-
936
- // Handle aspect ratio if no width/height specified
937
1034
  if (
938
1035
  width == null &&
939
1036
  height == null &&
@@ -1027,14 +1124,22 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1027
1124
  // Write EXIF data back to resized image
1028
1125
  bytes = writeExifToImageBytes(bytes, exifInterface);
1029
1126
  } else {
1030
- // For non-resized images, ensure EXIF is saved
1031
- exifInterface.saveAttributes();
1032
- bytes = new byte[(int) tempFile.length()];
1033
- java.io.FileInputStream fis2 = new java.io.FileInputStream(
1034
- tempFile
1127
+ // No explicit size/ratio: crop to match current preview content
1128
+ Bitmap originalBitmap = BitmapFactory.decodeByteArray(
1129
+ bytes,
1130
+ 0,
1131
+ bytes.length
1035
1132
  );
1036
- fis2.read(bytes);
1037
- fis2.close();
1133
+ Bitmap previewCropped = cropBitmapToMatchPreview(originalBitmap);
1134
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
1135
+ previewCropped.compress(
1136
+ Bitmap.CompressFormat.JPEG,
1137
+ quality,
1138
+ stream
1139
+ );
1140
+ bytes = stream.toByteArray();
1141
+ // Preserve EXIF
1142
+ bytes = writeExifToImageBytes(bytes, exifInterface);
1038
1143
  }
1039
1144
 
1040
1145
  if (saveToGallery) {
@@ -1380,6 +1485,49 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1380
1485
  return bytes;
1381
1486
  }
1382
1487
 
1488
+ private Bitmap cropBitmapToMatchPreview(Bitmap image) {
1489
+ if (previewContainer == null || previewView == null) {
1490
+ return image;
1491
+ }
1492
+ int containerWidth = previewContainer.getWidth();
1493
+ int containerHeight = previewContainer.getHeight();
1494
+ if (containerWidth == 0 || containerHeight == 0) {
1495
+ return image;
1496
+ }
1497
+ // Compute preview aspect based on actual camera content bounds
1498
+ Rect bounds = getActualCameraBounds();
1499
+ int previewW = Math.max(1, bounds.width());
1500
+ int previewH = Math.max(1, bounds.height());
1501
+ float previewRatio = (float) previewW / (float) previewH;
1502
+
1503
+ int imgW = image.getWidth();
1504
+ int imgH = image.getHeight();
1505
+ float imgRatio = (float) imgW / (float) imgH;
1506
+
1507
+ int targetW = imgW;
1508
+ int targetH = imgH;
1509
+ if (imgRatio > previewRatio) {
1510
+ // Image wider than preview: crop width
1511
+ targetW = Math.round(imgH * previewRatio);
1512
+ } else if (imgRatio < previewRatio) {
1513
+ // Image taller than preview: crop height
1514
+ targetH = Math.round(imgW / previewRatio);
1515
+ }
1516
+ int x = Math.max(0, (imgW - targetW) / 2);
1517
+ int y = Math.max(0, (imgH - targetH) / 2);
1518
+ try {
1519
+ return Bitmap.createBitmap(
1520
+ image,
1521
+ x,
1522
+ y,
1523
+ Math.min(targetW, imgW - x),
1524
+ Math.min(targetH, imgH - y)
1525
+ );
1526
+ } catch (Exception ignore) {
1527
+ return image;
1528
+ }
1529
+ }
1530
+
1383
1531
  // not workin for xiaomi https://xiaomi.eu/community/threads/mi-11-ultra-unable-to-access-camera-lenses-in-apps-camera2-api.61456/
1384
1532
  @OptIn(markerClass = ExperimentalCamera2Interop.class)
1385
1533
  public static List<
@@ -1698,12 +1846,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1698
1846
  MeteringPointFactory factory = previewView.getMeteringPointFactory();
1699
1847
  MeteringPoint point = factory.createPoint(x * viewWidth, y * viewHeight);
1700
1848
 
1701
- // Create focus and metering action
1849
+ // Create focus and metering action (persistent, no auto-cancel) to match iOS behavior
1702
1850
  FocusMeteringAction action = new FocusMeteringAction.Builder(
1703
1851
  point,
1704
1852
  FocusMeteringAction.FLAG_AF | FocusMeteringAction.FLAG_AE
1705
1853
  )
1706
- .setAutoCancelDuration(3, TimeUnit.SECONDS) // Auto-cancel after 3 seconds
1854
+ .disableAutoCancel()
1707
1855
  .build();
1708
1856
 
1709
1857
  try {
@@ -1777,8 +1925,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1777
1925
  }
1778
1926
 
1779
1927
  // Create an elegant focus indicator
1780
- View container = new View(context);
1781
- int size = (int) (60 * context.getResources().getDisplayMetrics().density); // 60dp size
1928
+ FrameLayout container = new FrameLayout(context);
1929
+ int size = (int) (80 * context.getResources().getDisplayMetrics().density); // match iOS size
1782
1930
  FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(size, size);
1783
1931
 
1784
1932
  // Center the indicator on the touch point with bounds checking
@@ -1794,25 +1942,73 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1794
1942
  Math.min((int) (y - size / 2), containerHeight - size)
1795
1943
  );
1796
1944
 
1797
- // Create an elegant focus ring - white stroke with transparent center
1798
- GradientDrawable drawable = new GradientDrawable();
1799
- drawable.setShape(GradientDrawable.OVAL);
1800
- drawable.setStroke(
1801
- (int) (2 * context.getResources().getDisplayMetrics().density),
1802
- Color.WHITE
1803
- ); // 2dp white stroke
1804
- drawable.setColor(Color.TRANSPARENT); // Transparent center
1805
- container.setBackground(drawable);
1945
+ // iOS Camera style: square with mid-edge ticks
1946
+ GradientDrawable border = new GradientDrawable();
1947
+ border.setShape(GradientDrawable.RECTANGLE);
1948
+ int stroke = (int) (2 * context.getResources().getDisplayMetrics().density);
1949
+ border.setStroke(stroke, Color.YELLOW);
1950
+ border.setCornerRadius(0);
1951
+ border.setColor(Color.TRANSPARENT);
1952
+ container.setBackground(border);
1953
+
1954
+ // Add 4 tiny mid-edge ticks inside the square
1955
+ int tickLen = (int) (12 *
1956
+ context.getResources().getDisplayMetrics().density);
1957
+ int inset = stroke; // ticks should touch the sides
1958
+ // Top tick (perpendicular): vertical inward from top edge
1959
+ View topTick = new View(context);
1960
+ FrameLayout.LayoutParams topParams = new FrameLayout.LayoutParams(
1961
+ stroke,
1962
+ tickLen
1963
+ );
1964
+ topParams.leftMargin = (size - stroke) / 2;
1965
+ topParams.topMargin = inset;
1966
+ topTick.setLayoutParams(topParams);
1967
+ topTick.setBackgroundColor(Color.YELLOW);
1968
+ container.addView(topTick);
1969
+ // Bottom tick (perpendicular): vertical inward from bottom edge
1970
+ View bottomTick = new View(context);
1971
+ FrameLayout.LayoutParams bottomParams = new FrameLayout.LayoutParams(
1972
+ stroke,
1973
+ tickLen
1974
+ );
1975
+ bottomParams.leftMargin = (size - stroke) / 2;
1976
+ bottomParams.topMargin = size - inset - tickLen;
1977
+ bottomTick.setLayoutParams(bottomParams);
1978
+ bottomTick.setBackgroundColor(Color.YELLOW);
1979
+ container.addView(bottomTick);
1980
+ // Left tick (perpendicular): horizontal inward from left edge
1981
+ View leftTick = new View(context);
1982
+ FrameLayout.LayoutParams leftParams = new FrameLayout.LayoutParams(
1983
+ tickLen,
1984
+ stroke
1985
+ );
1986
+ leftParams.leftMargin = inset;
1987
+ leftParams.topMargin = (size - stroke) / 2;
1988
+ leftTick.setLayoutParams(leftParams);
1989
+ leftTick.setBackgroundColor(Color.YELLOW);
1990
+ container.addView(leftTick);
1991
+ // Right tick (perpendicular): horizontal inward from right edge
1992
+ View rightTick = new View(context);
1993
+ FrameLayout.LayoutParams rightParams = new FrameLayout.LayoutParams(
1994
+ tickLen,
1995
+ stroke
1996
+ );
1997
+ rightParams.leftMargin = size - inset - tickLen;
1998
+ rightParams.topMargin = (size - stroke) / 2;
1999
+ rightTick.setLayoutParams(rightParams);
2000
+ rightTick.setBackgroundColor(Color.YELLOW);
2001
+ container.addView(rightTick);
1806
2002
 
1807
2003
  focusIndicatorView = container;
1808
2004
  // Bump animation token; everything after this must validate against this token
1809
2005
  final long thisAnimationId = ++focusIndicatorAnimationId;
1810
2006
  final View thisIndicatorView = focusIndicatorView;
1811
2007
 
1812
- // Set initial state for smooth animation
1813
- focusIndicatorView.setAlpha(1f); // Start visible
1814
- focusIndicatorView.setScaleX(1.4f); // Start slightly larger for a quick scale-in
1815
- focusIndicatorView.setScaleY(1.4f);
2008
+ // Set initial state for smooth animation (mirror iOS)
2009
+ focusIndicatorView.setAlpha(0f);
2010
+ focusIndicatorView.setScaleX(1.5f);
2011
+ focusIndicatorView.setScaleY(1.5f);
1816
2012
  focusIndicatorView.setVisibility(View.VISIBLE);
1817
2013
 
1818
2014
  // Ensure container doesn't intercept touch events
@@ -1836,26 +2032,16 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1836
2032
  // Force a layout pass to ensure the view is properly positioned
1837
2033
  previewContainer.requestLayout();
1838
2034
 
1839
- // Smooth scale down animation with easing (no fade needed since we start visible)
1840
- ScaleAnimation scaleAnimation = new ScaleAnimation(
1841
- 1.4f,
1842
- 1.0f,
1843
- 1.4f,
1844
- 1.0f,
1845
- Animation.RELATIVE_TO_SELF,
1846
- 0.5f,
1847
- Animation.RELATIVE_TO_SELF,
1848
- 0.5f
1849
- );
1850
- scaleAnimation.setDuration(120);
1851
- scaleAnimation.setInterpolator(
1852
- new android.view.animation.OvershootInterpolator(1.2f)
1853
- );
1854
-
1855
- // Start the animation
1856
- focusIndicatorView.startAnimation(scaleAnimation);
2035
+ // First phase: fade in and scale to 1.0 over 150ms
2036
+ focusIndicatorView
2037
+ .animate()
2038
+ .alpha(1f)
2039
+ .scaleX(1f)
2040
+ .scaleY(1f)
2041
+ .setDuration(150)
2042
+ .start();
1857
2043
 
1858
- // Schedule fast fade out and removal
2044
+ // Phase 2: after 500ms, fade to 0.3 over 200ms
1859
2045
  focusIndicatorView.postDelayed(
1860
2046
  new Runnable() {
1861
2047
  @Override
@@ -1868,37 +2054,66 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1868
2054
  ) {
1869
2055
  focusIndicatorView
1870
2056
  .animate()
1871
- .alpha(0f)
1872
- .scaleX(0.9f)
1873
- .scaleY(0.9f)
1874
- .setDuration(180)
1875
- .setInterpolator(
1876
- new android.view.animation.AccelerateInterpolator()
1877
- )
2057
+ .alpha(0.3f)
2058
+ .setDuration(200)
1878
2059
  .withEndAction(
1879
2060
  new Runnable() {
1880
2061
  @Override
1881
2062
  public void run() {
1882
- if (
1883
- focusIndicatorView != null &&
1884
- previewContainer != null &&
1885
- thisIndicatorView == focusIndicatorView &&
1886
- thisAnimationId == focusIndicatorAnimationId
1887
- ) {
1888
- try {
1889
- focusIndicatorView.clearAnimation();
1890
- } catch (Exception ignore) {}
1891
- previewContainer.removeView(focusIndicatorView);
1892
- focusIndicatorView = null;
1893
- }
2063
+ // Phase 3: after 200ms more, fade out to 0 and scale to 0.8 over 300ms
2064
+ focusIndicatorView.postDelayed(
2065
+ new Runnable() {
2066
+ @Override
2067
+ public void run() {
2068
+ if (
2069
+ focusIndicatorView != null &&
2070
+ thisIndicatorView == focusIndicatorView &&
2071
+ thisAnimationId == focusIndicatorAnimationId
2072
+ ) {
2073
+ focusIndicatorView
2074
+ .animate()
2075
+ .alpha(0f)
2076
+ .scaleX(0.8f)
2077
+ .scaleY(0.8f)
2078
+ .setDuration(300)
2079
+ .setInterpolator(
2080
+ new android.view.animation.AccelerateInterpolator()
2081
+ )
2082
+ .withEndAction(
2083
+ new Runnable() {
2084
+ @Override
2085
+ public void run() {
2086
+ if (
2087
+ focusIndicatorView != null &&
2088
+ previewContainer != null &&
2089
+ thisIndicatorView == focusIndicatorView &&
2090
+ thisAnimationId ==
2091
+ focusIndicatorAnimationId
2092
+ ) {
2093
+ try {
2094
+ focusIndicatorView.clearAnimation();
2095
+ } catch (Exception ignore) {}
2096
+ previewContainer.removeView(
2097
+ focusIndicatorView
2098
+ );
2099
+ focusIndicatorView = null;
2100
+ }
2101
+ }
2102
+ }
2103
+ );
2104
+ }
2105
+ }
2106
+ },
2107
+ 200
2108
+ );
1894
2109
  }
1895
2110
  }
1896
2111
  );
1897
2112
  }
1898
2113
  }
1899
2114
  },
1900
- 250
1901
- ); // Faster feedback
2115
+ 500
2116
+ );
1902
2117
  }
1903
2118
 
1904
2119
  public static List<Size> getSupportedPictureSizes(String facing) {
@@ -2149,13 +2364,48 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2149
2364
  Float y,
2150
2365
  Runnable callback
2151
2366
  ) {
2367
+ Log.d(
2368
+ TAG,
2369
+ "======================== SET ASPECT RATIO ========================"
2370
+ );
2371
+ Log.d(
2372
+ TAG,
2373
+ "Input parameters - aspectRatio: " +
2374
+ aspectRatio +
2375
+ ", x: " +
2376
+ x +
2377
+ ", y: " +
2378
+ y
2379
+ );
2380
+
2152
2381
  if (sessionConfig == null) {
2382
+ Log.d(TAG, "SessionConfig is null, returning");
2153
2383
  if (callback != null) callback.run();
2154
2384
  return;
2155
2385
  }
2156
2386
 
2157
2387
  String currentAspectRatio = sessionConfig.getAspectRatio();
2158
2388
 
2389
+ // Get current display information
2390
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
2391
+ int screenWidthPx = metrics.widthPixels;
2392
+ int screenHeightPx = metrics.heightPixels;
2393
+ boolean isPortrait =
2394
+ context.getResources().getConfiguration().orientation ==
2395
+ Configuration.ORIENTATION_PORTRAIT;
2396
+
2397
+ Log.d(
2398
+ TAG,
2399
+ "Current screen: " +
2400
+ screenWidthPx +
2401
+ "x" +
2402
+ screenHeightPx +
2403
+ " (" +
2404
+ (isPortrait ? "PORTRAIT" : "LANDSCAPE") +
2405
+ ")"
2406
+ );
2407
+ Log.d(TAG, "Current aspect ratio: " + currentAspectRatio);
2408
+
2159
2409
  // Don't restart camera if aspect ratio hasn't changed and no position specified
2160
2410
  if (
2161
2411
  aspectRatio != null &&
@@ -2163,12 +2413,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2163
2413
  x == null &&
2164
2414
  y == null
2165
2415
  ) {
2166
- Log.d(
2167
- TAG,
2168
- "setAspectRatio: Aspect ratio " +
2169
- aspectRatio +
2170
- " is already set and no position specified, skipping"
2171
- );
2416
+ Log.d(TAG, "Aspect ratio unchanged and no position specified, skipping");
2172
2417
  if (callback != null) callback.run();
2173
2418
  return;
2174
2419
  }
@@ -2176,22 +2421,16 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2176
2421
  String currentGridMode = sessionConfig.getGridMode();
2177
2422
  Log.d(
2178
2423
  TAG,
2179
- "setAspectRatio: Changing from " +
2180
- currentAspectRatio +
2181
- " to " +
2182
- aspectRatio +
2183
- (x != null && y != null
2184
- ? " at position (" + x + ", " + y + ")"
2185
- : " with auto-centering") +
2186
- ", preserving grid mode: " +
2187
- currentGridMode
2424
+ "Changing aspect ratio from " + currentAspectRatio + " to " + aspectRatio
2188
2425
  );
2426
+ Log.d(TAG, "Auto-centering will be applied (matching iOS behavior)");
2189
2427
 
2428
+ // Match iOS behavior: when aspect ratio changes, always auto-center
2190
2429
  sessionConfig = new CameraSessionConfiguration(
2191
2430
  sessionConfig.getDeviceId(),
2192
2431
  sessionConfig.getPosition(),
2193
- sessionConfig.getX(),
2194
- sessionConfig.getY(),
2432
+ -1, // Force auto-center X (iOS: self.posX = -1)
2433
+ -1, // Force auto-center Y (iOS: self.posY = -1)
2195
2434
  sessionConfig.getWidth(),
2196
2435
  sessionConfig.getHeight(),
2197
2436
  sessionConfig.getPaddingBottom(),
@@ -2205,12 +2444,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2205
2444
  aspectRatio,
2206
2445
  currentGridMode
2207
2446
  );
2447
+ sessionConfig.setCentered(true);
2208
2448
 
2209
2449
  // Update layout and rebind camera with new aspect ratio
2210
2450
  if (isRunning && previewContainer != null) {
2211
2451
  mainExecutor.execute(() -> {
2212
- // First update the UI layout
2213
- updatePreviewLayoutForAspectRatio(aspectRatio, x, y);
2452
+ // First update the UI layout - always pass null for x,y to force auto-centering (matching iOS)
2453
+ updatePreviewLayoutForAspectRatio(aspectRatio, null, null);
2214
2454
 
2215
2455
  // Then rebind the camera with new aspect ratio configuration
2216
2456
  Log.d(
@@ -2240,8 +2480,121 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2240
2480
  previewContainer.post(callback);
2241
2481
  }
2242
2482
  }
2483
+
2484
+ Log.d(
2485
+ TAG,
2486
+ "=================================================================="
2487
+ );
2243
2488
  });
2244
2489
  } else {
2490
+ Log.d(TAG, "Camera not running, just saving configuration");
2491
+ Log.d(
2492
+ TAG,
2493
+ "=================================================================="
2494
+ );
2495
+ if (callback != null) callback.run();
2496
+ }
2497
+ }
2498
+
2499
+ // Force aspect ratio recalculation (used during orientation changes)
2500
+ public void forceAspectRatioRecalculation(
2501
+ String aspectRatio,
2502
+ Float x,
2503
+ Float y,
2504
+ Runnable callback
2505
+ ) {
2506
+ Log.d(
2507
+ TAG,
2508
+ "======================== FORCE ASPECT RATIO RECALCULATION ========================"
2509
+ );
2510
+ Log.d(
2511
+ TAG,
2512
+ "Input parameters - aspectRatio: " +
2513
+ aspectRatio +
2514
+ ", x: " +
2515
+ x +
2516
+ ", y: " +
2517
+ y
2518
+ );
2519
+
2520
+ if (sessionConfig == null) {
2521
+ Log.d(TAG, "SessionConfig is null, returning");
2522
+ if (callback != null) callback.run();
2523
+ return;
2524
+ }
2525
+
2526
+ String currentGridMode = sessionConfig.getGridMode();
2527
+ Log.d(TAG, "Forcing aspect ratio recalculation for: " + aspectRatio);
2528
+ Log.d(TAG, "Auto-centering will be applied (matching iOS behavior)");
2529
+
2530
+ // Match iOS behavior: when aspect ratio changes, always auto-center
2531
+ sessionConfig = new CameraSessionConfiguration(
2532
+ sessionConfig.getDeviceId(),
2533
+ sessionConfig.getPosition(),
2534
+ -1, // Force auto-center X (iOS: self.posX = -1)
2535
+ -1, // Force auto-center Y (iOS: self.posY = -1)
2536
+ sessionConfig.getWidth(),
2537
+ sessionConfig.getHeight(),
2538
+ sessionConfig.getPaddingBottom(),
2539
+ sessionConfig.getToBack(),
2540
+ sessionConfig.getStoreToFile(),
2541
+ sessionConfig.getEnableOpacity(),
2542
+ sessionConfig.getEnableZoom(),
2543
+ sessionConfig.getDisableExifHeaderStripping(),
2544
+ sessionConfig.getDisableAudio(),
2545
+ sessionConfig.getZoomFactor(),
2546
+ aspectRatio,
2547
+ currentGridMode
2548
+ );
2549
+ sessionConfig.setCentered(true);
2550
+
2551
+ // Update layout and rebind camera with new aspect ratio
2552
+ if (isRunning && previewContainer != null) {
2553
+ mainExecutor.execute(() -> {
2554
+ // First update the UI layout - always pass null for x,y to force auto-centering (matching iOS)
2555
+ updatePreviewLayoutForAspectRatio(aspectRatio, null, null);
2556
+
2557
+ // Then rebind the camera with new aspect ratio configuration
2558
+ Log.d(
2559
+ TAG,
2560
+ "forceAspectRatioRecalculation: Rebinding camera with aspect ratio: " +
2561
+ aspectRatio
2562
+ );
2563
+ bindCameraUseCases();
2564
+
2565
+ // Preserve grid mode and wait for completion
2566
+ if (gridOverlayView != null) {
2567
+ gridOverlayView.post(() -> {
2568
+ Log.d(
2569
+ TAG,
2570
+ "forceAspectRatioRecalculation: Re-applying grid mode: " +
2571
+ currentGridMode
2572
+ );
2573
+ gridOverlayView.setGridMode(currentGridMode);
2574
+
2575
+ // Wait one more frame for grid to be applied, then call callback
2576
+ if (callback != null) {
2577
+ gridOverlayView.post(callback);
2578
+ }
2579
+ });
2580
+ } else {
2581
+ // No grid overlay, wait one frame for layout completion then call callback
2582
+ if (callback != null) {
2583
+ previewContainer.post(callback);
2584
+ }
2585
+ }
2586
+
2587
+ Log.d(
2588
+ TAG,
2589
+ "=================================================================="
2590
+ );
2591
+ });
2592
+ } else {
2593
+ Log.d(TAG, "Camera not running, just saving configuration");
2594
+ Log.d(
2595
+ TAG,
2596
+ "=================================================================="
2597
+ );
2245
2598
  if (callback != null) callback.run();
2246
2599
  }
2247
2600
  }
@@ -2352,15 +2705,40 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2352
2705
  return new Rect(0, 0, containerWidth, containerHeight);
2353
2706
  }
2354
2707
 
2355
- // The preview is rotated 90 degrees for portrait mode
2356
- // So we swap the dimensions
2357
- int cameraWidth = currentPreviewResolution.getHeight();
2358
- int cameraHeight = currentPreviewResolution.getWidth();
2708
+ // CameraX delivers preview in sensor orientation (always landscape)
2709
+ // But PreviewView internally rotates it to match device orientation
2710
+ // So we need to swap dimensions in portrait mode
2711
+ int cameraWidth = currentPreviewResolution.getWidth();
2712
+ int cameraHeight = currentPreviewResolution.getHeight();
2713
+
2714
+ // Check if we're in portrait mode
2715
+ boolean isPortrait =
2716
+ context.getResources().getConfiguration().orientation ==
2717
+ Configuration.ORIENTATION_PORTRAIT;
2718
+
2719
+ // Swap dimensions if in portrait mode to match how PreviewView displays it
2720
+ if (isPortrait) {
2721
+ int temp = cameraWidth;
2722
+ cameraWidth = cameraHeight;
2723
+ cameraHeight = temp;
2724
+ }
2725
+
2726
+ // When we have an aspect ratio set, we use FILL_CENTER which scales to fill
2727
+ // the container while maintaining aspect ratio, potentially cropping
2728
+ boolean usesFillCenter =
2729
+ sessionConfig != null && sessionConfig.getAspectRatio() != null;
2359
2730
 
2360
- // Calculate the scaling factor to fit the camera in the container
2361
2731
  float widthScale = (float) containerWidth / cameraWidth;
2362
2732
  float heightScale = (float) containerHeight / cameraHeight;
2363
- float scale = Math.min(widthScale, heightScale); // FIT_CENTER uses min scale
2733
+ float scale;
2734
+
2735
+ if (usesFillCenter) {
2736
+ // FILL_CENTER uses max scale to fill the container
2737
+ scale = Math.max(widthScale, heightScale);
2738
+ } else {
2739
+ // FIT_CENTER uses min scale to fit within the container
2740
+ scale = Math.min(widthScale, heightScale);
2741
+ }
2364
2742
 
2365
2743
  // Calculate the actual size of the camera content after scaling
2366
2744
  int scaledWidth = Math.round(cameraWidth * scale);
@@ -2380,8 +2758,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2380
2758
  cameraWidth +
2381
2759
  "x" +
2382
2760
  cameraHeight +
2761
+ " (swapped=" +
2762
+ isPortrait +
2763
+ ")" +
2383
2764
  ", scale=" +
2384
2765
  scale +
2766
+ " (fillCenter=" +
2767
+ usesFillCenter +
2768
+ ")" +
2385
2769
  ", scaled=" +
2386
2770
  scaledWidth +
2387
2771
  "x" +
@@ -2395,10 +2779,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2395
2779
 
2396
2780
  // Return the bounds relative to the container
2397
2781
  return new Rect(
2398
- offsetX,
2399
- offsetY,
2400
- offsetX + scaledWidth,
2401
- offsetY + scaledHeight
2782
+ Math.max(0, offsetX),
2783
+ Math.max(0, offsetY),
2784
+ Math.min(containerWidth, offsetX + scaledWidth),
2785
+ Math.min(containerHeight, offsetY + scaledHeight)
2402
2786
  );
2403
2787
  }
2404
2788
 
@@ -2630,27 +3014,137 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2630
3014
  ) {
2631
3015
  if (previewContainer == null || aspectRatio == null) return;
2632
3016
 
2633
- // Parse aspect ratio
3017
+ Log.d(
3018
+ TAG,
3019
+ "======================== UPDATE PREVIEW LAYOUT FOR ASPECT RATIO ========================"
3020
+ );
3021
+ Log.d(
3022
+ TAG,
3023
+ "Input parameters - aspectRatio: " +
3024
+ aspectRatio +
3025
+ ", x: " +
3026
+ x +
3027
+ ", y: " +
3028
+ y
3029
+ );
3030
+
3031
+ // Get comprehensive display information
3032
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
3033
+ int screenWidthPx = metrics.widthPixels;
3034
+ int screenHeightPx = metrics.heightPixels;
3035
+ float density = metrics.density;
3036
+
3037
+ // Get WebView dimensions
3038
+ int webViewWidth = webView.getWidth();
3039
+ int webViewHeight = webView.getHeight();
3040
+
3041
+ // Get current preview container info
3042
+ ViewGroup.LayoutParams currentParams = previewContainer.getLayoutParams();
3043
+ int currentWidth = currentParams != null ? currentParams.width : 0;
3044
+ int currentHeight = currentParams != null ? currentParams.height : 0;
3045
+ int currentX = 0;
3046
+ int currentY = 0;
3047
+ if (currentParams instanceof ViewGroup.MarginLayoutParams) {
3048
+ ViewGroup.MarginLayoutParams marginParams =
3049
+ (ViewGroup.MarginLayoutParams) currentParams;
3050
+ currentX = marginParams.leftMargin;
3051
+ currentY = marginParams.topMargin;
3052
+ }
3053
+
3054
+ Log.d(
3055
+ TAG,
3056
+ "Screen dimensions: " +
3057
+ screenWidthPx +
3058
+ "x" +
3059
+ screenHeightPx +
3060
+ " pixels, density: " +
3061
+ density
3062
+ );
3063
+ Log.d(TAG, "WebView dimensions: " + webViewWidth + "x" + webViewHeight);
3064
+ Log.d(
3065
+ TAG,
3066
+ "Current preview position: " +
3067
+ currentX +
3068
+ "," +
3069
+ currentY +
3070
+ " size: " +
3071
+ currentWidth +
3072
+ "x" +
3073
+ currentHeight
3074
+ );
3075
+
3076
+ // Parse aspect ratio as width:height (e.g., 4:3 -> r=4/3)
2634
3077
  String[] ratios = aspectRatio.split(":");
2635
- if (ratios.length != 2) return;
3078
+ if (ratios.length != 2) {
3079
+ Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio);
3080
+ return;
3081
+ }
2636
3082
 
2637
3083
  try {
2638
- // For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
2639
- float ratio = Float.parseFloat(ratios[1]) / Float.parseFloat(ratios[0]);
3084
+ // Match iOS logic exactly
3085
+ double ratioWidth = Double.parseDouble(ratios[0]);
3086
+ double ratioHeight = Double.parseDouble(ratios[1]);
3087
+ boolean isPortrait =
3088
+ context.getResources().getConfiguration().orientation ==
3089
+ Configuration.ORIENTATION_PORTRAIT;
3090
+
3091
+ Log.d(
3092
+ TAG,
3093
+ "Aspect ratio parsing - Original: " +
3094
+ aspectRatio +
3095
+ " (width=" +
3096
+ ratioWidth +
3097
+ ", height=" +
3098
+ ratioHeight +
3099
+ ")"
3100
+ );
3101
+ Log.d(
3102
+ TAG,
3103
+ "Device orientation: " + (isPortrait ? "PORTRAIT" : "LANDSCAPE")
3104
+ );
3105
+
3106
+ // iOS: let ratio = !isPortrait ? ratioParts[0] / ratioParts[1] : ratioParts[1] / ratioParts[0]
3107
+ double ratio = !isPortrait
3108
+ ? (ratioWidth / ratioHeight)
3109
+ : (ratioHeight / ratioWidth);
3110
+
3111
+ Log.d(
3112
+ TAG,
3113
+ "Computed ratio: " +
3114
+ ratio +
3115
+ " (iOS formula: " +
3116
+ (!isPortrait ? "width/height" : "height/width") +
3117
+ ")"
3118
+ );
2640
3119
 
2641
3120
  // Get available space from webview dimensions
2642
- int availableWidth = webView.getWidth();
2643
- int availableHeight = webView.getHeight();
3121
+ int availableWidth = webViewWidth;
3122
+ int availableHeight = webViewHeight;
3123
+
3124
+ Log.d(
3125
+ TAG,
3126
+ "Available space from WebView: " +
3127
+ availableWidth +
3128
+ "x" +
3129
+ availableHeight
3130
+ );
2644
3131
 
2645
3132
  // Calculate position and size
2646
3133
  int finalX, finalY, finalWidth, finalHeight;
2647
3134
 
2648
3135
  if (x != null && y != null) {
2649
- // Account for WebView insets from edge-to-edge support
3136
+ // Manual positioning mode
2650
3137
  int webViewTopInset = getWebViewTopInset();
2651
3138
  int webViewLeftInset = getWebViewLeftInset();
2652
3139
 
2653
- // Use provided coordinates with boundary checking, adjusted for insets
3140
+ Log.d(
3141
+ TAG,
3142
+ "Manual positioning mode - WebView insets: left=" +
3143
+ webViewLeftInset +
3144
+ ", top=" +
3145
+ webViewTopInset
3146
+ );
3147
+
2654
3148
  finalX = Math.max(
2655
3149
  0,
2656
3150
  Math.min(x.intValue() + webViewLeftInset, availableWidth)
@@ -2664,6 +3158,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2664
3158
  int maxWidth = availableWidth - finalX;
2665
3159
  int maxHeight = availableHeight - finalY;
2666
3160
 
3161
+ Log.d(
3162
+ TAG,
3163
+ "Max available space from position: " + maxWidth + "x" + maxHeight
3164
+ );
3165
+
2667
3166
  // Calculate optimal size while maintaining aspect ratio within available space
2668
3167
  finalWidth = maxWidth;
2669
3168
  finalHeight = (int) (maxWidth / ratio);
@@ -2672,76 +3171,147 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2672
3171
  // Height constraint is tighter, fit by height
2673
3172
  finalHeight = maxHeight;
2674
3173
  finalWidth = (int) (maxHeight * ratio);
3174
+ Log.d(TAG, "Height-constrained sizing");
3175
+ } else {
3176
+ Log.d(TAG, "Width-constrained sizing");
2675
3177
  }
2676
3178
 
2677
3179
  // Ensure final position stays within bounds
2678
3180
  finalX = Math.max(0, Math.min(finalX, availableWidth - finalWidth));
2679
3181
  finalY = Math.max(0, Math.min(finalY, availableHeight - finalHeight));
2680
3182
  } else {
2681
- // Auto-center the view
2682
- // Use full available space to match iOS behavior
2683
- int maxAvailableWidth = availableWidth;
2684
- int maxAvailableHeight = availableHeight;
2685
-
2686
- // Start with width-based calculation
2687
- finalWidth = maxAvailableWidth;
2688
- finalHeight = (int) (finalWidth / ratio);
2689
-
2690
- // If height exceeds available space, use height-based calculation
2691
- if (finalHeight > maxAvailableHeight) {
2692
- finalHeight = maxAvailableHeight;
2693
- finalWidth = (int) (finalHeight * ratio);
3183
+ // Auto-center mode - match iOS behavior exactly
3184
+ Log.d(TAG, "Auto-center mode");
3185
+
3186
+ // Calculate maximum size that fits the aspect ratio in available space
3187
+ double maxWidthByHeight = availableHeight * ratio;
3188
+ double maxHeightByWidth = availableWidth / ratio;
3189
+
3190
+ Log.d(
3191
+ TAG,
3192
+ "Aspect ratio calculations - maxWidthByHeight: " +
3193
+ maxWidthByHeight +
3194
+ ", maxHeightByWidth: " +
3195
+ maxHeightByWidth
3196
+ );
3197
+
3198
+ if (maxWidthByHeight <= availableWidth) {
3199
+ // Height is the limiting factor
3200
+ finalWidth = (int) maxWidthByHeight;
3201
+ finalHeight = availableHeight;
3202
+ Log.d(
3203
+ TAG,
3204
+ "Height-limited sizing: " + finalWidth + "x" + finalHeight
3205
+ );
3206
+ } else {
3207
+ // Width is the limiting factor
3208
+ finalWidth = availableWidth;
3209
+ finalHeight = (int) maxHeightByWidth;
3210
+ Log.d(TAG, "Width-limited sizing: " + finalWidth + "x" + finalHeight);
2694
3211
  }
2695
3212
 
2696
- // Center the view
3213
+ // Center the preview
2697
3214
  finalX = (availableWidth - finalWidth) / 2;
2698
3215
  finalY = (availableHeight - finalHeight) / 2;
2699
3216
 
2700
3217
  Log.d(
2701
3218
  TAG,
2702
- "updatePreviewLayoutForAspectRatio: Auto-center mode - ratio=" +
2703
- ratio +
2704
- ", calculated size=" +
3219
+ "Auto-center mode: calculated size " +
2705
3220
  finalWidth +
2706
3221
  "x" +
2707
3222
  finalHeight +
2708
- ", available=" +
2709
- availableWidth +
2710
- "x" +
2711
- availableHeight
3223
+ " at position (" +
3224
+ finalX +
3225
+ ", " +
3226
+ finalY +
3227
+ ")"
2712
3228
  );
2713
3229
  }
2714
3230
 
3231
+ Log.d(
3232
+ TAG,
3233
+ "Final calculated layout - Position: (" +
3234
+ finalX +
3235
+ "," +
3236
+ finalY +
3237
+ "), Size: " +
3238
+ finalWidth +
3239
+ "x" +
3240
+ finalHeight
3241
+ );
3242
+
3243
+ // Calculate and log the actual displayed aspect ratio
3244
+ double displayedRatio = (double) finalWidth / (double) finalHeight;
3245
+ Log.d(
3246
+ TAG,
3247
+ "Displayed aspect ratio: " +
3248
+ displayedRatio +
3249
+ " (width=" +
3250
+ finalWidth +
3251
+ ", height=" +
3252
+ finalHeight +
3253
+ ")"
3254
+ );
3255
+
3256
+ // Compare with expected ratio based on orientation
3257
+ if (aspectRatio != null) {
3258
+ String[] parts = aspectRatio.split(":");
3259
+ if (parts.length == 2) {
3260
+ double expectedDisplayRatio = isPortrait
3261
+ ? (ratioHeight / ratioWidth)
3262
+ : (ratioWidth / ratioHeight);
3263
+ double difference = Math.abs(displayedRatio - expectedDisplayRatio);
3264
+ Log.d(
3265
+ TAG,
3266
+ "Display ratio check - Expected: " +
3267
+ expectedDisplayRatio +
3268
+ ", Actual: " +
3269
+ displayedRatio +
3270
+ ", Difference: " +
3271
+ difference +
3272
+ " (tolerance should be < 0.01)"
3273
+ );
3274
+ }
3275
+ }
3276
+
2715
3277
  // Update layout params
2716
- ViewGroup.LayoutParams currentParams = previewContainer.getLayoutParams();
2717
- if (currentParams instanceof ViewGroup.MarginLayoutParams) {
3278
+ ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
3279
+ if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
2718
3280
  ViewGroup.MarginLayoutParams params =
2719
- (ViewGroup.MarginLayoutParams) currentParams;
3281
+ (ViewGroup.MarginLayoutParams) layoutParams;
2720
3282
  params.width = finalWidth;
2721
3283
  params.height = finalHeight;
2722
3284
  params.leftMargin = finalX;
2723
3285
  params.topMargin = finalY;
2724
3286
  previewContainer.setLayoutParams(params);
2725
3287
  previewContainer.requestLayout();
2726
- Log.d(
2727
- TAG,
2728
- "updatePreviewLayoutForAspectRatio: Updated to " +
2729
- finalWidth +
2730
- "x" +
2731
- finalHeight +
2732
- " at (" +
2733
- finalX +
2734
- "," +
2735
- finalY +
2736
- ")"
2737
- );
3288
+
3289
+ Log.d(TAG, "Layout params applied successfully");
2738
3290
 
2739
3291
  // Update grid overlay bounds after aspect ratio change
2740
- previewContainer.post(() -> updateGridOverlayBounds());
3292
+ previewContainer.post(() -> {
3293
+ Log.d(
3294
+ TAG,
3295
+ "Post-layout verification - Actual position: " +
3296
+ previewContainer.getLeft() +
3297
+ "," +
3298
+ previewContainer.getTop() +
3299
+ ", Actual size: " +
3300
+ previewContainer.getWidth() +
3301
+ "x" +
3302
+ previewContainer.getHeight()
3303
+ );
3304
+ updateGridOverlayBounds();
3305
+ });
2741
3306
  }
2742
3307
  } catch (NumberFormatException e) {
2743
3308
  Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio, e);
2744
3309
  }
3310
+
3311
+ Log.d(
3312
+ TAG,
3313
+ "========================================================================================"
3314
+ );
2745
3315
  }
2746
3316
 
2747
3317
  private int getWebViewTopInset() {
@@ -2794,10 +3364,19 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2794
3364
  int webViewTopInset = getWebViewTopInset();
2795
3365
  int webViewLeftInset = getWebViewLeftInset();
2796
3366
 
2797
- int x = Math.max(0, (int) ((actualX - webViewLeftInset) / pixelRatio));
2798
- int y = Math.max(0, (int) ((actualY - webViewTopInset) / pixelRatio));
2799
- int width = (int) (actualWidth / pixelRatio);
2800
- int height = (int) (actualHeight / pixelRatio);
3367
+ // Use proper rounding strategy to avoid gaps:
3368
+ // - For positions (x, y): floor to avoid gaps at top/left
3369
+ // - For dimensions (width, height): ceil to avoid gaps at bottom/right
3370
+ int x = Math.max(
3371
+ 0,
3372
+ (int) Math.ceil((actualX - webViewLeftInset) / pixelRatio)
3373
+ );
3374
+ int y = Math.max(
3375
+ 0,
3376
+ (int) Math.ceil((actualY - webViewTopInset) / pixelRatio)
3377
+ );
3378
+ int width = (int) Math.floor(actualWidth / pixelRatio);
3379
+ int height = (int) Math.floor(actualHeight / pixelRatio);
2801
3380
 
2802
3381
  return new int[] { x, y, width, height };
2803
3382
  }
@@ -2843,12 +3422,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2843
3422
  MeteringPointFactory factory = previewView.getMeteringPointFactory();
2844
3423
  MeteringPoint point = factory.createPoint(viewWidth / 2f, viewHeight / 2f);
2845
3424
 
2846
- // Create focus and metering action
3425
+ // Create focus and metering action (persistent, no auto-cancel) to match iOS behavior
2847
3426
  FocusMeteringAction action = new FocusMeteringAction.Builder(
2848
3427
  point,
2849
3428
  FocusMeteringAction.FLAG_AF | FocusMeteringAction.FLAG_AE
2850
3429
  )
2851
- .setAutoCancelDuration(3, TimeUnit.SECONDS) // Auto-cancel after 3 seconds
3430
+ .disableAutoCancel()
2852
3431
  .build();
2853
3432
 
2854
3433
  try {