文章目录

  • 布局文件
  • 请求有关权限
  • 加载布局,设置surface。避免画面拉升等问题



学习Camera2 时,开发一个简单自定义相机。具体流程看代码

布局文件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <SurfaceView android:id="@+id/surfaceView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="100"/>
    <LinearLayout android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <Button android:id="@+id/btnTakePhoto"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_margin="5dp"
            android:text="拍照"/>
        <Button android:id="@+id/btnSwitch"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text="切换"/>
        <Button android:id="@+id/record"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text="录像"/>
        <Button android:id="@+id/stop"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text="停止"/>
    </LinearLayout>
</LinearLayout>

请求有关权限

因为使用的是Android 11的手机,有些权限需要动态申请。

<uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>

代码中

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //动态获取权限
        List<String> permissionList = new ArrayList<>();
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            permissionList.add(Manifest.permission.CAMERA);
        }
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
                != PackageManager.PERMISSION_GRANTED) {
            permissionList.add(Manifest.permission.RECORD_AUDIO);
        }
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            permissionList.add(Manifest.permission.READ_EXTERNAL_STORAGE);
        }
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
        }

        if (!permissionList.isEmpty()) {
            String[] permissions = permissionList.toArray(new String[permissionList.size()]);
            ActivityCompat.requestPermissions(MainActivity.this, permissions, 1);
        } else {
            initView();
        }

    }
  @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
            case 1:
                if (grantResults.length > 0) {
                    for (int result : grantResults) {
                        if (result != PackageManager.PERMISSION_GRANTED) {
                            Toast.makeText(this, "拒绝权限无法使用", Toast.LENGTH_SHORT).show();
                            finish();
                        }
                    }
                    initView();
                } else {
                    Toast.makeText(this, "发生未知错误", Toast.LENGTH_SHORT).show();
                    finish();
                }
                break;
            default:
        }
    }

加载布局,设置surface。避免画面拉升等问题

同时为几个按钮设置点击事件。

private void initView() {

        surfaceView = findViewById(R.id.surfaceView);

        surfaceHolder = surfaceView.getHolder();
        surfaceHolder.addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(@NonNull SurfaceHolder holder) {

                Log.d("预览尺寸", "surfaceCreated: " + surfaceView.getHeight() + ".." + surfaceView.getWidth());
                //打开相机
                openCamera();
                int height = surfaceView.getHeight();
                int width = surfaceView.getWidth();
                if (height > width) {
                    float justH = width * 4.f / 3;
                    //设置View在水平方向的缩放比例,保证宽高比为3:4
                    surfaceView.setScaleX(height / justH);
                } else {
                    float justW = height * 4.f / 3;
                    surfaceView.setScaleY(width / justW);
                }
                Log.d("变更后预览尺寸", "surfaceCreated: " + surfaceView.getHeight() + ".." + surfaceView.getWidth());
            }

            @Override
            public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {

            }
            @Override
            public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
                //关闭相机释放资源
                closeCamera();
            }
        });
        findViewById(R.id.btnTakePhoto).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //点击按钮拍照的事件
                takePhoto();
            }
        });
        findViewById(R.id.btnSwitch).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //点击按钮切换摄像头的事件
                switchCamera();
            }
        });
        findViewById(R.id.record).setOnClickListener(new View.OnClickListener() {

            @RequiresApi(api = Build.VERSION_CODES.R)
            @Override
            public void onClick(View v) {
                if (Environment.isExternalStorageManager()) {
                    Log.d("有权限", "onClick: ");
                }
               /* config();
                startRecorder();*/
                videoRecorderUtils.startRecord(MainActivity.this, handler);

            }
        });
        findViewById(R.id.stop).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                /* stopRecorder();*/
                videoRecorderUtils.stopRecord();
                startPreview();
            }
        });
        //获取摄像头管理,并开启子线程
        initCameraManager();
    }

这个方法内包含了所有的过程,拆分为不同的方法
1.打开相机,首先获取cameraManager 对象,并且创建子线程。然后使用CameraManager获取使用哪一个摄像头,和此摄像头的具体参数。比如可以输出的照片尺寸。完事后检查权限,调用CameraManager的openCamera方法打开相机。

private void initCameraManager() {
        cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);

        handlerThread = new HandlerThread("Camera2");
        handlerThread.start();
        handler = new Handler(handlerThread.getLooper());
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void openCamera() {
        try {
            //获取摄像头属性描述
            CameraCharacteristics cameraCharacteristics = cameraManager.getCameraCharacteristics(String.valueOf(currentCameraId));
            //获取摄像头支持的配置属性
            StreamConfigurationMap map = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
            Log.d("摄像头支持属性", "openCamera: " + map.toString());
            Integer level = cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
            Log.d("等级", "openCamera: "+level);
            //获取预览画面的输出尺寸,使用SurfaceView做预览
            previewSize = getMaxsize(map.getOutputSizes(SurfaceHolder.class));
            Log.d("输出预览尺寸", "openCamera: " + previewSize.getHeight() + ".." + previewSize.getWidth());

            initImageReader();
            //打开相机,先获取权限
            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                return;
            }
            //打开摄像头
            cameraManager.openCamera(String.valueOf(currentCameraId), stateCallback, handler);
        } catch (CameraAccessException exception) {
            exception.printStackTrace();
        }
    }

中间的initImageReader方法,是初始化,接受拍摄请求时图像的数据,在onImageAvailable中对接受的数据进行操作,比如保存等。

private void initImageReader() {
        imageReader = ImageReader.newInstance(1920, 1080, ImageFormat.JPEG, 2);
        imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable(ImageReader reader) {
                handler.post(new ImageSaver(reader.acquireNextImage()));
            }
        }, handler);
    }

在OpenCamera方法中有一个回调,

/**
     * 打开相机的回调,
     */
    private CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            //打开相机后开启预览
            cameraDevice = camera;
            //打开照相机时初始化,videoRecord
            videoRecorderUtils = new VideoRecorderUtils();
            videoRecorderUtils.create(surfaceView, cameraDevice, VideoRecorderUtils.WH_720X480);
            startPreview();
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            camera.close();
            cameraDevice = null;
        }

        @Override
        public void onError(@NonNull CameraDevice camera, int error) {
            camera.close();
            cameraDevice = null;
            finish();
        }
    };

此回调为打开相机的状态回调,在onOpen方法中可获取CameraDevice对象,即代表当前连接的设备。在此方法中可以开启预览。videoRecorderUtils是一个录像的工具类,这里将CameraDevice对象传过去,后续是设置录像的一些操作。在别的回调方法中执行错误处理等操作。
接下来开启预览

private void startPreview() {
        try {
            //构建预览请求
            previewBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            //设置预览输出的界面
            previewBuilder.addTarget(surfaceHolder.getSurface());
            //创建相机的会话Session
            cameraDevice.createCaptureSession(Arrays.asList(surfaceHolder.getSurface(), imageReader.getSurface()), sessionStateCallback, handler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }

    }

    /**
     * session的回调
     */
    private CameraCaptureSession.StateCallback sessionStateCallback = new CameraCaptureSession.StateCallback() {
        @Override
        public void onConfigured(@NonNull CameraCaptureSession session) {
            //会话已经建立,可以开始预览了
            cameraCaptureSession = session;
            //设置自动对焦
            previewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE);

            //发送预览请求
            try {
                cameraCaptureSession.setRepeatingRequest(previewBuilder.build(), null, handler);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onConfigureFailed(@NonNull CameraCaptureSession session) {
            //关闭会话
            session.close();
            cameraCaptureSession = null;
            cameraDevice.close();
            cameraDevice = null;
        }
    };

2.开启预览,首先通过CameraDevice对象获得一个CaptureRequest.Builder对象,方法createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW),CameraDevice.TEMPLATE_PREVIEW 表示构建的请求为预览请求,此外还有拍照请求。录像请求等。

参数

功能

TEMPLATE_PREVIEW

适用于配置预览的模板

TEMPLATE_RECORD

适用于视频录制的模板

TEMPLATE_STILL_CAPTURE

适用于拍照的模板

TEMPLATE_VIDEO_SNAPSHOT

适用于在录制视频过程中支持拍照的模板。

TEMPLATE_MANUAL

适用于希望自己手动配置大部分参数的模板。

之后需要设置预览数据的输出界面。然后通过CameraDevice创建createCaptureSession,传递三个参数,第一个为数据接受的surface列表,第二个为Session创建状态的回调。第三个参数用于指定在哪一个线程中执行。在session的回调中,onConfigured方法表示会话建立成功。可以获取到这个CaptureSession,然后通过其setRepeatingRequest方法发送一个预览的请求,三个参数,第一个是之前构建的CaptureRequest.Builder 对象.build(),第二个是回调,第三个还是用于指定在那个线程中执行。当然在建立失败方法中执行一些错误处理的方法。只此,预览流程已经完成。
接下里时拍照流程。
3.拍照流程。

/**
     * 拍照
     */
    private void takePhoto() {
        try {
            //创建拍照请求的Request
            captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
            //设置拍照的画面
            captureBuilder.addTarget(imageReader.getSurface());
            //自动对焦
            captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            //自动曝光
            captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
            //获取手机方向
            int rotation = getWindowManager().getDefaultDisplay().getRotation();
            //根据设备方向计算设置照片的方向
            captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(rotation));
            //停止预览
            cameraCaptureSession.stopRepeating();
            //拍照
            cameraCaptureSession.capture(captureBuilder.build(), captureCallback, handler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    private final CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() {
        @Override
        public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) {
            super.onCaptureProgressed(session, request, partialResult);
        }

        @Override
        public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
            super.onCaptureCompleted(session, request, result);
            try {
                //自动对焦
                captureBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
                //重新打开预览
                cameraCaptureSession.setRepeatingRequest(previewBuilder.build(), null, handler);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
            super.onCaptureFailed(session, request, failure);
            cameraCaptureSession.close();
            cameraCaptureSession = null;
            cameraDevice.close();
            cameraDevice = null;
        }
    };

首先,还是使用之前获得的CameraDevice对象 创建CaptureRequest.Builder,只不过,这里的参数为, cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE),是一个拍照请求。然后设置这个拍照请求,比如画面。对焦,曝光等。之后先关闭预览,然后通过
CameraCaptureSession的Capture方法发送一个拍照的请求。
cameraCaptureSession.capture(captureBuilder.build(), captureCallback, handler);有三个参数,第一个为之前构建的CaptureRequest.Builder对象.build,第二个同样是一个请求状态的回调,第三个参数为指定在哪一个线程执行。CameraCaptureSession.CaptureCallback在此状态回调中,拍照完成方法onCaptureCompleted()里,重新打开预览。那么我们的图像到哪里去了呢?
就是之前设置的 imageReader.setOnImageAvailableListener(),在里面的onImageAvailable可拿到拍照的数据。至此拍照流程完成。
3.录像流程,第一步还是CameraDevice对象 创建CaptureRequest.Builder,这里参数变为CameraDevice.TEMPLATE_RECORD,代表录像,之后初始化MediaRecorder对象。然后设置MediaRecorder对象的一系列属性,然后拿到MediaRecorder对象的surface,CaptureRequest.Builder添加两个surface,一个为MediaRecorder的,还有一个为surfaceView的surface,之后还是通过CameraDevice的createCaptureSession,创建会话,传递三个参数,在创建成功方法里,发送重复捕获请求,将这次创建的CaptureRequest.Builder对象传递进去。开始录像时调用MediaRecorder的start方法即可。 关闭调用mediaRecorder.release();

public class VideoRecorderUtils {
   private MediaRecorder mediaRecorder;
   private SurfaceHolder.Callback callback;
   private SurfaceView surfaceView;
   private CameraDevice mCameraDevice;
   private int height;
   private int width;
   List<Surface> surfaces = new ArrayList<>();
   public static Size WH_720X480 = new Size(720, 480);
   private CaptureRequest.Builder mPreviewBuilder;
   private CaptureRequest mCaptureRequest;
   private CameraCaptureSession mPreviewSession;
   public void create(SurfaceView surfaceView, CameraDevice cameraDevice, Size size) {
       this.surfaceView = surfaceView;
       mCameraDevice = cameraDevice;
       //创建录制的session会话中的请求
       try {
           mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
       } catch (CameraAccessException e) {
           e.printStackTrace();
       }
       height = size.getHeight();
       width = size.getWidth();
       mediaRecorder = new MediaRecorder();
   }
   public void stopRecord() {
       mediaRecorder.release();
       mediaRecorder = null;
       mediaRecorder = new MediaRecorder();
       surfaces.clear();
   }
   public void stop() {
       if (mediaRecorder != null) {
           mediaRecorder.release();
       }
   }
   public void destroy() {
       if (mediaRecorder != null) {
           mediaRecorder.release();
           mediaRecorder = null;
       }
   }
   /**
    * @param mainActivity
    */
   @RequiresApi(api = Build.VERSION_CODES.R)
   public void startRecord(MainActivity mainActivity, Handler handler) {

       mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
       mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
       mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);


       mediaRecorder.setVideoSize(width, height);
       mediaRecorder.setVideoFrameRate(24);
       mediaRecorder.setVideoEncodingBitRate(700 * 1024);
       mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
       mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
       mediaRecorder.setOrientationHint(90);


       SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
       String fname = "video_" + sdf.format(new Date()) + ".mp4";
       File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() + File.separator + fname);
       Log.d("xxxxxx", "startRecord: "+file.getAbsolutePath());
       mediaRecorder.setOutputFile(file);
       try {
           mediaRecorder.prepare();
           mediaRecorder.start();
       } catch (IOException e) {
           e.printStackTrace();
       }
       review(handler);
   }
   public void review(Handler handler) {
       Surface previewSurface = surfaceView.getHolder().getSurface();
       surfaces.add(previewSurface);
       mPreviewBuilder.addTarget(previewSurface);
       Surface recorderSurface = mediaRecorder.getSurface();
       surfaces.add(recorderSurface);
       mPreviewBuilder.addTarget(recorderSurface);
       try {
           mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
               @Override
               public void onConfigured(CameraCaptureSession session) {
                   try {
                       //创建捕获请求
                       mCaptureRequest = mPreviewBuilder.build();
                       mPreviewSession = session;
                       //设置反复捕获数据的请求,这样预览界面就会一直有数据显示
                       mPreviewSession.setRepeatingRequest(mCaptureRequest, null, handler);
                   } catch (Exception e) {
                       e.printStackTrace();
                   }
               }
               @Override
               public void onConfigureFailed(CameraCaptureSession session) {
               }
           }, handler);
       } catch (CameraAccessException e) {
           e.printStackTrace();
       }

   }
   //清除预览Session
   private void closePreviewSession() {
       if (mPreviewSession != null) {
           mPreviewSession.close();
           mPreviewSession = null;
       }
   }
}

前面漏了两个方法。

/**
    * 关闭相机
    */
   private void closeCamera() {
       //关闭捕捉会话
       if (cameraCaptureSession != null) {
           cameraCaptureSession.close();
           cameraCaptureSession = null;
       }
       //关闭相机
       if (cameraDevice != null) {
           cameraDevice.close();
           cameraDevice = null;
       }
       //关闭拍照处理器
       if (imageReader != null) {
           imageReader.close();
           imageReader = null;
       }
   }


   public class ImageSaver implements Runnable {

       private Image mImage;
       private File mFile;

       public ImageSaver(Image image) {
           this.mImage = image;
       }

       @RequiresApi(api = Build.VERSION_CODES.KITKAT)
       @Override
       public void run() {
           ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
           byte[] bytes = new byte[buffer.remaining()];
           buffer.get(bytes);
           FileOutputStream output = null;

           SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
           String fname = "IMG_" + sdf.format(new Date()) + ".jpg";
           mFile = new File(getApplication().getExternalFilesDir(null), fname);

           try {
               output = new FileOutputStream(mFile);
               output.write(bytes);
           } catch (IOException e) {
               e.printStackTrace();
           } finally {
               mImage.close();
               if (null != output) {
                   try {
                       output.close();
                   } catch (IOException e) {
                       e.printStackTrace();
                   }
               }
           }
           // 把file里面的图片插入到系统相册中
           try {
               MediaStore.Images.Media.insertImage(getApplication().getContentResolver(),
                       mFile.getAbsolutePath(), fname, null);
           } catch (FileNotFoundException e) {
               e.printStackTrace();
           }
           // 通知相册更新
           getApplication().sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mFile)));
       }
   }

首次打开可能会出现没有预览画面的情况,完全关闭后,在次打开就有图像显示了。
源码:https://github.com/dqx-eterning/CustomCamera CSDN资源里也有。javascript:void(0)
部分转载自javascript:void(0)