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

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,24 +1,31 @@
1
1
  package com.ahm.capacitor.camera.preview;
2
2
 
3
3
  import android.content.Context;
4
- import android.content.Intent;
5
4
  import android.graphics.Bitmap;
6
5
  import android.graphics.BitmapFactory;
7
- import android.graphics.Matrix;
6
+ import android.graphics.Color;
7
+ import android.graphics.Rect;
8
+ import android.graphics.drawable.GradientDrawable;
8
9
  import android.hardware.camera2.CameraAccessException;
9
10
  import android.hardware.camera2.CameraCharacteristics;
10
11
  import android.hardware.camera2.CameraManager;
11
12
  import android.location.Location;
12
- import android.net.Uri;
13
+ import android.media.MediaScannerConnection;
13
14
  import android.os.Build;
14
15
  import android.os.Environment;
15
16
  import android.util.Base64;
16
17
  import android.util.DisplayMetrics;
17
18
  import android.util.Log;
18
- import android.util.Rational;
19
19
  import android.util.Size;
20
+ import android.view.MotionEvent;
21
+ import android.view.View;
20
22
  import android.view.ViewGroup;
21
- import android.view.ViewGroup;
23
+ import android.view.animation.AlphaAnimation;
24
+ import android.view.animation.Animation;
25
+ import android.view.animation.AnimationSet;
26
+ import android.view.animation.AnimationUtils;
27
+ import android.view.animation.ScaleAnimation;
28
+ import android.webkit.WebView;
22
29
  import android.webkit.WebView;
23
30
  import android.widget.FrameLayout;
24
31
  import android.widget.FrameLayout;
@@ -30,9 +37,13 @@ import androidx.camera.core.AspectRatio;
30
37
  import androidx.camera.core.Camera;
31
38
  import androidx.camera.core.CameraInfo;
32
39
  import androidx.camera.core.CameraSelector;
40
+ import androidx.camera.core.FocusMeteringAction;
41
+ import androidx.camera.core.FocusMeteringResult;
33
42
  import androidx.camera.core.ImageCapture;
34
43
  import androidx.camera.core.ImageCaptureException;
35
44
  import androidx.camera.core.ImageProxy;
45
+ import androidx.camera.core.MeteringPoint;
46
+ import androidx.camera.core.MeteringPointFactory;
36
47
  import androidx.camera.core.Preview;
37
48
  import androidx.camera.core.ResolutionInfo;
38
49
  import androidx.camera.core.ZoomState;
@@ -47,7 +58,6 @@ import androidx.lifecycle.Lifecycle;
47
58
  import androidx.lifecycle.LifecycleObserver;
48
59
  import androidx.lifecycle.LifecycleOwner;
49
60
  import androidx.lifecycle.LifecycleRegistry;
50
- import androidx.lifecycle.OnLifecycleEvent;
51
61
  import com.ahm.capacitor.camera.preview.model.CameraSessionConfiguration;
52
62
  import com.ahm.capacitor.camera.preview.model.LensInfo;
53
63
  import com.ahm.capacitor.camera.preview.model.ZoomFactors;
@@ -57,7 +67,6 @@ import java.io.File;
57
67
  import java.io.FileOutputStream;
58
68
  import java.io.IOException;
59
69
  import java.nio.ByteBuffer;
60
- import java.nio.file.Files;
61
70
  import java.text.SimpleDateFormat;
62
71
  import java.util.ArrayList;
63
72
  import java.util.Arrays;
@@ -69,6 +78,7 @@ import java.util.Set;
69
78
  import java.util.concurrent.Executor;
70
79
  import java.util.concurrent.ExecutorService;
71
80
  import java.util.concurrent.Executors;
81
+ import java.util.concurrent.TimeUnit;
72
82
  import org.json.JSONObject;
73
83
 
74
84
  public class CameraXView implements LifecycleOwner, LifecycleObserver {
@@ -91,6 +101,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
91
101
  private PreviewView previewView;
92
102
  private GridOverlayView gridOverlayView;
93
103
  private FrameLayout previewContainer;
104
+ private View focusIndicatorView;
94
105
  private CameraSelector currentCameraSelector;
95
106
  private String currentDeviceId;
96
107
  private int currentFlashMode = ImageCapture.FLASH_MODE_OFF;
@@ -102,6 +113,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
102
113
  private final Executor mainExecutor;
103
114
  private ExecutorService cameraExecutor;
104
115
  private boolean isRunning = false;
116
+ private Size currentPreviewResolution = null;
117
+ private ListenableFuture<FocusMeteringResult> currentFocusFuture = null; // Track current focus operation
105
118
 
106
119
  public CameraXView(Context context, WebView webView) {
107
120
  this.context = context;
@@ -130,6 +143,47 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
130
143
 
131
144
  private void saveImageToGallery(byte[] data) {
132
145
  try {
146
+ // Detect image format from byte array header
147
+ String extension = ".jpg";
148
+ String mimeType = "image/jpeg";
149
+
150
+ if (data.length >= 8) {
151
+ // Check for PNG signature (89 50 4E 47 0D 0A 1A 0A)
152
+ if (
153
+ data[0] == (byte) 0x89 &&
154
+ data[1] == 0x50 &&
155
+ data[2] == 0x4E &&
156
+ data[3] == 0x47
157
+ ) {
158
+ extension = ".png";
159
+ mimeType = "image/png";
160
+ }
161
+ // Check for JPEG signature (FF D8 FF)
162
+ else if (
163
+ data[0] == (byte) 0xFF &&
164
+ data[1] == (byte) 0xD8 &&
165
+ data[2] == (byte) 0xFF
166
+ ) {
167
+ extension = ".jpg";
168
+ mimeType = "image/jpeg";
169
+ }
170
+ // Check for WebP signature (RIFF ... WEBP)
171
+ else if (
172
+ data[0] == 0x52 &&
173
+ data[1] == 0x49 &&
174
+ data[2] == 0x46 &&
175
+ data[3] == 0x46 &&
176
+ data.length >= 12 &&
177
+ data[8] == 0x57 &&
178
+ data[9] == 0x45 &&
179
+ data[10] == 0x42 &&
180
+ data[11] == 0x50
181
+ ) {
182
+ extension = ".webp";
183
+ mimeType = "image/webp";
184
+ }
185
+ }
186
+
133
187
  File photo = new File(
134
188
  Environment.getExternalStoragePublicDirectory(
135
189
  Environment.DIRECTORY_PICTURES
@@ -138,19 +192,19 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
138
192
  new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(
139
193
  new java.util.Date()
140
194
  ) +
141
- ".jpg"
195
+ extension
142
196
  );
143
197
  FileOutputStream fos = new FileOutputStream(photo);
144
198
  fos.write(data);
145
199
  fos.close();
146
200
 
147
201
  // Notify the gallery of the new image
148
- Intent mediaScanIntent = new Intent(
149
- Intent.ACTION_MEDIA_SCANNER_SCAN_FILE
202
+ MediaScannerConnection.scanFile(
203
+ this.context,
204
+ new String[] { photo.getAbsolutePath() },
205
+ new String[] { mimeType },
206
+ null
150
207
  );
151
- Uri contentUri = Uri.fromFile(photo);
152
- mediaScanIntent.setData(contentUri);
153
- context.sendBroadcast(mediaScanIntent);
154
208
  } catch (IOException e) {
155
209
  Log.e(TAG, "Error saving image to gallery", e);
156
210
  }
@@ -167,6 +221,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
167
221
 
168
222
  public void stopSession() {
169
223
  isRunning = false;
224
+ // Cancel any ongoing focus operation when stopping session
225
+ if (currentFocusFuture != null && !currentFocusFuture.isDone()) {
226
+ currentFocusFuture.cancel(true);
227
+ }
228
+ currentFocusFuture = null;
229
+
170
230
  mainExecutor.execute(() -> {
171
231
  if (cameraProvider != null) {
172
232
  cameraProvider.unbindAll();
@@ -210,10 +270,70 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
210
270
 
211
271
  // Create a container to hold both the preview and grid overlay
212
272
  previewContainer = new FrameLayout(context);
273
+ // Ensure container can receive touch events
274
+ previewContainer.setClickable(true);
275
+ previewContainer.setFocusable(true);
213
276
 
214
277
  // Create and setup the preview view
215
278
  previewView = new PreviewView(context);
216
279
  previewView.setScaleType(PreviewView.ScaleType.FIT_CENTER);
280
+ // Also make preview view touchable as backup
281
+ previewView.setClickable(true);
282
+ previewView.setFocusable(true);
283
+
284
+ // Add touch listener to both container and preview view for maximum compatibility
285
+ View.OnTouchListener touchListener = new View.OnTouchListener() {
286
+ @Override
287
+ public boolean onTouch(View v, MotionEvent event) {
288
+ Log.d(
289
+ TAG,
290
+ "onTouch: " +
291
+ v.getClass().getSimpleName() +
292
+ " received touch event: " +
293
+ event.getAction() +
294
+ " at (" +
295
+ event.getX() +
296
+ ", " +
297
+ event.getY() +
298
+ ")"
299
+ );
300
+
301
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
302
+ float x = event.getX() / v.getWidth();
303
+ float y = event.getY() / v.getHeight();
304
+
305
+ Log.d(
306
+ TAG,
307
+ "onTouch: Touch detected at raw coords (" +
308
+ event.getX() +
309
+ ", " +
310
+ event.getY() +
311
+ "), view size: " +
312
+ v.getWidth() +
313
+ "x" +
314
+ v.getHeight() +
315
+ ", normalized: (" +
316
+ x +
317
+ ", " +
318
+ y +
319
+ ")"
320
+ );
321
+
322
+ try {
323
+ // Trigger focus with indicator
324
+ setFocus(x, y);
325
+ } catch (Exception e) {
326
+ Log.e(TAG, "Error during tap-to-focus: " + e.getMessage(), e);
327
+ }
328
+ return true;
329
+ }
330
+ return false;
331
+ }
332
+ };
333
+
334
+ previewContainer.setOnTouchListener(touchListener);
335
+ previewView.setOnTouchListener(touchListener);
336
+
217
337
  previewContainer.addView(
218
338
  previewView,
219
339
  new FrameLayout.LayoutParams(
@@ -224,6 +344,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
224
344
 
225
345
  // Create and setup the grid overlay
226
346
  gridOverlayView = new GridOverlayView(context);
347
+ // Make grid overlay not intercept touch events
348
+ gridOverlayView.setClickable(false);
349
+ gridOverlayView.setFocusable(false);
227
350
  previewContainer.addView(
228
351
  gridOverlayView,
229
352
  new FrameLayout.LayoutParams(
@@ -243,6 +366,47 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
243
366
  FrameLayout.LayoutParams layoutParams = calculatePreviewLayoutParams();
244
367
  parent.addView(previewContainer, layoutParams);
245
368
  if (sessionConfig.isToBack()) webView.bringToFront();
369
+
370
+ // Log the actual position after layout
371
+ previewContainer.post(() -> {
372
+ Log.d(TAG, "========================");
373
+ Log.d(TAG, "ACTUAL CAMERA VIEW POSITION (after layout):");
374
+ Log.d(
375
+ TAG,
376
+ "Container position - Left: " +
377
+ previewContainer.getLeft() +
378
+ ", Top: " +
379
+ previewContainer.getTop() +
380
+ ", Right: " +
381
+ previewContainer.getRight() +
382
+ ", Bottom: " +
383
+ previewContainer.getBottom()
384
+ );
385
+ Log.d(
386
+ TAG,
387
+ "Container size - Width: " +
388
+ previewContainer.getWidth() +
389
+ ", Height: " +
390
+ previewContainer.getHeight()
391
+ );
392
+
393
+ // Get parent info
394
+ ViewGroup containerParent = (ViewGroup) previewContainer.getParent();
395
+ if (containerParent != null) {
396
+ Log.d(
397
+ TAG,
398
+ "Parent class: " + containerParent.getClass().getSimpleName()
399
+ );
400
+ Log.d(
401
+ TAG,
402
+ "Parent size - Width: " +
403
+ containerParent.getWidth() +
404
+ ", Height: " +
405
+ containerParent.getHeight()
406
+ );
407
+ }
408
+ Log.d(TAG, "========================");
409
+ });
246
410
  }
247
411
  }
248
412
 
@@ -287,8 +451,51 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
287
451
  optimalWidth = (int) (height * ratio);
288
452
  }
289
453
 
454
+ // Store the old dimensions to check if we need to recenter
455
+ int oldWidth = width;
456
+ int oldHeight = height;
290
457
  width = optimalWidth;
291
458
  height = optimalHeight;
459
+
460
+ // If we're centered and dimensions changed, recalculate position
461
+ if (sessionConfig.isCentered()) {
462
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
463
+
464
+ if (width != oldWidth) {
465
+ int screenWidth = metrics.widthPixels;
466
+ x = (screenWidth - width) / 2;
467
+ Log.d(
468
+ TAG,
469
+ "calculatePreviewLayoutParams: Recentered X after aspect ratio - " +
470
+ "oldWidth=" +
471
+ oldWidth +
472
+ ", newWidth=" +
473
+ width +
474
+ ", screenWidth=" +
475
+ screenWidth +
476
+ ", newX=" +
477
+ x
478
+ );
479
+ }
480
+
481
+ if (height != oldHeight) {
482
+ int screenHeight = metrics.heightPixels;
483
+ y = (screenHeight - height) / 2;
484
+ Log.d(
485
+ TAG,
486
+ "calculatePreviewLayoutParams: Recentered Y after aspect ratio - " +
487
+ "oldHeight=" +
488
+ oldHeight +
489
+ ", newHeight=" +
490
+ height +
491
+ ", screenHeight=" +
492
+ screenHeight +
493
+ ", newY=" +
494
+ y
495
+ );
496
+ }
497
+ }
498
+
292
499
  Log.d(
293
500
  TAG,
294
501
  "calculatePreviewLayoutParams: Applied aspect ratio " +
@@ -309,17 +516,18 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
309
516
  height
310
517
  );
311
518
 
312
- // Only add insets for positioning coordinates, not for full-screen sizes
519
+ // Only add insets for positioning coordinates, not for full-screen sizes or centered content
313
520
  int webViewTopInset = getWebViewTopInset();
314
521
  int webViewLeftInset = getWebViewLeftInset();
315
522
 
316
- // Don't add insets if this looks like a calculated full-screen coordinate (x=0, y=0)
317
- if (x == 0 && y == 0) {
523
+ // Don't add insets if centered or full-screen
524
+ if (sessionConfig.isCentered() || (x == 0 && y == 0)) {
318
525
  layoutParams.leftMargin = x;
319
526
  layoutParams.topMargin = y;
320
527
  Log.d(
321
528
  TAG,
322
- "calculatePreviewLayoutParams: Full-screen mode - keeping position (0,0) without insets"
529
+ "calculatePreviewLayoutParams: Centered/Full-screen mode - keeping position without insets. isCentered=" +
530
+ sessionConfig.isCentered()
323
531
  );
324
532
  } else {
325
533
  layoutParams.leftMargin = x + webViewLeftInset;
@@ -332,18 +540,15 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
332
540
 
333
541
  Log.d(
334
542
  TAG,
335
- "calculatePreviewLayoutParams: Applied insets - x:" +
543
+ "calculatePreviewLayoutParams: Position calculation - x:" +
336
544
  x +
337
- "+" +
338
- webViewLeftInset +
339
- "=" +
545
+ " (leftMargin=" +
340
546
  layoutParams.leftMargin +
341
- ", y:" +
547
+ "), y:" +
342
548
  y +
343
- "+" +
344
- webViewTopInset +
345
- "=" +
346
- layoutParams.topMargin
549
+ " (topMargin=" +
550
+ layoutParams.topMargin +
551
+ ")"
347
552
  );
348
553
 
349
554
  Log.d(
@@ -357,6 +562,18 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
357
562
  " height:" +
358
563
  height
359
564
  );
565
+ Log.d(
566
+ TAG,
567
+ "calculatePreviewLayoutParams: Final margins - leftMargin:" +
568
+ layoutParams.leftMargin +
569
+ " topMargin:" +
570
+ layoutParams.topMargin +
571
+ " (WebView insets: left=" +
572
+ webViewLeftInset +
573
+ ", top=" +
574
+ webViewTopInset +
575
+ ")"
576
+ );
360
577
  return layoutParams;
361
578
  }
362
579
 
@@ -374,6 +591,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
374
591
  if (gridOverlayView != null) {
375
592
  gridOverlayView = null;
376
593
  }
594
+ if (focusIndicatorView != null) {
595
+ focusIndicatorView = null;
596
+ }
377
597
  webView.setBackgroundColor(android.graphics.Color.WHITE);
378
598
  }
379
599
 
@@ -478,10 +698,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
478
698
  // Log resolution info
479
699
  ResolutionInfo previewResolution = preview.getResolutionInfo();
480
700
  if (previewResolution != null) {
481
- Log.d(
482
- TAG,
483
- "Preview resolution: " + previewResolution.getResolution()
484
- );
701
+ currentPreviewResolution = previewResolution.getResolution();
702
+ Log.d(TAG, "Preview resolution: " + currentPreviewResolution);
485
703
  }
486
704
  ResolutionInfo imageCaptureResolution =
487
705
  imageCapture.getResolutionInfo();
@@ -499,6 +717,28 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
499
717
  : sessionConfig.getZoomFactor();
500
718
  if (initialZoom != 1.0f) {
501
719
  Log.d(TAG, "Applying initial zoom of " + initialZoom);
720
+
721
+ // Validate zoom is within bounds
722
+ if (zoomState != null) {
723
+ float minZoom = zoomState.getMinZoomRatio();
724
+ float maxZoom = zoomState.getMaxZoomRatio();
725
+
726
+ if (initialZoom < minZoom || initialZoom > maxZoom) {
727
+ if (listener != null) {
728
+ listener.onCameraStartError(
729
+ "Initial zoom level " +
730
+ initialZoom +
731
+ " is not available. " +
732
+ "Valid range is " +
733
+ minZoom +
734
+ " to " +
735
+ maxZoom
736
+ );
737
+ return;
738
+ }
739
+ }
740
+ }
741
+
502
742
  setZoomInternal(initialZoom);
503
743
  }
504
744
 
@@ -508,18 +748,24 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
508
748
  // Post the callback to ensure layout is complete
509
749
  previewContainer.post(() -> {
510
750
  // Return actual preview container dimensions instead of requested dimensions
511
- int actualWidth = previewContainer != null
512
- ? previewContainer.getWidth()
513
- : sessionConfig.getWidth();
514
- int actualHeight = previewContainer != null
515
- ? previewContainer.getHeight()
516
- : sessionConfig.getHeight();
517
- int actualX = previewContainer != null
518
- ? previewContainer.getLeft()
519
- : sessionConfig.getX();
520
- int actualY = previewContainer != null
521
- ? previewContainer.getTop()
522
- : sessionConfig.getY();
751
+ // Get the actual camera dimensions and position
752
+ int actualWidth = getPreviewWidth();
753
+ int actualHeight = getPreviewHeight();
754
+ int actualX = getPreviewX();
755
+ int actualY = getPreviewY();
756
+
757
+ Log.d(
758
+ TAG,
759
+ "onCameraStarted callback - actualX=" +
760
+ actualX +
761
+ ", actualY=" +
762
+ actualY +
763
+ ", actualWidth=" +
764
+ actualWidth +
765
+ ", actualHeight=" +
766
+ actualHeight
767
+ );
768
+
523
769
  listener.onCameraStarted(
524
770
  actualWidth,
525
771
  actualHeight,
@@ -1253,7 +1499,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1253
1499
  }
1254
1500
  }
1255
1501
 
1256
- public void setZoom(float zoomRatio) throws Exception {
1502
+ public void setZoom(float zoomRatio, boolean autoFocus) throws Exception {
1257
1503
  if (camera == null) {
1258
1504
  throw new Exception("Camera not initialized");
1259
1505
  }
@@ -1270,40 +1516,310 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1270
1516
  zoomFuture.addListener(
1271
1517
  () -> {
1272
1518
  try {
1273
- float actualZoom = Objects.requireNonNull(
1274
- camera.getCameraInfo().getZoomState().getValue()
1275
- ).getZoomRatio();
1276
- Log.d(
1277
- TAG,
1278
- "setZoom: CameraX set zoom to " +
1279
- actualZoom +
1280
- " (requested: " +
1281
- zoomRatio +
1282
- ")"
1283
- );
1284
- if (Math.abs(actualZoom - zoomRatio) > 0.1f) {
1285
- Log.w(
1519
+ zoomFuture.get();
1520
+ Log.d(TAG, "Zoom successfully set to " + zoomRatio);
1521
+ // Trigger autofocus after zoom if requested
1522
+ if (autoFocus) {
1523
+ triggerAutoFocus();
1524
+ }
1525
+ } catch (Exception e) {
1526
+ Log.e(TAG, "Error setting zoom: " + e.getMessage());
1527
+ }
1528
+ },
1529
+ ContextCompat.getMainExecutor(context)
1530
+ );
1531
+ } catch (Exception e) {
1532
+ Log.e(TAG, "Failed to set zoom: " + e.getMessage());
1533
+ throw e;
1534
+ }
1535
+ }
1536
+
1537
+ public void setFocus(float x, float y) throws Exception {
1538
+ if (camera == null) {
1539
+ throw new Exception("Camera not initialized");
1540
+ }
1541
+
1542
+ if (previewView == null) {
1543
+ throw new Exception("Preview view not initialized");
1544
+ }
1545
+
1546
+ // Cancel any ongoing focus operation
1547
+ if (currentFocusFuture != null && !currentFocusFuture.isDone()) {
1548
+ Log.d(TAG, "setFocus: Cancelling previous focus operation");
1549
+ currentFocusFuture.cancel(true);
1550
+ }
1551
+
1552
+ int viewWidth = previewView.getWidth();
1553
+ int viewHeight = previewView.getHeight();
1554
+ float indicatorX = x * viewWidth;
1555
+ float indicatorY = y * viewHeight;
1556
+ showFocusIndicator(indicatorX, indicatorY);
1557
+
1558
+ if (viewWidth <= 0 || viewHeight <= 0) {
1559
+ throw new Exception(
1560
+ "Preview view has invalid dimensions: " + viewWidth + "x" + viewHeight
1561
+ );
1562
+ }
1563
+
1564
+ // Create MeteringPoint using the preview view
1565
+ MeteringPointFactory factory = previewView.getMeteringPointFactory();
1566
+ MeteringPoint point = factory.createPoint(x * viewWidth, y * viewHeight);
1567
+
1568
+ // Create focus and metering action
1569
+ FocusMeteringAction action = new FocusMeteringAction.Builder(
1570
+ point,
1571
+ FocusMeteringAction.FLAG_AF | FocusMeteringAction.FLAG_AE
1572
+ )
1573
+ .setAutoCancelDuration(3, TimeUnit.SECONDS) // Auto-cancel after 3 seconds
1574
+ .build();
1575
+
1576
+ try {
1577
+ currentFocusFuture = camera
1578
+ .getCameraControl()
1579
+ .startFocusAndMetering(action);
1580
+
1581
+ currentFocusFuture.addListener(
1582
+ () -> {
1583
+ try {
1584
+ FocusMeteringResult result = currentFocusFuture.get();
1585
+ } catch (Exception e) {
1586
+ // Handle cancellation gracefully - this is expected when rapid taps occur
1587
+ if (
1588
+ e.getMessage() != null &&
1589
+ (e
1590
+ .getMessage()
1591
+ .contains("Cancelled by another startFocusAndMetering") ||
1592
+ e.getMessage().contains("OperationCanceledException") ||
1593
+ e
1594
+ .getClass()
1595
+ .getSimpleName()
1596
+ .contains("OperationCanceledException"))
1597
+ ) {
1598
+ Log.d(
1286
1599
  TAG,
1287
- "setZoom: CameraX clamped zoom from " +
1288
- zoomRatio +
1289
- " to " +
1290
- actualZoom
1600
+ "Focus operation was cancelled by a newer focus request"
1291
1601
  );
1292
1602
  } else {
1293
- Log.d(TAG, "setZoom: CameraX successfully set requested zoom");
1603
+ Log.e(TAG, "Error during focus: " + e.getMessage());
1604
+ }
1605
+ } finally {
1606
+ // Clear the reference if this is still the current operation
1607
+ if (currentFocusFuture != null && currentFocusFuture.isDone()) {
1608
+ currentFocusFuture = null;
1294
1609
  }
1295
- } catch (Exception e) {
1296
- Log.e(TAG, "setZoom: Error checking final zoom", e);
1297
1610
  }
1298
1611
  },
1299
- mainExecutor
1612
+ ContextCompat.getMainExecutor(context)
1300
1613
  );
1301
1614
  } catch (Exception e) {
1302
- Log.e(TAG, "setZoom: Failed to set zoom to " + zoomRatio, e);
1615
+ currentFocusFuture = null;
1616
+ Log.e(TAG, "Failed to set focus: " + e.getMessage());
1303
1617
  throw e;
1304
1618
  }
1305
1619
  }
1306
1620
 
1621
+ private void showFocusIndicator(float x, float y) {
1622
+ if (previewContainer == null) {
1623
+ Log.w(TAG, "showFocusIndicator: previewContainer is null");
1624
+ return;
1625
+ }
1626
+
1627
+ // Check if container has been laid out
1628
+ if (previewContainer.getWidth() == 0 || previewContainer.getHeight() == 0) {
1629
+ Log.w(
1630
+ TAG,
1631
+ "showFocusIndicator: previewContainer not laid out yet, posting to run after layout"
1632
+ );
1633
+ previewContainer.post(() -> showFocusIndicator(x, y));
1634
+ return;
1635
+ }
1636
+
1637
+ // Remove any existing focus indicator
1638
+ if (focusIndicatorView != null) {
1639
+ previewContainer.removeView(focusIndicatorView);
1640
+ focusIndicatorView = null;
1641
+ }
1642
+
1643
+ // Create an elegant focus indicator
1644
+ View container = new View(context);
1645
+ int size = (int) (60 * context.getResources().getDisplayMetrics().density); // 60dp size
1646
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(size, size);
1647
+
1648
+ // Center the indicator on the touch point with bounds checking
1649
+ int containerWidth = previewContainer.getWidth();
1650
+ int containerHeight = previewContainer.getHeight();
1651
+
1652
+ params.leftMargin = Math.max(
1653
+ 0,
1654
+ Math.min((int) (x - size / 2), containerWidth - size)
1655
+ );
1656
+ params.topMargin = Math.max(
1657
+ 0,
1658
+ Math.min((int) (y - size / 2), containerHeight - size)
1659
+ );
1660
+
1661
+ // Create an elegant focus ring - white stroke with transparent center
1662
+ GradientDrawable drawable = new GradientDrawable();
1663
+ drawable.setShape(GradientDrawable.OVAL);
1664
+ drawable.setStroke(
1665
+ (int) (2 * context.getResources().getDisplayMetrics().density),
1666
+ Color.WHITE
1667
+ ); // 2dp white stroke
1668
+ drawable.setColor(Color.TRANSPARENT); // Transparent center
1669
+ container.setBackground(drawable);
1670
+
1671
+ focusIndicatorView = container;
1672
+
1673
+ // Set initial state for smooth animation
1674
+ focusIndicatorView.setAlpha(1f); // Start visible
1675
+ focusIndicatorView.setScaleX(1.8f); // Start larger for scale-in effect
1676
+ focusIndicatorView.setScaleY(1.8f);
1677
+ focusIndicatorView.setVisibility(View.VISIBLE);
1678
+
1679
+ // Ensure container doesn't intercept touch events
1680
+ container.setClickable(false);
1681
+ container.setFocusable(false);
1682
+
1683
+ // Ensure the focus indicator has a high elevation for visibility
1684
+ if (
1685
+ android.os.Build.VERSION.SDK_INT >=
1686
+ android.os.Build.VERSION_CODES.LOLLIPOP
1687
+ ) {
1688
+ focusIndicatorView.setElevation(10f);
1689
+ }
1690
+
1691
+ // Add to container first
1692
+ previewContainer.addView(focusIndicatorView, params);
1693
+
1694
+ // Fix z-ordering: ensure focus indicator is always on top
1695
+ focusIndicatorView.bringToFront();
1696
+
1697
+ // Force a layout pass to ensure the view is properly positioned
1698
+ previewContainer.requestLayout();
1699
+
1700
+ // Smooth scale down animation with easing (no fade needed since we start visible)
1701
+ ScaleAnimation scaleAnimation = new ScaleAnimation(
1702
+ 1.8f,
1703
+ 1.0f,
1704
+ 1.8f,
1705
+ 1.0f,
1706
+ Animation.RELATIVE_TO_SELF,
1707
+ 0.5f,
1708
+ Animation.RELATIVE_TO_SELF,
1709
+ 0.5f
1710
+ );
1711
+ scaleAnimation.setDuration(300);
1712
+ scaleAnimation.setInterpolator(
1713
+ new android.view.animation.OvershootInterpolator(1.2f)
1714
+ );
1715
+
1716
+ // Start the animation
1717
+ focusIndicatorView.startAnimation(scaleAnimation);
1718
+
1719
+ // Schedule fade out and removal with smoother timing
1720
+ focusIndicatorView.postDelayed(
1721
+ new Runnable() {
1722
+ @Override
1723
+ public void run() {
1724
+ if (focusIndicatorView != null) {
1725
+ // Smooth fade to semi-transparent
1726
+ AlphaAnimation fadeToTransparent = new AlphaAnimation(1f, 0.4f);
1727
+ fadeToTransparent.setDuration(400);
1728
+ fadeToTransparent.setInterpolator(
1729
+ new android.view.animation.AccelerateInterpolator()
1730
+ );
1731
+
1732
+ fadeToTransparent.setAnimationListener(
1733
+ new Animation.AnimationListener() {
1734
+ @Override
1735
+ public void onAnimationStart(Animation animation) {
1736
+ Log.d(TAG, "showFocusIndicator: Fade to transparent started");
1737
+ }
1738
+
1739
+ @Override
1740
+ public void onAnimationEnd(Animation animation) {
1741
+ Log.d(
1742
+ TAG,
1743
+ "showFocusIndicator: Fade to transparent ended, starting final fade out"
1744
+ );
1745
+ // Final smooth fade out and scale down
1746
+ if (focusIndicatorView != null) {
1747
+ AnimationSet finalAnimation = new AnimationSet(false);
1748
+
1749
+ AlphaAnimation finalFadeOut = new AlphaAnimation(0.4f, 0f);
1750
+ finalFadeOut.setDuration(500);
1751
+ finalFadeOut.setStartOffset(300);
1752
+ finalFadeOut.setInterpolator(
1753
+ new android.view.animation.AccelerateInterpolator()
1754
+ );
1755
+
1756
+ ScaleAnimation finalScaleDown = new ScaleAnimation(
1757
+ 1.0f,
1758
+ 0.9f,
1759
+ 1.0f,
1760
+ 0.9f,
1761
+ Animation.RELATIVE_TO_SELF,
1762
+ 0.5f,
1763
+ Animation.RELATIVE_TO_SELF,
1764
+ 0.5f
1765
+ );
1766
+ finalScaleDown.setDuration(500);
1767
+ finalScaleDown.setStartOffset(300);
1768
+ finalScaleDown.setInterpolator(
1769
+ new android.view.animation.AccelerateInterpolator()
1770
+ );
1771
+
1772
+ finalAnimation.addAnimation(finalFadeOut);
1773
+ finalAnimation.addAnimation(finalScaleDown);
1774
+
1775
+ finalAnimation.setAnimationListener(
1776
+ new Animation.AnimationListener() {
1777
+ @Override
1778
+ public void onAnimationStart(Animation animation) {
1779
+ Log.d(
1780
+ TAG,
1781
+ "showFocusIndicator: Final animation started"
1782
+ );
1783
+ }
1784
+
1785
+ @Override
1786
+ public void onAnimationEnd(Animation animation) {
1787
+ Log.d(
1788
+ TAG,
1789
+ "showFocusIndicator: Final animation ended, removing indicator"
1790
+ );
1791
+ // Remove the focus indicator
1792
+ if (
1793
+ focusIndicatorView != null &&
1794
+ previewContainer != null
1795
+ ) {
1796
+ previewContainer.removeView(focusIndicatorView);
1797
+ focusIndicatorView = null;
1798
+ }
1799
+ }
1800
+
1801
+ @Override
1802
+ public void onAnimationRepeat(Animation animation) {}
1803
+ }
1804
+ );
1805
+
1806
+ focusIndicatorView.startAnimation(finalAnimation);
1807
+ }
1808
+ }
1809
+
1810
+ @Override
1811
+ public void onAnimationRepeat(Animation animation) {}
1812
+ }
1813
+ );
1814
+
1815
+ focusIndicatorView.startAnimation(fadeToTransparent);
1816
+ }
1817
+ }
1818
+ },
1819
+ 800
1820
+ ); // Optimal timing for smooth focus feedback
1821
+ }
1822
+
1307
1823
  public static List<Size> getSupportedPictureSizes(String facing) {
1308
1824
  List<Size> sizes = new ArrayList<>();
1309
1825
  try {
@@ -1766,54 +2282,138 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1766
2282
 
1767
2283
  public int getPreviewX() {
1768
2284
  if (previewContainer == null) return 0;
2285
+
2286
+ // Get the container position
1769
2287
  ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
1770
2288
  if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
1771
- // Return position relative to WebView content (subtract insets)
1772
- int margin = ((ViewGroup.MarginLayoutParams) layoutParams).leftMargin;
1773
- int leftInset = getWebViewLeftInset();
1774
- int result = margin - leftInset;
2289
+ int containerX = ((ViewGroup.MarginLayoutParams) layoutParams).leftMargin;
2290
+
2291
+ // Get the actual camera bounds within the container
2292
+ Rect cameraBounds = getActualCameraBounds();
2293
+ int actualX = containerX + cameraBounds.left;
2294
+
1775
2295
  Log.d(
1776
2296
  TAG,
1777
- "getPreviewX: leftMargin=" +
1778
- margin +
1779
- ", leftInset=" +
1780
- leftInset +
1781
- ", result=" +
1782
- result
2297
+ "getPreviewX: containerX=" +
2298
+ containerX +
2299
+ ", cameraBounds.left=" +
2300
+ cameraBounds.left +
2301
+ ", actualX=" +
2302
+ actualX
1783
2303
  );
1784
- return result;
2304
+
2305
+ return actualX;
1785
2306
  }
1786
2307
  return previewContainer.getLeft();
1787
2308
  }
1788
2309
 
1789
2310
  public int getPreviewY() {
1790
2311
  if (previewContainer == null) return 0;
2312
+
2313
+ // Get the container position
1791
2314
  ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
1792
2315
  if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
1793
- // Return position relative to WebView content (subtract insets)
1794
- int margin = ((ViewGroup.MarginLayoutParams) layoutParams).topMargin;
1795
- int topInset = getWebViewTopInset();
1796
- int result = margin - topInset;
2316
+ int containerY = ((ViewGroup.MarginLayoutParams) layoutParams).topMargin;
2317
+
2318
+ // Get the actual camera bounds within the container
2319
+ Rect cameraBounds = getActualCameraBounds();
2320
+ int actualY = containerY + cameraBounds.top;
2321
+
1797
2322
  Log.d(
1798
2323
  TAG,
1799
- "getPreviewY: topMargin=" +
1800
- margin +
1801
- ", topInset=" +
1802
- topInset +
1803
- ", result=" +
1804
- result
2324
+ "getPreviewY: containerY=" +
2325
+ containerY +
2326
+ ", cameraBounds.top=" +
2327
+ cameraBounds.top +
2328
+ ", actualY=" +
2329
+ actualY
1805
2330
  );
1806
- return result;
2331
+
2332
+ return actualY;
1807
2333
  }
1808
2334
  return previewContainer.getTop();
1809
2335
  }
1810
2336
 
2337
+ // Get the actual camera content bounds within the PreviewView
2338
+ private Rect getActualCameraBounds() {
2339
+ if (previewView == null || previewContainer == null) {
2340
+ return new Rect(0, 0, 0, 0);
2341
+ }
2342
+
2343
+ // Get the container bounds
2344
+ int containerWidth = previewContainer.getWidth();
2345
+ int containerHeight = previewContainer.getHeight();
2346
+
2347
+ // Get the preview transformation info to understand how the camera is scaled/positioned
2348
+ // For FIT_CENTER, the camera content is scaled to fit within the container
2349
+ // This might create letterboxing (black bars) on top/bottom or left/right
2350
+
2351
+ // Get the actual preview resolution
2352
+ if (currentPreviewResolution == null) {
2353
+ // If we don't have the resolution yet, assume the container is filled
2354
+ return new Rect(0, 0, containerWidth, containerHeight);
2355
+ }
2356
+
2357
+ // The preview is rotated 90 degrees for portrait mode
2358
+ // So we swap the dimensions
2359
+ int cameraWidth = currentPreviewResolution.getHeight();
2360
+ int cameraHeight = currentPreviewResolution.getWidth();
2361
+
2362
+ // Calculate the scaling factor to fit the camera in the container
2363
+ float widthScale = (float) containerWidth / cameraWidth;
2364
+ float heightScale = (float) containerHeight / cameraHeight;
2365
+ float scale = Math.min(widthScale, heightScale); // FIT_CENTER uses min scale
2366
+
2367
+ // Calculate the actual size of the camera content after scaling
2368
+ int scaledWidth = Math.round(cameraWidth * scale);
2369
+ int scaledHeight = Math.round(cameraHeight * scale);
2370
+
2371
+ // Calculate the offset to center the content
2372
+ int offsetX = (containerWidth - scaledWidth) / 2;
2373
+ int offsetY = (containerHeight - scaledHeight) / 2;
2374
+
2375
+ Log.d(
2376
+ TAG,
2377
+ "getActualCameraBounds: container=" +
2378
+ containerWidth +
2379
+ "x" +
2380
+ containerHeight +
2381
+ ", camera=" +
2382
+ cameraWidth +
2383
+ "x" +
2384
+ cameraHeight +
2385
+ ", scale=" +
2386
+ scale +
2387
+ ", scaled=" +
2388
+ scaledWidth +
2389
+ "x" +
2390
+ scaledHeight +
2391
+ ", offset=(" +
2392
+ offsetX +
2393
+ "," +
2394
+ offsetY +
2395
+ ")"
2396
+ );
2397
+
2398
+ // Return the bounds relative to the container
2399
+ return new Rect(
2400
+ offsetX,
2401
+ offsetY,
2402
+ offsetX + scaledWidth,
2403
+ offsetY + scaledHeight
2404
+ );
2405
+ }
2406
+
1811
2407
  public int getPreviewWidth() {
1812
- return previewContainer != null ? previewContainer.getWidth() : 0;
2408
+ if (previewContainer == null) return 0;
2409
+ Rect bounds = getActualCameraBounds();
2410
+ return bounds.width();
1813
2411
  }
1814
2412
 
1815
2413
  public int getPreviewHeight() {
1816
- return previewContainer != null ? previewContainer.getHeight() : 0;
2414
+ if (previewContainer == null) return 0;
2415
+ Rect bounds = getActualCameraBounds();
2416
+ return bounds.height();
1817
2417
  }
1818
2418
 
1819
2419
  public void setPreviewSize(int x, int y, int width, int height) {
@@ -2133,15 +2733,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2133
2733
  }
2134
2734
  }
2135
2735
 
2136
- private void updatePreviewLayout() {
2137
- if (previewContainer == null || sessionConfig == null) return;
2138
-
2139
- String aspectRatio = sessionConfig.getAspectRatio();
2140
- if (aspectRatio == null) return;
2141
-
2142
- updatePreviewLayoutForAspectRatio(aspectRatio);
2143
- }
2144
-
2145
2736
  private int getWebViewTopInset() {
2146
2737
  try {
2147
2738
  if (webView != null) {
@@ -2206,4 +2797,85 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2206
2797
 
2207
2798
  return new int[] { x, y, width, height };
2208
2799
  }
2800
+
2801
+ private void triggerAutoFocus() {
2802
+ if (camera == null) {
2803
+ return;
2804
+ }
2805
+
2806
+ Log.d(TAG, "triggerAutoFocus: Triggering autofocus at center");
2807
+
2808
+ // Cancel any ongoing focus operation
2809
+ if (currentFocusFuture != null && !currentFocusFuture.isDone()) {
2810
+ Log.d(TAG, "triggerAutoFocus: Cancelling previous focus operation");
2811
+ currentFocusFuture.cancel(true);
2812
+ }
2813
+
2814
+ // Focus on the center of the view
2815
+ int viewWidth = previewView.getWidth();
2816
+ int viewHeight = previewView.getHeight();
2817
+
2818
+ if (viewWidth == 0 || viewHeight == 0) {
2819
+ return;
2820
+ }
2821
+
2822
+ // Create MeteringPoint at the center of the preview
2823
+ MeteringPointFactory factory = previewView.getMeteringPointFactory();
2824
+ MeteringPoint point = factory.createPoint(viewWidth / 2f, viewHeight / 2f);
2825
+
2826
+ // Create focus and metering action
2827
+ FocusMeteringAction action = new FocusMeteringAction.Builder(
2828
+ point,
2829
+ FocusMeteringAction.FLAG_AF | FocusMeteringAction.FLAG_AE
2830
+ )
2831
+ .setAutoCancelDuration(3, TimeUnit.SECONDS) // Auto-cancel after 3 seconds
2832
+ .build();
2833
+
2834
+ try {
2835
+ currentFocusFuture = camera
2836
+ .getCameraControl()
2837
+ .startFocusAndMetering(action);
2838
+ currentFocusFuture.addListener(
2839
+ () -> {
2840
+ try {
2841
+ FocusMeteringResult result = currentFocusFuture.get();
2842
+ Log.d(
2843
+ TAG,
2844
+ "triggerAutoFocus: Focus completed successfully: " +
2845
+ result.isFocusSuccessful()
2846
+ );
2847
+ } catch (Exception e) {
2848
+ // Handle cancellation gracefully - this is expected when rapid operations occur
2849
+ if (
2850
+ e.getMessage() != null &&
2851
+ (e
2852
+ .getMessage()
2853
+ .contains("Cancelled by another startFocusAndMetering") ||
2854
+ e.getMessage().contains("OperationCanceledException") ||
2855
+ e
2856
+ .getClass()
2857
+ .getSimpleName()
2858
+ .contains("OperationCanceledException"))
2859
+ ) {
2860
+ Log.d(
2861
+ TAG,
2862
+ "triggerAutoFocus: Auto-focus was cancelled by a newer focus request"
2863
+ );
2864
+ } else {
2865
+ Log.e(TAG, "triggerAutoFocus: Error during focus", e);
2866
+ }
2867
+ } finally {
2868
+ // Clear the reference if this is still the current operation
2869
+ if (currentFocusFuture != null && currentFocusFuture.isDone()) {
2870
+ currentFocusFuture = null;
2871
+ }
2872
+ }
2873
+ },
2874
+ ContextCompat.getMainExecutor(context)
2875
+ );
2876
+ } catch (Exception e) {
2877
+ currentFocusFuture = null;
2878
+ Log.e(TAG, "triggerAutoFocus: Failed to trigger autofocus", e);
2879
+ }
2880
+ }
2209
2881
  }