作者 liyan

新增自定义相机页面VerifyCameraActivity

... ... @@ -10,7 +10,7 @@ android {
defaultConfig {
applicationId "com.br_technology.securitytrain_master"
minSdkVersion 19
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
... ... @@ -56,6 +56,8 @@ dependencies {
// viewModel
api "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
api "androidx.lifecycle:lifecycle-extensions:2.2.0"
// App compat and UI things
api 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
//glide
api 'com.github.bumptech.glide:glide:4.11.0'
//retrofit
... ... @@ -90,4 +92,18 @@ dependencies {
//权限
implementation 'com.github.tbruyelle:rxpermissions:0.12'
// CameraX core library
def camerax_version = '1.1.0-alpha07'
implementation "androidx.camera:camera-core:$camerax_version"
// CameraX Camera2 extensions
implementation "androidx.camera:camera-camera2:$camerax_version"
// CameraX Lifecycle library
implementation "androidx.camera:camera-lifecycle:$camerax_version"
// CameraX View class
implementation 'androidx.camera:camera-view:1.0.0-alpha27'
}
\ No newline at end of file
... ...
... ... @@ -162,7 +162,7 @@
<activity android:name=".ui.mine.activity.SignInActivity" />
<activity android:name=".ui.mine.activity.CollectActivity" />
<activity android:name=".ui.mine.activity.ServiceActivity" />
<activity android:name=".ui.home.activity.VerifyCameraActivity"/>
</application>
... ...
... ... @@ -66,4 +66,8 @@ object Constant {
val ACCOUNT_DIR: String =
Environment.getExternalStorageDirectory().toString() + File.separator.toString() + "images/"
/** Milliseconds used for UI animations */
const val ANIMATION_FAST_MILLIS = 50L
const val ANIMATION_SLOW_MILLIS = 100L
}
\ No newline at end of file
... ...
package com.br_technology.securitytrain_master.ui.home.activity
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.util.Log
import android.view.LayoutInflater
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContentProviderCompat.requireContext
import androidx.core.content.ContextCompat
import androidx.core.net.toFile
import androidx.lifecycle.lifecycleScope
import com.br_technology.securitytrain_master.R
import com.br_technology.securitytrain_master.base.common.Constant.ANIMATION_FAST_MILLIS
import com.br_technology.securitytrain_master.base.common.Constant.ANIMATION_SLOW_MILLIS
import com.br_technology.securitytrain_master.databinding.ActivityVerifyCameraBinding
import com.br_technology.securitytrain_master.expand.screenHeight
import com.br_technology.securitytrain_master.expand.screenWidth
import com.br_technology.securitytrain_master.ui.home.viewmodel.VerifyCameraViewModel
import com.wjx.android.wanandroidmvvm.base.view.BaseLifeCycleActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
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
import kotlin.math.max
import kotlin.math.min
/**
* Time: 8/20/2021 14:27
* Author: Captain
* Description: 初见时你很迷人
*/
/** Helper type alias used for analysis use case callbacks */
typealias LumaListener = (luma: Double) -> Unit
class VerifyCameraActivity :
BaseLifeCycleActivity<VerifyCameraViewModel, ActivityVerifyCameraBinding>(
ActivityVerifyCameraBinding::inflate
) {
private lateinit var outputDirectory: File
private var cameraProvider: ProcessCameraProvider? = null
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
private var preview: Preview? = null
private var camera: Camera? = null
private var imageCapture: ImageCapture? = null
private var imageAnalyzer: ImageAnalysis? = null
/** Blocking camera operations are performed using this executor */
private lateinit var cameraExecutor: ExecutorService
override fun initDataObserver() {
}
override fun initData() {
super.initData()
cameraExecutor = Executors.newSingleThreadExecutor()
outputDirectory = getOutputDirectory(this)
updateCameraUi()
setUpCamera()
binding.apply {
ivBack.setOnClickListener {
finish()
}
}
}
private fun updateCameraUi() {
// In the background, load latest photo taken (if any) for gallery thumbnail
// lifecycleScope.launch(Dispatchers.IO) {
// outputDirectory.listFiles { file ->
// EXTENSION_WHITELIST.contains(file.extension.toUpperCase(Locale.ROOT))
// }?.maxOrNull()?.let {
// setGalleryThumbnail(Uri.fromFile(it))
// }
// }
// Listener for button used to capture photo
binding.ivTakePhoto.setOnClickListener {
Toast.makeText(baseContext, "aaaaa", Toast.LENGTH_SHORT).show()
// Get a stable reference of the modifiable image capture use case
imageCapture?.let { imageCapture ->
// Create output file to hold the image
val photoFile = createFile(outputDirectory, FILENAME, PHOTO_EXTENSION)
// Setup image capture metadata
val metadata = ImageCapture.Metadata().apply {
// Mirror image when using the front camera
isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
}
// Create output options object which contains file + metadata
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile)
.setMetadata(metadata)
.build()
// Setup image capture listener which is triggered after photo has been taken
imageCapture.takePicture(
outputOptions, cameraExecutor, 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 = output.savedUri ?: Uri.fromFile(photoFile)
Log.d(TAG, "Photo capture succeeded: $savedUri")
// We can only change the foreground Drawable using API level 23+ API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Update the gallery thumbnail with latest picture taken
// setGalleryThumbnail(savedUri)
//todo 拿到照片的uri上传服务器
}
// Implicit broadcasts will be ignored for devices running API level >= 24
// so if you only target API level 24+ you can remove this statement
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
sendBroadcast(
Intent(android.hardware.Camera.ACTION_NEW_PICTURE, savedUri)
)
}
// If the folder selected is an external media directory, this is
// unnecessary but otherwise other apps will not be able to access our
// images unless we scan them using [MediaScannerConnection]
val mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(savedUri.toFile().extension)
MediaScannerConnection.scanFile(
this@VerifyCameraActivity,
arrayOf(savedUri.toFile().absolutePath),
arrayOf(mimeType)
) { _, uri ->
Log.d(TAG, "Image capture scanned into media store: $uri")
}
}
})
// We can only change the foreground Drawable using API level 23+ API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Display flash animation to indicate that photo was captured
binding.root.postDelayed({
binding.root.foreground = ColorDrawable(Color.WHITE)
binding.root.postDelayed(
{ binding.root.foreground = null }, ANIMATION_FAST_MILLIS)
}, ANIMATION_SLOW_MILLIS)
}
}
}
// Setup for button used to switch cameras
binding.ivSwitchScreen.let {
// Disable the button until the camera is set up
it.isEnabled = false
// Listener for button used to switch cameras. Only called if the button is enabled
it.setOnClickListener {
lensFacing = if (CameraSelector.LENS_FACING_FRONT == lensFacing) {
CameraSelector.LENS_FACING_BACK
} else {
CameraSelector.LENS_FACING_FRONT
}
// Re-bind use cases to update selected camera
bindCameraUseCases()
}
}
// Listener for button used to view the most recent photo
// cameraUiContainerBinding?.photoViewButton?.setOnClickListener {
// // Only navigate when the gallery has photos
// if (true == outputDirectory.listFiles()?.isNotEmpty()) {
// Navigation.findNavController(
// requireActivity(), R.id.fragment_container
// ).navigate(CameraFragmentDirections
// .actionCameraToGallery(outputDirectory.absolutePath))
// }
// }
}
fun getOutputDirectory(context: Context): File {
val appContext = context.applicationContext
val mediaDir = context.externalMediaDirs.firstOrNull()?.let {
File(it, appContext.resources.getString(R.string.app_name)).apply { mkdirs() } }
return if (mediaDir != null && mediaDir.exists())
mediaDir else appContext.filesDir
}
/** Initialize CameraX, and prepare to bind the camera use cases */
private fun setUpCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener(Runnable {
// CameraProvider
cameraProvider = cameraProviderFuture.get()
// Select lensFacing depending on the available cameras
lensFacing = when {
hasBackCamera() -> CameraSelector.LENS_FACING_BACK
hasFrontCamera() -> CameraSelector.LENS_FACING_FRONT
else -> throw IllegalStateException("Back and front camera are unavailable")
}
// Enable or disable switching between cameras
updateCameraSwitchButton()
// Build and bind the camera use cases
bindCameraUseCases()
}, ContextCompat.getMainExecutor(this))
}
/** Enabled or disabled a button to switch cameras depending on the available cameras */
private fun updateCameraSwitchButton() {
try {
binding.ivSwitchScreen.isEnabled = hasBackCamera() && hasFrontCamera()
} catch (exception: CameraInfoUnavailableException) {
binding.ivSwitchScreen.isEnabled = false
}
}
/** Returns true if the device has an available back camera. False otherwise */
private fun hasBackCamera(): Boolean {
return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
}
/** Returns true if the device has an available front camera. False otherwise */
private fun hasFrontCamera(): Boolean {
return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
}
private fun bindCameraUseCases() {
val screenAspectRatio = aspectRatio(binding.root.screenWidth(), binding.root.screenHeight())
Log.d(TAG, "Preview aspect ratio: $screenAspectRatio")
// val rotation = binding.viewFinder.display.rotation
// CameraProvider
val cameraProvider = cameraProvider
?: throw IllegalStateException("Camera initialization failed.")
// CameraSelector
val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).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()
// We request aspect ratio but no resolution
.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()
// The analyzer can then be assigned to the instance
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
// Values returned from our analyzer are passed to the attached listener
// We log image analysis results here - you should do something useful
// instead!
Log.d(TAG, "Average luminosity: $luma")
})
}
// 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(
this, cameraSelector, preview, imageCapture, imageAnalyzer)
// Attach the viewfinder's surface provider to preview use case
preview?.setSurfaceProvider(binding.viewFinder.surfaceProvider)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}
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 class LuminosityAnalyzer(listener: LumaListener? = null) : ImageAnalysis.Analyzer {
private val frameRateWindow = 8
private val frameTimestamps = ArrayDeque<Long>(5)
private val listeners = ArrayList<LumaListener>().apply { listener?.let { add(it) } }
private var lastAnalyzedTimestamp = 0L
var framesPerSecond: Double = -1.0
private set
/**
* Used to add listeners that will be called with each luma computed
*/
fun onFrameAnalyzed(listener: LumaListener) = listeners.add(listener)
/**
* Helper extension function used to extract a byte array from an image plane buffer
*/
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
}
/**
* Analyzes an image to produce a result.
*
* <p>The caller is responsible for ensuring this analysis method can be executed quickly
* enough to prevent stalls in the image acquisition pipeline. Otherwise, newly available
* images will not be acquired and analyzed.
*
* <p>The image passed to this method becomes invalid after this method returns. The caller
* should not store external references to this image, as these references will become
* invalid.
*
* @param image image being analyzed VERY IMPORTANT: Analyzer method implementation must
* call image.close() on received images when finished using them. Otherwise, new images
* may not be received or the camera may stall, depending on back pressure setting.
*
*/
override fun analyze(image: ImageProxy) {
// If there are no listeners attached, we don't need to perform analysis
if (listeners.isEmpty()) {
image.close()
return
}
// Keep track of frames analyzed
val currentTime = System.currentTimeMillis()
frameTimestamps.push(currentTime)
// Compute the FPS using a moving average
while (frameTimestamps.size >= frameRateWindow) frameTimestamps.removeLast()
val timestampFirst = frameTimestamps.peekFirst() ?: currentTime
val timestampLast = frameTimestamps.peekLast() ?: currentTime
framesPerSecond = 1.0 / ((timestampFirst - timestampLast) /
frameTimestamps.size.coerceAtLeast(1).toDouble()) * 1000.0
// Analysis could take an arbitrarily long amount of time
// Since we are running in a different thread, it won't stall other use cases
lastAnalyzedTimestamp = frameTimestamps.first
// Since format in ImageAnalysis is YUV, image.planes[0] contains the luminance plane
val buffer = image.planes[0].buffer
// Extract image data from callback object
val data = buffer.toByteArray()
// Convert the data into an array of pixel values ranging 0-255
val pixels = data.map { it.toInt() and 0xFF }
// Compute average luminance for the image
val luma = pixels.average()
// Call all listeners with new value
listeners.forEach { it(luma) }
image.close()
}
}
companion object {
private const val TAG = "CameraXBasic"
private const val FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val PHOTO_EXTENSION = ".jpg"
private const val RATIO_4_3_VALUE = 4.0 / 3.0
private const val RATIO_16_9_VALUE = 16.0 / 9.0
/** Helper function used to create a timestamped file */
private fun createFile(baseFolder: File, format: String, extension: String) =
File(baseFolder, SimpleDateFormat(format, Locale.US)
.format(System.currentTimeMillis()) + extension)
}
}
\ No newline at end of file
... ...
package com.br_technology.securitytrain_master.ui.home.repository
import androidx.lifecycle.MutableLiveData
import com.br_technology.securitytrain_master.base.common.State
import com.br_technology.securitytrain_master.base.repository.ApiRepository
/**
* Time: 8/20/2021 14:37
* Author: Captain
* Description: 初见时你很迷人
*/
class VerifyCameraRepository (val loadState: MutableLiveData<State>): ApiRepository(){
}
\ No newline at end of file
... ...
package com.br_technology.securitytrain_master.ui.home.viewmodel
import com.br_technology.securitytrain_master.base.view.BaseViewModel
import com.br_technology.securitytrain_master.ui.home.repository.VerifyCameraRepository
/**
* Time: 8/20/2021 14:43
* Author: Captain
* Description: 初见时你很迷人
*/
class VerifyCameraViewModel : BaseViewModel<VerifyCameraRepository>() {
}
\ No newline at end of file
... ...
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.camera.view.PreviewView
android:id="@+id/view_finder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<include
android:id="@+id/include"
layout="@layout/layout_tool_bar" />
<!-- <com.br_technology.securitytrain_master.view.ViewToolBar-->
<!-- android:id="@+id/tool_bar"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_below="@id/include"-->
<!-- android:background="@color/transparent" />-->
<ImageView
android:layout_below="@id/include"
android:id="@+id/iv_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:src="@mipmap/camera_back" />
<ImageView
android:layout_below="@id/include"
android:id="@+id/iv_switch_screen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:src="@mipmap/camera_switch_screen" />
<ImageView
android:id="@+id/iv_take_photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="84dp"
android:src="@mipmap/camera_start" />
</RelativeLayout>
\ No newline at end of file
... ...