@capgo/camera-preview 7.4.0-alpha.18 → 7.4.0-alpha.21

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 {
@@ -277,7 +270,15 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
277
270
 
278
271
  // Create and setup the preview view
279
272
  previewView = new PreviewView(context);
280
- previewView.setScaleType(PreviewView.ScaleType.FIT_CENTER);
273
+ // Match iOS behavior: FIT when no aspect ratio, FILL when aspect ratio is set
274
+ String initialAspectRatio = sessionConfig != null
275
+ ? sessionConfig.getAspectRatio()
276
+ : null;
277
+ previewView.setScaleType(
278
+ (initialAspectRatio == null || initialAspectRatio.isEmpty())
279
+ ? PreviewView.ScaleType.FIT_CENTER
280
+ : PreviewView.ScaleType.FILL_CENTER
281
+ );
281
282
  // Also make preview view touchable as backup
282
283
  previewView.setClickable(true);
283
284
  previewView.setFocusable(true);
@@ -434,9 +435,45 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
434
435
  int height = sessionConfig.getHeight();
435
436
  String aspectRatio = sessionConfig.getAspectRatio();
436
437
 
438
+ // Get comprehensive display information
439
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
440
+ int screenWidthPx = metrics.widthPixels;
441
+ int screenHeightPx = metrics.heightPixels;
442
+ float density = metrics.density;
443
+ int screenWidthDp = (int) (screenWidthPx / density);
444
+ int screenHeightDp = (int) (screenHeightPx / density);
445
+
446
+ // Get WebView dimensions
447
+ int webViewWidth = webView != null ? webView.getWidth() : 0;
448
+ int webViewHeight = webView != null ? webView.getHeight() : 0;
449
+
450
+ // Get parent dimensions
451
+ ViewGroup parent = (ViewGroup) webView.getParent();
452
+ int parentWidth = parent != null ? parent.getWidth() : 0;
453
+ int parentHeight = parent != null ? parent.getHeight() : 0;
454
+
455
+ Log.d(
456
+ TAG,
457
+ "======================== CALCULATE PREVIEW LAYOUT PARAMS ========================"
458
+ );
437
459
  Log.d(
438
460
  TAG,
439
- "calculatePreviewLayoutParams: Using sessionConfig values - x:" +
461
+ "Screen dimensions - Pixels: " +
462
+ screenWidthPx +
463
+ "x" +
464
+ screenHeightPx +
465
+ ", DP: " +
466
+ screenWidthDp +
467
+ "x" +
468
+ screenHeightDp +
469
+ ", Density: " +
470
+ density
471
+ );
472
+ Log.d(TAG, "WebView dimensions: " + webViewWidth + "x" + webViewHeight);
473
+ Log.d(TAG, "Parent dimensions: " + parentWidth + "x" + parentHeight);
474
+ Log.d(
475
+ TAG,
476
+ "SessionConfig values - x:" +
440
477
  x +
441
478
  " y:" +
442
479
  y +
@@ -445,83 +482,97 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
445
482
  " height:" +
446
483
  height +
447
484
  " aspectRatio:" +
448
- aspectRatio
485
+ aspectRatio +
486
+ " isCentered:" +
487
+ sessionConfig.isCentered()
449
488
  );
450
489
 
451
- // Apply aspect ratio if specified and no explicit size was given
452
- if (aspectRatio != null && !aspectRatio.isEmpty()) {
490
+ // Apply aspect ratio if specified
491
+ if (
492
+ aspectRatio != null &&
493
+ !aspectRatio.isEmpty() &&
494
+ sessionConfig.isCentered()
495
+ ) {
453
496
  String[] ratios = aspectRatio.split(":");
454
497
  if (ratios.length == 2) {
455
498
  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
- }
499
+ // Match iOS logic exactly
500
+ double ratioWidth = Double.parseDouble(ratios[0]);
501
+ double ratioHeight = Double.parseDouble(ratios[1]);
502
+ boolean isPortrait =
503
+ context.getResources().getConfiguration().orientation ==
504
+ Configuration.ORIENTATION_PORTRAIT;
469
505
 
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;
506
+ Log.d(
507
+ TAG,
508
+ "Aspect ratio parsing - Original: " +
509
+ aspectRatio +
510
+ " (width=" +
511
+ ratioWidth +
512
+ ", height=" +
513
+ ratioHeight +
514
+ ")"
515
+ );
516
+ Log.d(
517
+ TAG,
518
+ "Device orientation: " + (isPortrait ? "PORTRAIT" : "LANDSCAPE")
519
+ );
475
520
 
476
- // If we're centered and dimensions changed, recalculate position
477
- if (sessionConfig.isCentered()) {
478
- DisplayMetrics metrics = context.getResources().getDisplayMetrics();
521
+ // iOS: let ratio = !isPortrait ? ratioParts[0] / ratioParts[1] : ratioParts[1] / ratioParts[0]
522
+ double ratio = !isPortrait
523
+ ? (ratioWidth / ratioHeight)
524
+ : (ratioHeight / ratioWidth);
479
525
 
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
- }
526
+ Log.d(
527
+ TAG,
528
+ "Computed ratio: " +
529
+ ratio +
530
+ " (iOS formula: " +
531
+ (!isPortrait ? "width/height" : "height/width") +
532
+ ")"
533
+ );
496
534
 
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
- }
535
+ // For centered mode with aspect ratio, calculate maximum size that fits
536
+ int availableWidth = metrics.widthPixels;
537
+ int availableHeight = metrics.heightPixels;
515
538
 
516
539
  Log.d(
517
540
  TAG,
518
- "calculatePreviewLayoutParams: Applied aspect ratio " +
519
- aspectRatio +
520
- " - new size: " +
521
- width +
541
+ "Available space for preview: " +
542
+ availableWidth +
522
543
  "x" +
523
- height
544
+ availableHeight
524
545
  );
546
+
547
+ // Calculate maximum size that fits the aspect ratio in available space
548
+ double maxWidthByHeight = availableHeight * ratio;
549
+ double maxHeightByWidth = availableWidth / ratio;
550
+
551
+ Log.d(
552
+ TAG,
553
+ "Aspect ratio calculations - maxWidthByHeight: " +
554
+ maxWidthByHeight +
555
+ ", maxHeightByWidth: " +
556
+ maxHeightByWidth
557
+ );
558
+
559
+ if (maxWidthByHeight <= availableWidth) {
560
+ // Height is the limiting factor
561
+ width = (int) maxWidthByHeight;
562
+ height = availableHeight;
563
+ Log.d(TAG, "Height-limited sizing: " + width + "x" + height);
564
+ } else {
565
+ // Width is the limiting factor
566
+ width = availableWidth;
567
+ height = (int) maxHeightByWidth;
568
+ Log.d(TAG, "Width-limited sizing: " + width + "x" + height);
569
+ }
570
+
571
+ // Center the preview
572
+ x = (availableWidth - width) / 2;
573
+ y = (availableHeight - height) / 2;
574
+
575
+ Log.d(TAG, "Auto-centered position: x=" + x + ", y=" + y);
525
576
  } catch (NumberFormatException e) {
526
577
  Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio, e);
527
578
  }
@@ -540,28 +591,20 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
540
591
 
541
592
  Log.d(
542
593
  TAG,
543
- "calculatePreviewLayoutParams: Position calculation - x:" +
544
- x +
545
- " (leftMargin=" +
594
+ "Final layout params - Margins: left=" +
546
595
  layoutParams.leftMargin +
547
- "), y:" +
548
- y +
549
- " (topMargin=" +
596
+ ", top=" +
550
597
  layoutParams.topMargin +
551
- ")"
598
+ ", Size: " +
599
+ width +
600
+ "x" +
601
+ height
552
602
  );
553
-
554
603
  Log.d(
555
604
  TAG,
556
- "calculatePreviewLayoutParams: Final layout - x:" +
557
- x +
558
- " y:" +
559
- y +
560
- " width:" +
561
- width +
562
- " height:" +
563
- height
605
+ "================================================================================"
564
606
  );
607
+
565
608
  return layoutParams;
566
609
  }
567
610
 
@@ -623,13 +666,19 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
623
666
  ResolutionSelector resolutionSelector =
624
667
  resolutionSelectorBuilder.build();
625
668
 
669
+ int rotation = previewView != null && previewView.getDisplay() != null
670
+ ? previewView.getDisplay().getRotation()
671
+ : android.view.Surface.ROTATION_0;
672
+
626
673
  Preview preview = new Preview.Builder()
627
674
  .setResolutionSelector(resolutionSelector)
675
+ .setTargetRotation(rotation)
628
676
  .build();
629
677
  imageCapture = new ImageCapture.Builder()
630
678
  .setResolutionSelector(resolutionSelector)
631
679
  .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
632
680
  .setFlashMode(currentFlashMode)
681
+ .setTargetRotation(rotation)
633
682
  .build();
634
683
  sampleImageCapture = imageCapture;
635
684
  preview.setSurfaceProvider(previewView.getSurfaceProvider());
@@ -688,6 +737,50 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
688
737
  if (previewResolution != null) {
689
738
  currentPreviewResolution = previewResolution.getResolution();
690
739
  Log.d(TAG, "Preview resolution: " + currentPreviewResolution);
740
+
741
+ // Log the actual aspect ratio of the selected resolution
742
+ if (currentPreviewResolution != null) {
743
+ double actualRatio =
744
+ (double) currentPreviewResolution.getWidth() /
745
+ (double) currentPreviewResolution.getHeight();
746
+ Log.d(
747
+ TAG,
748
+ "Actual preview aspect ratio: " +
749
+ actualRatio +
750
+ " (width=" +
751
+ currentPreviewResolution.getWidth() +
752
+ ", height=" +
753
+ currentPreviewResolution.getHeight() +
754
+ ")"
755
+ );
756
+
757
+ // Compare with requested ratio
758
+ if ("4:3".equals(sessionConfig.getAspectRatio())) {
759
+ double expectedRatio = 4.0 / 3.0;
760
+ double difference = Math.abs(actualRatio - expectedRatio);
761
+ Log.d(
762
+ TAG,
763
+ "4:3 ratio check - Expected: " +
764
+ expectedRatio +
765
+ ", Actual: " +
766
+ actualRatio +
767
+ ", Difference: " +
768
+ difference
769
+ );
770
+ } else if ("16:9".equals(sessionConfig.getAspectRatio())) {
771
+ double expectedRatio = 16.0 / 9.0;
772
+ double difference = Math.abs(actualRatio - expectedRatio);
773
+ Log.d(
774
+ TAG,
775
+ "16:9 ratio check - Expected: " +
776
+ expectedRatio +
777
+ ", Actual: " +
778
+ actualRatio +
779
+ ", Difference: " +
780
+ difference
781
+ );
782
+ }
783
+ }
691
784
  }
692
785
  ResolutionInfo imageCaptureResolution =
693
786
  imageCapture.getResolutionInfo();
@@ -699,6 +792,16 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
699
792
  );
700
793
  }
701
794
 
795
+ // Update scale type based on aspect ratio whenever (re)binding
796
+ String ar = sessionConfig != null
797
+ ? sessionConfig.getAspectRatio()
798
+ : null;
799
+ previewView.setScaleType(
800
+ (ar == null || ar.isEmpty())
801
+ ? PreviewView.ScaleType.FIT_CENTER
802
+ : PreviewView.ScaleType.FILL_CENTER
803
+ );
804
+
702
805
  // Set initial zoom if specified, prioritizing targetZoom over default zoomFactor
703
806
  float initialZoom = sessionConfig.getTargetZoom() != 1.0f
704
807
  ? sessionConfig.getTargetZoom()
@@ -914,26 +1017,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
914
1017
 
915
1018
  JSONObject exifData = getExifData(exifInterface);
916
1019
 
917
- // Use the stored aspectRatio if none is provided and no width/height is specified
1020
+ // Determine final output: explicit size wins, then explicit aspectRatio,
1021
+ // otherwise crop to match what is visible in the preview (iOS parity)
918
1022
  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
1023
  if (
938
1024
  width == null &&
939
1025
  height == null &&
@@ -1027,14 +1113,22 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1027
1113
  // Write EXIF data back to resized image
1028
1114
  bytes = writeExifToImageBytes(bytes, exifInterface);
1029
1115
  } 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
1116
+ // No explicit size/ratio: crop to match current preview content
1117
+ Bitmap originalBitmap = BitmapFactory.decodeByteArray(
1118
+ bytes,
1119
+ 0,
1120
+ bytes.length
1121
+ );
1122
+ Bitmap previewCropped = cropBitmapToMatchPreview(originalBitmap);
1123
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
1124
+ previewCropped.compress(
1125
+ Bitmap.CompressFormat.JPEG,
1126
+ quality,
1127
+ stream
1035
1128
  );
1036
- fis2.read(bytes);
1037
- fis2.close();
1129
+ bytes = stream.toByteArray();
1130
+ // Preserve EXIF
1131
+ bytes = writeExifToImageBytes(bytes, exifInterface);
1038
1132
  }
1039
1133
 
1040
1134
  if (saveToGallery) {
@@ -1380,6 +1474,49 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1380
1474
  return bytes;
1381
1475
  }
1382
1476
 
1477
+ private Bitmap cropBitmapToMatchPreview(Bitmap image) {
1478
+ if (previewContainer == null || previewView == null) {
1479
+ return image;
1480
+ }
1481
+ int containerWidth = previewContainer.getWidth();
1482
+ int containerHeight = previewContainer.getHeight();
1483
+ if (containerWidth == 0 || containerHeight == 0) {
1484
+ return image;
1485
+ }
1486
+ // Compute preview aspect based on actual camera content bounds
1487
+ Rect bounds = getActualCameraBounds();
1488
+ int previewW = Math.max(1, bounds.width());
1489
+ int previewH = Math.max(1, bounds.height());
1490
+ float previewRatio = (float) previewW / (float) previewH;
1491
+
1492
+ int imgW = image.getWidth();
1493
+ int imgH = image.getHeight();
1494
+ float imgRatio = (float) imgW / (float) imgH;
1495
+
1496
+ int targetW = imgW;
1497
+ int targetH = imgH;
1498
+ if (imgRatio > previewRatio) {
1499
+ // Image wider than preview: crop width
1500
+ targetW = Math.round(imgH * previewRatio);
1501
+ } else if (imgRatio < previewRatio) {
1502
+ // Image taller than preview: crop height
1503
+ targetH = Math.round(imgW / previewRatio);
1504
+ }
1505
+ int x = Math.max(0, (imgW - targetW) / 2);
1506
+ int y = Math.max(0, (imgH - targetH) / 2);
1507
+ try {
1508
+ return Bitmap.createBitmap(
1509
+ image,
1510
+ x,
1511
+ y,
1512
+ Math.min(targetW, imgW - x),
1513
+ Math.min(targetH, imgH - y)
1514
+ );
1515
+ } catch (Exception ignore) {
1516
+ return image;
1517
+ }
1518
+ }
1519
+
1383
1520
  // not workin for xiaomi https://xiaomi.eu/community/threads/mi-11-ultra-unable-to-access-camera-lenses-in-apps-camera2-api.61456/
1384
1521
  @OptIn(markerClass = ExperimentalCamera2Interop.class)
1385
1522
  public static List<
@@ -1698,12 +1835,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1698
1835
  MeteringPointFactory factory = previewView.getMeteringPointFactory();
1699
1836
  MeteringPoint point = factory.createPoint(x * viewWidth, y * viewHeight);
1700
1837
 
1701
- // Create focus and metering action
1838
+ // Create focus and metering action (persistent, no auto-cancel) to match iOS behavior
1702
1839
  FocusMeteringAction action = new FocusMeteringAction.Builder(
1703
1840
  point,
1704
1841
  FocusMeteringAction.FLAG_AF | FocusMeteringAction.FLAG_AE
1705
1842
  )
1706
- .setAutoCancelDuration(3, TimeUnit.SECONDS) // Auto-cancel after 3 seconds
1843
+ .disableAutoCancel()
1707
1844
  .build();
1708
1845
 
1709
1846
  try {
@@ -1777,8 +1914,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1777
1914
  }
1778
1915
 
1779
1916
  // Create an elegant focus indicator
1780
- View container = new View(context);
1781
- int size = (int) (60 * context.getResources().getDisplayMetrics().density); // 60dp size
1917
+ FrameLayout container = new FrameLayout(context);
1918
+ int size = (int) (80 * context.getResources().getDisplayMetrics().density); // match iOS size
1782
1919
  FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(size, size);
1783
1920
 
1784
1921
  // Center the indicator on the touch point with bounds checking
@@ -1794,25 +1931,73 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1794
1931
  Math.min((int) (y - size / 2), containerHeight - size)
1795
1932
  );
1796
1933
 
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);
1934
+ // iOS Camera style: square with mid-edge ticks
1935
+ GradientDrawable border = new GradientDrawable();
1936
+ border.setShape(GradientDrawable.RECTANGLE);
1937
+ int stroke = (int) (2 * context.getResources().getDisplayMetrics().density);
1938
+ border.setStroke(stroke, Color.YELLOW);
1939
+ border.setCornerRadius(0);
1940
+ border.setColor(Color.TRANSPARENT);
1941
+ container.setBackground(border);
1942
+
1943
+ // Add 4 tiny mid-edge ticks inside the square
1944
+ int tickLen = (int) (12 *
1945
+ context.getResources().getDisplayMetrics().density);
1946
+ int inset = stroke; // ticks should touch the sides
1947
+ // Top tick (perpendicular): vertical inward from top edge
1948
+ View topTick = new View(context);
1949
+ FrameLayout.LayoutParams topParams = new FrameLayout.LayoutParams(
1950
+ stroke,
1951
+ tickLen
1952
+ );
1953
+ topParams.leftMargin = (size - stroke) / 2;
1954
+ topParams.topMargin = inset;
1955
+ topTick.setLayoutParams(topParams);
1956
+ topTick.setBackgroundColor(Color.YELLOW);
1957
+ container.addView(topTick);
1958
+ // Bottom tick (perpendicular): vertical inward from bottom edge
1959
+ View bottomTick = new View(context);
1960
+ FrameLayout.LayoutParams bottomParams = new FrameLayout.LayoutParams(
1961
+ stroke,
1962
+ tickLen
1963
+ );
1964
+ bottomParams.leftMargin = (size - stroke) / 2;
1965
+ bottomParams.topMargin = size - inset - tickLen;
1966
+ bottomTick.setLayoutParams(bottomParams);
1967
+ bottomTick.setBackgroundColor(Color.YELLOW);
1968
+ container.addView(bottomTick);
1969
+ // Left tick (perpendicular): horizontal inward from left edge
1970
+ View leftTick = new View(context);
1971
+ FrameLayout.LayoutParams leftParams = new FrameLayout.LayoutParams(
1972
+ tickLen,
1973
+ stroke
1974
+ );
1975
+ leftParams.leftMargin = inset;
1976
+ leftParams.topMargin = (size - stroke) / 2;
1977
+ leftTick.setLayoutParams(leftParams);
1978
+ leftTick.setBackgroundColor(Color.YELLOW);
1979
+ container.addView(leftTick);
1980
+ // Right tick (perpendicular): horizontal inward from right edge
1981
+ View rightTick = new View(context);
1982
+ FrameLayout.LayoutParams rightParams = new FrameLayout.LayoutParams(
1983
+ tickLen,
1984
+ stroke
1985
+ );
1986
+ rightParams.leftMargin = size - inset - tickLen;
1987
+ rightParams.topMargin = (size - stroke) / 2;
1988
+ rightTick.setLayoutParams(rightParams);
1989
+ rightTick.setBackgroundColor(Color.YELLOW);
1990
+ container.addView(rightTick);
1806
1991
 
1807
1992
  focusIndicatorView = container;
1808
1993
  // Bump animation token; everything after this must validate against this token
1809
1994
  final long thisAnimationId = ++focusIndicatorAnimationId;
1810
1995
  final View thisIndicatorView = focusIndicatorView;
1811
1996
 
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);
1997
+ // Set initial state for smooth animation (mirror iOS)
1998
+ focusIndicatorView.setAlpha(0f);
1999
+ focusIndicatorView.setScaleX(1.5f);
2000
+ focusIndicatorView.setScaleY(1.5f);
1816
2001
  focusIndicatorView.setVisibility(View.VISIBLE);
1817
2002
 
1818
2003
  // Ensure container doesn't intercept touch events
@@ -1836,26 +2021,16 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1836
2021
  // Force a layout pass to ensure the view is properly positioned
1837
2022
  previewContainer.requestLayout();
1838
2023
 
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);
2024
+ // First phase: fade in and scale to 1.0 over 150ms
2025
+ focusIndicatorView
2026
+ .animate()
2027
+ .alpha(1f)
2028
+ .scaleX(1f)
2029
+ .scaleY(1f)
2030
+ .setDuration(150)
2031
+ .start();
1857
2032
 
1858
- // Schedule fast fade out and removal
2033
+ // Phase 2: after 500ms, fade to 0.3 over 200ms
1859
2034
  focusIndicatorView.postDelayed(
1860
2035
  new Runnable() {
1861
2036
  @Override
@@ -1868,37 +2043,66 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1868
2043
  ) {
1869
2044
  focusIndicatorView
1870
2045
  .animate()
1871
- .alpha(0f)
1872
- .scaleX(0.9f)
1873
- .scaleY(0.9f)
1874
- .setDuration(180)
1875
- .setInterpolator(
1876
- new android.view.animation.AccelerateInterpolator()
1877
- )
2046
+ .alpha(0.3f)
2047
+ .setDuration(200)
1878
2048
  .withEndAction(
1879
2049
  new Runnable() {
1880
2050
  @Override
1881
2051
  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
- }
2052
+ // Phase 3: after 200ms more, fade out to 0 and scale to 0.8 over 300ms
2053
+ focusIndicatorView.postDelayed(
2054
+ new Runnable() {
2055
+ @Override
2056
+ public void run() {
2057
+ if (
2058
+ focusIndicatorView != null &&
2059
+ thisIndicatorView == focusIndicatorView &&
2060
+ thisAnimationId == focusIndicatorAnimationId
2061
+ ) {
2062
+ focusIndicatorView
2063
+ .animate()
2064
+ .alpha(0f)
2065
+ .scaleX(0.8f)
2066
+ .scaleY(0.8f)
2067
+ .setDuration(300)
2068
+ .setInterpolator(
2069
+ new android.view.animation.AccelerateInterpolator()
2070
+ )
2071
+ .withEndAction(
2072
+ new Runnable() {
2073
+ @Override
2074
+ public void run() {
2075
+ if (
2076
+ focusIndicatorView != null &&
2077
+ previewContainer != null &&
2078
+ thisIndicatorView == focusIndicatorView &&
2079
+ thisAnimationId ==
2080
+ focusIndicatorAnimationId
2081
+ ) {
2082
+ try {
2083
+ focusIndicatorView.clearAnimation();
2084
+ } catch (Exception ignore) {}
2085
+ previewContainer.removeView(
2086
+ focusIndicatorView
2087
+ );
2088
+ focusIndicatorView = null;
2089
+ }
2090
+ }
2091
+ }
2092
+ );
2093
+ }
2094
+ }
2095
+ },
2096
+ 200
2097
+ );
1894
2098
  }
1895
2099
  }
1896
2100
  );
1897
2101
  }
1898
2102
  }
1899
2103
  },
1900
- 250
1901
- ); // Faster feedback
2104
+ 500
2105
+ );
1902
2106
  }
1903
2107
 
1904
2108
  public static List<Size> getSupportedPictureSizes(String facing) {
@@ -2149,13 +2353,48 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2149
2353
  Float y,
2150
2354
  Runnable callback
2151
2355
  ) {
2356
+ Log.d(
2357
+ TAG,
2358
+ "======================== SET ASPECT RATIO ========================"
2359
+ );
2360
+ Log.d(
2361
+ TAG,
2362
+ "Input parameters - aspectRatio: " +
2363
+ aspectRatio +
2364
+ ", x: " +
2365
+ x +
2366
+ ", y: " +
2367
+ y
2368
+ );
2369
+
2152
2370
  if (sessionConfig == null) {
2371
+ Log.d(TAG, "SessionConfig is null, returning");
2153
2372
  if (callback != null) callback.run();
2154
2373
  return;
2155
2374
  }
2156
2375
 
2157
2376
  String currentAspectRatio = sessionConfig.getAspectRatio();
2158
2377
 
2378
+ // Get current display information
2379
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
2380
+ int screenWidthPx = metrics.widthPixels;
2381
+ int screenHeightPx = metrics.heightPixels;
2382
+ boolean isPortrait =
2383
+ context.getResources().getConfiguration().orientation ==
2384
+ Configuration.ORIENTATION_PORTRAIT;
2385
+
2386
+ Log.d(
2387
+ TAG,
2388
+ "Current screen: " +
2389
+ screenWidthPx +
2390
+ "x" +
2391
+ screenHeightPx +
2392
+ " (" +
2393
+ (isPortrait ? "PORTRAIT" : "LANDSCAPE") +
2394
+ ")"
2395
+ );
2396
+ Log.d(TAG, "Current aspect ratio: " + currentAspectRatio);
2397
+
2159
2398
  // Don't restart camera if aspect ratio hasn't changed and no position specified
2160
2399
  if (
2161
2400
  aspectRatio != null &&
@@ -2163,12 +2402,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2163
2402
  x == null &&
2164
2403
  y == null
2165
2404
  ) {
2166
- Log.d(
2167
- TAG,
2168
- "setAspectRatio: Aspect ratio " +
2169
- aspectRatio +
2170
- " is already set and no position specified, skipping"
2171
- );
2405
+ Log.d(TAG, "Aspect ratio unchanged and no position specified, skipping");
2172
2406
  if (callback != null) callback.run();
2173
2407
  return;
2174
2408
  }
@@ -2176,22 +2410,16 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2176
2410
  String currentGridMode = sessionConfig.getGridMode();
2177
2411
  Log.d(
2178
2412
  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
2413
+ "Changing aspect ratio from " + currentAspectRatio + " to " + aspectRatio
2188
2414
  );
2415
+ Log.d(TAG, "Auto-centering will be applied (matching iOS behavior)");
2189
2416
 
2417
+ // Match iOS behavior: when aspect ratio changes, always auto-center
2190
2418
  sessionConfig = new CameraSessionConfiguration(
2191
2419
  sessionConfig.getDeviceId(),
2192
2420
  sessionConfig.getPosition(),
2193
- sessionConfig.getX(),
2194
- sessionConfig.getY(),
2421
+ -1, // Force auto-center X (iOS: self.posX = -1)
2422
+ -1, // Force auto-center Y (iOS: self.posY = -1)
2195
2423
  sessionConfig.getWidth(),
2196
2424
  sessionConfig.getHeight(),
2197
2425
  sessionConfig.getPaddingBottom(),
@@ -2205,12 +2433,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2205
2433
  aspectRatio,
2206
2434
  currentGridMode
2207
2435
  );
2436
+ sessionConfig.setCentered(true);
2208
2437
 
2209
2438
  // Update layout and rebind camera with new aspect ratio
2210
2439
  if (isRunning && previewContainer != null) {
2211
2440
  mainExecutor.execute(() -> {
2212
- // First update the UI layout
2213
- updatePreviewLayoutForAspectRatio(aspectRatio, x, y);
2441
+ // First update the UI layout - always pass null for x,y to force auto-centering (matching iOS)
2442
+ updatePreviewLayoutForAspectRatio(aspectRatio, null, null);
2214
2443
 
2215
2444
  // Then rebind the camera with new aspect ratio configuration
2216
2445
  Log.d(
@@ -2240,8 +2469,121 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2240
2469
  previewContainer.post(callback);
2241
2470
  }
2242
2471
  }
2472
+
2473
+ Log.d(
2474
+ TAG,
2475
+ "=================================================================="
2476
+ );
2243
2477
  });
2244
2478
  } else {
2479
+ Log.d(TAG, "Camera not running, just saving configuration");
2480
+ Log.d(
2481
+ TAG,
2482
+ "=================================================================="
2483
+ );
2484
+ if (callback != null) callback.run();
2485
+ }
2486
+ }
2487
+
2488
+ // Force aspect ratio recalculation (used during orientation changes)
2489
+ public void forceAspectRatioRecalculation(
2490
+ String aspectRatio,
2491
+ Float x,
2492
+ Float y,
2493
+ Runnable callback
2494
+ ) {
2495
+ Log.d(
2496
+ TAG,
2497
+ "======================== FORCE ASPECT RATIO RECALCULATION ========================"
2498
+ );
2499
+ Log.d(
2500
+ TAG,
2501
+ "Input parameters - aspectRatio: " +
2502
+ aspectRatio +
2503
+ ", x: " +
2504
+ x +
2505
+ ", y: " +
2506
+ y
2507
+ );
2508
+
2509
+ if (sessionConfig == null) {
2510
+ Log.d(TAG, "SessionConfig is null, returning");
2511
+ if (callback != null) callback.run();
2512
+ return;
2513
+ }
2514
+
2515
+ String currentGridMode = sessionConfig.getGridMode();
2516
+ Log.d(TAG, "Forcing aspect ratio recalculation for: " + aspectRatio);
2517
+ Log.d(TAG, "Auto-centering will be applied (matching iOS behavior)");
2518
+
2519
+ // Match iOS behavior: when aspect ratio changes, always auto-center
2520
+ sessionConfig = new CameraSessionConfiguration(
2521
+ sessionConfig.getDeviceId(),
2522
+ sessionConfig.getPosition(),
2523
+ -1, // Force auto-center X (iOS: self.posX = -1)
2524
+ -1, // Force auto-center Y (iOS: self.posY = -1)
2525
+ sessionConfig.getWidth(),
2526
+ sessionConfig.getHeight(),
2527
+ sessionConfig.getPaddingBottom(),
2528
+ sessionConfig.getToBack(),
2529
+ sessionConfig.getStoreToFile(),
2530
+ sessionConfig.getEnableOpacity(),
2531
+ sessionConfig.getEnableZoom(),
2532
+ sessionConfig.getDisableExifHeaderStripping(),
2533
+ sessionConfig.getDisableAudio(),
2534
+ sessionConfig.getZoomFactor(),
2535
+ aspectRatio,
2536
+ currentGridMode
2537
+ );
2538
+ sessionConfig.setCentered(true);
2539
+
2540
+ // Update layout and rebind camera with new aspect ratio
2541
+ if (isRunning && previewContainer != null) {
2542
+ mainExecutor.execute(() -> {
2543
+ // First update the UI layout - always pass null for x,y to force auto-centering (matching iOS)
2544
+ updatePreviewLayoutForAspectRatio(aspectRatio, null, null);
2545
+
2546
+ // Then rebind the camera with new aspect ratio configuration
2547
+ Log.d(
2548
+ TAG,
2549
+ "forceAspectRatioRecalculation: Rebinding camera with aspect ratio: " +
2550
+ aspectRatio
2551
+ );
2552
+ bindCameraUseCases();
2553
+
2554
+ // Preserve grid mode and wait for completion
2555
+ if (gridOverlayView != null) {
2556
+ gridOverlayView.post(() -> {
2557
+ Log.d(
2558
+ TAG,
2559
+ "forceAspectRatioRecalculation: Re-applying grid mode: " +
2560
+ currentGridMode
2561
+ );
2562
+ gridOverlayView.setGridMode(currentGridMode);
2563
+
2564
+ // Wait one more frame for grid to be applied, then call callback
2565
+ if (callback != null) {
2566
+ gridOverlayView.post(callback);
2567
+ }
2568
+ });
2569
+ } else {
2570
+ // No grid overlay, wait one frame for layout completion then call callback
2571
+ if (callback != null) {
2572
+ previewContainer.post(callback);
2573
+ }
2574
+ }
2575
+
2576
+ Log.d(
2577
+ TAG,
2578
+ "=================================================================="
2579
+ );
2580
+ });
2581
+ } else {
2582
+ Log.d(TAG, "Camera not running, just saving configuration");
2583
+ Log.d(
2584
+ TAG,
2585
+ "=================================================================="
2586
+ );
2245
2587
  if (callback != null) callback.run();
2246
2588
  }
2247
2589
  }
@@ -2352,15 +2694,40 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2352
2694
  return new Rect(0, 0, containerWidth, containerHeight);
2353
2695
  }
2354
2696
 
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();
2697
+ // CameraX delivers preview in sensor orientation (always landscape)
2698
+ // But PreviewView internally rotates it to match device orientation
2699
+ // So we need to swap dimensions in portrait mode
2700
+ int cameraWidth = currentPreviewResolution.getWidth();
2701
+ int cameraHeight = currentPreviewResolution.getHeight();
2702
+
2703
+ // Check if we're in portrait mode
2704
+ boolean isPortrait =
2705
+ context.getResources().getConfiguration().orientation ==
2706
+ Configuration.ORIENTATION_PORTRAIT;
2707
+
2708
+ // Swap dimensions if in portrait mode to match how PreviewView displays it
2709
+ if (isPortrait) {
2710
+ int temp = cameraWidth;
2711
+ cameraWidth = cameraHeight;
2712
+ cameraHeight = temp;
2713
+ }
2714
+
2715
+ // When we have an aspect ratio set, we use FILL_CENTER which scales to fill
2716
+ // the container while maintaining aspect ratio, potentially cropping
2717
+ boolean usesFillCenter =
2718
+ sessionConfig != null && sessionConfig.getAspectRatio() != null;
2359
2719
 
2360
- // Calculate the scaling factor to fit the camera in the container
2361
2720
  float widthScale = (float) containerWidth / cameraWidth;
2362
2721
  float heightScale = (float) containerHeight / cameraHeight;
2363
- float scale = Math.min(widthScale, heightScale); // FIT_CENTER uses min scale
2722
+ float scale;
2723
+
2724
+ if (usesFillCenter) {
2725
+ // FILL_CENTER uses max scale to fill the container
2726
+ scale = Math.max(widthScale, heightScale);
2727
+ } else {
2728
+ // FIT_CENTER uses min scale to fit within the container
2729
+ scale = Math.min(widthScale, heightScale);
2730
+ }
2364
2731
 
2365
2732
  // Calculate the actual size of the camera content after scaling
2366
2733
  int scaledWidth = Math.round(cameraWidth * scale);
@@ -2380,8 +2747,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2380
2747
  cameraWidth +
2381
2748
  "x" +
2382
2749
  cameraHeight +
2750
+ " (swapped=" +
2751
+ isPortrait +
2752
+ ")" +
2383
2753
  ", scale=" +
2384
2754
  scale +
2755
+ " (fillCenter=" +
2756
+ usesFillCenter +
2757
+ ")" +
2385
2758
  ", scaled=" +
2386
2759
  scaledWidth +
2387
2760
  "x" +
@@ -2395,10 +2768,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2395
2768
 
2396
2769
  // Return the bounds relative to the container
2397
2770
  return new Rect(
2398
- offsetX,
2399
- offsetY,
2400
- offsetX + scaledWidth,
2401
- offsetY + scaledHeight
2771
+ Math.max(0, offsetX),
2772
+ Math.max(0, offsetY),
2773
+ Math.min(containerWidth, offsetX + scaledWidth),
2774
+ Math.min(containerHeight, offsetY + scaledHeight)
2402
2775
  );
2403
2776
  }
2404
2777
 
@@ -2630,27 +3003,137 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2630
3003
  ) {
2631
3004
  if (previewContainer == null || aspectRatio == null) return;
2632
3005
 
2633
- // Parse aspect ratio
3006
+ Log.d(
3007
+ TAG,
3008
+ "======================== UPDATE PREVIEW LAYOUT FOR ASPECT RATIO ========================"
3009
+ );
3010
+ Log.d(
3011
+ TAG,
3012
+ "Input parameters - aspectRatio: " +
3013
+ aspectRatio +
3014
+ ", x: " +
3015
+ x +
3016
+ ", y: " +
3017
+ y
3018
+ );
3019
+
3020
+ // Get comprehensive display information
3021
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
3022
+ int screenWidthPx = metrics.widthPixels;
3023
+ int screenHeightPx = metrics.heightPixels;
3024
+ float density = metrics.density;
3025
+
3026
+ // Get WebView dimensions
3027
+ int webViewWidth = webView.getWidth();
3028
+ int webViewHeight = webView.getHeight();
3029
+
3030
+ // Get current preview container info
3031
+ ViewGroup.LayoutParams currentParams = previewContainer.getLayoutParams();
3032
+ int currentWidth = currentParams != null ? currentParams.width : 0;
3033
+ int currentHeight = currentParams != null ? currentParams.height : 0;
3034
+ int currentX = 0;
3035
+ int currentY = 0;
3036
+ if (currentParams instanceof ViewGroup.MarginLayoutParams) {
3037
+ ViewGroup.MarginLayoutParams marginParams =
3038
+ (ViewGroup.MarginLayoutParams) currentParams;
3039
+ currentX = marginParams.leftMargin;
3040
+ currentY = marginParams.topMargin;
3041
+ }
3042
+
3043
+ Log.d(
3044
+ TAG,
3045
+ "Screen dimensions: " +
3046
+ screenWidthPx +
3047
+ "x" +
3048
+ screenHeightPx +
3049
+ " pixels, density: " +
3050
+ density
3051
+ );
3052
+ Log.d(TAG, "WebView dimensions: " + webViewWidth + "x" + webViewHeight);
3053
+ Log.d(
3054
+ TAG,
3055
+ "Current preview position: " +
3056
+ currentX +
3057
+ "," +
3058
+ currentY +
3059
+ " size: " +
3060
+ currentWidth +
3061
+ "x" +
3062
+ currentHeight
3063
+ );
3064
+
3065
+ // Parse aspect ratio as width:height (e.g., 4:3 -> r=4/3)
2634
3066
  String[] ratios = aspectRatio.split(":");
2635
- if (ratios.length != 2) return;
3067
+ if (ratios.length != 2) {
3068
+ Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio);
3069
+ return;
3070
+ }
2636
3071
 
2637
3072
  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]);
3073
+ // Match iOS logic exactly
3074
+ double ratioWidth = Double.parseDouble(ratios[0]);
3075
+ double ratioHeight = Double.parseDouble(ratios[1]);
3076
+ boolean isPortrait =
3077
+ context.getResources().getConfiguration().orientation ==
3078
+ Configuration.ORIENTATION_PORTRAIT;
3079
+
3080
+ Log.d(
3081
+ TAG,
3082
+ "Aspect ratio parsing - Original: " +
3083
+ aspectRatio +
3084
+ " (width=" +
3085
+ ratioWidth +
3086
+ ", height=" +
3087
+ ratioHeight +
3088
+ ")"
3089
+ );
3090
+ Log.d(
3091
+ TAG,
3092
+ "Device orientation: " + (isPortrait ? "PORTRAIT" : "LANDSCAPE")
3093
+ );
3094
+
3095
+ // iOS: let ratio = !isPortrait ? ratioParts[0] / ratioParts[1] : ratioParts[1] / ratioParts[0]
3096
+ double ratio = !isPortrait
3097
+ ? (ratioWidth / ratioHeight)
3098
+ : (ratioHeight / ratioWidth);
3099
+
3100
+ Log.d(
3101
+ TAG,
3102
+ "Computed ratio: " +
3103
+ ratio +
3104
+ " (iOS formula: " +
3105
+ (!isPortrait ? "width/height" : "height/width") +
3106
+ ")"
3107
+ );
2640
3108
 
2641
3109
  // Get available space from webview dimensions
2642
- int availableWidth = webView.getWidth();
2643
- int availableHeight = webView.getHeight();
3110
+ int availableWidth = webViewWidth;
3111
+ int availableHeight = webViewHeight;
3112
+
3113
+ Log.d(
3114
+ TAG,
3115
+ "Available space from WebView: " +
3116
+ availableWidth +
3117
+ "x" +
3118
+ availableHeight
3119
+ );
2644
3120
 
2645
3121
  // Calculate position and size
2646
3122
  int finalX, finalY, finalWidth, finalHeight;
2647
3123
 
2648
3124
  if (x != null && y != null) {
2649
- // Account for WebView insets from edge-to-edge support
3125
+ // Manual positioning mode
2650
3126
  int webViewTopInset = getWebViewTopInset();
2651
3127
  int webViewLeftInset = getWebViewLeftInset();
2652
3128
 
2653
- // Use provided coordinates with boundary checking, adjusted for insets
3129
+ Log.d(
3130
+ TAG,
3131
+ "Manual positioning mode - WebView insets: left=" +
3132
+ webViewLeftInset +
3133
+ ", top=" +
3134
+ webViewTopInset
3135
+ );
3136
+
2654
3137
  finalX = Math.max(
2655
3138
  0,
2656
3139
  Math.min(x.intValue() + webViewLeftInset, availableWidth)
@@ -2664,6 +3147,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2664
3147
  int maxWidth = availableWidth - finalX;
2665
3148
  int maxHeight = availableHeight - finalY;
2666
3149
 
3150
+ Log.d(
3151
+ TAG,
3152
+ "Max available space from position: " + maxWidth + "x" + maxHeight
3153
+ );
3154
+
2667
3155
  // Calculate optimal size while maintaining aspect ratio within available space
2668
3156
  finalWidth = maxWidth;
2669
3157
  finalHeight = (int) (maxWidth / ratio);
@@ -2672,76 +3160,147 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2672
3160
  // Height constraint is tighter, fit by height
2673
3161
  finalHeight = maxHeight;
2674
3162
  finalWidth = (int) (maxHeight * ratio);
3163
+ Log.d(TAG, "Height-constrained sizing");
3164
+ } else {
3165
+ Log.d(TAG, "Width-constrained sizing");
2675
3166
  }
2676
3167
 
2677
3168
  // Ensure final position stays within bounds
2678
3169
  finalX = Math.max(0, Math.min(finalX, availableWidth - finalWidth));
2679
3170
  finalY = Math.max(0, Math.min(finalY, availableHeight - finalHeight));
2680
3171
  } 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);
3172
+ // Auto-center mode - match iOS behavior exactly
3173
+ Log.d(TAG, "Auto-center mode");
3174
+
3175
+ // Calculate maximum size that fits the aspect ratio in available space
3176
+ double maxWidthByHeight = availableHeight * ratio;
3177
+ double maxHeightByWidth = availableWidth / ratio;
3178
+
3179
+ Log.d(
3180
+ TAG,
3181
+ "Aspect ratio calculations - maxWidthByHeight: " +
3182
+ maxWidthByHeight +
3183
+ ", maxHeightByWidth: " +
3184
+ maxHeightByWidth
3185
+ );
3186
+
3187
+ if (maxWidthByHeight <= availableWidth) {
3188
+ // Height is the limiting factor
3189
+ finalWidth = (int) maxWidthByHeight;
3190
+ finalHeight = availableHeight;
3191
+ Log.d(
3192
+ TAG,
3193
+ "Height-limited sizing: " + finalWidth + "x" + finalHeight
3194
+ );
3195
+ } else {
3196
+ // Width is the limiting factor
3197
+ finalWidth = availableWidth;
3198
+ finalHeight = (int) maxHeightByWidth;
3199
+ Log.d(TAG, "Width-limited sizing: " + finalWidth + "x" + finalHeight);
2694
3200
  }
2695
3201
 
2696
- // Center the view
3202
+ // Center the preview
2697
3203
  finalX = (availableWidth - finalWidth) / 2;
2698
3204
  finalY = (availableHeight - finalHeight) / 2;
2699
3205
 
2700
3206
  Log.d(
2701
3207
  TAG,
2702
- "updatePreviewLayoutForAspectRatio: Auto-center mode - ratio=" +
2703
- ratio +
2704
- ", calculated size=" +
3208
+ "Auto-center mode: calculated size " +
2705
3209
  finalWidth +
2706
3210
  "x" +
2707
3211
  finalHeight +
2708
- ", available=" +
2709
- availableWidth +
2710
- "x" +
2711
- availableHeight
3212
+ " at position (" +
3213
+ finalX +
3214
+ ", " +
3215
+ finalY +
3216
+ ")"
2712
3217
  );
2713
3218
  }
2714
3219
 
3220
+ Log.d(
3221
+ TAG,
3222
+ "Final calculated layout - Position: (" +
3223
+ finalX +
3224
+ "," +
3225
+ finalY +
3226
+ "), Size: " +
3227
+ finalWidth +
3228
+ "x" +
3229
+ finalHeight
3230
+ );
3231
+
3232
+ // Calculate and log the actual displayed aspect ratio
3233
+ double displayedRatio = (double) finalWidth / (double) finalHeight;
3234
+ Log.d(
3235
+ TAG,
3236
+ "Displayed aspect ratio: " +
3237
+ displayedRatio +
3238
+ " (width=" +
3239
+ finalWidth +
3240
+ ", height=" +
3241
+ finalHeight +
3242
+ ")"
3243
+ );
3244
+
3245
+ // Compare with expected ratio based on orientation
3246
+ if (aspectRatio != null) {
3247
+ String[] parts = aspectRatio.split(":");
3248
+ if (parts.length == 2) {
3249
+ double expectedDisplayRatio = isPortrait
3250
+ ? (ratioHeight / ratioWidth)
3251
+ : (ratioWidth / ratioHeight);
3252
+ double difference = Math.abs(displayedRatio - expectedDisplayRatio);
3253
+ Log.d(
3254
+ TAG,
3255
+ "Display ratio check - Expected: " +
3256
+ expectedDisplayRatio +
3257
+ ", Actual: " +
3258
+ displayedRatio +
3259
+ ", Difference: " +
3260
+ difference +
3261
+ " (tolerance should be < 0.01)"
3262
+ );
3263
+ }
3264
+ }
3265
+
2715
3266
  // Update layout params
2716
- ViewGroup.LayoutParams currentParams = previewContainer.getLayoutParams();
2717
- if (currentParams instanceof ViewGroup.MarginLayoutParams) {
3267
+ ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
3268
+ if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
2718
3269
  ViewGroup.MarginLayoutParams params =
2719
- (ViewGroup.MarginLayoutParams) currentParams;
3270
+ (ViewGroup.MarginLayoutParams) layoutParams;
2720
3271
  params.width = finalWidth;
2721
3272
  params.height = finalHeight;
2722
3273
  params.leftMargin = finalX;
2723
3274
  params.topMargin = finalY;
2724
3275
  previewContainer.setLayoutParams(params);
2725
3276
  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
- );
3277
+
3278
+ Log.d(TAG, "Layout params applied successfully");
2738
3279
 
2739
3280
  // Update grid overlay bounds after aspect ratio change
2740
- previewContainer.post(() -> updateGridOverlayBounds());
3281
+ previewContainer.post(() -> {
3282
+ Log.d(
3283
+ TAG,
3284
+ "Post-layout verification - Actual position: " +
3285
+ previewContainer.getLeft() +
3286
+ "," +
3287
+ previewContainer.getTop() +
3288
+ ", Actual size: " +
3289
+ previewContainer.getWidth() +
3290
+ "x" +
3291
+ previewContainer.getHeight()
3292
+ );
3293
+ updateGridOverlayBounds();
3294
+ });
2741
3295
  }
2742
3296
  } catch (NumberFormatException e) {
2743
3297
  Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio, e);
2744
3298
  }
3299
+
3300
+ Log.d(
3301
+ TAG,
3302
+ "========================================================================================"
3303
+ );
2745
3304
  }
2746
3305
 
2747
3306
  private int getWebViewTopInset() {
@@ -2843,12 +3402,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2843
3402
  MeteringPointFactory factory = previewView.getMeteringPointFactory();
2844
3403
  MeteringPoint point = factory.createPoint(viewWidth / 2f, viewHeight / 2f);
2845
3404
 
2846
- // Create focus and metering action
3405
+ // Create focus and metering action (persistent, no auto-cancel) to match iOS behavior
2847
3406
  FocusMeteringAction action = new FocusMeteringAction.Builder(
2848
3407
  point,
2849
3408
  FocusMeteringAction.FLAG_AF | FocusMeteringAction.FLAG_AE
2850
3409
  )
2851
- .setAutoCancelDuration(3, TimeUnit.SECONDS) // Auto-cancel after 3 seconds
3410
+ .disableAutoCancel()
2852
3411
  .build();
2853
3412
 
2854
3413
  try {