@capgo/camera-preview 7.4.0-beta.13 → 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;
@@ -31,15 +38,11 @@ import androidx.camera.core.Camera;
31
38
  import androidx.camera.core.CameraInfo;
32
39
  import androidx.camera.core.CameraSelector;
33
40
  import androidx.camera.core.FocusMeteringAction;
34
- import androidx.camera.core.FocusMeteringAction;
35
- import androidx.camera.core.FocusMeteringResult;
36
41
  import androidx.camera.core.FocusMeteringResult;
37
42
  import androidx.camera.core.ImageCapture;
38
43
  import androidx.camera.core.ImageCaptureException;
39
44
  import androidx.camera.core.ImageProxy;
40
45
  import androidx.camera.core.MeteringPoint;
41
- import androidx.camera.core.MeteringPoint;
42
- import androidx.camera.core.MeteringPointFactory;
43
46
  import androidx.camera.core.MeteringPointFactory;
44
47
  import androidx.camera.core.Preview;
45
48
  import androidx.camera.core.ResolutionInfo;
@@ -55,7 +58,6 @@ import androidx.lifecycle.Lifecycle;
55
58
  import androidx.lifecycle.LifecycleObserver;
56
59
  import androidx.lifecycle.LifecycleOwner;
57
60
  import androidx.lifecycle.LifecycleRegistry;
58
- import androidx.lifecycle.OnLifecycleEvent;
59
61
  import com.ahm.capacitor.camera.preview.model.CameraSessionConfiguration;
60
62
  import com.ahm.capacitor.camera.preview.model.LensInfo;
61
63
  import com.ahm.capacitor.camera.preview.model.ZoomFactors;
@@ -65,7 +67,6 @@ import java.io.File;
65
67
  import java.io.FileOutputStream;
66
68
  import java.io.IOException;
67
69
  import java.nio.ByteBuffer;
68
- import java.nio.file.Files;
69
70
  import java.text.SimpleDateFormat;
70
71
  import java.util.ArrayList;
71
72
  import java.util.Arrays;
@@ -100,6 +101,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
100
101
  private PreviewView previewView;
101
102
  private GridOverlayView gridOverlayView;
102
103
  private FrameLayout previewContainer;
104
+ private View focusIndicatorView;
103
105
  private CameraSelector currentCameraSelector;
104
106
  private String currentDeviceId;
105
107
  private int currentFlashMode = ImageCapture.FLASH_MODE_OFF;
@@ -111,6 +113,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
111
113
  private final Executor mainExecutor;
112
114
  private ExecutorService cameraExecutor;
113
115
  private boolean isRunning = false;
116
+ private Size currentPreviewResolution = null;
117
+ private ListenableFuture<FocusMeteringResult> currentFocusFuture = null; // Track current focus operation
114
118
 
115
119
  public CameraXView(Context context, WebView webView) {
116
120
  this.context = context;
@@ -139,6 +143,47 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
139
143
 
140
144
  private void saveImageToGallery(byte[] data) {
141
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
+
142
187
  File photo = new File(
143
188
  Environment.getExternalStoragePublicDirectory(
144
189
  Environment.DIRECTORY_PICTURES
@@ -147,19 +192,19 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
147
192
  new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(
148
193
  new java.util.Date()
149
194
  ) +
150
- ".jpg"
195
+ extension
151
196
  );
152
197
  FileOutputStream fos = new FileOutputStream(photo);
153
198
  fos.write(data);
154
199
  fos.close();
155
200
 
156
201
  // Notify the gallery of the new image
157
- Intent mediaScanIntent = new Intent(
158
- Intent.ACTION_MEDIA_SCANNER_SCAN_FILE
202
+ MediaScannerConnection.scanFile(
203
+ this.context,
204
+ new String[] { photo.getAbsolutePath() },
205
+ new String[] { mimeType },
206
+ null
159
207
  );
160
- Uri contentUri = Uri.fromFile(photo);
161
- mediaScanIntent.setData(contentUri);
162
- context.sendBroadcast(mediaScanIntent);
163
208
  } catch (IOException e) {
164
209
  Log.e(TAG, "Error saving image to gallery", e);
165
210
  }
@@ -176,6 +221,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
176
221
 
177
222
  public void stopSession() {
178
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
+
179
230
  mainExecutor.execute(() -> {
180
231
  if (cameraProvider != null) {
181
232
  cameraProvider.unbindAll();
@@ -219,10 +270,70 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
219
270
 
220
271
  // Create a container to hold both the preview and grid overlay
221
272
  previewContainer = new FrameLayout(context);
273
+ // Ensure container can receive touch events
274
+ previewContainer.setClickable(true);
275
+ previewContainer.setFocusable(true);
222
276
 
223
277
  // Create and setup the preview view
224
278
  previewView = new PreviewView(context);
225
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
+
226
337
  previewContainer.addView(
227
338
  previewView,
228
339
  new FrameLayout.LayoutParams(
@@ -233,6 +344,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
233
344
 
234
345
  // Create and setup the grid overlay
235
346
  gridOverlayView = new GridOverlayView(context);
347
+ // Make grid overlay not intercept touch events
348
+ gridOverlayView.setClickable(false);
349
+ gridOverlayView.setFocusable(false);
236
350
  previewContainer.addView(
237
351
  gridOverlayView,
238
352
  new FrameLayout.LayoutParams(
@@ -252,6 +366,47 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
252
366
  FrameLayout.LayoutParams layoutParams = calculatePreviewLayoutParams();
253
367
  parent.addView(previewContainer, layoutParams);
254
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
+ });
255
410
  }
256
411
  }
257
412
 
@@ -296,8 +451,51 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
296
451
  optimalWidth = (int) (height * ratio);
297
452
  }
298
453
 
454
+ // Store the old dimensions to check if we need to recenter
455
+ int oldWidth = width;
456
+ int oldHeight = height;
299
457
  width = optimalWidth;
300
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
+
301
499
  Log.d(
302
500
  TAG,
303
501
  "calculatePreviewLayoutParams: Applied aspect ratio " +
@@ -318,17 +516,18 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
318
516
  height
319
517
  );
320
518
 
321
- // 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
322
520
  int webViewTopInset = getWebViewTopInset();
323
521
  int webViewLeftInset = getWebViewLeftInset();
324
522
 
325
- // Don't add insets if this looks like a calculated full-screen coordinate (x=0, y=0)
326
- if (x == 0 && y == 0) {
523
+ // Don't add insets if centered or full-screen
524
+ if (sessionConfig.isCentered() || (x == 0 && y == 0)) {
327
525
  layoutParams.leftMargin = x;
328
526
  layoutParams.topMargin = y;
329
527
  Log.d(
330
528
  TAG,
331
- "calculatePreviewLayoutParams: Full-screen mode - keeping position (0,0) without insets"
529
+ "calculatePreviewLayoutParams: Centered/Full-screen mode - keeping position without insets. isCentered=" +
530
+ sessionConfig.isCentered()
332
531
  );
333
532
  } else {
334
533
  layoutParams.leftMargin = x + webViewLeftInset;
@@ -341,18 +540,15 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
341
540
 
342
541
  Log.d(
343
542
  TAG,
344
- "calculatePreviewLayoutParams: Applied insets - x:" +
543
+ "calculatePreviewLayoutParams: Position calculation - x:" +
345
544
  x +
346
- "+" +
347
- webViewLeftInset +
348
- "=" +
545
+ " (leftMargin=" +
349
546
  layoutParams.leftMargin +
350
- ", y:" +
547
+ "), y:" +
351
548
  y +
352
- "+" +
353
- webViewTopInset +
354
- "=" +
355
- layoutParams.topMargin
549
+ " (topMargin=" +
550
+ layoutParams.topMargin +
551
+ ")"
356
552
  );
357
553
 
358
554
  Log.d(
@@ -366,6 +562,18 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
366
562
  " height:" +
367
563
  height
368
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
+ );
369
577
  return layoutParams;
370
578
  }
371
579
 
@@ -383,6 +591,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
383
591
  if (gridOverlayView != null) {
384
592
  gridOverlayView = null;
385
593
  }
594
+ if (focusIndicatorView != null) {
595
+ focusIndicatorView = null;
596
+ }
386
597
  webView.setBackgroundColor(android.graphics.Color.WHITE);
387
598
  }
388
599
 
@@ -487,10 +698,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
487
698
  // Log resolution info
488
699
  ResolutionInfo previewResolution = preview.getResolutionInfo();
489
700
  if (previewResolution != null) {
490
- Log.d(
491
- TAG,
492
- "Preview resolution: " + previewResolution.getResolution()
493
- );
701
+ currentPreviewResolution = previewResolution.getResolution();
702
+ Log.d(TAG, "Preview resolution: " + currentPreviewResolution);
494
703
  }
495
704
  ResolutionInfo imageCaptureResolution =
496
705
  imageCapture.getResolutionInfo();
@@ -508,6 +717,28 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
508
717
  : sessionConfig.getZoomFactor();
509
718
  if (initialZoom != 1.0f) {
510
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
+
511
742
  setZoomInternal(initialZoom);
512
743
  }
513
744
 
@@ -517,18 +748,24 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
517
748
  // Post the callback to ensure layout is complete
518
749
  previewContainer.post(() -> {
519
750
  // Return actual preview container dimensions instead of requested dimensions
520
- int actualWidth = previewContainer != null
521
- ? previewContainer.getWidth()
522
- : sessionConfig.getWidth();
523
- int actualHeight = previewContainer != null
524
- ? previewContainer.getHeight()
525
- : sessionConfig.getHeight();
526
- int actualX = previewContainer != null
527
- ? previewContainer.getLeft()
528
- : sessionConfig.getX();
529
- int actualY = previewContainer != null
530
- ? previewContainer.getTop()
531
- : 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
+
532
769
  listener.onCameraStarted(
533
770
  actualWidth,
534
771
  actualHeight,
@@ -1262,7 +1499,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1262
1499
  }
1263
1500
  }
1264
1501
 
1265
- public void setZoom(float zoomRatio) throws Exception {
1502
+ public void setZoom(float zoomRatio, boolean autoFocus) throws Exception {
1266
1503
  if (camera == null) {
1267
1504
  throw new Exception("Camera not initialized");
1268
1505
  }
@@ -1281,6 +1518,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1281
1518
  try {
1282
1519
  zoomFuture.get();
1283
1520
  Log.d(TAG, "Zoom successfully set to " + zoomRatio);
1521
+ // Trigger autofocus after zoom if requested
1522
+ if (autoFocus) {
1523
+ triggerAutoFocus();
1524
+ }
1284
1525
  } catch (Exception e) {
1285
1526
  Log.e(TAG, "Error setting zoom: " + e.getMessage());
1286
1527
  }
@@ -1298,11 +1539,27 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1298
1539
  throw new Exception("Camera not initialized");
1299
1540
  }
1300
1541
 
1301
- Log.d(TAG, "setFocus: Setting focus at (" + x + ", " + y + ")");
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
+ }
1302
1551
 
1303
- // Convert normalized coordinates (0-1) to view coordinates
1304
1552
  int viewWidth = previewView.getWidth();
1305
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
+ }
1306
1563
 
1307
1564
  // Create MeteringPoint using the preview view
1308
1565
  MeteringPointFactory factory = previewView.getMeteringPointFactory();
@@ -1317,27 +1574,252 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1317
1574
  .build();
1318
1575
 
1319
1576
  try {
1320
- ListenableFuture<FocusMeteringResult> focusFuture = camera
1577
+ currentFocusFuture = camera
1321
1578
  .getCameraControl()
1322
1579
  .startFocusAndMetering(action);
1323
1580
 
1324
- focusFuture.addListener(
1581
+ currentFocusFuture.addListener(
1325
1582
  () -> {
1326
1583
  try {
1327
- FocusMeteringResult result = focusFuture.get();
1328
- Log.d(TAG, "Focus result: " + result.isFocusSuccessful());
1584
+ FocusMeteringResult result = currentFocusFuture.get();
1329
1585
  } catch (Exception e) {
1330
- Log.e(TAG, "Error during focus: " + e.getMessage());
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(
1599
+ TAG,
1600
+ "Focus operation was cancelled by a newer focus request"
1601
+ );
1602
+ } else {
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;
1609
+ }
1331
1610
  }
1332
1611
  },
1333
1612
  ContextCompat.getMainExecutor(context)
1334
1613
  );
1335
1614
  } catch (Exception e) {
1615
+ currentFocusFuture = null;
1336
1616
  Log.e(TAG, "Failed to set focus: " + e.getMessage());
1337
1617
  throw e;
1338
1618
  }
1339
1619
  }
1340
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
+
1341
1823
  public static List<Size> getSupportedPictureSizes(String facing) {
1342
1824
  List<Size> sizes = new ArrayList<>();
1343
1825
  try {
@@ -1800,54 +2282,138 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1800
2282
 
1801
2283
  public int getPreviewX() {
1802
2284
  if (previewContainer == null) return 0;
2285
+
2286
+ // Get the container position
1803
2287
  ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
1804
2288
  if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
1805
- // Return position relative to WebView content (subtract insets)
1806
- int margin = ((ViewGroup.MarginLayoutParams) layoutParams).leftMargin;
1807
- int leftInset = getWebViewLeftInset();
1808
- 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
+
1809
2295
  Log.d(
1810
2296
  TAG,
1811
- "getPreviewX: leftMargin=" +
1812
- margin +
1813
- ", leftInset=" +
1814
- leftInset +
1815
- ", result=" +
1816
- result
2297
+ "getPreviewX: containerX=" +
2298
+ containerX +
2299
+ ", cameraBounds.left=" +
2300
+ cameraBounds.left +
2301
+ ", actualX=" +
2302
+ actualX
1817
2303
  );
1818
- return result;
2304
+
2305
+ return actualX;
1819
2306
  }
1820
2307
  return previewContainer.getLeft();
1821
2308
  }
1822
2309
 
1823
2310
  public int getPreviewY() {
1824
2311
  if (previewContainer == null) return 0;
2312
+
2313
+ // Get the container position
1825
2314
  ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
1826
2315
  if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
1827
- // Return position relative to WebView content (subtract insets)
1828
- int margin = ((ViewGroup.MarginLayoutParams) layoutParams).topMargin;
1829
- int topInset = getWebViewTopInset();
1830
- 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
+
1831
2322
  Log.d(
1832
2323
  TAG,
1833
- "getPreviewY: topMargin=" +
1834
- margin +
1835
- ", topInset=" +
1836
- topInset +
1837
- ", result=" +
1838
- result
2324
+ "getPreviewY: containerY=" +
2325
+ containerY +
2326
+ ", cameraBounds.top=" +
2327
+ cameraBounds.top +
2328
+ ", actualY=" +
2329
+ actualY
1839
2330
  );
1840
- return result;
2331
+
2332
+ return actualY;
1841
2333
  }
1842
2334
  return previewContainer.getTop();
1843
2335
  }
1844
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
+
1845
2407
  public int getPreviewWidth() {
1846
- return previewContainer != null ? previewContainer.getWidth() : 0;
2408
+ if (previewContainer == null) return 0;
2409
+ Rect bounds = getActualCameraBounds();
2410
+ return bounds.width();
1847
2411
  }
1848
2412
 
1849
2413
  public int getPreviewHeight() {
1850
- return previewContainer != null ? previewContainer.getHeight() : 0;
2414
+ if (previewContainer == null) return 0;
2415
+ Rect bounds = getActualCameraBounds();
2416
+ return bounds.height();
1851
2417
  }
1852
2418
 
1853
2419
  public void setPreviewSize(int x, int y, int width, int height) {
@@ -2167,15 +2733,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2167
2733
  }
2168
2734
  }
2169
2735
 
2170
- private void updatePreviewLayout() {
2171
- if (previewContainer == null || sessionConfig == null) return;
2172
-
2173
- String aspectRatio = sessionConfig.getAspectRatio();
2174
- if (aspectRatio == null) return;
2175
-
2176
- updatePreviewLayoutForAspectRatio(aspectRatio);
2177
- }
2178
-
2179
2736
  private int getWebViewTopInset() {
2180
2737
  try {
2181
2738
  if (webView != null) {
@@ -2240,4 +2797,85 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2240
2797
 
2241
2798
  return new int[] { x, y, width, height };
2242
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
+ }
2243
2881
  }