The author has obtained authorization, the writer: the original cain_huang link: https://www.jianshu.com/p/9e0f3fc5a3b4

Android uses Camera API + SurfaceView to preview photos.

SurfaceView creation and callback

Create a SurfaceView and implement the SurfaceHolder callback.

Since the Camera in the SurfaceView uses the SurfaceHolder to enable the SurfaceView to preview the data returned by the Camera, we need to implement the SurfaceHolder callback as follows:

public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback { private static final String TAG =  CameraSurfaceView.class.getSimpleName(); private SurfaceHolder mSurfaceHolder; public CameraSurfaceView(Context context) { super(context); init(); } public CameraSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public CameraSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr);  init(); } private void init() { mSurfaceHolder = getHolder(); mSurfaceHolder.addCallback(this); } @Override public void surfaceCreated(SurfaceHolder holder) { CameraUtils.openFrontalCamera(CameraUtils.DESIRED_PREVIEW_FPS); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { CameraUtils.startPreviewDisplay(holder); } @Override public void surfaceDestroyed(SurfaceHolder holder) { CameraUtils.releaseCamera(); }}Copy the code

Helper classes for Camera operations

The CameraUtils helper class is mainly used for Camera API operations, such as opening the Camera, starting the preview, stopping the preview, switching the Camera, setting the preview parameters, etc. The specific implementation is as follows:

public class CameraUtils {    // 相机默认宽高,相机的宽度和高度跟屏幕坐标不一样,手机屏幕的宽度和高度是反过来的。    public static final int DEFAULT_WIDTH = 1280;    public static final int DEFAULT_HEIGHT = 720;    public static final int DESIRED_PREVIEW_FPS = 30;    private static int mCameraID = Camera.CameraInfo.CAMERA_FACING_FRONT;    private static Camera mCamera;    private static int mCameraPreviewFps;    private static int mOrientation = 0;    /**     * 打开相机,默认打开前置相机     * @param expectFps     */    public static void openFrontalCamera(int expectFps) {        if (mCamera != null) {            throw new RuntimeException("camera already initialized!");        }        Camera.CameraInfo info = new Camera.CameraInfo();        int numCameras = Camera.getNumberOfCameras();        for (int i = 0; i < numCameras; i++) {            Camera.getCameraInfo(i, info);            if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {                mCamera = Camera.open(i);                mCameraID = info.facing;                break;            }        }        // 如果没有前置摄像头,则打开默认的后置摄像头        if (mCamera == null) {            mCamera = Camera.open();            mCameraID = Camera.CameraInfo.CAMERA_FACING_BACK;        }        // 没有摄像头时,抛出异常        if (mCamera == null) {            throw new RuntimeException("Unable to open camera");        }        Camera.Parameters parameters = mCamera.getParameters();        mCameraPreviewFps = CameraUtils.chooseFixedPreviewFps(parameters, expectFps * 1000);        parameters.setRecordingHint(true);        mCamera.setParameters(parameters);        setPreviewSize(mCamera, CameraUtils.DEFAULT_WIDTH, CameraUtils.DEFAULT_HEIGHT);        setPictureSize(mCamera, CameraUtils.DEFAULT_WIDTH, CameraUtils.DEFAULT_HEIGHT);        mCamera.setDisplayOrientation(mOrientation);    }    /**     * 根据ID打开相机     * @param cameraID     * @param expectFps     */    public static void openCamera(int cameraID, int expectFps) {        if (mCamera != null) {            throw new RuntimeException("camera already initialized!");        }        mCamera = Camera.open(cameraID);        if (mCamera == null) {            throw new RuntimeException("Unable to open camera");        }        mCameraID = cameraID;        Camera.Parameters parameters = mCamera.getParameters();        mCameraPreviewFps = CameraUtils.chooseFixedPreviewFps(parameters, expectFps * 1000);        parameters.setRecordingHint(true);        mCamera.setParameters(parameters);        setPreviewSize(mCamera, CameraUtils.DEFAULT_WIDTH, CameraUtils.DEFAULT_HEIGHT);        setPictureSize(mCamera, CameraUtils.DEFAULT_WIDTH, CameraUtils.DEFAULT_HEIGHT);        mCamera.setDisplayOrientation(mOrientation);    }    /**     * 开始预览     * @param holder     */    public static void startPreviewDisplay(SurfaceHolder holder) {        if (mCamera == null) {            throw new IllegalStateException("Camera must be set when start preview");        }        try {            mCamera.setPreviewDisplay(holder);            mCamera.startPreview();        } catch (IOException e) {            e.printStackTrace();        }    }    /**     * 切换相机     * @param cameraID     */    public static void switchCamera(int cameraID, SurfaceHolder holder) {        if (mCameraID == cameraID) {            return;        }        mCameraID = cameraID;        // 释放原来的相机        releaseCamera();        // 打开相机        openCamera(cameraID, CameraUtils.DESIRED_PREVIEW_FPS);        // 打开预览        startPreviewDisplay(holder);    }    /**     * 释放相机     */    public static void releaseCamera() {        if (mCamera != null) {            mCamera.stopPreview();            mCamera.release();            mCamera = null;        }    }    /**     * 开始预览     */    public static void startPreview() {        if (mCamera != null) {            mCamera.startPreview();        }    }    /**     * 停止预览     */    public static void stopPreview() {        if (mCamera != null) {            mCamera.stopPreview();        }    }    /**     * 拍照     */    public static void takePicture(Camera.ShutterCallback shutterCallback,                                   Camera.PictureCallback rawCallback,                                   Camera.PictureCallback pictureCallback) {        if (mCamera != null) {            mCamera.takePicture(shutterCallback, rawCallback, pictureCallback);        }    }    /**     * 设置预览大小     * @param camera     * @param expectWidth     * @param expectHeight     */    public static void setPreviewSize(Camera camera, int expectWidth, int expectHeight) {        Camera.Parameters parameters = camera.getParameters();        Camera.Size size = calculatePerfectSize(parameters.getSupportedPreviewSizes(),                expectWidth, expectHeight);        parameters.setPreviewSize(size.width, size.height);        camera.setParameters(parameters);    }    /**     * 获取预览大小     * @return     */    public static Camera.Size getPreviewSize() {        if (mCamera != null) {            return mCamera.getParameters().getPreviewSize();        }        return null;    }    /**     * 设置拍摄的照片大小     * @param camera     * @param expectWidth     * @param expectHeight     */    public static void setPictureSize(Camera camera, int expectWidth, int expectHeight) {        Camera.Parameters parameters = camera.getParameters();        Camera.Size size = calculatePerfectSize(parameters.getSupportedPictureSizes(),                expectWidth, expectHeight);        parameters.setPictureSize(size.width, size.height);        camera.setParameters(parameters);    }    /**     * 获取照片大小     * @return     */    public static Camera.Size getPictureSize() {        if (mCamera != null) {            return mCamera.getParameters().getPictureSize();        }        return null;    }    /**     * 计算最完美的Size     * @param sizes     * @param expectWidth     * @param expectHeight     * @return     */    public static Camera.Size calculatePerfectSize(List<Camera.Size> sizes, int expectWidth,                                                   int expectHeight) {        sortList(sizes); // 根据宽度进行排序        Camera.Size result = sizes.get(0);        boolean widthOrHeight = false; // 判断存在宽或高相等的Size        // 辗转计算宽高最接近的值        for (Camera.Size size: sizes) {            // 如果宽高相等,则直接返回            if (size.width == expectWidth && size.height == expectHeight) {                result = size;                break;            }            // 仅仅是宽度相等,计算高度最接近的size            if (size.width == expectWidth) {                widthOrHeight = true;                if (Math.abs(result.height - expectHeight)                        > Math.abs(size.height - expectHeight)) {                    result = size;                }            }            // 高度相等,则计算宽度最接近的Size            else if (size.height == expectHeight) {                widthOrHeight = true;                if (Math.abs(result.width - expectWidth)                        > Math.abs(size.width - expectWidth)) {                    result = size;                }            }            // 如果之前的查找不存在宽或高相等的情况,则计算宽度和高度都最接近的期望值的Size            else if (!widthOrHeight) {                if (Math.abs(result.width - expectWidth)                        > Math.abs(size.width - expectWidth)                        && Math.abs(result.height - expectHeight)                        > Math.abs(size.height - expectHeight)) {                    result = size;                }            }        }        return result;    }    /**     * 排序     * @param list     */    private static void sortList(List<Camera.Size> list) {        Collections.sort(list, new Comparator<Camera.Size>() {            @Override            public int compare(Camera.Size pre, Camera.Size after) {                if (pre.width > after.width) {                    return 1;                } else if (pre.width < after.width) {                    return -1;                }                return 0;            }        });    }    /**     * 选择合适的FPS     * @param parameters     * @param expectedThoudandFps 期望的FPS     * @return     */    public static int chooseFixedPreviewFps(Camera.Parameters parameters, int expectedThoudandFps) {        List<int[]> supportedFps = parameters.getSupportedPreviewFpsRange();        for (int[] entry : supportedFps) {            if (entry[0] == entry[1] && entry[0] == expectedThoudandFps) {                parameters.setPreviewFpsRange(entry[0], entry[1]);                return entry[0];            }        }        int[] temp = new int[2];        int guess;        parameters.getPreviewFpsRange(temp);        if (temp[0] == temp[1]) {            guess = temp[0];        } else {            guess = temp[1] / 2;        }        return guess;    }    /**     * 设置预览角度,setDisplayOrientation本身只能改变预览的角度     * previewFrameCallback以及拍摄出来的照片是不会发生改变的,拍摄出来的照片角度依旧不正常的     * 拍摄的照片需要自行处理     * 这里Nexus5X的相机简直没法吐槽,后置摄像头倒置了,切换摄像头之后就出现问题了。     * @param activity     */    public static int calculateCameraPreviewOrientation(Activity activity) {        Camera.CameraInfo info = new Camera.CameraInfo();        Camera.getCameraInfo(mCameraID, info);        int rotation = activity.getWindowManager().getDefaultDisplay()                .getRotation();        int degrees = 0;        switch (rotation) {            case Surface.ROTATION_0:                degrees = 0;                break;            case Surface.ROTATION_90:                degrees = 90;                break;            case Surface.ROTATION_180:                degrees = 180;                break;            case Surface.ROTATION_270:                degrees = 270;                break;        }        int result;        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {            result = (info.orientation + degrees) % 360;            result = (360 - result) % 360;        } else {            result = (info.orientation - degrees + 360) % 360;        }        mOrientation = result;        return result;    }    /**     * 获取当前的Camera ID     * @return     */    public static int getCameraID() {        return mCameraID;    }    /**     * 获取当前预览的角度     * @return     */    public static int getPreviewOrientation() {        return mOrientation;    }    /**     * 获取FPS(千秒值)     * @return     */    public static int getCameraPreviewThousandFps() {        return mCameraPreviewFps;    }}Copy the code

Permission to apply for

CameraSurfaceview: Android6.0 dynamic permission application: CameraSurfaceview: Android6.0 dynamic permission application:

public class CameraSurfaceViewActivity extends AppCompatActivity implements View.OnClickListener { private static final int REQUEST_CAMERA = 0x01; private CameraSurfaceView mCameraSurfaceView; private Button mBtnTake; private Button mBtnSwitch; private int mOrientation; // CameraSurfaceView container wrapper class private FrameLayout mAspectLayout; private boolean mCameraRequested; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); setContentView(R.layout.activity_camera_surface); / / Android 6.0 camera dynamic permission check the if (ContextCompat. CheckSelfPermission (this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { initView(); } else { ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE }, REQUEST_CAMERA); Private void initView() {mAspectLayout = (FrameLayout) findViewById(r.id.layout_aspect); mCameraSurfaceView = new CameraSurfaceView(this); mAspectLayout.addView(mCameraSurfaceView); mOrientation = CameraUtils.calculateCameraPreviewOrientation(CameraSurfaceViewActivity.this); mBtnTake = (Button) findViewById(R.id.btn_take); mBtnTake.setOnClickListener(this); mBtnSwitch = (Button) findViewById(R.id.btn_switch); mBtnSwitch.setOnClickListener(this); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); Switch (requestCode) {// Camera permission case REQUEST_CAMERA: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { mCameraRequested = true; initView(); } break; } } @Override protected void onResume() { super.onResume(); if (mCameraRequested) { CameraUtils.startPreview(); } } @Override protected void onPause() { super.onPause(); CameraUtils.stopPreview(); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_take: takePicture(); break; case R.id.btn_switch: switchCamera(); break; CameraUtils. TakePicture (new Camera.ShutterCallback() {@override public void onShutter() { } }, null, new Camera.PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { CameraUtils.startPreview(); Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); if (bitmap ! = null) { bitmap = ImageUtils.getRotatedBitmap(bitmap, mOrientation); String path = Environment.getExternalStorageDirectory() + "/DCIM/Camera/" + System.currentTimeMillis() + ".jpg"; try { FileOutputStream fout = new FileOutputStream(path); BufferedOutputStream bos = new BufferedOutputStream(fout); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos); bos.flush(); bos.close(); fout.close(); } catch (IOException e) { e.printStackTrace(); } } CameraUtils.startPreview(); }}); Private void switchCamera() {if (mCameraSurfaceView! = null) { CameraUtils.switchCamera(1 - CameraUtils.getCameraID(), mCameraSurfaceView.getHolder()); / / need to recalculate the rotation Angle camera after switch mOrientation = CameraUtils. CalculateCameraPreviewOrientation (CameraSurfaceViewActivity. This); }}}Copy the code

Since the camera and storage permissions are used, we need to register the camera and storage permissions in the MANIFEST. It should be explained here that the manifest uses use-permission only to declare which permissions need to be used. However, when using these two permissions in our actual project, you need to check whether the permissions have been authorized. If not, request authorization:

<! -- Storage permission --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <! - CAMERA permissions - > < USES - permission android: name = "android. Permission. CAMERA" / > < USES - the permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.FLASHLIGHT" />Copy the code

Image processing auxiliary classes

In addition, the ImageUtils class implementation is as follows:

Public class ImageUtils {/** * @param bitmap * @param rotation * @return */ public static bitmap getRotatedBitmap(Bitmap bitmap, int rotation) { Matrix matrix = new Matrix(); matrix.postRotate(rotation); return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false); } public static bitmap getFlipBitmap(bitmap bitmap) {Matrix Matrix = new Matrix(); matrix.setScale(-1, 1); matrix.postTranslate(bitmap.getWidth(), 0); return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false); }}Copy the code

The layout is as follows:

<? The XML version = "1.0" encoding = "utf-8"? ><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.cgfay.camerasample.CameraSurfaceViewActivity"> <FrameLayout android:id="@+id/layout_aspect" android:layout_width="match_parent" android:layout_height="wrap_content"> </FrameLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_gravity="bottom" android:gravity="center"> <Button android:id="@+id/btn_take" Android :layout_width="wrap_content" Android :layout_height="wrap_content" Android :text=" take a picture "/> <Button android:id="@+id/btn_switch" android:layout_width="wrap_content" android:layout_height="wrap_content" Android :text=" Switch camera "/> </LinearLayout></FrameLayout>Copy the code

At this point, the SurfaceView + Camera API preview photo function has been implemented.

note

The onPreviewFrame callback is executed in the same thread in which the Camera API opened the Camera.

Therefore, if you want to use preview data through an onPreviewFrame callback, you can do so asynchronously through a HandlerThread call to the Camera. Another problem, onPreviewFrame method do not perform too complex logic operations, this will block the Camera, can not get a new Frame, resulting in a lower Frame rate.


Follow wechat public account [paper talk], read more audio and video, Camera, OpenGL, NDK development related articles ~~~