Android 相机CameraX框架

Android 相机CameraX框架

CameraX是Jetpack 支持库,利用的是 camera2 的功能,它具有生命周期的感应,使用更加简单,代码量也减少了不少。可以灵活的录制视频和拍照

官方文档
https://developer.android.google.cn/media/camera/camerax?hl=ro

build.gradle的dependencies中引入框架

java 复制代码
def cameraxVersion = "1.1.0-alpha05";
implementation "androidx.camera:camera-core:${cameraxVersion}"
implementation "androidx.camera:camera-camera2:${cameraxVersion}"
implementation "androidx.camera:camera-lifecycle:${cameraxVersion}"
//CameraX添加依赖
implementation "androidx.camera:camera-view:1.3.0-alpha07"
implementation 'androidx.camera:camera-video:'
implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"

配置CameraXConfig

使用 setAvailableCameraLimiter() 优化启动延迟时间。

使用 setCameraExecutor() 向 CameraX 提供应用执行器。

将默认调度器处理程序替换为 setSchedulerHandler()。

使用 setMinimumLoggingLevel() 更改日志记录级别。

图像预览PreviewView

选择相机并绑定生命周期和用例:

创建 Preview。

指定所需的相机 LensFacing 选项。

将所选相机和任意用例绑定到生命周期。

将 Preview 连接到 PreviewView。

java 复制代码
    void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
    Preview preview = new Preview.Builder()
            .build();
            //设置缩放类型可选FIT_CENTER、FIT_START 和 FIT_END,默认缩放类型是 FILL_CENTER。
     preview .setScaleType(PreviewView.ScaleType.FIT_CENTER);
    CameraSelector cameraSelector = new CameraSelector.Builder()
            .requireLensFacing(CameraSelector.LENS_FACING_BACK)
            .build();
    preview.setSurfaceProvider(previewView.getSurfaceProvider());
    Camera camera = cameraProvider.bindToLifecycle((LifecycleOwner)this, cameraSelector, preview);
}

图像分析ImageAnalysis

设置图片输出尺寸,输出格式,旋转角度...

输出格式:CameraX 可通过 setOutputImageFormat(int) 支持 YUV_420_888 和 RGBA_8888。默认格式为 YUV_420_888。

注意:使用 ProcessCameraProvider.bindToLifecycle() 函数将 ImageAnalysis 绑定到现有的 AndroidX 生命周期

java 复制代码
    imageAnalysis = ImageAnalysis.Builder()
            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
//            .setTargetResolution(Size(1280, 720))设置实际的尺寸
            .setTargetAspectRatio(AspectRatio.RATIO_4_3) //设计宽高比
            .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
            .build()
            //创建分析器
        imageAnalysis!!.setAnalyzer(
            cameraExecutor!!,
            ImageAnalysis.Analyzer { imageProxy: ImageProxy ->
            //通过调用 ImageProxy.close() 将 ImageProxy 发布到 CameraX
                imageProxy.close()
            })
...
cameraProvider.bindToLifecycle((LifecycleOwner) this, cameraSelector, imageAnalysis, preview);

图像拍摄输出保存

拍摄的配置参数,例如闪光灯、连续自动对焦、零快门延迟等。

java 复制代码
        imageCapture = new ImageCapture.Builder()
                //控制闪光灯模式 FLASH_MODE_ON(拍照时,闪光灯会亮起), 
                //FLASH_MODE_OFF 闪光灯关闭,FLASH_MODE_AUTO:在弱光环境下拍摄时,自动开启闪光灯。
                .setFlashMode(ImageCapture.FLASH_MODE_ON)
                //ImageCapture.Builder.setCaptureMode() 可用于配置拍摄照片时所采用的拍摄模式:
				//CAPTURE_MODE_MINIMIZE_LATENCY:缩短图片拍摄的延迟时间。
				//CAPTURE_MODE_MAXIMIZE_QUALITY:提高图片拍摄的图片质量。
                 .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                .build();

拍照保存图片的方式有2种,

takePicture(OutputFileOptions, Executor, OnImageSavedCallback):此方法将拍摄的图片保存到提供的文件位置。

java 复制代码
            val name = System.currentTimeMillis().toString() + ".jpg"
            val contentValues = ContentValues()
            contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
            contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                contentValues.put(
                    MediaStore.MediaColumns.RELATIVE_PATH,
                    FileUtils.getSystemPicDataPath() //图片路径
                )
            }
            //图片输出
            val outputFileOptions = OutputFileOptions.Builder(
                contentResolver,
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                contentValues
            )
                .build()
            imageCapture!!.takePicture(
                outputFileOptions,
                ContextCompat.getMainExecutor(this),
                object : ImageCapture.OnImageSavedCallback {
                    override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                        //content://media/external/images/media/125
                        //Pictures/CameraXImage
                        //aaa图片路径.jpg
                        printLog(" aaa图片路径--" + FileUtils.getSystemPicDataPath() + name)
                        CoroutineScope(Dispatchers.IO).launch {
                            val blo = FileUtils.copyFile(
                                FileUtils.getSystemPicDataPath() + name,
                                FileUtils.getAppPicDataPath() + name
                            )
                            withContext(Dispatchers.Main) {
                                printLog("文件复制成功$blo")
                                if (blo) {
                                    File(FileUtils.getAppPicDataPath() + name).delete()
                                    binding.ivCamera.visibility = View.VISIBLE
                                    FileUtils.showGlidePic(
                                        this@CameraActivity,
                                        FileUtils.getAppPicDataPath() + name,
                                        binding.ivCamera
                                    )
                                }
                            }
                        }
                        //  printLog( Objects.requireNonNull(outputFileResults.savedUri).toString())
                    }

                    override fun onError(exception: ImageCaptureException) {
                        printLog(exception.toString())
                    }
                })

另一种takePicture(Executor, OnImageCapturedCallback):此方法为拍摄的图片提供内存缓冲区。

java 复制代码
   imageCapture!!.takePicture(ContextCompat.getMainExecutor(this), object :
                ImageCapture.OnImageCapturedCallback() {

                override fun onCaptureSuccess(image: ImageProxy) {
                    super.onCaptureSuccess(image)
                   // println(image)
                   // printLog("文件保存成功${image.format}")
                    if (image.format === ImageFormat.JPEG) {
                        val planes = image.planes
                        val buffer = planes[0].buffer
                        val size = buffer.remaining()
                        val jpeg = ByteArray(size)
                        buffer[jpeg, 0, size]
                        val bitmap: Bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.size);
                        val path=FileUtils.getAppPicDataPath() + name
                        //将bitmap保存为图片
                        val isFlag=FileUtils.bitmapToFile(bitmap,path)
                        printLog("文件保存成功$isFlag")
                        if (isFlag){
                            binding.ivCamera.visibility = View.VISIBLE
                            //存储图片成功,现实图片
                            FileUtils.showGlidePic(
                                this@CameraActivity,
                                path,
                                binding.ivCamera
                            )
                            
                        }else{
                            toast("获取图片失败")
                        
                        }
                    }
                    image.close()
                }

                override fun onError(exception: ImageCaptureException) {
                    printLog(exception.toString())
                }
            })

完整代码演示:

xml文件自定义UI

java 复制代码
<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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.activity.CameraVideo2Activity">

    <androidx.camera.view.PreviewView
        android:id="@+id/preview_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <Button
        android:id="@+id/takeVideoBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="录像"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/takePhotoBtn"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias=".9" />

    <Button
        android:id="@+id/takePhotoBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="拍照"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/takeVideoBtn"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias=".9" />
</androidx.constraintlayout.widget.ConstraintLayout>

activity代码

java 复制代码
public class CameraVideo2Activity extends AppCompatActivity {
    //按钮
    Button takePhotoButton;
    Button takeVideoButton;
    //预览
    PreviewView previewView;
    //权限
    private static final String[] REQUIRE_PERMISSIONS = new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO};
    public static final int REQUEST_CODE_PERMISSIONS = 10;
    //capture
    ImageCapture imageCapture;
    ListenableFuture<ProcessCameraProvider> processCameraProviderListenableFuture;
    //录像
    VideoCapture videoCapture;
    Recording recording;
    //executor & imageAnalysis
    private ExecutorService cameraExecutor;
    private ImageAnalysis imageAnalysis;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_camera_video2);
        //绑定控件
        takePhotoButton = findViewById(R.id.takePhotoBtn);
        takeVideoButton = findViewById(R.id.takeVideoBtn);
        previewView = findViewById(R.id.preview_view);
        takePhotoButton.setOnClickListener(v -> takePhoto());
        takeVideoButton.setOnClickListener(v -> takeVideo());
        //获取权限
        if (havePermissions()) {
            initCamera();
        } else {
            ActivityCompat.requestPermissions(this, REQUIRE_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
        }
        //executor实例
        cameraExecutor = Executors.newSingleThreadExecutor();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        cameraExecutor.shutdown();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            initCamera();
        } else {
            finish();
        }
    }

    //判断权限是否获取
    private boolean havePermissions() {
        for (String permission : REQUIRE_PERMISSIONS) {
            if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
                return false;
            }
        }
        return true;
    }

    //初始化Camera
    @SuppressLint("UnsafeOptInUsageError")
    private void initCamera() {
        ///实例化(可以设置许多属性)
        imageCapture = new ImageCapture.Builder()
                //控制闪光灯模式 FLASH_MODE_ON(拍照时,闪光灯会亮起), FLASH_MODE_OFF 闪光灯关闭
                .setFlashMode(ImageCapture.FLASH_MODE_ON)
                .build();
        Recorder recorder = new Recorder.Builder().build();
        videoCapture = VideoCapture.withOutput(recorder);
        processCameraProviderListenableFuture = ProcessCameraProvider.getInstance(this);
        processCameraProviderListenableFuture.addListener(() -> {
            try {
                //配置预览(https://developer.android.google.cn/training/camerax/preview?hl=zh-cn)
                previewView.setScaleType(PreviewView.ScaleType.FIT_CENTER);
                Preview preview = new Preview.Builder().build();
                preview.setSurfaceProvider(previewView.getSurfaceProvider());
                //绑定到生命周期
                ProcessCameraProvider processCameraProvider = processCameraProviderListenableFuture.get();
                //图片分析
                initImageAnalysis();
                //设置旋转
                setOrientationEventListener();
                //剪裁矩形(拍摄之后,对图片进行裁剪)
                ViewPort viewPort = null;
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
                    viewPort = new ViewPort.Builder(
                            new Rational(100, 100), getDisplay().getRotation()
                    ).build();
                } else {
                    viewPort = new ViewPort.Builder(
                            new Rational(100, 100), Surface.ROTATION_0
                    ).build();
                }
                UseCaseGroup useCaseGroup = new UseCaseGroup.Builder()
                        .addUseCase(preview)
                        .addUseCase(imageAnalysis)
                        .addUseCase(imageCapture)
                        .addUseCase(videoCapture)
                        .setViewPort(viewPort)
                        .build();
                processCameraProvider.unbindAll();
                Camera camera = processCameraProvider.bindToLifecycle(CameraVideo2Activity.this, CameraSelector.DEFAULT_BACK_CAMERA, useCaseGroup);//DEFAULT_BACK_CAMERA 后置摄像头
                //Camera
                CameraControl cameraControl = camera.getCameraControl();
                CameraInfo cameraInfo = camera.getCameraInfo();
            } catch (ExecutionException | InterruptedException e) {
                e.printStackTrace();
            }
        }, ContextCompat.getMainExecutor(this));
    }

    //图片分析
    @SuppressLint("UnsafeOptInUsageError")
    private void initImageAnalysis() {
        imageAnalysis = new ImageAnalysis.Builder()
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build();
        imageAnalysis.setAnalyzer(cameraExecutor, imageProxy -> {
            int rotationDegrees = imageProxy.getImageInfo().getRotationDegrees();
            imageProxy.close();
        });
    }

    ///旋转
    //orientation为北为0,顺时针度数0-360
    //Surface.ROTATION_270将拍摄好的图片顺时针旋转270度
    private void setOrientationEventListener() {
        OrientationEventListener orientationEventListener = new OrientationEventListener(this) {
            @Override
            public void onOrientationChanged(int orientation) {
                int rotation;
                if (orientation >= 45 && orientation < 135) {
                    rotation = Surface.ROTATION_270;
                } else if (orientation >= 135 && orientation < 225) {
                    rotation = Surface.ROTATION_180;
                } else if (orientation >= 225 && orientation < 315) {
                    rotation = Surface.ROTATION_90;
                } else {
                    rotation = Surface.ROTATION_0;
                }
                imageCapture.setTargetRotation(rotation);
            }
        };
        orientationEventListener.enable();
    }

    //拍照
    private void takePhoto() {
        if (imageCapture != null) {
            //ContentValues
            String name = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.SIMPLIFIED_CHINESE).format(System.currentTimeMillis());
            ContentValues contentValues = new ContentValues();
            contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
            contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, "Pictures/CameraXImage");
            }
            //图片输出
            ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions
                    .Builder(getContentResolver(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
                    .build();
            imageCapture.takePicture(outputFileOptions, ContextCompat.getMainExecutor(this), new ImageCapture.OnImageSavedCallback() {
                @Override
                public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
                    //content://media/external/images/media/125
                    //Pictures/CameraXImage
                    //aaa图片路径5.jpg
                    System.out.println("图片路径--"+name+".jpg");
                    Log.i("camera", Objects.requireNonNull(outputFileResults.getSavedUri()).toString());
                }

                @Override
                public void onError(@NonNull ImageCaptureException exception) {
                    Log.e("camera", exception.toString());
                }
            });
        }
    }

    //录像
    private void takeVideo() {
        if (videoCapture != null) {
            takeVideoButton.setEnabled(false);
            if (recording != null) {
                recording.stop();
                recording = null;
                return;
            }
            String name = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.SIMPLIFIED_CHINESE).format(System.currentTimeMillis()) + ".mp4";
            ContentValues contentValues = new ContentValues();
            contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
            contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, "Movies/CameraX-Video");
            }
            MediaStoreOutputOptions mediaStoreOutputOptions = new MediaStoreOutputOptions
                    .Builder(getContentResolver(), MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
                    .setContentValues(contentValues)
                    .build();
            Recorder recorder = (Recorder) videoCapture.getOutput();
            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this, REQUIRE_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
                return;
            }
            recording = recorder.prepareRecording(this, mediaStoreOutputOptions)
                    .withAudioEnabled()
                    .start(ContextCompat.getMainExecutor(this), videoRecordEvent -> {
                        if (videoRecordEvent instanceof VideoRecordEvent.Start) {
                            takeVideoButton.setText("停止");
                            takeVideoButton.setEnabled(true);
                        } else if (videoRecordEvent instanceof VideoRecordEvent.Finalize) {
                            if (((VideoRecordEvent.Finalize) videoRecordEvent).hasError()) {
                                if (recording != null) {
                                    recording.close();
                                    recording = null;
                                }
                            } else {
                                //视频为content://media/external/video/media/122
                                //Movies/CameraX-Video/name
                                String msg = "视频为" + ((VideoRecordEvent.Finalize) videoRecordEvent).getOutputResults().getOutputUri();
                                Log.i("camera", msg);
                                Log.i("camera", "视频路径为"+name);
                            }
                            takeVideoButton.setEnabled(true);
                            takeVideoButton.setText("录像");
                        }
                    });
        }
    }
}
相关推荐
我命由我123453 小时前
Jetpack Compose - MaterialExpressiveTheme 与 MaterialTheme、ColorScheme
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
lilian2334 小时前
Harmony os 技术实战|拼豆制图06:收藏 ID、生成记录与重启恢复怎么不打架
android·java·数据库·harmonyos
2601_955759624 小时前
ClaudeAPI成本中心与业务标签设计指南
android·java·数据库
余衫马4 小时前
2026最新版!Android Studio 极速安装与配置指南
android·ide·android studio
AFinalStone4 小时前
Android 7系统异常问题排查(五)Framework层(下)—System Server崩溃
android·tombstone·系统异常
AFinalStone4 小时前
Android 7系统异常问题排查(八)系统追踪—Trace机制与性能诊断
android·系统异常
极客猴子8 小时前
Android录音转写频繁卡顿?多款APP长时间会议场景稳定性实测
android·人工智能·智能手机·飞书
Kapaseker8 小时前
Boolean 变量到底该怎么命名?
android·kotlin
AFinalStone8 小时前
Android 7系统异常问题排查(六)应用异常(上)—ANR机制全解
android·系统异常
雾屿_Mistisle8 小时前
文件包含漏洞(File Inclusion)
android