Merge pull request #354 from aarongrider/android-lifecycle

Fix Android camera lifecycle
This commit is contained in:
Aaron Grider 2021-01-14 12:53:42 -08:00 committed by GitHub
commit feacdfb0bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -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
}
}