JetPack之使用CameraX完成拍照和拍視頻

前段時間CameraX的Beta版發佈了,這幾天有時間也來嘗試一下。Beta版本是對外測試版本,意味着它已經走出實驗室走向生產,API的調用基本穩定不會大改了,bug也會更少可以用於生成環境。

之前使用Camera1和Camera2開發相機功能的時候需要調用非常複雜的API,而且由於Android手機的碎片化嚴重,不同手機對相機功能的支持度也不一樣,因此很多做相機相關應用的公司都會封裝自己的相機庫來簡化相機的使用步驟和處理兼容性問題。

CameraX其實就是Google開發的一個用來簡化相機開發時候API的調用和處理各種兼容性問題的庫。最多兼容到Android 5.0,底層調用的也是Camera2,不過比Camera2用起來更簡單,而且可以綁定生命週期,從而可以自動的處理相機的開啓釋放等工作。

下面開始來嘗試吧

添加依賴

dependencies {
   // CameraX 核心庫使用 camera2 實現
    implementation "androidx.camera:camera-camera2:1.0.0-beta03"
    // 可以使用CameraView
    implementation "androidx.camera:camera-view:1.0.0-alpha10"
    // 可以使用供應商擴展
    implementation "androidx.camera:camera-extensions:1.0.0-alpha10"
    //camerax的生命週期庫
    implementation "androidx.camera:camera-lifecycle:1.0.0-beta03"
    }

如果想要使用CameraX拍照非常簡單,只需要配置不同的使用狀態,然後綁定到生命週期中即可。比如預覽需要設置預覽相關的狀態,拍照需要設置拍照相關的狀態,錄製視頻需要設置錄製相關的狀態。

配置狀態

預覽配置:Preview用於相機預覽的時候顯示預覽畫面。

Preview preview = new Preview.Builder()
                //設置寬高比
                .setTargetAspectRatio(screenAspectRatio)
                //設置當前屏幕的旋轉
                .setTargetRotation(rotation)
                .build();

照相配置:ImageCapture 用於拍照,並將圖片保存

ImageCapture imageCapture = new ImageCapture.Builder()
                //優化捕獲速度,可能降低圖片質量
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                //設置寬高比
                .setTargetAspectRatio(screenAspectRatio)
                //設置初始的旋轉角度
                .setTargetRotation(rotation)
                .build();

錄製視頻設置:VideoCapture 用來錄製視頻和保存視頻,寬高比和分辨率設置一個就可以了,不要同時設置否則報錯。根據實際的需求來設置,如果對寬高比要求高就設置寬高比,反之就設置分辨率

 VideoCapture videoCapture = new VideoCaptureConfig.Builder()
                //設置當前旋轉
                .setTargetRotation(rotation)
                //設置寬高比
                .setTargetAspectRatio(screenAspectRatio)
                //分辨率
                //.setTargetResolution(resolution)
                //視頻幀率  越高視頻體積越大
                .setVideoFrameRate(25)
                //bit率  越大視頻體積越大
                .setBitRate(3 * 1024 * 1024)
                .build();

綁定到生命週期:ProcessCameraProvider 是一個單例類,可以把相機的生命週期綁定到任何LifecycleOwner類中。AppCompatActivity和Fragment都是LifecycleOwner

//Future表示一個異步的任務,ListenableFuture可以監聽這個任務,當任務完成的時候執行回調
 ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
                ProcessCameraProvider.getInstance(this);
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();

//重新綁定之前必須先取消綁定
cameraProvider.unbindAll();

Camera camera = cameraProvider.bindToLifecycle(CameraActivity.this,
                cameraSelector,preview,imageCapture,videoCapture);

OK預覽,照相,錄視頻的配置和綁定到生命週期的工作就完成了

預覽的時候需要顯示到一個View控件上吧,CameraX中提供了一個PreviewView用來顯示預覽畫面。其內部封裝了TextureView和SurfaceView,可以根據不同的模式來選擇其內部使用TextureView還是SurfaceView來顯示。

xml中添加PreviewView,並在代碼中將其附加到前面創建出來的Preview這個實例上

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

 preview.setSurfaceProvider(mPreviewView.createSurfaceProvider(camera .getCameraInfo()));

這樣當我們進入該頁面的時候就可以看到相機的預覽效果呢,接下來就是執行拍照和錄製的功能了

執行拍照錄像

拍照:

 //創建圖片保存的文件地址
  File file = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath(),
          System.currentTimeMillis() + ".jpeg");
  ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(file).build();
  mImageCapture.takePicture(outputFileOptions,mExecutorService , new ImageCapture.OnImageSavedCallback() {
      @Override
      public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
          Uri savedUri = outputFileResults.getSavedUri();
          if(savedUri == null){
              savedUri = Uri.fromFile(file);
          }
          outputFilePath = file.getAbsolutePath();
          onFileSaved(savedUri);
      }

      @Override
      public void onError(@NonNull ImageCaptureException exception) {
          Log.e(TAG, "Photo capture failed: "+exception.getMessage(), exception);
      }
  });
//將前面保存的文件添加到媒體中
private void onFileSaved(Uri savedUri) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            sendBroadcast(new Intent(android.hardware.Camera.ACTION_NEW_PICTURE, savedUri));
        }
        String mimeTypeFromExtension = MimeTypeMap.getSingleton().getMimeTypeFromExtension(MimeTypeMap
                .getFileExtensionFromUrl(savedUri.getPath()));
        MediaScannerConnection.scanFile(getApplicationContext(),
                new String[]{new File(savedUri.getPath()).getAbsolutePath()},
                new String[]{mimeTypeFromExtension}, new MediaScannerConnection.OnScanCompletedListener() {
                    @Override
                    public void onScanCompleted(String path, Uri uri) {
                        Log.d(TAG, "Image capture scanned into media store: $uri"+uri);
                    }
                });
        PreviewActivity.start(this, outputFilePath, !takingPicture);
    }
  • 調用ImageCapture的takePicture方法來拍照
  • 傳入一個文件地址用來保存拍好的照片
  • onImageSaved方法是照片已經拍好並存好之後的回調
  • onFileSaved方法中將前面保存的文件添加到媒體中,最後跳轉到預覽界面。

錄視頻:

  //創建視頻保存的文件地址
File file = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath(),
        System.currentTimeMillis() + ".mp4");
mVideoCapture.startRecording(file, Executors.newSingleThreadExecutor(), new VideoCapture.OnVideoSavedCallback() {
    @Override
    public void onVideoSaved(@NonNull File file) {
        outputFilePath = file.getAbsolutePath();
        onFileSaved(Uri.fromFile(file));
    }

    @Override
    public void onError(int videoCaptureError, @NonNull String message, @Nullable Throwable cause) {
        Log.i(TAG,message);
    }
});
  • 使用VideoCapture的startRecording方法來錄視頻
  • 傳入一個File文件用來保存視頻,
  • 錄製完成之後回調onVideoSaved方法,並返回該文件的實例。
  • 調用onFileSaved方法將前面保存的文件添加到媒體中,最後跳轉到預覽界面。
  • 到達錄製時間的時候,需要調用videoCapture.stopRecording();方法來停止錄像。

到這裏使用CameraX拍照和錄製視頻的功能都能完成了,是不是非常簡單。下面來點題外的,自定義一個View,實現點擊拍照,長按錄像的效果。效果如下:
在這裏插入圖片描述
代碼:

public class RecordView extends View implements View.OnLongClickListener, View.OnClickListener {
    private static final int PROGRESS_INTERVAL = 100;
    private int mBgColor;
    private int mStrokeColor;
    private int mStrokeWidth;
    private int mDuration;
    private int mWidth;
    private int mHeight;
    private int mRadius;
    private int mProgressValue;
    private boolean isRecording;
    private RectF mArcRectF;
    private Paint mBgPaint, mProgressPaint;
    private OnRecordListener mOnRecordListener;
    private long mStartRecordTime;

    public void setOnRecordListener(OnRecordListener onRecordListener) {
        mOnRecordListener = onRecordListener;
    }

    public RecordView(Context context) {
        this(context, null);
    }

    public RecordView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RecordView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RecordView);
        mBgColor = typedArray.getColor(R.styleable.RecordView_bg_color, Color.WHITE);
        mStrokeColor = typedArray.getColor(R.styleable.RecordView_stroke_color, Color.RED);
        mStrokeWidth = typedArray.getDimensionPixelOffset(R.styleable.RecordView_stroke_width, SizeUtils.dp2px(5));
        mDuration = typedArray.getInteger(R.styleable.RecordView_duration, 10);
        mRadius = typedArray.getDimensionPixelOffset(R.styleable.RecordView_radius, SizeUtils.dp2px(40));
        typedArray.recycle();

        mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBgPaint.setStyle(Paint.Style.FILL);
        mBgPaint.setColor(mBgColor);

        mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mProgressPaint.setStyle(Paint.Style.STROKE);
        mProgressPaint.setColor(mStrokeColor);
        mProgressPaint.setStrokeWidth(mStrokeWidth);

        setEvent();
    }

    private void setEvent() {
        Handler handler = new Handler(Looper.getMainLooper()) {
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
                mProgressValue++;
                postInvalidate();
                if (mProgressValue < mDuration*10) {
                    sendEmptyMessageDelayed(0, PROGRESS_INTERVAL);
                } else {
                    finishRecord();
                }
            }
        };
        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if(event.getAction() == MotionEvent.ACTION_DOWN){
                    mStartRecordTime = System.currentTimeMillis();
                    handler.sendEmptyMessage(0);
                }else if(event.getAction() == MotionEvent.ACTION_UP){
                    long duration = System.currentTimeMillis() - mStartRecordTime;
                    //是否大於系統設定的最小長按時間
                    if(duration > ViewConfiguration.getLongPressTimeout()){
                        finishRecord();
                    }
                    handler.removeCallbacksAndMessages(null);
                    isRecording = false;
                    mStartRecordTime = 0;
                    mProgressValue = 0;
                    postInvalidate();
                }
                return false;
            }
        });
        setOnClickListener(this);
        setOnLongClickListener(this);
    }

    private void finishRecord() {
         if(mOnRecordListener!=null){
             mOnRecordListener.onFinish();
         }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = w;
        mArcRectF = new RectF(mStrokeWidth / 2f, mStrokeWidth / 2f,
                mWidth - mStrokeWidth / 2f, mHeight - mStrokeWidth / 2f);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawCircle(mWidth / 2f, mHeight / 2f, mRadius, mBgPaint);

        if (isRecording) {
            canvas.drawCircle(mWidth / 2f, mHeight / 2f, mRadius/10f*11, mBgPaint);
            float sweepAngle = 360f * mProgressValue / (mDuration*10);
            Log.i("sweepAngle",sweepAngle+"");
            canvas.drawArc(mArcRectF, -90, sweepAngle, false, mProgressPaint);
        }

    }

    @Override
    public boolean onLongClick(View v) {
        isRecording = true;
        if(mOnRecordListener!=null){
            mOnRecordListener.onRecordVideo();
        }
        return true;
    }

    @Override
    public void onClick(View v) {
        if(mOnRecordListener!=null){
            mOnRecordListener.onTackPicture();
        }
    }

    public interface OnRecordListener {
        void onTackPicture();

        void onRecordVideo();

        void onFinish();
    }

}

實現起來也非常簡單,首先繪製一個圓,監聽該View的點擊和長按事件,長按的時候在根據總錄製時長和當前錄製時間算出需要繪製的角度,就可以在圓上面繪製進度了。

最後通過接口將點擊 長按和錄製完成的事件返回,跟前面的拍照,錄製,錄製完成的代碼結合起來就完成上面的效果了。

CameraView

如果覺得前面的初始化還不夠簡單,那麼可以使用CameraX提供的CameraView了,這裏面將PreviewView,Preview,ImageCapture,VideoCapture等都封裝起來了,而且還能實現縮放,裁剪,旋轉等功能,使用起來更加簡單。

首先xml文件中添加CameraView

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

然後在Activity中實例化CameraView,直接綁定當前生命週期就可以了。

 mBtnCameraSwitch = findViewById(R.id.camera_switch_button);
 mCameraView.bindToLifecycle(this);

只需兩句話就完成了前面的初始工作。然後就可以愉快的拍照和錄製視頻了。

拍照和錄製的代碼跟前面一樣只不過全都是通過CamerView對象來調用mCameraView.takePicture , mCameraView.startRecording,調用之前需要通過mCameraView.setCaptureMode(CameraView.CaptureMode.IMAGE)來切換當前的模式是拍照還是錄像。

將前面的自定義的RecordView加入佈局文件中,跟CameraView的拍照、錄像代碼一結合,很快就能實現跟前面一樣的效果了。

其他

圖片分析

CameraX還提供了圖像分析功能,它提供了可供 CPU 訪問以執行圖像處理、計算機視覺或機器學習推斷的圖像,可以無縫的訪問緩衝區,一般用不到但功能很強大。創建一個圖片分析器然後綁定聲明週期即可。

 mImageAnalysis = new ImageAnalysis.Builder()
                .setTargetAspectRatio(screenAspectRatio)
                .setTargetRotation(rotation)
                .build();
        mImageAnalysis.setAnalyzer(mExecutorService, new ImageAnalysis.Analyzer() {
            @Override
            public void analyze(@NonNull ImageProxy image) {
                
            }
    });
cameraProvider.bindToLifecycle(CameraActivity.this,
               cameraSelector,mPreview,mImageCapture,mVideoCapture,mImageAnalysis);

供應商擴展

供應商擴展程序:CameraX提供了外部擴展的API,可以直接對接手機產商,如果該手機廠商實現了CameraX的擴展程序,就可以使用VamerX的擴展API直接調用這些效果比如:美顏、DHR、夜間、自動等模式。

因爲不是所有的手機廠商都支持擴展程序,所以在使用擴展的時候需要判斷一下該手機是否支持,支持才添加。

給預覽界面設置外部擴展,需要Preview.BuilderCameraSelector cameraSelector)兩個參數

  private void setPreviewExtender(Preview.Builder builder, CameraSelector cameraSelector) {
        AutoPreviewExtender extender = AutoPreviewExtender.create(builder);
        if(extender.isExtensionAvailable(cameraSelector)){
            extender.enableExtension(cameraSelector);
        }
        BokehPreviewExtender bokehPreviewExtender = BokehPreviewExtender.create(builder);
        if(bokehPreviewExtender.isExtensionAvailable(cameraSelector)){
            bokehPreviewExtender.enableExtension(cameraSelector);
        }
        HdrPreviewExtender hdrPreviewExtender = HdrPreviewExtender.create(builder);
        if(hdrPreviewExtender.isExtensionAvailable(cameraSelector)){
            hdrPreviewExtender.enableExtension(cameraSelector);
        }
        BeautyPreviewExtender beautyPreviewExtender = BeautyPreviewExtender.create(builder);
        if(beautyPreviewExtender.isExtensionAvailable(cameraSelector)){
            beautyPreviewExtender.enableExtension(cameraSelector);
        }
        NightPreviewExtender nightPreviewExtender = NightPreviewExtender.create(builder);
        if(nightPreviewExtender.isExtensionAvailable(cameraSelector)){
            nightPreviewExtender.enableExtension(cameraSelector);
        }
    }

給拍攝的圖片設置外部擴展,,需要ImageCapture.BuilderCameraSelector cameraSelector)兩個參數

private void setImageCaptureExtender(ImageCapture.Builder builder, CameraSelector cameraSelector) {
        AutoImageCaptureExtender autoImageCaptureExtender = AutoImageCaptureExtender.create(builder);
        if (autoImageCaptureExtender.isExtensionAvailable(cameraSelector)) {
            autoImageCaptureExtender.enableExtension(cameraSelector);
        }
        BokehImageCaptureExtender bokehImageCaptureExtender = BokehImageCaptureExtender.create(builder);
        if(bokehImageCaptureExtender.isExtensionAvailable(cameraSelector)){
            bokehImageCaptureExtender.enableExtension(cameraSelector);
        }
        HdrImageCaptureExtender hdrImageCaptureExtender = HdrImageCaptureExtender.create(builder);
        if(hdrImageCaptureExtender.isExtensionAvailable(cameraSelector)){
            hdrImageCaptureExtender.enableExtension(cameraSelector);
        }
        BeautyImageCaptureExtender beautyImageCaptureExtender = BeautyImageCaptureExtender.create(builder);
        if(beautyImageCaptureExtender.isExtensionAvailable(cameraSelector)){
            beautyImageCaptureExtender.enableExtension(cameraSelector);
        }
        NightImageCaptureExtender nightImageCaptureExtender = NightImageCaptureExtender.create(builder);
        if(nightImageCaptureExtender.isExtensionAvailable(cameraSelector)){
            nightImageCaptureExtender.enableExtension(cameraSelector);
        }
    }

demo地址:地址鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章