@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.
- package/README.md +10 -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 +177 -19
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +725 -87
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +9 -0
- package/dist/docs.json +27 -23
- package/dist/esm/definitions.d.ts +13 -10
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +1 -0
- package/dist/esm/web.js +223 -34
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +223 -34
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +223 -34
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreview/CameraController.swift +373 -65
- package/ios/Sources/CapgoCameraPreview/Plugin.swift +35 -22
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
491
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
int
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1577
|
+
currentFocusFuture = camera
|
|
1321
1578
|
.getCameraControl()
|
|
1322
1579
|
.startFocusAndMetering(action);
|
|
1323
1580
|
|
|
1324
|
-
|
|
1581
|
+
currentFocusFuture.addListener(
|
|
1325
1582
|
() -> {
|
|
1326
1583
|
try {
|
|
1327
|
-
FocusMeteringResult result =
|
|
1328
|
-
Log.d(TAG, "Focus result: " + result.isFocusSuccessful());
|
|
1584
|
+
FocusMeteringResult result = currentFocusFuture.get();
|
|
1329
1585
|
} catch (Exception e) {
|
|
1330
|
-
|
|
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
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
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:
|
|
1812
|
-
|
|
1813
|
-
",
|
|
1814
|
-
|
|
1815
|
-
",
|
|
1816
|
-
|
|
2297
|
+
"getPreviewX: containerX=" +
|
|
2298
|
+
containerX +
|
|
2299
|
+
", cameraBounds.left=" +
|
|
2300
|
+
cameraBounds.left +
|
|
2301
|
+
", actualX=" +
|
|
2302
|
+
actualX
|
|
1817
2303
|
);
|
|
1818
|
-
|
|
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
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
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:
|
|
1834
|
-
|
|
1835
|
-
",
|
|
1836
|
-
|
|
1837
|
-
",
|
|
1838
|
-
|
|
2324
|
+
"getPreviewY: containerY=" +
|
|
2325
|
+
containerY +
|
|
2326
|
+
", cameraBounds.top=" +
|
|
2327
|
+
cameraBounds.top +
|
|
2328
|
+
", actualY=" +
|
|
2329
|
+
actualY
|
|
1839
2330
|
);
|
|
1840
|
-
|
|
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
|
-
|
|
2408
|
+
if (previewContainer == null) return 0;
|
|
2409
|
+
Rect bounds = getActualCameraBounds();
|
|
2410
|
+
return bounds.width();
|
|
1847
2411
|
}
|
|
1848
2412
|
|
|
1849
2413
|
public int getPreviewHeight() {
|
|
1850
|
-
|
|
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
|
}
|