@capgo/camera-preview 7.4.0-beta.13 → 7.4.0-beta.16
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 +19 -18
- 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 +271 -20
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +816 -134
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +9 -0
- package/dist/docs.json +39 -23
- package/dist/esm/definitions.d.ts +20 -10
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +1 -0
- package/dist/esm/web.js +266 -38
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +266 -38
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +266 -38
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreview/CameraController.swift +430 -71
- package/ios/Sources/CapgoCameraPreview/Plugin.swift +139 -114
- 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,52 @@ 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
|
+
// Always center based on full screen height
|
|
484
|
+
y = (screenHeight - height) / 2;
|
|
485
|
+
Log.d(
|
|
486
|
+
TAG,
|
|
487
|
+
"calculatePreviewLayoutParams: Recentered Y after aspect ratio - " +
|
|
488
|
+
"oldHeight=" +
|
|
489
|
+
oldHeight +
|
|
490
|
+
", newHeight=" +
|
|
491
|
+
height +
|
|
492
|
+
", screenHeight=" +
|
|
493
|
+
screenHeight +
|
|
494
|
+
", newY=" +
|
|
495
|
+
y
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
301
500
|
Log.d(
|
|
302
501
|
TAG,
|
|
303
502
|
"calculatePreviewLayoutParams: Applied aspect ratio " +
|
|
@@ -318,41 +517,22 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
318
517
|
height
|
|
319
518
|
);
|
|
320
519
|
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
// Don't add insets if this looks like a calculated full-screen coordinate (x=0, y=0)
|
|
326
|
-
if (x == 0 && y == 0) {
|
|
327
|
-
layoutParams.leftMargin = x;
|
|
328
|
-
layoutParams.topMargin = y;
|
|
329
|
-
Log.d(
|
|
330
|
-
TAG,
|
|
331
|
-
"calculatePreviewLayoutParams: Full-screen mode - keeping position (0,0) without insets"
|
|
332
|
-
);
|
|
333
|
-
} else {
|
|
334
|
-
layoutParams.leftMargin = x + webViewLeftInset;
|
|
335
|
-
layoutParams.topMargin = y + webViewTopInset;
|
|
336
|
-
Log.d(
|
|
337
|
-
TAG,
|
|
338
|
-
"calculatePreviewLayoutParams: Positioned mode - applying insets"
|
|
339
|
-
);
|
|
340
|
-
}
|
|
520
|
+
// The X and Y positions passed from CameraPreview already include webView insets
|
|
521
|
+
// when edge-to-edge is active, so we don't need to add them again here
|
|
522
|
+
layoutParams.leftMargin = x;
|
|
523
|
+
layoutParams.topMargin = y;
|
|
341
524
|
|
|
342
525
|
Log.d(
|
|
343
526
|
TAG,
|
|
344
|
-
"calculatePreviewLayoutParams:
|
|
527
|
+
"calculatePreviewLayoutParams: Position calculation - x:" +
|
|
345
528
|
x +
|
|
346
|
-
"
|
|
347
|
-
webViewLeftInset +
|
|
348
|
-
"=" +
|
|
529
|
+
" (leftMargin=" +
|
|
349
530
|
layoutParams.leftMargin +
|
|
350
|
-
", y:" +
|
|
531
|
+
"), y:" +
|
|
351
532
|
y +
|
|
352
|
-
"
|
|
353
|
-
|
|
354
|
-
"
|
|
355
|
-
layoutParams.topMargin
|
|
533
|
+
" (topMargin=" +
|
|
534
|
+
layoutParams.topMargin +
|
|
535
|
+
")"
|
|
356
536
|
);
|
|
357
537
|
|
|
358
538
|
Log.d(
|
|
@@ -383,6 +563,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
383
563
|
if (gridOverlayView != null) {
|
|
384
564
|
gridOverlayView = null;
|
|
385
565
|
}
|
|
566
|
+
if (focusIndicatorView != null) {
|
|
567
|
+
focusIndicatorView = null;
|
|
568
|
+
}
|
|
386
569
|
webView.setBackgroundColor(android.graphics.Color.WHITE);
|
|
387
570
|
}
|
|
388
571
|
|
|
@@ -487,10 +670,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
487
670
|
// Log resolution info
|
|
488
671
|
ResolutionInfo previewResolution = preview.getResolutionInfo();
|
|
489
672
|
if (previewResolution != null) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
"Preview resolution: " + previewResolution.getResolution()
|
|
493
|
-
);
|
|
673
|
+
currentPreviewResolution = previewResolution.getResolution();
|
|
674
|
+
Log.d(TAG, "Preview resolution: " + currentPreviewResolution);
|
|
494
675
|
}
|
|
495
676
|
ResolutionInfo imageCaptureResolution =
|
|
496
677
|
imageCapture.getResolutionInfo();
|
|
@@ -508,6 +689,28 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
508
689
|
: sessionConfig.getZoomFactor();
|
|
509
690
|
if (initialZoom != 1.0f) {
|
|
510
691
|
Log.d(TAG, "Applying initial zoom of " + initialZoom);
|
|
692
|
+
|
|
693
|
+
// Validate zoom is within bounds
|
|
694
|
+
if (zoomState != null) {
|
|
695
|
+
float minZoom = zoomState.getMinZoomRatio();
|
|
696
|
+
float maxZoom = zoomState.getMaxZoomRatio();
|
|
697
|
+
|
|
698
|
+
if (initialZoom < minZoom || initialZoom > maxZoom) {
|
|
699
|
+
if (listener != null) {
|
|
700
|
+
listener.onCameraStartError(
|
|
701
|
+
"Initial zoom level " +
|
|
702
|
+
initialZoom +
|
|
703
|
+
" is not available. " +
|
|
704
|
+
"Valid range is " +
|
|
705
|
+
minZoom +
|
|
706
|
+
" to " +
|
|
707
|
+
maxZoom
|
|
708
|
+
);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
511
714
|
setZoomInternal(initialZoom);
|
|
512
715
|
}
|
|
513
716
|
|
|
@@ -517,18 +720,24 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
517
720
|
// Post the callback to ensure layout is complete
|
|
518
721
|
previewContainer.post(() -> {
|
|
519
722
|
// Return actual preview container dimensions instead of requested dimensions
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
int
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
723
|
+
// Get the actual camera dimensions and position
|
|
724
|
+
int actualWidth = getPreviewWidth();
|
|
725
|
+
int actualHeight = getPreviewHeight();
|
|
726
|
+
int actualX = getPreviewX();
|
|
727
|
+
int actualY = getPreviewY();
|
|
728
|
+
|
|
729
|
+
Log.d(
|
|
730
|
+
TAG,
|
|
731
|
+
"onCameraStarted callback - actualX=" +
|
|
732
|
+
actualX +
|
|
733
|
+
", actualY=" +
|
|
734
|
+
actualY +
|
|
735
|
+
", actualWidth=" +
|
|
736
|
+
actualWidth +
|
|
737
|
+
", actualHeight=" +
|
|
738
|
+
actualHeight
|
|
739
|
+
);
|
|
740
|
+
|
|
532
741
|
listener.onCameraStarted(
|
|
533
742
|
actualWidth,
|
|
534
743
|
actualHeight,
|
|
@@ -615,9 +824,18 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
615
824
|
final boolean saveToGallery,
|
|
616
825
|
Integer width,
|
|
617
826
|
Integer height,
|
|
827
|
+
String aspectRatio,
|
|
618
828
|
Location location
|
|
619
829
|
) {
|
|
620
|
-
Log.d(TAG, "capturePhoto: Starting photo capture with quality: " + quality);
|
|
830
|
+
Log.d(TAG, "capturePhoto: Starting photo capture with quality: " + quality + ", aspectRatio: " + aspectRatio);
|
|
831
|
+
|
|
832
|
+
// Check for conflicting parameters
|
|
833
|
+
if (aspectRatio != null && (width != null || height != null)) {
|
|
834
|
+
if (listener != null) {
|
|
835
|
+
listener.onPictureTakenError("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.");
|
|
836
|
+
}
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
621
839
|
|
|
622
840
|
if (imageCapture == null) {
|
|
623
841
|
if (listener != null) {
|
|
@@ -665,7 +883,63 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
665
883
|
|
|
666
884
|
JSONObject exifData = getExifData(exifInterface);
|
|
667
885
|
|
|
668
|
-
|
|
886
|
+
// Handle aspect ratio if no width/height specified
|
|
887
|
+
if (width == null && height == null && aspectRatio != null && !aspectRatio.isEmpty()) {
|
|
888
|
+
// Get the original image dimensions
|
|
889
|
+
Bitmap originalBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
|
|
890
|
+
int originalWidth = originalBitmap.getWidth();
|
|
891
|
+
int originalHeight = originalBitmap.getHeight();
|
|
892
|
+
|
|
893
|
+
// Parse aspect ratio
|
|
894
|
+
String[] ratios = aspectRatio.split(":");
|
|
895
|
+
if (ratios.length == 2) {
|
|
896
|
+
try {
|
|
897
|
+
float widthRatio = Float.parseFloat(ratios[0]);
|
|
898
|
+
float heightRatio = Float.parseFloat(ratios[1]);
|
|
899
|
+
|
|
900
|
+
// For capture in portrait orientation, swap the aspect ratio (16:9 becomes 9:16)
|
|
901
|
+
boolean isPortrait = originalHeight > originalWidth;
|
|
902
|
+
float targetAspectRatio = isPortrait ? heightRatio / widthRatio : widthRatio / heightRatio;
|
|
903
|
+
float originalAspectRatio = (float) originalWidth / originalHeight;
|
|
904
|
+
|
|
905
|
+
int targetWidth, targetHeight;
|
|
906
|
+
|
|
907
|
+
if (originalAspectRatio > targetAspectRatio) {
|
|
908
|
+
// Original is wider than target - fit by height
|
|
909
|
+
targetHeight = originalHeight;
|
|
910
|
+
targetWidth = (int) (targetHeight * targetAspectRatio);
|
|
911
|
+
} else {
|
|
912
|
+
// Original is taller than target - fit by width
|
|
913
|
+
targetWidth = originalWidth;
|
|
914
|
+
targetHeight = (int) (targetWidth / targetAspectRatio);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Center crop the image
|
|
918
|
+
int xOffset = (originalWidth - targetWidth) / 2;
|
|
919
|
+
int yOffset = (originalHeight - targetHeight) / 2;
|
|
920
|
+
|
|
921
|
+
Bitmap croppedBitmap = Bitmap.createBitmap(
|
|
922
|
+
originalBitmap,
|
|
923
|
+
xOffset,
|
|
924
|
+
yOffset,
|
|
925
|
+
targetWidth,
|
|
926
|
+
targetHeight
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
930
|
+
croppedBitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream);
|
|
931
|
+
bytes = stream.toByteArray();
|
|
932
|
+
|
|
933
|
+
// Write EXIF data back to cropped image
|
|
934
|
+
bytes = writeExifToImageBytes(bytes, exifInterface);
|
|
935
|
+
|
|
936
|
+
originalBitmap.recycle();
|
|
937
|
+
croppedBitmap.recycle();
|
|
938
|
+
} catch (NumberFormatException e) {
|
|
939
|
+
Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio, e);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
} else if (width != null && height != null) {
|
|
669
943
|
Bitmap bitmap = BitmapFactory.decodeByteArray(
|
|
670
944
|
bytes,
|
|
671
945
|
0,
|
|
@@ -1262,7 +1536,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1262
1536
|
}
|
|
1263
1537
|
}
|
|
1264
1538
|
|
|
1265
|
-
public void setZoom(float zoomRatio) throws Exception {
|
|
1539
|
+
public void setZoom(float zoomRatio, boolean autoFocus) throws Exception {
|
|
1266
1540
|
if (camera == null) {
|
|
1267
1541
|
throw new Exception("Camera not initialized");
|
|
1268
1542
|
}
|
|
@@ -1281,6 +1555,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1281
1555
|
try {
|
|
1282
1556
|
zoomFuture.get();
|
|
1283
1557
|
Log.d(TAG, "Zoom successfully set to " + zoomRatio);
|
|
1558
|
+
// Trigger autofocus after zoom if requested
|
|
1559
|
+
if (autoFocus) {
|
|
1560
|
+
triggerAutoFocus();
|
|
1561
|
+
}
|
|
1284
1562
|
} catch (Exception e) {
|
|
1285
1563
|
Log.e(TAG, "Error setting zoom: " + e.getMessage());
|
|
1286
1564
|
}
|
|
@@ -1298,11 +1576,35 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1298
1576
|
throw new Exception("Camera not initialized");
|
|
1299
1577
|
}
|
|
1300
1578
|
|
|
1301
|
-
|
|
1579
|
+
if (previewView == null) {
|
|
1580
|
+
throw new Exception("Preview view not initialized");
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// Validate that coordinates are within bounds (0-1 range)
|
|
1584
|
+
if (x < 0f || x > 1f || y < 0f || y > 1f) {
|
|
1585
|
+
Log.w(TAG, "setFocus: Coordinates out of bounds - x: " + x + ", y: " + y);
|
|
1586
|
+
throw new Exception("Focus coordinates must be between 0 and 1");
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// Cancel any ongoing focus operation
|
|
1590
|
+
if (currentFocusFuture != null && !currentFocusFuture.isDone()) {
|
|
1591
|
+
Log.d(TAG, "setFocus: Cancelling previous focus operation");
|
|
1592
|
+
currentFocusFuture.cancel(true);
|
|
1593
|
+
}
|
|
1302
1594
|
|
|
1303
|
-
// Convert normalized coordinates (0-1) to view coordinates
|
|
1304
1595
|
int viewWidth = previewView.getWidth();
|
|
1305
1596
|
int viewHeight = previewView.getHeight();
|
|
1597
|
+
|
|
1598
|
+
if (viewWidth <= 0 || viewHeight <= 0) {
|
|
1599
|
+
throw new Exception(
|
|
1600
|
+
"Preview view has invalid dimensions: " + viewWidth + "x" + viewHeight
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// Only show focus indicator after validation passes
|
|
1605
|
+
float indicatorX = x * viewWidth;
|
|
1606
|
+
float indicatorY = y * viewHeight;
|
|
1607
|
+
showFocusIndicator(indicatorX, indicatorY);
|
|
1306
1608
|
|
|
1307
1609
|
// Create MeteringPoint using the preview view
|
|
1308
1610
|
MeteringPointFactory factory = previewView.getMeteringPointFactory();
|
|
@@ -1317,27 +1619,252 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1317
1619
|
.build();
|
|
1318
1620
|
|
|
1319
1621
|
try {
|
|
1320
|
-
|
|
1622
|
+
currentFocusFuture = camera
|
|
1321
1623
|
.getCameraControl()
|
|
1322
1624
|
.startFocusAndMetering(action);
|
|
1323
1625
|
|
|
1324
|
-
|
|
1626
|
+
currentFocusFuture.addListener(
|
|
1325
1627
|
() -> {
|
|
1326
1628
|
try {
|
|
1327
|
-
FocusMeteringResult result =
|
|
1328
|
-
Log.d(TAG, "Focus result: " + result.isFocusSuccessful());
|
|
1629
|
+
FocusMeteringResult result = currentFocusFuture.get();
|
|
1329
1630
|
} catch (Exception e) {
|
|
1330
|
-
|
|
1631
|
+
// Handle cancellation gracefully - this is expected when rapid taps occur
|
|
1632
|
+
if (
|
|
1633
|
+
e.getMessage() != null &&
|
|
1634
|
+
(e
|
|
1635
|
+
.getMessage()
|
|
1636
|
+
.contains("Cancelled by another startFocusAndMetering") ||
|
|
1637
|
+
e.getMessage().contains("OperationCanceledException") ||
|
|
1638
|
+
e
|
|
1639
|
+
.getClass()
|
|
1640
|
+
.getSimpleName()
|
|
1641
|
+
.contains("OperationCanceledException"))
|
|
1642
|
+
) {
|
|
1643
|
+
Log.d(
|
|
1644
|
+
TAG,
|
|
1645
|
+
"Focus operation was cancelled by a newer focus request"
|
|
1646
|
+
);
|
|
1647
|
+
} else {
|
|
1648
|
+
Log.e(TAG, "Error during focus: " + e.getMessage());
|
|
1649
|
+
}
|
|
1650
|
+
} finally {
|
|
1651
|
+
// Clear the reference if this is still the current operation
|
|
1652
|
+
if (currentFocusFuture != null && currentFocusFuture.isDone()) {
|
|
1653
|
+
currentFocusFuture = null;
|
|
1654
|
+
}
|
|
1331
1655
|
}
|
|
1332
1656
|
},
|
|
1333
1657
|
ContextCompat.getMainExecutor(context)
|
|
1334
1658
|
);
|
|
1335
1659
|
} catch (Exception e) {
|
|
1660
|
+
currentFocusFuture = null;
|
|
1336
1661
|
Log.e(TAG, "Failed to set focus: " + e.getMessage());
|
|
1337
1662
|
throw e;
|
|
1338
1663
|
}
|
|
1339
1664
|
}
|
|
1340
1665
|
|
|
1666
|
+
private void showFocusIndicator(float x, float y) {
|
|
1667
|
+
if (previewContainer == null) {
|
|
1668
|
+
Log.w(TAG, "showFocusIndicator: previewContainer is null");
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// Check if container has been laid out
|
|
1673
|
+
if (previewContainer.getWidth() == 0 || previewContainer.getHeight() == 0) {
|
|
1674
|
+
Log.w(
|
|
1675
|
+
TAG,
|
|
1676
|
+
"showFocusIndicator: previewContainer not laid out yet, posting to run after layout"
|
|
1677
|
+
);
|
|
1678
|
+
previewContainer.post(() -> showFocusIndicator(x, y));
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Remove any existing focus indicator
|
|
1683
|
+
if (focusIndicatorView != null) {
|
|
1684
|
+
previewContainer.removeView(focusIndicatorView);
|
|
1685
|
+
focusIndicatorView = null;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// Create an elegant focus indicator
|
|
1689
|
+
View container = new View(context);
|
|
1690
|
+
int size = (int) (60 * context.getResources().getDisplayMetrics().density); // 60dp size
|
|
1691
|
+
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(size, size);
|
|
1692
|
+
|
|
1693
|
+
// Center the indicator on the touch point with bounds checking
|
|
1694
|
+
int containerWidth = previewContainer.getWidth();
|
|
1695
|
+
int containerHeight = previewContainer.getHeight();
|
|
1696
|
+
|
|
1697
|
+
params.leftMargin = Math.max(
|
|
1698
|
+
0,
|
|
1699
|
+
Math.min((int) (x - size / 2), containerWidth - size)
|
|
1700
|
+
);
|
|
1701
|
+
params.topMargin = Math.max(
|
|
1702
|
+
0,
|
|
1703
|
+
Math.min((int) (y - size / 2), containerHeight - size)
|
|
1704
|
+
);
|
|
1705
|
+
|
|
1706
|
+
// Create an elegant focus ring - white stroke with transparent center
|
|
1707
|
+
GradientDrawable drawable = new GradientDrawable();
|
|
1708
|
+
drawable.setShape(GradientDrawable.OVAL);
|
|
1709
|
+
drawable.setStroke(
|
|
1710
|
+
(int) (2 * context.getResources().getDisplayMetrics().density),
|
|
1711
|
+
Color.WHITE
|
|
1712
|
+
); // 2dp white stroke
|
|
1713
|
+
drawable.setColor(Color.TRANSPARENT); // Transparent center
|
|
1714
|
+
container.setBackground(drawable);
|
|
1715
|
+
|
|
1716
|
+
focusIndicatorView = container;
|
|
1717
|
+
|
|
1718
|
+
// Set initial state for smooth animation
|
|
1719
|
+
focusIndicatorView.setAlpha(1f); // Start visible
|
|
1720
|
+
focusIndicatorView.setScaleX(1.8f); // Start larger for scale-in effect
|
|
1721
|
+
focusIndicatorView.setScaleY(1.8f);
|
|
1722
|
+
focusIndicatorView.setVisibility(View.VISIBLE);
|
|
1723
|
+
|
|
1724
|
+
// Ensure container doesn't intercept touch events
|
|
1725
|
+
container.setClickable(false);
|
|
1726
|
+
container.setFocusable(false);
|
|
1727
|
+
|
|
1728
|
+
// Ensure the focus indicator has a high elevation for visibility
|
|
1729
|
+
if (
|
|
1730
|
+
android.os.Build.VERSION.SDK_INT >=
|
|
1731
|
+
android.os.Build.VERSION_CODES.LOLLIPOP
|
|
1732
|
+
) {
|
|
1733
|
+
focusIndicatorView.setElevation(10f);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// Add to container first
|
|
1737
|
+
previewContainer.addView(focusIndicatorView, params);
|
|
1738
|
+
|
|
1739
|
+
// Fix z-ordering: ensure focus indicator is always on top
|
|
1740
|
+
focusIndicatorView.bringToFront();
|
|
1741
|
+
|
|
1742
|
+
// Force a layout pass to ensure the view is properly positioned
|
|
1743
|
+
previewContainer.requestLayout();
|
|
1744
|
+
|
|
1745
|
+
// Smooth scale down animation with easing (no fade needed since we start visible)
|
|
1746
|
+
ScaleAnimation scaleAnimation = new ScaleAnimation(
|
|
1747
|
+
1.8f,
|
|
1748
|
+
1.0f,
|
|
1749
|
+
1.8f,
|
|
1750
|
+
1.0f,
|
|
1751
|
+
Animation.RELATIVE_TO_SELF,
|
|
1752
|
+
0.5f,
|
|
1753
|
+
Animation.RELATIVE_TO_SELF,
|
|
1754
|
+
0.5f
|
|
1755
|
+
);
|
|
1756
|
+
scaleAnimation.setDuration(300);
|
|
1757
|
+
scaleAnimation.setInterpolator(
|
|
1758
|
+
new android.view.animation.OvershootInterpolator(1.2f)
|
|
1759
|
+
);
|
|
1760
|
+
|
|
1761
|
+
// Start the animation
|
|
1762
|
+
focusIndicatorView.startAnimation(scaleAnimation);
|
|
1763
|
+
|
|
1764
|
+
// Schedule fade out and removal with smoother timing
|
|
1765
|
+
focusIndicatorView.postDelayed(
|
|
1766
|
+
new Runnable() {
|
|
1767
|
+
@Override
|
|
1768
|
+
public void run() {
|
|
1769
|
+
if (focusIndicatorView != null) {
|
|
1770
|
+
// Smooth fade to semi-transparent
|
|
1771
|
+
AlphaAnimation fadeToTransparent = new AlphaAnimation(1f, 0.4f);
|
|
1772
|
+
fadeToTransparent.setDuration(400);
|
|
1773
|
+
fadeToTransparent.setInterpolator(
|
|
1774
|
+
new android.view.animation.AccelerateInterpolator()
|
|
1775
|
+
);
|
|
1776
|
+
|
|
1777
|
+
fadeToTransparent.setAnimationListener(
|
|
1778
|
+
new Animation.AnimationListener() {
|
|
1779
|
+
@Override
|
|
1780
|
+
public void onAnimationStart(Animation animation) {
|
|
1781
|
+
Log.d(TAG, "showFocusIndicator: Fade to transparent started");
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
@Override
|
|
1785
|
+
public void onAnimationEnd(Animation animation) {
|
|
1786
|
+
Log.d(
|
|
1787
|
+
TAG,
|
|
1788
|
+
"showFocusIndicator: Fade to transparent ended, starting final fade out"
|
|
1789
|
+
);
|
|
1790
|
+
// Final smooth fade out and scale down
|
|
1791
|
+
if (focusIndicatorView != null) {
|
|
1792
|
+
AnimationSet finalAnimation = new AnimationSet(false);
|
|
1793
|
+
|
|
1794
|
+
AlphaAnimation finalFadeOut = new AlphaAnimation(0.4f, 0f);
|
|
1795
|
+
finalFadeOut.setDuration(500);
|
|
1796
|
+
finalFadeOut.setStartOffset(300);
|
|
1797
|
+
finalFadeOut.setInterpolator(
|
|
1798
|
+
new android.view.animation.AccelerateInterpolator()
|
|
1799
|
+
);
|
|
1800
|
+
|
|
1801
|
+
ScaleAnimation finalScaleDown = new ScaleAnimation(
|
|
1802
|
+
1.0f,
|
|
1803
|
+
0.9f,
|
|
1804
|
+
1.0f,
|
|
1805
|
+
0.9f,
|
|
1806
|
+
Animation.RELATIVE_TO_SELF,
|
|
1807
|
+
0.5f,
|
|
1808
|
+
Animation.RELATIVE_TO_SELF,
|
|
1809
|
+
0.5f
|
|
1810
|
+
);
|
|
1811
|
+
finalScaleDown.setDuration(500);
|
|
1812
|
+
finalScaleDown.setStartOffset(300);
|
|
1813
|
+
finalScaleDown.setInterpolator(
|
|
1814
|
+
new android.view.animation.AccelerateInterpolator()
|
|
1815
|
+
);
|
|
1816
|
+
|
|
1817
|
+
finalAnimation.addAnimation(finalFadeOut);
|
|
1818
|
+
finalAnimation.addAnimation(finalScaleDown);
|
|
1819
|
+
|
|
1820
|
+
finalAnimation.setAnimationListener(
|
|
1821
|
+
new Animation.AnimationListener() {
|
|
1822
|
+
@Override
|
|
1823
|
+
public void onAnimationStart(Animation animation) {
|
|
1824
|
+
Log.d(
|
|
1825
|
+
TAG,
|
|
1826
|
+
"showFocusIndicator: Final animation started"
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
@Override
|
|
1831
|
+
public void onAnimationEnd(Animation animation) {
|
|
1832
|
+
Log.d(
|
|
1833
|
+
TAG,
|
|
1834
|
+
"showFocusIndicator: Final animation ended, removing indicator"
|
|
1835
|
+
);
|
|
1836
|
+
// Remove the focus indicator
|
|
1837
|
+
if (
|
|
1838
|
+
focusIndicatorView != null &&
|
|
1839
|
+
previewContainer != null
|
|
1840
|
+
) {
|
|
1841
|
+
previewContainer.removeView(focusIndicatorView);
|
|
1842
|
+
focusIndicatorView = null;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
@Override
|
|
1847
|
+
public void onAnimationRepeat(Animation animation) {}
|
|
1848
|
+
}
|
|
1849
|
+
);
|
|
1850
|
+
|
|
1851
|
+
focusIndicatorView.startAnimation(finalAnimation);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
@Override
|
|
1856
|
+
public void onAnimationRepeat(Animation animation) {}
|
|
1857
|
+
}
|
|
1858
|
+
);
|
|
1859
|
+
|
|
1860
|
+
focusIndicatorView.startAnimation(fadeToTransparent);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
},
|
|
1864
|
+
800
|
|
1865
|
+
); // Optimal timing for smooth focus feedback
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1341
1868
|
public static List<Size> getSupportedPictureSizes(String facing) {
|
|
1342
1869
|
List<Size> sizes = new ArrayList<>();
|
|
1343
1870
|
try {
|
|
@@ -1800,54 +2327,138 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1800
2327
|
|
|
1801
2328
|
public int getPreviewX() {
|
|
1802
2329
|
if (previewContainer == null) return 0;
|
|
2330
|
+
|
|
2331
|
+
// Get the container position
|
|
1803
2332
|
ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
|
|
1804
2333
|
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
2334
|
+
int containerX = ((ViewGroup.MarginLayoutParams) layoutParams).leftMargin;
|
|
2335
|
+
|
|
2336
|
+
// Get the actual camera bounds within the container
|
|
2337
|
+
Rect cameraBounds = getActualCameraBounds();
|
|
2338
|
+
int actualX = containerX + cameraBounds.left;
|
|
2339
|
+
|
|
1809
2340
|
Log.d(
|
|
1810
2341
|
TAG,
|
|
1811
|
-
"getPreviewX:
|
|
1812
|
-
|
|
1813
|
-
",
|
|
1814
|
-
|
|
1815
|
-
",
|
|
1816
|
-
|
|
2342
|
+
"getPreviewX: containerX=" +
|
|
2343
|
+
containerX +
|
|
2344
|
+
", cameraBounds.left=" +
|
|
2345
|
+
cameraBounds.left +
|
|
2346
|
+
", actualX=" +
|
|
2347
|
+
actualX
|
|
1817
2348
|
);
|
|
1818
|
-
|
|
2349
|
+
|
|
2350
|
+
return actualX;
|
|
1819
2351
|
}
|
|
1820
2352
|
return previewContainer.getLeft();
|
|
1821
2353
|
}
|
|
1822
2354
|
|
|
1823
2355
|
public int getPreviewY() {
|
|
1824
2356
|
if (previewContainer == null) return 0;
|
|
2357
|
+
|
|
2358
|
+
// Get the container position
|
|
1825
2359
|
ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
|
|
1826
2360
|
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2361
|
+
int containerY = ((ViewGroup.MarginLayoutParams) layoutParams).topMargin;
|
|
2362
|
+
|
|
2363
|
+
// Get the actual camera bounds within the container
|
|
2364
|
+
Rect cameraBounds = getActualCameraBounds();
|
|
2365
|
+
int actualY = containerY + cameraBounds.top;
|
|
2366
|
+
|
|
1831
2367
|
Log.d(
|
|
1832
2368
|
TAG,
|
|
1833
|
-
"getPreviewY:
|
|
1834
|
-
|
|
1835
|
-
",
|
|
1836
|
-
|
|
1837
|
-
",
|
|
1838
|
-
|
|
2369
|
+
"getPreviewY: containerY=" +
|
|
2370
|
+
containerY +
|
|
2371
|
+
", cameraBounds.top=" +
|
|
2372
|
+
cameraBounds.top +
|
|
2373
|
+
", actualY=" +
|
|
2374
|
+
actualY
|
|
1839
2375
|
);
|
|
1840
|
-
|
|
2376
|
+
|
|
2377
|
+
return actualY;
|
|
1841
2378
|
}
|
|
1842
2379
|
return previewContainer.getTop();
|
|
1843
2380
|
}
|
|
1844
2381
|
|
|
2382
|
+
// Get the actual camera content bounds within the PreviewView
|
|
2383
|
+
private Rect getActualCameraBounds() {
|
|
2384
|
+
if (previewView == null || previewContainer == null) {
|
|
2385
|
+
return new Rect(0, 0, 0, 0);
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
// Get the container bounds
|
|
2389
|
+
int containerWidth = previewContainer.getWidth();
|
|
2390
|
+
int containerHeight = previewContainer.getHeight();
|
|
2391
|
+
|
|
2392
|
+
// Get the preview transformation info to understand how the camera is scaled/positioned
|
|
2393
|
+
// For FIT_CENTER, the camera content is scaled to fit within the container
|
|
2394
|
+
// This might create letterboxing (black bars) on top/bottom or left/right
|
|
2395
|
+
|
|
2396
|
+
// Get the actual preview resolution
|
|
2397
|
+
if (currentPreviewResolution == null) {
|
|
2398
|
+
// If we don't have the resolution yet, assume the container is filled
|
|
2399
|
+
return new Rect(0, 0, containerWidth, containerHeight);
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
// The preview is rotated 90 degrees for portrait mode
|
|
2403
|
+
// So we swap the dimensions
|
|
2404
|
+
int cameraWidth = currentPreviewResolution.getHeight();
|
|
2405
|
+
int cameraHeight = currentPreviewResolution.getWidth();
|
|
2406
|
+
|
|
2407
|
+
// Calculate the scaling factor to fit the camera in the container
|
|
2408
|
+
float widthScale = (float) containerWidth / cameraWidth;
|
|
2409
|
+
float heightScale = (float) containerHeight / cameraHeight;
|
|
2410
|
+
float scale = Math.min(widthScale, heightScale); // FIT_CENTER uses min scale
|
|
2411
|
+
|
|
2412
|
+
// Calculate the actual size of the camera content after scaling
|
|
2413
|
+
int scaledWidth = Math.round(cameraWidth * scale);
|
|
2414
|
+
int scaledHeight = Math.round(cameraHeight * scale);
|
|
2415
|
+
|
|
2416
|
+
// Calculate the offset to center the content
|
|
2417
|
+
int offsetX = (containerWidth - scaledWidth) / 2;
|
|
2418
|
+
int offsetY = (containerHeight - scaledHeight) / 2;
|
|
2419
|
+
|
|
2420
|
+
Log.d(
|
|
2421
|
+
TAG,
|
|
2422
|
+
"getActualCameraBounds: container=" +
|
|
2423
|
+
containerWidth +
|
|
2424
|
+
"x" +
|
|
2425
|
+
containerHeight +
|
|
2426
|
+
", camera=" +
|
|
2427
|
+
cameraWidth +
|
|
2428
|
+
"x" +
|
|
2429
|
+
cameraHeight +
|
|
2430
|
+
", scale=" +
|
|
2431
|
+
scale +
|
|
2432
|
+
", scaled=" +
|
|
2433
|
+
scaledWidth +
|
|
2434
|
+
"x" +
|
|
2435
|
+
scaledHeight +
|
|
2436
|
+
", offset=(" +
|
|
2437
|
+
offsetX +
|
|
2438
|
+
"," +
|
|
2439
|
+
offsetY +
|
|
2440
|
+
")"
|
|
2441
|
+
);
|
|
2442
|
+
|
|
2443
|
+
// Return the bounds relative to the container
|
|
2444
|
+
return new Rect(
|
|
2445
|
+
offsetX,
|
|
2446
|
+
offsetY,
|
|
2447
|
+
offsetX + scaledWidth,
|
|
2448
|
+
offsetY + scaledHeight
|
|
2449
|
+
);
|
|
2450
|
+
}
|
|
2451
|
+
|
|
1845
2452
|
public int getPreviewWidth() {
|
|
1846
|
-
|
|
2453
|
+
if (previewContainer == null) return 0;
|
|
2454
|
+
Rect bounds = getActualCameraBounds();
|
|
2455
|
+
return bounds.width();
|
|
1847
2456
|
}
|
|
1848
2457
|
|
|
1849
2458
|
public int getPreviewHeight() {
|
|
1850
|
-
|
|
2459
|
+
if (previewContainer == null) return 0;
|
|
2460
|
+
Rect bounds = getActualCameraBounds();
|
|
2461
|
+
return bounds.height();
|
|
1851
2462
|
}
|
|
1852
2463
|
|
|
1853
2464
|
public void setPreviewSize(int x, int y, int width, int height) {
|
|
@@ -2167,22 +2778,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2167
2778
|
}
|
|
2168
2779
|
}
|
|
2169
2780
|
|
|
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
2781
|
private int getWebViewTopInset() {
|
|
2180
2782
|
try {
|
|
2181
2783
|
if (webView != null) {
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2784
|
+
// Get the actual WebView position on screen
|
|
2785
|
+
int[] location = new int[2];
|
|
2786
|
+
webView.getLocationOnScreen(location);
|
|
2787
|
+
return location[1]; // Y position is the top inset
|
|
2186
2788
|
}
|
|
2187
2789
|
} catch (Exception e) {
|
|
2188
2790
|
Log.w(TAG, "Failed to get WebView top inset", e);
|
|
@@ -2193,10 +2795,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2193
2795
|
private int getWebViewLeftInset() {
|
|
2194
2796
|
try {
|
|
2195
2797
|
if (webView != null) {
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2798
|
+
// Get the actual WebView position on screen for consistency
|
|
2799
|
+
int[] location = new int[2];
|
|
2800
|
+
webView.getLocationOnScreen(location);
|
|
2801
|
+
return location[0]; // X position is the left inset
|
|
2200
2802
|
}
|
|
2201
2803
|
} catch (Exception e) {
|
|
2202
2804
|
Log.w(TAG, "Failed to get WebView left inset", e);
|
|
@@ -2212,32 +2814,112 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2212
2814
|
return new int[] { 0, 0, 0, 0 }; // x, y, width, height
|
|
2213
2815
|
}
|
|
2214
2816
|
|
|
2215
|
-
|
|
2216
|
-
int
|
|
2817
|
+
// Get actual camera preview bounds (accounts for letterboxing/pillarboxing)
|
|
2818
|
+
int actualX = getPreviewX();
|
|
2819
|
+
int actualY = getPreviewY();
|
|
2820
|
+
int actualWidth = getPreviewWidth();
|
|
2821
|
+
int actualHeight = getPreviewHeight();
|
|
2217
2822
|
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2823
|
+
// Convert to logical pixels for JavaScript
|
|
2824
|
+
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
|
2825
|
+
float pixelRatio = metrics.density;
|
|
2221
2826
|
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2827
|
+
// Remove WebView insets from coordinates
|
|
2828
|
+
int webViewTopInset = getWebViewTopInset();
|
|
2829
|
+
int webViewLeftInset = getWebViewLeftInset();
|
|
2225
2830
|
|
|
2226
|
-
|
|
2227
|
-
|
|
2831
|
+
int x = Math.max(
|
|
2832
|
+
0,
|
|
2833
|
+
(int) ((actualX - webViewLeftInset) / pixelRatio)
|
|
2834
|
+
);
|
|
2835
|
+
int y = Math.max(
|
|
2836
|
+
0,
|
|
2837
|
+
(int) ((actualY - webViewTopInset) / pixelRatio)
|
|
2838
|
+
);
|
|
2839
|
+
int width = (int) (actualWidth / pixelRatio);
|
|
2840
|
+
int height = (int) (actualHeight / pixelRatio);
|
|
2228
2841
|
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
(int) ((params.topMargin - webViewTopInset) / pixelRatio)
|
|
2236
|
-
);
|
|
2237
|
-
width = (int) (params.width / pixelRatio);
|
|
2238
|
-
height = (int) (params.height / pixelRatio);
|
|
2842
|
+
return new int[] { x, y, width, height };
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
private void triggerAutoFocus() {
|
|
2846
|
+
if (camera == null) {
|
|
2847
|
+
return;
|
|
2239
2848
|
}
|
|
2240
2849
|
|
|
2241
|
-
|
|
2850
|
+
Log.d(TAG, "triggerAutoFocus: Triggering autofocus at center");
|
|
2851
|
+
|
|
2852
|
+
// Cancel any ongoing focus operation
|
|
2853
|
+
if (currentFocusFuture != null && !currentFocusFuture.isDone()) {
|
|
2854
|
+
Log.d(TAG, "triggerAutoFocus: Cancelling previous focus operation");
|
|
2855
|
+
currentFocusFuture.cancel(true);
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
// Focus on the center of the view
|
|
2859
|
+
int viewWidth = previewView.getWidth();
|
|
2860
|
+
int viewHeight = previewView.getHeight();
|
|
2861
|
+
|
|
2862
|
+
if (viewWidth == 0 || viewHeight == 0) {
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
// Create MeteringPoint at the center of the preview
|
|
2867
|
+
MeteringPointFactory factory = previewView.getMeteringPointFactory();
|
|
2868
|
+
MeteringPoint point = factory.createPoint(viewWidth / 2f, viewHeight / 2f);
|
|
2869
|
+
|
|
2870
|
+
// Create focus and metering action
|
|
2871
|
+
FocusMeteringAction action = new FocusMeteringAction.Builder(
|
|
2872
|
+
point,
|
|
2873
|
+
FocusMeteringAction.FLAG_AF | FocusMeteringAction.FLAG_AE
|
|
2874
|
+
)
|
|
2875
|
+
.setAutoCancelDuration(3, TimeUnit.SECONDS) // Auto-cancel after 3 seconds
|
|
2876
|
+
.build();
|
|
2877
|
+
|
|
2878
|
+
try {
|
|
2879
|
+
currentFocusFuture = camera
|
|
2880
|
+
.getCameraControl()
|
|
2881
|
+
.startFocusAndMetering(action);
|
|
2882
|
+
currentFocusFuture.addListener(
|
|
2883
|
+
() -> {
|
|
2884
|
+
try {
|
|
2885
|
+
FocusMeteringResult result = currentFocusFuture.get();
|
|
2886
|
+
Log.d(
|
|
2887
|
+
TAG,
|
|
2888
|
+
"triggerAutoFocus: Focus completed successfully: " +
|
|
2889
|
+
result.isFocusSuccessful()
|
|
2890
|
+
);
|
|
2891
|
+
} catch (Exception e) {
|
|
2892
|
+
// Handle cancellation gracefully - this is expected when rapid operations occur
|
|
2893
|
+
if (
|
|
2894
|
+
e.getMessage() != null &&
|
|
2895
|
+
(e
|
|
2896
|
+
.getMessage()
|
|
2897
|
+
.contains("Cancelled by another startFocusAndMetering") ||
|
|
2898
|
+
e.getMessage().contains("OperationCanceledException") ||
|
|
2899
|
+
e
|
|
2900
|
+
.getClass()
|
|
2901
|
+
.getSimpleName()
|
|
2902
|
+
.contains("OperationCanceledException"))
|
|
2903
|
+
) {
|
|
2904
|
+
Log.d(
|
|
2905
|
+
TAG,
|
|
2906
|
+
"triggerAutoFocus: Auto-focus was cancelled by a newer focus request"
|
|
2907
|
+
);
|
|
2908
|
+
} else {
|
|
2909
|
+
Log.e(TAG, "triggerAutoFocus: Error during focus", e);
|
|
2910
|
+
}
|
|
2911
|
+
} finally {
|
|
2912
|
+
// Clear the reference if this is still the current operation
|
|
2913
|
+
if (currentFocusFuture != null && currentFocusFuture.isDone()) {
|
|
2914
|
+
currentFocusFuture = null;
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
},
|
|
2918
|
+
ContextCompat.getMainExecutor(context)
|
|
2919
|
+
);
|
|
2920
|
+
} catch (Exception e) {
|
|
2921
|
+
currentFocusFuture = null;
|
|
2922
|
+
Log.e(TAG, "triggerAutoFocus: Failed to trigger autofocus", e);
|
|
2923
|
+
}
|
|
2242
2924
|
}
|
|
2243
2925
|
}
|