@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.
- package/README.md +28 -10
- package/android/.gradle/8.14.2/executionHistory/executionHistory.bin +0 -0
- package/android/.gradle/8.14.2/executionHistory/executionHistory.lock +0 -0
- package/android/.gradle/8.14.2/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/file-system.probe +0 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +200 -15
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +770 -98
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +9 -0
- package/dist/docs.json +56 -23
- package/dist/esm/definitions.d.ts +26 -10
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +5 -0
- package/dist/esm/web.js +256 -34
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +256 -34
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +256 -34
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreview/CameraController.swift +418 -66
- package/ios/Sources/CapgoCameraPreview/Plugin.swift +69 -18
- package/package.json +2 -2
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
482
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
int
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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
|
-
"
|
|
1288
|
-
zoomRatio +
|
|
1289
|
-
" to " +
|
|
1290
|
-
actualZoom
|
|
1600
|
+
"Focus operation was cancelled by a newer focus request"
|
|
1291
1601
|
);
|
|
1292
1602
|
} else {
|
|
1293
|
-
Log.
|
|
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
|
-
|
|
1612
|
+
ContextCompat.getMainExecutor(context)
|
|
1300
1613
|
);
|
|
1301
1614
|
} catch (Exception e) {
|
|
1302
|
-
|
|
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
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
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:
|
|
1778
|
-
|
|
1779
|
-
",
|
|
1780
|
-
|
|
1781
|
-
",
|
|
1782
|
-
|
|
2297
|
+
"getPreviewX: containerX=" +
|
|
2298
|
+
containerX +
|
|
2299
|
+
", cameraBounds.left=" +
|
|
2300
|
+
cameraBounds.left +
|
|
2301
|
+
", actualX=" +
|
|
2302
|
+
actualX
|
|
1783
2303
|
);
|
|
1784
|
-
|
|
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
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
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:
|
|
1800
|
-
|
|
1801
|
-
",
|
|
1802
|
-
|
|
1803
|
-
",
|
|
1804
|
-
|
|
2324
|
+
"getPreviewY: containerY=" +
|
|
2325
|
+
containerY +
|
|
2326
|
+
", cameraBounds.top=" +
|
|
2327
|
+
cameraBounds.top +
|
|
2328
|
+
", actualY=" +
|
|
2329
|
+
actualY
|
|
1805
2330
|
);
|
|
1806
|
-
|
|
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
|
-
|
|
2408
|
+
if (previewContainer == null) return 0;
|
|
2409
|
+
Rect bounds = getActualCameraBounds();
|
|
2410
|
+
return bounds.width();
|
|
1813
2411
|
}
|
|
1814
2412
|
|
|
1815
2413
|
public int getPreviewHeight() {
|
|
1816
|
-
|
|
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
|
}
|