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