Last active
July 4, 2022 06:32
-
-
Save Rickyip/83108b2006023a2fd3730d350feb477f to your computer and use it in GitHub Desktop.
CameraX on Android Fragment in Kotlin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.Manifest | |
import android.annotation.SuppressLint | |
import android.content.Context | |
import android.content.pm.PackageManager | |
import android.graphics.drawable.GradientDrawable | |
import android.net.Uri | |
import android.os.Bundle | |
import android.util.Log | |
import android.view.LayoutInflater | |
import android.view.View | |
import android.view.ViewGroup | |
import android.widget.Toast | |
import androidx.camera.core.* | |
import androidx.camera.lifecycle.ProcessCameraProvider | |
import androidx.core.app.ActivityCompat | |
import androidx.core.content.ContextCompat | |
import androidx.fragment.app.Fragment | |
import kotlinx.android.synthetic.main.fragment_camera_x.* | |
import org.opencv.android.OpenCVLoader | |
import org.opencv.android.Utils | |
import org.opencv.core.Mat | |
import java.io.File | |
import java.nio.ByteBuffer | |
import java.text.SimpleDateFormat | |
import java.util.* | |
import java.util.concurrent.ExecutorService | |
import java.util.concurrent.Executors | |
import kotlin.math.abs | |
typealias CornersListener = () -> Unit | |
class CameraXFragment : Fragment() { | |
private var preview: Preview? = null | |
private var imageCapture: ImageCapture? = null | |
private var imageAnalyzer: ImageAnalysis? = null | |
private var camera: Camera? = null | |
private lateinit var safeContext: Context | |
private lateinit var outputDirectory: File | |
private lateinit var cameraExecutor: ExecutorService | |
override fun onAttach(context: Context) { | |
super.onAttach(context) | |
safeContext = context | |
} | |
private fun getStatusBarHeight(): Int { | |
val resourceId = safeContext.resources.getIdentifier("status_bar_height", "dimen", "android") | |
return if (resourceId > 0) { | |
safeContext.resources.getDimensionPixelSize(resourceId) | |
} else 0 | |
} | |
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { | |
// Inflate the layout for this fragment | |
return inflater.inflate(R.layout.fragment_camera_x, container, false) | |
} | |
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | |
super.onViewCreated(view, savedInstanceState) | |
// Request camera permissions | |
if (allPermissionsGranted()) { | |
startCamera() | |
} else { | |
ActivityCompat.requestPermissions(activity!!, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS) | |
} | |
// Setup the listener for take photo button | |
camera_capture_button.setOnClickListener { takePhoto() } | |
outputDirectory = getOutputDirectory() | |
cameraExecutor = Executors.newSingleThreadExecutor() | |
// cameraExecutor = Executors.newCachedThreadPool() | |
} | |
private fun startCamera() { | |
OpenCVLoader.initDebug() | |
val cameraProviderFuture = ProcessCameraProvider.getInstance(safeContext) | |
cameraProviderFuture.addListener(Runnable { | |
// Used to bind the lifecycle of cameras to the lifecycle owner | |
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() | |
// Preview | |
preview = Preview.Builder().build() | |
imageCapture = ImageCapture.Builder().build() | |
imageAnalyzer = ImageAnalysis.Builder().build().apply { | |
setAnalyzer(Executors.newSingleThreadExecutor(), CornerAnalyzer { | |
val bitmap = viewFinder.bitmap | |
val img = Mat() | |
Utils.bitmapToMat(bitmap, img) | |
bitmap?.recycle() | |
// Do image analysis here if you need bitmap | |
}) | |
} | |
// Select back camera | |
val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build() | |
try { | |
// Unbind use cases before rebinding | |
cameraProvider.unbindAll() | |
// Bind use cases to camera | |
camera = cameraProvider.bindToLifecycle(this, cameraSelector, imageAnalyzer, preview, imageCapture) | |
preview?.setSurfaceProvider(viewFinder.createSurfaceProvider()) | |
} catch (exc: Exception) { | |
Log.e(TAG, "Use case binding failed", exc) | |
} | |
}, ContextCompat.getMainExecutor(safeContext)) | |
} | |
private fun takePhoto() { | |
// Get a stable reference of the modifiable image capture use case | |
val imageCapture = imageCapture ?: return | |
// Create timestamped output file to hold the image | |
val photoFile = File(outputDirectory, SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis()) + ".jpg") | |
// Create output options object which contains file + metadata | |
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() | |
// Setup image capture listener which is triggered after photo has | |
// been taken | |
imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(safeContext), object : ImageCapture.OnImageSavedCallback { | |
override fun onError(exc: ImageCaptureException) { | |
Log.e(TAG, "Photo capture failed: ${exc.message}", exc) | |
} | |
override fun onImageSaved(output: ImageCapture.OutputFileResults) { | |
val savedUri = Uri.fromFile(photoFile) | |
val msg = "Photo capture succeeded: $savedUri" | |
Toast.makeText(safeContext, msg, Toast.LENGTH_SHORT).show() | |
Log.d(TAG, msg) | |
} | |
}) | |
} | |
override fun onPause() { | |
super.onPause() | |
isOffline = true | |
} | |
override fun onResume() { | |
super.onResume() | |
isOffline = false | |
} | |
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { | |
ContextCompat.checkSelfPermission(safeContext, it) == PackageManager.PERMISSION_GRANTED | |
} | |
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { | |
if (requestCode == REQUEST_CODE_PERMISSIONS) { | |
if (allPermissionsGranted()) { | |
startCamera() | |
} else { | |
Toast.makeText(safeContext, "Permissions not granted by the user.", Toast.LENGTH_SHORT).show() | |
// finish() | |
} | |
} | |
super.onRequestPermissionsResult(requestCode, permissions, grantResults) | |
} | |
fun getOutputDirectory(): File { | |
val mediaDir = activity?.externalMediaDirs?.firstOrNull()?.let { | |
File(it, resources.getString(R.string.app_name)).apply { mkdirs() } | |
} | |
return if (mediaDir != null && mediaDir.exists()) mediaDir else activity?.filesDir!! | |
} | |
companion object { | |
val TAG = "CameraXFragment" | |
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" | |
internal const val REQUEST_CODE_PERMISSIONS = 10 | |
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA) | |
var isOffline = false // prevent app crash when goes offline | |
} | |
private class CornerAnalyzer(private val listener: CornersListener) : ImageAnalysis.Analyzer { | |
private fun ByteBuffer.toByteArray(): ByteArray { | |
rewind() // Rewind the buffer to zero | |
val data = ByteArray(remaining()) | |
get(data) // Copy the buffer into a byte array | |
return data // Return the byte array | |
} | |
@SuppressLint("UnsafeExperimentalUsageError") | |
override fun analyze(imageProxy: ImageProxy) { | |
if (!isOffline) { | |
listener() | |
} | |
imageProxy.close() // important! if it is not closed it will only run once | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment