androidx.camera:camera-core:1.3.3
androidx.camera:camera-lifecycle:1.3.3
androidx.camera:camera-view:1.3.3
androidx.camera:camera-camera2:1.3.3
/**
* CameraX加载器
* @author DokiWei
* @sample cameraXSample
* @param mActivity 上下问,通过此对象以绑定相机,以及获取窗口的角度
* @param previewView CameraX提供的预览视图,可传空并使用[setOnPreviewFrameListener]获取元数据
*/
class CameraXLoader(
private val mActivity: FragmentActivity,
private val previewView: PreviewView?,
) : CameraLoader(), DefaultLifecycleObserver {
private val cameraProviderFuture = ProcessCameraProvider.getInstance(mActivity)
private val cameraExecutor = Executors.newSingleThreadExecutor()
private var cameraControl: CameraControl? = null
private val imageCapture by lazy {
ImageCapture.Builder().setTargetRotation(mActivity.windowManager.defaultDisplay.rotation)
.build()
}
/**
* 使用示例
*/
private fun cameraXSample(){
// 创建实例
val cameraXLoader = CameraXLoader(mActivity, previewView)
// 监听生命周期
mActivity.lifecycle.addObserver(cameraXLoader)
// 创建一个空位图来循环使用
// 在上下文销毁时调用bitmap.recycle()销毁bitmap
var bitmap :Bitmap?=null
cameraXLoader.setOnPreviewFrameListener{data,width,height->
if (bitmap != null) {
val buffer = ByteBuffer.wrap(data)
bitmap!!.copyPixelsFromBuffer(buffer)
// 在这里使用bitmap刷新你的视图
} else bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
}
}
/**
* 切换摄像头
*/
override fun switchCamera() {
isFrontCamera = !isFrontCamera
startCamera()
}
override fun flashEnable(enable: Boolean) {
cameraControl?.enableTorch(enable)
}
override var isFrontCamera: Boolean = false
private set
/**
* 拍照
* @param file 保存的位置
* @param callback 保存回调
*/
fun takePhoto(file: File, callback: ImageCapture.OnImageSavedCallback) {
file.parentFile?.takeIf { !it.exists() }?.mkdirs()
val outputFileOptions = ImageCapture.OutputFileOptions.Builder(file).build()
imageCapture.takePicture(outputFileOptions, cameraExecutor, callback)
MediaScannerConnection.scanFile(
BaseApplication.getApplication(),
arrayOf<String>(file.toString()),
null
) { path, uri ->
}
}
private fun startCamera() {
cameraProviderFuture.addListener({
runCatching {
val cameraProvider = cameraProviderFuture.get()
bindPreview(cameraProvider)
}.onFailure {
LoggerUtils.e(it, "初始化相机失败")
}
}, ContextCompat.getMainExecutor(mActivity))
}
@OptIn(ExperimentalGetImage::class)
private fun bindPreview(cameraProvider: ProcessCameraProvider) {
val preview = Preview.Builder().build()
val imageAnalysis = ImageAnalysis.Builder().setResolutionSelector(
ResolutionSelector.Builder().setResolutionStrategy(
ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY
).setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY).build()
).setBackgroundExecutor(cameraExecutor)
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).build()
imageAnalysis.setAnalyzer(
ContextCompat.getMainExecutor(mActivity)
) { imageProxy ->
if (mOnPreviewFrameListener != null) {
val data: ByteArray = ImageUtils.generateRGBAData(imageProxy)
mOnPreviewFrameListener?.onPreviewFrame(data, imageProxy.width, imageProxy.height)
}
imageProxy.close()
}
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(if (isFrontCamera) CameraSelector.LENS_FACING_FRONT else CameraSelector.LENS_FACING_BACK)
.build()
cameraProvider.unbindAll()
val camera: Camera = cameraProvider.bindToLifecycle(
mActivity, cameraSelector, preview, imageAnalysis, imageCapture
)
cameraControl = camera.cameraControl
previewView?.let { preview.setSurfaceProvider(it.surfaceProvider) }
}
private fun rotationDegrees(rotation: Int): Int {
return when (rotation) {
Surface.ROTATION_0 -> 0
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
else -> 0
}
}
//==============================================================================================
//======================================生命周期管理==============================================
//==============================================================================================
override fun onResume(owner: LifecycleOwner) {
startCamera()
}
override fun onPause(owner: LifecycleOwner) {
cameraProviderFuture.cancel(true)
}
override fun onDestroy(owner: LifecycleOwner) {
cameraControl = null
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.activity.CameraActivity">
<ImageView
android:id="@+id/iv_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/aa_icon_back"
android:padding="10dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"/>
<ImageView
android:id="@+id/iv_light"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/selector_shangguan"
app:layout_constraintTop_toTopOf="@+id/iv_back"
app:layout_constraintBottom_toBottomOf="@+id/iv_back"
app:layout_constraintEnd_toEndOf="parent"
android:padding="5dp"
android:layout_marginEnd="15dp"/>
<ImageView
android:id="@+id/iv_grid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/selector_fuzhuxian"
app:layout_constraintTop_toTopOf="@+id/iv_back"
app:layout_constraintBottom_toBottomOf="@+id/iv_back"
app:layout_constraintEnd_toStartOf="@+id/iv_light"
android:padding="5dp"
android:layout_marginEnd="5dp"/>
<androidx.camera.view.PreviewView
android:id="@+id/pv"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@+id/iv_back"
app:layout_constraintBottom_toTopOf="@+id/iv_take_photo"
android:layout_marginVertical="30dp"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:visibility="gone"
android:id="@+id/cl_grid_line"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="@+id/pv"
app:layout_constraintBottom_toBottomOf="@+id/pv">
<com.dokiwei.module_home.widget.DottedLineView
android:layout_width="match_parent"
android:layout_height="1dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.3333"/>
<com.dokiwei.module_home.widget.DottedLineView
android:layout_width="match_parent"
android:layout_height="1dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.6666"/>
<View
android:layout_width="0.5dp"
android:layout_height="match_parent"
android:background="@color/white"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.25"/>
<View
android:layout_width="0.5dp"
android:layout_height="match_parent"
android:background="@color/white"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.75"/>
<com.dokiwei.module_home.widget.DottedLineView
android:layout_width="1dp"
android:layout_height="match_parent"
app:orientation="ver"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.5"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="@+id/iv_take_photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/aa_icon_paishe"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="45dp"/>
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/iv_pic"
android:layout_width="47dp"
android:layout_height="47dp"
app:shapeAppearance="@style/RoundedStyle"
android:background="@color/black"
android:scaleType="centerCrop"
app:layout_constraintTop_toTopOf="@+id/iv_take_photo"
app:layout_constraintBottom_toBottomOf="@+id/iv_take_photo"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="30dp"/>
<ImageView
android:id="@+id/iv_reverse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/aa_icon_xuanzhuan_white"
android:padding="5dp"
app:layout_constraintTop_toTopOf="@+id/iv_take_photo"
app:layout_constraintBottom_toBottomOf="@+id/iv_take_photo"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="35dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
class CameraActivity : BaseActivity<BaseViewModel,ActivityCameraBinding>(
ActivityCameraBinding::inflate
) {
private lateinit var cameraXLoader: CameraXLoader
private var bitmap: Bitmap? = null
override fun initView() {
setRootStatusBarPadding()
initCamera()
binding.ivLight.setOnClickListener {
cameraXLoader.flashEnable(!it.isSelected)
it.isSelected = !it.isSelected
}
binding.ivGrid.setOnClickListener {
if (it.isSelected){
binding.clGridLine.hide()
}else {
binding.clGridLine.show()
}
it.isSelected = !it.isSelected
}
binding.ivReverse.setOnClickListener {
cameraXLoader.switchCamera()
}
binding.ivPic.setOnClickListener {
val intent = Intent()
intent.setAction(Intent.ACTION_MAIN)
intent.addCategory(Intent.CATEGORY_APP_GALLERY)
startActivity(intent)
}
binding.ivTakePhoto.setOnClickListener {
lifecycleScope.launch {
PermissionUtils.doOnHasStoragePermission(this@CameraActivity){
val child = "${System.currentTimeMillis()}.jpg"
cameraXLoader.takePhoto(
File(FileUtils.PICTURE_FILE, child),
object :OnImageSavedCallback{
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
lifecycleScope.launch(Dispatchers.Main) {
Glide.with(this@CameraActivity)
.load(outputFileResults.savedUri).into(binding.ivPic)
}
lifecycleScope.launch(Dispatchers.IO) {
showLoading()
bitmap = BitmapFactory.decodeFile(outputFileResults.savedUri?.path)
// 其次把文件插入到系统图库
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
try {
val values = ContentValues()
values.put(MediaStore.Images.Media.DISPLAY_NAME, child)
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png ")
values.put(
MediaStore.MediaColumns.RELATIVE_PATH,
FileUtils.PICTURE_FILE.absolutePath
)
val uri = contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
values
)
uri?.let {
contentResolver.openOutputStream(it)?.use { outputStream ->
bitmap?.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
}
}
stopLoading()
showToast("已保存至相册")
} catch (e: Exception) {
e.printStackTrace()
stopLoading()
showToast("保存图片失败")
}
}else {
val parentFile = FileUtils.PICTURE_FILE.absolutePath
val file = File(parentFile, child)
file.parentFile?.takeIf { !it.exists() }?.mkdirs()
FileOutputStream(file).use {
bitmap?.compress(Bitmap.CompressFormat.PNG, 100, it)
}
notifyMedia(file.path, this@CameraActivity)
stopLoading()
}
}
}
override fun onError(exception: ImageCaptureException) {
showToast("拍照失败")
exception.printStackTrace()
}
})
}
}
}
}
private fun initCamera() {
cameraXLoader = CameraXLoader(this, binding.pv)
lifecycle.addObserver(cameraXLoader)
}
override fun onDestroy() {
super.onDestroy()
bitmap?.recycle()
lifecycle.removeObserver(cameraXLoader)
}
}