diff --git a/android/src/main/java/com/rncamerakit/CKCamera.kt b/android/src/main/java/com/rncamerakit/CKCamera.kt index 50a2e12..0bcb48f 100644 --- a/android/src/main/java/com/rncamerakit/CKCamera.kt +++ b/android/src/main/java/com/rncamerakit/CKCamera.kt @@ -9,11 +9,11 @@ import android.content.ContentValues import android.content.Context import android.content.pm.PackageManager import android.graphics.Color -import android.graphics.Rect import android.hardware.SensorManager import android.media.AudioManager import android.media.MediaActionSound import android.provider.MediaStore +import android.util.DisplayMetrics import android.util.Log import android.view.* import android.widget.FrameLayout @@ -25,19 +25,20 @@ import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat -import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.OnLifecycleEvent import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Promise import com.facebook.react.bridge.WritableMap -import com.facebook.react.common.ReactConstants.TAG import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.events.RCTEventEmitter import com.rncamerakit.barcode.BarcodeFrame import java.io.File +import java.util.* import java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min @SuppressLint("ViewConstructor") // Extra constructors unused. Not using visual layout tools class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObserver { @@ -46,8 +47,7 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs private var camera: Camera? = null private var preview: Preview? = null private var imageCapture: ImageCapture? = null - private var qrCodeAnalyzer: ImageAnalysis? = null - private var cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + private var imageAnalyzer: ImageAnalysis? = null private var orientationListener: OrientationEventListener? = null private var viewFinder: PreviewView = PreviewView(context) private var barcodeFrame: BarcodeFrame? = null @@ -56,7 +56,6 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs private var outputPath: String? = null private var shutterAnimationDuration: Int = 50 private var effectLayer = View(context) - private var counter = 0 // Camera Props private var lensType = CameraSelector.LENS_FACING_BACK @@ -65,8 +64,6 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs // Barcode Props private var scanBarcode: Boolean = false - private var showFrame = false - private var frameRect: Rect? = null private var frameColor = Color.GREEN private var laserColor = Color.RED @@ -85,38 +82,18 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs effectLayer.alpha = 0F effectLayer.setBackgroundColor(Color.BLACK) addView(effectLayer) + } + override fun onAttachedToWindow() { + super.onAttachedToWindow() if (hasPermissions()) { - viewFinder.post { startCamera() } + viewFinder.post { setupCamera() } } - - (getActivity() as AppCompatActivity).lifecycle.addObserver(this) - } - - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) - fun onResume() { - Log.d(TAG, "onResume") - viewFinder.post { startCamera() } - } - - @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) - fun onPause() { - Log.d(TAG, "onPause") - stopCamera() - } - - @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) - fun onDestroy() { - Log.d(TAG, "onDestroy") - stopCamera() } override fun onDetachedFromWindow() { super.onDetachedFromWindow() - stopCamera() - } - private fun stopCamera() { cameraExecutor.shutdown() orientationListener?.disable() cameraProvider?.unbindAll() @@ -141,33 +118,13 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs } } + /** Initialize CameraX, and prepare to bind the camera use cases */ @SuppressLint("ClickableViewAccessibility") - private fun startCamera() { + private fun setupCamera() { val cameraProviderFuture = ProcessCameraProvider.getInstance(getActivity()) - - val onScaleGestureListener = object: ScaleGestureDetector.SimpleOnScaleGestureListener() { - override fun onScale(detector: ScaleGestureDetector?): Boolean { - if (zoomMode == "off") return true - val cameraControl = camera?.cameraControl ?: return true - val zoom = camera?.cameraInfo?.zoomState?.value?.zoomRatio ?: return true - val scaleFactor = detector?.scaleFactor ?: return true - val scale = zoom * scaleFactor - - cameraControl.setZoomRatio(scale) - return true - } - } - cameraProviderFuture.addListener(Runnable { // Used to bind the lifecycle of cameras to the lifecycle owner cameraProvider = cameraProviderFuture.get() - val scaleDetector = ScaleGestureDetector(context, onScaleGestureListener) - - cameraSelector = CameraSelector.Builder().requireLensFacing(lensType).build() - preview = Preview.Builder().build().also { - it.setSurfaceProvider(viewFinder.surfaceProvider) - } - imageCapture = ImageCapture.Builder().build() // Rotate the image according to device orientation, even when UI orientation is locked orientationListener = object : OrientationEventListener(context, SensorManager.SENSOR_DELAY_UI) { @@ -191,6 +148,18 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs } orientationListener!!.enable() + val scaleDetector = ScaleGestureDetector(context, object: ScaleGestureDetector.SimpleOnScaleGestureListener() { + override fun onScale(detector: ScaleGestureDetector?): Boolean { + if (zoomMode == "off") return true + val cameraControl = camera?.cameraControl ?: return true + val zoom = camera?.cameraInfo?.zoomState?.value?.zoomRatio ?: return true + val scaleFactor = detector?.scaleFactor ?: return true + val scale = zoom * scaleFactor + cameraControl.setZoomRatio(scale) + return true + } + }) + // Tap to focus viewFinder.setOnTouchListener { _, event -> if (event.action != MotionEvent.ACTION_UP) { @@ -200,40 +169,98 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs return@setOnTouchListener true } - qrCodeAnalyzer = ImageAnalysis.Builder() - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) - .build() - - val useCases = mutableListOf(preview, imageCapture) - - if (scanBarcode) { - val analyzer = QRCodeAnalyzer { barcodes -> - if (barcodes.isNotEmpty()) { - onBarcodeRead(barcodes) - } - } - qrCodeAnalyzer!!.setAnalyzer(cameraExecutor, analyzer) - useCases.add(qrCodeAnalyzer!!) - } - - try { - // Unbind use cases before rebinding - cameraProvider!!.unbindAll() - - // Bind use cases to camera - camera = cameraProvider!!.bindToLifecycle( - getActivity() as AppCompatActivity, - cameraSelector, - *useCases.toTypedArray() - ) - preview!!.setSurfaceProvider(viewFinder.surfaceProvider) - Log.d(TAG, "CameraView: Use cases bound") - } catch (exc: Exception) { - Log.e(TAG, "CameraView: Use cases binding failed", exc) - } + bindCameraUseCases() }, ContextCompat.getMainExecutor(getActivity())) } + private fun bindCameraUseCases() { + if (viewFinder.display == null) return + // Get screen metrics used to setup camera for full screen resolution + val metrics = DisplayMetrics().also { viewFinder.display.getRealMetrics(it) } + Log.d(TAG, "Screen metrics: ${metrics.widthPixels} x ${metrics.heightPixels}") + + val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels) + Log.d(TAG, "Preview aspect ratio: $screenAspectRatio") + + val rotation = viewFinder.display.rotation + + // CameraProvider + val cameraProvider = cameraProvider + ?: throw IllegalStateException("Camera initialization failed.") + + // CameraSelector + val cameraSelector = CameraSelector.Builder().requireLensFacing(lensType).build() + + // Preview + preview = Preview.Builder() + // We request aspect ratio but no resolution + .setTargetAspectRatio(screenAspectRatio) + // Set initial target rotation + .setTargetRotation(rotation) + .build() + + // ImageCapture + imageCapture = ImageCapture.Builder() + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + // We request aspect ratio but no resolution to match preview config, but letting + // CameraX optimize for whatever specific resolution best fits our use cases + .setTargetAspectRatio(screenAspectRatio) + // Set initial target rotation, we will have to call this again if rotation changes + // during the lifecycle of this use case + .setTargetRotation(rotation) + .build() + + // ImageAnalysis + imageAnalyzer = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + + val useCases = mutableListOf(preview, imageCapture) + + if (scanBarcode) { + val analyzer = QRCodeAnalyzer { barcodes -> + if (barcodes.isNotEmpty()) { + onBarcodeRead(barcodes) + } + } + imageAnalyzer!!.setAnalyzer(cameraExecutor, analyzer) + useCases.add(imageAnalyzer) + } + + // Must unbind the use-cases before rebinding them + cameraProvider.unbindAll() + + try { + // A variable number of use-cases can be passed here - + // camera provides access to CameraControl & CameraInfo + camera = cameraProvider.bindToLifecycle(getActivity() as AppCompatActivity, cameraSelector, *useCases.toTypedArray()) + + // Attach the viewfinder's surface provider to preview use case + preview?.setSurfaceProvider(viewFinder.surfaceProvider) + } catch (exc: Exception) { + Log.e(TAG, "Use case binding failed", exc) + } + } + + /** + * [androidx.camera.core.ImageAnalysisConfig] requires enum value of + * [androidx.camera.core.AspectRatio]. Currently it has values of 4:3 & 16:9. + * + * Detecting the most suitable ratio for dimensions provided in @params by counting absolute + * of preview ratio to one of the provided values. + * + * @param width - preview width + * @param height - preview height + * @return suitable aspect ratio + */ + private fun aspectRatio(width: Int, height: Int): Int { + val previewRatio = max(width, height).toDouble() / min(width, height) + if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) { + return AspectRatio.RATIO_4_3 + } + return AspectRatio.RATIO_16_9 + } + private fun flashViewFinder() { if (shutterAnimationDuration == 0) return @@ -299,7 +326,6 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs imageInfo.putString("uri", savedUri) imageInfo.putString("id", output.savedUri?.path) imageInfo.putString("name", output.savedUri?.lastPathSegment) - // imageInfo.putInt("size", null) imageInfo.putInt("width", width) imageInfo.putInt("height", height) imageInfo.putString("path", output.savedUri?.path) @@ -419,7 +445,7 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs fun setScanBarcode(enabled: Boolean) { val restartCamera = enabled != scanBarcode scanBarcode = enabled - if (restartCamera) startCamera() + if (restartCamera) bindCameraUseCases() } fun setCameraType(type: String = "back") { @@ -429,7 +455,7 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs } val restartCamera = lensType != newLensType lensType = newLensType - if (restartCamera) startCamera() + if (restartCamera) bindCameraUseCases() } fun setOutputPath(path: String) { @@ -485,4 +511,11 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs ) return false } + + companion object { + + private const val TAG = "CameraKit" + private const val RATIO_4_3_VALUE = 4.0 / 3.0 + private const val RATIO_16_9_VALUE = 16.0 / 9.0 + } } \ No newline at end of file